pstream is dead; long live pstream
taciturnaxolotl.github.io/pstream-ng/
1import {
2 ProgressEpisodeItem,
3 ProgressItem,
4 ProgressMediaItem,
5 ProgressSeasonItem,
6} from "@/stores/progress";
7
8export interface ShowProgressResult {
9 episode?: ProgressEpisodeItem;
10 season?: ProgressSeasonItem;
11 progress: ProgressItem;
12 show: boolean;
13}
14
15const defaultProgress = {
16 duration: 0,
17 watched: 0,
18};
19
20function progressIsCompleted(duration: number, watched: number): boolean {
21 const timeFromEnd = duration - watched;
22
23 // too close to the end, is completed
24 if (timeFromEnd < 60 * 2) return true;
25
26 // satisfies all constraints, not completed
27 return false;
28}
29
30function progressIsNotStarted(duration: number, watched: number): boolean {
31 // too short watch time
32 if (watched < 20) return true;
33
34 // satisfies all constraints, not completed
35 return false;
36}
37
38function progressIsAcceptableRange(duration: number, watched: number): boolean {
39 // not started enough yet, not acceptable
40 if (progressIsNotStarted(duration, watched)) return false;
41
42 // is already at the end, not acceptable
43 if (progressIsCompleted(duration, watched)) return false;
44
45 // satisfied all constraints
46 return true;
47}
48
49function isFirstEpisodeOfShow(
50 item: ProgressMediaItem,
51 episode: ProgressEpisodeItem,
52): boolean {
53 const seasonId = episode.seasonId;
54 const season = item.seasons[seasonId];
55 return season.number === 1 && episode.number === 1;
56}
57
58export function getProgressPercentage(
59 watched: number,
60 duration: number,
61): number {
62 // Handle edge cases to prevent infinity or invalid percentages
63 if (!duration || duration <= 0) return 0;
64 if (!watched || watched < 0) return 0;
65
66 // Cap percentage at 100% to prevent >100% values
67 const percentage = Math.min((watched / duration) * 100, 100);
68 return percentage;
69}
70
71export function shouldShowProgress(
72 item: ProgressMediaItem,
73): ShowProgressResult {
74 // non shows just hide or show depending on acceptable ranges
75 if (item.type !== "show") {
76 return {
77 show: progressIsAcceptableRange(
78 item.progress?.duration ?? 0,
79 item.progress?.watched ?? 0,
80 ),
81 progress: item.progress ?? defaultProgress,
82 };
83 }
84
85 // shows only hide an item if its too early in episode, it still shows if its near the end.
86 // Otherwise you would lose episode progress
87 const ep = Object.values(item.episodes)
88 .sort((a, b) => b.updatedAt - a.updatedAt)
89 .filter(
90 (epi) =>
91 !progressIsNotStarted(epi.progress.duration, epi.progress.watched) ||
92 !isFirstEpisodeOfShow(item, epi),
93 )[0];
94
95 const season = item.seasons[ep?.seasonId];
96 if (!ep || !season)
97 return {
98 show: false,
99 progress: defaultProgress,
100 };
101 return {
102 season,
103 episode: ep,
104 show: true,
105 progress: ep.progress,
106 };
107}