diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json
index f9fd3941..de33a8e3 100644
--- a/src/assets/locales/en.json
+++ b/src/assets/locales/en.json
@@ -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",
diff --git a/src/backend/accounts/settings.ts b/src/backend/accounts/settings.ts
index 1460aa9c..14f9ca67 100644
--- a/src/backend/accounts/settings.ts
+++ b/src/backend/accounts/settings.ts
@@ -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;
diff --git a/src/components/player/atoms/NextEpisodeButton.tsx b/src/components/player/atoms/NextEpisodeButton.tsx
index 86e082c5..a59d2efb 100644
--- a/src/components/player/atoms/NextEpisodeButton.tsx
+++ b/src/components/player/atoms/NextEpisodeButton.tsx
@@ -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(() => {
diff --git a/src/components/player/atoms/settings/SourceSelectingView.tsx b/src/components/player/atoms/settings/SourceSelectingView.tsx
index c9ec789e..344c87d3 100644
--- a/src/components/player/atoms/settings/SourceSelectingView.tsx
+++ b/src/components/player/atoms/settings/SourceSelectingView.tsx
@@ -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 (
<>
diff --git a/src/components/player/internals/SkipEpisodeButton.tsx b/src/components/player/internals/SkipEpisodeButton.tsx
index f02208ad..2cf04aa6 100644
--- a/src/components/player/internals/SkipEpisodeButton.tsx
+++ b/src/components/player/internals/SkipEpisodeButton.tsx
@@ -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
diff --git a/src/hooks/auth/useAuthData.ts b/src/hooks/auth/useAuthData.ts
index 38339071..68cc2ac8 100644
--- a/src/hooks/auth/useAuthData.ts
+++ b/src/hooks/auth/useAuthData.ts
@@ -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,
diff --git a/src/hooks/useProviderScrape.tsx b/src/hooks/useProviderScrape.tsx
index 5ecca8f1..be0e73c9 100644
--- a/src/hooks/useProviderScrape.tsx
+++ b/src/hooks/useProviderScrape.tsx
@@ -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,
diff --git a/src/hooks/useSettingsState.ts b/src/hooks/useSettingsState.ts
index 33be4592..48fc0c61 100644
--- a/src/hooks/useSettingsState.ts
+++ b/src/hooks/useSettingsState.ts
@@ -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,
diff --git a/src/pages/PlayerView.tsx b/src/pages/PlayerView.tsx
index 4dc69a71..13e4131d 100644
--- a/src/pages/PlayerView.tsx
+++ b/src/pages/PlayerView.tsx
@@ -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
+ {t("settings.preferences.lastSuccessfulSource")} +
++ {t("settings.preferences.lastSuccessfulSourceDescription")} +
++ {t("settings.preferences.lastSuccessfulSourceEnableLabel")} +
+{t("settings.preferences.sourceOrder")}
diff --git a/src/stores/preferences/index.tsx b/src/stores/preferences/index.tsx index 256227ed..59a1f2aa 100644 --- a/src/stores/preferences/index.tsx +++ b/src/stores/preferences/index.tsx @@ -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;