diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs
index 3085ae1b..4b3008cd 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs
@@ -18,50 +18,152 @@
using System;
using System.Collections.Generic;
-using System.Linq;
+using System.Data.Common;
+using System.IO;
using System.Linq.Expressions;
using System.Threading.Tasks;
+using Dapper;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
+using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Abstractions.Models.Utils;
-using Kyoo.Postgresql;
-using Microsoft.EntityFrameworkCore;
namespace Kyoo.Core.Controllers
{
///
/// A local repository to handle library items.
///
- public class LibraryItemRepository : LocalRepository
+ public class LibraryItemRepository : IRepository
{
- ///
- /// The database handle
- ///
- private readonly DatabaseContext _database;
+ private readonly DbConnection _database;
- ///
- protected override Sort DefaultSort => new Sort.By(x => x.Name);
+ protected Sort DefaultSort => new Sort.By(x => x.Name);
- ///
- /// Create a new .
- ///
- /// The database instance
- /// The thumbnail manager used to store images.
- public LibraryItemRepository(DatabaseContext database, IThumbnailsManager thumbs)
- : base(database, thumbs)
+ public Type RepositoryType => typeof(LibraryItem);
+
+ public LibraryItemRepository(DbConnection database)
{
_database = database;
}
- ///
- public override async Task> Search(string query, Include? include = default)
+ ///
+ public virtual async Task Get(int id, Include? include = default)
{
- return await Sort(
- AddIncludes(_database.LibraryItems, include)
- .Where(_database.Like(x => x.Name, $"%{query}%"))
- )
- .Take(20)
- .ToListAsync();
+ LibraryItem? ret = await GetOrDefault(id, include);
+ if (ret == null)
+ throw new ItemNotFoundException($"No {nameof(LibraryItem)} found with the id {id}");
+ return ret;
+ }
+
+ ///
+ public virtual async Task Get(string slug, Include? include = default)
+ {
+ LibraryItem? ret = await GetOrDefault(slug, include);
+ if (ret == null)
+ throw new ItemNotFoundException($"No {nameof(LibraryItem)} found with the slug {slug}");
+ return ret;
+ }
+
+ ///
+ public virtual async Task Get(
+ Expression> where,
+ Include? include = default)
+ {
+ LibraryItem? ret = await GetOrDefault(where, include: include);
+ if (ret == null)
+ throw new ItemNotFoundException($"No {nameof(LibraryItem)} found with the given predicate.");
+ return ret;
+ }
+
+ public Task GetOrDefault(int id, Include? include = null)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task GetOrDefault(string slug, Include? include = null)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task GetOrDefault(Expression> where, Include? include = null, Sort? sortBy = null)
+ {
+ throw new NotImplementedException();
+ }
+
+ public async Task> GetAll(
+ Expression>? where = null,
+ Sort? sort = null,
+ Pagination? limit = null,
+ Include? include = null)
+ {
+ List ret = new(limit.Limit);
+
+ // language=PostgreSQL
+ string sql = @"
+ select s.*, m.*, c.* from shows as s full outer join (
+ select * from movies
+ ) as m on false
+ full outer join (
+ select * from collections
+ ) as c on false
+ ";
+
+ var data = await _database.QueryAsync(sql, new[] { typeof(Show), typeof(Movie), typeof(Collection) }, items =>
+ {
+ if (items[0] is Show show && show.Id != 0)
+ return show;
+ if (items[1] is Movie movie && movie.Id != 0)
+ return movie;
+ if (items[2] is Collection collection && collection.Id != 0)
+ return collection;
+ throw new InvalidDataException();
+ }, where);
+
+ // await using DbDataReader reader = await _database.ExecuteReaderAsync(sql);
+ // int kindOrdinal = reader.GetOrdinal("kind");
+ // var showParser = reader.GetRowParser(typeof(Show));
+ // var movieParser = reader.GetRowParser(typeof(Movie));
+ // var collectionParser = reader.GetRowParser(typeof(Collection));
+ //
+ // while (await reader.ReadAsync())
+ // {
+ // ItemKind type = await reader.GetFieldValueAsync(kindOrdinal);
+ // ret.Add(type switch
+ // {
+ // ItemKind.Show => showParser(reader),
+ // ItemKind.Movie => movieParser(reader),
+ // ItemKind.Collection => collectionParser(reader),
+ // _ => throw new InvalidDataException(),
+ // });
+ // }
+ throw new NotImplementedException();
+ // return ret;
+ }
+
+ public Task GetCount(Expression>? where = null)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task> FromIds(IList ids, Include? include = null)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task DeleteAll(Expression> where)
+ {
+ throw new NotImplementedException();
+ }
+ ///
+ public async Task> Search(string query, Include? include = default)
+ {
+ throw new NotImplementedException();
+ // return await Sort(
+ // AddIncludes(_database.LibraryItems, include)
+ // .Where(_database.Like(x => x.Name, $"%{query}%"))
+ // )
+ // .Take(20)
+ // .ToListAsync();
}
public async Task> GetAllOfCollection(
@@ -71,48 +173,49 @@ namespace Kyoo.Core.Controllers
Pagination? limit = default,
Include? include = default)
{
- return await ApplyFilters(
- _database.LibraryItems
- .Where(item =>
- _database.Movies
- .Where(x => x.Id == -item.Id)
- .Any(x => x.Collections!.AsQueryable().Any(selector))
- || _database.Shows
- .Where(x => x.Id == item.Id)
- .Any(x => x.Collections!.AsQueryable().Any(selector))
- ),
- where,
- sort,
- limit,
- include);
+ throw new NotImplementedException();
+ // return await ApplyFilters(
+ // _database.LibraryItems
+ // .Where(item =>
+ // _database.Movies
+ // .Where(x => x.Id == -item.Id)
+ // .Any(x => x.Collections!.AsQueryable().Any(selector))
+ // || _database.Shows
+ // .Where(x => x.Id == item.Id)
+ // .Any(x => x.Collections!.AsQueryable().Any(selector))
+ // ),
+ // where,
+ // sort,
+ // limit,
+ // include);
}
///
- public override Task Create(LibraryItem obj)
+ public Task Create(LibraryItem obj)
=> throw new InvalidOperationException();
///
- public override Task CreateIfNotExists(LibraryItem obj)
+ public Task CreateIfNotExists(LibraryItem obj)
=> throw new InvalidOperationException();
///
- public override Task Edit(LibraryItem edited)
+ public Task Edit(LibraryItem edited)
=> throw new InvalidOperationException();
///
- public override Task Patch(int id, Func> patch)
+ public Task Patch(int id, Func> patch)
=> throw new InvalidOperationException();
///
- public override Task Delete(int id)
+ public Task Delete(int id)
=> throw new InvalidOperationException();
///
- public override Task Delete(string slug)
+ public Task Delete(string slug)
=> throw new InvalidOperationException();
///
- public override Task Delete(LibraryItem obj)
+ public Task Delete(LibraryItem obj)
=> throw new InvalidOperationException();
}
}
diff --git a/back/src/Kyoo.Core/Kyoo.Core.csproj b/back/src/Kyoo.Core/Kyoo.Core.csproj
index 3ad70404..1f9bb856 100644
--- a/back/src/Kyoo.Core/Kyoo.Core.csproj
+++ b/back/src/Kyoo.Core/Kyoo.Core.csproj
@@ -8,6 +8,7 @@
+
diff --git a/back/src/Kyoo.Postgresql/Kyoo.Postgresql.csproj b/back/src/Kyoo.Postgresql/Kyoo.Postgresql.csproj
index 29710b92..b852ee67 100644
--- a/back/src/Kyoo.Postgresql/Kyoo.Postgresql.csproj
+++ b/back/src/Kyoo.Postgresql/Kyoo.Postgresql.csproj
@@ -6,6 +6,7 @@
+
diff --git a/back/src/Kyoo.Postgresql/PostgresModule.cs b/back/src/Kyoo.Postgresql/PostgresModule.cs
index 1f7f46c9..3a4440e5 100644
--- a/back/src/Kyoo.Postgresql/PostgresModule.cs
+++ b/back/src/Kyoo.Postgresql/PostgresModule.cs
@@ -17,8 +17,14 @@
// along with Kyoo. If not, see .
using System;
+using System.Collections.Generic;
using System.Data.Common;
+using System.Text.RegularExpressions;
+using Dapper;
+using EFCore.NamingConventions.Internal;
using Kyoo.Abstractions.Controllers;
+using Kyoo.Abstractions.Models;
+using Kyoo.Postgresql.Utils;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
@@ -69,29 +75,44 @@ namespace Kyoo.Postgresql
using NpgsqlConnection conn = (NpgsqlConnection)context.Database.GetDbConnection();
conn.Open();
conn.ReloadTypes();
+
+ SqlMapper.TypeMapProvider = (type) =>
+ {
+ return new CustomPropertyTypeMap(type, (type, name) =>
+ {
+ string newName = Regex.Replace(name, "(^|_)([a-z])", (match) => match.Groups[2].Value.ToUpperInvariant());
+ // TODO: Add images handling here (name: poster_source, newName: PosterSource) should set Poster.Source
+ return type.GetProperty(newName)!;
+ });
+ };
+ SqlMapper.AddTypeHandler(typeof(Dictionary), new JsonTypeHandler>());
+ SqlMapper.AddTypeHandler(typeof(List), new ListTypeHandler());
+ SqlMapper.AddTypeHandler(typeof(List), new ListTypeHandler());
}
///
public void Configure(IServiceCollection services)
{
+ DbConnectionStringBuilder builder = new()
+ {
+ ["USER ID"] = _configuration.GetValue("POSTGRES_USER", "KyooUser"),
+ ["PASSWORD"] = _configuration.GetValue("POSTGRES_PASSWORD", "KyooPassword"),
+ ["SERVER"] = _configuration.GetValue("POSTGRES_SERVER", "db"),
+ ["PORT"] = _configuration.GetValue("POSTGRES_PORT", "5432"),
+ ["DATABASE"] = _configuration.GetValue("POSTGRES_DB", "kyooDB"),
+ ["POOLING"] = "true",
+ ["MAXPOOLSIZE"] = "95",
+ ["TIMEOUT"] = "30"
+ };
+
services.AddDbContext(x =>
{
- DbConnectionStringBuilder builder = new()
- {
- ["USER ID"] = _configuration.GetValue("POSTGRES_USER", "KyooUser"),
- ["PASSWORD"] = _configuration.GetValue("POSTGRES_PASSWORD", "KyooPassword"),
- ["SERVER"] = _configuration.GetValue("POSTGRES_SERVER", "db"),
- ["PORT"] = _configuration.GetValue("POSTGRES_PORT", "5432"),
- ["DATABASE"] = _configuration.GetValue("POSTGRES_DB", "kyooDB"),
- ["POOLING"] = "true",
- ["MAXPOOLSIZE"] = "95",
- ["TIMEOUT"] = "30"
- };
x.UseNpgsql(builder.ConnectionString)
.UseProjectables();
if (_environment.IsDevelopment())
x.EnableDetailedErrors().EnableSensitiveDataLogging();
}, ServiceLifetime.Transient);
+ services.AddTransient((_) => new NpgsqlConnection(builder.ConnectionString));
services.AddHealthChecks().AddDbContextCheck();
}
diff --git a/back/src/Kyoo.Postgresql/Utils/JsonTypeHandler.cs b/back/src/Kyoo.Postgresql/Utils/JsonTypeHandler.cs
new file mode 100644
index 00000000..1eff28a2
--- /dev/null
+++ b/back/src/Kyoo.Postgresql/Utils/JsonTypeHandler.cs
@@ -0,0 +1,42 @@
+// 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.Data;
+using Newtonsoft.Json;
+using Npgsql;
+using NpgsqlTypes;
+using static Dapper.SqlMapper;
+
+namespace Kyoo.Postgresql.Utils;
+
+public class JsonTypeHandler : TypeHandler
+ where T : class
+{
+ public override T? Parse(object value)
+ {
+ if (value is string str)
+ return JsonConvert.DeserializeObject(str);
+ return default;
+ }
+
+ public override void SetValue(IDbDataParameter parameter, T? value)
+ {
+ parameter.Value = JsonConvert.SerializeObject(value);
+ ((NpgsqlParameter)parameter).NpgsqlDbType = NpgsqlDbType.Jsonb;
+ }
+}
diff --git a/back/src/Kyoo.Postgresql/Utils/ListTypeHandler.cs b/back/src/Kyoo.Postgresql/Utils/ListTypeHandler.cs
new file mode 100644
index 00000000..8f20f9ba
--- /dev/null
+++ b/back/src/Kyoo.Postgresql/Utils/ListTypeHandler.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using Dapper;
+
+namespace Kyoo.Postgresql.Utils;
+
+// See https://github.com/DapperLib/Dapper/issues/1424
+public class ListTypeHandler : SqlMapper.TypeHandler>
+{
+ public override List Parse(object value)
+ {
+ T[] typedValue = (T[])value; // looks like Dapper did not indicate the property type to Npgsql, so it defaults to string[] (default CLR type for text[] PostgreSQL type)
+ return typedValue?.ToList() ?? new();
+ }
+
+ public override void SetValue(IDbDataParameter parameter, List? value)
+ {
+ parameter.Value = value; // no need to convert to string[] in this direction
+ }
+}
diff --git a/back/tests/Kyoo.Tests/Database/RepositoryActivator.cs b/back/tests/Kyoo.Tests/Database/RepositoryActivator.cs
index 6a7a864c..08b914fe 100644
--- a/back/tests/Kyoo.Tests/Database/RepositoryActivator.cs
+++ b/back/tests/Kyoo.Tests/Database/RepositoryActivator.cs
@@ -54,7 +54,7 @@ namespace Kyoo.Tests.Database
MovieRepository movies = new(_NewContext(), studio, people, thumbs.Object);
ShowRepository show = new(_NewContext(), studio, people, thumbs.Object);
SeasonRepository season = new(_NewContext(), thumbs.Object);
- LibraryItemRepository libraryItem = new(_NewContext(), thumbs.Object);
+ LibraryItemRepository libraryItem = new(_NewContext());
EpisodeRepository episode = new(_NewContext(), show, thumbs.Object);
UserRepository user = new(_NewContext(), thumbs.Object);
diff --git a/shell.nix b/shell.nix
index 46e12cf1..21e7baea 100644
--- a/shell.nix
+++ b/shell.nix
@@ -27,6 +27,7 @@ in
postgresql_15
eslint_d
prettierd
+ pgformatter
];
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";