1
0
Fork 0
mirror of https://github.com/sussy-code/smov.git synced 2024-12-20 14:37:43 +01:00
Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
mrjvs 2023-10-25 16:41:52 +02:00
parent 51b7305799
commit e267482d33
12 changed files with 257 additions and 25 deletions

View file

@ -1,4 +1,5 @@
import { gql, request } from "graphql-request"; import { gql, request } from "graphql-request";
import { list } from "subsrt-ts";
import { unzip } from "unzipit"; import { unzip } from "unzipit";
import { proxiedFetch } from "@/backend/helpers/fetch"; import { proxiedFetch } from "@/backend/helpers/fetch";
@ -115,3 +116,5 @@ export async function downloadSrt(legacySubId: string): Promise<string> {
const srtData = srtEntry.text(); const srtData = srtEntry.text();
return srtData; return srtData;
} }
export const subtitleTypeList = list().map((type) => `.${type}`);

File diff suppressed because one or more lines are too long

View file

@ -12,6 +12,8 @@ interface Props {
padding?: string; padding?: string;
className?: string; className?: string;
href?: string; href?: string;
disabled?: boolean;
download?: boolean;
} }
export function Button(props: Props) { export function Button(props: Props) {
@ -25,13 +27,23 @@ export function Button(props: Props) {
colorClasses = colorClasses =
"bg-video-buttons-cancel hover:bg-video-buttons-cancelHover transition-colors duration-100 text-white"; "bg-video-buttons-cancel hover:bg-video-buttons-cancelHover transition-colors duration-100 text-white";
const classes = classNames( let classes = classNames(
"cursor-pointer inline-flex items-center justify-center rounded-lg font-medium transition-[transform,background-color] duration-100 active:scale-105 md:px-8", "cursor-pointer inline-flex items-center justify-center rounded-lg font-medium transition-[transform,background-color] duration-100 active:scale-105 md:px-8",
props.padding ?? "px-4 py-3", props.padding ?? "px-4 py-3",
props.className, props.className,
colorClasses colorClasses,
props.disabled ? "cursor-not-allowed bg-opacity-60 text-opacity-60" : null
); );
if (props.disabled)
classes = classes
.split(" ")
.filter(
(className) =>
!className.startsWith("hover:") && !className.startsWith("active:")
)
.join(" ");
const content = ( const content = (
<> <>
{props.icon ? ( {props.icon ? (
@ -47,9 +59,18 @@ export function Button(props: Props) {
history.push(href); history.push(href);
} }
if (props.href && props.href.startsWith("https://")) if (
props.href &&
(props.href.startsWith("https://") || props.href?.startsWith("data:"))
)
return ( return (
<a className={classes} href={props.href} target="_blank" rel="noreferrer"> <a
className={classes}
href={props.href}
target="_blank"
rel="noreferrer"
download={props.download}
>
{content} {content}
</a> </a>
); );

View file

@ -0,0 +1,9 @@
import { ReactNode } from "react";
export function Box(props: { children?: ReactNode }) {
return (
<div className="bg-video-scraping-card rounded-xl p-8">
{props.children}
</div>
);
}

View file

@ -270,6 +270,7 @@ export function CaptionSettingsView({ id }: { id: string }) {
onClick={() => updateStyling({ color: v })} onClick={() => updateStyling({ color: v })}
color={v} color={v}
active={styling.color === v} active={styling.color === v}
key={v}
/> />
))} ))}
</div> </div>

View file

@ -1,8 +1,13 @@
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import { ReactNode, useState } from "react"; import { ReactNode, useRef, useState } from "react";
import { useAsync, useAsyncFn } from "react-use"; import { useAsync, useAsyncFn } from "react-use";
import { convert } from "subsrt-ts";
import { SubtitleSearchItem, languageIdToName } from "@/backend/helpers/subs"; import {
SubtitleSearchItem,
languageIdToName,
subtitleTypeList,
} from "@/backend/helpers/subs";
import { FlagIcon } from "@/components/FlagIcon"; import { FlagIcon } from "@/components/FlagIcon";
import { useCaptions } from "@/components/player/hooks/useCaptions"; import { useCaptions } from "@/components/player/hooks/useCaptions";
import { Menu } from "@/components/player/internals/ContextMenu"; import { Menu } from "@/components/player/internals/ContextMenu";
@ -86,6 +91,41 @@ function searchSubs(
return results; return results;
} }
function CustomCaptionOption() {
const lang = usePlayerStore((s) => s.caption.selected?.language);
const setCaption = usePlayerStore((s) => s.setCaption);
const fileInput = useRef<HTMLInputElement>(null);
return (
<CaptionOption
selected={lang === "custom"}
onClick={() => fileInput.current?.click()}
>
Upload captions
<input
className="hidden"
ref={fileInput}
accept={subtitleTypeList.join(",")}
type="file"
onChange={(e) => {
if (!e.target.files) return;
const reader = new FileReader();
reader.addEventListener("load", (event) => {
if (!event.target || typeof event.target.result !== "string")
return;
const converted = convert(event.target.result, "srt");
setCaption({
language: "custom",
srtData: converted,
});
});
reader.readAsText(e.target.files[0], "utf-8");
}}
/>
</CaptionOption>
);
}
// TODO on initialize, download captions // TODO on initialize, download captions
// TODO fix language names, some are unknown // TODO fix language names, some are unknown
// TODO delay setting for captions // TODO delay setting for captions
@ -177,6 +217,7 @@ export function CaptionsView({ id }: { id: string }) {
<CaptionOption onClick={() => disable()} selected={!lang}> <CaptionOption onClick={() => disable()} selected={!lang}>
Off Off
</CaptionOption> </CaptionOption>
<CustomCaptionOption />
{content} {content}
</Menu.ScrollToActiveSection> </Menu.ScrollToActiveSection>
</> </>

View file

@ -4,6 +4,7 @@ import { Button } from "@/components/Button";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
import { OverlayPage } from "@/components/overlays/OverlayPage"; import { OverlayPage } from "@/components/overlays/OverlayPage";
import { Menu } from "@/components/player/internals/ContextMenu"; import { Menu } from "@/components/player/internals/ContextMenu";
import { convertSubtitlesToDataurl } from "@/components/player/utils/captions";
import { useOverlayRouter } from "@/hooks/useOverlayRouter"; import { useOverlayRouter } from "@/hooks/useOverlayRouter";
import { usePlayerStore } from "@/stores/player/store"; import { usePlayerStore } from "@/stores/player/store";
@ -22,6 +23,13 @@ export function DownloadView({ id }: { id: string }) {
const router = useOverlayRouter(id); const router = useOverlayRouter(id);
const downloadUrl = useDownloadLink(); const downloadUrl = useDownloadLink();
const selectedCaption = usePlayerStore((s) => s.caption?.selected);
const subtitleUrl = selectedCaption
? convertSubtitlesToDataurl(selectedCaption?.srtData)
: null;
console.log(subtitleUrl);
if (!downloadUrl) return null; if (!downloadUrl) return null;
return ( return (
@ -30,7 +38,7 @@ export function DownloadView({ id }: { id: string }) {
Download Download
</Menu.BackLink> </Menu.BackLink>
<Menu.Section> <Menu.Section>
<div className="mt-3"> <div>
<Menu.ChevronLink onClick={() => router.navigate("/download/pc")}> <Menu.ChevronLink onClick={() => router.navigate("/download/pc")}>
Downloading on PC Downloading on PC
</Menu.ChevronLink> </Menu.ChevronLink>
@ -45,13 +53,22 @@ export function DownloadView({ id }: { id: string }) {
<Menu.Divider /> <Menu.Divider />
<Menu.Paragraph> <Menu.Paragraph marginClass="my-6">
Downloads are taken directly from the provider. movie-web does not Downloads are taken directly from the provider. movie-web does not
have control over how the downloads are provided. have control over how the downloads are provided.
</Menu.Paragraph> </Menu.Paragraph>
<Button className="w-full" href={downloadUrl} theme="purple"> <Button className="w-full" href={downloadUrl} theme="purple">
Download Download video
</Button>
<Button
className="w-full mt-2"
href={subtitleUrl ?? undefined}
disabled={!subtitleUrl}
theme="secondary"
download
>
Download current caption
</Button> </Button>
</div> </div>
</Menu.Section> </Menu.Section>
@ -148,7 +165,7 @@ function IOSExplanationView({ id }: { id: string }) {
export function DownloadRoutes({ id }: { id: string }) { export function DownloadRoutes({ id }: { id: string }) {
return ( return (
<> <>
<OverlayPage id={id} path="/download" width={343} height={440}> <OverlayPage id={id} path="/download" width={343} height={490}>
<Menu.CardWithScrollable> <Menu.CardWithScrollable>
<DownloadView id={id} /> <DownloadView id={id} />
</Menu.CardWithScrollable> </Menu.CardWithScrollable>

View file

@ -49,8 +49,11 @@ export function FieldTitle(props: { children: React.ReactNode }) {
return <p className="font-medium">{props.children}</p>; return <p className="font-medium">{props.children}</p>;
} }
export function Paragraph(props: { children: React.ReactNode }) { export function Paragraph(props: {
return <p className="my-3">{props.children}</p>; children: React.ReactNode;
marginClass?: string;
}) {
return <p className={props.marginClass ?? "my-3"}>{props.children}</p>;
} }
export function Highlight(props: { children: React.ReactNode }) { export function Highlight(props: { children: React.ReactNode }) {

View file

@ -1,3 +1,17 @@
export function Paragraph(props: { children: React.ReactNode }) { import classNames from "classnames";
return <p className="text-errors-type-secondary mt-6">{props.children}</p>;
export function Paragraph(props: {
children: React.ReactNode;
marginClass?: string;
}) {
return (
<p
className={classNames(
"text-errors-type-secondary",
props.marginClass ?? "mt-6"
)}
>
{props.children}
</p>
);
} }

View file

@ -0,0 +1,17 @@
import { ThinContainer } from "@/components/layout/ThinContainer";
import { Heading1, Paragraph } from "@/components/utils/Text";
import { SubPageLayout } from "@/pages/layouts/SubPageLayout";
import { WorkerTestPart } from "@/pages/parts/admin/WorkerTestPart";
export function AdminPage() {
return (
<SubPageLayout>
<ThinContainer>
<Heading1>Admin tools</Heading1>
<Paragraph>Useful tools to test out your current deployment</Paragraph>
<WorkerTestPart />
</ThinContainer>
</SubPageLayout>
);
}

View file

@ -0,0 +1,113 @@
import classNames from "classnames";
import { f } from "ofetch/dist/shared/ofetch.441891d5";
import { useCallback, useMemo, useState } from "react";
import { useAsyncFn } from "react-use";
import { mwFetch } from "@/backend/helpers/fetch";
import { Button } from "@/components/Button";
import { Icon, Icons } from "@/components/Icon";
import { Box } from "@/components/layout/Box";
import { Divider } from "@/components/player/internals/ContextMenu/Misc";
import { Heading2 } from "@/components/utils/Text";
import { conf } from "@/setup/config";
export function WorkerItem(props: {
name: string;
errored?: boolean;
success?: boolean;
errorText?: string;
}) {
return (
<div className="flex mb-2">
<Icon
icon={
props.errored
? Icons.WARNING
: props.success
? Icons.CIRCLE_CHECK
: Icons.EYE_SLASH
}
className={classNames({
"text-xl mr-2 mt-0.5": true,
"text-video-scraping-error": props.errored,
"text-video-scraping-noresult": !props.errored && !props.success,
"text-video-scraping-success": props.success,
})}
/>
<div className="flex-1">
<p className="text-white font-bold">{props.name}</p>
{props.errorText ? <p>{props.errorText}</p> : null}
</div>
</div>
);
}
export function WorkerTestPart() {
const workerList = useMemo(() => {
return conf().PROXY_URLS.map((v, ind) => ({
id: ind.toString(),
url: v,
}));
}, []);
const [workerState, setWorkerState] = useState<
{ id: string; status: "error" | "success"; error?: Error }[]
>([]);
const runTests = useAsyncFn(async () => {
function updateWorker(id: string, data: (typeof workerState)[number]) {
setWorkerState((s) => {
return [...s.filter((v) => v.id !== id), data];
});
}
setWorkerState([]);
for (const worker of workerList) {
try {
await mwFetch(worker.url, {
query: {
destination: "https://postman-echo.com/get",
},
});
updateWorker(worker.id, {
id: worker.id,
status: "success",
});
} catch (err) {
updateWorker(worker.id, {
id: worker.id,
status: "error",
error: err as Error,
});
}
}
}, [workerList, setWorkerState]);
return (
<>
<Heading2 className="mb-0 mt-12">Worker tests</Heading2>
<p className="mb-8 mt-2">15 workers registered</p>
<Box>
{workerList.map((v, i) => {
const s = workerState.find((segment) => segment.id);
const name = `Worker ${i + 1}`;
if (!s) return <WorkerItem name={name} key={v.id} />;
if (s.status === "error")
return (
<WorkerItem
name={name}
errored
key={v.id}
errorText={s.error?.toString()}
/>
);
if (s.status === "success")
return <WorkerItem name={name} success key={v.id} />;
return <WorkerItem name={name} key={v.id} />;
})}
<Divider />
<div className="flex justify-end">
<Button theme="purple">Run tests</Button>
</div>
</Box>
</>
);
}

View file

@ -12,6 +12,7 @@ import { convertLegacyUrl, isLegacyUrl } from "@/backend/metadata/getmeta";
import { generateQuickSearchMediaUrl } from "@/backend/metadata/tmdb"; import { generateQuickSearchMediaUrl } from "@/backend/metadata/tmdb";
import { useOnlineListener } from "@/hooks/usePing"; import { useOnlineListener } from "@/hooks/usePing";
import { AboutPage } from "@/pages/About"; import { AboutPage } from "@/pages/About";
import { AdminPage } from "@/pages/admin/AdminPage";
import { DmcaPage } from "@/pages/Dmca"; import { DmcaPage } from "@/pages/Dmca";
import { NotFoundPage } from "@/pages/errors/NotFoundPage"; import { NotFoundPage } from "@/pages/errors/NotFoundPage";
import { HomePage } from "@/pages/HomePage"; import { HomePage } from "@/pages/HomePage";
@ -102,6 +103,9 @@ function App() {
<Route exact path="/faq" component={AboutPage} /> <Route exact path="/faq" component={AboutPage} />
<Route exact path="/dmca" component={DmcaPage} /> <Route exact path="/dmca" component={DmcaPage} />
{/* admin routes */}
<Route exact path="/admin" component={AdminPage} />
{/* other */} {/* other */}
<Route <Route
exact exact