diff --git a/src/hooks/useProviderScrape.tsx b/src/hooks/useProviderScrape.tsx index d208b82c..9191b1dc 100644 --- a/src/hooks/useProviderScrape.tsx +++ b/src/hooks/useProviderScrape.tsx @@ -175,12 +175,28 @@ export function useScrape() { const failedSources = playerState.failedSources; const failedEmbeds = playerState.failedEmbeds; + // Get media-specific failures + const mediaFailureKey = { + type: media.type, + tmdbId: media.tmdbId, + ...(media.type === "show" && media.episode && media.season + ? { + seasonNumber: media.season.number, + episodeNumber: media.episode.number, + } + : {}), + }; + const mediaFailures = playerState.getMediaFailures(mediaFailureKey); + const mediaFailedSources = mediaFailures.failedSources; + const mediaFailedEmbeds = mediaFailures.failedEmbeds; + // Start with all available sources (filtered by disabled and failed ones) let baseSourceOrder = allSources .filter( (source) => !(disabledSources || []).includes(source.id) && - !failedSources.includes(source.id), + !failedSources.includes(source.id) && + !mediaFailedSources.includes(source.id), ) .map((source) => source.id); @@ -222,15 +238,17 @@ export function useScrape() { } } - // Collect all failed embed IDs across all sources + // Collect all failed embed IDs across all sources (both global and media-specific) const allFailedEmbedIds = Object.values(failedEmbeds).flat(); + const allMediaFailedEmbedIds = Object.values(mediaFailedEmbeds).flat(); // Filter out disabled and failed embeds from the embed order const filteredEmbedOrder = enableEmbedOrder ? (preferredEmbedOrder || []).filter( (id) => !(disabledEmbeds || []).includes(id) && - !allFailedEmbedIds.includes(id), + !allFailedEmbedIds.includes(id) && + !allMediaFailedEmbedIds.includes(id), ) : undefined; diff --git a/src/pages/parts/player/PlaybackErrorPart.tsx b/src/pages/parts/player/PlaybackErrorPart.tsx index dfc90c55..e03692c9 100644 --- a/src/pages/parts/player/PlaybackErrorPart.tsx +++ b/src/pages/parts/player/PlaybackErrorPart.tsx @@ -24,9 +24,10 @@ export function PlaybackErrorPart(props: PlaybackErrorPartProps) { const playbackError = usePlayerStore((s) => s.interface.error); const currentSourceId = usePlayerStore((s) => s.sourceId); const currentEmbedId = usePlayerStore((s) => s.embedId); - const addFailedSource = usePlayerStore((s) => s.addFailedSource); - const addFailedEmbed = usePlayerStore((s) => s.addFailedEmbed); - const failedEmbeds = usePlayerStore((s) => s.failedEmbeds); + const meta = usePlayerStore((s) => s.meta); + const addMediaFailedSource = usePlayerStore((s) => s.addMediaFailedSource); + const addMediaFailedEmbed = usePlayerStore((s) => s.addMediaFailedEmbed); + const getMediaFailures = usePlayerStore((s) => s.getMediaFailures); const modal = useModal("error"); const settingsRouter = useOverlayRouter("settings"); const hasOpenedSettings = useRef(false); @@ -40,7 +41,7 @@ export function PlaybackErrorPart(props: PlaybackErrorPartProps) { // Mark the failed source/embed and handle UI when a playback error occurs useEffect(() => { - if (playbackError && currentSourceId) { + if (playbackError && currentSourceId && meta) { // Only mark source/embed as failed for fatal errors const isFatalError = playbackError.type === "hls" @@ -48,21 +49,37 @@ export function PlaybackErrorPart(props: PlaybackErrorPartProps) { : playbackError.type === "htmlvideo"; if (isFatalError) { + // Create media failure key + const mediaFailureKey = { + type: meta.type, + tmdbId: meta.tmdbId, + ...(meta.type === "show" && meta.episode && meta.season + ? { + seasonNumber: meta.season.number, + episodeNumber: meta.episode.number, + } + : {}), + }; + + // Get current media failures + const mediaFailures = getMediaFailures(mediaFailureKey); + // If there's an active embed, disable that embed instead of the source if (currentEmbedId) { - addFailedEmbed(currentSourceId, currentEmbedId); + addMediaFailedEmbed(mediaFailureKey, currentSourceId, currentEmbedId); // Check if all embeds for this source have now failed // If so, disable the entire source - const failedEmbedsForSource = failedEmbeds[currentSourceId] || []; + const failedEmbedsForSource = + mediaFailures.failedEmbeds[currentSourceId] || []; // For now, we'll assume if we have 2+ failed embeds for a source, disable it // This is a simple heuristic - we could make it more sophisticated if (failedEmbedsForSource.length >= 2) { - addFailedSource(currentSourceId); + addMediaFailedSource(mediaFailureKey, currentSourceId); } } else { // No embed active, disable the source - addFailedSource(currentSourceId); + addMediaFailedSource(mediaFailureKey, currentSourceId); } } @@ -78,9 +95,10 @@ export function PlaybackErrorPart(props: PlaybackErrorPartProps) { playbackError, currentSourceId, currentEmbedId, - failedEmbeds, - addFailedSource, - addFailedEmbed, + meta, + getMediaFailures, + addMediaFailedSource, + addMediaFailedEmbed, settingsRouter, setLastSuccessfulSource, enableAutoResumeOnPlaybackError, diff --git a/src/setup/App.tsx b/src/setup/App.tsx index ff6bf327..ad6c0b19 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -41,7 +41,10 @@ import { RegisterPage } from "@/pages/Register"; import { SupportPage } from "@/pages/Support"; import { Layout } from "@/setup/Layout"; import { useHistoryListener } from "@/stores/history"; -import { useClearModalsOnNavigation } from "@/stores/interface/overlayStack"; +import { + useClearMediaFailuresOnNavigation, + useClearModalsOnNavigation, +} from "@/stores/interface/overlayStack"; import { LanguageProvider } from "@/stores/language"; const DeveloperPage = lazy(() => import("@/pages/DeveloperPage")); @@ -107,6 +110,7 @@ function App() { useOnlineListener(); useGlobalKeyboardEvents(); useClearModalsOnNavigation(); + useClearMediaFailuresOnNavigation(); const maintenance = false; // Shows maintance page const [showDowntime, setShowDowntime] = useState(maintenance); diff --git a/src/stores/interface/overlayStack.ts b/src/stores/interface/overlayStack.ts index c84a9f90..e2b11f84 100644 --- a/src/stores/interface/overlayStack.ts +++ b/src/stores/interface/overlayStack.ts @@ -3,6 +3,8 @@ import { useLocation } from "react-router-dom"; import { create } from "zustand"; import { immer } from "zustand/middleware/immer"; +import { usePlayerStore } from "@/stores/player/store"; + type OverlayType = "volume" | "subtitle" | "speed" | null; interface ModalData { @@ -75,3 +77,15 @@ export function useClearModalsOnNavigation() { clearAllModals(); }, [location.pathname, clearAllModals]); } + +// Hook to clear media failures on navigation +export function useClearMediaFailuresOnNavigation() { + const location = useLocation(); + const clearAllMediaFailures = usePlayerStore( + (state) => state.clearAllMediaFailures, + ); + + useEffect(() => { + clearAllMediaFailures(); + }, [location.pathname, clearAllMediaFailures]); +} diff --git a/src/stores/player/slices/mediaFailures.ts b/src/stores/player/slices/mediaFailures.ts new file mode 100644 index 00000000..40d48026 --- /dev/null +++ b/src/stores/player/slices/mediaFailures.ts @@ -0,0 +1,112 @@ +import { MakeSlice } from "@/stores/player/slices/types"; + +export interface MediaFailureKey { + type: "movie" | "show"; + tmdbId: string; + seasonNumber?: number; + episodeNumber?: number; +} + +export interface MediaFailuresSlice { + mediaFailures: Record< + string, + { + failedSources: string[]; + failedEmbeds: Record; + } + >; + getMediaFailureKey(meta: MediaFailureKey): string; + getMediaFailures(meta: MediaFailureKey): { + failedSources: string[]; + failedEmbeds: Record; + }; + addMediaFailedSource(meta: MediaFailureKey, sourceId: string): void; + addMediaFailedEmbed( + meta: MediaFailureKey, + sourceId: string, + embedId: string, + ): void; + clearMediaFailures(meta: MediaFailureKey): void; + clearAllMediaFailures(): void; + reset(): void; +} + +function createMediaFailureKey(meta: MediaFailureKey): string { + const baseKey = `${meta.type}-${meta.tmdbId}`; + if ( + meta.type === "show" && + meta.seasonNumber !== undefined && + meta.episodeNumber !== undefined + ) { + return `${baseKey}-s${meta.seasonNumber}e${meta.episodeNumber}`; + } + return baseKey; +} + +export const createMediaFailuresSlice: MakeSlice = ( + set, + get, +) => ({ + mediaFailures: {}, + + getMediaFailureKey(meta) { + return createMediaFailureKey(meta); + }, + + getMediaFailures(meta) { + const key = createMediaFailureKey(meta); + return get().mediaFailures[key] || { failedSources: [], failedEmbeds: {} }; + }, + + addMediaFailedSource(meta, sourceId) { + const key = createMediaFailureKey(meta); + set((s) => { + if (!s.mediaFailures[key]) { + s.mediaFailures[key] = { failedSources: [], failedEmbeds: {} }; + } + if (!s.mediaFailures[key].failedSources.includes(sourceId)) { + s.mediaFailures[key].failedSources = [ + ...s.mediaFailures[key].failedSources, + sourceId, + ]; + } + }); + }, + + addMediaFailedEmbed(meta, sourceId, embedId) { + const key = createMediaFailureKey(meta); + set((s) => { + if (!s.mediaFailures[key]) { + s.mediaFailures[key] = { failedSources: [], failedEmbeds: {} }; + } + if (!s.mediaFailures[key].failedEmbeds[sourceId]) { + s.mediaFailures[key].failedEmbeds[sourceId] = []; + } + if (!s.mediaFailures[key].failedEmbeds[sourceId].includes(embedId)) { + s.mediaFailures[key].failedEmbeds[sourceId] = [ + ...s.mediaFailures[key].failedEmbeds[sourceId], + embedId, + ]; + } + }); + }, + + clearMediaFailures(meta) { + const key = createMediaFailureKey(meta); + set((s) => { + delete s.mediaFailures[key]; + }); + }, + + clearAllMediaFailures() { + set((s) => { + s.mediaFailures = {}; + }); + }, + + reset() { + set((s) => { + s.mediaFailures = {}; + }); + }, +}); diff --git a/src/stores/player/slices/types.ts b/src/stores/player/slices/types.ts index 6f358945..b1edac5d 100644 --- a/src/stores/player/slices/types.ts +++ b/src/stores/player/slices/types.ts @@ -3,6 +3,7 @@ import { StateCreator } from "zustand"; import { CastingSlice } from "@/stores/player/slices/casting"; import { DisplaySlice } from "@/stores/player/slices/display"; import { InterfaceSlice } from "@/stores/player/slices/interface"; +import { MediaFailuresSlice } from "@/stores/player/slices/mediaFailures"; import { PlayingSlice } from "@/stores/player/slices/playing"; import { ProgressSlice } from "@/stores/player/slices/progress"; import { SourceSlice } from "@/stores/player/slices/source"; @@ -14,7 +15,8 @@ export type AllSlices = InterfaceSlice & SourceSlice & DisplaySlice & CastingSlice & - ThumbnailSlice; + ThumbnailSlice & + MediaFailuresSlice; export type MakeSlice = StateCreator< AllSlices, [["zustand/immer", never]], diff --git a/src/stores/player/store.ts b/src/stores/player/store.ts index 2235c30f..34d97637 100644 --- a/src/stores/player/store.ts +++ b/src/stores/player/store.ts @@ -4,6 +4,7 @@ import { immer } from "zustand/middleware/immer"; import { createCastingSlice } from "@/stores/player/slices/casting"; import { createDisplaySlice } from "@/stores/player/slices/display"; import { createInterfaceSlice } from "@/stores/player/slices/interface"; +import { createMediaFailuresSlice } from "@/stores/player/slices/mediaFailures"; import { createPlayingSlice } from "@/stores/player/slices/playing"; import { createProgressSlice } from "@/stores/player/slices/progress"; import { createSourceSlice } from "@/stores/player/slices/source"; @@ -19,5 +20,6 @@ export const usePlayerStore = create( ...createDisplaySlice(...a), ...createCastingSlice(...a), ...createThumbnailSlice(...a), + ...createMediaFailuresSlice(...a), })), );