mirror of
https://github.com/p-stream/p-stream.git
synced 2026-03-11 17:55:33 +00:00
add manual scrape setting
This commit is contained in:
parent
583599d6a3
commit
678a5e4806
8 changed files with 259 additions and 14 deletions
|
|
@ -1055,7 +1055,10 @@
|
|||
"sourceOrderEnableLabel": "Custom source order",
|
||||
"embedOrder": "Reordering embeds",
|
||||
"embedOrderDescription": "Drag and drop to reorder embeds. This will determine the order in which embeds are checked for the media you are trying to watch. <br><br> <strong>(The default order is best for most users)</strong>",
|
||||
"embedOrderEnableLabel": "Custom embed order"
|
||||
"embedOrderEnableLabel": "Custom embed order",
|
||||
"manualSource": "Manual source selection",
|
||||
"manualSourceDescription": "Require picking a source before scraping. Disables automatic source selection and opens the source picker when starting playback.",
|
||||
"manualSourceLabel": "Manual source selection"
|
||||
},
|
||||
"reset": "Reset",
|
||||
"save": "Save",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export interface SettingsInput {
|
|||
enableLowPerformanceMode?: boolean;
|
||||
enableNativeSubtitles?: boolean;
|
||||
enableHoldToBoost?: boolean;
|
||||
manualSourceSelection?: boolean;
|
||||
}
|
||||
|
||||
export interface SettingsResponse {
|
||||
|
|
@ -52,6 +53,7 @@ export interface SettingsResponse {
|
|||
enableLowPerformanceMode?: boolean;
|
||||
enableNativeSubtitles?: boolean;
|
||||
enableHoldToBoost?: boolean;
|
||||
manualSourceSelection?: boolean;
|
||||
}
|
||||
|
||||
export function updateSettings(
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ export function useSettingsState(
|
|||
enableLowPerformanceMode: boolean,
|
||||
enableHoldToBoost: boolean,
|
||||
homeSectionOrder: string[],
|
||||
manualSourceSelection: boolean,
|
||||
) {
|
||||
const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] =
|
||||
useDerived(proxyUrls);
|
||||
|
|
@ -188,6 +189,12 @@ export function useSettingsState(
|
|||
resetHomeSectionOrder,
|
||||
homeSectionOrderChanged,
|
||||
] = useDerived(homeSectionOrder);
|
||||
const [
|
||||
manualSourceSelectionState,
|
||||
setManualSourceSelectionState,
|
||||
resetManualSourceSelection,
|
||||
manualSourceSelectionChanged,
|
||||
] = useDerived(manualSourceSelection);
|
||||
|
||||
function reset() {
|
||||
resetTheme();
|
||||
|
|
@ -215,6 +222,7 @@ export function useSettingsState(
|
|||
resetEnableLowPerformanceMode();
|
||||
resetEnableHoldToBoost();
|
||||
resetHomeSectionOrder();
|
||||
resetManualSourceSelection();
|
||||
}
|
||||
|
||||
const changed =
|
||||
|
|
@ -241,7 +249,8 @@ export function useSettingsState(
|
|||
forceCompactEpisodeViewChanged ||
|
||||
enableLowPerformanceModeChanged ||
|
||||
enableHoldToBoostChanged ||
|
||||
homeSectionOrderChanged;
|
||||
homeSectionOrderChanged ||
|
||||
manualSourceSelectionChanged;
|
||||
|
||||
return {
|
||||
reset,
|
||||
|
|
@ -366,5 +375,10 @@ export function useSettingsState(
|
|||
set: setHomeSectionOrderState,
|
||||
changed: homeSectionOrderChanged,
|
||||
},
|
||||
manualSourceSelection: {
|
||||
state: manualSourceSelectionState,
|
||||
set: setManualSourceSelectionState,
|
||||
changed: manualSourceSelectionChanged,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,10 @@ import { PlayerPart } from "@/pages/parts/player/PlayerPart";
|
|||
import { ResumePart } from "@/pages/parts/player/ResumePart";
|
||||
import { ScrapeErrorPart } from "@/pages/parts/player/ScrapeErrorPart";
|
||||
import { ScrapingPart } from "@/pages/parts/player/ScrapingPart";
|
||||
import { SourceSelectPart } from "@/pages/parts/player/SourceSelectPart";
|
||||
import { useLastNonPlayerLink } from "@/stores/history";
|
||||
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
|
||||
import { usePreferencesStore } from "@/stores/preferences";
|
||||
import { useProgressStore } from "@/stores/progress";
|
||||
import { needsOnboarding } from "@/utils/onboarding";
|
||||
import { parseTimestamp } from "@/utils/timestamp";
|
||||
|
|
@ -51,6 +53,9 @@ export function RealPlayerView() {
|
|||
} = usePlayer();
|
||||
const { setPlayerMeta, scrapeMedia } = usePlayerMeta();
|
||||
const backUrl = useLastNonPlayerLink();
|
||||
const manualSourceSelection = usePreferencesStore(
|
||||
(s) => s.manualSourceSelection,
|
||||
);
|
||||
const router = useOverlayRouter("settings");
|
||||
const openedWatchPartyRef = useRef<boolean>(false);
|
||||
const progressItems = useProgressStore((s) => s.items);
|
||||
|
|
@ -175,17 +180,21 @@ export function RealPlayerView() {
|
|||
/>
|
||||
) : null}
|
||||
{status === playerStatus.SCRAPING && scrapeMedia ? (
|
||||
<ScrapingPart
|
||||
media={scrapeMedia}
|
||||
onResult={(sources, sourceOrder) => {
|
||||
setErrorData({
|
||||
sourceOrder,
|
||||
sources,
|
||||
});
|
||||
setScrapeNotFound();
|
||||
}}
|
||||
onGetStream={playAfterScrape}
|
||||
/>
|
||||
manualSourceSelection ? (
|
||||
<SourceSelectPart media={scrapeMedia} />
|
||||
) : (
|
||||
<ScrapingPart
|
||||
media={scrapeMedia}
|
||||
onResult={(sources, sourceOrder) => {
|
||||
setErrorData({
|
||||
sourceOrder,
|
||||
sources,
|
||||
});
|
||||
setScrapeNotFound();
|
||||
}}
|
||||
onGetStream={playAfterScrape}
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
{status === playerStatus.SCRAPE_NOT_FOUND && errorData ? (
|
||||
<ScrapeErrorPart data={errorData} />
|
||||
|
|
|
|||
|
|
@ -200,6 +200,13 @@ export function SettingsPage() {
|
|||
const homeSectionOrder = usePreferencesStore((s) => s.homeSectionOrder);
|
||||
const setHomeSectionOrder = usePreferencesStore((s) => s.setHomeSectionOrder);
|
||||
|
||||
const manualSourceSelection = usePreferencesStore(
|
||||
(s) => s.manualSourceSelection,
|
||||
);
|
||||
const setManualSourceSelection = usePreferencesStore(
|
||||
(s) => s.setManualSourceSelection,
|
||||
);
|
||||
|
||||
const account = useAuthStore((s) => s.account);
|
||||
const updateProfile = useAuthStore((s) => s.setAccountProfile);
|
||||
const updateDeviceName = useAuthStore((s) => s.updateDeviceName);
|
||||
|
|
@ -253,6 +260,7 @@ export function SettingsPage() {
|
|||
enableLowPerformanceMode,
|
||||
enableHoldToBoost,
|
||||
homeSectionOrder,
|
||||
manualSourceSelection,
|
||||
);
|
||||
|
||||
const availableSources = useMemo(() => {
|
||||
|
|
@ -311,7 +319,8 @@ export function SettingsPage() {
|
|||
state.enableCarouselView.changed ||
|
||||
state.forceCompactEpisodeView.changed ||
|
||||
state.enableLowPerformanceMode.changed ||
|
||||
state.enableHoldToBoost.changed
|
||||
state.enableHoldToBoost.changed ||
|
||||
state.manualSourceSelection.changed
|
||||
) {
|
||||
await updateSettings(backendUrl, account, {
|
||||
applicationLanguage: state.appLanguage.state,
|
||||
|
|
@ -333,6 +342,7 @@ export function SettingsPage() {
|
|||
forceCompactEpisodeView: state.forceCompactEpisodeView.state,
|
||||
enableLowPerformanceMode: state.enableLowPerformanceMode.state,
|
||||
enableHoldToBoost: state.enableHoldToBoost.state,
|
||||
manualSourceSelection: state.manualSourceSelection.state,
|
||||
});
|
||||
}
|
||||
if (state.deviceName.changed) {
|
||||
|
|
@ -374,6 +384,7 @@ export function SettingsPage() {
|
|||
setEnableLowPerformanceMode(state.enableLowPerformanceMode.state);
|
||||
setEnableHoldToBoost(state.enableHoldToBoost.state);
|
||||
setHomeSectionOrder(state.homeSectionOrder.state);
|
||||
setManualSourceSelection(state.manualSourceSelection.state);
|
||||
|
||||
if (state.profile.state) {
|
||||
updateProfile(state.profile.state);
|
||||
|
|
@ -419,6 +430,7 @@ export function SettingsPage() {
|
|||
setEnableLowPerformanceMode,
|
||||
setEnableHoldToBoost,
|
||||
setHomeSectionOrder,
|
||||
setManualSourceSelection,
|
||||
]);
|
||||
return (
|
||||
<SubPageLayout>
|
||||
|
|
@ -471,6 +483,8 @@ export function SettingsPage() {
|
|||
setEnableLowPerformanceMode={state.enableLowPerformanceMode.set}
|
||||
enableHoldToBoost={state.enableHoldToBoost.state}
|
||||
setEnableHoldToBoost={state.enableHoldToBoost.set}
|
||||
manualSourceSelection={state.manualSourceSelection.state}
|
||||
setManualSourceSelection={state.manualSourceSelection.set}
|
||||
/>
|
||||
</div>
|
||||
<div id="settings-appearance" className="mt-28">
|
||||
|
|
|
|||
173
src/pages/parts/player/SourceSelectPart.tsx
Normal file
173
src/pages/parts/player/SourceSelectPart.tsx
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import { ScrapeMedia } from "@p-stream/providers";
|
||||
import React, { ReactNode, useEffect, useMemo, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { getCachedMetadata } from "@/backend/helpers/providerApi";
|
||||
import { Loading } from "@/components/layout/Loading";
|
||||
import {
|
||||
useEmbedScraping,
|
||||
useSourceScraping,
|
||||
} from "@/components/player/hooks/useSourceSelection";
|
||||
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||
import { SelectableLink } from "@/components/player/internals/ContextMenu/Links";
|
||||
|
||||
// Embed option component
|
||||
function EmbedOption(props: {
|
||||
embedId: string;
|
||||
url: string;
|
||||
sourceId: string;
|
||||
routerId: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const unknownEmbedName = t("player.menus.sources.unknownOption");
|
||||
|
||||
const embedName = useMemo(() => {
|
||||
if (!props.embedId) return unknownEmbedName;
|
||||
const sourceMeta = getCachedMetadata().find((s) => s.id === props.embedId);
|
||||
return sourceMeta?.name ?? unknownEmbedName;
|
||||
}, [props.embedId, unknownEmbedName]);
|
||||
|
||||
const { run, errored, loading } = useEmbedScraping(
|
||||
props.routerId,
|
||||
props.sourceId,
|
||||
props.url,
|
||||
props.embedId,
|
||||
);
|
||||
|
||||
return (
|
||||
<SelectableLink loading={loading} error={errored} onClick={run}>
|
||||
<span className="flex flex-col">
|
||||
<span>{embedName}</span>
|
||||
</span>
|
||||
</SelectableLink>
|
||||
);
|
||||
}
|
||||
|
||||
// Embed selection view (when a source is selected)
|
||||
function EmbedSelectionView(props: {
|
||||
sourceId: string;
|
||||
routerId: string;
|
||||
onBack: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { run, notfound, loading, items, errored } = useSourceScraping(
|
||||
props.sourceId,
|
||||
props.routerId,
|
||||
);
|
||||
|
||||
const sourceName = useMemo(() => {
|
||||
if (!props.sourceId) return "...";
|
||||
const sourceMeta = getCachedMetadata().find((s) => s.id === props.sourceId);
|
||||
return sourceMeta?.name ?? "...";
|
||||
}, [props.sourceId]);
|
||||
|
||||
const lastSourceId = useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
if (lastSourceId.current === props.sourceId) return;
|
||||
lastSourceId.current = props.sourceId;
|
||||
if (!props.sourceId) return;
|
||||
run();
|
||||
}, [run, props.sourceId]);
|
||||
|
||||
let content: ReactNode = null;
|
||||
if (loading)
|
||||
content = (
|
||||
<Menu.TextDisplay noIcon>
|
||||
<Loading />
|
||||
</Menu.TextDisplay>
|
||||
);
|
||||
else if (notfound)
|
||||
content = (
|
||||
<Menu.TextDisplay
|
||||
title={t("player.menus.sources.noStream.title") ?? undefined}
|
||||
>
|
||||
{t("player.menus.sources.noStream.text")}
|
||||
</Menu.TextDisplay>
|
||||
);
|
||||
else if (items?.length === 0)
|
||||
content = (
|
||||
<Menu.TextDisplay
|
||||
title={t("player.menus.sources.noEmbeds.title") ?? undefined}
|
||||
>
|
||||
{t("player.menus.sources.noEmbeds.text")}
|
||||
</Menu.TextDisplay>
|
||||
);
|
||||
else if (errored)
|
||||
content = (
|
||||
<Menu.TextDisplay
|
||||
title={t("player.menus.sources.failed.title") ?? undefined}
|
||||
>
|
||||
{t("player.menus.sources.failed.text")}
|
||||
</Menu.TextDisplay>
|
||||
);
|
||||
else if (items && props.sourceId)
|
||||
content = items.map((v) => (
|
||||
<EmbedOption
|
||||
key={`${v.embedId}-${v.url}`}
|
||||
embedId={v.embedId}
|
||||
url={v.url}
|
||||
routerId={props.routerId}
|
||||
sourceId={props.sourceId}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.BackLink onClick={props.onBack}>{sourceName}</Menu.BackLink>
|
||||
<Menu.Section>{content}</Menu.Section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Main source selection view
|
||||
export function SourceSelectPart(props: { media: ScrapeMedia }) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedSourceId, setSelectedSourceId] = React.useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const routerId = "manualSourceSelect";
|
||||
|
||||
const sources = useMemo(() => {
|
||||
const metaType = props.media.type;
|
||||
if (!metaType) return [];
|
||||
return getCachedMetadata()
|
||||
.filter((v) => v.type === "source")
|
||||
.filter((v) => v.mediaTypes?.includes(metaType));
|
||||
}, [props.media.type]);
|
||||
|
||||
if (selectedSourceId) {
|
||||
return (
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<div className="w-full max-w-md">
|
||||
<Menu.CardWithScrollable>
|
||||
<EmbedSelectionView
|
||||
sourceId={selectedSourceId}
|
||||
routerId={routerId}
|
||||
onBack={() => setSelectedSourceId(null)}
|
||||
/>
|
||||
</Menu.CardWithScrollable>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<div className="w-full max-w-md">
|
||||
<Menu.CardWithScrollable>
|
||||
<Menu.Title>{t("player.menus.sources.title")}</Menu.Title>
|
||||
<Menu.Section className="pb-4">
|
||||
{sources.map((v) => (
|
||||
<SelectableLink
|
||||
key={v.id}
|
||||
onClick={() => setSelectedSourceId(v.id)}
|
||||
>
|
||||
{v.name}
|
||||
</SelectableLink>
|
||||
))}
|
||||
</Menu.Section>
|
||||
</Menu.CardWithScrollable>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -31,6 +31,8 @@ export function PreferencesPart(props: {
|
|||
setEnableLowPerformanceMode: (v: boolean) => void;
|
||||
enableHoldToBoost: boolean;
|
||||
setEnableHoldToBoost: (v: boolean) => void;
|
||||
manualSourceSelection: boolean;
|
||||
setManualSourceSelection: (v: boolean) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const sorted = sortLangCodes(appLanguageOptions.map((item) => item.code));
|
||||
|
|
@ -219,6 +221,26 @@ export function PreferencesPart(props: {
|
|||
{/* Column */}
|
||||
<div id="source-order" className="space-y-8">
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Manual Source Selection */}
|
||||
<div>
|
||||
<p className="text-white font-bold mb-3">
|
||||
{t("settings.preferences.manualSource")}
|
||||
</p>
|
||||
<p className="max-w-[25rem] font-medium">
|
||||
{t("settings.preferences.manualSourceDescription")}
|
||||
</p>
|
||||
<div
|
||||
onClick={() =>
|
||||
props.setManualSourceSelection(!props.manualSourceSelection)
|
||||
}
|
||||
className="bg-dropdown-background hover:bg-dropdown-hoverBackground select-none my-4 cursor-pointer space-x-3 flex items-center max-w-[25rem] py-3 px-4 rounded-lg"
|
||||
>
|
||||
<Toggle enabled={props.manualSourceSelection} />
|
||||
<p className="flex-1 text-white font-bold">
|
||||
{t("settings.preferences.manualSourceLabel")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-white font-bold">
|
||||
{t("settings.preferences.sourceOrder")}
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export interface PreferencesStore {
|
|||
enableNativeSubtitles: boolean;
|
||||
enableHoldToBoost: boolean;
|
||||
homeSectionOrder: string[];
|
||||
manualSourceSelection: boolean;
|
||||
|
||||
setEnableThumbnails(v: boolean): void;
|
||||
setEnableAutoplay(v: boolean): void;
|
||||
|
|
@ -44,6 +45,7 @@ export interface PreferencesStore {
|
|||
setEnableNativeSubtitles(v: boolean): void;
|
||||
setEnableHoldToBoost(v: boolean): void;
|
||||
setHomeSectionOrder(v: string[]): void;
|
||||
setManualSourceSelection(v: boolean): void;
|
||||
}
|
||||
|
||||
export const usePreferencesStore = create(
|
||||
|
|
@ -69,6 +71,7 @@ export const usePreferencesStore = create(
|
|||
enableNativeSubtitles: false,
|
||||
enableHoldToBoost: true,
|
||||
homeSectionOrder: ["watching", "bookmarks"],
|
||||
manualSourceSelection: false,
|
||||
setEnableThumbnails(v) {
|
||||
set((s) => {
|
||||
s.enableThumbnails = v;
|
||||
|
|
@ -169,6 +172,11 @@ export const usePreferencesStore = create(
|
|||
s.homeSectionOrder = v;
|
||||
});
|
||||
},
|
||||
setManualSourceSelection(v) {
|
||||
set((s) => {
|
||||
s.manualSourceSelection = v;
|
||||
});
|
||||
},
|
||||
})),
|
||||
{
|
||||
name: "__MW::preferences",
|
||||
|
|
|
|||
Loading…
Reference in a new issue