Make runtime nullable

This commit is contained in:
Zoe Roux 2024-01-20 16:37:30 +01:00
parent b6df0ba2b1
commit c0e6012d70
12 changed files with 63 additions and 40 deletions

View File

@ -48,7 +48,8 @@ public interface IWatchStatusRepository
Guid movieId, Guid movieId,
Guid userId, Guid userId,
WatchStatus status, WatchStatus status,
int? watchedTime int? watchedTime,
int? percent
); );
Task DeleteMovieStatus(Guid movieId, Guid userId); Task DeleteMovieStatus(Guid movieId, Guid userId);
@ -67,7 +68,8 @@ public interface IWatchStatusRepository
Guid episodeId, Guid episodeId,
Guid userId, Guid userId,
WatchStatus status, WatchStatus status,
int? watchedTime int? watchedTime,
int? percent
); );
Task DeleteEpisodeStatus(Guid episodeId, Guid userId); Task DeleteEpisodeStatus(Guid episodeId, Guid userId);

View File

@ -152,7 +152,7 @@ namespace Kyoo.Abstractions.Models
/// <summary> /// <summary>
/// How long is this episode? (in minutes) /// How long is this episode? (in minutes)
/// </summary> /// </summary>
public int Runtime { get; set; } public int? Runtime { get; set; }
/// <summary> /// <summary>
/// The release date of this episode. It can be null if unknown. /// The release date of this episode. It can be null if unknown.

View File

@ -99,7 +99,7 @@ namespace Kyoo.Abstractions.Models
/// <summary> /// <summary>
/// How long is this movie? (in minutes) /// How long is this movie? (in minutes)
/// </summary> /// </summary>
public int Runtime { get; set; } public int? Runtime { get; set; }
/// <summary> /// <summary>
/// The date this movie aired. /// The date this movie aired.

View File

@ -236,14 +236,14 @@ public class WatchStatusRepository : IWatchStatusRepository
Guid movieId, Guid movieId,
Guid userId, Guid userId,
WatchStatus status, WatchStatus status,
int? watchedTime int? watchedTime,
int? percent
) )
{ {
Movie movie = await _movies.Get(movieId); Movie movie = await _movies.Get(movieId);
int? percent =
watchedTime != null && movie.Runtime > 0 if (percent == null && watchedTime != null && movie.Runtime > 0)
? (int)Math.Round(watchedTime.Value / (movie.Runtime * 60f) * 100f) percent = (int)Math.Round(watchedTime.Value / (movie.Runtime.Value * 60f) * 100f);
: null;
if (percent < MinWatchPercent) if (percent < MinWatchPercent)
return null; return null;
@ -259,6 +259,12 @@ public class WatchStatusRepository : IWatchStatusRepository
"Can't have a watched time if the status is not watching." "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 = MovieWatchStatus ret =
new() new()
{ {
@ -463,14 +469,14 @@ public class WatchStatusRepository : IWatchStatusRepository
Guid episodeId, Guid episodeId,
Guid userId, Guid userId,
WatchStatus status, WatchStatus status,
int? watchedTime int? watchedTime,
int? percent
) )
{ {
Episode episode = await _database.Episodes.FirstAsync(x => x.Id == episodeId); Episode episode = await _database.Episodes.FirstAsync(x => x.Id == episodeId);
int? percent =
watchedTime != null && episode.Runtime > 0 if (percent == null && watchedTime != null && episode.Runtime > 0)
? (int)Math.Round(watchedTime.Value / (episode.Runtime * 60f) * 100f) percent = (int)Math.Round(watchedTime.Value / (episode.Runtime.Value * 60f) * 100f);
: null;
if (percent < MinWatchPercent) if (percent < MinWatchPercent)
return null; return null;
@ -486,6 +492,12 @@ public class WatchStatusRepository : IWatchStatusRepository
"Can't have a watched time if the status is not watching." "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 = EpisodeWatchStatus ret =
new() new()
{ {

View File

@ -147,7 +147,8 @@ namespace Kyoo.Core.Api
/// </remarks> /// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Episode"/>.</param> /// <param name="identifier">The ID or slug of the <see cref="Episode"/>.</param>
/// <param name="status">The new watch status.</param> /// <param name="status">The new watch status.</param>
/// <param name="watchedTime">Where the user stopped watching.</param> /// <param name="watchedTime">Where the user stopped watching (in seconds).</param>
/// <param name="percent">Where the user stopped watching (in percent).</param>
/// <returns>The newly set status.</returns> /// <returns>The newly set status.</returns>
/// <response code="200">The status has been set</response> /// <response code="200">The status has been set</response>
/// <response code="204">The status was not considered impactfull enough to be saved (less then 5% of watched for example).</response> /// <response code="204">The status was not considered impactfull enough to be saved (less then 5% of watched for example).</response>
@ -161,7 +162,8 @@ namespace Kyoo.Core.Api
public async Task<EpisodeWatchStatus?> SetWatchStatus( public async Task<EpisodeWatchStatus?> SetWatchStatus(
Identifier identifier, Identifier identifier,
WatchStatus status, WatchStatus status,
int? watchedTime int? watchedTime,
int? percent
) )
{ {
Guid id = await identifier.Match( Guid id = await identifier.Match(
@ -170,7 +172,7 @@ namespace Kyoo.Core.Api
); );
return await _libraryManager return await _libraryManager
.WatchStatus .WatchStatus
.SetEpisodeStatus(id, User.GetIdOrThrow(), status, watchedTime); .SetEpisodeStatus(id, User.GetIdOrThrow(), status, watchedTime, percent);
} }
/// <summary> /// <summary>

View File

@ -196,6 +196,7 @@ namespace Kyoo.Core.Api
/// <param name="identifier">The ID or slug of the <see cref="Movie"/>.</param> /// <param name="identifier">The ID or slug of the <see cref="Movie"/>.</param>
/// <param name="status">The new watch status.</param> /// <param name="status">The new watch status.</param>
/// <param name="watchedTime">Where the user stopped watching.</param> /// <param name="watchedTime">Where the user stopped watching.</param>
/// <param name="percent">Where the user stopped watching (in percent).</param>
/// <returns>The newly set status.</returns> /// <returns>The newly set status.</returns>
/// <response code="200">The status has been set</response> /// <response code="200">The status has been set</response>
/// <response code="204">The status was not considered impactfull enough to be saved (less then 5% of watched for example).</response> /// <response code="204">The status was not considered impactfull enough to be saved (less then 5% of watched for example).</response>
@ -210,7 +211,8 @@ namespace Kyoo.Core.Api
public async Task<MovieWatchStatus?> SetWatchStatus( public async Task<MovieWatchStatus?> SetWatchStatus(
Identifier identifier, Identifier identifier,
WatchStatus status, WatchStatus status,
int? watchedTime int? watchedTime,
int? percent
) )
{ {
Guid id = await identifier.Match( Guid id = await identifier.Match(
@ -219,7 +221,7 @@ namespace Kyoo.Core.Api
); );
return await _libraryManager return await _libraryManager
.WatchStatus .WatchStatus
.SetMovieStatus(id, User.GetIdOrThrow(), status, watchedTime); .SetMovieStatus(id, User.GetIdOrThrow(), status, watchedTime, percent);
} }
/// <summary> /// <summary>

View File

@ -49,7 +49,7 @@ export const BaseEpisodeP = withImages(
/** /**
* How long is this movie? (in minutes). * 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. * The release date of this episode. It can be null if unknown.
*/ */

View File

@ -61,7 +61,7 @@ export const MovieP = withImages(
/** /**
* How long is this movie? (in minutes). * 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. * The date this movie aired. It can also be null if this is unknown.
*/ */

View File

@ -106,6 +106,10 @@ export const WatchInfoP = z.object({
* The sha1 of the video file. * The sha1 of the video file.
*/ */
sha: z.string(), sha: z.string(),
/**
* The duration of the video (in seconds).
*/
length: z.number(),
/** /**
* The internal path of the video file. * The internal path of the video file.
*/ */

View File

@ -57,7 +57,8 @@ export const episodeDisplayNumber = (
return def; return def;
}; };
export const displayRuntime = (runtime: number) => { export const displayRuntime = (runtime: number | null) => {
if (!runtime) return null;
if (runtime < 60) return `${runtime}min`; if (runtime < 60) return `${runtime}min`;
return `${Math.floor(runtime / 60)}h${runtime % 60}`; return `${Math.floor(runtime / 60)}h${runtime % 60}`;
}; };
@ -300,22 +301,19 @@ export const EpisodeLine = ({
)} )}
</Skeleton> </Skeleton>
<View {...css({ flexDirection: "row", alignItems: "center" })}> <View {...css({ flexDirection: "row", alignItems: "center" })}>
{isLoading || <Skeleton>
(runtime && ( {isLoading || (
<Skeleton> <SubP>
{isLoading || ( {/* Source https://www.i18next.com/translation-function/formatting#datetime */}
<SubP> {[
{/* Source https://www.i18next.com/translation-function/formatting#datetime */} releaseDate ? t("{{val, datetime}}", { val: releaseDate }) : null,
{[ displayRuntime(runtime),
releaseDate ? t("{{val, datetime}}", { val: releaseDate }) : null, ]
displayRuntime(runtime), .filter((item) => item != null)
] .join(" · ")}
.filter((item) => item != null) </SubP>
.join(" · ")} )}
</SubP> </Skeleton>
)}
</Skeleton>
))}
{slug && watchedStatus !== undefined && ( {slug && watchedStatus !== undefined && (
<EpisodesContext <EpisodesContext
slug={slug} slug={slug}

View File

@ -127,7 +127,7 @@ export const Player = ({ slug, type }: { slug: string; type: "episode" | "movie"
next={next} next={next}
previous={previous} previous={previous}
/> />
{data && <WatchStatusObserver type={type} slug={data.slug} />} {data && info && <WatchStatusObserver type={type} slug={data.slug} duration={info.length} />}
<View <View
{...css({ {...css({
flexGrow: 1, flexGrow: 1,

View File

@ -28,9 +28,11 @@ import { playAtom, progressAtom } from "./state";
export const WatchStatusObserver = ({ export const WatchStatusObserver = ({
type, type,
slug, slug,
duration,
}: { }: {
type: "episode" | "movie"; type: "episode" | "movie";
slug: string; slug: string;
duration: number;
}) => { }) => {
const account = useAccount(); const account = useAccount();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@ -47,9 +49,10 @@ export const WatchStatusObserver = ({
params: { params: {
status: WatchStatusV.Watching, status: WatchStatusV.Watching,
watchedTime: Math.round(seconds), watchedTime: Math.round(seconds),
percent: Math.round((seconds / duration) * 100),
}, },
}), }),
[_mutate, type, slug], [_mutate, type, slug, duration],
); );
const readProgress = useAtomCallback( const readProgress = useAtomCallback(
useCallback((get) => { useCallback((get) => {