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.

refactor media failure tracking

Pas f68bb15c 8a622f03

+186 -16
+21 -3
src/hooks/useProviderScrape.tsx
··· 175 175 const failedSources = playerState.failedSources; 176 176 const failedEmbeds = playerState.failedEmbeds; 177 177 178 + // Get media-specific failures 179 + const mediaFailureKey = { 180 + type: media.type, 181 + tmdbId: media.tmdbId, 182 + ...(media.type === "show" && media.episode && media.season 183 + ? { 184 + seasonNumber: media.season.number, 185 + episodeNumber: media.episode.number, 186 + } 187 + : {}), 188 + }; 189 + const mediaFailures = playerState.getMediaFailures(mediaFailureKey); 190 + const mediaFailedSources = mediaFailures.failedSources; 191 + const mediaFailedEmbeds = mediaFailures.failedEmbeds; 192 + 178 193 // Start with all available sources (filtered by disabled and failed ones) 179 194 let baseSourceOrder = allSources 180 195 .filter( 181 196 (source) => 182 197 !(disabledSources || []).includes(source.id) && 183 - !failedSources.includes(source.id), 198 + !failedSources.includes(source.id) && 199 + !mediaFailedSources.includes(source.id), 184 200 ) 185 201 .map((source) => source.id); 186 202 ··· 222 238 } 223 239 } 224 240 225 - // Collect all failed embed IDs across all sources 241 + // Collect all failed embed IDs across all sources (both global and media-specific) 226 242 const allFailedEmbedIds = Object.values(failedEmbeds).flat(); 243 + const allMediaFailedEmbedIds = Object.values(mediaFailedEmbeds).flat(); 227 244 228 245 // Filter out disabled and failed embeds from the embed order 229 246 const filteredEmbedOrder = enableEmbedOrder 230 247 ? (preferredEmbedOrder || []).filter( 231 248 (id) => 232 249 !(disabledEmbeds || []).includes(id) && 233 - !allFailedEmbedIds.includes(id), 250 + !allFailedEmbedIds.includes(id) && 251 + !allMediaFailedEmbedIds.includes(id), 234 252 ) 235 253 : undefined; 236 254
+29 -11
src/pages/parts/player/PlaybackErrorPart.tsx
··· 24 24 const playbackError = usePlayerStore((s) => s.interface.error); 25 25 const currentSourceId = usePlayerStore((s) => s.sourceId); 26 26 const currentEmbedId = usePlayerStore((s) => s.embedId); 27 - const addFailedSource = usePlayerStore((s) => s.addFailedSource); 28 - const addFailedEmbed = usePlayerStore((s) => s.addFailedEmbed); 29 - const failedEmbeds = usePlayerStore((s) => s.failedEmbeds); 27 + const meta = usePlayerStore((s) => s.meta); 28 + const addMediaFailedSource = usePlayerStore((s) => s.addMediaFailedSource); 29 + const addMediaFailedEmbed = usePlayerStore((s) => s.addMediaFailedEmbed); 30 + const getMediaFailures = usePlayerStore((s) => s.getMediaFailures); 30 31 const modal = useModal("error"); 31 32 const settingsRouter = useOverlayRouter("settings"); 32 33 const hasOpenedSettings = useRef(false); ··· 40 41 41 42 // Mark the failed source/embed and handle UI when a playback error occurs 42 43 useEffect(() => { 43 - if (playbackError && currentSourceId) { 44 + if (playbackError && currentSourceId && meta) { 44 45 // Only mark source/embed as failed for fatal errors 45 46 const isFatalError = 46 47 playbackError.type === "hls" ··· 48 49 : playbackError.type === "htmlvideo"; 49 50 50 51 if (isFatalError) { 52 + // Create media failure key 53 + const mediaFailureKey = { 54 + type: meta.type, 55 + tmdbId: meta.tmdbId, 56 + ...(meta.type === "show" && meta.episode && meta.season 57 + ? { 58 + seasonNumber: meta.season.number, 59 + episodeNumber: meta.episode.number, 60 + } 61 + : {}), 62 + }; 63 + 64 + // Get current media failures 65 + const mediaFailures = getMediaFailures(mediaFailureKey); 66 + 51 67 // If there's an active embed, disable that embed instead of the source 52 68 if (currentEmbedId) { 53 - addFailedEmbed(currentSourceId, currentEmbedId); 69 + addMediaFailedEmbed(mediaFailureKey, currentSourceId, currentEmbedId); 54 70 55 71 // Check if all embeds for this source have now failed 56 72 // If so, disable the entire source 57 - const failedEmbedsForSource = failedEmbeds[currentSourceId] || []; 73 + const failedEmbedsForSource = 74 + mediaFailures.failedEmbeds[currentSourceId] || []; 58 75 // For now, we'll assume if we have 2+ failed embeds for a source, disable it 59 76 // This is a simple heuristic - we could make it more sophisticated 60 77 if (failedEmbedsForSource.length >= 2) { 61 - addFailedSource(currentSourceId); 78 + addMediaFailedSource(mediaFailureKey, currentSourceId); 62 79 } 63 80 } else { 64 81 // No embed active, disable the source 65 - addFailedSource(currentSourceId); 82 + addMediaFailedSource(mediaFailureKey, currentSourceId); 66 83 } 67 84 } 68 85 ··· 78 95 playbackError, 79 96 currentSourceId, 80 97 currentEmbedId, 81 - failedEmbeds, 82 - addFailedSource, 83 - addFailedEmbed, 98 + meta, 99 + getMediaFailures, 100 + addMediaFailedSource, 101 + addMediaFailedEmbed, 84 102 settingsRouter, 85 103 setLastSuccessfulSource, 86 104 enableAutoResumeOnPlaybackError,
+5 -1
src/setup/App.tsx
··· 41 41 import { SupportPage } from "@/pages/Support"; 42 42 import { Layout } from "@/setup/Layout"; 43 43 import { useHistoryListener } from "@/stores/history"; 44 - import { useClearModalsOnNavigation } from "@/stores/interface/overlayStack"; 44 + import { 45 + useClearMediaFailuresOnNavigation, 46 + useClearModalsOnNavigation, 47 + } from "@/stores/interface/overlayStack"; 45 48 import { LanguageProvider } from "@/stores/language"; 46 49 47 50 const DeveloperPage = lazy(() => import("@/pages/DeveloperPage")); ··· 107 110 useOnlineListener(); 108 111 useGlobalKeyboardEvents(); 109 112 useClearModalsOnNavigation(); 113 + useClearMediaFailuresOnNavigation(); 110 114 const maintenance = false; // Shows maintance page 111 115 const [showDowntime, setShowDowntime] = useState(maintenance); 112 116
+14
src/stores/interface/overlayStack.ts
··· 3 3 import { create } from "zustand"; 4 4 import { immer } from "zustand/middleware/immer"; 5 5 6 + import { usePlayerStore } from "@/stores/player/store"; 7 + 6 8 type OverlayType = "volume" | "subtitle" | "speed" | null; 7 9 8 10 interface ModalData { ··· 75 77 clearAllModals(); 76 78 }, [location.pathname, clearAllModals]); 77 79 } 80 + 81 + // Hook to clear media failures on navigation 82 + export function useClearMediaFailuresOnNavigation() { 83 + const location = useLocation(); 84 + const clearAllMediaFailures = usePlayerStore( 85 + (state) => state.clearAllMediaFailures, 86 + ); 87 + 88 + useEffect(() => { 89 + clearAllMediaFailures(); 90 + }, [location.pathname, clearAllMediaFailures]); 91 + }
+112
src/stores/player/slices/mediaFailures.ts
··· 1 + import { MakeSlice } from "@/stores/player/slices/types"; 2 + 3 + export interface MediaFailureKey { 4 + type: "movie" | "show"; 5 + tmdbId: string; 6 + seasonNumber?: number; 7 + episodeNumber?: number; 8 + } 9 + 10 + export interface MediaFailuresSlice { 11 + mediaFailures: Record< 12 + string, 13 + { 14 + failedSources: string[]; 15 + failedEmbeds: Record<string, string[]>; 16 + } 17 + >; 18 + getMediaFailureKey(meta: MediaFailureKey): string; 19 + getMediaFailures(meta: MediaFailureKey): { 20 + failedSources: string[]; 21 + failedEmbeds: Record<string, string[]>; 22 + }; 23 + addMediaFailedSource(meta: MediaFailureKey, sourceId: string): void; 24 + addMediaFailedEmbed( 25 + meta: MediaFailureKey, 26 + sourceId: string, 27 + embedId: string, 28 + ): void; 29 + clearMediaFailures(meta: MediaFailureKey): void; 30 + clearAllMediaFailures(): void; 31 + reset(): void; 32 + } 33 + 34 + function createMediaFailureKey(meta: MediaFailureKey): string { 35 + const baseKey = `${meta.type}-${meta.tmdbId}`; 36 + if ( 37 + meta.type === "show" && 38 + meta.seasonNumber !== undefined && 39 + meta.episodeNumber !== undefined 40 + ) { 41 + return `${baseKey}-s${meta.seasonNumber}e${meta.episodeNumber}`; 42 + } 43 + return baseKey; 44 + } 45 + 46 + export const createMediaFailuresSlice: MakeSlice<MediaFailuresSlice> = ( 47 + set, 48 + get, 49 + ) => ({ 50 + mediaFailures: {}, 51 + 52 + getMediaFailureKey(meta) { 53 + return createMediaFailureKey(meta); 54 + }, 55 + 56 + getMediaFailures(meta) { 57 + const key = createMediaFailureKey(meta); 58 + return get().mediaFailures[key] || { failedSources: [], failedEmbeds: {} }; 59 + }, 60 + 61 + addMediaFailedSource(meta, sourceId) { 62 + const key = createMediaFailureKey(meta); 63 + set((s) => { 64 + if (!s.mediaFailures[key]) { 65 + s.mediaFailures[key] = { failedSources: [], failedEmbeds: {} }; 66 + } 67 + if (!s.mediaFailures[key].failedSources.includes(sourceId)) { 68 + s.mediaFailures[key].failedSources = [ 69 + ...s.mediaFailures[key].failedSources, 70 + sourceId, 71 + ]; 72 + } 73 + }); 74 + }, 75 + 76 + addMediaFailedEmbed(meta, sourceId, embedId) { 77 + const key = createMediaFailureKey(meta); 78 + set((s) => { 79 + if (!s.mediaFailures[key]) { 80 + s.mediaFailures[key] = { failedSources: [], failedEmbeds: {} }; 81 + } 82 + if (!s.mediaFailures[key].failedEmbeds[sourceId]) { 83 + s.mediaFailures[key].failedEmbeds[sourceId] = []; 84 + } 85 + if (!s.mediaFailures[key].failedEmbeds[sourceId].includes(embedId)) { 86 + s.mediaFailures[key].failedEmbeds[sourceId] = [ 87 + ...s.mediaFailures[key].failedEmbeds[sourceId], 88 + embedId, 89 + ]; 90 + } 91 + }); 92 + }, 93 + 94 + clearMediaFailures(meta) { 95 + const key = createMediaFailureKey(meta); 96 + set((s) => { 97 + delete s.mediaFailures[key]; 98 + }); 99 + }, 100 + 101 + clearAllMediaFailures() { 102 + set((s) => { 103 + s.mediaFailures = {}; 104 + }); 105 + }, 106 + 107 + reset() { 108 + set((s) => { 109 + s.mediaFailures = {}; 110 + }); 111 + }, 112 + });
+3 -1
src/stores/player/slices/types.ts
··· 3 3 import { CastingSlice } from "@/stores/player/slices/casting"; 4 4 import { DisplaySlice } from "@/stores/player/slices/display"; 5 5 import { InterfaceSlice } from "@/stores/player/slices/interface"; 6 + import { MediaFailuresSlice } from "@/stores/player/slices/mediaFailures"; 6 7 import { PlayingSlice } from "@/stores/player/slices/playing"; 7 8 import { ProgressSlice } from "@/stores/player/slices/progress"; 8 9 import { SourceSlice } from "@/stores/player/slices/source"; ··· 14 15 SourceSlice & 15 16 DisplaySlice & 16 17 CastingSlice & 17 - ThumbnailSlice; 18 + ThumbnailSlice & 19 + MediaFailuresSlice; 18 20 export type MakeSlice<Slice> = StateCreator< 19 21 AllSlices, 20 22 [["zustand/immer", never]],
+2
src/stores/player/store.ts
··· 4 4 import { createCastingSlice } from "@/stores/player/slices/casting"; 5 5 import { createDisplaySlice } from "@/stores/player/slices/display"; 6 6 import { createInterfaceSlice } from "@/stores/player/slices/interface"; 7 + import { createMediaFailuresSlice } from "@/stores/player/slices/mediaFailures"; 7 8 import { createPlayingSlice } from "@/stores/player/slices/playing"; 8 9 import { createProgressSlice } from "@/stores/player/slices/progress"; 9 10 import { createSourceSlice } from "@/stores/player/slices/source"; ··· 19 20 ...createDisplaySlice(...a), 20 21 ...createCastingSlice(...a), 21 22 ...createThumbnailSlice(...a), 23 + ...createMediaFailuresSlice(...a), 22 24 })), 23 25 );