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 lastSuccessfulSource feature

to sort the last successful source to the top

Pas d575d711 627d66ec

+251 -8
+4 -1
src/assets/locales/en.json
··· 1107 1107 "embedOrderEnableLabel": "Custom embed order", 1108 1108 "manualSource": "Manual source selection", 1109 1109 "manualSourceDescription": "Require picking a source before scraping. Disables automatic source selection and opens the source picker when starting playback.", 1110 - "manualSourceLabel": "Manual source selection" 1110 + "manualSourceLabel": "Manual source selection", 1111 + "lastSuccessfulSource": "Last successful source", 1112 + "lastSuccessfulSourceDescription": "Automatically prioritize the source that successfully provided content for the previous episode. This helps ensure continuity when watching series.", 1113 + "lastSuccessfulSourceEnableLabel": "Enable last successful source" 1111 1114 }, 1112 1115 "reset": "Reset", 1113 1116 "save": "Save",
+4
src/backend/accounts/settings.ts
··· 21 21 forceCompactEpisodeView?: boolean; 22 22 sourceOrder?: string[] | null; 23 23 enableSourceOrder?: boolean; 24 + lastSuccessfulSource?: string | null; 25 + enableLastSuccessfulSource?: boolean; 24 26 disabledSources?: string[] | null; 25 27 embedOrder?: string[] | null; 26 28 enableEmbedOrder?: boolean; ··· 52 54 forceCompactEpisodeView?: boolean; 53 55 sourceOrder?: string[] | null; 54 56 enableSourceOrder?: boolean; 57 + lastSuccessfulSource?: string | null; 58 + enableLastSuccessfulSource?: boolean; 55 59 disabledSources?: string[] | null; 56 60 embedOrder?: string[] | null; 57 61 enableEmbedOrder?: boolean;
+12
src/components/player/atoms/NextEpisodeButton.tsx
··· 106 106 const time = usePlayerStore((s) => s.progress.time); 107 107 const enableAutoplay = usePreferencesStore((s) => s.enableAutoplay); 108 108 const enableSkipCredits = usePreferencesStore((s) => s.enableSkipCredits); 109 + const setLastSuccessfulSource = usePreferencesStore( 110 + (s) => s.setLastSuccessfulSource, 111 + ); 109 112 const showingState = shouldShowNextEpisodeButton(time, duration); 110 113 const status = usePlayerStore((s) => s.status); 111 114 const setShouldStartFromBeginning = usePlayerStore( 112 115 (s) => s.setShouldStartFromBeginning, 113 116 ); 114 117 const updateItem = useProgressStore((s) => s.updateItem); 118 + const sourceId = usePlayerStore((s) => s.sourceId); 115 119 116 120 const isLastEpisode = 117 121 !meta?.episode?.number || !meta?.episodes?.at(-1)?.number ··· 147 151 148 152 const loadNextEpisode = useCallback(() => { 149 153 if (!meta || !nextEp) return; 154 + 155 + // Store the current source as the last successful source 156 + if (sourceId) { 157 + setLastSuccessfulSource(sourceId); 158 + } 159 + 150 160 const metaCopy = { ...meta }; 151 161 metaCopy.episode = nextEp; 152 162 metaCopy.season = ··· 173 183 updateItem, 174 184 isLastEpisode, 175 185 nextSeason, 186 + sourceId, 187 + setLastSuccessfulSource, 176 188 ]); 177 189 178 190 const startCurrentEpisodeFromBeginning = useCallback(() => {
+36 -2
src/components/player/atoms/settings/SourceSelectingView.tsx
··· 144 144 const currentSourceId = usePlayerStore((s) => s.sourceId); 145 145 const preferredSourceOrder = usePreferencesStore((s) => s.sourceOrder); 146 146 const enableSourceOrder = usePreferencesStore((s) => s.enableSourceOrder); 147 + const lastSuccessfulSource = usePreferencesStore( 148 + (s) => s.lastSuccessfulSource, 149 + ); 150 + const enableLastSuccessfulSource = usePreferencesStore( 151 + (s) => s.enableLastSuccessfulSource, 152 + ); 147 153 const disabledSources = usePreferencesStore((s) => s.disabledSources); 148 154 149 155 const sources = useMemo(() => { ··· 154 160 .filter((v) => !disabledSources.includes(v.id)); 155 161 156 162 if (!enableSourceOrder || preferredSourceOrder.length === 0) { 163 + // Even without custom source order, prioritize last successful source if enabled 164 + if (enableLastSuccessfulSource && lastSuccessfulSource) { 165 + const lastSourceIndex = allSources.findIndex( 166 + (s) => s.id === lastSuccessfulSource, 167 + ); 168 + if (lastSourceIndex !== -1) { 169 + const lastSource = allSources.splice(lastSourceIndex, 1)[0]; 170 + return [lastSource, ...allSources]; 171 + } 172 + } 157 173 return allSources; 158 174 } 159 175 160 - // Sort sources according to preferred order 176 + // Sort sources according to preferred order, but prioritize last successful source 161 177 const orderedSources = []; 162 178 const remainingSources = [...allSources]; 163 179 180 + // First, add the last successful source if it exists, is available, and the feature is enabled 181 + if (enableLastSuccessfulSource && lastSuccessfulSource) { 182 + const lastSourceIndex = remainingSources.findIndex( 183 + (s) => s.id === lastSuccessfulSource, 184 + ); 185 + if (lastSourceIndex !== -1) { 186 + orderedSources.push(remainingSources[lastSourceIndex]); 187 + remainingSources.splice(lastSourceIndex, 1); 188 + } 189 + } 190 + 164 191 // Add sources in preferred order 165 192 for (const sourceId of preferredSourceOrder) { 166 193 const sourceIndex = remainingSources.findIndex((s) => s.id === sourceId); ··· 174 201 orderedSources.push(...remainingSources); 175 202 176 203 return orderedSources; 177 - }, [metaType, preferredSourceOrder, enableSourceOrder, disabledSources]); 204 + }, [ 205 + metaType, 206 + preferredSourceOrder, 207 + enableSourceOrder, 208 + disabledSources, 209 + lastSuccessfulSource, 210 + enableLastSuccessfulSource, 211 + ]); 178 212 179 213 return ( 180 214 <>
+10 -1
src/components/player/internals/SkipEpisodeButton.tsx
··· 5 5 import { VideoPlayerButton } from "@/components/player/internals/Button"; 6 6 import { PlayerMeta } from "@/stores/player/slices/source"; 7 7 import { usePlayerStore } from "@/stores/player/store"; 8 + import { usePreferencesStore } from "@/stores/preferences"; 8 9 import { useProgressStore } from "@/stores/progress"; 9 10 10 11 interface SkipEpisodeButtonProps { ··· 19 20 (s) => s.setShouldStartFromBeginning, 20 21 ); 21 22 const updateItem = useProgressStore((s) => s.updateItem); 22 - 23 + const sourceId = usePlayerStore((s) => s.sourceId); 24 + const setLastSuccessfulSource = usePreferencesStore( 25 + (s) => s.setLastSuccessfulSource, 26 + ); 23 27 const nextEp = meta?.episodes?.find( 24 28 (v) => v.number === (meta?.episode?.number ?? 0) + 1, 25 29 ); 26 30 27 31 const loadNextEpisode = useCallback(() => { 28 32 if (!meta || !nextEp) return; 33 + if (sourceId) { 34 + setLastSuccessfulSource(sourceId); 35 + } 29 36 const metaCopy = { ...meta }; 30 37 metaCopy.episode = nextEp; 31 38 setShouldStartFromBeginning(true); ··· 43 50 props, 44 51 setShouldStartFromBeginning, 45 52 updateItem, 53 + sourceId, 54 + setLastSuccessfulSource, 46 55 ]); 47 56 48 57 // Don't show button if not in control, not a show, or no next episode
+16
src/hooks/auth/useAuthData.ts
··· 58 58 const setEnableSourceOrder = usePreferencesStore( 59 59 (s) => s.setEnableSourceOrder, 60 60 ); 61 + const setLastSuccessfulSource = usePreferencesStore( 62 + (s) => s.setLastSuccessfulSource, 63 + ); 64 + const setEnableLastSuccessfulSource = usePreferencesStore( 65 + (s) => s.setEnableLastSuccessfulSource, 66 + ); 61 67 const setDisabledSources = usePreferencesStore((s) => s.setDisabledSources); 62 68 const setEmbedOrder = usePreferencesStore((s) => s.setEmbedOrder); 63 69 const setEnableEmbedOrder = usePreferencesStore((s) => s.setEnableEmbedOrder); ··· 193 199 setEnableSourceOrder(settings.enableSourceOrder); 194 200 } 195 201 202 + if (settings.lastSuccessfulSource !== undefined) { 203 + setLastSuccessfulSource(settings.lastSuccessfulSource); 204 + } 205 + 206 + if (settings.enableLastSuccessfulSource !== undefined) { 207 + setEnableLastSuccessfulSource(settings.enableLastSuccessfulSource); 208 + } 209 + 196 210 if (settings.disabledSources !== undefined) { 197 211 setDisabledSources(settings.disabledSources ?? []); 198 212 } ··· 265 279 setForceCompactEpisodeView, 266 280 setSourceOrder, 267 281 setEnableSourceOrder, 282 + setLastSuccessfulSource, 283 + setEnableLastSuccessfulSource, 268 284 setDisabledSources, 269 285 setEmbedOrder, 270 286 setEnableEmbedOrder,
+28 -2
src/hooks/useProviderScrape.tsx
··· 155 155 156 156 const preferredSourceOrder = usePreferencesStore((s) => s.sourceOrder); 157 157 const enableSourceOrder = usePreferencesStore((s) => s.enableSourceOrder); 158 + const lastSuccessfulSource = usePreferencesStore( 159 + (s) => s.lastSuccessfulSource, 160 + ); 161 + const enableLastSuccessfulSource = usePreferencesStore( 162 + (s) => s.enableLastSuccessfulSource, 163 + ); 158 164 const disabledSources = usePreferencesStore((s) => s.disabledSources); 159 165 const preferredEmbedOrder = usePreferencesStore((s) => s.embedOrder); 160 166 const enableEmbedOrder = usePreferencesStore((s) => s.enableEmbedOrder); ··· 162 168 163 169 const startScraping = useCallback( 164 170 async (media: ScrapeMedia) => { 165 - // Filter out disabled sources from the source order 166 - const filteredSourceOrder = enableSourceOrder 171 + // Create source order that prioritizes last successful source 172 + let filteredSourceOrder = enableSourceOrder 167 173 ? preferredSourceOrder.filter((id) => !disabledSources.includes(id)) 168 174 : undefined; 175 + 176 + // If we have a last successful source and the feature is enabled, prioritize it 177 + if (enableLastSuccessfulSource && lastSuccessfulSource) { 178 + // Get all available sources (either from custom order or default) 179 + const availableSources = filteredSourceOrder || []; 180 + 181 + // If the last successful source is not disabled and exists in available sources, 182 + // move it to the front 183 + if ( 184 + !disabledSources.includes(lastSuccessfulSource) && 185 + availableSources.includes(lastSuccessfulSource) 186 + ) { 187 + filteredSourceOrder = [ 188 + lastSuccessfulSource, 189 + ...availableSources.filter((id) => id !== lastSuccessfulSource), 190 + ]; 191 + } 192 + } 169 193 170 194 // Filter out disabled embeds from the embed order 171 195 const filteredEmbedOrder = enableEmbedOrder ··· 223 247 startScrape, 224 248 preferredSourceOrder, 225 249 enableSourceOrder, 250 + lastSuccessfulSource, 251 + enableLastSuccessfulSource, 226 252 disabledSources, 227 253 preferredEmbedOrder, 228 254 enableEmbedOrder,
+28
src/hooks/useSettingsState.ts
··· 60 60 enableDetailsModal: boolean, 61 61 sourceOrder: string[], 62 62 enableSourceOrder: boolean, 63 + lastSuccessfulSource: string | null, 64 + enableLastSuccessfulSource: boolean, 63 65 disabledSources: string[], 64 66 embedOrder: string[], 65 67 enableEmbedOrder: boolean, ··· 164 166 resetEnableSourceOrder, 165 167 enableSourceOrderChanged, 166 168 ] = useDerived(enableSourceOrder); 169 + const [ 170 + lastSuccessfulSourceState, 171 + setLastSuccessfulSourceState, 172 + resetLastSuccessfulSource, 173 + lastSuccessfulSourceChanged, 174 + ] = useDerived(lastSuccessfulSource); 175 + const [ 176 + enableLastSuccessfulSourceState, 177 + setEnableLastSuccessfulSourceState, 178 + resetEnableLastSuccessfulSource, 179 + enableLastSuccessfulSourceChanged, 180 + ] = useDerived(enableLastSuccessfulSource); 167 181 const [ 168 182 disabledSourcesState, 169 183 setDisabledSourcesState, ··· 259 273 resetEnableImageLogos(); 260 274 resetSourceOrder(); 261 275 resetEnableSourceOrder(); 276 + resetLastSuccessfulSource(); 277 + resetEnableLastSuccessfulSource(); 262 278 resetDisabledSources(); 263 279 resetEmbedOrder(); 264 280 resetEnableEmbedOrder(); ··· 293 309 enableImageLogosChanged || 294 310 sourceOrderChanged || 295 311 enableSourceOrderChanged || 312 + lastSuccessfulSourceChanged || 313 + enableLastSuccessfulSourceChanged || 296 314 disabledSourcesChanged || 297 315 embedOrderChanged || 298 316 enableEmbedOrderChanged || ··· 399 417 state: enableSourceOrderState, 400 418 set: setEnableSourceOrderState, 401 419 changed: enableSourceOrderChanged, 420 + }, 421 + lastSuccessfulSource: { 422 + state: lastSuccessfulSourceState, 423 + set: setLastSuccessfulSourceState, 424 + changed: lastSuccessfulSourceChanged, 425 + }, 426 + enableLastSuccessfulSource: { 427 + state: enableLastSuccessfulSourceState, 428 + set: setEnableLastSuccessfulSourceState, 429 + changed: enableLastSuccessfulSourceChanged, 402 430 }, 403 431 proxyTmdb: { 404 432 state: proxyTmdbState,
+10
src/pages/PlayerView.tsx
··· 56 56 const manualSourceSelection = usePreferencesStore( 57 57 (s) => s.manualSourceSelection, 58 58 ); 59 + const setLastSuccessfulSource = usePreferencesStore( 60 + (s) => s.setLastSuccessfulSource, 61 + ); 59 62 const router = useOverlayRouter("settings"); 60 63 const openedWatchPartyRef = useRef<boolean>(false); 61 64 const progressItems = useProgressStore((s) => s.items); 65 + 66 + // Reset last successful source when leaving the player 67 + useEffect(() => { 68 + return () => { 69 + setLastSuccessfulSource(null); 70 + }; 71 + }, [setLastSuccessfulSource]); 62 72 63 73 const paramsData = JSON.stringify({ 64 74 media: params.media,
+26
src/pages/Settings.tsx
··· 282 282 (s) => s.setEnableSourceOrder, 283 283 ); 284 284 285 + const lastSuccessfulSource = usePreferencesStore( 286 + (s) => s.lastSuccessfulSource, 287 + ); 288 + const setLastSuccessfulSource = usePreferencesStore( 289 + (s) => s.setLastSuccessfulSource, 290 + ); 291 + 292 + const enableLastSuccessfulSource = usePreferencesStore( 293 + (s) => s.enableLastSuccessfulSource, 294 + ); 295 + const setEnableLastSuccessfulSource = usePreferencesStore( 296 + (s) => s.setEnableLastSuccessfulSource, 297 + ); 298 + 285 299 const disabledSources = usePreferencesStore((s) => s.disabledSources); 286 300 const setDisabledSources = usePreferencesStore((s) => s.setDisabledSources); 287 301 ··· 406 420 enableDetailsModal, 407 421 sourceOrder, 408 422 enableSourceOrder, 423 + lastSuccessfulSource, 424 + enableLastSuccessfulSource, 409 425 disabledSources, 410 426 embedOrder, 411 427 enableEmbedOrder, ··· 475 491 state.enableImageLogos.changed || 476 492 state.sourceOrder.changed || 477 493 state.enableSourceOrder.changed || 494 + state.lastSuccessfulSource.changed || 495 + state.enableLastSuccessfulSource.changed || 478 496 state.disabledSources.changed || 479 497 state.proxyTmdb.changed || 480 498 state.enableCarouselView.changed || ··· 500 518 enableImageLogos: state.enableImageLogos.state, 501 519 sourceOrder: state.sourceOrder.state, 502 520 enableSourceOrder: state.enableSourceOrder.state, 521 + lastSuccessfulSource: state.lastSuccessfulSource.state, 522 + enableLastSuccessfulSource: state.enableLastSuccessfulSource.state, 503 523 disabledSources: state.disabledSources.state, 504 524 proxyTmdb: state.proxyTmdb.state, 505 525 enableCarouselView: state.enableCarouselView.state, ··· 537 557 setEnableImageLogos(state.enableImageLogos.state); 538 558 setSourceOrder(state.sourceOrder.state); 539 559 setEnableSourceOrder(state.enableSourceOrder.state); 560 + setLastSuccessfulSource(state.lastSuccessfulSource.state); 561 + setEnableLastSuccessfulSource(state.enableLastSuccessfulSource.state); 540 562 setDisabledSources(state.disabledSources.state); 541 563 setAppLanguage(state.appLanguage.state); 542 564 setTheme(state.theme.state); ··· 584 606 setEnableImageLogos, 585 607 setSourceOrder, 586 608 setEnableSourceOrder, 609 + setLastSuccessfulSource, 610 + setEnableLastSuccessfulSource, 587 611 setDisabledSources, 588 612 setAppLanguage, 589 613 setTheme, ··· 650 674 setSourceOrder={state.sourceOrder.set} 651 675 enableSourceOrder={state.enableSourceOrder.state} 652 676 setenableSourceOrder={state.enableSourceOrder.set} 677 + enableLastSuccessfulSource={state.enableLastSuccessfulSource.state} 678 + setEnableLastSuccessfulSource={state.enableLastSuccessfulSource.set} 653 679 disabledSources={state.disabledSources.state} 654 680 setDisabledSources={state.disabledSources.set} 655 681 enableLowPerformanceMode={state.enableLowPerformanceMode.state}
+35 -2
src/pages/parts/player/SourceSelectPart.tsx
··· 129 129 const routerId = "manualSourceSelect"; 130 130 const preferredSourceOrder = usePreferencesStore((s) => s.sourceOrder); 131 131 const enableSourceOrder = usePreferencesStore((s) => s.enableSourceOrder); 132 + const lastSuccessfulSource = usePreferencesStore( 133 + (s) => s.lastSuccessfulSource, 134 + ); 135 + const enableLastSuccessfulSource = usePreferencesStore( 136 + (s) => s.enableLastSuccessfulSource, 137 + ); 132 138 133 139 const sources = useMemo(() => { 134 140 const metaType = props.media.type; ··· 138 144 .filter((v) => v.mediaTypes?.includes(metaType)); 139 145 140 146 if (!enableSourceOrder || preferredSourceOrder.length === 0) { 147 + // Even without custom source order, prioritize last successful source if enabled 148 + if (enableLastSuccessfulSource && lastSuccessfulSource) { 149 + const lastSourceIndex = allSources.findIndex( 150 + (s) => s.id === lastSuccessfulSource, 151 + ); 152 + if (lastSourceIndex !== -1) { 153 + const lastSource = allSources.splice(lastSourceIndex, 1)[0]; 154 + return [lastSource, ...allSources]; 155 + } 156 + } 141 157 return allSources; 142 158 } 143 159 144 - // Sort sources according to preferred order 160 + // Sort sources according to preferred order, but prioritize last successful source 145 161 const orderedSources = []; 146 162 const remainingSources = [...allSources]; 147 163 164 + // First, add the last successful source if it exists, is available, and the feature is enabled 165 + if (enableLastSuccessfulSource && lastSuccessfulSource) { 166 + const lastSourceIndex = remainingSources.findIndex( 167 + (s) => s.id === lastSuccessfulSource, 168 + ); 169 + if (lastSourceIndex !== -1) { 170 + orderedSources.push(remainingSources[lastSourceIndex]); 171 + remainingSources.splice(lastSourceIndex, 1); 172 + } 173 + } 174 + 148 175 // Add sources in preferred order 149 176 for (const sourceId of preferredSourceOrder) { 150 177 const sourceIndex = remainingSources.findIndex((s) => s.id === sourceId); ··· 158 185 orderedSources.push(...remainingSources); 159 186 160 187 return orderedSources; 161 - }, [props.media.type, preferredSourceOrder, enableSourceOrder]); 188 + }, [ 189 + props.media.type, 190 + preferredSourceOrder, 191 + enableSourceOrder, 192 + lastSuccessfulSource, 193 + enableLastSuccessfulSource, 194 + ]); 162 195 163 196 if (selectedSourceId) { 164 197 return (
+26
src/pages/parts/settings/PreferencesPart.tsx
··· 27 27 setSourceOrder: (v: string[]) => void; 28 28 enableSourceOrder: boolean; 29 29 setenableSourceOrder: (v: boolean) => void; 30 + enableLastSuccessfulSource: boolean; 31 + setEnableLastSuccessfulSource: (v: boolean) => void; 30 32 disabledSources: string[]; 31 33 setDisabledSources: (v: string[]) => void; 32 34 enableLowPerformanceMode: boolean; ··· 267 269 </p> 268 270 </div> 269 271 </div> 272 + 273 + {/* Last Successful Source Preference */} 274 + <div> 275 + <p className="text-white font-bold mb-3"> 276 + {t("settings.preferences.lastSuccessfulSource")} 277 + </p> 278 + <p className="max-w-[25rem] font-medium"> 279 + {t("settings.preferences.lastSuccessfulSourceDescription")} 280 + </p> 281 + <div 282 + onClick={() => 283 + props.setEnableLastSuccessfulSource( 284 + !props.enableLastSuccessfulSource, 285 + ) 286 + } 287 + 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" 288 + > 289 + <Toggle enabled={props.enableLastSuccessfulSource} /> 290 + <p className="flex-1 text-white font-bold"> 291 + {t("settings.preferences.lastSuccessfulSourceEnableLabel")} 292 + </p> 293 + </div> 294 + </div> 295 + 270 296 <p className="text-white font-bold"> 271 297 {t("settings.preferences.sourceOrder")} 272 298 </p>
+16
src/stores/preferences/index.tsx
··· 14 14 forceCompactEpisodeView: boolean; 15 15 sourceOrder: string[]; 16 16 enableSourceOrder: boolean; 17 + lastSuccessfulSource: string | null; 18 + enableLastSuccessfulSource: boolean; 17 19 disabledSources: string[]; 18 20 embedOrder: string[]; 19 21 enableEmbedOrder: boolean; ··· 39 41 setForceCompactEpisodeView(v: boolean): void; 40 42 setSourceOrder(v: string[]): void; 41 43 setEnableSourceOrder(v: boolean): void; 44 + setLastSuccessfulSource(v: string | null): void; 45 + setEnableLastSuccessfulSource(v: boolean): void; 42 46 setDisabledSources(v: string[]): void; 43 47 setEmbedOrder(v: string[]): void; 44 48 setEnableEmbedOrder(v: boolean): void; ··· 68 72 forceCompactEpisodeView: false, 69 73 sourceOrder: [], 70 74 enableSourceOrder: false, 75 + lastSuccessfulSource: null, 76 + enableLastSuccessfulSource: true, 71 77 disabledSources: [], 72 78 embedOrder: [], 73 79 enableEmbedOrder: false, ··· 134 140 setEnableSourceOrder(v) { 135 141 set((s) => { 136 142 s.enableSourceOrder = v; 143 + }); 144 + }, 145 + setLastSuccessfulSource(v) { 146 + set((s) => { 147 + s.lastSuccessfulSource = v; 148 + }); 149 + }, 150 + setEnableLastSuccessfulSource(v) { 151 + set((s) => { 152 + s.enableLastSuccessfulSource = v; 137 153 }); 138 154 }, 139 155 setDisabledSources(v) {