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

add Find Next Source button

replace edit order button

Pas 02a179b1 45e5abd0

+76 -14
+1 -1
src/assets/locales/en.json
··· 823 823 }, 824 824 "title": "Sources", 825 825 "unknownOption": "Unknown", 826 - "editOrder": "Edit order" 826 + "findNextSource": "Find next source" 827 827 }, 828 828 "subtitles": { 829 829 "customChoice": "Drop or upload file",
+27 -9
src/components/player/atoms/settings/SourceSelectingView.tsx
··· 10 10 import { Menu } from "@/components/player/internals/ContextMenu"; 11 11 import { SelectableLink } from "@/components/player/internals/ContextMenu/Links"; 12 12 import { useOverlayRouter } from "@/hooks/useOverlayRouter"; 13 + import { playerStatus } from "@/stores/player/slices/source"; 13 14 import { usePlayerStore } from "@/stores/player/store"; 14 15 import { usePreferencesStore } from "@/stores/preferences"; 15 16 ··· 156 157 const router = useOverlayRouter(id); 157 158 const metaType = usePlayerStore((s) => s.meta?.type); 158 159 const currentSourceId = usePlayerStore((s) => s.sourceId); 160 + const setResumeFromSourceId = usePlayerStore((s) => s.setResumeFromSourceId); 161 + const setStatus = usePlayerStore((s) => s.setStatus); 159 162 const preferredSourceOrder = usePreferencesStore((s) => s.sourceOrder); 160 163 const enableSourceOrder = usePreferencesStore((s) => s.enableSourceOrder); 161 164 const lastSuccessfulSource = usePreferencesStore( ··· 163 166 ); 164 167 const enableLastSuccessfulSource = usePreferencesStore( 165 168 (s) => s.enableLastSuccessfulSource, 169 + ); 170 + const manualSourceSelection = usePreferencesStore( 171 + (s) => s.manualSourceSelection, 166 172 ); 167 173 168 174 const sources = useMemo(() => { ··· 221 227 enableLastSuccessfulSource, 222 228 ]); 223 229 230 + const handleFindNextSource = () => { 231 + if (!currentSourceId) return; 232 + // Set the resume source ID in the store 233 + setResumeFromSourceId(currentSourceId); 234 + // Close the settings overlay 235 + router.close(); 236 + // Set status to SCRAPING to trigger scraping from next source 237 + setStatus(playerStatus.SCRAPING); 238 + }; 239 + 224 240 return ( 225 241 <> 226 242 <Menu.BackLink 227 243 onClick={() => router.navigate("/")} 228 244 rightSide={ 229 - <button 230 - type="button" 231 - onClick={() => { 232 - window.location.href = "/settings#source-order"; 233 - }} 234 - className="-mr-2 -my-1 px-2 p-[0.4em] rounded tabbable hover:bg-video-context-light hover:bg-opacity-10" 235 - > 236 - {t("player.menus.sources.editOrder")} 237 - </button> 245 + <div className="flex items-center gap-2"> 246 + {currentSourceId && !manualSourceSelection && ( 247 + <button 248 + type="button" 249 + onClick={handleFindNextSource} 250 + className="-mr-2 -my-1 px-2 p-[0.4em] rounded tabbable hover:bg-video-context-light hover:bg-opacity-10" 251 + > 252 + {t("player.menus.sources.findNextSource")} 253 + </button> 254 + )} 255 + </div> 238 256 } 239 257 > 240 258 {t("player.menus.sources.title")}
+7 -1
src/hooks/useProviderScrape.tsx
··· 211 211 } 212 212 213 213 // If we have a last successful source and the feature is enabled, prioritize it 214 - if (enableLastSuccessfulSource && lastSuccessfulSource) { 214 + // BUT only if we're not resuming from a specific source (to preserve custom order) 215 + if ( 216 + enableLastSuccessfulSource && 217 + lastSuccessfulSource && 218 + !startFromSourceId 219 + ) { 215 220 const lastSourceIndex = baseSourceOrder.indexOf(lastSuccessfulSource); 216 221 if (lastSourceIndex !== -1) { 217 222 baseSourceOrder = [ ··· 222 227 } 223 228 224 229 // If starting from a specific source ID, filter the order to start AFTER that source 230 + // This preserves the custom order while starting from the next source 225 231 let filteredSourceOrder = baseSourceOrder; 226 232 if (startFromSourceId) { 227 233 const startIndex = filteredSourceOrder.indexOf(startFromSourceId);
+32 -3
src/pages/PlayerView.tsx
··· 47 47 const [resumeFromSourceId, setResumeFromSourceId] = useState<string | null>( 48 48 null, 49 49 ); 50 + const storeResumeFromSourceId = usePlayerStore((s) => s.resumeFromSourceId); 51 + const setResumeFromSourceIdInStore = usePlayerStore( 52 + (s) => s.setResumeFromSourceId, 53 + ); 50 54 const [startAtParam] = useQueryParam("t"); 51 55 const { 52 56 status, ··· 76 80 setLastSuccessfulSource(null); 77 81 }; 78 82 }, [setLastSuccessfulSource]); 83 + 84 + // Reset resume from source ID when leaving the player 85 + useEffect(() => { 86 + return () => { 87 + setResumeFromSourceId(null); 88 + setResumeFromSourceIdInStore(null); 89 + }; 90 + }, [setResumeFromSourceIdInStore]); 79 91 80 92 const paramsData = JSON.stringify({ 81 93 media: params.media, ··· 169 181 (startFromSourceId: string) => { 170 182 // Set resume source first 171 183 setResumeFromSourceId(startFromSourceId); 184 + setResumeFromSourceIdInStore(startFromSourceId); 172 185 // Then change status in next tick to ensure re-render 173 186 setTimeout(() => { 174 187 setStatus(playerStatus.SCRAPING); 175 188 }, 0); 176 189 }, 177 - [setStatus], 190 + [setStatus, setResumeFromSourceIdInStore], 178 191 ); 179 192 193 + // Sync store value to local state when it changes (e.g., from settings) 194 + // or when status changes to SCRAPING 195 + useEffect(() => { 196 + if (storeResumeFromSourceId && status === playerStatus.SCRAPING) { 197 + if ( 198 + !resumeFromSourceId || 199 + resumeFromSourceId !== storeResumeFromSourceId 200 + ) { 201 + setResumeFromSourceId(storeResumeFromSourceId); 202 + } 203 + } 204 + }, [storeResumeFromSourceId, resumeFromSourceId, status]); 205 + 180 206 const playAfterScrape = useCallback( 181 207 (out: RunOutput | null) => { 182 208 if (!out) return; ··· 223 249 <SourceSelectPart media={scrapeMedia} /> 224 250 ) : ( 225 251 <ScrapingPart 226 - key={`scraping-${resumeFromSourceId || "default"}`} 252 + key={`scraping-${resumeFromSourceId || storeResumeFromSourceId || "default"}`} 227 253 media={scrapeMedia} 228 - startFromSourceId={resumeFromSourceId || undefined} 254 + startFromSourceId={ 255 + resumeFromSourceId || storeResumeFromSourceId || undefined 256 + } 229 257 onResult={(sources, sourceOrder) => { 230 258 setErrorData({ 231 259 sourceOrder, ··· 234 262 setScrapeNotFound(); 235 263 // Clear resume state after scraping 236 264 setResumeFromSourceId(null); 265 + setResumeFromSourceIdInStore(null); 237 266 }} 238 267 onGetStream={playAfterScrape} 239 268 />
+9
src/stores/player/slices/source.ts
··· 105 105 meta: PlayerMeta | null; 106 106 failedSourcesPerMedia: Record<string, string[]>; // mediaKey -> array of failed sourceIds 107 107 failedEmbedsPerMedia: Record<string, Record<string, string[]>>; // mediaKey -> sourceId -> array of failed embedIds 108 + resumeFromSourceId: string | null; 108 109 setStatus(status: PlayerStatus): void; 109 110 setSource( 110 111 stream: SourceSliceSource, ··· 129 130 addFailedEmbed(sourceId: string, embedId: string): void; 130 131 clearFailedSources(mediaKey?: string): void; 131 132 clearFailedEmbeds(mediaKey?: string): void; 133 + setResumeFromSourceId(sourceId: string | null): void; 132 134 reset(): void; 133 135 } 134 136 ··· 190 192 meta: null, 191 193 failedSourcesPerMedia: {}, 192 194 failedEmbedsPerMedia: {}, 195 + resumeFromSourceId: null, 193 196 caption: { 194 197 selected: null, 195 198 asTrack: false, ··· 387 390 } 388 391 }); 389 392 }, 393 + setResumeFromSourceId(sourceId: string | null) { 394 + set((s) => { 395 + s.resumeFromSourceId = sourceId; 396 + }); 397 + }, 390 398 reset() { 391 399 set((s) => { 392 400 s.source = null; ··· 402 410 s.meta = null; 403 411 s.failedSourcesPerMedia = {}; 404 412 s.failedEmbedsPerMedia = {}; 413 + s.resumeFromSourceId = null; 405 414 this.clearTranslateTask(); 406 415 s.caption = { 407 416 selected: null,