diff --git a/src/components/player/internals/ProgressSaver.tsx b/src/components/player/internals/ProgressSaver.tsx index 59edefa5..bac294eb 100644 --- a/src/components/player/internals/ProgressSaver.tsx +++ b/src/components/player/internals/ProgressSaver.tsx @@ -5,10 +5,62 @@ import { playerStatus } from "@/stores/player/slices/source"; import { usePlayerStore } from "@/stores/player/store"; import { ProgressItem, useProgressStore } from "@/stores/progress"; +function progressIsNotStarted(duration: number, watched: number): boolean { + // too short watch time + if (watched < 20) return true; + return false; +} + +function progressIsCompleted(duration: number, watched: number): boolean { + const timeFromEnd = duration - watched; + // too close to the end, is completed + if (timeFromEnd < 60 * 2) return true; + return false; +} + +function shouldSaveProgress( + meta: any, + progress: ProgressItem, + existingItems: Record, +): boolean { + const { duration, watched } = progress; + + // Check if progress is acceptable + const isNotStarted = progressIsNotStarted(duration, watched); + const isCompleted = progressIsCompleted(duration, watched); + const isAcceptable = !isNotStarted && !isCompleted; + + // For movies, only save if acceptable + if (meta.type === "movie") { + return isAcceptable; + } + + // For shows, save if acceptable OR if season has other watched episodes + if (isAcceptable) return true; + + // Check if this season has other episodes with progress + const showItem = existingItems[meta.tmdbId]; + if (!showItem || !meta.season) return false; + + const seasonEpisodes = Object.values(showItem.episodes).filter( + (episode: any) => episode.seasonId === meta.season.tmdbId, + ); + + // Check if any other episode in this season has acceptable progress + return seasonEpisodes.some((episode: any) => { + const epProgress = episode.progress; + return ( + !progressIsNotStarted(epProgress.duration, epProgress.watched) && + !progressIsCompleted(epProgress.duration, epProgress.watched) + ); + }); +} + export function ProgressSaver() { const meta = usePlayerStore((s) => s.meta); const progress = usePlayerStore((s) => s.progress); const updateItem = useProgressStore((s) => s.updateItem); + const progressItems = useProgressStore((s) => s.items); const status = usePlayerStore((s) => s.status); const hasPlayedOnce = usePlayerStore((s) => s.mediaPlaying.hasPlayedOnce); @@ -16,6 +68,7 @@ export function ProgressSaver() { const dataRef = useRef({ updateItem, + progressItems, meta, progress, status, @@ -23,11 +76,12 @@ export function ProgressSaver() { }); useEffect(() => { dataRef.current.updateItem = updateItem; + dataRef.current.progressItems = progressItems; dataRef.current.meta = meta; dataRef.current.progress = progress; dataRef.current.status = status; dataRef.current.hasPlayedOnce = hasPlayedOnce; - }, [updateItem, progress, meta, status, hasPlayedOnce]); + }, [updateItem, progressItems, progress, meta, status, hasPlayedOnce]); useInterval(() => { const d = dataRef.current; @@ -47,7 +101,10 @@ export function ProgressSaver() { duration: progress.duration, watched: progress.time, }; - if (isDifferent) + if ( + isDifferent && + shouldSaveProgress(d.meta, lastSavedRef.current, d.progressItems) + ) d.updateItem({ meta: d.meta, progress: lastSavedRef.current, diff --git a/src/pages/admin/AdminPage.tsx b/src/pages/admin/AdminPage.tsx index eaa9dfac..389c3029 100644 --- a/src/pages/admin/AdminPage.tsx +++ b/src/pages/admin/AdminPage.tsx @@ -14,6 +14,7 @@ import { WorkerTestPart } from "@/pages/parts/admin/WorkerTestPart"; import { BackendTestPart } from "../parts/admin/BackendTestPart"; import { EmbedOrderPart } from "../parts/admin/EmbedOrderPart"; +import { ProgressCleanupPart } from "../parts/admin/ProgressCleanupPart"; export function AdminPage() { const { t } = useTranslation(); @@ -50,6 +51,7 @@ export function AdminPage() { disabledEmbeds={embedOrderState.disabledEmbeds} setDisabledEmbeds={embedOrderState.setDisabledEmbeds} /> + (`/users/${account.userId}/progress/cleanup`, { + method: "DELETE", + headers: getAuthHeaders(account.token), + baseURL: backendUrl, + }); +} + +export function ProgressCleanupPart() { + const backendUrl = useBackendUrl(); + const account = useAuthStore((s) => s.account); + + const [status, setStatus] = useState<{ + hasRun: boolean; + success: boolean; + errorText: string; + result: CleanupResponse | null; + }>({ + hasRun: false, + success: false, + errorText: "", + result: null, + }); + + const [cleanupState, runCleanup] = useAsyncFn(async () => { + setStatus({ + hasRun: false, + success: false, + errorText: "", + result: null, + }); + + if (!backendUrl || !account) { + return setStatus({ + hasRun: true, + success: false, + errorText: "Backend URL or account not available", + result: null, + }); + } + + try { + const result = await cleanupProgressItems(backendUrl, account); + return setStatus({ + hasRun: true, + success: true, + errorText: "", + result, + }); + } catch (err) { + console.error("Progress cleanup failed:", err); + return setStatus({ + hasRun: true, + success: false, + errorText: + "Failed to clean up progress items. Check console for details.", + result: null, + }); + } + }, [backendUrl, account]); + + return ( + <> + Progress Cleanup + +
+ {!status.hasRun ? ( +

Remove unwanted progress items from the database

+ ) : status.success ? ( +

+ + Cleanup completed +

+ ) : ( +
+

+ + Cleanup failed +

+

{status.errorText}

+
+ )} + +
+
+ + ); +}