diff --git a/back/src/Kyoo.Abstractions/Controllers/IIssueRepository.cs b/back/src/Kyoo.Abstractions/Controllers/IIssueRepository.cs
new file mode 100644
index 00000000..831e50bb
--- /dev/null
+++ b/back/src/Kyoo.Abstractions/Controllers/IIssueRepository.cs
@@ -0,0 +1,35 @@
+// 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 .
+
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Kyoo.Abstractions.Models;
+using Kyoo.Abstractions.Models.Utils;
+
+namespace Kyoo.Abstractions.Controllers;
+
+public interface IIssueRepository
+{
+ Task> GetAll(Filter? filter = default);
+
+ Task GetCount(Filter? filter = default);
+
+ Task Upsert(Issue issue);
+
+ Task DeleteAll(Filter? filter = default);
+}
diff --git a/back/src/Kyoo.Abstractions/Models/Issues.cs b/back/src/Kyoo.Abstractions/Models/Issues.cs
index acc62b76..851dc29f 100644
--- a/back/src/Kyoo.Abstractions/Models/Issues.cs
+++ b/back/src/Kyoo.Abstractions/Models/Issues.cs
@@ -45,7 +45,7 @@ public class Issue : IAddedDate
///
/// Some extra data that could store domain-specific info.
///
- public Dictionary Extra { get; set; }
+ public Dictionary Extra { get; set; } = new();
///
public DateTime AddedDate { get; set; }
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/EfHelpers.cs b/back/src/Kyoo.Core/Controllers/Repositories/EfHelpers.cs
new file mode 100644
index 00000000..76f0e731
--- /dev/null
+++ b/back/src/Kyoo.Core/Controllers/Repositories/EfHelpers.cs
@@ -0,0 +1,140 @@
+// 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 .
+
+using System;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using Kyoo.Abstractions.Models.Utils;
+using Kyoo.Postgresql;
+using Kyoo.Utils;
+
+namespace Kyoo.Core.Controllers;
+
+public static class EfHelpers
+{
+ public static Expression> ToEfLambda(this Filter? filter)
+ {
+ if (filter == null)
+ return x => true;
+
+ ParameterExpression x = Expression.Parameter(typeof(T), "x");
+
+ Expression CmpRandomHandler(string cmp, string seed, Guid refId)
+ {
+ MethodInfo concat = typeof(string).GetMethod(
+ nameof(string.Concat),
+ new[] { typeof(string), typeof(string) }
+ )!;
+ Expression id = Expression.Call(
+ Expression.Property(x, "ID"),
+ nameof(Guid.ToString),
+ null
+ );
+ Expression xrng = Expression.Call(concat, Expression.Constant(seed), id);
+ Expression left = Expression.Call(
+ typeof(DatabaseContext),
+ nameof(DatabaseContext.MD5),
+ null,
+ xrng
+ );
+ Expression right = Expression.Call(
+ typeof(DatabaseContext),
+ nameof(DatabaseContext.MD5),
+ null,
+ Expression.Constant($"{seed}{refId}")
+ );
+ return cmp switch
+ {
+ "=" => Expression.Equal(left, right),
+ "<" => Expression.GreaterThan(left, right),
+ ">" => Expression.LessThan(left, right),
+ _ => throw new NotImplementedException()
+ };
+ }
+
+ BinaryExpression StringCompatibleExpression(
+ Func operand,
+ string property,
+ object value
+ )
+ {
+ var left = Expression.Property(x, property);
+ var right = Expression.Constant(value, ((PropertyInfo)left.Member).PropertyType);
+ if (left.Type != typeof(string))
+ return operand(left, right);
+ MethodCallExpression call = Expression.Call(
+ typeof(string),
+ "Compare",
+ null,
+ left,
+ right
+ );
+ return operand(call, Expression.Constant(0));
+ }
+
+ Expression Exp(
+ Func operand,
+ string property,
+ object? value
+ )
+ {
+ var prop = Expression.Property(x, property);
+ var val = Expression.Constant(value, ((PropertyInfo)prop.Member).PropertyType);
+ return operand(prop, val);
+ }
+
+ Expression Parse(Filter f)
+ {
+ return f switch
+ {
+ Filter.And(var first, var second)
+ => Expression.AndAlso(Parse(first), Parse(second)),
+ Filter.Or(var first, var second)
+ => Expression.OrElse(Parse(first), Parse(second)),
+ Filter.Not(var inner) => Expression.Not(Parse(inner)),
+ Filter.Eq(var property, var value) => Exp(Expression.Equal, property, value),
+ Filter.Ne(var property, var value) => Exp(Expression.NotEqual, property, value),
+ Filter.Gt(var property, var value)
+ => StringCompatibleExpression(Expression.GreaterThan, property, value),
+ Filter.Ge(var property, var value)
+ => StringCompatibleExpression(Expression.GreaterThanOrEqual, property, value),
+ Filter.Lt(var property, var value)
+ => StringCompatibleExpression(Expression.LessThan, property, value),
+ Filter.Le(var property, var value)
+ => StringCompatibleExpression(Expression.LessThanOrEqual, property, value),
+ Filter.Has(var property, var value)
+ => Expression.Call(
+ typeof(Enumerable),
+ "Contains",
+ new[] { value.GetType() },
+ Expression.Property(x, property),
+ Expression.Constant(value)
+ ),
+ Filter.CmpRandom(var op, var seed, var refId)
+ => CmpRandomHandler(op, seed, refId),
+ Filter.Lambda(var lambda)
+ => ExpressionArgumentReplacer.ReplaceParams(lambda.Body, lambda.Parameters, x),
+ _ => throw new NotImplementedException(),
+ };
+ }
+
+ Expression body = Parse(filter);
+ return Expression.Lambda>(body, x);
+ }
+}
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/IssueRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/IssueRepository.cs
new file mode 100644
index 00000000..f4cb64ae
--- /dev/null
+++ b/back/src/Kyoo.Core/Controllers/Repositories/IssueRepository.cs
@@ -0,0 +1,54 @@
+// 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 .
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Kyoo.Abstractions.Controllers;
+using Kyoo.Abstractions.Models;
+using Kyoo.Abstractions.Models.Utils;
+using Kyoo.Postgresql;
+using Microsoft.EntityFrameworkCore;
+
+namespace Kyoo.Core.Controllers;
+
+public class IssueRepository(DatabaseContext database) : IIssueRepository
+{
+ public async Task> GetAll(Filter? filter = null)
+ {
+ return await database.Issues.Where(filter.ToEfLambda()).ToListAsync();
+ }
+
+ public Task GetCount(Filter? filter = null)
+ {
+ return database.Issues.Where(filter.ToEfLambda()).CountAsync();
+ }
+
+ public async Task Upsert(Issue issue)
+ {
+ issue.AddedDate = DateTime.UtcNow;
+ await database.Issues.Upsert(issue).RunAsync();
+ return issue;
+ }
+
+ public Task DeleteAll(Filter? filter = null)
+ {
+ return database.Issues.Where(filter.ToEfLambda()).ExecuteDeleteAsync();
+ }
+}
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs
index 38130e24..f502d380 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs
@@ -118,125 +118,6 @@ namespace Kyoo.Core.Controllers
return _Sort(query, sortBy, false).ThenBy(x => x.Id);
}
- protected Expression> ParseFilter(Filter? filter)
- {
- if (filter == null)
- return x => true;
-
- ParameterExpression x = Expression.Parameter(typeof(T), "x");
-
- Expression CmpRandomHandler(string cmp, string seed, Guid refId)
- {
- MethodInfo concat = typeof(string).GetMethod(
- nameof(string.Concat),
- new[] { typeof(string), typeof(string) }
- )!;
- Expression id = Expression.Call(
- Expression.Property(x, "ID"),
- nameof(Guid.ToString),
- null
- );
- Expression xrng = Expression.Call(concat, Expression.Constant(seed), id);
- Expression left = Expression.Call(
- typeof(DatabaseContext),
- nameof(DatabaseContext.MD5),
- null,
- xrng
- );
- Expression right = Expression.Call(
- typeof(DatabaseContext),
- nameof(DatabaseContext.MD5),
- null,
- Expression.Constant($"{seed}{refId}")
- );
- return cmp switch
- {
- "=" => Expression.Equal(left, right),
- "<" => Expression.GreaterThan(left, right),
- ">" => Expression.LessThan(left, right),
- _ => throw new NotImplementedException()
- };
- }
-
- BinaryExpression StringCompatibleExpression(
- Func operand,
- string property,
- object value
- )
- {
- var left = Expression.Property(x, property);
- var right = Expression.Constant(value, ((PropertyInfo)left.Member).PropertyType);
- if (left.Type != typeof(string))
- return operand(left, right);
- MethodCallExpression call = Expression.Call(
- typeof(string),
- "Compare",
- null,
- left,
- right
- );
- return operand(call, Expression.Constant(0));
- }
-
- Expression Exp(
- Func operand,
- string property,
- object? value
- )
- {
- var prop = Expression.Property(x, property);
- var val = Expression.Constant(value, ((PropertyInfo)prop.Member).PropertyType);
- return operand(prop, val);
- }
-
- Expression Parse(Filter f)
- {
- return f switch
- {
- Filter.And(var first, var second)
- => Expression.AndAlso(Parse(first), Parse(second)),
- Filter.Or(var first, var second)
- => Expression.OrElse(Parse(first), Parse(second)),
- Filter.Not(var inner) => Expression.Not(Parse(inner)),
- Filter.Eq(var property, var value) => Exp(Expression.Equal, property, value),
- Filter.Ne(var property, var value)
- => Exp(Expression.NotEqual, property, value),
- Filter.Gt(var property, var value)
- => StringCompatibleExpression(Expression.GreaterThan, property, value),
- Filter.Ge(var property, var value)
- => StringCompatibleExpression(
- Expression.GreaterThanOrEqual,
- property,
- value
- ),
- Filter.Lt(var property, var value)
- => StringCompatibleExpression(Expression.LessThan, property, value),
- Filter.Le(var property, var value)
- => StringCompatibleExpression(Expression.LessThanOrEqual, property, value),
- Filter.Has(var property, var value)
- => Expression.Call(
- typeof(Enumerable),
- "Contains",
- new[] { value.GetType() },
- Expression.Property(x, property),
- Expression.Constant(value)
- ),
- Filter.CmpRandom(var op, var seed, var refId)
- => CmpRandomHandler(op, seed, refId),
- Filter.Lambda(var lambda)
- => ExpressionArgumentReplacer.ReplaceParams(
- lambda.Body,
- lambda.Parameters,
- x
- ),
- _ => throw new NotImplementedException(),
- };
- }
-
- Expression body = Parse(filter);
- return Expression.Lambda>(body, x);
- }
-
protected IQueryable AddIncludes(IQueryable query, Include? include)
{
if (include == null)
@@ -405,7 +286,7 @@ namespace Kyoo.Core.Controllers
filter = Filter.And(filter, keysetFilter);
}
if (filter != null)
- query = query.Where(ParseFilter(filter));
+ query = query.Where(filter.ToEfLambda());
if (limit.Reverse)
query = query.Reverse();
@@ -422,7 +303,7 @@ namespace Kyoo.Core.Controllers
{
IQueryable query = Database.Set();
if (filter != null)
- query = query.Where(ParseFilter(filter));
+ query = query.Where(filter.ToEfLambda());
return query.CountAsync();
}
diff --git a/back/src/Kyoo.Core/CoreModule.cs b/back/src/Kyoo.Core/CoreModule.cs
index 121fe7b1..24138b52 100644
--- a/back/src/Kyoo.Core/CoreModule.cs
+++ b/back/src/Kyoo.Core/CoreModule.cs
@@ -75,6 +75,11 @@ namespace Kyoo.Core
.As()
.AsSelf()
.InstancePerLifetimeScope();
+ builder
+ .RegisterType()
+ .As()
+ .AsSelf()
+ .InstancePerLifetimeScope();
builder.RegisterType().InstancePerLifetimeScope();
}
diff --git a/back/src/Kyoo.Core/Views/Metadata/IssueApi.cs b/back/src/Kyoo.Core/Views/Metadata/IssueApi.cs
new file mode 100644
index 00000000..f1d0a83a
--- /dev/null
+++ b/back/src/Kyoo.Core/Views/Metadata/IssueApi.cs
@@ -0,0 +1,119 @@
+// 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 .
+
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Kyoo.Abstractions.Controllers;
+using Kyoo.Abstractions.Models;
+using Kyoo.Abstractions.Models.Attributes;
+using Kyoo.Abstractions.Models.Permissions;
+using Kyoo.Abstractions.Models.Utils;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using static Kyoo.Abstractions.Models.Utils.Constants;
+
+namespace Kyoo.Core.Api;
+
+///
+/// Create or list issues on the instance
+///
+[Route("issues")]
+[Route("issue", Order = AlternativeRoute)]
+[ApiController]
+[PartialPermission(nameof(Issue), Group = Group.Admin)]
+[ApiDefinition("Issue", Group = AdminGroup)]
+public class IssueApi(IIssueRepository issues) : Controller
+{
+ ///
+ /// Get count
+ ///
+ ///
+ /// Get the number of issues that match the filters.
+ ///
+ /// A list of filters to respect.
+ /// How many issues matched that filter.
+ /// Invalid filters.
+ [HttpGet("count")]
+ [PartialPermission(Kind.Read)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
+ public async Task> GetCount([FromQuery] Filter filter)
+ {
+ return await issues.GetCount(filter);
+ }
+
+ ///
+ /// Get all issues
+ ///
+ ///
+ /// Get all issues that match the given filter.
+ ///
+ /// Filter the returned items.
+ /// A list of issues that match every filters.
+ /// Invalid filters or sort information.
+ [HttpGet]
+ [PartialPermission(Kind.Read)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
+ public async Task>> GetAll([FromQuery] Filter? filter)
+ {
+ return Ok(await issues.GetAll(filter));
+ }
+
+ ///
+ /// Upsert issue
+ ///
+ ///
+ /// Create or update an issue.
+ ///
+ /// The issue to create.
+ /// The created issue.
+ /// The issue in the request body is invalid.
+ [HttpPost]
+ [PartialPermission(Kind.Create)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
+ public async Task> Create([FromBody] Issue issue)
+ {
+ return await issues.Upsert(issue);
+ }
+
+ ///
+ /// Delete issues
+ ///
+ ///
+ /// Delete all issues matching the given filters.
+ ///
+ /// The list of filters.
+ /// The item(s) has successfully been deleted.
+ /// One or multiple filters are invalid.
+ [HttpDelete]
+ [PartialPermission(Kind.Delete)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
+ public async Task Delete([FromQuery] Filter filter)
+ {
+ if (filter == null)
+ return BadRequest(
+ new RequestError("Incule a filter to delete items, all items won't be deleted.")
+ );
+
+ await issues.DeleteAll(filter);
+ return NoContent();
+ }
+}
diff --git a/back/src/Kyoo.Core/Views/Resources/UserApi.cs b/back/src/Kyoo.Core/Views/Resources/UserApi.cs
index a3fdb9ef..345037d6 100644
--- a/back/src/Kyoo.Core/Views/Resources/UserApi.cs
+++ b/back/src/Kyoo.Core/Views/Resources/UserApi.cs
@@ -39,7 +39,7 @@ namespace Kyoo.Core.Api;
[PartialPermission(nameof(User), Group = Group.Admin)]
[ApiDefinition("Users", Group = ResourcesGroup)]
public class UserApi(ILibraryManager libraryManager, IThumbnailsManager thumbs)
- : CrudApi(libraryManager.Users)
+ : CrudApi(libraryManager!.Users)
{
///
/// Get profile picture
diff --git a/back/src/Kyoo.Postgresql/DatabaseContext.cs b/back/src/Kyoo.Postgresql/DatabaseContext.cs
index 683a0fc5..2e559989 100644
--- a/back/src/Kyoo.Postgresql/DatabaseContext.cs
+++ b/back/src/Kyoo.Postgresql/DatabaseContext.cs
@@ -409,9 +409,7 @@ namespace Kyoo.Postgresql
modelBuilder.Entity().Ignore(x => x.Links);
-
- modelBuilder.Entity()
- .HasKey(x => new { x.Domain, x.Cause });
+ modelBuilder.Entity().HasKey(x => new { x.Domain, x.Cause });
// TODO: Waiting for https://github.com/dotnet/efcore/issues/29825
// modelBuilder.Entity()
@@ -419,7 +417,8 @@ namespace Kyoo.Postgresql
// {
// x.ToJson();
// });
- modelBuilder.Entity()
+ modelBuilder
+ .Entity()
.Property(x => x.Extra)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
diff --git a/back/src/Kyoo.Postgresql/Migrations/20240217143306_AddIssues.cs b/back/src/Kyoo.Postgresql/Migrations/20240217143306_AddIssues.cs
index 8c8a6ae4..f4763cf6 100644
--- a/back/src/Kyoo.Postgresql/Migrations/20240217143306_AddIssues.cs
+++ b/back/src/Kyoo.Postgresql/Migrations/20240217143306_AddIssues.cs
@@ -13,7 +13,8 @@ namespace Kyoo.Postgresql.Migrations
{
migrationBuilder.DropForeignKey(
name: "fk_show_watch_status_episodes_next_episode_id",
- table: "show_watch_status");
+ table: "show_watch_status"
+ );
migrationBuilder.CreateTable(
name: "issues",
@@ -23,12 +24,17 @@ namespace Kyoo.Postgresql.Migrations
cause = table.Column(type: "text", nullable: false),
reason = table.Column(type: "text", nullable: false),
extra = table.Column(type: "json", nullable: false),
- added_date = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'")
+ added_date = table.Column(
+ type: "timestamp with time zone",
+ nullable: false,
+ defaultValueSql: "now() at time zone 'utc'"
+ )
},
constraints: table =>
{
table.PrimaryKey("pk_issues", x => new { x.domain, x.cause });
- });
+ }
+ );
migrationBuilder.AddForeignKey(
name: "fk_show_watch_status_episodes_next_episode_id",
@@ -36,7 +42,8 @@ namespace Kyoo.Postgresql.Migrations
column: "next_episode_id",
principalTable: "episodes",
principalColumn: "id",
- onDelete: ReferentialAction.SetNull);
+ onDelete: ReferentialAction.SetNull
+ );
}
///
@@ -44,17 +51,18 @@ namespace Kyoo.Postgresql.Migrations
{
migrationBuilder.DropForeignKey(
name: "fk_show_watch_status_episodes_next_episode_id",
- table: "show_watch_status");
+ table: "show_watch_status"
+ );
- migrationBuilder.DropTable(
- name: "issues");
+ migrationBuilder.DropTable(name: "issues");
migrationBuilder.AddForeignKey(
name: "fk_show_watch_status_episodes_next_episode_id",
table: "show_watch_status",
column: "next_episode_id",
principalTable: "episodes",
- principalColumn: "id");
+ principalColumn: "id"
+ );
}
}
}