From c460c159665a9ad8bd597c556c669e862dd93dcd Mon Sep 17 00:00:00 2001 From: Pas <74743263+Pasithea0@users.noreply.github.com> Date: Sun, 30 Nov 2025 17:50:28 -0700 Subject: [PATCH] Track and skip failed sources during playback Introduces a mechanism to track failed sources in the player store. When a playback error occurs, the current source is marked as failed and subsequent attempts will skip these sources. Failed sources are cleared when a working source is found. UI text is updated to reflect the new behavior. --- src/assets/locales/en.json | 4 +-- src/hooks/useProviderScrape.tsx | 10 ++++-- src/pages/PlayerView.tsx | 4 +++ src/pages/parts/player/PlaybackErrorPart.tsx | 27 ++++++++------ src/stores/player/slices/source.ts | 37 ++++++++++++++++++++ 5 files changed, 67 insertions(+), 15 deletions(-) diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index 08e2b1b5..0c603511 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -841,11 +841,11 @@ "errorNetwork": "Some kind of network error occurred which prevented the media from being successfully fetched, despite having previously been available.", "errorNotSupported": "The media or media provider object is not supported." }, - "autoResumeText": "There was an error trying to play the media 😖. Automatically trying the next source...", + "autoResumeText": "There was an error trying to play the media 😖. Automatically trying the other sources...", "copyDebugInfo": "Copy debug info", "debugInfo": "Check console for more details.", "homeButton": "Go home", - "resumeButton": "Try next source", + "resumeButton": "Try next sources", "text": "There was an error trying to play the media 😖. Please try again or try a different source!", "title": "Failed to play video!" }, diff --git a/src/hooks/useProviderScrape.tsx b/src/hooks/useProviderScrape.tsx index 6123dbc9..57e4aed5 100644 --- a/src/hooks/useProviderScrape.tsx +++ b/src/hooks/useProviderScrape.tsx @@ -10,6 +10,7 @@ import { } from "@/backend/helpers/providerApi"; import { getLoadbalancedProviderApiUrl } from "@/backend/providers/fetchers"; import { getProviders } from "@/backend/providers/providers"; +import { usePlayerStore } from "@/stores/player/store"; import { usePreferencesStore } from "@/stores/preferences"; export interface ScrapingItems { @@ -170,10 +171,15 @@ export function useScrape() { async (media: ScrapeMedia, startFromSourceId?: string) => { const providerInstance = getProviders(); const allSources = providerInstance.listSources(); + const failedSources = usePlayerStore.getState().failedSources; - // Start with all available sources (filtered by disabled ones) + // Start with all available sources (filtered by disabled and failed ones) let baseSourceOrder = allSources - .filter((source) => !disabledSources.includes(source.id)) + .filter( + (source) => + !disabledSources.includes(source.id) && + !failedSources.includes(source.id), + ) .map((source) => source.id); // Apply custom source ordering if enabled diff --git a/src/pages/PlayerView.tsx b/src/pages/PlayerView.tsx index f6734991..76759197 100644 --- a/src/pages/PlayerView.tsx +++ b/src/pages/PlayerView.tsx @@ -182,6 +182,10 @@ export function RealPlayerView() { let startAt: number | undefined; if (startAtParam) startAt = parseTimestamp(startAtParam) ?? undefined; + // Clear failed sources when we successfully find a working source + const playerStore = usePlayerStore.getState(); + playerStore.clearFailedSources(); + playMedia( convertRunoutputToSource(out), convertProviderCaption(out.stream.captions), diff --git a/src/pages/parts/player/PlaybackErrorPart.tsx b/src/pages/parts/player/PlaybackErrorPart.tsx index 3561f727..d29cd986 100644 --- a/src/pages/parts/player/PlaybackErrorPart.tsx +++ b/src/pages/parts/player/PlaybackErrorPart.tsx @@ -22,6 +22,8 @@ export interface PlaybackErrorPartProps { export function PlaybackErrorPart(props: PlaybackErrorPartProps) { const { t } = useTranslation(); const playbackError = usePlayerStore((s) => s.interface.error); + const currentSourceId = usePlayerStore((s) => s.sourceId); + const addFailedSource = usePlayerStore((s) => s.addFailedSource); const modal = useModal("error"); const settingsRouter = useOverlayRouter("settings"); const hasOpenedSettings = useRef(false); @@ -33,21 +35,24 @@ export function PlaybackErrorPart(props: PlaybackErrorPartProps) { (s) => s.enableAutoResumeOnPlaybackError, ); - // Automatically open the settings overlay when a playback error occurs (unless auto-resume is enabled) + // Mark the failed source and handle UI when a playback error occurs useEffect(() => { - if ( - playbackError && - !hasOpenedSettings.current && - !enableAutoResumeOnPlaybackError - ) { - hasOpenedSettings.current = true; - // Reset the last successful source when a playback error occurs - setLastSuccessfulSource(null); - settingsRouter.open(); - settingsRouter.navigate("/source"); + if (playbackError && currentSourceId) { + // Mark this source as failed + addFailedSource(currentSourceId); + + if (!hasOpenedSettings.current && !enableAutoResumeOnPlaybackError) { + hasOpenedSettings.current = true; + // Reset the last successful source when a playback error occurs + setLastSuccessfulSource(null); + settingsRouter.open(); + settingsRouter.navigate("/source"); + } } }, [ playbackError, + currentSourceId, + addFailedSource, settingsRouter, setLastSuccessfulSource, enableAutoResumeOnPlaybackError, diff --git a/src/stores/player/slices/source.ts b/src/stores/player/slices/source.ts index f4496af9..82e3c0b2 100644 --- a/src/stores/player/slices/source.ts +++ b/src/stores/player/slices/source.ts @@ -89,6 +89,7 @@ export interface SourceSlice { asTrack: boolean; }; meta: PlayerMeta | null; + failedSources: string[]; setStatus(status: PlayerStatus): void; setSource( stream: SourceSliceSource, @@ -104,6 +105,9 @@ export interface SourceSlice { redisplaySource(startAt: number): void; setCaptionAsTrack(asTrack: boolean): void; addExternalSubtitles(): Promise; + addFailedSource(sourceId: string): void; + clearFailedSources(): void; + reset(): void; } export function metaToScrapeMedia(meta: PlayerMeta): ScrapeMedia { @@ -141,6 +145,7 @@ export const createSourceSlice: MakeSlice = (set, get) => ({ currentAudioTrack: null, status: playerStatus.IDLE, meta: null, + failedSources: [], caption: { selected: null, asTrack: false, @@ -256,6 +261,38 @@ export const createSourceSlice: MakeSlice = (set, get) => ({ s.caption.asTrack = asTrack; }); }, + addFailedSource(sourceId: string) { + set((s) => { + if (!s.failedSources.includes(sourceId)) { + s.failedSources = [...s.failedSources, sourceId]; + } + }); + }, + clearFailedSources() { + set((s) => { + s.failedSources = []; + }); + }, + reset() { + set((s) => { + s.source = null; + s.sourceId = null; + s.embedId = null; + s.qualities = []; + s.audioTracks = []; + s.captionList = []; + s.isLoadingExternalSubtitles = false; + s.currentQuality = null; + s.currentAudioTrack = null; + s.status = playerStatus.IDLE; + s.meta = null; + s.failedSources = []; + s.caption = { + selected: null, + asTrack: false, + }; + }); + }, async addExternalSubtitles() { const store = get(); if (!store.meta) return;