mirror of
https://github.com/p-stream/p-stream.git
synced 2026-03-11 17:55:33 +00:00
384 lines
12 KiB
TypeScript
384 lines
12 KiB
TypeScript
import { useCallback, useMemo } from "react";
|
|
import { Trans, useTranslation } from "react-i18next";
|
|
import { useCopyToClipboard } from "react-use";
|
|
|
|
import { downloadCaption } from "@/backend/helpers/subs";
|
|
import { Button } from "@/components/buttons/Button";
|
|
import { Icon, Icons } from "@/components/Icon";
|
|
import { OverlayPage } from "@/components/overlays/OverlayPage";
|
|
import { Menu } from "@/components/player/internals/ContextMenu";
|
|
import { convertSubtitlesToSrtDataurl } from "@/components/player/utils/captions";
|
|
import { useIsDesktopApp } from "@/hooks/useIsDesktopApp";
|
|
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
|
import { usePlayerStore } from "@/stores/player/store";
|
|
|
|
export function useDownloadLink() {
|
|
const source = usePlayerStore((s) => s.source);
|
|
const currentQuality = usePlayerStore((s) => s.currentQuality);
|
|
const url = useMemo(() => {
|
|
if (source?.type === "file") {
|
|
const quality = currentQuality
|
|
? source.qualities[currentQuality]
|
|
: undefined;
|
|
if (quality) return quality.url;
|
|
// Fallback to the first available quality if currentQuality is not set
|
|
const firstQuality = Object.values(source.qualities)[0];
|
|
return firstQuality?.url;
|
|
}
|
|
if (source?.type === "hls") return source.url;
|
|
return undefined;
|
|
}, [source, currentQuality]);
|
|
return url;
|
|
}
|
|
|
|
function StyleTrans(props: { k: string }) {
|
|
return (
|
|
<Trans
|
|
i18nKey={props.k}
|
|
components={{
|
|
bold: <Menu.Highlight />,
|
|
br: <br />,
|
|
ios_share: (
|
|
<Icon icon={Icons.IOS_SHARE} className="inline-block text-xl -mb-1" />
|
|
),
|
|
ios_files: (
|
|
<Icon icon={Icons.IOS_FILES} className="inline-block text-xl -mb-1" />
|
|
),
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function DownloadView({ id }: { id: string }) {
|
|
const router = useOverlayRouter(id);
|
|
const { t } = useTranslation();
|
|
const downloadUrl = useDownloadLink();
|
|
|
|
// Custom function to process the download URL
|
|
const processDownloadUrl = useCallback(() => {
|
|
if (!downloadUrl) return "";
|
|
|
|
// Check if the URL contains the m3u8-proxy and the ?url= parameter
|
|
const match = downloadUrl.match(/m3u8-proxy\?url=(.*)$/);
|
|
if (match && match[1]) {
|
|
// Decode the URL component
|
|
return decodeURIComponent(match[1]);
|
|
}
|
|
|
|
return downloadUrl; // Return original if no specific pattern is found
|
|
}, [downloadUrl]);
|
|
|
|
const hlsDownload = `https://hls-downloader.pstream.mov/?url=${encodeURIComponent(processDownloadUrl())}`;
|
|
const [, copyToClipboard] = useCopyToClipboard();
|
|
|
|
const sourceType = usePlayerStore((s) => s.source?.type);
|
|
const selectedCaption = usePlayerStore((s) => s.caption?.selected);
|
|
const captionList = usePlayerStore((s) => s.captionList);
|
|
const meta = usePlayerStore((s) => s.meta);
|
|
const duration = usePlayerStore((s) => s.progress.duration);
|
|
const source = usePlayerStore((s) => s.source);
|
|
const isDesktopApp = useIsDesktopApp();
|
|
|
|
const startOfflineDownload = useCallback(async () => {
|
|
if (!downloadUrl) return;
|
|
const title = meta?.title ? meta.title : t("player.menus.downloads.title");
|
|
const poster = meta?.poster;
|
|
let subtitleText: string | undefined;
|
|
|
|
if (selectedCaption?.srtData) {
|
|
subtitleText = selectedCaption.srtData;
|
|
} else if (captionList.length > 0) {
|
|
// Auto-fetch the first English caption, or the first available one
|
|
const defaultCaption =
|
|
captionList.find((c) => c.language === "en") ?? captionList[0];
|
|
try {
|
|
subtitleText = await downloadCaption(defaultCaption);
|
|
} catch {
|
|
// Continue without subtitles if fetch fails
|
|
}
|
|
}
|
|
|
|
const headers = {
|
|
...(source?.headers ?? {}),
|
|
...(source?.preferredHeaders ?? {}),
|
|
};
|
|
|
|
window.desktopApi?.startDownload({
|
|
url: downloadUrl,
|
|
title,
|
|
poster,
|
|
subtitleText,
|
|
duration,
|
|
type: sourceType,
|
|
headers,
|
|
});
|
|
|
|
if (window.desktopApi?.openOffline) {
|
|
window.desktopApi.openOffline();
|
|
} else {
|
|
router.navigate("/");
|
|
}
|
|
}, [
|
|
downloadUrl,
|
|
meta,
|
|
selectedCaption,
|
|
captionList,
|
|
duration,
|
|
router,
|
|
source,
|
|
sourceType,
|
|
t,
|
|
]);
|
|
|
|
const openSubtitleDownload = useCallback(() => {
|
|
const dataUrl = selectedCaption
|
|
? convertSubtitlesToSrtDataurl(selectedCaption?.srtData)
|
|
: null;
|
|
if (!dataUrl) return;
|
|
window.open(dataUrl);
|
|
}, [selectedCaption]);
|
|
|
|
return (
|
|
<>
|
|
<Menu.BackLink onClick={() => router.navigate("/")}>
|
|
{t("player.menus.downloads.title")}
|
|
</Menu.BackLink>
|
|
<Menu.Section>
|
|
<div className="mb-4">
|
|
{sourceType === "hls" ? (
|
|
<div className="mb-6">
|
|
{isDesktopApp ? (
|
|
<>
|
|
<Menu.Paragraph marginClass="mb-6">
|
|
<StyleTrans k="player.menus.downloads.disclaimer" />
|
|
</Menu.Paragraph>
|
|
|
|
<Menu.Paragraph marginClass="mb-6">
|
|
<Trans i18nKey="player.menus.downloads.desktopDisclaimer" />
|
|
</Menu.Paragraph>
|
|
<Button
|
|
className="w-full mt-2"
|
|
theme="purple"
|
|
onClick={startOfflineDownload}
|
|
>
|
|
{t("player.menus.downloads.offlineButton")}
|
|
</Button>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Menu.Paragraph marginClass="mb-6">
|
|
<StyleTrans k="player.menus.downloads.hlsDisclaimer" />
|
|
</Menu.Paragraph>
|
|
|
|
<Button
|
|
className="w-full mt-2"
|
|
theme="purple"
|
|
href={hlsDownload}
|
|
>
|
|
{t("player.menus.downloads.button")}
|
|
</Button>
|
|
<p className="text-xs py-4">
|
|
<Trans i18nKey="player.menus.downloads.hlsDownloader">
|
|
<a
|
|
className="text-type-link"
|
|
href="https://hls-downloader.pstream.mov/"
|
|
/>
|
|
</Trans>
|
|
</p>
|
|
</>
|
|
)}
|
|
<Button
|
|
className="w-full mt-2"
|
|
theme="secondary"
|
|
href={downloadUrl}
|
|
onClick={(event) => {
|
|
// Allow context menu & left click to copy
|
|
event.preventDefault();
|
|
|
|
copyToClipboard(downloadUrl ?? "");
|
|
}}
|
|
>
|
|
{t("player.menus.downloads.copyHlsPlaylist")}
|
|
</Button>
|
|
<Button
|
|
className="w-full mt-2"
|
|
onClick={openSubtitleDownload}
|
|
disabled={!selectedCaption}
|
|
theme="secondary"
|
|
>
|
|
{t("player.menus.downloads.downloadSubtitle")}
|
|
</Button>
|
|
</div>
|
|
) : sourceType === "file" ? (
|
|
<div className="mb-6">
|
|
{isDesktopApp ? (
|
|
<>
|
|
<Menu.Paragraph marginClass="mb-6">
|
|
<Trans i18nKey="player.menus.downloads.desktopDisclaimer" />
|
|
</Menu.Paragraph>
|
|
<Button
|
|
className="w-full mt-2"
|
|
theme="purple"
|
|
onClick={startOfflineDownload}
|
|
>
|
|
{t("player.menus.downloads.offlineButton")}
|
|
</Button>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Menu.ChevronLink
|
|
onClick={() => router.navigate("/download/pc")}
|
|
>
|
|
{t("player.menus.downloads.onPc.title")}
|
|
</Menu.ChevronLink>
|
|
<Menu.ChevronLink
|
|
onClick={() => router.navigate("/download/ios")}
|
|
>
|
|
{t("player.menus.downloads.onIos.title")}
|
|
</Menu.ChevronLink>
|
|
<Menu.ChevronLink
|
|
onClick={() => router.navigate("/download/android")}
|
|
>
|
|
{t("player.menus.downloads.onAndroid.title")}
|
|
</Menu.ChevronLink>
|
|
|
|
<Menu.Divider />
|
|
|
|
<Menu.Paragraph marginClass="my-6">
|
|
<StyleTrans k="player.menus.downloads.disclaimer" />
|
|
</Menu.Paragraph>
|
|
<Button className="w-full" href={downloadUrl} theme="purple">
|
|
{t("player.menus.downloads.downloadVideo")}
|
|
</Button>
|
|
</>
|
|
)}
|
|
<Button
|
|
className="w-full mt-2"
|
|
onClick={openSubtitleDownload}
|
|
disabled={!selectedCaption}
|
|
theme="secondary"
|
|
download="subtitles.srt"
|
|
>
|
|
{t("player.menus.downloads.downloadSubtitle")}
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<Menu.ChevronLink onClick={() => router.navigate("/download/pc")}>
|
|
{t("player.menus.downloads.onPc.title")}
|
|
</Menu.ChevronLink>
|
|
<Menu.ChevronLink
|
|
onClick={() => router.navigate("/download/ios")}
|
|
>
|
|
{t("player.menus.downloads.onIos.title")}
|
|
</Menu.ChevronLink>
|
|
<Menu.ChevronLink
|
|
onClick={() => router.navigate("/download/android")}
|
|
>
|
|
{t("player.menus.downloads.onAndroid.title")}
|
|
</Menu.ChevronLink>
|
|
|
|
<Menu.Divider />
|
|
|
|
<Menu.Paragraph marginClass="my-6">
|
|
<StyleTrans k="player.menus.downloads.disclaimer" />
|
|
</Menu.Paragraph>
|
|
<Button className="w-full" href={downloadUrl} theme="purple">
|
|
{t("player.menus.downloads.downloadVideo")}
|
|
</Button>
|
|
<Button
|
|
className="w-full mt-2"
|
|
onClick={openSubtitleDownload}
|
|
disabled={!selectedCaption}
|
|
theme="secondary"
|
|
download="subtitles.srt"
|
|
>
|
|
{t("player.menus.downloads.downloadSubtitle")}
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</Menu.Section>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function AndroidExplanationView({ id }: { id: string }) {
|
|
const router = useOverlayRouter(id);
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<>
|
|
<Menu.BackLink onClick={() => router.navigate("/download")}>
|
|
{t("player.menus.downloads.onAndroid.shortTitle")}
|
|
</Menu.BackLink>
|
|
<Menu.Section>
|
|
<Menu.Paragraph>
|
|
<StyleTrans k="player.menus.downloads.onAndroid.1" />
|
|
</Menu.Paragraph>
|
|
</Menu.Section>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function PCExplanationView({ id }: { id: string }) {
|
|
const router = useOverlayRouter(id);
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<>
|
|
<Menu.BackLink onClick={() => router.navigate("/download")}>
|
|
{t("player.menus.downloads.onPc.shortTitle")}
|
|
</Menu.BackLink>
|
|
<Menu.Section>
|
|
<Menu.Paragraph>
|
|
<StyleTrans k="player.menus.downloads.onPc.1" />
|
|
</Menu.Paragraph>
|
|
</Menu.Section>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function IOSExplanationView({ id }: { id: string }) {
|
|
const router = useOverlayRouter(id);
|
|
|
|
return (
|
|
<>
|
|
<Menu.BackLink onClick={() => router.navigate("/download")}>
|
|
<StyleTrans k="player.menus.downloads.onIos.shortTitle" />
|
|
</Menu.BackLink>
|
|
<Menu.Section>
|
|
<Menu.Paragraph>
|
|
<StyleTrans k="player.menus.downloads.onIos.1" />
|
|
</Menu.Paragraph>
|
|
</Menu.Section>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export function DownloadRoutes({ id }: { id: string }) {
|
|
return (
|
|
<>
|
|
<OverlayPage id={id} path="/download" width={343} height={490}>
|
|
<Menu.CardWithScrollable>
|
|
<DownloadView id={id} />
|
|
</Menu.CardWithScrollable>
|
|
</OverlayPage>
|
|
<OverlayPage id={id} path="/download/ios" width={343} height={440}>
|
|
<Menu.CardWithScrollable>
|
|
<IOSExplanationView id={id} />
|
|
</Menu.CardWithScrollable>
|
|
</OverlayPage>
|
|
<OverlayPage id={id} path="/download/android" width={343} height={440}>
|
|
<Menu.CardWithScrollable>
|
|
<AndroidExplanationView id={id} />
|
|
</Menu.CardWithScrollable>
|
|
</OverlayPage>
|
|
<OverlayPage id={id} path="/download/pc" width={343} height={440}>
|
|
<Menu.CardWithScrollable>
|
|
<PCExplanationView id={id} />
|
|
</Menu.CardWithScrollable>
|
|
</OverlayPage>
|
|
</>
|
|
);
|
|
}
|