mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-10-31 02:27:11 -04:00 
			
		
		
		
	Add watchlists on news and library items
This commit is contained in:
		
							parent
							
								
									948c98f95b
								
							
						
					
					
						commit
						6fbd00a38f
					
				| @ -0,0 +1,37 @@ | ||||
| // 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.Utils; | ||||
| 
 | ||||
| namespace Kyoo.Abstractions.Models.Attributes; | ||||
| 
 | ||||
| [AttributeUsage(AttributeTargets.Class)] | ||||
| public class SqlFirstColumnAttribute : Attribute | ||||
| { | ||||
| 	/// <summary> | ||||
| 	/// The name of the first column of the element. Used to split multiples | ||||
| 	/// items on a single sql query. If not specified, it defaults to "Id". | ||||
| 	/// </summary> | ||||
| 	public string Name { get; set; } | ||||
| 
 | ||||
| 	public SqlFirstColumnAttribute(string name) | ||||
| 	{ | ||||
| 		Name = name.ToSnakeCase(); | ||||
| 	} | ||||
| } | ||||
| @ -244,7 +244,11 @@ namespace Kyoo.Abstractions.Models | ||||
| 		/// Metadata of what an user as started/planned to watch. | ||||
| 		/// </summary> | ||||
| 		[Projectable(UseMemberBody = nameof(_WatchStatus), OnlyOnInclude = true)] | ||||
| 		[LoadableRelation] public EpisodeWatchStatus? WatchStatus { get; set; } | ||||
| 		[LoadableRelation( | ||||
| 			Sql = "episode_watch_status", | ||||
| 			On = "episode_id = \"this\".id and \"relation\".user_id = [current_user]" | ||||
| 		)] | ||||
| 		public EpisodeWatchStatus? WatchStatus { get; set; } | ||||
| 
 | ||||
| 		// There is a global query filter to filter by user so we just need to do single. | ||||
| 		private EpisodeWatchStatus? _WatchStatus => Watched!.FirstOrDefault(); | ||||
|  | ||||
| @ -20,6 +20,7 @@ using System; | ||||
| using System.ComponentModel; | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.Globalization; | ||||
| using Kyoo.Abstractions.Models.Attributes; | ||||
| using Newtonsoft.Json; | ||||
| 
 | ||||
| namespace Kyoo.Abstractions.Models | ||||
| @ -47,6 +48,7 @@ namespace Kyoo.Abstractions.Models | ||||
| 	} | ||||
| 
 | ||||
| 	[TypeConverter(typeof(ImageConvertor))] | ||||
| 	[SqlFirstColumn(nameof(Source))] | ||||
| 	public class Image | ||||
| 	{ | ||||
| 		/// <summary> | ||||
|  | ||||
| @ -152,7 +152,11 @@ namespace Kyoo.Abstractions.Models | ||||
| 		/// Metadata of what an user as started/planned to watch. | ||||
| 		/// </summary> | ||||
| 		[Projectable(UseMemberBody = nameof(_WatchStatus), OnlyOnInclude = true)] | ||||
| 		[LoadableRelation] public MovieWatchStatus? WatchStatus { get; set; } | ||||
| 		[LoadableRelation( | ||||
| 			Sql = "movie_watch_status", | ||||
| 			On = "movie_id = \"this\".id and \"relation\".user_id = [current_user]" | ||||
| 		)] | ||||
| 		public MovieWatchStatus? WatchStatus { get; set; } | ||||
| 
 | ||||
| 		// There is a global query filter to filter by user so we just need to do single. | ||||
| 		private MovieWatchStatus? _WatchStatus => Watched!.FirstOrDefault(); | ||||
|  | ||||
| @ -193,7 +193,11 @@ namespace Kyoo.Abstractions.Models | ||||
| 		/// Metadata of what an user as started/planned to watch. | ||||
| 		/// </summary> | ||||
| 		[Projectable(UseMemberBody = nameof(_WatchStatus), OnlyOnInclude = true)] | ||||
| 		[LoadableRelation] public ShowWatchStatus? WatchStatus { get; set; } | ||||
| 		[LoadableRelation( | ||||
| 			Sql = "show_watch_status", | ||||
| 			On = "show_id = \"this\".id and \"relation\".user_id = [current_user]" | ||||
| 		)] | ||||
| 		public ShowWatchStatus? WatchStatus { get; set; } | ||||
| 
 | ||||
| 		// There is a global query filter to filter by user so we just need to do single. | ||||
| 		private ShowWatchStatus? _WatchStatus => Watched!.FirstOrDefault(); | ||||
|  | ||||
| @ -53,6 +53,7 @@ namespace Kyoo.Abstractions.Models | ||||
| 	/// <summary> | ||||
| 	/// Metadata of what an user as started/planned to watch. | ||||
| 	/// </summary> | ||||
| 	[SqlFirstColumn(nameof(UserId))] | ||||
| 	public class MovieWatchStatus : IAddedDate | ||||
| 	{ | ||||
| 		/// <summary> | ||||
| @ -105,6 +106,7 @@ namespace Kyoo.Abstractions.Models | ||||
| 		public int? WatchedPercent { get; set; } | ||||
| 	} | ||||
| 
 | ||||
| 	[SqlFirstColumn(nameof(UserId))] | ||||
| 	public class EpisodeWatchStatus : IAddedDate | ||||
| 	{ | ||||
| 		/// <summary> | ||||
| @ -157,6 +159,7 @@ namespace Kyoo.Abstractions.Models | ||||
| 		public int? WatchedPercent { get; set; } | ||||
| 	} | ||||
| 
 | ||||
| 	[SqlFirstColumn(nameof(UserId))] | ||||
| 	public class ShowWatchStatus : IAddedDate | ||||
| 	{ | ||||
| 		/// <summary> | ||||
|  | ||||
| @ -28,15 +28,35 @@ using System.Text.RegularExpressions; | ||||
| using System.Threading.Tasks; | ||||
| using Dapper; | ||||
| using InterpolatedSql.Dapper; | ||||
| using InterpolatedSql.Dapper.SqlBuilders; | ||||
| using Kyoo.Abstractions.Controllers; | ||||
| using Kyoo.Abstractions.Models; | ||||
| using Kyoo.Abstractions.Models.Attributes; | ||||
| using Kyoo.Abstractions.Models.Utils; | ||||
| using Kyoo.Authentication; | ||||
| using Kyoo.Utils; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| 
 | ||||
| namespace Kyoo.Core.Controllers; | ||||
| 
 | ||||
| public static class DapperHelper | ||||
| { | ||||
| 	public static SqlBuilder ProcessVariables(SqlBuilder sql, SqlVariableContext context) | ||||
| 	{ | ||||
| 		int start = 0; | ||||
| 		while ((start = sql.IndexOf("[", start, false)) != -1) | ||||
| 		{ | ||||
| 			int end = sql.IndexOf("]", start, false); | ||||
| 			if (end == -1) | ||||
| 				throw new ArgumentException("Invalid sql variable substitue (missing ])"); | ||||
| 			string var = sql.Format[(start + 1)..end]; | ||||
| 			sql.Remove(start, end - start + 1); | ||||
| 			sql.Insert(start, $"{context.ReadVar(var)}"); | ||||
| 		} | ||||
| 
 | ||||
| 		return sql; | ||||
| 	} | ||||
| 
 | ||||
| 	public static string Property(string key, Dictionary<string, Type> config) | ||||
| 	{ | ||||
| 		if (key == "kind") | ||||
| @ -95,10 +115,12 @@ public static class DapperHelper | ||||
| 					string owner = config.First(x => x.Value == declaring).Key; | ||||
| 					string lateral = sql.Contains("\"this\"") ? " lateral" : string.Empty; | ||||
| 					sql = sql.Replace("\"this\"", owner); | ||||
| 					on = on?.Replace("\"this\"", owner); | ||||
| 					on = on?.Replace("\"this\"", owner)?.Replace("\"relation\"", $"r{relation}"); | ||||
| 					if (sql.Any(char.IsWhiteSpace)) | ||||
| 						sql = $"({sql})"; | ||||
| 					types.Add(type); | ||||
| 					projection.AppendLine($", r{relation}.*"); | ||||
| 					join.Append($"\nleft join{lateral} ({sql}) as r{relation} on r{relation}.{on}"); | ||||
| 					join.Append($"\nleft join{lateral} {sql} as r{relation} on r{relation}.{on}"); | ||||
| 					break; | ||||
| 				case Include.ProjectedRelation: | ||||
| 					continue; | ||||
| @ -197,13 +219,14 @@ public static class DapperHelper | ||||
| 		Dictionary<string, Type> config, | ||||
| 		Func<List<object?>, T> mapper, | ||||
| 		Func<Guid, Task<T>> get, | ||||
| 		SqlVariableContext context, | ||||
| 		Include<T>? include, | ||||
| 		Filter<T>? filter, | ||||
| 		Sort<T>? sort, | ||||
| 		Pagination? limit) | ||||
| 		where T : class, IResource, IQuery | ||||
| 	{ | ||||
| 		InterpolatedSql.Dapper.SqlBuilders.SqlBuilder query = new(db, command); | ||||
| 		SqlBuilder query = new(db, command); | ||||
| 
 | ||||
| 		// Include handling | ||||
| 		include ??= new(); | ||||
| @ -227,6 +250,8 @@ public static class DapperHelper | ||||
| 		if (limit != null) | ||||
| 			query += $"\nlimit {limit.Limit}"; | ||||
| 
 | ||||
| 		ProcessVariables(query, context); | ||||
| 
 | ||||
| 		// Build query and prepare to do the query/projections | ||||
| 		IDapperSqlCommand cmd = query.Build(); | ||||
| 		string sql = cmd.Sql; | ||||
| @ -280,7 +305,7 @@ public static class DapperHelper | ||||
| 				return mapIncludes(mapper(nItems), nItems.Skip(config.Count)); | ||||
| 			}, | ||||
| 			ParametersDictionary.LoadFrom(cmd), | ||||
| 			splitOn: string.Join(',', types.Select(x => x == typeof(Image) ? "source" : "id")) | ||||
| 			splitOn: string.Join(',', types.Select(x => x.GetCustomAttribute<SqlFirstColumnAttribute>()?.Name ?? "id")) | ||||
| 		); | ||||
| 		if (limit?.Reverse == true) | ||||
| 			data = data.Reverse(); | ||||
| @ -292,6 +317,7 @@ public static class DapperHelper | ||||
| 		FormattableString command, | ||||
| 		Dictionary<string, Type> config, | ||||
| 		Func<List<object?>, T> mapper, | ||||
| 		SqlVariableContext context, | ||||
| 		Include<T>? include, | ||||
| 		Filter<T>? filter, | ||||
| 		Sort<T>? sort = null, | ||||
| @ -303,6 +329,7 @@ public static class DapperHelper | ||||
| 			config, | ||||
| 			mapper, | ||||
| 			get: null!, | ||||
| 			context, | ||||
| 			include, | ||||
| 			filter, | ||||
| 			sort, | ||||
| @ -315,6 +342,7 @@ public static class DapperHelper | ||||
| 		this IDbConnection db, | ||||
| 		FormattableString command, | ||||
| 		Dictionary<string, Type> config, | ||||
| 		SqlVariableContext context, | ||||
| 		Filter<T>? filter) | ||||
| 		where T : class, IResource | ||||
| 	{ | ||||
| @ -322,7 +350,7 @@ public static class DapperHelper | ||||
| 
 | ||||
| 		if (filter != null) | ||||
| 			query += ProcessFilter(filter, config); | ||||
| 
 | ||||
| 		ProcessVariables(query, context); | ||||
| 		IDapperSqlCommand cmd = query.Build(); | ||||
| 
 | ||||
| 		// language=postgreSQL | ||||
| @ -334,3 +362,22 @@ public static class DapperHelper | ||||
| 		); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| public class SqlVariableContext | ||||
| { | ||||
| 	private readonly IHttpContextAccessor _accessor; | ||||
| 
 | ||||
| 	public SqlVariableContext(IHttpContextAccessor accessor) | ||||
| 	{ | ||||
| 		_accessor = accessor; | ||||
| 	} | ||||
| 
 | ||||
| 	public object? ReadVar(string var) | ||||
| 	{ | ||||
| 		return var switch | ||||
| 		{ | ||||
| 			"current_user" => _accessor.HttpContext?.User.GetId(), | ||||
| 			_ => throw new ArgumentException($"Invalid sql variable name: {var}") | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -25,7 +25,6 @@ using Kyoo.Abstractions.Controllers; | ||||
| using Kyoo.Abstractions.Models; | ||||
| using Kyoo.Abstractions.Models.Exceptions; | ||||
| using Kyoo.Abstractions.Models.Utils; | ||||
| using static Kyoo.Core.Controllers.DapperHelper; | ||||
| 
 | ||||
| namespace Kyoo.Core.Controllers; | ||||
| 
 | ||||
| @ -42,9 +41,13 @@ public abstract class DapperRepository<T> : IRepository<T> | ||||
| 
 | ||||
| 	protected DbConnection Database { get; init; } | ||||
| 
 | ||||
| 	public DapperRepository(DbConnection database) | ||||
| 	protected SqlVariableContext Context { get; init; } | ||||
| 
 | ||||
| 
 | ||||
| 	public DapperRepository(DbConnection database, SqlVariableContext context) | ||||
| 	{ | ||||
| 		Database = database; | ||||
| 		Context = context; | ||||
| 	} | ||||
| 
 | ||||
| 	/// <inheritdoc/> | ||||
| @ -83,6 +86,7 @@ public abstract class DapperRepository<T> : IRepository<T> | ||||
| 				Config, | ||||
| 				Mapper, | ||||
| 				(id) => Get(id), | ||||
| 				Context, | ||||
| 				include, | ||||
| 				Filter.Or(ids.Select(x => new Filter<T>.Eq("id", x)).ToArray()), | ||||
| 				sort: null, | ||||
| @ -99,6 +103,7 @@ public abstract class DapperRepository<T> : IRepository<T> | ||||
| 			Sql, | ||||
| 			Config, | ||||
| 			Mapper, | ||||
| 			Context, | ||||
| 			include, | ||||
| 			new Filter<T>.Eq(nameof(IResource.Id), id) | ||||
| 		); | ||||
| @ -113,6 +118,7 @@ public abstract class DapperRepository<T> : IRepository<T> | ||||
| 				Sql, | ||||
| 				Config, | ||||
| 				Mapper, | ||||
| 				Context, | ||||
| 				include, | ||||
| 				filter: null, | ||||
| 				new Sort<T>.Random() | ||||
| @ -122,6 +128,7 @@ public abstract class DapperRepository<T> : IRepository<T> | ||||
| 			Sql, | ||||
| 			Config, | ||||
| 			Mapper, | ||||
| 			Context, | ||||
| 			include, | ||||
| 			new Filter<T>.Eq(nameof(IResource.Slug), slug) | ||||
| 		); | ||||
| @ -137,6 +144,7 @@ public abstract class DapperRepository<T> : IRepository<T> | ||||
| 			Sql, | ||||
| 			Config, | ||||
| 			Mapper, | ||||
| 			Context, | ||||
| 			include, | ||||
| 			filter, | ||||
| 			sortBy | ||||
| @ -154,6 +162,7 @@ public abstract class DapperRepository<T> : IRepository<T> | ||||
| 			Config, | ||||
| 			Mapper, | ||||
| 			(id) => Get(id), | ||||
| 			Context, | ||||
| 			include, | ||||
| 			filter, | ||||
| 			sort ?? new Sort<T>.Default(), | ||||
| @ -167,6 +176,7 @@ public abstract class DapperRepository<T> : IRepository<T> | ||||
| 		return Database.Count( | ||||
| 			Sql, | ||||
| 			Config, | ||||
| 			Context, | ||||
| 			filter | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| @ -76,8 +76,8 @@ namespace Kyoo.Core.Controllers | ||||
| 			throw new InvalidDataException(); | ||||
| 		} | ||||
| 
 | ||||
| 		public LibraryItemRepository(DbConnection database) | ||||
| 			: base(database) | ||||
| 		public LibraryItemRepository(DbConnection database, SqlVariableContext context) | ||||
| 			: base(database, context) | ||||
| 		{ } | ||||
| 
 | ||||
| 		public async Task<ICollection<ILibraryItem>> GetAllOfCollection( | ||||
| @ -118,6 +118,7 @@ namespace Kyoo.Core.Controllers | ||||
| 				}, | ||||
| 				Mapper, | ||||
| 				(id) => Get(id), | ||||
| 				Context, | ||||
| 				include, | ||||
| 				filter, | ||||
| 				sort ?? new Sort<ILibraryItem>.Default(), | ||||
|  | ||||
| @ -60,8 +60,8 @@ namespace Kyoo.Core.Controllers | ||||
| 			throw new InvalidDataException(); | ||||
| 		} | ||||
| 
 | ||||
| 		public NewsRepository(DbConnection database) | ||||
| 			: base(database) | ||||
| 		public NewsRepository(DbConnection database, SqlVariableContext context) | ||||
| 			: base(database, context) | ||||
| 		{ } | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -68,6 +68,7 @@ namespace Kyoo.Core | ||||
| 			builder.RegisterRepository<UserRepository>(); | ||||
| 			builder.RegisterRepository<NewsRepository>(); | ||||
| 			builder.RegisterType<WatchStatusRepository>().As<IWatchStatusRepository>().AsSelf().InstancePerLifetimeScope(); | ||||
| 			builder.RegisterType<SqlVariableContext>().InstancePerLifetimeScope(); | ||||
| 		} | ||||
| 
 | ||||
| 		/// <inheritdoc /> | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user