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) => {