add manual scrape setting

This commit is contained in:
Pas 2025-10-08 19:30:12 -06:00
parent 583599d6a3
commit 678a5e4806
8 changed files with 259 additions and 14 deletions

View file

@ -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",

View file

@ -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(

View file

@ -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,
},
};
}

View file

@ -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} />

View file

@ -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">

View 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>
);
}

View file

@ -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>

View file

@ -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",