mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-20 14:37:43 +01:00
opush
Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
parent
51b7305799
commit
e267482d33
12 changed files with 257 additions and 25 deletions
|
@ -1,4 +1,5 @@
|
|||
import { gql, request } from "graphql-request";
|
||||
import { list } from "subsrt-ts";
|
||||
import { unzip } from "unzipit";
|
||||
|
||||
import { proxiedFetch } from "@/backend/helpers/fetch";
|
||||
|
@ -115,3 +116,5 @@ export async function downloadSrt(legacySubId: string): Promise<string> {
|
|||
const srtData = srtEntry.text();
|
||||
return srtData;
|
||||
}
|
||||
|
||||
export const subtitleTypeList = list().map((type) => `.${type}`);
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -12,6 +12,8 @@ interface Props {
|
|||
padding?: string;
|
||||
className?: string;
|
||||
href?: string;
|
||||
disabled?: boolean;
|
||||
download?: boolean;
|
||||
}
|
||||
|
||||
export function Button(props: Props) {
|
||||
|
@ -25,13 +27,23 @@ export function Button(props: Props) {
|
|||
colorClasses =
|
||||
"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",
|
||||
props.padding ?? "px-4 py-3",
|
||||
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 = (
|
||||
<>
|
||||
{props.icon ? (
|
||||
|
@ -47,9 +59,18 @@ export function Button(props: Props) {
|
|||
history.push(href);
|
||||
}
|
||||
|
||||
if (props.href && props.href.startsWith("https://"))
|
||||
if (
|
||||
props.href &&
|
||||
(props.href.startsWith("https://") || props.href?.startsWith("data:"))
|
||||
)
|
||||
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}
|
||||
</a>
|
||||
);
|
||||
|
|
9
src/components/layout/Box.tsx
Normal file
9
src/components/layout/Box.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -270,6 +270,7 @@ export function CaptionSettingsView({ id }: { id: string }) {
|
|||
onClick={() => updateStyling({ color: v })}
|
||||
color={v}
|
||||
active={styling.color === v}
|
||||
key={v}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import Fuse from "fuse.js";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { ReactNode, useRef, useState } from "react";
|
||||
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 { useCaptions } from "@/components/player/hooks/useCaptions";
|
||||
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||
|
@ -86,6 +91,41 @@ function searchSubs(
|
|||
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 fix language names, some are unknown
|
||||
// TODO delay setting for captions
|
||||
|
@ -177,6 +217,7 @@ export function CaptionsView({ id }: { id: string }) {
|
|||
<CaptionOption onClick={() => disable()} selected={!lang}>
|
||||
Off
|
||||
</CaptionOption>
|
||||
<CustomCaptionOption />
|
||||
{content}
|
||||
</Menu.ScrollToActiveSection>
|
||||
</>
|
||||
|
|
|
@ -4,6 +4,7 @@ import { Button } from "@/components/Button";
|
|||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { OverlayPage } from "@/components/overlays/OverlayPage";
|
||||
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||
import { convertSubtitlesToDataurl } from "@/components/player/utils/captions";
|
||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
|
||||
|
@ -22,6 +23,13 @@ export function DownloadView({ id }: { id: string }) {
|
|||
const router = useOverlayRouter(id);
|
||||
const downloadUrl = useDownloadLink();
|
||||
|
||||
const selectedCaption = usePlayerStore((s) => s.caption?.selected);
|
||||
const subtitleUrl = selectedCaption
|
||||
? convertSubtitlesToDataurl(selectedCaption?.srtData)
|
||||
: null;
|
||||
|
||||
console.log(subtitleUrl);
|
||||
|
||||
if (!downloadUrl) return null;
|
||||
|
||||
return (
|
||||
|
@ -30,7 +38,7 @@ export function DownloadView({ id }: { id: string }) {
|
|||
Download
|
||||
</Menu.BackLink>
|
||||
<Menu.Section>
|
||||
<div className="mt-3">
|
||||
<div>
|
||||
<Menu.ChevronLink onClick={() => router.navigate("/download/pc")}>
|
||||
Downloading on PC
|
||||
</Menu.ChevronLink>
|
||||
|
@ -45,13 +53,22 @@ export function DownloadView({ id }: { id: string }) {
|
|||
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Paragraph>
|
||||
<Menu.Paragraph marginClass="my-6">
|
||||
Downloads are taken directly from the provider. movie-web does not
|
||||
have control over how the downloads are provided.
|
||||
</Menu.Paragraph>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</Menu.Section>
|
||||
|
@ -148,7 +165,7 @@ function IOSExplanationView({ id }: { id: string }) {
|
|||
export function DownloadRoutes({ id }: { id: string }) {
|
||||
return (
|
||||
<>
|
||||
<OverlayPage id={id} path="/download" width={343} height={440}>
|
||||
<OverlayPage id={id} path="/download" width={343} height={490}>
|
||||
<Menu.CardWithScrollable>
|
||||
<DownloadView id={id} />
|
||||
</Menu.CardWithScrollable>
|
||||
|
|
|
@ -49,8 +49,11 @@ export function FieldTitle(props: { children: React.ReactNode }) {
|
|||
return <p className="font-medium">{props.children}</p>;
|
||||
}
|
||||
|
||||
export function Paragraph(props: { children: React.ReactNode }) {
|
||||
return <p className="my-3">{props.children}</p>;
|
||||
export function Paragraph(props: {
|
||||
children: React.ReactNode;
|
||||
marginClass?: string;
|
||||
}) {
|
||||
return <p className={props.marginClass ?? "my-3"}>{props.children}</p>;
|
||||
}
|
||||
|
||||
export function Highlight(props: { children: React.ReactNode }) {
|
||||
|
|
|
@ -1,3 +1,17 @@
|
|||
export function Paragraph(props: { children: React.ReactNode }) {
|
||||
return <p className="text-errors-type-secondary mt-6">{props.children}</p>;
|
||||
import classNames from "classnames";
|
||||
|
||||
export function Paragraph(props: {
|
||||
children: React.ReactNode;
|
||||
marginClass?: string;
|
||||
}) {
|
||||
return (
|
||||
<p
|
||||
className={classNames(
|
||||
"text-errors-type-secondary",
|
||||
props.marginClass ?? "mt-6"
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
|
17
src/pages/admin/AdminPage.tsx
Normal file
17
src/pages/admin/AdminPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
113
src/pages/parts/admin/WorkerTestPart.tsx
Normal file
113
src/pages/parts/admin/WorkerTestPart.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -12,6 +12,7 @@ import { convertLegacyUrl, isLegacyUrl } from "@/backend/metadata/getmeta";
|
|||
import { generateQuickSearchMediaUrl } from "@/backend/metadata/tmdb";
|
||||
import { useOnlineListener } from "@/hooks/usePing";
|
||||
import { AboutPage } from "@/pages/About";
|
||||
import { AdminPage } from "@/pages/admin/AdminPage";
|
||||
import { DmcaPage } from "@/pages/Dmca";
|
||||
import { NotFoundPage } from "@/pages/errors/NotFoundPage";
|
||||
import { HomePage } from "@/pages/HomePage";
|
||||
|
@ -102,6 +103,9 @@ function App() {
|
|||
<Route exact path="/faq" component={AboutPage} />
|
||||
<Route exact path="/dmca" component={DmcaPage} />
|
||||
|
||||
{/* admin routes */}
|
||||
<Route exact path="/admin" component={AdminPage} />
|
||||
|
||||
{/* other */}
|
||||
<Route
|
||||
exact
|
||||
|
|
Loading…
Reference in a new issue