add lastSuccessfulSource feature

to sort the last successful source to the top
This commit is contained in:
Pas 2025-11-02 22:10:48 -07:00
parent 627d66eced
commit d575d71108
13 changed files with 251 additions and 8 deletions

View file

@ -1107,7 +1107,10 @@
"embedOrderEnableLabel": "Custom embed order",
"manualSource": "Manual source selection",
"manualSourceDescription": "Require picking a source before scraping. Disables automatic source selection and opens the source picker when starting playback.",
"manualSourceLabel": "Manual source selection"
"manualSourceLabel": "Manual source selection",
"lastSuccessfulSource": "Last successful source",
"lastSuccessfulSourceDescription": "Automatically prioritize the source that successfully provided content for the previous episode. This helps ensure continuity when watching series.",
"lastSuccessfulSourceEnableLabel": "Enable last successful source"
},
"reset": "Reset",
"save": "Save",

View file

@ -21,6 +21,8 @@ export interface SettingsInput {
forceCompactEpisodeView?: boolean;
sourceOrder?: string[] | null;
enableSourceOrder?: boolean;
lastSuccessfulSource?: string | null;
enableLastSuccessfulSource?: boolean;
disabledSources?: string[] | null;
embedOrder?: string[] | null;
enableEmbedOrder?: boolean;
@ -52,6 +54,8 @@ export interface SettingsResponse {
forceCompactEpisodeView?: boolean;
sourceOrder?: string[] | null;
enableSourceOrder?: boolean;
lastSuccessfulSource?: string | null;
enableLastSuccessfulSource?: boolean;
disabledSources?: string[] | null;
embedOrder?: string[] | null;
enableEmbedOrder?: boolean;

View file

@ -106,12 +106,16 @@ export function NextEpisodeButton(props: {
const time = usePlayerStore((s) => s.progress.time);
const enableAutoplay = usePreferencesStore((s) => s.enableAutoplay);
const enableSkipCredits = usePreferencesStore((s) => s.enableSkipCredits);
const setLastSuccessfulSource = usePreferencesStore(
(s) => s.setLastSuccessfulSource,
);
const showingState = shouldShowNextEpisodeButton(time, duration);
const status = usePlayerStore((s) => s.status);
const setShouldStartFromBeginning = usePlayerStore(
(s) => s.setShouldStartFromBeginning,
);
const updateItem = useProgressStore((s) => s.updateItem);
const sourceId = usePlayerStore((s) => s.sourceId);
const isLastEpisode =
!meta?.episode?.number || !meta?.episodes?.at(-1)?.number
@ -147,6 +151,12 @@ export function NextEpisodeButton(props: {
const loadNextEpisode = useCallback(() => {
if (!meta || !nextEp) return;
// Store the current source as the last successful source
if (sourceId) {
setLastSuccessfulSource(sourceId);
}
const metaCopy = { ...meta };
metaCopy.episode = nextEp;
metaCopy.season =
@ -173,6 +183,8 @@ export function NextEpisodeButton(props: {
updateItem,
isLastEpisode,
nextSeason,
sourceId,
setLastSuccessfulSource,
]);
const startCurrentEpisodeFromBeginning = useCallback(() => {

View file

@ -144,6 +144,12 @@ export function SourceSelectionView({
const currentSourceId = usePlayerStore((s) => s.sourceId);
const preferredSourceOrder = usePreferencesStore((s) => s.sourceOrder);
const enableSourceOrder = usePreferencesStore((s) => s.enableSourceOrder);
const lastSuccessfulSource = usePreferencesStore(
(s) => s.lastSuccessfulSource,
);
const enableLastSuccessfulSource = usePreferencesStore(
(s) => s.enableLastSuccessfulSource,
);
const disabledSources = usePreferencesStore((s) => s.disabledSources);
const sources = useMemo(() => {
@ -154,13 +160,34 @@ export function SourceSelectionView({
.filter((v) => !disabledSources.includes(v.id));
if (!enableSourceOrder || preferredSourceOrder.length === 0) {
// Even without custom source order, prioritize last successful source if enabled
if (enableLastSuccessfulSource && lastSuccessfulSource) {
const lastSourceIndex = allSources.findIndex(
(s) => s.id === lastSuccessfulSource,
);
if (lastSourceIndex !== -1) {
const lastSource = allSources.splice(lastSourceIndex, 1)[0];
return [lastSource, ...allSources];
}
}
return allSources;
}
// Sort sources according to preferred order
// Sort sources according to preferred order, but prioritize last successful source
const orderedSources = [];
const remainingSources = [...allSources];
// First, add the last successful source if it exists, is available, and the feature is enabled
if (enableLastSuccessfulSource && lastSuccessfulSource) {
const lastSourceIndex = remainingSources.findIndex(
(s) => s.id === lastSuccessfulSource,
);
if (lastSourceIndex !== -1) {
orderedSources.push(remainingSources[lastSourceIndex]);
remainingSources.splice(lastSourceIndex, 1);
}
}
// Add sources in preferred order
for (const sourceId of preferredSourceOrder) {
const sourceIndex = remainingSources.findIndex((s) => s.id === sourceId);
@ -174,7 +201,14 @@ export function SourceSelectionView({
orderedSources.push(...remainingSources);
return orderedSources;
}, [metaType, preferredSourceOrder, enableSourceOrder, disabledSources]);
}, [
metaType,
preferredSourceOrder,
enableSourceOrder,
disabledSources,
lastSuccessfulSource,
enableLastSuccessfulSource,
]);
return (
<>

View file

@ -5,6 +5,7 @@ import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta";
import { VideoPlayerButton } from "@/components/player/internals/Button";
import { PlayerMeta } from "@/stores/player/slices/source";
import { usePlayerStore } from "@/stores/player/store";
import { usePreferencesStore } from "@/stores/preferences";
import { useProgressStore } from "@/stores/progress";
interface SkipEpisodeButtonProps {
@ -19,13 +20,19 @@ export function SkipEpisodeButton(props: SkipEpisodeButtonProps) {
(s) => s.setShouldStartFromBeginning,
);
const updateItem = useProgressStore((s) => s.updateItem);
const sourceId = usePlayerStore((s) => s.sourceId);
const setLastSuccessfulSource = usePreferencesStore(
(s) => s.setLastSuccessfulSource,
);
const nextEp = meta?.episodes?.find(
(v) => v.number === (meta?.episode?.number ?? 0) + 1,
);
const loadNextEpisode = useCallback(() => {
if (!meta || !nextEp) return;
if (sourceId) {
setLastSuccessfulSource(sourceId);
}
const metaCopy = { ...meta };
metaCopy.episode = nextEp;
setShouldStartFromBeginning(true);
@ -43,6 +50,8 @@ export function SkipEpisodeButton(props: SkipEpisodeButtonProps) {
props,
setShouldStartFromBeginning,
updateItem,
sourceId,
setLastSuccessfulSource,
]);
// Don't show button if not in control, not a show, or no next episode

View file

@ -58,6 +58,12 @@ export function useAuthData() {
const setEnableSourceOrder = usePreferencesStore(
(s) => s.setEnableSourceOrder,
);
const setLastSuccessfulSource = usePreferencesStore(
(s) => s.setLastSuccessfulSource,
);
const setEnableLastSuccessfulSource = usePreferencesStore(
(s) => s.setEnableLastSuccessfulSource,
);
const setDisabledSources = usePreferencesStore((s) => s.setDisabledSources);
const setEmbedOrder = usePreferencesStore((s) => s.setEmbedOrder);
const setEnableEmbedOrder = usePreferencesStore((s) => s.setEnableEmbedOrder);
@ -193,6 +199,14 @@ export function useAuthData() {
setEnableSourceOrder(settings.enableSourceOrder);
}
if (settings.lastSuccessfulSource !== undefined) {
setLastSuccessfulSource(settings.lastSuccessfulSource);
}
if (settings.enableLastSuccessfulSource !== undefined) {
setEnableLastSuccessfulSource(settings.enableLastSuccessfulSource);
}
if (settings.disabledSources !== undefined) {
setDisabledSources(settings.disabledSources ?? []);
}
@ -265,6 +279,8 @@ export function useAuthData() {
setForceCompactEpisodeView,
setSourceOrder,
setEnableSourceOrder,
setLastSuccessfulSource,
setEnableLastSuccessfulSource,
setDisabledSources,
setEmbedOrder,
setEnableEmbedOrder,

View file

@ -155,6 +155,12 @@ export function useScrape() {
const preferredSourceOrder = usePreferencesStore((s) => s.sourceOrder);
const enableSourceOrder = usePreferencesStore((s) => s.enableSourceOrder);
const lastSuccessfulSource = usePreferencesStore(
(s) => s.lastSuccessfulSource,
);
const enableLastSuccessfulSource = usePreferencesStore(
(s) => s.enableLastSuccessfulSource,
);
const disabledSources = usePreferencesStore((s) => s.disabledSources);
const preferredEmbedOrder = usePreferencesStore((s) => s.embedOrder);
const enableEmbedOrder = usePreferencesStore((s) => s.enableEmbedOrder);
@ -162,11 +168,29 @@ export function useScrape() {
const startScraping = useCallback(
async (media: ScrapeMedia) => {
// Filter out disabled sources from the source order
const filteredSourceOrder = enableSourceOrder
// Create source order that prioritizes last successful source
let filteredSourceOrder = enableSourceOrder
? preferredSourceOrder.filter((id) => !disabledSources.includes(id))
: undefined;
// If we have a last successful source and the feature is enabled, prioritize it
if (enableLastSuccessfulSource && lastSuccessfulSource) {
// Get all available sources (either from custom order or default)
const availableSources = filteredSourceOrder || [];
// If the last successful source is not disabled and exists in available sources,
// move it to the front
if (
!disabledSources.includes(lastSuccessfulSource) &&
availableSources.includes(lastSuccessfulSource)
) {
filteredSourceOrder = [
lastSuccessfulSource,
...availableSources.filter((id) => id !== lastSuccessfulSource),
];
}
}
// Filter out disabled embeds from the embed order
const filteredEmbedOrder = enableEmbedOrder
? preferredEmbedOrder.filter((id) => !disabledEmbeds.includes(id))
@ -223,6 +247,8 @@ export function useScrape() {
startScrape,
preferredSourceOrder,
enableSourceOrder,
lastSuccessfulSource,
enableLastSuccessfulSource,
disabledSources,
preferredEmbedOrder,
enableEmbedOrder,

View file

@ -60,6 +60,8 @@ export function useSettingsState(
enableDetailsModal: boolean,
sourceOrder: string[],
enableSourceOrder: boolean,
lastSuccessfulSource: string | null,
enableLastSuccessfulSource: boolean,
disabledSources: string[],
embedOrder: string[],
enableEmbedOrder: boolean,
@ -164,6 +166,18 @@ export function useSettingsState(
resetEnableSourceOrder,
enableSourceOrderChanged,
] = useDerived(enableSourceOrder);
const [
lastSuccessfulSourceState,
setLastSuccessfulSourceState,
resetLastSuccessfulSource,
lastSuccessfulSourceChanged,
] = useDerived(lastSuccessfulSource);
const [
enableLastSuccessfulSourceState,
setEnableLastSuccessfulSourceState,
resetEnableLastSuccessfulSource,
enableLastSuccessfulSourceChanged,
] = useDerived(enableLastSuccessfulSource);
const [
disabledSourcesState,
setDisabledSourcesState,
@ -259,6 +273,8 @@ export function useSettingsState(
resetEnableImageLogos();
resetSourceOrder();
resetEnableSourceOrder();
resetLastSuccessfulSource();
resetEnableLastSuccessfulSource();
resetDisabledSources();
resetEmbedOrder();
resetEnableEmbedOrder();
@ -293,6 +309,8 @@ export function useSettingsState(
enableImageLogosChanged ||
sourceOrderChanged ||
enableSourceOrderChanged ||
lastSuccessfulSourceChanged ||
enableLastSuccessfulSourceChanged ||
disabledSourcesChanged ||
embedOrderChanged ||
enableEmbedOrderChanged ||
@ -400,6 +418,16 @@ export function useSettingsState(
set: setEnableSourceOrderState,
changed: enableSourceOrderChanged,
},
lastSuccessfulSource: {
state: lastSuccessfulSourceState,
set: setLastSuccessfulSourceState,
changed: lastSuccessfulSourceChanged,
},
enableLastSuccessfulSource: {
state: enableLastSuccessfulSourceState,
set: setEnableLastSuccessfulSourceState,
changed: enableLastSuccessfulSourceChanged,
},
proxyTmdb: {
state: proxyTmdbState,
set: setProxyTmdbState,

View file

@ -56,10 +56,20 @@ export function RealPlayerView() {
const manualSourceSelection = usePreferencesStore(
(s) => s.manualSourceSelection,
);
const setLastSuccessfulSource = usePreferencesStore(
(s) => s.setLastSuccessfulSource,
);
const router = useOverlayRouter("settings");
const openedWatchPartyRef = useRef<boolean>(false);
const progressItems = useProgressStore((s) => s.items);
// Reset last successful source when leaving the player
useEffect(() => {
return () => {
setLastSuccessfulSource(null);
};
}, [setLastSuccessfulSource]);
const paramsData = JSON.stringify({
media: params.media,
season: params.season,

View file

@ -282,6 +282,20 @@ export function SettingsPage() {
(s) => s.setEnableSourceOrder,
);
const lastSuccessfulSource = usePreferencesStore(
(s) => s.lastSuccessfulSource,
);
const setLastSuccessfulSource = usePreferencesStore(
(s) => s.setLastSuccessfulSource,
);
const enableLastSuccessfulSource = usePreferencesStore(
(s) => s.enableLastSuccessfulSource,
);
const setEnableLastSuccessfulSource = usePreferencesStore(
(s) => s.setEnableLastSuccessfulSource,
);
const disabledSources = usePreferencesStore((s) => s.disabledSources);
const setDisabledSources = usePreferencesStore((s) => s.setDisabledSources);
@ -406,6 +420,8 @@ export function SettingsPage() {
enableDetailsModal,
sourceOrder,
enableSourceOrder,
lastSuccessfulSource,
enableLastSuccessfulSource,
disabledSources,
embedOrder,
enableEmbedOrder,
@ -475,6 +491,8 @@ export function SettingsPage() {
state.enableImageLogos.changed ||
state.sourceOrder.changed ||
state.enableSourceOrder.changed ||
state.lastSuccessfulSource.changed ||
state.enableLastSuccessfulSource.changed ||
state.disabledSources.changed ||
state.proxyTmdb.changed ||
state.enableCarouselView.changed ||
@ -500,6 +518,8 @@ export function SettingsPage() {
enableImageLogos: state.enableImageLogos.state,
sourceOrder: state.sourceOrder.state,
enableSourceOrder: state.enableSourceOrder.state,
lastSuccessfulSource: state.lastSuccessfulSource.state,
enableLastSuccessfulSource: state.enableLastSuccessfulSource.state,
disabledSources: state.disabledSources.state,
proxyTmdb: state.proxyTmdb.state,
enableCarouselView: state.enableCarouselView.state,
@ -537,6 +557,8 @@ export function SettingsPage() {
setEnableImageLogos(state.enableImageLogos.state);
setSourceOrder(state.sourceOrder.state);
setEnableSourceOrder(state.enableSourceOrder.state);
setLastSuccessfulSource(state.lastSuccessfulSource.state);
setEnableLastSuccessfulSource(state.enableLastSuccessfulSource.state);
setDisabledSources(state.disabledSources.state);
setAppLanguage(state.appLanguage.state);
setTheme(state.theme.state);
@ -584,6 +606,8 @@ export function SettingsPage() {
setEnableImageLogos,
setSourceOrder,
setEnableSourceOrder,
setLastSuccessfulSource,
setEnableLastSuccessfulSource,
setDisabledSources,
setAppLanguage,
setTheme,
@ -650,6 +674,8 @@ export function SettingsPage() {
setSourceOrder={state.sourceOrder.set}
enableSourceOrder={state.enableSourceOrder.state}
setenableSourceOrder={state.enableSourceOrder.set}
enableLastSuccessfulSource={state.enableLastSuccessfulSource.state}
setEnableLastSuccessfulSource={state.enableLastSuccessfulSource.set}
disabledSources={state.disabledSources.state}
setDisabledSources={state.disabledSources.set}
enableLowPerformanceMode={state.enableLowPerformanceMode.state}

View file

@ -129,6 +129,12 @@ export function SourceSelectPart(props: { media: ScrapeMedia }) {
const routerId = "manualSourceSelect";
const preferredSourceOrder = usePreferencesStore((s) => s.sourceOrder);
const enableSourceOrder = usePreferencesStore((s) => s.enableSourceOrder);
const lastSuccessfulSource = usePreferencesStore(
(s) => s.lastSuccessfulSource,
);
const enableLastSuccessfulSource = usePreferencesStore(
(s) => s.enableLastSuccessfulSource,
);
const sources = useMemo(() => {
const metaType = props.media.type;
@ -138,13 +144,34 @@ export function SourceSelectPart(props: { media: ScrapeMedia }) {
.filter((v) => v.mediaTypes?.includes(metaType));
if (!enableSourceOrder || preferredSourceOrder.length === 0) {
// Even without custom source order, prioritize last successful source if enabled
if (enableLastSuccessfulSource && lastSuccessfulSource) {
const lastSourceIndex = allSources.findIndex(
(s) => s.id === lastSuccessfulSource,
);
if (lastSourceIndex !== -1) {
const lastSource = allSources.splice(lastSourceIndex, 1)[0];
return [lastSource, ...allSources];
}
}
return allSources;
}
// Sort sources according to preferred order
// Sort sources according to preferred order, but prioritize last successful source
const orderedSources = [];
const remainingSources = [...allSources];
// First, add the last successful source if it exists, is available, and the feature is enabled
if (enableLastSuccessfulSource && lastSuccessfulSource) {
const lastSourceIndex = remainingSources.findIndex(
(s) => s.id === lastSuccessfulSource,
);
if (lastSourceIndex !== -1) {
orderedSources.push(remainingSources[lastSourceIndex]);
remainingSources.splice(lastSourceIndex, 1);
}
}
// Add sources in preferred order
for (const sourceId of preferredSourceOrder) {
const sourceIndex = remainingSources.findIndex((s) => s.id === sourceId);
@ -158,7 +185,13 @@ export function SourceSelectPart(props: { media: ScrapeMedia }) {
orderedSources.push(...remainingSources);
return orderedSources;
}, [props.media.type, preferredSourceOrder, enableSourceOrder]);
}, [
props.media.type,
preferredSourceOrder,
enableSourceOrder,
lastSuccessfulSource,
enableLastSuccessfulSource,
]);
if (selectedSourceId) {
return (

View file

@ -27,6 +27,8 @@ export function PreferencesPart(props: {
setSourceOrder: (v: string[]) => void;
enableSourceOrder: boolean;
setenableSourceOrder: (v: boolean) => void;
enableLastSuccessfulSource: boolean;
setEnableLastSuccessfulSource: (v: boolean) => void;
disabledSources: string[];
setDisabledSources: (v: string[]) => void;
enableLowPerformanceMode: boolean;
@ -267,6 +269,30 @@ export function PreferencesPart(props: {
</p>
</div>
</div>
{/* Last Successful Source Preference */}
<div>
<p className="text-white font-bold mb-3">
{t("settings.preferences.lastSuccessfulSource")}
</p>
<p className="max-w-[25rem] font-medium">
{t("settings.preferences.lastSuccessfulSourceDescription")}
</p>
<div
onClick={() =>
props.setEnableLastSuccessfulSource(
!props.enableLastSuccessfulSource,
)
}
className="bg-dropdown-background hover:bg-dropdown-hoverBackground select-none my-4 cursor-pointer space-x-3 flex items-center max-w-[25rem] py-3 px-4 rounded-lg"
>
<Toggle enabled={props.enableLastSuccessfulSource} />
<p className="flex-1 text-white font-bold">
{t("settings.preferences.lastSuccessfulSourceEnableLabel")}
</p>
</div>
</div>
<p className="text-white font-bold">
{t("settings.preferences.sourceOrder")}
</p>

View file

@ -14,6 +14,8 @@ export interface PreferencesStore {
forceCompactEpisodeView: boolean;
sourceOrder: string[];
enableSourceOrder: boolean;
lastSuccessfulSource: string | null;
enableLastSuccessfulSource: boolean;
disabledSources: string[];
embedOrder: string[];
enableEmbedOrder: boolean;
@ -39,6 +41,8 @@ export interface PreferencesStore {
setForceCompactEpisodeView(v: boolean): void;
setSourceOrder(v: string[]): void;
setEnableSourceOrder(v: boolean): void;
setLastSuccessfulSource(v: string | null): void;
setEnableLastSuccessfulSource(v: boolean): void;
setDisabledSources(v: string[]): void;
setEmbedOrder(v: string[]): void;
setEnableEmbedOrder(v: boolean): void;
@ -68,6 +72,8 @@ export const usePreferencesStore = create(
forceCompactEpisodeView: false,
sourceOrder: [],
enableSourceOrder: false,
lastSuccessfulSource: null,
enableLastSuccessfulSource: true,
disabledSources: [],
embedOrder: [],
enableEmbedOrder: false,
@ -136,6 +142,16 @@ export const usePreferencesStore = create(
s.enableSourceOrder = v;
});
},
setLastSuccessfulSource(v) {
set((s) => {
s.lastSuccessfulSource = v;
});
},
setEnableLastSuccessfulSource(v) {
set((s) => {
s.enableLastSuccessfulSource = v;
});
},
setDisabledSources(v) {
set((s) => {
s.disabledSources = v;