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.
This commit is contained in:
Pas 2025-11-30 17:50:28 -07:00
parent 4ced25623f
commit c460c15966
5 changed files with 67 additions and 15 deletions

View file

@ -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!"
},

View file

@ -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

View file

@ -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),

View file

@ -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,

View file

@ -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<void>;
addFailedSource(sourceId: string): void;
clearFailedSources(): void;
reset(): void;
}
export function metaToScrapeMedia(meta: PlayerMeta): ScrapeMedia {
@ -141,6 +145,7 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (set, get) => ({
currentAudioTrack: null,
status: playerStatus.IDLE,
meta: null,
failedSources: [],
caption: {
selected: null,
asTrack: false,
@ -256,6 +261,38 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (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;