diff --git a/src/components/media/WatchedMediaCard.tsx b/src/components/media/WatchedMediaCard.tsx
index 4d799809..68e43618 100644
--- a/src/components/media/WatchedMediaCard.tsx
+++ b/src/components/media/WatchedMediaCard.tsx
@@ -1,6 +1,6 @@
import { useMemo } from "react";
-import { useProgressStore } from "@/stores/progress";
+import { getProgressPercentage, useProgressStore } from "@/stores/progress";
import {
ShowProgressResult,
shouldShowProgress,
@@ -36,7 +36,10 @@ export function WatchedMediaCard(props: WatchedMediaCardProps) {
[item],
);
const percentage = itemToDisplay?.show
- ? (itemToDisplay.progress.watched / itemToDisplay.progress.duration) * 100
+ ? getProgressPercentage(
+ itemToDisplay.progress.watched,
+ itemToDisplay.progress.duration,
+ )
: undefined;
return (
diff --git a/src/components/overlays/detailsModal/components/carousels/EpisodeCarousel.tsx b/src/components/overlays/detailsModal/components/carousels/EpisodeCarousel.tsx
index 7c0f1d2a..b6eeacb6 100644
--- a/src/components/overlays/detailsModal/components/carousels/EpisodeCarousel.tsx
+++ b/src/components/overlays/detailsModal/components/carousels/EpisodeCarousel.tsx
@@ -9,7 +9,7 @@ import { Icon, Icons } from "@/components/Icon";
import { Modal, ModalCard, useModal } from "@/components/overlays/Modal";
import { hasAired } from "@/components/player/utils/aired";
import { useBookmarkStore } from "@/stores/bookmarks";
-import { useProgressStore } from "@/stores/progress";
+import { getProgressPercentage, useProgressStore } from "@/stores/progress";
import { EpisodeCarouselProps } from "../../types";
@@ -170,9 +170,10 @@ export function EpisodeCarousel({
const episodeProgress =
progress[mediaId.toString()]?.episodes?.[episodeId];
const percentage = episodeProgress
- ? (episodeProgress.progress.watched /
- episodeProgress.progress.duration) *
- 100
+ ? getProgressPercentage(
+ episodeProgress.progress.watched,
+ episodeProgress.progress.duration,
+ )
: 0;
// If watched (>90%), reset to 0%, otherwise set to 100%
@@ -282,9 +283,10 @@ export function EpisodeCarousel({
const episodeProgress =
progress[mediaId?.toString() ?? ""]?.episodes?.[episode.id];
const percentage = episodeProgress
- ? (episodeProgress.progress.watched /
- episodeProgress.progress.duration) *
- 100
+ ? getProgressPercentage(
+ episodeProgress.progress.watched,
+ episodeProgress.progress.duration,
+ )
: 0;
const isAired = hasAired(episode.air_date);
const isWatched = percentage > 90;
@@ -299,9 +301,10 @@ export function EpisodeCarousel({
const episodeProgress =
progress[mediaId?.toString() ?? ""]?.episodes?.[episode.id];
const percentage = episodeProgress
- ? (episodeProgress.progress.watched /
- episodeProgress.progress.duration) *
- 100
+ ? getProgressPercentage(
+ episodeProgress.progress.watched,
+ episodeProgress.progress.duration,
+ )
: 0;
const isAired = hasAired(episode.air_date);
const isWatched = percentage > 90;
@@ -378,9 +381,10 @@ export function EpisodeCarousel({
const episodeProgress =
progress[mediaId?.toString() ?? ""]?.episodes?.[episode.id];
const percentage = episodeProgress
- ? (episodeProgress.progress.watched /
- episodeProgress.progress.duration) *
- 100
+ ? getProgressPercentage(
+ episodeProgress.progress.watched,
+ episodeProgress.progress.duration,
+ )
: 0;
const isAired = hasAired(episode.air_date);
const isWatched = percentage > 90;
@@ -565,9 +569,10 @@ export function EpisodeCarousel({
const episodeProgress =
progress[mediaId?.toString() ?? ""]?.episodes?.[episode.id];
const percentage = episodeProgress
- ? (episodeProgress.progress.watched /
- episodeProgress.progress.duration) *
- 100
+ ? getProgressPercentage(
+ episodeProgress.progress.watched,
+ episodeProgress.progress.duration,
+ )
: 0;
const isAired = hasAired(episode.air_date);
const isExpanded = expandedEpisodes[episode.id];
diff --git a/src/components/player/atoms/WatchPartyStatus.tsx b/src/components/player/atoms/WatchPartyStatus.tsx
index a4bf3840..5f04ed68 100644
--- a/src/components/player/atoms/WatchPartyStatus.tsx
+++ b/src/components/player/atoms/WatchPartyStatus.tsx
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
import { Button } from "@/components/buttons/Button";
import { Icon, Icons } from "@/components/Icon";
import { useWatchPartySync } from "@/hooks/useWatchPartySync";
+import { getProgressPercentage } from "@/stores/progress";
import { useWatchPartyStore } from "@/stores/watchParty";
export function WatchPartyStatus() {
@@ -110,7 +111,7 @@ export function WatchPartyStatus() {
{user.player.duration > 0
- ? `${Math.floor((user.player.time / user.player.duration) * 100)}%`
+ ? `${Math.floor(getProgressPercentage(user.player.time, user.player.duration))}%`
: `${Math.floor(user.player.time)}s`}
diff --git a/src/components/player/atoms/settings/WatchPartyView.tsx b/src/components/player/atoms/settings/WatchPartyView.tsx
index 7434ad83..d10b0eab 100644
--- a/src/components/player/atoms/settings/WatchPartyView.tsx
+++ b/src/components/player/atoms/settings/WatchPartyView.tsx
@@ -13,6 +13,7 @@ import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
import { useWatchPartySync } from "@/hooks/useWatchPartySync";
import { useAuthStore } from "@/stores/auth";
+import { getProgressPercentage } from "@/stores/progress";
import { useWatchPartyStore } from "@/stores/watchParty";
import { useDownloadLink } from "./Downloads";
@@ -326,7 +327,7 @@ export function WatchPartyView({ id }: { id: string }) {
{user.player.duration > 0
- ? `${Math.floor((user.player.time / user.player.duration) * 100)}%`
+ ? `${Math.floor(getProgressPercentage(user.player.time, user.player.duration))}%`
: `${Math.floor(user.player.time)}s`}
diff --git a/src/pages/PlayerView.tsx b/src/pages/PlayerView.tsx
index b08d7154..4dc69a71 100644
--- a/src/pages/PlayerView.tsx
+++ b/src/pages/PlayerView.tsx
@@ -26,7 +26,7 @@ import { SourceSelectPart } from "@/pages/parts/player/SourceSelectPart";
import { useLastNonPlayerLink } from "@/stores/history";
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
import { usePreferencesStore } from "@/stores/preferences";
-import { useProgressStore } from "@/stores/progress";
+import { getProgressPercentage, useProgressStore } from "@/stores/progress";
import { needsOnboarding } from "@/utils/onboarding";
import { parseTimestamp } from "@/utils/timestamp";
@@ -107,16 +107,20 @@ export function RealPlayerView() {
if (meta.type === "movie") {
if (!item.progress) return false;
- const percentage =
- (item.progress.watched / item.progress.duration) * 100;
+ const percentage = getProgressPercentage(
+ item.progress.watched,
+ item.progress.duration,
+ );
return percentage > 80;
}
if (meta.type === "show" && meta.episode?.tmdbId) {
const episode = item.episodes?.[meta.episode.tmdbId];
if (!episode) return false;
- const percentage =
- (episode.progress.watched / episode.progress.duration) * 100;
+ const percentage = getProgressPercentage(
+ episode.progress.watched,
+ episode.progress.duration,
+ );
return percentage > 80;
}
diff --git a/src/pages/parts/player/ResumePart.tsx b/src/pages/parts/player/ResumePart.tsx
index 98272352..98191d13 100644
--- a/src/pages/parts/player/ResumePart.tsx
+++ b/src/pages/parts/player/ResumePart.tsx
@@ -8,7 +8,7 @@ import { Title } from "@/components/text/Title";
import { ErrorContainer, ErrorLayout } from "@/pages/layouts/ErrorLayout";
import { PlayerMeta } from "@/stores/player/slices/source";
import { usePlayerStore } from "@/stores/player/store";
-import { useProgressStore } from "@/stores/progress";
+import { getProgressPercentage, useProgressStore } from "@/stores/progress";
export interface ResumePartProps {
onResume: () => void;
@@ -30,13 +30,19 @@ export function ResumePart(props: ResumePartProps) {
if (meta.type === "movie") {
if (!item.progress) return 0;
- return (item.progress.watched / item.progress.duration) * 100;
+ return getProgressPercentage(
+ item.progress.watched,
+ item.progress.duration,
+ );
}
if (meta.type === "show" && meta.episode?.tmdbId) {
const episode = item.episodes?.[meta.episode.tmdbId];
if (!episode) return 0;
- return (episode.progress.watched / episode.progress.duration) * 100;
+ return getProgressPercentage(
+ episode.progress.watched,
+ episode.progress.duration,
+ );
}
return 0;
diff --git a/src/stores/progress/index.ts b/src/stores/progress/index.ts
index a209658f..8ff3d292 100644
--- a/src/stores/progress/index.ts
+++ b/src/stores/progress/index.ts
@@ -4,6 +4,8 @@ import { immer } from "zustand/middleware/immer";
import { PlayerMeta } from "@/stores/player/slices/source";
+export { getProgressPercentage } from "./utils";
+
export interface ProgressItem {
watched: number;
duration: number;
diff --git a/src/stores/progress/utils.ts b/src/stores/progress/utils.ts
index 6ea09462..eebe9e67 100644
--- a/src/stores/progress/utils.ts
+++ b/src/stores/progress/utils.ts
@@ -55,6 +55,19 @@ function isFirstEpisodeOfShow(
return season.number === 1 && episode.number === 1;
}
+export function getProgressPercentage(
+ watched: number,
+ duration: number,
+): number {
+ // Handle edge cases to prevent infinity or invalid percentages
+ if (!duration || duration <= 0) return 0;
+ if (!watched || watched < 0) return 0;
+
+ // Cap percentage at 100% to prevent >100% values
+ const percentage = Math.min((watched / duration) * 100, 100);
+ return percentage;
+}
+
export function shouldShowProgress(
item: ProgressMediaItem,
): ShowProgressResult {