diff --git a/back/src/Kyoo.Abstractions/Controllers/IWatchStatusRepository.cs b/back/src/Kyoo.Abstractions/Controllers/IWatchStatusRepository.cs
index d9d9dcbf..e21806cf 100644
--- a/back/src/Kyoo.Abstractions/Controllers/IWatchStatusRepository.cs
+++ b/back/src/Kyoo.Abstractions/Controllers/IWatchStatusRepository.cs
@@ -48,7 +48,8 @@ public interface IWatchStatusRepository
Guid movieId,
Guid userId,
WatchStatus status,
- int? watchedTime
+ int? watchedTime,
+ int? percent
);
Task DeleteMovieStatus(Guid movieId, Guid userId);
@@ -67,7 +68,8 @@ public interface IWatchStatusRepository
Guid episodeId,
Guid userId,
WatchStatus status,
- int? watchedTime
+ int? watchedTime,
+ int? percent
);
Task DeleteEpisodeStatus(Guid episodeId, Guid userId);
diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs b/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs
index 8cd63b1a..da363959 100644
--- a/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs
+++ b/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs
@@ -152,7 +152,7 @@ namespace Kyoo.Abstractions.Models
///
/// How long is this episode? (in minutes)
///
- public int Runtime { get; set; }
+ public int? Runtime { get; set; }
///
/// The release date of this episode. It can be null if unknown.
diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs b/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs
index bfaf4bb5..f97c0f05 100644
--- a/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs
+++ b/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs
@@ -99,7 +99,7 @@ namespace Kyoo.Abstractions.Models
///
/// How long is this movie? (in minutes)
///
- public int Runtime { get; set; }
+ public int? Runtime { get; set; }
///
/// The date this movie aired.
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/WatchStatusRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/WatchStatusRepository.cs
index aee11760..2bd896b2 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/WatchStatusRepository.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/WatchStatusRepository.cs
@@ -236,14 +236,14 @@ public class WatchStatusRepository : IWatchStatusRepository
Guid movieId,
Guid userId,
WatchStatus status,
- int? watchedTime
+ int? watchedTime,
+ int? percent
)
{
Movie movie = await _movies.Get(movieId);
- int? percent =
- watchedTime != null && movie.Runtime > 0
- ? (int)Math.Round(watchedTime.Value / (movie.Runtime * 60f) * 100f)
- : null;
+
+ if (percent == null && watchedTime != null && movie.Runtime > 0)
+ percent = (int)Math.Round(watchedTime.Value / (movie.Runtime.Value * 60f) * 100f);
if (percent < MinWatchPercent)
return null;
@@ -259,6 +259,12 @@ public class WatchStatusRepository : IWatchStatusRepository
"Can't have a watched time if the status is not watching."
);
+ if (watchedTime.HasValue != percent.HasValue)
+ throw new ValidationException(
+ "Can't specify watched time without specifing percent (or vise-versa)."
+ + "Percent could not be guessed since duration is unknown."
+ );
+
MovieWatchStatus ret =
new()
{
@@ -463,14 +469,14 @@ public class WatchStatusRepository : IWatchStatusRepository
Guid episodeId,
Guid userId,
WatchStatus status,
- int? watchedTime
+ int? watchedTime,
+ int? percent
)
{
Episode episode = await _database.Episodes.FirstAsync(x => x.Id == episodeId);
- int? percent =
- watchedTime != null && episode.Runtime > 0
- ? (int)Math.Round(watchedTime.Value / (episode.Runtime * 60f) * 100f)
- : null;
+
+ if (percent == null && watchedTime != null && episode.Runtime > 0)
+ percent = (int)Math.Round(watchedTime.Value / (episode.Runtime.Value * 60f) * 100f);
if (percent < MinWatchPercent)
return null;
@@ -486,6 +492,12 @@ public class WatchStatusRepository : IWatchStatusRepository
"Can't have a watched time if the status is not watching."
);
+ if (watchedTime.HasValue != percent.HasValue)
+ throw new ValidationException(
+ "Can't specify watched time without specifing percent (or vise-versa)."
+ + "Percent could not be guessed since duration is unknown."
+ );
+
EpisodeWatchStatus ret =
new()
{
diff --git a/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs b/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs
index d2a73852..7a89df3c 100644
--- a/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs
+++ b/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs
@@ -147,7 +147,8 @@ namespace Kyoo.Core.Api
///
/// The ID or slug of the .
/// The new watch status.
- /// Where the user stopped watching.
+ /// Where the user stopped watching (in seconds).
+ /// Where the user stopped watching (in percent).
/// The newly set status.
/// The status has been set
/// The status was not considered impactfull enough to be saved (less then 5% of watched for example).
@@ -161,7 +162,8 @@ namespace Kyoo.Core.Api
public async Task SetWatchStatus(
Identifier identifier,
WatchStatus status,
- int? watchedTime
+ int? watchedTime,
+ int? percent
)
{
Guid id = await identifier.Match(
@@ -170,7 +172,7 @@ namespace Kyoo.Core.Api
);
return await _libraryManager
.WatchStatus
- .SetEpisodeStatus(id, User.GetIdOrThrow(), status, watchedTime);
+ .SetEpisodeStatus(id, User.GetIdOrThrow(), status, watchedTime, percent);
}
///
diff --git a/back/src/Kyoo.Core/Views/Resources/MovieApi.cs b/back/src/Kyoo.Core/Views/Resources/MovieApi.cs
index db604836..627a7b93 100644
--- a/back/src/Kyoo.Core/Views/Resources/MovieApi.cs
+++ b/back/src/Kyoo.Core/Views/Resources/MovieApi.cs
@@ -196,6 +196,7 @@ namespace Kyoo.Core.Api
/// The ID or slug of the .
/// The new watch status.
/// Where the user stopped watching.
+ /// Where the user stopped watching (in percent).
/// The newly set status.
/// The status has been set
/// The status was not considered impactfull enough to be saved (less then 5% of watched for example).
@@ -210,7 +211,8 @@ namespace Kyoo.Core.Api
public async Task SetWatchStatus(
Identifier identifier,
WatchStatus status,
- int? watchedTime
+ int? watchedTime,
+ int? percent
)
{
Guid id = await identifier.Match(
@@ -219,7 +221,7 @@ namespace Kyoo.Core.Api
);
return await _libraryManager
.WatchStatus
- .SetMovieStatus(id, User.GetIdOrThrow(), status, watchedTime);
+ .SetMovieStatus(id, User.GetIdOrThrow(), status, watchedTime, percent);
}
///
diff --git a/front/packages/models/src/resources/episode.base.ts b/front/packages/models/src/resources/episode.base.ts
index cd236f93..db2e86d2 100644
--- a/front/packages/models/src/resources/episode.base.ts
+++ b/front/packages/models/src/resources/episode.base.ts
@@ -49,7 +49,7 @@ export const BaseEpisodeP = withImages(
/**
* How long is this movie? (in minutes).
*/
- runtime: z.number().int(),
+ runtime: z.number().int().nullable(),
/**
* The release date of this episode. It can be null if unknown.
*/
diff --git a/front/packages/models/src/resources/movie.ts b/front/packages/models/src/resources/movie.ts
index 481cb5dd..df425d6d 100644
--- a/front/packages/models/src/resources/movie.ts
+++ b/front/packages/models/src/resources/movie.ts
@@ -61,7 +61,7 @@ export const MovieP = withImages(
/**
* How long is this movie? (in minutes).
*/
- runtime: z.number().int(),
+ runtime: z.number().int().nullable(),
/**
* The date this movie aired. It can also be null if this is unknown.
*/
diff --git a/front/packages/models/src/resources/watch-info.ts b/front/packages/models/src/resources/watch-info.ts
index b0b0e32f..a058b3d5 100644
--- a/front/packages/models/src/resources/watch-info.ts
+++ b/front/packages/models/src/resources/watch-info.ts
@@ -106,6 +106,10 @@ export const WatchInfoP = z.object({
* The sha1 of the video file.
*/
sha: z.string(),
+ /**
+ * The duration of the video (in seconds).
+ */
+ length: z.number(),
/**
* The internal path of the video file.
*/
diff --git a/front/packages/ui/src/details/episode.tsx b/front/packages/ui/src/details/episode.tsx
index 4a701bf5..5b4b10f7 100644
--- a/front/packages/ui/src/details/episode.tsx
+++ b/front/packages/ui/src/details/episode.tsx
@@ -57,7 +57,8 @@ export const episodeDisplayNumber = (
return def;
};
-export const displayRuntime = (runtime: number) => {
+export const displayRuntime = (runtime: number | null) => {
+ if (!runtime) return null;
if (runtime < 60) return `${runtime}min`;
return `${Math.floor(runtime / 60)}h${runtime % 60}`;
};
@@ -300,22 +301,19 @@ export const EpisodeLine = ({
)}
- {isLoading ||
- (runtime && (
-
- {isLoading || (
-
- {/* Source https://www.i18next.com/translation-function/formatting#datetime */}
- {[
- releaseDate ? t("{{val, datetime}}", { val: releaseDate }) : null,
- displayRuntime(runtime),
- ]
- .filter((item) => item != null)
- .join(" · ")}
-
- )}
-
- ))}
+
+ {isLoading || (
+
+ {/* Source https://www.i18next.com/translation-function/formatting#datetime */}
+ {[
+ releaseDate ? t("{{val, datetime}}", { val: releaseDate }) : null,
+ displayRuntime(runtime),
+ ]
+ .filter((item) => item != null)
+ .join(" · ")}
+
+ )}
+
{slug && watchedStatus !== undefined && (
- {data && }
+ {data && info && }
{
const account = useAccount();
const queryClient = useQueryClient();
@@ -47,9 +49,10 @@ export const WatchStatusObserver = ({
params: {
status: WatchStatusV.Watching,
watchedTime: Math.round(seconds),
+ percent: Math.round((seconds / duration) * 100),
},
}),
- [_mutate, type, slug],
+ [_mutate, type, slug, duration],
);
const readProgress = useAtomCallback(
useCallback((get) => {