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 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);

View File

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

View File

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

View File

@ -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()
{

View File

@ -147,7 +147,8 @@ namespace Kyoo.Core.Api
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Episode"/>.</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>
/// <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>
@ -161,7 +162,8 @@ namespace Kyoo.Core.Api
public async Task<EpisodeWatchStatus?> 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);
}
/// <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="status">The new watch status.</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>
/// <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>
@ -210,7 +211,8 @@ namespace Kyoo.Core.Api
public async Task<MovieWatchStatus?> 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);
}
/// <summary>

View File

@ -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.
*/

View File

@ -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.
*/

View File

@ -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.
*/

View File

@ -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 = ({
)}
</Skeleton>
<View {...css({ flexDirection: "row", alignItems: "center" })}>
{isLoading ||
(runtime && (
<Skeleton>
{isLoading || (
<SubP>
{/* Source https://www.i18next.com/translation-function/formatting#datetime */}
{[
releaseDate ? t("{{val, datetime}}", { val: releaseDate }) : null,
displayRuntime(runtime),
]
.filter((item) => item != null)
.join(" · ")}
</SubP>
)}
</Skeleton>
))}
<Skeleton>
{isLoading || (
<SubP>
{/* Source https://www.i18next.com/translation-function/formatting#datetime */}
{[
releaseDate ? t("{{val, datetime}}", { val: releaseDate }) : null,
displayRuntime(runtime),
]
.filter((item) => item != null)
.join(" · ")}
</SubP>
)}
</Skeleton>
{slug && watchedStatus !== undefined && (
<EpisodesContext
slug={slug}

View File

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

View File

@ -28,9 +28,11 @@ import { playAtom, progressAtom } from "./state";
export const WatchStatusObserver = ({
type,
slug,
duration,
}: {
type: "episode" | "movie";
slug: string;
duration: number;
}) => {
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) => {