track failed sources and disable multiple

This commit is contained in:
Pas 2025-12-20 12:23:17 -07:00
parent 4f6e56fd22
commit 58594ae4b5
3 changed files with 117 additions and 23 deletions

View file

@ -10,6 +10,7 @@ import {
} from "@/backend/helpers/providerApi";
import { getLoadbalancedProviderApiUrl } from "@/backend/providers/fetchers";
import { getProviders } from "@/backend/providers/providers";
import { getMediaKey } from "@/stores/player/slices/source";
import { usePlayerStore } from "@/stores/player/store";
import { usePreferencesStore } from "@/stores/preferences";
@ -172,8 +173,26 @@ export function useScrape() {
const providerInstance = getProviders();
const allSources = providerInstance.listSources();
const playerState = usePlayerStore.getState();
const failedSources = playerState.failedSources;
const failedEmbeds = playerState.failedEmbeds;
// Get media-specific failed sources/embeds
// Try to get media key from player state first, fallback to deriving from ScrapeMedia
let mediaKey = getMediaKey(playerState.meta);
if (!mediaKey) {
// Derive media key from ScrapeMedia if meta is not set yet
if (media.type === "movie") {
mediaKey = `movie-${media.tmdbId}`;
} else if (media.type === "show" && media.season && media.episode) {
mediaKey = `show-${media.tmdbId}-${media.season.tmdbId}-${media.episode.tmdbId}`;
} else if (media.type === "show") {
mediaKey = `show-${media.tmdbId}`;
}
}
const failedSources = mediaKey
? playerState.failedSourcesPerMedia[mediaKey] || []
: [];
const failedEmbeds = mediaKey
? playerState.failedEmbedsPerMedia[mediaKey] || {}
: {};
// Start with all available sources (filtered by disabled and failed ones)
let baseSourceOrder = allSources
@ -222,7 +241,7 @@ export function useScrape() {
}
}
// Collect all failed embed IDs across all sources
// Collect all failed embed IDs across all sources for current media
const allFailedEmbedIds = Object.values(failedEmbeds).flat();
// Filter out disabled and failed embeds from the embed order

View file

@ -9,6 +9,7 @@ import { Paragraph } from "@/components/text/Paragraph";
import { Title } from "@/components/text/Title";
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
import { ErrorContainer, ErrorLayout } from "@/pages/layouts/ErrorLayout";
import { getMediaKey } from "@/stores/player/slices/source";
import { usePlayerStore } from "@/stores/player/store";
import { usePreferencesStore } from "@/stores/preferences";
@ -24,9 +25,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 meta = usePlayerStore((s) => s.meta);
const failedEmbedsPerMedia = usePlayerStore((s) => s.failedEmbedsPerMedia);
const addFailedSource = usePlayerStore((s) => s.addFailedSource);
const addFailedEmbed = usePlayerStore((s) => s.addFailedEmbed);
const failedEmbeds = usePlayerStore((s) => s.failedEmbeds);
const modal = useModal("error");
const settingsRouter = useOverlayRouter("settings");
const hasOpenedSettings = useRef(false);
@ -54,6 +56,11 @@ export function PlaybackErrorPart(props: PlaybackErrorPartProps) {
// Check if all embeds for this source have now failed
// If so, disable the entire source
const mediaKey = getMediaKey(meta);
const failedEmbeds =
mediaKey && failedEmbedsPerMedia[mediaKey]
? failedEmbedsPerMedia[mediaKey]
: {};
const failedEmbedsForSource = 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
@ -78,7 +85,8 @@ export function PlaybackErrorPart(props: PlaybackErrorPartProps) {
playbackError,
currentSourceId,
currentEmbedId,
failedEmbeds,
meta,
failedEmbedsPerMedia,
addFailedSource,
addFailedEmbed,
settingsRouter,

View file

@ -89,8 +89,8 @@ export interface SourceSlice {
asTrack: boolean;
};
meta: PlayerMeta | null;
failedSources: string[];
failedEmbeds: Record<string, string[]>; // sourceId -> array of failed embedIds
failedSourcesPerMedia: Record<string, string[]>; // mediaKey -> array of failed sourceIds
failedEmbedsPerMedia: Record<string, Record<string, string[]>>; // mediaKey -> sourceId -> array of failed embedIds
setStatus(status: PlayerStatus): void;
setSource(
stream: SourceSliceSource,
@ -108,11 +108,32 @@ export interface SourceSlice {
addExternalSubtitles(): Promise<void>;
addFailedSource(sourceId: string): void;
addFailedEmbed(sourceId: string, embedId: string): void;
clearFailedSources(): void;
clearFailedEmbeds(): void;
clearFailedSources(mediaKey?: string): void;
clearFailedEmbeds(mediaKey?: string): void;
reset(): void;
}
/**
* Generates a unique media key for tracking failed sources per media.
* For movies: `${type}-${tmdbId}`
* For shows: `${type}-${tmdbId}-${season.tmdbId}-${episode.tmdbId}`
*/
export function getMediaKey(meta: PlayerMeta | null): string | null {
if (!meta) return null;
if (meta.type === "movie") {
return `${meta.type}-${meta.tmdbId}`;
}
// For shows, include season and episode IDs for per-episode tracking
if (meta.type === "show" && meta.season && meta.episode) {
return `${meta.type}-${meta.tmdbId}-${meta.season.tmdbId}-${meta.episode.tmdbId}`;
}
// Fallback if show data is incomplete
return `${meta.type}-${meta.tmdbId}`;
}
export function metaToScrapeMedia(meta: PlayerMeta): ScrapeMedia {
if (meta.type === "show") {
if (!meta.episode || !meta.season) throw new Error("missing show data");
@ -148,8 +169,8 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (set, get) => ({
currentAudioTrack: null,
status: playerStatus.IDLE,
meta: null,
failedSources: [],
failedEmbeds: {},
failedSourcesPerMedia: {},
failedEmbedsPerMedia: {},
caption: {
selected: null,
asTrack: false,
@ -172,12 +193,26 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (set, get) => ({
});
},
setMeta(meta, newStatus) {
const store = get();
const oldMediaKey = getMediaKey(store.meta);
const newMediaKey = getMediaKey(meta);
set((s) => {
s.meta = meta;
s.embedId = null;
s.sourceId = null;
s.interface.hideNextEpisodeBtn = false;
if (newStatus) s.status = newStatus;
// Clear failed sources/embeds for the new media when media changes
// Since we're doing per-episode tracking, we clear whenever media key changes
// Only clear if we're actually switching to different media (not just setting meta for the first time)
if (newMediaKey && oldMediaKey && oldMediaKey !== newMediaKey) {
// Clear failed sources/embeds for the new media (if any exist from previous session)
// This ensures a fresh start for each media/episode
delete s.failedSourcesPerMedia[newMediaKey];
delete s.failedEmbedsPerMedia[newMediaKey];
}
});
},
setCaption(caption) {
@ -267,30 +302,62 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (set, get) => ({
});
},
addFailedSource(sourceId: string) {
const store = get();
const mediaKey = getMediaKey(store.meta);
if (!mediaKey) return; // Skip tracking if no media is set
set((s) => {
if (!s.failedSources.includes(sourceId)) {
s.failedSources = [...s.failedSources, sourceId];
if (!s.failedSourcesPerMedia[mediaKey]) {
s.failedSourcesPerMedia[mediaKey] = [];
}
if (!s.failedSourcesPerMedia[mediaKey].includes(sourceId)) {
s.failedSourcesPerMedia[mediaKey] = [
...s.failedSourcesPerMedia[mediaKey],
sourceId,
];
}
});
},
addFailedEmbed(sourceId: string, embedId: string) {
const store = get();
const mediaKey = getMediaKey(store.meta);
if (!mediaKey) return; // Skip tracking if no media is set
set((s) => {
if (!s.failedEmbeds[sourceId]) {
s.failedEmbeds[sourceId] = [];
if (!s.failedEmbedsPerMedia[mediaKey]) {
s.failedEmbedsPerMedia[mediaKey] = {};
}
if (!s.failedEmbeds[sourceId].includes(embedId)) {
s.failedEmbeds[sourceId] = [...s.failedEmbeds[sourceId], embedId];
if (!s.failedEmbedsPerMedia[mediaKey][sourceId]) {
s.failedEmbedsPerMedia[mediaKey][sourceId] = [];
}
if (!s.failedEmbedsPerMedia[mediaKey][sourceId].includes(embedId)) {
s.failedEmbedsPerMedia[mediaKey][sourceId] = [
...s.failedEmbedsPerMedia[mediaKey][sourceId],
embedId,
];
}
});
},
clearFailedSources() {
clearFailedSources(mediaKey?: string) {
set((s) => {
s.failedSources = [];
if (mediaKey) {
// Clear for specific media
delete s.failedSourcesPerMedia[mediaKey];
} else {
// Clear all
s.failedSourcesPerMedia = {};
}
});
},
clearFailedEmbeds() {
clearFailedEmbeds(mediaKey?: string) {
set((s) => {
s.failedEmbeds = {};
if (mediaKey) {
// Clear for specific media
delete s.failedEmbedsPerMedia[mediaKey];
} else {
// Clear all
s.failedEmbedsPerMedia = {};
}
});
},
reset() {
@ -306,8 +373,8 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (set, get) => ({
s.currentAudioTrack = null;
s.status = playerStatus.IDLE;
s.meta = null;
s.failedSources = [];
s.failedEmbeds = {};
s.failedSourcesPerMedia = {};
s.failedEmbedsPerMedia = {};
s.caption = {
selected: null,
asTrack: false,