pstream is dead; long live pstream taciturnaxolotl.github.io/pstream-ng/
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

add manual scrape setting

Pas 678a5e48 583599d6

+259 -14
+4 -1
src/assets/locales/en.json
··· 1055 1055 "sourceOrderEnableLabel": "Custom source order", 1056 1056 "embedOrder": "Reordering embeds", 1057 1057 "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>", 1058 - "embedOrderEnableLabel": "Custom embed order" 1058 + "embedOrderEnableLabel": "Custom embed order", 1059 + "manualSource": "Manual source selection", 1060 + "manualSourceDescription": "Require picking a source before scraping. Disables automatic source selection and opens the source picker when starting playback.", 1061 + "manualSourceLabel": "Manual source selection" 1059 1062 }, 1060 1063 "reset": "Reset", 1061 1064 "save": "Save",
+2
src/backend/accounts/settings.ts
··· 27 27 enableLowPerformanceMode?: boolean; 28 28 enableNativeSubtitles?: boolean; 29 29 enableHoldToBoost?: boolean; 30 + manualSourceSelection?: boolean; 30 31 } 31 32 32 33 export interface SettingsResponse { ··· 52 53 enableLowPerformanceMode?: boolean; 53 54 enableNativeSubtitles?: boolean; 54 55 enableHoldToBoost?: boolean; 56 + manualSourceSelection?: boolean; 55 57 } 56 58 57 59 export function updateSettings(
+15 -1
src/hooks/useSettingsState.ts
··· 67 67 enableLowPerformanceMode: boolean, 68 68 enableHoldToBoost: boolean, 69 69 homeSectionOrder: string[], 70 + manualSourceSelection: boolean, 70 71 ) { 71 72 const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] = 72 73 useDerived(proxyUrls); ··· 188 189 resetHomeSectionOrder, 189 190 homeSectionOrderChanged, 190 191 ] = useDerived(homeSectionOrder); 192 + const [ 193 + manualSourceSelectionState, 194 + setManualSourceSelectionState, 195 + resetManualSourceSelection, 196 + manualSourceSelectionChanged, 197 + ] = useDerived(manualSourceSelection); 191 198 192 199 function reset() { 193 200 resetTheme(); ··· 215 222 resetEnableLowPerformanceMode(); 216 223 resetEnableHoldToBoost(); 217 224 resetHomeSectionOrder(); 225 + resetManualSourceSelection(); 218 226 } 219 227 220 228 const changed = ··· 241 249 forceCompactEpisodeViewChanged || 242 250 enableLowPerformanceModeChanged || 243 251 enableHoldToBoostChanged || 244 - homeSectionOrderChanged; 252 + homeSectionOrderChanged || 253 + manualSourceSelectionChanged; 245 254 246 255 return { 247 256 reset, ··· 365 374 state: homeSectionOrderState, 366 375 set: setHomeSectionOrderState, 367 376 changed: homeSectionOrderChanged, 377 + }, 378 + manualSourceSelection: { 379 + state: manualSourceSelectionState, 380 + set: setManualSourceSelectionState, 381 + changed: manualSourceSelectionChanged, 368 382 }, 369 383 }; 370 384 }
+20 -11
src/pages/PlayerView.tsx
··· 22 22 import { ResumePart } from "@/pages/parts/player/ResumePart"; 23 23 import { ScrapeErrorPart } from "@/pages/parts/player/ScrapeErrorPart"; 24 24 import { ScrapingPart } from "@/pages/parts/player/ScrapingPart"; 25 + import { SourceSelectPart } from "@/pages/parts/player/SourceSelectPart"; 25 26 import { useLastNonPlayerLink } from "@/stores/history"; 26 27 import { PlayerMeta, playerStatus } from "@/stores/player/slices/source"; 28 + import { usePreferencesStore } from "@/stores/preferences"; 27 29 import { useProgressStore } from "@/stores/progress"; 28 30 import { needsOnboarding } from "@/utils/onboarding"; 29 31 import { parseTimestamp } from "@/utils/timestamp"; ··· 51 53 } = usePlayer(); 52 54 const { setPlayerMeta, scrapeMedia } = usePlayerMeta(); 53 55 const backUrl = useLastNonPlayerLink(); 56 + const manualSourceSelection = usePreferencesStore( 57 + (s) => s.manualSourceSelection, 58 + ); 54 59 const router = useOverlayRouter("settings"); 55 60 const openedWatchPartyRef = useRef<boolean>(false); 56 61 const progressItems = useProgressStore((s) => s.items); ··· 175 180 /> 176 181 ) : null} 177 182 {status === playerStatus.SCRAPING && scrapeMedia ? ( 178 - <ScrapingPart 179 - media={scrapeMedia} 180 - onResult={(sources, sourceOrder) => { 181 - setErrorData({ 182 - sourceOrder, 183 - sources, 184 - }); 185 - setScrapeNotFound(); 186 - }} 187 - onGetStream={playAfterScrape} 188 - /> 183 + manualSourceSelection ? ( 184 + <SourceSelectPart media={scrapeMedia} /> 185 + ) : ( 186 + <ScrapingPart 187 + media={scrapeMedia} 188 + onResult={(sources, sourceOrder) => { 189 + setErrorData({ 190 + sourceOrder, 191 + sources, 192 + }); 193 + setScrapeNotFound(); 194 + }} 195 + onGetStream={playAfterScrape} 196 + /> 197 + ) 189 198 ) : null} 190 199 {status === playerStatus.SCRAPE_NOT_FOUND && errorData ? ( 191 200 <ScrapeErrorPart data={errorData} />
+15 -1
src/pages/Settings.tsx
··· 200 200 const homeSectionOrder = usePreferencesStore((s) => s.homeSectionOrder); 201 201 const setHomeSectionOrder = usePreferencesStore((s) => s.setHomeSectionOrder); 202 202 203 + const manualSourceSelection = usePreferencesStore( 204 + (s) => s.manualSourceSelection, 205 + ); 206 + const setManualSourceSelection = usePreferencesStore( 207 + (s) => s.setManualSourceSelection, 208 + ); 209 + 203 210 const account = useAuthStore((s) => s.account); 204 211 const updateProfile = useAuthStore((s) => s.setAccountProfile); 205 212 const updateDeviceName = useAuthStore((s) => s.updateDeviceName); ··· 253 260 enableLowPerformanceMode, 254 261 enableHoldToBoost, 255 262 homeSectionOrder, 263 + manualSourceSelection, 256 264 ); 257 265 258 266 const availableSources = useMemo(() => { ··· 311 319 state.enableCarouselView.changed || 312 320 state.forceCompactEpisodeView.changed || 313 321 state.enableLowPerformanceMode.changed || 314 - state.enableHoldToBoost.changed 322 + state.enableHoldToBoost.changed || 323 + state.manualSourceSelection.changed 315 324 ) { 316 325 await updateSettings(backendUrl, account, { 317 326 applicationLanguage: state.appLanguage.state, ··· 333 342 forceCompactEpisodeView: state.forceCompactEpisodeView.state, 334 343 enableLowPerformanceMode: state.enableLowPerformanceMode.state, 335 344 enableHoldToBoost: state.enableHoldToBoost.state, 345 + manualSourceSelection: state.manualSourceSelection.state, 336 346 }); 337 347 } 338 348 if (state.deviceName.changed) { ··· 374 384 setEnableLowPerformanceMode(state.enableLowPerformanceMode.state); 375 385 setEnableHoldToBoost(state.enableHoldToBoost.state); 376 386 setHomeSectionOrder(state.homeSectionOrder.state); 387 + setManualSourceSelection(state.manualSourceSelection.state); 377 388 378 389 if (state.profile.state) { 379 390 updateProfile(state.profile.state); ··· 419 430 setEnableLowPerformanceMode, 420 431 setEnableHoldToBoost, 421 432 setHomeSectionOrder, 433 + setManualSourceSelection, 422 434 ]); 423 435 return ( 424 436 <SubPageLayout> ··· 471 483 setEnableLowPerformanceMode={state.enableLowPerformanceMode.set} 472 484 enableHoldToBoost={state.enableHoldToBoost.state} 473 485 setEnableHoldToBoost={state.enableHoldToBoost.set} 486 + manualSourceSelection={state.manualSourceSelection.state} 487 + setManualSourceSelection={state.manualSourceSelection.set} 474 488 /> 475 489 </div> 476 490 <div id="settings-appearance" className="mt-28">
+173
src/pages/parts/player/SourceSelectPart.tsx
··· 1 + import { ScrapeMedia } from "@p-stream/providers"; 2 + import React, { ReactNode, useEffect, useMemo, useRef } from "react"; 3 + import { useTranslation } from "react-i18next"; 4 + 5 + import { getCachedMetadata } from "@/backend/helpers/providerApi"; 6 + import { Loading } from "@/components/layout/Loading"; 7 + import { 8 + useEmbedScraping, 9 + useSourceScraping, 10 + } from "@/components/player/hooks/useSourceSelection"; 11 + import { Menu } from "@/components/player/internals/ContextMenu"; 12 + import { SelectableLink } from "@/components/player/internals/ContextMenu/Links"; 13 + 14 + // Embed option component 15 + function EmbedOption(props: { 16 + embedId: string; 17 + url: string; 18 + sourceId: string; 19 + routerId: string; 20 + }) { 21 + const { t } = useTranslation(); 22 + const unknownEmbedName = t("player.menus.sources.unknownOption"); 23 + 24 + const embedName = useMemo(() => { 25 + if (!props.embedId) return unknownEmbedName; 26 + const sourceMeta = getCachedMetadata().find((s) => s.id === props.embedId); 27 + return sourceMeta?.name ?? unknownEmbedName; 28 + }, [props.embedId, unknownEmbedName]); 29 + 30 + const { run, errored, loading } = useEmbedScraping( 31 + props.routerId, 32 + props.sourceId, 33 + props.url, 34 + props.embedId, 35 + ); 36 + 37 + return ( 38 + <SelectableLink loading={loading} error={errored} onClick={run}> 39 + <span className="flex flex-col"> 40 + <span>{embedName}</span> 41 + </span> 42 + </SelectableLink> 43 + ); 44 + } 45 + 46 + // Embed selection view (when a source is selected) 47 + function EmbedSelectionView(props: { 48 + sourceId: string; 49 + routerId: string; 50 + onBack: () => void; 51 + }) { 52 + const { t } = useTranslation(); 53 + const { run, notfound, loading, items, errored } = useSourceScraping( 54 + props.sourceId, 55 + props.routerId, 56 + ); 57 + 58 + const sourceName = useMemo(() => { 59 + if (!props.sourceId) return "..."; 60 + const sourceMeta = getCachedMetadata().find((s) => s.id === props.sourceId); 61 + return sourceMeta?.name ?? "..."; 62 + }, [props.sourceId]); 63 + 64 + const lastSourceId = useRef<string | null>(null); 65 + useEffect(() => { 66 + if (lastSourceId.current === props.sourceId) return; 67 + lastSourceId.current = props.sourceId; 68 + if (!props.sourceId) return; 69 + run(); 70 + }, [run, props.sourceId]); 71 + 72 + let content: ReactNode = null; 73 + if (loading) 74 + content = ( 75 + <Menu.TextDisplay noIcon> 76 + <Loading /> 77 + </Menu.TextDisplay> 78 + ); 79 + else if (notfound) 80 + content = ( 81 + <Menu.TextDisplay 82 + title={t("player.menus.sources.noStream.title") ?? undefined} 83 + > 84 + {t("player.menus.sources.noStream.text")} 85 + </Menu.TextDisplay> 86 + ); 87 + else if (items?.length === 0) 88 + content = ( 89 + <Menu.TextDisplay 90 + title={t("player.menus.sources.noEmbeds.title") ?? undefined} 91 + > 92 + {t("player.menus.sources.noEmbeds.text")} 93 + </Menu.TextDisplay> 94 + ); 95 + else if (errored) 96 + content = ( 97 + <Menu.TextDisplay 98 + title={t("player.menus.sources.failed.title") ?? undefined} 99 + > 100 + {t("player.menus.sources.failed.text")} 101 + </Menu.TextDisplay> 102 + ); 103 + else if (items && props.sourceId) 104 + content = items.map((v) => ( 105 + <EmbedOption 106 + key={`${v.embedId}-${v.url}`} 107 + embedId={v.embedId} 108 + url={v.url} 109 + routerId={props.routerId} 110 + sourceId={props.sourceId} 111 + /> 112 + )); 113 + 114 + return ( 115 + <> 116 + <Menu.BackLink onClick={props.onBack}>{sourceName}</Menu.BackLink> 117 + <Menu.Section>{content}</Menu.Section> 118 + </> 119 + ); 120 + } 121 + 122 + // Main source selection view 123 + export function SourceSelectPart(props: { media: ScrapeMedia }) { 124 + const { t } = useTranslation(); 125 + const [selectedSourceId, setSelectedSourceId] = React.useState<string | null>( 126 + null, 127 + ); 128 + const routerId = "manualSourceSelect"; 129 + 130 + const sources = useMemo(() => { 131 + const metaType = props.media.type; 132 + if (!metaType) return []; 133 + return getCachedMetadata() 134 + .filter((v) => v.type === "source") 135 + .filter((v) => v.mediaTypes?.includes(metaType)); 136 + }, [props.media.type]); 137 + 138 + if (selectedSourceId) { 139 + return ( 140 + <div className="h-full w-full flex items-center justify-center"> 141 + <div className="w-full max-w-md"> 142 + <Menu.CardWithScrollable> 143 + <EmbedSelectionView 144 + sourceId={selectedSourceId} 145 + routerId={routerId} 146 + onBack={() => setSelectedSourceId(null)} 147 + /> 148 + </Menu.CardWithScrollable> 149 + </div> 150 + </div> 151 + ); 152 + } 153 + 154 + return ( 155 + <div className="h-full w-full flex items-center justify-center"> 156 + <div className="w-full max-w-md"> 157 + <Menu.CardWithScrollable> 158 + <Menu.Title>{t("player.menus.sources.title")}</Menu.Title> 159 + <Menu.Section className="pb-4"> 160 + {sources.map((v) => ( 161 + <SelectableLink 162 + key={v.id} 163 + onClick={() => setSelectedSourceId(v.id)} 164 + > 165 + {v.name} 166 + </SelectableLink> 167 + ))} 168 + </Menu.Section> 169 + </Menu.CardWithScrollable> 170 + </div> 171 + </div> 172 + ); 173 + }
+22
src/pages/parts/settings/PreferencesPart.tsx
··· 31 31 setEnableLowPerformanceMode: (v: boolean) => void; 32 32 enableHoldToBoost: boolean; 33 33 setEnableHoldToBoost: (v: boolean) => void; 34 + manualSourceSelection: boolean; 35 + setManualSourceSelection: (v: boolean) => void; 34 36 }) { 35 37 const { t } = useTranslation(); 36 38 const sorted = sortLangCodes(appLanguageOptions.map((item) => item.code)); ··· 219 221 {/* Column */} 220 222 <div id="source-order" className="space-y-8"> 221 223 <div className="flex flex-col gap-3"> 224 + {/* Manual Source Selection */} 225 + <div> 226 + <p className="text-white font-bold mb-3"> 227 + {t("settings.preferences.manualSource")} 228 + </p> 229 + <p className="max-w-[25rem] font-medium"> 230 + {t("settings.preferences.manualSourceDescription")} 231 + </p> 232 + <div 233 + onClick={() => 234 + props.setManualSourceSelection(!props.manualSourceSelection) 235 + } 236 + 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" 237 + > 238 + <Toggle enabled={props.manualSourceSelection} /> 239 + <p className="flex-1 text-white font-bold"> 240 + {t("settings.preferences.manualSourceLabel")} 241 + </p> 242 + </div> 243 + </div> 222 244 <p className="text-white font-bold"> 223 245 {t("settings.preferences.sourceOrder")} 224 246 </p>
+8
src/stores/preferences/index.tsx
··· 23 23 enableNativeSubtitles: boolean; 24 24 enableHoldToBoost: boolean; 25 25 homeSectionOrder: string[]; 26 + manualSourceSelection: boolean; 26 27 27 28 setEnableThumbnails(v: boolean): void; 28 29 setEnableAutoplay(v: boolean): void; ··· 44 45 setEnableNativeSubtitles(v: boolean): void; 45 46 setEnableHoldToBoost(v: boolean): void; 46 47 setHomeSectionOrder(v: string[]): void; 48 + setManualSourceSelection(v: boolean): void; 47 49 } 48 50 49 51 export const usePreferencesStore = create( ··· 69 71 enableNativeSubtitles: false, 70 72 enableHoldToBoost: true, 71 73 homeSectionOrder: ["watching", "bookmarks"], 74 + manualSourceSelection: false, 72 75 setEnableThumbnails(v) { 73 76 set((s) => { 74 77 s.enableThumbnails = v; ··· 167 170 setHomeSectionOrder(v) { 168 171 set((s) => { 169 172 s.homeSectionOrder = v; 173 + }); 174 + }, 175 + setManualSourceSelection(v) { 176 + set((s) => { 177 + s.manualSourceSelection = v; 170 178 }); 171 179 }, 172 180 })),