Start watchlist implementations

This commit is contained in:
Zoe Roux 2023-11-12 17:15:41 +01:00
parent c1ba51b903
commit 4135fc5703
7 changed files with 166 additions and 24 deletions

View File

@ -39,5 +39,18 @@ namespace Kyoo.Authentication
return user.Claims.FirstOrDefault(x => x.Type == Claims.Permissions)?.Value.Split(',') return user.Claims.FirstOrDefault(x => x.Type == Claims.Permissions)?.Value.Split(',')
?? Array.Empty<string>(); ?? Array.Empty<string>();
} }
/// <summary>
/// Get the id of the current user or null if unlogged or invalid.
/// </summary>
/// <param name="user">The user.</param>
/// <returns>The id of the user or null.</returns>
public static Guid? GetId(this ClaimsPrincipal user)
{
Claim? value = user.FindFirst(Claims.Id);
if (Guid.TryParse(value?.Value, out Guid id))
return id;
return null;
}
} }
} }

View File

@ -19,6 +19,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq;
using EntityFrameworkCore.Projectables;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Utils; using Kyoo.Utils;
@ -144,6 +146,17 @@ namespace Kyoo.Abstractions.Models
Hls = $"/video/movie/{Slug}/master.m3u8", Hls = $"/video/movie/{Slug}/master.m3u8",
}; };
[SerializeIgnore] public ICollection<WatchInfo> Watched { get; set; }
/// <summary>
/// Metadata of what an user as started/planned to watch.
/// </summary>
[Projectable(UseMemberBody = nameof(_WatchInfo), OnlyOnInclude = true)]
[LoadableRelation] public WatchInfo? WatchInfo { get; set; }
// There is a global query filter to filter by user so we just need to do single.
private WatchInfo? _WatchInfo => Watched.FirstOrDefault();
/// <inheritdoc /> /// <inheritdoc />
public void OnMerge(object merged) public void OnMerge(object merged)
{ {

View File

@ -69,6 +69,12 @@ namespace Kyoo.Abstractions.Models
/// </summary> /// </summary>
public Image? Logo { get; set; } public Image? Logo { get; set; }
/// <summary>
/// The user's watch list.
/// </summary>
[SerializeIgnore]
public ICollection<WatchInfo>? Watchlist { get; set; }
public User() { } public User() { }
[JsonConstructor] [JsonConstructor]

View File

@ -0,0 +1,101 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using Kyoo.Abstractions.Models.Attributes;
namespace Kyoo.Abstractions.Models
{
/// <summary>
/// Has the user started watching, is it planned?
/// </summary>
public enum WatchStatus
{
/// <summary>
/// The user has already watched this.
/// </summary>
Completed,
/// <summary>
/// The user started watching this but has not finished.
/// </summary>
Watching,
/// <summary>
/// The user does not plan to continue watching.
/// </summary>
Droped,
/// <summary>
/// The user has not started watching this but plans to.
/// </summary>
Planned,
}
/// <summary>
/// Metadata of what an user as started/planned to watch.
/// </summary>
public class WatchInfo : IAddedDate
{
/// <summary>
/// The ID of the user that started watching this episode.
/// </summary>
[SerializeIgnore] public Guid UserId { get; set; }
/// <summary>
/// The user that started watching this episode.
/// </summary>
[SerializeIgnore] public User User { get; set; }
/// <summary>
/// The ID of the episode started.
/// </summary>
[SerializeIgnore] public Guid? EpisodeId { get; set; }
/// <summary>
/// The <see cref="Episode"/> started.
/// </summary>
[SerializeIgnore] public Episode? Episode { get; set; }
/// <summary>
/// The ID of the movie started.
/// </summary>
[SerializeIgnore] public Guid? MovieId { get; set; }
/// <summary>
/// The <see cref="Movie"/> started.
/// </summary>
[SerializeIgnore] public Movie? Movie { get; set; }
/// <inheritdoc/>
[SerializeIgnore] public DateTime AddedDate { get; set; }
/// <summary>
/// Has the user started watching, is it planned?
/// </summary>
public WatchStatus Status { get; set; }
/// <summary>
/// Where the player has stopped watching the episode (in seconds).
/// </summary>
/// <remarks>
/// Null if the status is not Watching.
/// </remarks>
public int? WatchedTime { get; set; }
}
}

View File

@ -27,6 +27,8 @@ using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.ValueGeneration; using Microsoft.EntityFrameworkCore.ValueGeneration;
@ -42,6 +44,8 @@ namespace Kyoo.Postgresql
/// </remarks> /// </remarks>
public abstract class DatabaseContext : DbContext public abstract class DatabaseContext : DbContext
{ {
private readonly IHttpContextAccessor _accessor;
/// <summary> /// <summary>
/// Calculate the MD5 of a string, can only be used in database context. /// Calculate the MD5 of a string, can only be used in database context.
/// </summary> /// </summary>
@ -49,6 +53,8 @@ namespace Kyoo.Postgresql
/// <returns>The hash</returns> /// <returns>The hash</returns>
public static string MD5(string str) => throw new NotSupportedException(); public static string MD5(string str) => throw new NotSupportedException();
public Guid? CurrentUserId => _accessor.HttpContext?.User.GetId();
/// <summary> /// <summary>
/// All collections of Kyoo. See <see cref="Collection"/>. /// All collections of Kyoo. See <see cref="Collection"/>.
/// </summary> /// </summary>
@ -94,6 +100,8 @@ namespace Kyoo.Postgresql
// /// </summary> // /// </summary>
// public DbSet<PeopleRole> PeopleRoles { get; set; } // public DbSet<PeopleRole> PeopleRoles { get; set; }
public DbSet<WatchInfo> WatchInfo { get; set; }
/// <summary> /// <summary>
/// Add a many to many link between two resources. /// Add a many to many link between two resources.
/// </summary> /// </summary>
@ -114,18 +122,16 @@ namespace Kyoo.Postgresql
}); });
} }
/// <summary> protected DatabaseContext(IHttpContextAccessor accessor)
/// The default constructor {
/// </summary> _accessor = accessor;
protected DatabaseContext() { } }
/// <summary> protected DatabaseContext(DbContextOptions options, IHttpContextAccessor accessor)
/// Create a new <see cref="DatabaseContext"/> using specific options
/// </summary>
/// <param name="options">The options to use.</param>
protected DatabaseContext(DbContextOptions options)
: base(options) : base(options)
{ } {
_accessor = accessor;
}
/// <summary> /// <summary>
/// Get the name of the link table of the two given types. /// Get the name of the link table of the two given types.
@ -296,6 +302,11 @@ namespace Kyoo.Postgresql
modelBuilder.Entity<User>().OwnsOne(x => x.Logo); modelBuilder.Entity<User>().OwnsOne(x => x.Logo);
modelBuilder.Entity<WatchInfo>()
.HasKey(x => new { User = x.UserId, Episode = x.EpisodeId, Movie = x.MovieId });
modelBuilder.Entity<WatchInfo>().HasQueryFilter(x => x.UserId == CurrentUserId);
modelBuilder.Entity<Movie>().Ignore(x => x.WatchInfo);
modelBuilder.Entity<Collection>() modelBuilder.Entity<Collection>()
.HasIndex(x => x.Slug) .HasIndex(x => x.Slug)
.IsUnique(); .IsUnique();

View File

@ -20,6 +20,8 @@ using System;
using System.Globalization; using System.Globalization;
using EFCore.NamingConventions.Internal; using EFCore.NamingConventions.Internal;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
using Kyoo.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Npgsql; using Npgsql;
@ -47,29 +49,24 @@ namespace Kyoo.Postgresql
{ {
NpgsqlConnection.GlobalTypeMapper.MapEnum<Status>(); NpgsqlConnection.GlobalTypeMapper.MapEnum<Status>();
NpgsqlConnection.GlobalTypeMapper.MapEnum<Genre>(); NpgsqlConnection.GlobalTypeMapper.MapEnum<Genre>();
NpgsqlConnection.GlobalTypeMapper.MapEnum<WatchStatus>();
} }
/// <summary> /// <summary>
/// A basic constructor that set default values (query tracker behaviors, mapping enums...) /// Design time constructor (dotnet ef migrations add). Do not use
/// </summary> /// </summary>
public PostgresContext() { } public PostgresContext()
: base(null!)
{ }
/// <summary> public PostgresContext(DbContextOptions options, IHttpContextAccessor accessor)
/// Create a new <see cref="PostgresContext"/> using specific options : base(options, accessor)
/// </summary>
/// <param name="options">The options to use.</param>
public PostgresContext(DbContextOptions options)
: base(options)
{ {
_skipConfigure = true; _skipConfigure = true;
} }
/// <summary> public PostgresContext(string connection, bool debugMode, IHttpContextAccessor accessor)
/// A basic constructor that set default values (query tracker behaviors, mapping enums...) : base(accessor)
/// </summary>
/// <param name="connection">The connection string to use</param>
/// <param name="debugMode">Is this instance in debug mode?</param>
public PostgresContext(string connection, bool debugMode)
{ {
_debugMode = debugMode; _debugMode = debugMode;
} }
@ -99,6 +96,7 @@ namespace Kyoo.Postgresql
{ {
modelBuilder.HasPostgresEnum<Status>(); modelBuilder.HasPostgresEnum<Status>();
modelBuilder.HasPostgresEnum<Genre>(); modelBuilder.HasPostgresEnum<Genre>();
modelBuilder.HasPostgresEnum<WatchStatus>();
modelBuilder.HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(MD5))!) modelBuilder.HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(MD5))!)
.HasTranslation(args => .HasTranslation(args =>