diff --git a/back/src/Kyoo.Abstractions/Controllers/IRepository.cs b/back/src/Kyoo.Abstractions/Controllers/IRepository.cs
index 9665e091..65f6df39 100644
--- a/back/src/Kyoo.Abstractions/Controllers/IRepository.cs
+++ b/back/src/Kyoo.Abstractions/Controllers/IRepository.cs
@@ -62,9 +62,18 @@ namespace Kyoo.Abstractions.Controllers
///
/// A predicate to filter the resource.
/// The related fields to include.
+ /// A custom sort method to handle cases where multiples items match the filters.
+ /// Reverse the sort.
+ /// Select the first element after this id if it was in a list.
/// If the item could not be found.
/// The resource found
- Task Get(Filter filter, Include? include = default);
+ Task Get(
+ Filter filter,
+ Include? include = default,
+ Sort? sortBy = default,
+ bool reverse = false,
+ Guid? afterId = default
+ );
///
/// Get a resource from it's ID or null if it is not found.
@@ -89,11 +98,13 @@ namespace Kyoo.Abstractions.Controllers
/// The related fields to include.
/// A custom sort method to handle cases where multiples items match the filters.
/// Reverse the sort.
+ /// Select the first element after this id if it was in a list.
/// The resource found
Task GetOrDefault(Filter? filter,
Include? include = default,
Sort? sortBy = default,
- bool reverse = false);
+ bool reverse = false,
+ Guid? afterId = default);
///
/// Search for resources with the database.
diff --git a/back/src/Kyoo.Abstractions/Models/Resources/WatchStatus.cs b/back/src/Kyoo.Abstractions/Models/Resources/WatchStatus.cs
index 1a72fbfb..c6f14bc5 100644
--- a/back/src/Kyoo.Abstractions/Models/Resources/WatchStatus.cs
+++ b/back/src/Kyoo.Abstractions/Models/Resources/WatchStatus.cs
@@ -216,22 +216,14 @@ namespace Kyoo.Abstractions.Models
///
/// Null if the status is not Watching or if the next episode is not started.
///
- [Projectable(UseMemberBody = nameof(_WatchedTime), NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)]
- [NotMapped]
public int? WatchedTime { get; set; }
- private int? _WatchedTime => NextEpisode?.Watched!.FirstOrDefault()?.WatchedTime;
-
///
/// Where the player has stopped watching the episode (in percentage between 0 and 100).
///
///
/// Null if the status is not Watching or if the next episode is not started.
///
- [Projectable(UseMemberBody = nameof(_WatchedPercent), NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)]
- [NotMapped]
public int? WatchedPercent { get; set; }
-
- private int? _WatchedPercent => NextEpisode?.Watched!.FirstOrDefault()?.WatchedPercent;
}
}
diff --git a/back/src/Kyoo.Abstractions/Models/Utils/Include.cs b/back/src/Kyoo.Abstractions/Models/Utils/Include.cs
index 87218ae2..42ea5c34 100644
--- a/back/src/Kyoo.Abstractions/Models/Utils/Include.cs
+++ b/back/src/Kyoo.Abstractions/Models/Utils/Include.cs
@@ -52,50 +52,41 @@ public class Include : Include
///
public ICollection Fields => Metadatas.Select(x => x.Name).ToList();
+ public Include() { }
+
+ public Include(params string[] fields)
+ {
+ Type[] types = typeof(T).GetCustomAttribute()?.Types ?? new[] { typeof(T) };
+ Metadatas = fields.SelectMany(key =>
+ {
+ var relations = types
+ .Select(x => x.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance)!)
+ .Select(prop => (prop, attr: prop?.GetCustomAttribute()!))
+ .Where(x => x.prop != null && x.attr != null)
+ .ToList();
+ if (!relations.Any())
+ throw new ValidationException($"No loadable relation with the name {key}.");
+ return relations
+ .Select(x =>
+ {
+ (PropertyInfo prop, LoadableRelationAttribute attr) = x;
+
+ if (attr.RelationID != null)
+ return new SingleRelation(prop.Name, prop.PropertyType, attr.RelationID) as Metadata;
+ if (attr.Sql != null)
+ return new CustomRelation(prop.Name, prop.PropertyType, attr.Sql, attr.On, prop.DeclaringType!);
+ if (attr.Projected != null)
+ return new ProjectedRelation(prop.Name, attr.Projected);
+ throw new NotImplementedException();
+ })
+ .Distinct();
+ }).ToArray();
+ }
+
public static Include From(string? fields)
{
if (string.IsNullOrEmpty(fields))
return new Include();
-
- Type[] types = typeof(T).GetCustomAttribute()?.Types ?? new[] { typeof(T) };
- return new Include
- {
- Metadatas = fields.Split(',').SelectMany(key =>
- {
- var relations = types
- .Select(x => x.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance)!)
- .Select(prop => (prop, attr: prop?.GetCustomAttribute()!))
- .Where(x => x.prop != null && x.attr != null)
- .ToList();
- if (!relations.Any())
- throw new ValidationException($"No loadable relation with the name {key}.");
- return relations
- .Select(x =>
- {
- (PropertyInfo prop, LoadableRelationAttribute attr) = x;
-
- if (attr.RelationID != null)
- return new SingleRelation(prop.Name, prop.PropertyType, attr.RelationID) as Metadata;
-
- // Multiples relations are disabled due to:
- // - Cartesian Explosions perfs
- // - Code complexity added.
- // if (typeof(IEnumerable).IsAssignableFrom(prop.PropertyType) && prop.PropertyType != typeof(string))
- // {
- // // The property is either a list or a an array.
- // return new MultipleRelation(
- // prop.Name,
- // prop.PropertyType.GetElementType() ?? prop.PropertyType.GenericTypeArguments.First()
- // );
- // }
- if (attr.Sql != null)
- return new CustomRelation(prop.Name, prop.PropertyType, attr.Sql, attr.On, prop.DeclaringType!);
- if (attr.Projected != null)
- return new ProjectedRelation(prop.Name, attr.Projected);
- throw new NotImplementedException();
- })
- .Distinct();
- }).ToArray()
- };
+ return new Include(fields.Split(','));
}
}
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs b/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs
index c6b1aaf6..fba4c5b4 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs
@@ -321,7 +321,8 @@ public static class DapperHelper
Include? include,
Filter? filter,
Sort? sort = null,
- bool reverse = false)
+ bool reverse = false,
+ Guid? afterId = default)
where T : class, IResource, IQuery
{
ICollection ret = await db.Query(
@@ -333,7 +334,7 @@ public static class DapperHelper
include,
filter,
sort,
- new Pagination(1, reverse: reverse)
+ new Pagination(1, afterId, reverse)
);
return ret.FirstOrDefault();
}
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs
index 7806fe65..0a7c05a1 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs
@@ -69,10 +69,13 @@ public abstract class DapperRepository : IRepository
}
///
- public virtual async Task Get(Filter filter,
- Include? include = default)
+ public virtual async Task Get(Filter? filter,
+ Include? include = default,
+ Sort? sortBy = default,
+ bool reverse = false,
+ Guid? afterId = default)
{
- T? ret = await GetOrDefault(filter, include: include);
+ T? ret = await GetOrDefault(filter, include, sortBy, reverse, afterId);
if (ret == null)
throw new ItemNotFoundException($"No {typeof(T).Name} found with the given predicate.");
return ret;
@@ -135,10 +138,11 @@ public abstract class DapperRepository : IRepository
}
///
- public Task GetOrDefault(Filter? filter,
- Include? include = null,
- Sort? sortBy = null,
- bool reverse = false)
+ public virtual Task GetOrDefault(Filter? filter,
+ Include? include = default,
+ Sort? sortBy = default,
+ bool reverse = false,
+ Guid? afterId = default)
{
return Database.QuerySingle(
Sql,
@@ -147,7 +151,9 @@ public abstract class DapperRepository : IRepository
Context,
include,
filter,
- sortBy
+ sortBy,
+ reverse,
+ afterId
);
}
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs
index 4f69ca5f..68d03cf5 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs
@@ -221,9 +221,15 @@ namespace Kyoo.Core.Controllers
}
///
- public virtual async Task Get(Filter filter, Include? include = default)
+ public virtual async Task Get(
+ Filter filter,
+ Include? include = default,
+ Sort? sortBy = default,
+ bool reverse = false,
+ Guid? afterId = default
+ )
{
- T? ret = await GetOrDefault(filter, include: include);
+ T? ret = await GetOrDefault(filter, include, sortBy, reverse, afterId);
if (ret == null)
throw new ItemNotFoundException($"No {typeof(T).Name} found with the given predicate.");
return ret;
@@ -255,18 +261,20 @@ namespace Kyoo.Core.Controllers
}
///
- public virtual Task GetOrDefault(Filter? filter,
+ public virtual async Task GetOrDefault(Filter? filter,
Include? include = default,
Sort? sortBy = default,
- bool reverse = false)
+ bool reverse = false,
+ Guid? afterId = default)
{
- IQueryable query = Sort(
- AddIncludes(Database.Set(), include),
- sortBy
+ IQueryable query = await ApplyFilters(
+ Database.Set(),
+ filter,
+ sortBy,
+ new Pagination(1, afterId, reverse),
+ include
);
- if (reverse)
- query = query.Reverse();
- return query.FirstOrDefaultAsync(ParseFilter(filter));
+ return await query.FirstOrDefaultAsync();
}
///
@@ -285,12 +293,13 @@ namespace Kyoo.Core.Controllers
public abstract Task> Search(string query, Include? include = default);
///
- public virtual Task> GetAll(Filter? filter = null,
+ public virtual async Task> GetAll(Filter? filter = null,
Sort? sort = default,
Include? include = default,
Pagination? limit = default)
{
- return ApplyFilters(Database.Set(), filter, sort, limit, include);
+ IQueryable query = await ApplyFilters(Database.Set(), filter, sort, limit, include);
+ return await query.ToListAsync();
}
///
@@ -302,7 +311,7 @@ namespace Kyoo.Core.Controllers
/// Pagination information (where to start and how many to get)
/// Related fields to also load with this query.
/// The filtered query
- protected async Task> ApplyFilters(IQueryable query,
+ protected async Task> ApplyFilters(IQueryable query,
Filter? filter = null,
Sort? sort = default,
Pagination? limit = default,
@@ -317,7 +326,6 @@ namespace Kyoo.Core.Controllers
T reference = await Get(limit.AfterID.Value);
Filter? keysetFilter = RepositoryHelper.KeysetPaginate(sort, reference, !limit.Reverse);
filter = Filter.And(filter, keysetFilter);
- Console.WriteLine(filter);
}
if (filter != null)
query = query.Where(ParseFilter(filter));
@@ -329,7 +337,7 @@ namespace Kyoo.Core.Controllers
if (limit.Reverse)
query = query.Reverse();
- return await query.ToListAsync();
+ return query;
}
///
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/WatchStatusRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/WatchStatusRepository.cs
index 13d84c4a..dd17cf86 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/WatchStatusRepository.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/WatchStatusRepository.cs
@@ -19,6 +19,7 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
+using System.Text.Json;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
@@ -98,7 +99,8 @@ public class WatchStatusRepository : IWatchStatusRepository
MovieId = movieId,
Status = status,
WatchedTime = watchedTime,
- PlayedDate = DateTime.UtcNow
+ AddedDate = DateTime.UtcNow,
+ PlayedDate = status == WatchStatus.Completed ? DateTime.UtcNow : null,
};
await _database.MovieWatchStatus.Upsert(ret)
.UpdateIf(x => status != Watching || x.Status != Completed)
@@ -135,23 +137,44 @@ public class WatchStatusRepository : IWatchStatusRepository
if (unseenEpisodeCount == 0)
status = WatchStatus.Completed;
+ Episode? cursor = null;
+ Guid? nextEpisodeId = null;
+ if (status == WatchStatus.Watching)
+ {
+ cursor = await _episodes.GetOrDefault(
+ new Filter.Lambda(
+ x => x.ShowId == showId
+ && (x.WatchStatus!.Status == WatchStatus.Completed
+ || x.WatchStatus.Status == WatchStatus.Watching)
+ ),
+ new Include(nameof(Episode.WatchStatus)),
+ reverse: true
+ );
+ nextEpisodeId = cursor?.WatchStatus?.Status == WatchStatus.Watching
+ ? cursor.Id
+ : ((await _episodes.GetOrDefault(
+ new Filter.Lambda(
+ x => x.ShowId == showId && x.WatchStatus!.Status != WatchStatus.Completed
+ ),
+ afterId: cursor?.Id
+ ))?.Id);
+ }
+
ShowWatchStatus ret = new()
{
UserId = userId,
ShowId = showId,
Status = status,
- NextEpisode = status == WatchStatus.Watching
- ? await _episodes.GetOrDefault(
- new Filter.Lambda(
- x => x.ShowId == showId
- && (x.WatchStatus!.Status == WatchStatus.Watching
- || x.WatchStatus.Status == WatchStatus.Completed)
- ),
- reverse: true
- )
+ AddedDate = DateTime.UtcNow,
+ NextEpisodeId = nextEpisodeId,
+ WatchedTime = cursor?.WatchStatus?.Status == WatchStatus.Watching
+ ? cursor.WatchStatus.WatchedTime
+ : null,
+ WatchedPercent = cursor?.WatchStatus?.Status == WatchStatus.Watching
+ ? cursor.WatchStatus.WatchedPercent
: null,
UnseenEpisodesCount = unseenEpisodeCount,
- PlayedDate = DateTime.UtcNow
+ PlayedDate = status == WatchStatus.Completed ? DateTime.UtcNow : null,
};
await _database.ShowWatchStatus.Upsert(ret)
.UpdateIf(x => status != Watching || x.Status != Completed)
@@ -210,7 +233,8 @@ public class WatchStatusRepository : IWatchStatusRepository
Status = status,
WatchedTime = watchedTime,
WatchedPercent = percent,
- PlayedDate = DateTime.UtcNow
+ AddedDate = DateTime.UtcNow,
+ PlayedDate = status == WatchStatus.Completed ? DateTime.UtcNow : null,
};
await _database.EpisodeWatchStatus.Upsert(ret)
.UpdateIf(x => status != Watching || x.Status != Completed)
diff --git a/back/src/Kyoo.Postgresql/Migrations/20231203194301_Watchlist.Designer.cs b/back/src/Kyoo.Postgresql/Migrations/20231204000849_Watchlist.Designer.cs
similarity index 99%
rename from back/src/Kyoo.Postgresql/Migrations/20231203194301_Watchlist.Designer.cs
rename to back/src/Kyoo.Postgresql/Migrations/20231204000849_Watchlist.Designer.cs
index 12456592..2d6f5523 100644
--- a/back/src/Kyoo.Postgresql/Migrations/20231203194301_Watchlist.Designer.cs
+++ b/back/src/Kyoo.Postgresql/Migrations/20231204000849_Watchlist.Designer.cs
@@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace Kyoo.Postgresql.Migrations
{
[DbContext(typeof(PostgresContext))]
- [Migration("20231203194301_Watchlist")]
+ [Migration("20231204000849_Watchlist")]
partial class Watchlist
{
///
@@ -511,6 +511,14 @@ namespace Kyoo.Postgresql.Migrations
.HasColumnType("integer")
.HasColumnName("unseen_episodes_count");
+ b.Property("WatchedPercent")
+ .HasColumnType("integer")
+ .HasColumnName("watched_percent");
+
+ b.Property("WatchedTime")
+ .HasColumnType("integer")
+ .HasColumnName("watched_time");
+
b.HasKey("UserId", "ShowId")
.HasName("pk_show_watch_status");
diff --git a/back/src/Kyoo.Postgresql/Migrations/20231203194301_Watchlist.cs b/back/src/Kyoo.Postgresql/Migrations/20231204000849_Watchlist.cs
similarity index 97%
rename from back/src/Kyoo.Postgresql/Migrations/20231203194301_Watchlist.cs
rename to back/src/Kyoo.Postgresql/Migrations/20231204000849_Watchlist.cs
index 4c893e21..cb699d9d 100644
--- a/back/src/Kyoo.Postgresql/Migrations/20231203194301_Watchlist.cs
+++ b/back/src/Kyoo.Postgresql/Migrations/20231204000849_Watchlist.cs
@@ -105,7 +105,9 @@ namespace Kyoo.Postgresql.Migrations
played_date = table.Column(type: "timestamp with time zone", nullable: true),
status = table.Column(type: "watch_status", nullable: false),
unseen_episodes_count = table.Column(type: "integer", nullable: false),
- next_episode_id = table.Column(type: "uuid", nullable: true)
+ next_episode_id = table.Column(type: "uuid", nullable: true),
+ watched_time = table.Column(type: "integer", nullable: true),
+ watched_percent = table.Column(type: "integer", nullable: true)
},
constraints: table =>
{
diff --git a/back/src/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs b/back/src/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs
index 1b236f4f..78f883ff 100644
--- a/back/src/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs
+++ b/back/src/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs
@@ -508,6 +508,14 @@ namespace Kyoo.Postgresql.Migrations
.HasColumnType("integer")
.HasColumnName("unseen_episodes_count");
+ b.Property("WatchedPercent")
+ .HasColumnType("integer")
+ .HasColumnName("watched_percent");
+
+ b.Property("WatchedTime")
+ .HasColumnType("integer")
+ .HasColumnName("watched_time");
+
b.HasKey("UserId", "ShowId")
.HasName("pk_show_watch_status");