mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-27 22:21:50 +01:00
feature: subtitle uploading
This commit is contained in:
parent
f76db3e4b7
commit
404cd897f3
@ -2,6 +2,7 @@ import { mwFetch, proxiedFetch } from "@/backend/helpers/fetch";
|
||||
import { MWCaption, MWCaptionType } from "@/backend/helpers/streams";
|
||||
import toWebVTT from "srt-webvtt";
|
||||
|
||||
export const CUSTOM_CAPTION_ID = "customCaption";
|
||||
export async function getCaptionUrl(caption: MWCaption): Promise<string> {
|
||||
if (caption.type === MWCaptionType.SRT) {
|
||||
let captionBlob: Blob;
|
||||
@ -32,3 +33,18 @@ export async function getCaptionUrl(caption: MWCaption): Promise<string> {
|
||||
|
||||
throw new Error("invalid type");
|
||||
}
|
||||
|
||||
export async function convertCustomCaptionFileToWebVTT(file: File) {
|
||||
const header = await file.slice(0, 6).text();
|
||||
const isWebVTT = header === "WEBVTT";
|
||||
if (!isWebVTT) {
|
||||
return toWebVTT(file);
|
||||
}
|
||||
return URL.createObjectURL(file);
|
||||
}
|
||||
|
||||
export function revokeCaptionBlob(url: string | undefined) {
|
||||
if (url && url.startsWith("blob:")) {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
@ -70,6 +70,8 @@
|
||||
"episode": "E{{index}} - {{title}}",
|
||||
"noCaptions": "No captions",
|
||||
"linkedCaptions": "Linked captions",
|
||||
"customCaption": "Custom caption",
|
||||
"uploadCustomCaption": "Upload caption (SRT, VTT)",
|
||||
"noEmbeds": "No embeds were found for this source",
|
||||
"errors": {
|
||||
"loadingWentWong": "Something went wrong loading the episodes for {{seasonTitle}}",
|
||||
|
@ -1,4 +1,8 @@
|
||||
import { getCaptionUrl } from "@/backend/helpers/captions";
|
||||
import {
|
||||
getCaptionUrl,
|
||||
convertCustomCaptionFileToWebVTT,
|
||||
CUSTOM_CAPTION_ID,
|
||||
} from "@/backend/helpers/captions";
|
||||
import { MWCaption } from "@/backend/helpers/streams";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { useLoading } from "@/hooks/useLoading";
|
||||
@ -6,7 +10,7 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||
import { useControls } from "@/video/state/logic/controls";
|
||||
import { useMeta } from "@/video/state/logic/meta";
|
||||
import { useSource } from "@/video/state/logic/source";
|
||||
import { useMemo, useRef } from "react";
|
||||
import { ChangeEvent, useMemo, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
|
||||
|
||||
@ -37,6 +41,29 @@ export function CaptionSelectionPopout() {
|
||||
);
|
||||
|
||||
const currentCaption = source.source?.caption?.id;
|
||||
const customCaptionUploadElement = useRef<HTMLInputElement>(null);
|
||||
const [setCustomCaption, loadingCustomCaption, errorCustomCaption] =
|
||||
useLoading(async (captionFile: File) => {
|
||||
if (
|
||||
!captionFile.name.endsWith(".srt") &&
|
||||
!captionFile.name.endsWith(".vtt")
|
||||
) {
|
||||
throw new Error("Only SRT or VTT files are allowed");
|
||||
}
|
||||
controls.setCaption(
|
||||
CUSTOM_CAPTION_ID,
|
||||
await convertCustomCaptionFileToWebVTT(captionFile)
|
||||
);
|
||||
controls.closePopout();
|
||||
});
|
||||
|
||||
async function handleUploadCaption(e: ChangeEvent<HTMLInputElement>) {
|
||||
if (!e.target.files) {
|
||||
return;
|
||||
}
|
||||
const captionFile = e.target.files[0];
|
||||
setCustomCaption(captionFile);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -54,6 +81,26 @@ export function CaptionSelectionPopout() {
|
||||
>
|
||||
{t("videoPlayer.popouts.noCaptions")}
|
||||
</PopoutListEntry>
|
||||
<PopoutListEntry
|
||||
key={CUSTOM_CAPTION_ID}
|
||||
active={currentCaption === CUSTOM_CAPTION_ID}
|
||||
loading={loadingCustomCaption}
|
||||
errored={!!errorCustomCaption}
|
||||
onClick={() => {
|
||||
customCaptionUploadElement.current?.click();
|
||||
}}
|
||||
>
|
||||
{currentCaption === CUSTOM_CAPTION_ID
|
||||
? t("videoPlayer.popouts.customCaption")
|
||||
: t("videoPlayer.popouts.uploadCustomCaption")}
|
||||
<input
|
||||
ref={customCaptionUploadElement}
|
||||
type="file"
|
||||
onChange={handleUploadCaption}
|
||||
className="hidden"
|
||||
accept=".vtt, .srt"
|
||||
/>
|
||||
</PopoutListEntry>
|
||||
</PopoutSection>
|
||||
|
||||
<p className="sticky top-0 z-10 flex items-center space-x-1 bg-ash-200 px-5 py-3 text-sm font-bold uppercase">
|
||||
|
@ -96,7 +96,7 @@ export function PopoutListEntry(props: PopoutListEntryTypes) {
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
"group -mx-2 flex cursor-pointer items-center justify-between space-x-1 rounded p-2 font-semibold transition-[background-color,color] duration-150",
|
||||
"group my-2 -mx-2 flex cursor-pointer items-center justify-between space-x-1 rounded p-2 font-semibold transition-[background-color,color] duration-150",
|
||||
hover,
|
||||
props.active
|
||||
? `${bg} active text-white outline-denim-700`
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
} from "@/video/components/hooks/volumeStore";
|
||||
import { resetStateForSource } from "@/video/state/providers/helpers";
|
||||
import { updateInterface } from "@/video/state/logic/interface";
|
||||
import { revokeCaptionBlob } from "@/backend/helpers/captions";
|
||||
import { getPlayerState } from "../cache";
|
||||
import { updateMediaPlaying } from "../logic/mediaplaying";
|
||||
import { VideoPlayerStateProvider } from "./providerTypes";
|
||||
@ -135,6 +136,7 @@ export function createCastingStateProvider(
|
||||
},
|
||||
setCaption(id, url) {
|
||||
if (state.source) {
|
||||
revokeCaptionBlob(state.source.caption?.url);
|
||||
state.source.caption = {
|
||||
id,
|
||||
url,
|
||||
@ -144,6 +146,7 @@ export function createCastingStateProvider(
|
||||
},
|
||||
clearCaption() {
|
||||
if (state.source) {
|
||||
revokeCaptionBlob(state.source.caption?.url);
|
||||
state.source.caption = null;
|
||||
updateSource(descriptor, state);
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
import { updateError } from "@/video/state/logic/error";
|
||||
import { updateMisc } from "@/video/state/logic/misc";
|
||||
import { resetStateForSource } from "@/video/state/providers/helpers";
|
||||
import { revokeCaptionBlob } from "@/backend/helpers/captions";
|
||||
import { getPlayerState } from "../cache";
|
||||
import { updateMediaPlaying } from "../logic/mediaplaying";
|
||||
import { VideoPlayerStateProvider } from "./providerTypes";
|
||||
@ -191,6 +192,7 @@ export function createVideoStateProvider(
|
||||
},
|
||||
setCaption(id, url) {
|
||||
if (state.source) {
|
||||
revokeCaptionBlob(state.source.caption?.url);
|
||||
state.source.caption = {
|
||||
id,
|
||||
url,
|
||||
@ -200,6 +202,7 @@ export function createVideoStateProvider(
|
||||
},
|
||||
clearCaption() {
|
||||
if (state.source) {
|
||||
revokeCaptionBlob(state.source.caption?.url);
|
||||
state.source.caption = null;
|
||||
updateSource(descriptor, state);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user