diff --git a/src/backend/accounts/user.ts b/src/backend/accounts/user.ts index 2b6492f1..5bde3d4e 100644 --- a/src/backend/accounts/user.ts +++ b/src/backend/accounts/user.ts @@ -45,8 +45,8 @@ export interface ProgressResponse { poster?: string; type: "show" | "movie"; }; - duration: number; - watched: number; + duration: string; + watched: string; updatedAt: string; } @@ -81,8 +81,8 @@ export function progressResponsesToEntries(responses: ProgressResponse[]) { const item = items[v.tmdbId]; if (item.type === "movie") { item.progress = { - duration: v.duration, - watched: v.watched, + duration: Number(v.duration), + watched: Number(v.watched), }; } @@ -97,8 +97,8 @@ export function progressResponsesToEntries(responses: ProgressResponse[]) { number: v.episode.number ?? 0, title: "", progress: { - duration: v.duration, - watched: v.watched, + duration: Number(v.duration), + watched: Number(v.watched), }, seasonId: v.season.id, updatedAt: new Date(v.updatedAt).getTime(), diff --git a/src/components/media/WatchedMediaCard.tsx b/src/components/media/WatchedMediaCard.tsx index 0e66e235..b9a28e39 100644 --- a/src/components/media/WatchedMediaCard.tsx +++ b/src/components/media/WatchedMediaCard.tsx @@ -1,48 +1,47 @@ import { useMemo } from "react"; -import { ProgressMediaItem, useProgressStore } from "@/stores/progress"; +import { useProgressStore } from "@/stores/progress"; +import { + ShowProgressResult, + shouldShowProgress, +} from "@/stores/progress/utils"; import { MediaItem } from "@/utils/mediaTypes"; import { MediaCard } from "./MediaCard"; +function formatSeries(series?: ShowProgressResult | null) { + if (!series || !series.episode || !series.season) return undefined; + return { + episode: series.episode?.number, + season: series.season?.number, + episodeId: series.episode?.id, + seasonId: series.season?.id, + }; +} + export interface WatchedMediaCardProps { media: MediaItem; closable?: boolean; onClose?: () => void; } -function formatSeries(obj: ProgressMediaItem | undefined) { - if (!obj) return undefined; - if (obj.type !== "show") return; - const ep = Object.values(obj.episodes).sort( - (a, b) => b.updatedAt - a.updatedAt - )[0]; - const season = obj.seasons[ep?.seasonId]; - if (!ep || !season) return; - return { - season: season.number, - episode: ep.number, - episodeId: ep.id, - seasonId: ep.seasonId, - progress: ep.progress, - }; -} - export function WatchedMediaCard(props: WatchedMediaCardProps) { const progressItems = useProgressStore((s) => s.items); const item = useMemo(() => { return progressItems[props.media.id]; }, [progressItems, props.media]); - const series = useMemo(() => formatSeries(item), [item]); - const progress = item?.progress ?? series?.progress; - const percentage = progress - ? (progress.watched / progress.duration) * 100 + const itemToDisplay = useMemo( + () => (item ? shouldShowProgress(item) : null), + [item] + ); + const percentage = itemToDisplay?.show + ? (itemToDisplay.progress.watched / itemToDisplay.progress.duration) * 100 : undefined; return ( { let output: MediaItem[] = []; Object.entries(progressItems) + .filter((entry) => shouldShowProgress(entry[1]).show) .sort((a, b) => b[1].updatedAt - a[1].updatedAt) .forEach((entry) => { output.push({ diff --git a/src/stores/progress/utils.ts b/src/stores/progress/utils.ts new file mode 100644 index 00000000..24db571f --- /dev/null +++ b/src/stores/progress/utils.ts @@ -0,0 +1,83 @@ +import { + ProgressEpisodeItem, + ProgressItem, + ProgressMediaItem, + ProgressSeasonItem, +} from "@/stores/progress"; + +export interface ShowProgressResult { + episode?: ProgressEpisodeItem; + season?: ProgressSeasonItem; + progress: ProgressItem; + show: boolean; +} + +const defaultProgress = { + duration: 0, + watched: 0, +}; + +function progressIsCompleted(duration: number, watched: number): boolean { + const timeFromEnd = duration - watched; + + // too close to the end, is completed + if (timeFromEnd < 60 * 2) return true; + + // satisfies all constraints, not completed + return false; +} + +function progressIsNotStarted(duration: number, watched: number): boolean { + // too short watch time + if (watched < 20) return true; + + // satisfies all constraints, not completed + return false; +} + +function progressIsAcceptableRange(duration: number, watched: number): boolean { + // not started enough yet, not acceptable + if (progressIsNotStarted(duration, watched)) return false; + + // is already at the end, not acceptable + if (progressIsCompleted(duration, watched)) return false; + + // satisfied all constraints + return true; +} + +export function shouldShowProgress( + item: ProgressMediaItem +): ShowProgressResult { + // non shows just hide or show depending on acceptable ranges + if (item.type !== "show") { + return { + show: progressIsAcceptableRange( + item.progress?.duration ?? 0, + item.progress?.watched ?? 0 + ), + progress: item.progress ?? defaultProgress, + }; + } + + // shows only hide an item if its too early in episode, it still shows if its near the end. + // Otherwise you would lose episode progress + const ep = Object.values(item.episodes) + .sort((a, b) => b.updatedAt - a.updatedAt) + .filter( + (epi) => + !progressIsNotStarted(epi.progress.duration, epi.progress.watched) + )[0]; + const season = item.seasons[ep?.seasonId]; + if (!ep || !season) + return { + show: false, + progress: defaultProgress, + }; + return { + season, + episode: ep, + show: true, + progress: ep.progress, + }; +}