diff --git a/.env.example b/.env.example
index c70dc9fc..30ffddba 100644
--- a/.env.example
+++ b/.env.example
@@ -75,5 +75,6 @@ MEILI_HOST="http://meilisearch:7700"
MEILI_MASTER_KEY="ghvjkgisbgkbgskegblfqbgjkebbhgwkjfb"
RABBITMQ_HOST=rabbitmq
+RABBITMQ_PORT=5672
RABBITMQ_DEFAULT_USER=kyoo
RABBITMQ_DEFAULT_PASS=aohohunuhouhuhhoahothonseuhaoensuthoaentsuhha
diff --git a/autosync/autosync/__init__.py b/autosync/autosync/__init__.py
index 8b1eda73..58727949 100644
--- a/autosync/autosync/__init__.py
+++ b/autosync/autosync/__init__.py
@@ -46,6 +46,7 @@ def main():
connection = pika.BlockingConnection(
pika.ConnectionParameters(
host=os.environ.get("RABBITMQ_HOST", "rabbitmq"),
+ port=os.environ.get("RABBITMQ_PORT", 5672),
credentials=pika.credentials.PlainCredentials(
os.environ.get("RABBITMQ_DEFAULT_USER", "guest"),
os.environ.get("RABBITMQ_DEFAULT_PASS", "guest"),
diff --git a/autosync/autosync/services/simkl.py b/autosync/autosync/services/simkl.py
index c78319d9..74df8cbc 100644
--- a/autosync/autosync/services/simkl.py
+++ b/autosync/autosync/services/simkl.py
@@ -56,7 +56,7 @@ class Simkl(Service):
]
},
headers={
- "Authorization": f"Bearer {user.external_id["simkl"].token.access_token}",
+ "Authorization": f"Bearer {user.external_id['simkl'].token.access_token}",
"simkl-api-key": self._api_key,
},
)
@@ -85,7 +85,7 @@ class Simkl(Service):
]
},
headers={
- "Authorization": f"Bearer {user.external_id["simkl"].token.access_token}",
+ "Authorization": f"Bearer {user.external_id['simkl'].token.access_token}",
"simkl-api-key": self._api_key,
},
)
diff --git a/back/Dockerfile b/back/Dockerfile
index a29161df..b2c5e2b4 100644
--- a/back/Dockerfile
+++ b/back/Dockerfile
@@ -22,7 +22,7 @@ FROM mcr.microsoft.com/dotnet/aspnet:8.0
RUN apt-get update && apt-get install -y curl
COPY --from=builder /app /app
-WORKDIR /kyoo
+WORKDIR /app
EXPOSE 5000
# The back can take a long time to start if meilisearch is initializing
HEALTHCHECK --interval=5s --retries=15 CMD curl --fail http://localhost:5000/health || exit
diff --git a/back/Dockerfile.dev b/back/Dockerfile.dev
index 6f3e1a27..e33a87a5 100644
--- a/back/Dockerfile.dev
+++ b/back/Dockerfile.dev
@@ -14,7 +14,7 @@ COPY src/Kyoo.RabbitMq/Kyoo.RabbitMq.csproj src/Kyoo.RabbitMq/Kyoo.RabbitMq.cspr
COPY src/Kyoo.Swagger/Kyoo.Swagger.csproj src/Kyoo.Swagger/Kyoo.Swagger.csproj
RUN dotnet restore
-WORKDIR /kyoo
+WORKDIR /app
EXPOSE 5000
ENV DOTNET_USE_POLLING_FILE_WATCHER 1
# HEALTHCHECK --interval=5s CMD curl --fail http://localhost:5000/health || exit
diff --git a/back/Dockerfile.migrations b/back/Dockerfile.migrations
index 55377552..2278e642 100644
--- a/back/Dockerfile.migrations
+++ b/back/Dockerfile.migrations
@@ -19,7 +19,10 @@ RUN dotnet restore -a $TARGETARCH
COPY . .
RUN dotnet build
-RUN dotnet ef migrations bundle --no-build --self-contained -r linux-${TARGETARCH} -f -o /app/migrate -p src/Kyoo.Postgresql --verbose
+RUN dotnet ef migrations bundle \
+ --msbuildprojectextensionspath out/obj/Kyoo.Postgresql \
+ --no-build --self-contained -r linux-${TARGETARCH} -f \
+ -o /app/migrate -p src/Kyoo.Postgresql --verbose
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0
COPY --from=builder /app/migrate /app/migrate
diff --git a/back/src/Directory.Build.props b/back/src/Directory.Build.props
index 3b9f144a..76832704 100644
--- a/back/src/Directory.Build.props
+++ b/back/src/Directory.Build.props
@@ -28,6 +28,11 @@
true
+
+ $(MsBuildThisFileDirectory)/../out/obj/$(MSBuildProjectName)
+ $(MsBuildThisFileDirectory)/../out/bin/$(MSBuildProjectName)
+
+
diff --git a/back/src/Kyoo.Abstractions/Controllers/IThumbnailsManager.cs b/back/src/Kyoo.Abstractions/Controllers/IThumbnailsManager.cs
index 715fbedc..21e68f70 100644
--- a/back/src/Kyoo.Abstractions/Controllers/IThumbnailsManager.cs
+++ b/back/src/Kyoo.Abstractions/Controllers/IThumbnailsManager.cs
@@ -23,56 +23,19 @@ using Kyoo.Abstractions.Models;
namespace Kyoo.Abstractions.Controllers;
-///
-/// Download images and retrieve the path of those images for a resource.
-///
public interface IThumbnailsManager
{
- ///
- /// Download images of a specified item.
- /// If no images is available to download, do nothing and silently return.
- ///
- ///
- /// The item to cache images.
- ///
- /// The type of the item
- /// A representing the asynchronous operation.
Task DownloadImages(T item)
where T : IThumbnails;
- ///
- /// Retrieve the local path of an image of the given item.
- ///
- /// The item to retrieve the poster from.
- /// The ID of the image.
- /// The quality of the image
- /// The type of the item
- /// The path of the image for the given resource or null if it does not exists.
- string GetImagePath(T item, string image, ImageQuality quality)
- where T : IThumbnails;
+ Task DownloadImage(Image? image, string what);
+
+ string GetImagePath(Guid imageId, ImageQuality quality);
- ///
- /// Delete images associated with the item.
- ///
- ///
- /// The item with cached images.
- ///
- /// The type of the item
- /// A representing the asynchronous operation.
Task DeleteImages(T item)
where T : IThumbnails;
- ///
- /// Set the user's profile picture
- ///
- /// The id of the user.
- /// The byte stream of the image. Null if no image exist.
Task GetUserImage(Guid userId);
- ///
- /// Set the user's profile picture
- ///
- /// The id of the user.
- /// The byte stream of the image. Null to delete the image.
Task SetUserImage(Guid userId, Stream? image);
}
diff --git a/back/src/Kyoo.Abstractions/Models/Attributes/Permission/PermissionAttribute.cs b/back/src/Kyoo.Abstractions/Models/Attributes/Permission/PermissionAttribute.cs
index 29aad1e4..ba1ff743 100644
--- a/back/src/Kyoo.Abstractions/Models/Attributes/Permission/PermissionAttribute.cs
+++ b/back/src/Kyoo.Abstractions/Models/Attributes/Permission/PermissionAttribute.cs
@@ -94,7 +94,7 @@ public class PermissionAttribute : Attribute, IFilterFactory
///
/// The group of this permission.
///
- public Group Group { get; }
+ public Group Group { get; set; }
///
/// Ask a permission to run an action.
diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs
index 5095dfe4..69fbca66 100644
--- a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs
+++ b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs
@@ -17,12 +17,9 @@
// along with Kyoo. If not, see .
using System;
-using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
-using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
-using Kyoo.Abstractions.Models.Attributes;
namespace Kyoo.Abstractions.Models;
@@ -49,9 +46,13 @@ public interface IThumbnails
}
[JsonConverter(typeof(ImageConvertor))]
-[SqlFirstColumn(nameof(Source))]
public class Image
{
+ ///
+ /// A unique identifier for the image. Used for proper http caches.
+ ///
+ public Guid Id { get; set; }
+
///
/// The original image from another server.
///
@@ -63,6 +64,21 @@ public class Image
[MaxLength(32)]
public string Blurhash { get; set; }
+ ///
+ /// The url to access the image in low quality.
+ ///
+ public string Low => $"/thumbnails/{Id}?quality=low";
+
+ ///
+ /// The url to access the image in medium quality.
+ ///
+ public string Medium => $"/thumbnails/{Id}?quality=medium";
+
+ ///
+ /// The url to access the image in high quality.
+ ///
+ public string High => $"/thumbnails/{Id}?quality=high";
+
public Image() { }
[JsonConstructor]
@@ -72,6 +88,7 @@ public class Image
Blurhash = blurhash ?? "000000";
}
+ //
public class ImageConvertor : JsonConverter
{
///
@@ -84,7 +101,10 @@ public class Image
if (reader.TokenType == JsonTokenType.String && reader.GetString() is string source)
return new Image(source);
using JsonDocument document = JsonDocument.ParseValue(ref reader);
- return document.RootElement.Deserialize();
+ string? src = document.RootElement.GetProperty("Source").GetString();
+ string? blurhash = document.RootElement.GetProperty("Blurhash").GetString();
+ Guid? id = document.RootElement.GetProperty("Id").GetGuid();
+ return new Image(src ?? string.Empty, blurhash) { Id = id ?? Guid.Empty };
}
///
@@ -97,6 +117,9 @@ public class Image
writer.WriteStartObject();
writer.WriteString("source", value.Source);
writer.WriteString("blurhash", value.Blurhash);
+ writer.WriteString("low", value.Low);
+ writer.WriteString("medium", value.Medium);
+ writer.WriteString("high", value.High);
writer.WriteEndObject();
}
}
diff --git a/back/src/Kyoo.Abstractions/Models/Utils/Constants.cs b/back/src/Kyoo.Abstractions/Models/Utils/Constants.cs
index 6db78c51..f12c44d5 100644
--- a/back/src/Kyoo.Abstractions/Models/Utils/Constants.cs
+++ b/back/src/Kyoo.Abstractions/Models/Utils/Constants.cs
@@ -56,4 +56,5 @@ public static class Constants
/// A group name for . It should be used for endpoints used by admins.
///
public const string AdminGroup = "4:Admin";
+ public const string OtherGroup = "5:Other";
}
diff --git a/back/src/Kyoo.Abstractions/Utility/EnumerableExtensions.cs b/back/src/Kyoo.Abstractions/Utility/EnumerableExtensions.cs
deleted file mode 100644
index 4c3b72b0..00000000
--- a/back/src/Kyoo.Abstractions/Utility/EnumerableExtensions.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-// 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;
-
-namespace Kyoo.Utils;
-
-///
-/// A set of extensions class for enumerable.
-///
-public static class EnumerableExtensions
-{
- ///
- /// If the enumerable is empty, execute an action.
- ///
- /// The enumerable to check
- /// The action to execute is the list is empty
- /// The type of items inside the list
- /// The iterator proxied, there is no dual iterations.
- public static IEnumerable IfEmpty(this IEnumerable self, Action action)
- {
- static IEnumerable Generator(IEnumerable self, Action action)
- {
- using IEnumerator enumerator = self.GetEnumerator();
-
- if (!enumerator.MoveNext())
- {
- action();
- yield break;
- }
-
- do
- {
- yield return enumerator.Current;
- } while (enumerator.MoveNext());
- }
-
- return Generator(self, action);
- }
-
- ///
- /// A foreach used as a function with a little specificity: the list can be null.
- ///
- /// The list to enumerate. If this is null, the function result in a no-op
- /// The action to execute for each arguments
- /// The type of items in the list
- public static void ForEach(this IEnumerable? self, Action action)
- {
- if (self == null)
- return;
- foreach (T i in self)
- action(i);
- }
-}
diff --git a/back/src/Kyoo.Abstractions/Utility/JsonKindResolver.cs b/back/src/Kyoo.Abstractions/Utility/JsonKindResolver.cs
index 45cdfdaa..db72f7e2 100644
--- a/back/src/Kyoo.Abstractions/Utility/JsonKindResolver.cs
+++ b/back/src/Kyoo.Abstractions/Utility/JsonKindResolver.cs
@@ -25,7 +25,6 @@ using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
-using Microsoft.AspNetCore.Http;
using static System.Text.Json.JsonNamingPolicy;
namespace Kyoo.Utils;
diff --git a/back/src/Kyoo.Abstractions/Utility/Merger.cs b/back/src/Kyoo.Abstractions/Utility/Merger.cs
deleted file mode 100644
index a97530ef..00000000
--- a/back/src/Kyoo.Abstractions/Utility/Merger.cs
+++ /dev/null
@@ -1,133 +0,0 @@
-// 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.Reflection;
-using Kyoo.Abstractions.Models.Attributes;
-
-namespace Kyoo.Utils;
-
-///
-/// A class containing helper methods to merge objects.
-///
-public static class Merger
-{
- ///
- /// Merge two dictionary, if the same key is found on both dictionary, the values of the second one is kept.
- ///
- /// The first dictionary to merge
- /// The second dictionary to merge
- ///
- /// true if a new items has been added to the dictionary, false otherwise.
- ///
- /// The type of the keys in dictionaries
- /// The type of values in the dictionaries
- ///
- /// A dictionary with the missing elements of
- /// set to those of .
- ///
- public static IDictionary? CompleteDictionaries(
- IDictionary? first,
- IDictionary? second,
- out bool hasChanged
- )
- {
- if (first == null)
- {
- hasChanged = true;
- return second;
- }
-
- hasChanged = false;
- if (second == null)
- return first;
- hasChanged = second.Any(x =>
- !first.ContainsKey(x.Key) || x.Value?.Equals(first[x.Key]) == false
- );
- foreach ((T key, T2 value) in first)
- second.TryAdd(key, value);
- return second;
- }
-
- ///
- /// Set every non-default values of seconds to the corresponding property of second.
- /// Dictionaries are handled like anonymous objects with a property per key/pair value
- /// At the end, the OnMerge method of first will be called if first is a
- ///
- ///
- /// {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "foo"}
- ///
- ///
- /// The object to complete
- ///
- ///
- /// Missing fields of first will be completed by fields of this item. If second is null, the function no-op.
- ///
- ///
- /// Filter fields that will be merged
- ///
- /// Fields of T will be completed
- ///
- public static T Complete(T first, T? second, Func? where = null)
- {
- if (second == null)
- return first;
-
- Type type = typeof(T);
- IEnumerable properties = type.GetProperties()
- .Where(x =>
- x is { CanRead: true, CanWrite: true }
- && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null
- );
-
- if (where != null)
- properties = properties.Where(where);
-
- foreach (PropertyInfo property in properties)
- {
- object? value = property.GetValue(second);
-
- if (value?.Equals(property.GetValue(first)) == true)
- continue;
-
- if (Utility.IsOfGenericType(property.PropertyType, typeof(IDictionary<,>)))
- {
- Type[] dictionaryTypes = Utility
- .GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>))!
- .GenericTypeArguments;
- object?[] parameters = { property.GetValue(first), value, false };
- object newDictionary = Utility.RunGenericMethod(
- typeof(Merger),
- nameof(CompleteDictionaries),
- dictionaryTypes,
- parameters
- )!;
- if ((bool)parameters[2]!)
- property.SetValue(first, newDictionary);
- }
- else
- property.SetValue(first, value);
- }
-
- if (first is IOnMerge merge)
- merge.OnMerge(second);
- return first;
- }
-}
diff --git a/back/src/Kyoo.Abstractions/Utility/Utility.cs b/back/src/Kyoo.Abstractions/Utility/Utility.cs
index 422d673d..9e085457 100644
--- a/back/src/Kyoo.Abstractions/Utility/Utility.cs
+++ b/back/src/Kyoo.Abstractions/Utility/Utility.cs
@@ -177,25 +177,6 @@ public static class Utility
yield return type;
}
- ///
- /// Check if inherit from a generic type .
- ///
- /// The type to check
- /// The generic type to check against (Only generic types are supported like typeof(IEnumerable<>).
- /// True if obj inherit from genericType. False otherwise
- public static bool IsOfGenericType(Type type, Type genericType)
- {
- if (!genericType.IsGenericType)
- throw new ArgumentException($"{nameof(genericType)} is not a generic type.");
-
- IEnumerable types = genericType.IsInterface
- ? type.GetInterfaces()
- : type.GetInheritanceTree();
- return types
- .Prepend(type)
- .Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
- }
-
///
/// Get the generic definition of .
/// For example, calling this function with List<string> and typeof(IEnumerable<>) will return IEnumerable<string>
@@ -217,147 +198,6 @@ public static class Utility
.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
}
- ///
- /// Retrieve a method from an with the given name and respect the
- /// amount of parameters and generic parameters. This works for polymorphic methods.
- ///
- ///
- /// The type owning the method. For non static methods, this is the this .
- ///
- ///
- /// The binding flags of the method. This allow you to specify public/private and so on.
- ///
- ///
- /// The name of the method.
- ///
- ///
- /// The list of generic parameters.
- ///
- ///
- /// The list of parameters.
- ///
- /// No method match the given constraints.
- /// The method handle of the matching method.
- public static MethodInfo GetMethod(
- Type type,
- BindingFlags flag,
- string name,
- Type[] generics,
- object?[] args
- )
- {
- MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public)
- .Where(x => x.Name == name)
- .Where(x => x.GetGenericArguments().Length == generics.Length)
- .Where(x => x.GetParameters().Length == args.Length)
- .IfEmpty(() =>
- {
- throw new ArgumentException(
- $"A method named {name} with "
- + $"{args.Length} arguments and {generics.Length} generic "
- + $"types could not be found on {type.Name}."
- );
- })
- // TODO this won't work but I don't know why.
- // .Where(x =>
- // {
- // int i = 0;
- // return x.GetGenericArguments().All(y => y.IsAssignableFrom(generics[i++]));
- // })
- // .IfEmpty(() => throw new NullReferenceException($"No method {name} match the generics specified."))
-
- // TODO this won't work for Type because T is specified in arguments but not in the parameters type.
- // .Where(x =>
- // {
- // int i = 0;
- // return x.GetParameters().All(y => y.ParameterType.IsInstanceOfType(args[i++]));
- // })
- // .IfEmpty(() => throw new NullReferenceException($"No method {name} match the parameters's types."))
- .Take(2)
- .ToArray();
-
- if (methods.Length == 1)
- return methods[0];
- throw new ArgumentException(
- $"Multiple methods named {name} match the generics and parameters constraints."
- );
- }
-
- ///
- /// Run a generic static method for a runtime .
- ///
- ///
- /// To run Merger.MergeLists{T} for a List where you don't know the type at compile type,
- /// you could do:
- ///
- /// Utility.RunGenericMethod<object>(
- /// typeof(Utility),
- /// nameof(MergeLists),
- /// enumerableType,
- /// oldValue, newValue, equalityComparer)
- ///
- ///
- /// The type that owns the method. For non static methods, the type of this .
- /// The name of the method. You should use the nameof keyword.
- /// The generic type to run the method with.
- /// The list of arguments of the method
- ///
- /// The return type of the method. You can put for an unknown one.
- ///
- /// No method match the given constraints.
- /// The return of the method you wanted to run.
- ///
- public static T? RunGenericMethod(
- Type owner,
- string methodName,
- Type type,
- params object[] args
- )
- {
- return RunGenericMethod(owner, methodName, new[] { type }, args);
- }
-
- ///
- /// Run a generic static method for a multiple runtime .
- /// If your generic method only needs one type, see
- ///
- ///
- ///
- /// To run Merger.MergeLists{T} for a List where you don't know the type at compile type,
- /// you could do:
- ///
- /// Utility.RunGenericMethod<object>(
- /// typeof(Utility),
- /// nameof(MergeLists),
- /// enumerableType,
- /// oldValue, newValue, equalityComparer)
- ///
- ///
- /// The type that owns the method. For non static methods, the type of this .
- /// The name of the method. You should use the nameof keyword.
- /// The list of generic types to run the method with.
- /// The list of arguments of the method
- ///
- /// The return type of the method. You can put for an unknown one.
- ///
- /// No method match the given constraints.
- /// The return of the method you wanted to run.
- ///
- public static T? RunGenericMethod(
- Type owner,
- string methodName,
- Type[] types,
- params object?[] args
- )
- {
- if (types.Length < 1)
- throw new ArgumentException(
- $"The {nameof(types)} array is empty. At least one type is needed."
- );
- MethodInfo method = GetMethod(owner, BindingFlags.Static, methodName, types, args);
- return (T?)method.MakeGenericMethod(types).Invoke(null, args);
- }
-
///
/// Convert a dictionary to a query string.
///
diff --git a/back/src/Kyoo.Core/Controllers/MiscRepository.cs b/back/src/Kyoo.Core/Controllers/MiscRepository.cs
new file mode 100644
index 00000000..f3013a5e
--- /dev/null
+++ b/back/src/Kyoo.Core/Controllers/MiscRepository.cs
@@ -0,0 +1,85 @@
+// 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.Data.Common;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Dapper;
+using Kyoo.Abstractions.Controllers;
+using Kyoo.Abstractions.Models;
+using Kyoo.Postgresql;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Kyoo.Core.Controllers;
+
+public class MiscRepository(
+ DatabaseContext context,
+ DbConnection database,
+ IThumbnailsManager thumbnails
+)
+{
+ public static async Task DownloadMissingImages(IServiceProvider services)
+ {
+ await using AsyncServiceScope scope = services.CreateAsyncScope();
+ await scope.ServiceProvider.GetRequiredService().DownloadMissingImages();
+ }
+
+ private async Task> _GetAllImages()
+ {
+ string GetSql(string type) =>
+ $"""
+ select poster from {type}
+ union all select thumbnail from {type}
+ union all select logo from {type}
+ """;
+ var queries = new string[]
+ {
+ "movies",
+ "collections",
+ "shows",
+ "seasons",
+ "episodes"
+ }.Select(x => GetSql(x));
+ string sql = string.Join(" union all ", queries);
+ IEnumerable ret = await database.QueryAsync(sql);
+ return ret.Where(x => x != null).ToArray() as Image[];
+ }
+
+ public async Task DownloadMissingImages()
+ {
+ ICollection images = await _GetAllImages();
+ IEnumerable tasks = images
+ .Where(x => !File.Exists(thumbnails.GetImagePath(x.Id, ImageQuality.Low)))
+ .Select(x => thumbnails.DownloadImage(x, x.Id.ToString()));
+ // Chunk tasks to prevent http timouts
+ foreach (IEnumerable batch in tasks.Chunk(30))
+ await Task.WhenAll(batch);
+ }
+
+ public async Task> GetRegisteredPaths()
+ {
+ return await context
+ .Episodes.Select(x => x.Path)
+ .Concat(context.Movies.Select(x => x.Path))
+ .ToListAsync();
+ }
+}
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs
index a217fc85..d9f8499b 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs
@@ -31,46 +31,21 @@ namespace Kyoo.Core.Controllers;
///
/// A local repository to handle collections
///
-public class CollectionRepository : LocalRepository
+public class CollectionRepository(DatabaseContext database, IThumbnailsManager thumbnails)
+ : GenericRepository(database)
{
- ///
- /// The database handle
- ///
- private readonly DatabaseContext _database;
-
- ///
- /// Create a new .
- ///
- /// The database handle to use
- /// The thumbnail manager used to store images.
- public CollectionRepository(DatabaseContext database, IThumbnailsManager thumbs)
- : base(database, thumbs)
- {
- _database = database;
- }
-
///
public override async Task> Search(
string query,
Include? include = default
)
{
- return await AddIncludes(_database.Collections, include)
+ return await AddIncludes(Database.Collections, include)
.Where(x => EF.Functions.ILike(x.Name + " " + x.Slug, $"%{query}%"))
.Take(20)
.ToListAsync();
}
- ///
- public override async Task Create(Collection obj)
- {
- await base.Create(obj);
- _database.Entry(obj).State = EntityState.Added;
- await _database.SaveChangesAsync(() => Get(obj.Slug));
- await IRepository.OnResourceCreated(obj);
- return obj;
- }
-
///
protected override async Task Validate(Collection resource)
{
@@ -78,25 +53,18 @@ public class CollectionRepository : LocalRepository
if (string.IsNullOrEmpty(resource.Name))
throw new ArgumentException("The collection's name must be set and not empty");
+ await thumbnails.DownloadImages(resource);
}
public async Task AddMovie(Guid id, Guid movieId)
{
- _database.AddLinks(id, movieId);
- await _database.SaveChangesAsync();
+ Database.AddLinks(id, movieId);
+ await Database.SaveChangesAsync();
}
public async Task AddShow(Guid id, Guid showId)
{
- _database.AddLinks(id, showId);
- await _database.SaveChangesAsync();
- }
-
- ///
- public override async Task Delete(Collection obj)
- {
- _database.Entry(obj).State = EntityState.Deleted;
- await _database.SaveChangesAsync();
- await base.Delete(obj);
+ Database.AddLinks(id, showId);
+ await Database.SaveChangesAsync();
}
}
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs b/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs
index f98e84ce..19383169 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs
@@ -252,7 +252,7 @@ public static class DapperHelper
this IDbConnection db,
FormattableString command,
Dictionary config,
- Func, T> mapper,
+ Func, T> mapper,
Func> get,
SqlVariableContext context,
Include? include,
@@ -327,23 +327,6 @@ public static class DapperHelper
? ExpendProjections(typeV, prefix, include)
: null;
- if (typeV.IsAssignableTo(typeof(IThumbnails)))
- {
- string posterProj = string.Join(
- ", ",
- new[] { "poster", "thumbnail", "logo" }.Select(x =>
- $"{prefix}{x}_source as source, {prefix}{x}_blurhash as blurhash"
- )
- );
- projection = string.IsNullOrEmpty(projection)
- ? posterProj
- : $"{projection}, {posterProj}";
- types.InsertRange(
- types.IndexOf(typeV) + 1,
- Enumerable.Repeat(typeof(Image), 3)
- );
- }
-
if (string.IsNullOrEmpty(projection))
return leadingComa;
return $", {projection}{leadingComa}";
@@ -355,19 +338,7 @@ public static class DapperHelper
types.ToArray(),
items =>
{
- List nItems = new(items.Length);
- for (int i = 0; i < items.Length; i++)
- {
- if (types[i] == typeof(Image))
- continue;
- nItems.Add(items[i]);
- if (items[i] is not IThumbnails thumbs)
- continue;
- thumbs.Poster = items[++i] as Image;
- thumbs.Thumbnail = items[++i] as Image;
- thumbs.Logo = items[++i] as Image;
- }
- return mapIncludes(mapper(nItems), nItems.Skip(config.Count));
+ return mapIncludes(mapper(items), items.Skip(config.Count));
},
ParametersDictionary.LoadFrom(cmd),
splitOn: string.Join(
@@ -384,7 +355,7 @@ public static class DapperHelper
this IDbConnection db,
FormattableString command,
Dictionary config,
- Func, T> mapper,
+ Func, T> mapper,
SqlVariableContext context,
Include? include,
Filter? filter,
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs
index 18f0677b..7c37d79d 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs
@@ -37,7 +37,7 @@ public abstract class DapperRepository : IRepository
protected abstract Dictionary Config { get; }
- protected abstract T Mapper(List items);
+ protected abstract T Mapper(IList items);
protected DbConnection Database { get; init; }
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs
index b89b3204..d3162666 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs
@@ -18,6 +18,7 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
@@ -35,8 +36,8 @@ namespace Kyoo.Core.Controllers;
public class EpisodeRepository(
DatabaseContext database,
IRepository shows,
- IThumbnailsManager thumbs
-) : LocalRepository(database, thumbs)
+ IThumbnailsManager thumbnails
+) : GenericRepository(database)
{
static EpisodeRepository()
{
@@ -64,70 +65,77 @@ public class EpisodeRepository(
Include? include = default
)
{
- return await AddIncludes(database.Episodes, include)
+ return await AddIncludes(Database.Episodes, include)
.Where(x => EF.Functions.ILike(x.Name!, $"%{query}%"))
.Take(20)
.ToListAsync();
}
- protected override Task GetDuplicated(Episode item)
- {
- if (item is { SeasonNumber: not null, EpisodeNumber: not null })
- return database.Episodes.FirstOrDefaultAsync(x =>
- x.ShowId == item.ShowId
- && x.SeasonNumber == item.SeasonNumber
- && x.EpisodeNumber == item.EpisodeNumber
- );
- return database.Episodes.FirstOrDefaultAsync(x =>
- x.ShowId == item.ShowId && x.AbsoluteNumber == item.AbsoluteNumber
- );
- }
-
///
public override async Task Create(Episode obj)
{
+ // Set it for the OnResourceCreated event and the return value.
obj.ShowSlug = obj.Show?.Slug ?? (await shows.Get(obj.ShowId)).Slug;
- await base.Create(obj);
- database.Entry(obj).State = EntityState.Added;
- await database.SaveChangesAsync(() => GetDuplicated(obj));
- await IRepository.OnResourceCreated(obj);
- return obj;
+ return await base.Create(obj);
}
///
protected override async Task Validate(Episode resource)
{
await base.Validate(resource);
+ resource.Show = null;
if (resource.ShowId == Guid.Empty)
- {
- if (resource.Show == null)
- {
- throw new ArgumentException(
- $"Can't store an episode not related "
- + $"to any show (showID: {resource.ShowId})."
- );
- }
- resource.ShowId = resource.Show.Id;
- }
+ throw new ValidationException("Missing show id");
+ resource.Season = null;
if (resource.SeasonId == null && resource.SeasonNumber != null)
{
- resource.Season = await database.Seasons.FirstOrDefaultAsync(x =>
- x.ShowId == resource.ShowId && x.SeasonNumber == resource.SeasonNumber
- );
+ resource.SeasonId = await Database
+ .Seasons.Where(x =>
+ x.ShowId == resource.ShowId && x.SeasonNumber == resource.SeasonNumber
+ )
+ .Select(x => x.Id)
+ .FirstOrDefaultAsync();
}
+ await thumbnails.DownloadImages(resource);
}
///
public override async Task Delete(Episode obj)
{
- int epCount = await database
+ int epCount = await Database
.Episodes.Where(x => x.ShowId == obj.ShowId)
.Take(2)
.CountAsync();
- database.Entry(obj).State = EntityState.Deleted;
- await database.SaveChangesAsync();
- await base.Delete(obj);
if (epCount == 1)
await shows.Delete(obj.ShowId);
+ else
+ await base.Delete(obj);
+ }
+
+ ///
+ public override async Task DeleteAll(Filter filter)
+ {
+ ICollection items = await GetAll(filter);
+ Guid[] ids = items.Select(x => x.Id).ToArray();
+
+ await Database.Set().Where(x => ids.Contains(x.Id)).ExecuteDeleteAsync();
+ foreach (Episode resource in items)
+ await IRepository.OnResourceDeleted(resource);
+
+ Guid[] showIds = await Database
+ .Set()
+ .Where(filter.ToEfLambda())
+ .Select(x => x.Show!)
+ .Where(x => !x.Episodes!.Any())
+ .Select(x => x.Id)
+ .ToArrayAsync();
+
+ if (!showIds.Any())
+ return;
+
+ Filter[] showFilters = showIds
+ .Select(x => new Filter.Eq(nameof(Show.Id), x))
+ .ToArray();
+ await shows.DeleteAll(Filter.Or(showFilters)!);
}
}
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/GenericRepository.cs
similarity index 66%
rename from back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs
rename to back/src/Kyoo.Core/Controllers/Repositories/GenericRepository.cs
index c0e56b7f..dbe199f2 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/GenericRepository.cs
@@ -18,6 +18,7 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
@@ -29,38 +30,14 @@ using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Abstractions.Models.Utils;
using Kyoo.Postgresql;
-using Kyoo.Utils;
using Microsoft.EntityFrameworkCore;
namespace Kyoo.Core.Controllers;
-///
-/// A base class to create repositories using Entity Framework.
-///
-/// The type of this repository
-public abstract class LocalRepository : IRepository
+public abstract class GenericRepository(DatabaseContext database) : IRepository
where T : class, IResource, IQuery
{
- ///
- /// The Entity Framework's Database handle.
- ///
- protected DbContext Database { get; }
-
- ///
- /// The thumbnail manager used to store images.
- ///
- private readonly IThumbnailsManager _thumbs;
-
- ///
- /// Create a new base with the given database handle.
- ///
- /// A database connection to load resources of type
- /// The thumbnail manager used to store images.
- protected LocalRepository(DbContext database, IThumbnailsManager thumbs)
- {
- Database = database;
- _thumbs = thumbs;
- }
+ public DatabaseContext Database => database;
///
public Type RepositoryType => typeof(T);
@@ -127,12 +104,6 @@ public abstract class LocalRepository : IRepository
return query;
}
- ///
- /// Get a resource from it's ID and make the instance track it.
- ///
- /// The ID of the resource
- /// If the item is not found
- /// The tracked resource with the given ID
protected virtual async Task GetWithTracking(Guid id)
{
T? ret = await Database.Set().AsTracking().FirstOrDefaultAsync(x => x.Id == id);
@@ -174,11 +145,6 @@ public abstract class LocalRepository : IRepository
return ret;
}
- protected virtual Task GetDuplicated(T item)
- {
- return GetOrDefault(item.Slug);
- }
-
///
public virtual Task GetOrDefault(Guid id, Include? include = default)
{
@@ -303,26 +269,9 @@ public abstract class LocalRepository : IRepository
public virtual async Task Create(T obj)
{
await Validate(obj);
- if (obj is IThumbnails thumbs)
- {
- try
- {
- await _thumbs.DownloadImages(thumbs);
- }
- catch (DuplicatedItemException e) when (e.Existing is null)
- {
- throw new DuplicatedItemException(await GetDuplicated(obj));
- }
- if (thumbs.Poster != null)
- Database.Entry(thumbs).Reference(x => x.Poster).TargetEntry!.State =
- EntityState.Added;
- if (thumbs.Thumbnail != null)
- Database.Entry(thumbs).Reference(x => x.Thumbnail).TargetEntry!.State =
- EntityState.Added;
- if (thumbs.Logo != null)
- Database.Entry(thumbs).Reference(x => x.Logo).TargetEntry!.State =
- EntityState.Added;
- }
+ Database.Add(obj);
+ await Database.SaveChangesAsync(() => Get(obj.Slug));
+ await IRepository.OnResourceCreated(obj);
return obj;
}
@@ -346,27 +295,11 @@ public abstract class LocalRepository : IRepository
///
public virtual async Task Edit(T edited)
{
- bool lazyLoading = Database.ChangeTracker.LazyLoadingEnabled;
- Database.ChangeTracker.LazyLoadingEnabled = false;
- try
- {
- T old = await GetWithTracking(edited.Id);
-
- Merger.Complete(
- old,
- edited,
- x => x.GetCustomAttribute() == null
- );
- await EditRelations(old, edited);
- await Database.SaveChangesAsync();
- await IRepository.OnResourceEdited(old);
- return old;
- }
- finally
- {
- Database.ChangeTracker.LazyLoadingEnabled = lazyLoading;
- Database.ChangeTracker.Clear();
- }
+ await Validate(edited);
+ Database.Update(edited);
+ await Database.SaveChangesAsync();
+ await IRepository.OnResourceEdited(edited);
+ return edited;
}
///
@@ -391,39 +324,9 @@ public abstract class LocalRepository : IRepository
}
}
- ///
- /// An overridable method to edit relation of a resource.
- ///
- ///
- /// The non edited resource
- ///
- ///
- /// The new version of .
- /// This item will be saved on the database and replace
- ///
- /// A representing the asynchronous operation.
- protected virtual Task EditRelations(T resource, T changed)
- {
- if (resource is IThumbnails thumbs && changed is IThumbnails chng)
- {
- Database.Entry(thumbs).Reference(x => x.Poster).IsModified =
- thumbs.Poster != chng.Poster;
- Database.Entry(thumbs).Reference(x => x.Thumbnail).IsModified =
- thumbs.Thumbnail != chng.Thumbnail;
- Database.Entry(thumbs).Reference(x => x.Logo).IsModified = thumbs.Logo != chng.Logo;
- }
- return Validate(resource);
- }
-
- ///
- /// A method called just before saving a new resource to the database.
- /// It is also called on the default implementation of
- ///
- /// The resource that will be saved
- ///
+ ///
/// You can throw this if the resource is illegal and should not be saved.
///
- /// A representing the asynchronous operation.
protected virtual Task Validate(T resource)
{
if (
@@ -432,26 +335,9 @@ public abstract class LocalRepository : IRepository
)
return Task.CompletedTask;
if (string.IsNullOrEmpty(resource.Slug))
- throw new ArgumentException("Resource can't have null as a slug.");
- if (int.TryParse(resource.Slug, out int _) || resource.Slug == "random")
- {
- try
- {
- MethodInfo? setter = typeof(T).GetProperty(nameof(resource.Slug))!.GetSetMethod();
- if (setter != null)
- setter.Invoke(resource, new object[] { resource.Slug + '!' });
- else
- throw new ArgumentException(
- "Resources slug can't be number only or the literal \"random\"."
- );
- }
- catch
- {
- throw new ArgumentException(
- "Resources slug can't be number only or the literal \"random\"."
- );
- }
- }
+ throw new ValidationException("Resource can't have null as a slug.");
+ if (resource.Slug == "random")
+ throw new ValidationException("Resources slug can't be the literal \"random\".");
return Task.CompletedTask;
}
@@ -470,18 +356,20 @@ public abstract class LocalRepository : IRepository
}
///
- public virtual Task Delete(T obj)
+ public virtual async Task Delete(T obj)
{
- IRepository.OnResourceDeleted(obj);
- if (obj is IThumbnails thumbs)
- return _thumbs.DeleteImages(thumbs);
- return Task.CompletedTask;
+ await Database.Set().Where(x => x.Id == obj.Id).ExecuteDeleteAsync();
+ await IRepository.OnResourceDeleted(obj);
}
///
- public async Task DeleteAll(Filter filter)
+ public virtual async Task DeleteAll(Filter filter)
{
- foreach (T resource in await GetAll(filter))
- await Delete(resource);
+ ICollection items = await GetAll(filter);
+ Guid[] ids = items.Select(x => x.Id).ToArray();
+ await Database.Set().Where(x => ids.Contains(x.Id)).ExecuteDeleteAsync();
+
+ foreach (T resource in items)
+ await IRepository.OnResourceDeleted(resource);
}
}
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs
index 958f12ca..27d6e844 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs
@@ -30,7 +30,8 @@ namespace Kyoo.Core.Controllers;
///
/// A local repository to handle library items.
///
-public class LibraryItemRepository : DapperRepository
+public class LibraryItemRepository(DbConnection database, SqlVariableContext context)
+ : DapperRepository(database, context)
{
// language=PostgreSQL
protected override FormattableString Sql =>
@@ -67,7 +68,7 @@ public class LibraryItemRepository : DapperRepository
{ "c", typeof(Collection) }
};
- protected override ILibraryItem Mapper(List items)
+ protected override ILibraryItem Mapper(IList items)
{
if (items[0] is Show show && show.Id != Guid.Empty)
return show;
@@ -78,9 +79,6 @@ public class LibraryItemRepository : DapperRepository
throw new InvalidDataException();
}
- public LibraryItemRepository(DbConnection database, SqlVariableContext context)
- : base(database, context) { }
-
public async Task> GetAllOfCollection(
Guid collectionId,
Filter? filter = default,
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs
index 602f8e8a..c04a72cb 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs
@@ -21,58 +21,48 @@ using System.Linq;
using System.Threading.Tasks;
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 shows
-///
-public class MovieRepository : LocalRepository
+public class MovieRepository(
+ DatabaseContext database,
+ IRepository studios,
+ IThumbnailsManager thumbnails
+) : GenericRepository(database)
{
- ///
- /// The database handle
- ///
- private readonly DatabaseContext _database;
-
- ///
- /// A studio repository to handle creation/validation of related studios.
- ///
- private readonly IRepository _studios;
-
- public MovieRepository(
- DatabaseContext database,
- IRepository studios,
- IThumbnailsManager thumbs
- )
- : base(database, thumbs)
- {
- _database = database;
- _studios = studios;
- }
-
///
public override async Task> Search(
string query,
Include? include = default
)
{
- return await AddIncludes(_database.Movies, include)
+ return await AddIncludes(Database.Movies, include)
.Where(x => EF.Functions.ILike(x.Name + " " + x.Slug, $"%{query}%"))
.Take(20)
.ToListAsync();
}
///
- public override async Task Create(Movie obj)
+ public override Task Create(Movie obj)
{
- await base.Create(obj);
- _database.Entry(obj).State = EntityState.Added;
- await _database.SaveChangesAsync(() => Get(obj.Slug));
- await IRepository.OnResourceCreated(obj);
- return obj;
+ try
+ {
+ return base.Create(obj);
+ }
+ catch (DuplicatedItemException ex)
+ when (ex.Existing is Movie existing
+ && existing.Slug == obj.Slug
+ && obj.AirDate is not null
+ && existing.AirDate?.Year != obj.AirDate?.Year
+ )
+ {
+ obj.Slug = $"{obj.Slug}-{obj.AirDate!.Value.Year}";
+ return base.Create(obj);
+ }
}
///
@@ -81,28 +71,9 @@ public class MovieRepository : LocalRepository
await base.Validate(resource);
if (resource.Studio != null)
{
- resource.Studio = await _studios.CreateIfNotExists(resource.Studio);
- resource.StudioId = resource.Studio.Id;
+ resource.StudioId = (await studios.CreateIfNotExists(resource.Studio)).Id;
+ resource.Studio = null;
}
- }
-
- ///
- protected override async Task EditRelations(Movie resource, Movie changed)
- {
- await Validate(changed);
-
- if (changed.Studio != null || changed.StudioId == null)
- {
- await Database.Entry(resource).Reference(x => x.Studio).LoadAsync();
- resource.Studio = changed.Studio;
- }
- }
-
- ///
- public override async Task Delete(Movie obj)
- {
- _database.Remove(obj);
- await _database.SaveChangesAsync();
- await base.Delete(obj);
+ await thumbnails.DownloadImages(resource);
}
}
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/NewsRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/NewsRepository.cs
index f55cb31a..c91c2eed 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/NewsRepository.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/NewsRepository.cs
@@ -49,7 +49,7 @@ public class NewsRepository : DapperRepository
protected override Dictionary Config =>
new() { { "e", typeof(Episode) }, { "m", typeof(Movie) }, };
- protected override INews Mapper(List items)
+ protected override INews Mapper(IList items)
{
if (items[0] is Episode episode && episode.Id != Guid.Empty)
return episode;
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs
index 18f53e96..590d0b10 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs
@@ -31,16 +31,9 @@ using Microsoft.Extensions.DependencyInjection;
namespace Kyoo.Core.Controllers;
-///
-/// A local repository to handle seasons.
-///
-public class SeasonRepository : LocalRepository
+public class SeasonRepository(DatabaseContext database, IThumbnailsManager thumbnails)
+ : GenericRepository(database)
{
- ///
- /// The database handle
- ///
- private readonly DatabaseContext _database;
-
static SeasonRepository()
{
// Edit seasons slugs when the show's slug changes.
@@ -61,31 +54,13 @@ public class SeasonRepository : LocalRepository
};
}
- ///
- /// Create a new .
- ///
- /// The database handle that will be used
- /// The thumbnail manager used to store images.
- public SeasonRepository(DatabaseContext database, IThumbnailsManager thumbs)
- : base(database, thumbs)
- {
- _database = database;
- }
-
- protected override Task GetDuplicated(Season item)
- {
- return _database.Seasons.FirstOrDefaultAsync(x =>
- x.ShowId == item.ShowId && x.SeasonNumber == item.SeasonNumber
- );
- }
-
///
public override async Task> Search(
string query,
Include? include = default
)
{
- return await AddIncludes(_database.Seasons, include)
+ return await AddIncludes(Database.Seasons, include)
.Where(x => EF.Functions.ILike(x.Name!, $"%{query}%"))
.Take(20)
.ToListAsync();
@@ -94,38 +69,20 @@ public class SeasonRepository : LocalRepository
///
public override async Task Create(Season obj)
{
- await base.Create(obj);
+ // Set it for the OnResourceCreated event and the return value.
obj.ShowSlug =
- (await _database.Shows.FirstOrDefaultAsync(x => x.Id == obj.ShowId))?.Slug
+ (await Database.Shows.FirstOrDefaultAsync(x => x.Id == obj.ShowId))?.Slug
?? throw new ItemNotFoundException($"No show found with ID {obj.ShowId}");
- _database.Entry(obj).State = EntityState.Added;
- await _database.SaveChangesAsync(() => GetDuplicated(obj));
- await IRepository.OnResourceCreated(obj);
- return obj;
+ return await base.Create(obj);
}
///
protected override async Task Validate(Season resource)
{
await base.Validate(resource);
+ resource.Show = null;
if (resource.ShowId == Guid.Empty)
- {
- if (resource.Show == null)
- {
- throw new ValidationException(
- $"Can't store a season not related to any show "
- + $"(showID: {resource.ShowId})."
- );
- }
- resource.ShowId = resource.Show.Id;
- }
- }
-
- ///
- public override async Task Delete(Season obj)
- {
- _database.Remove(obj);
- await _database.SaveChangesAsync();
- await base.Delete(obj);
+ throw new ValidationException("Missing show id");
+ await thumbnails.DownloadImages(resource);
}
}
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs
index 2253da0f..17ee8251 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs
@@ -21,59 +21,48 @@ using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
+using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Abstractions.Models.Utils;
using Kyoo.Postgresql;
-using Kyoo.Utils;
using Microsoft.EntityFrameworkCore;
namespace Kyoo.Core.Controllers;
-///
-/// A local repository to handle shows
-///
-public class ShowRepository : LocalRepository
+public class ShowRepository(
+ DatabaseContext database,
+ IRepository studios,
+ IThumbnailsManager thumbnails
+) : GenericRepository(database)
{
- ///
- /// The database handle
- ///
- private readonly DatabaseContext _database;
-
- ///
- /// A studio repository to handle creation/validation of related studios.
- ///
- private readonly IRepository _studios;
-
- public ShowRepository(
- DatabaseContext database,
- IRepository studios,
- IThumbnailsManager thumbs
- )
- : base(database, thumbs)
- {
- _database = database;
- _studios = studios;
- }
-
///
public override async Task> Search(
string query,
Include? include = default
)
{
- return await AddIncludes(_database.Shows, include)
+ return await AddIncludes(Database.Shows, include)
.Where(x => EF.Functions.ILike(x.Name + " " + x.Slug, $"%{query}%"))
.Take(20)
.ToListAsync();
}
///
- public override async Task Create(Show obj)
+ public override Task Create(Show obj)
{
- await base.Create(obj);
- _database.Entry(obj).State = EntityState.Added;
- await _database.SaveChangesAsync(() => Get(obj.Slug));
- await IRepository.OnResourceCreated(obj);
- return obj;
+ try
+ {
+ return base.Create(obj);
+ }
+ catch (DuplicatedItemException ex)
+ when (ex.Existing is Show existing
+ && existing.Slug == obj.Slug
+ && obj.StartAir is not null
+ && existing.StartAir?.Year != obj.StartAir?.Year
+ )
+ {
+ obj.Slug = $"{obj.Slug}-{obj.AirDate!.Value.Year}";
+ return base.Create(obj);
+ }
}
///
@@ -82,28 +71,9 @@ public class ShowRepository : LocalRepository
await base.Validate(resource);
if (resource.Studio != null)
{
- resource.Studio = await _studios.CreateIfNotExists(resource.Studio);
- resource.StudioId = resource.Studio.Id;
+ resource.StudioId = (await studios.CreateIfNotExists(resource.Studio)).Id;
+ resource.Studio = null;
}
- }
-
- ///
- protected override async Task EditRelations(Show resource, Show changed)
- {
- await Validate(changed);
-
- if (changed.Studio != null || changed.StudioId == null)
- {
- await Database.Entry(resource).Reference(x => x.Studio).LoadAsync();
- resource.Studio = changed.Studio;
- }
- }
-
- ///
- public override async Task Delete(Show obj)
- {
- _database.Remove(obj);
- await _database.SaveChangesAsync();
- await base.Delete(obj);
+ await thumbnails.DownloadImages(resource);
}
}
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs
index 7cdc1358..91aba67f 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs
@@ -19,11 +19,9 @@
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 Kyoo.Utils;
using Microsoft.EntityFrameworkCore;
namespace Kyoo.Core.Controllers;
@@ -31,51 +29,17 @@ namespace Kyoo.Core.Controllers;
///
/// A local repository to handle studios
///
-public class StudioRepository : LocalRepository
+public class StudioRepository(DatabaseContext database) : GenericRepository(database)
{
- ///
- /// The database handle
- ///
- private readonly DatabaseContext _database;
-
- ///
- /// Create a new .
- ///
- /// The database handle
- /// The thumbnail manager used to store images.
- public StudioRepository(DatabaseContext database, IThumbnailsManager thumbs)
- : base(database, thumbs)
- {
- _database = database;
- }
-
///
public override async Task> Search(
string query,
Include? include = default
)
{
- return await AddIncludes(_database.Studios, include)
+ return await AddIncludes(Database.Studios, include)
.Where(x => EF.Functions.ILike(x.Name, $"%{query}%"))
.Take(20)
.ToListAsync();
}
-
- ///
- public override async Task Create(Studio obj)
- {
- await base.Create(obj);
- _database.Entry(obj).State = EntityState.Added;
- await _database.SaveChangesAsync(() => Get(obj.Slug));
- await IRepository.OnResourceCreated(obj);
- return obj;
- }
-
- ///
- public override async Task Delete(Studio obj)
- {
- _database.Entry(obj).State = EntityState.Deleted;
- await _database.SaveChangesAsync();
- await base.Delete(obj);
- }
}
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs
index e4a62db9..c98d8eb0 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs
@@ -40,9 +40,8 @@ public class UserRepository(
DatabaseContext database,
DbConnection db,
SqlVariableContext context,
- IThumbnailsManager thumbs,
PermissionOption options
-) : LocalRepository(database, thumbs), IUserRepository
+) : GenericRepository(database), IUserRepository
{
///
public override async Task> Search(
@@ -50,7 +49,7 @@ public class UserRepository(
Include? include = default
)
{
- return await AddIncludes(database.Users, include)
+ return await AddIncludes(Database.Users, include)
.Where(x => EF.Functions.ILike(x.Username, $"%{query}%"))
.Take(20)
.ToListAsync();
@@ -60,26 +59,14 @@ public class UserRepository(
public override async Task Create(User obj)
{
// If no users exists, the new one will be an admin. Give it every permissions.
- if (!await database.Users.AnyAsync())
+ if (!await Database.Users.AnyAsync())
obj.Permissions = PermissionOption.Admin;
else if (!options.RequireVerification)
obj.Permissions = options.NewUser;
else
obj.Permissions = Array.Empty();
- await base.Create(obj);
- database.Entry(obj).State = EntityState.Added;
- await database.SaveChangesAsync(() => Get(obj.Slug));
- await IRepository.OnResourceCreated(obj);
- return obj;
- }
-
- ///
- public override async Task Delete(User obj)
- {
- database.Entry(obj).State = EntityState.Deleted;
- await database.SaveChangesAsync();
- await base.Delete(obj);
+ return await base.Create(obj);
}
public Task GetByExternalId(string provider, string id)
@@ -109,8 +96,8 @@ public class UserRepository(
User user = await GetWithTracking(userId);
user.ExternalId[provider] = token;
// without that, the change tracker does not find the modification. /shrug
- database.Entry(user).Property(x => x.ExternalId).IsModified = true;
- await database.SaveChangesAsync();
+ Database.Entry(user).Property(x => x.ExternalId).IsModified = true;
+ await Database.SaveChangesAsync();
return user;
}
@@ -119,8 +106,8 @@ public class UserRepository(
User user = await GetWithTracking(userId);
user.ExternalId.Remove(provider);
// without that, the change tracker does not find the modification. /shrug
- database.Entry(user).Property(x => x.ExternalId).IsModified = true;
- await database.SaveChangesAsync();
+ Database.Entry(user).Property(x => x.ExternalId).IsModified = true;
+ await Database.SaveChangesAsync();
return user;
}
}
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/WatchStatusRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/WatchStatusRepository.cs
index e3137616..3159cbfb 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/WatchStatusRepository.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/WatchStatusRepository.cs
@@ -135,7 +135,7 @@ public class WatchStatusRepository(
{ "_mw", typeof(MovieWatchStatus) },
};
- protected IWatchlist Mapper(List items)
+ protected IWatchlist Mapper(IList items)
{
if (items[0] is Show show && show.Id != Guid.Empty)
{
diff --git a/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs b/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs
index c9ba209a..f60dee73 100644
--- a/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs
+++ b/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs
@@ -42,8 +42,6 @@ public class ThumbnailsManager(
Lazy> users
) : IThumbnailsManager
{
- private static readonly Dictionary> _downloading = [];
-
private static async Task _WriteTo(SKBitmap bitmap, string path, int quality)
{
SKData data = bitmap.Encode(SKEncodedImageFormat.Webp, quality);
@@ -52,12 +50,15 @@ public class ThumbnailsManager(
await reader.CopyToAsync(file);
}
- private async Task _DownloadImage(Image? image, string localPath, string what)
+ public async Task DownloadImage(Image? image, string what)
{
if (image == null)
return;
try
{
+ if (image.Id == Guid.Empty)
+ image.Id = Guid.NewGuid();
+
logger.LogInformation("Downloading image {What}", what);
HttpClient client = clientFactory.CreateClient();
@@ -79,31 +80,19 @@ public class ThumbnailsManager(
new SKSizeI(original.Width, original.Height),
SKFilterQuality.High
);
- await _WriteTo(
- original,
- $"{localPath}.{ImageQuality.High.ToString().ToLowerInvariant()}.webp",
- 90
- );
+ await _WriteTo(original, GetImagePath(image.Id, ImageQuality.High), 90);
using SKBitmap medium = high.Resize(
new SKSizeI((int)(high.Width / 1.5), (int)(high.Height / 1.5)),
SKFilterQuality.Medium
);
- await _WriteTo(
- medium,
- $"{localPath}.{ImageQuality.Medium.ToString().ToLowerInvariant()}.webp",
- 75
- );
+ await _WriteTo(medium, GetImagePath(image.Id, ImageQuality.Medium), 75);
using SKBitmap low = medium.Resize(
new SKSizeI(original.Width / 2, original.Height / 2),
SKFilterQuality.Low
);
- await _WriteTo(
- low,
- $"{localPath}.{ImageQuality.Low.ToString().ToLowerInvariant()}.webp",
- 50
- );
+ await _WriteTo(low, GetImagePath(image.Id, ImageQuality.Low), 50);
image.Blurhash = Blurhasher.Encode(low, 4, 3);
}
@@ -119,86 +108,24 @@ public class ThumbnailsManager(
{
string name = item is IResource res ? res.Slug : "???";
- string posterPath =
- $"{_GetBaseImagePath(item, "poster")}.{ImageQuality.High.ToString().ToLowerInvariant()}.webp";
- bool duplicated = false;
- TaskCompletionSource? sync = null;
- try
- {
- lock (_downloading)
- {
- if (_downloading.ContainsKey(posterPath))
- {
- duplicated = true;
- sync = _downloading.GetValueOrDefault(posterPath);
- }
- else
- {
- sync = new();
- _downloading.Add(posterPath, sync);
- }
- }
- if (duplicated)
- {
- object? dup = sync != null ? await sync.Task : null;
- if (dup != null)
- throw new DuplicatedItemException(dup);
- }
-
- await _DownloadImage(
- item.Poster,
- _GetBaseImagePath(item, "poster"),
- $"The poster of {name}"
- );
- await _DownloadImage(
- item.Thumbnail,
- _GetBaseImagePath(item, "thumbnail"),
- $"The poster of {name}"
- );
- await _DownloadImage(
- item.Logo,
- _GetBaseImagePath(item, "logo"),
- $"The poster of {name}"
- );
- }
- finally
- {
- if (!duplicated)
- {
- lock (_downloading)
- {
- _downloading.Remove(posterPath);
- sync!.SetResult(item);
- }
- }
- }
- }
-
- private static string _GetBaseImagePath(T item, string image)
- {
- string directory = item switch
- {
- IResource res
- => Path.Combine("/metadata", item.GetType().Name.ToLowerInvariant(), res.Slug),
- _ => Path.Combine("/metadata", typeof(T).Name.ToLowerInvariant())
- };
- Directory.CreateDirectory(directory);
- return Path.Combine(directory, image);
+ await DownloadImage(item.Poster, $"The poster of {name}");
+ await DownloadImage(item.Thumbnail, $"The thumbnail of {name}");
+ await DownloadImage(item.Logo, $"The logo of {name}");
}
///
- public string GetImagePath(T item, string image, ImageQuality quality)
- where T : IThumbnails
+ public string GetImagePath(Guid imageId, ImageQuality quality)
{
- return $"{_GetBaseImagePath(item, image)}.{quality.ToString().ToLowerInvariant()}.webp";
+ return $"/metadata/{imageId}.{quality.ToString().ToLowerInvariant()}.webp";
}
///
public Task DeleteImages(T item)
where T : IThumbnails
{
- IEnumerable images = new[] { "poster", "thumbnail", "logo" }
- .SelectMany(x => _GetBaseImagePath(item, x))
+ IEnumerable images = new[] { item.Poster?.Id, item.Thumbnail?.Id, item.Logo?.Id }
+ .Where(x => x is not null)
+ .SelectMany(x => $"/metadata/{x}")
.SelectMany(x =>
new[]
{
diff --git a/back/src/Kyoo.Core/CoreModule.cs b/back/src/Kyoo.Core/CoreModule.cs
index 8fbfd36c..1e853065 100644
--- a/back/src/Kyoo.Core/CoreModule.cs
+++ b/back/src/Kyoo.Core/CoreModule.cs
@@ -63,5 +63,6 @@ public static class CoreModule
);
builder.Services.AddScoped();
builder.Services.AddScoped();
+ builder.Services.AddScoped();
}
}
diff --git a/back/src/Kyoo.Core/Extensions/ServiceExtensions.cs b/back/src/Kyoo.Core/Extensions/ServiceExtensions.cs
index 2c92fb56..452aed44 100644
--- a/back/src/Kyoo.Core/Extensions/ServiceExtensions.cs
+++ b/back/src/Kyoo.Core/Extensions/ServiceExtensions.cs
@@ -16,13 +16,12 @@
// 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.Text.Json;
using System.Text.Json.Serialization;
using AspNetCore.Proxy;
using Kyoo.Abstractions.Models.Utils;
+using Kyoo.Authentication;
using Kyoo.Core.Api;
using Kyoo.Core.Controllers;
using Kyoo.Utils;
@@ -47,6 +46,8 @@ public static class ServiceExtensions
options.ModelBinderProviders.Insert(0, new IncludeBinder.Provider());
options.ModelBinderProviders.Insert(0, new FilterBinder.Provider());
})
+ .AddApplicationPart(typeof(CoreModule).Assembly)
+ .AddApplicationPart(typeof(AuthenticationModule).Assembly)
.AddJsonOptions(x =>
{
x.JsonSerializerOptions.TypeInfoResolver = new JsonKindResolver()
diff --git a/back/src/Kyoo.Core/Program.cs b/back/src/Kyoo.Core/Program.cs
index bebd1c2b..fc4660d1 100644
--- a/back/src/Kyoo.Core/Program.cs
+++ b/back/src/Kyoo.Core/Program.cs
@@ -17,9 +17,9 @@
// along with Kyoo. If not, see .
using System;
-using System.IO;
using Kyoo.Authentication;
using Kyoo.Core;
+using Kyoo.Core.Controllers;
using Kyoo.Core.Extensions;
using Kyoo.Meiliseach;
using Kyoo.Postgresql;
@@ -70,11 +70,6 @@ AppDomain.CurrentDomain.UnhandledException += (_, ex) =>
Log.Fatal(ex.ExceptionObject as Exception, "Unhandled exception");
builder.Host.UseSerilog();
-builder
- .Services.AddMvcCore()
- .AddApplicationPart(typeof(CoreModule).Assembly)
- .AddApplicationPart(typeof(AuthenticationModule).Assembly);
-
builder.Services.ConfigureMvc();
builder.Services.ConfigureOpenApi();
builder.ConfigureKyoo();
@@ -93,42 +88,6 @@ app.UseRouting();
app.UseAuthentication();
app.MapControllers();
-// TODO: wait 4.5.0 and delete this
-static void MoveAll(DirectoryInfo source, DirectoryInfo target)
-{
- if (source.FullName == target.FullName)
- return;
-
- Directory.CreateDirectory(target.FullName);
-
- foreach (FileInfo fi in source.GetFiles())
- fi.MoveTo(Path.Combine(target.ToString(), fi.Name), true);
-
- foreach (DirectoryInfo diSourceSubDir in source.GetDirectories())
- {
- DirectoryInfo nextTargetSubDir = target.CreateSubdirectory(diSourceSubDir.Name);
- MoveAll(diSourceSubDir, nextTargetSubDir);
- }
- Directory.Delete(source.FullName);
-}
-
-try
-{
- string oldDir = "/kyoo/kyoo_datadir/metadata";
- if (Path.Exists(oldDir))
- {
- MoveAll(new DirectoryInfo(oldDir), new DirectoryInfo("/metadata"));
- Log.Warning("Old metadata directory migrated.");
- }
-}
-catch (Exception ex)
-{
- Log.Fatal(
- ex,
- "Unhandled error while trying to migrate old metadata images to new directory. Giving up and continuing normal startup."
- );
-}
-
// Activate services that always run in the background
app.Services.GetRequiredService();
app.Services.GetRequiredService();
@@ -138,4 +97,7 @@ await using (AsyncServiceScope scope = app.Services.CreateAsyncScope())
await MeilisearchModule.Initialize(scope.ServiceProvider);
}
-app.Run(Environment.GetEnvironmentVariable("KYOO_BIND_URL") ?? "http://*:5000");
+// The methods takes care of creating a scope and will download images on the background.
+_ = MiscRepository.DownloadMissingImages(app.Services);
+
+await app.RunAsync(Environment.GetEnvironmentVariable("KYOO_BIND_URL") ?? "http://*:5000");
diff --git a/back/src/Kyoo.Core/Views/Health.cs b/back/src/Kyoo.Core/Views/Admin/Health.cs
similarity index 83%
rename from back/src/Kyoo.Core/Views/Health.cs
rename to back/src/Kyoo.Core/Views/Admin/Health.cs
index 7680b3b4..3fa1d7aa 100644
--- a/back/src/Kyoo.Core/Views/Health.cs
+++ b/back/src/Kyoo.Core/Views/Admin/Health.cs
@@ -30,19 +30,8 @@ namespace Kyoo.Core.Api;
[Route("health")]
[ApiController]
[ApiDefinition("Health")]
-public class Health : BaseApi
+public class Health(HealthCheckService healthCheckService) : BaseApi
{
- private readonly HealthCheckService _healthCheckService;
-
- ///
- /// Create a new .
- ///
- /// The service to check health.
- public Health(HealthCheckService healthCheckService)
- {
- _healthCheckService = healthCheckService;
- }
-
///
/// Check if the api is ready to accept requests.
///
@@ -57,7 +46,7 @@ public class Health : BaseApi
headers.Pragma = "no-cache";
headers.Expires = "Thu, 01 Jan 1970 00:00:00 GMT";
- HealthReport result = await _healthCheckService.CheckHealthAsync();
+ HealthReport result = await healthCheckService.CheckHealthAsync();
return result.Status switch
{
HealthStatus.Healthy => Ok(new HealthResult("Healthy")),
diff --git a/back/src/Kyoo.Core/Views/Admin/Misc.cs b/back/src/Kyoo.Core/Views/Admin/Misc.cs
new file mode 100644
index 00000000..dd54fea7
--- /dev/null
+++ b/back/src/Kyoo.Core/Views/Admin/Misc.cs
@@ -0,0 +1,45 @@
+// 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.Permissions;
+using Kyoo.Core.Controllers;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Kyoo.Core.Api;
+
+///
+/// Private APIs only used for other services. Can change at any time without notice.
+///
+[ApiController]
+[Permission(nameof(Misc), Kind.Read, Group = Group.Admin)]
+public class Misc(MiscRepository repo) : BaseApi
+{
+ ///
+ /// List all registered paths.
+ ///
+ /// The list of paths known to Kyoo.
+ [HttpGet("/paths")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public Task> GetAllPaths()
+ {
+ return repo.GetRegisteredPaths();
+ }
+}
diff --git a/back/src/Kyoo.Core/Views/Watch/ProxyApi.cs b/back/src/Kyoo.Core/Views/Content/ProxyApi.cs
similarity index 100%
rename from back/src/Kyoo.Core/Views/Watch/ProxyApi.cs
rename to back/src/Kyoo.Core/Views/Content/ProxyApi.cs
diff --git a/back/src/Kyoo.Core/Views/Content/ThumbnailsApi.cs b/back/src/Kyoo.Core/Views/Content/ThumbnailsApi.cs
new file mode 100644
index 00000000..908a88d7
--- /dev/null
+++ b/back/src/Kyoo.Core/Views/Content/ThumbnailsApi.cs
@@ -0,0 +1,67 @@
+// 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.IO;
+using Kyoo.Abstractions.Controllers;
+using Kyoo.Abstractions.Models;
+using Kyoo.Abstractions.Models.Attributes;
+using Kyoo.Abstractions.Models.Permissions;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using static Kyoo.Abstractions.Models.Utils.Constants;
+
+namespace Kyoo.Core.Api;
+
+///
+/// Retrive images.
+///
+[ApiController]
+[Route("thumbnails")]
+[Route("images", Order = AlternativeRoute)]
+[Route("image", Order = AlternativeRoute)]
+[Permission(nameof(Image), Kind.Read, Group = Group.Overall)]
+[ApiDefinition("Images", Group = OtherGroup)]
+public class ThumbnailsApi(IThumbnailsManager thumbs) : BaseApi
+{
+ ///
+ /// Get Image
+ ///
+ ///
+ /// Get an image from it's id. You can select a specefic quality.
+ ///
+ /// The ID of the image to retrive.
+ /// The quality of the image to retrieve.
+ /// The image asked.
+ ///
+ /// The image does not exists on kyoo.
+ ///
+ [HttpGet("{id:guid}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public IActionResult GetPoster(Guid id, [FromQuery] ImageQuality? quality)
+ {
+ string path = thumbs.GetImagePath(id, quality ?? ImageQuality.High);
+ if (!System.IO.File.Exists(path))
+ return NotFound();
+
+ // Allow clients to cache the image for 6 month.
+ Response.Headers.CacheControl = $"public, max-age={60 * 60 * 24 * 31 * 6}";
+ return PhysicalFile(Path.GetFullPath(path), "image/webp", true);
+ }
+}
diff --git a/back/src/Kyoo.Core/Views/Helper/CrudApi.cs b/back/src/Kyoo.Core/Views/Helper/CrudApi.cs
index ae389f95..44e4447f 100644
--- a/back/src/Kyoo.Core/Views/Helper/CrudApi.cs
+++ b/back/src/Kyoo.Core/Views/Helper/CrudApi.cs
@@ -170,6 +170,33 @@ public class CrudApi : BaseApi
return await Repository.Edit(resource);
}
+ ///
+ /// Edit
+ ///
+ ///
+ /// Edit an item. If the ID is specified it will be used to identify the resource.
+ /// If not, the slug will be used to identify it.
+ ///
+ /// The id or slug of the resource.
+ /// The resource to edit.
+ /// The edited resource.
+ /// The resource in the request body is invalid.
+ /// No item found with the specified ID (or slug).
+ [HttpPut("{identifier:id}")]
+ [PartialPermission(Kind.Write)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> Edit(Identifier identifier, [FromBody] T resource)
+ {
+ Guid id = await identifier.Match(
+ id => Task.FromResult(id),
+ async slug => (await Repository.Get(slug)).Id
+ );
+ resource.Id = id;
+ return await Repository.Edit(resource);
+ }
+
///
/// Patch
///
diff --git a/back/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs b/back/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs
index 6ec2470c..bf8aa7ca 100644
--- a/back/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs
+++ b/back/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs
@@ -16,7 +16,7 @@
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see .
-using System.IO;
+using System;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
@@ -28,14 +28,8 @@ using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api;
-///
-/// A base class to handle CRUD operations and services thumbnails for
-/// a specific resource type .
-///
-/// The type of resource to make CRUD and thumbnails apis for.
[ApiController]
-public class CrudThumbsApi(IRepository repository, IThumbnailsManager thumbs)
- : CrudApi(repository)
+public class CrudThumbsApi(IRepository repository) : CrudApi(repository)
where T : class, IResource, IThumbnails, IQuery
{
private async Task _GetImage(
@@ -50,18 +44,19 @@ public class CrudThumbsApi(IRepository repository, IThumbnailsManager thum
);
if (resource == null)
return NotFound();
- string path = thumbs.GetImagePath(resource, image, quality ?? ImageQuality.High);
- if (!System.IO.File.Exists(path))
+
+ Image? img = image switch
+ {
+ "poster" => resource.Poster,
+ "thumbnail" => resource.Thumbnail,
+ "logo" => resource.Logo,
+ _ => throw new ArgumentException(nameof(image)),
+ };
+ if (img is null)
return NotFound();
- if (!identifier.Match(id => false, slug => slug == "random"))
- {
- // Allow clients to cache the image for 6 month.
- Response.Headers.CacheControl = $"public, max-age={60 * 60 * 24 * 31 * 6}";
- }
- else
- Response.Headers.CacheControl = $"public, no-store";
- return PhysicalFile(Path.GetFullPath(path), "image/webp", true);
+ // TODO: Remove the /api and use a proxy rewrite instead.
+ return Redirect($"/api/thumbnails/{img.Id}");
}
///
@@ -78,7 +73,7 @@ public class CrudThumbsApi(IRepository repository, IThumbnailsManager thum
///
[HttpGet("{identifier:id}/poster")]
[PartialPermission(Kind.Read)]
- [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status302Found)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public Task GetPoster(Identifier identifier, [FromQuery] ImageQuality? quality)
{
@@ -99,7 +94,7 @@ public class CrudThumbsApi(IRepository repository, IThumbnailsManager thum
///
[HttpGet("{identifier:id}/logo")]
[PartialPermission(Kind.Read)]
- [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status302Found)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public Task GetLogo(Identifier identifier, [FromQuery] ImageQuality? quality)
{
@@ -120,6 +115,8 @@ public class CrudThumbsApi(IRepository repository, IThumbnailsManager thum
///
[HttpGet("{identifier:id}/thumbnail")]
[HttpGet("{identifier:id}/backdrop", Order = AlternativeRoute)]
+ [ProducesResponseType(StatusCodes.Status302Found)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public Task GetBackdrop(Identifier identifier, [FromQuery] ImageQuality? quality)
{
return _GetImage(identifier, "thumbnail", quality);
diff --git a/back/src/Kyoo.Core/Views/Helper/Transcoder.cs b/back/src/Kyoo.Core/Views/Helper/Transcoder.cs
index b6220fe5..337f437b 100644
--- a/back/src/Kyoo.Core/Views/Helper/Transcoder.cs
+++ b/back/src/Kyoo.Core/Views/Helper/Transcoder.cs
@@ -35,8 +35,7 @@ public static class Transcoder
Environment.GetEnvironmentVariable("TRANSCODER_URL") ?? "http://transcoder:7666";
}
-public abstract class TranscoderApi(IRepository repository, IThumbnailsManager thumbs)
- : CrudThumbsApi(repository, thumbs)
+public abstract class TranscoderApi(IRepository repository) : CrudThumbsApi(repository)
where T : class, IResource, IThumbnails, IQuery
{
private Task _Proxy(string route, (string path, string route) info)
diff --git a/back/src/Kyoo.Core/Views/Resources/CollectionApi.cs b/back/src/Kyoo.Core/Views/Resources/CollectionApi.cs
index c40b1bc6..24acee4a 100644
--- a/back/src/Kyoo.Core/Views/Resources/CollectionApi.cs
+++ b/back/src/Kyoo.Core/Views/Resources/CollectionApi.cs
@@ -40,25 +40,13 @@ namespace Kyoo.Core.Api;
[ApiController]
[PartialPermission(nameof(Collection))]
[ApiDefinition("Collections", Group = ResourcesGroup)]
-public class CollectionApi : CrudThumbsApi
+public class CollectionApi(
+ IRepository movies,
+ IRepository shows,
+ CollectionRepository collections,
+ LibraryItemRepository items
+) : CrudThumbsApi(collections)
{
- private readonly ILibraryManager _libraryManager;
- private readonly CollectionRepository _collections;
- private readonly LibraryItemRepository _items;
-
- public CollectionApi(
- ILibraryManager libraryManager,
- CollectionRepository collections,
- LibraryItemRepository items,
- IThumbnailsManager thumbs
- )
- : base(libraryManager.Collections, thumbs)
- {
- _libraryManager = libraryManager;
- _collections = collections;
- _items = items;
- }
-
///
/// Add a movie
///
@@ -79,14 +67,14 @@ public class CollectionApi : CrudThumbsApi
public async Task AddMovie(Identifier identifier, Identifier movie)
{
Guid collectionId = await identifier.Match(
- async id => (await _libraryManager.Collections.Get(id)).Id,
- async slug => (await _libraryManager.Collections.Get(slug)).Id
+ async id => (await collections.Get(id)).Id,
+ async slug => (await collections.Get(slug)).Id
);
Guid movieId = await movie.Match(
- async id => (await _libraryManager.Movies.Get(id)).Id,
- async slug => (await _libraryManager.Movies.Get(slug)).Id
+ async id => (await movies.Get(id)).Id,
+ async slug => (await movies.Get(slug)).Id
);
- await _collections.AddMovie(collectionId, movieId);
+ await collections.AddMovie(collectionId, movieId);
return NoContent();
}
@@ -110,14 +98,14 @@ public class CollectionApi : CrudThumbsApi
public async Task AddShow(Identifier identifier, Identifier show)
{
Guid collectionId = await identifier.Match(
- async id => (await _libraryManager.Collections.Get(id)).Id,
- async slug => (await _libraryManager.Collections.Get(slug)).Id
+ async id => (await collections.Get(id)).Id,
+ async slug => (await collections.Get(slug)).Id
);
Guid showId = await show.Match(
- async id => (await _libraryManager.Shows.Get(id)).Id,
- async slug => (await _libraryManager.Shows.Get(slug)).Id
+ async id => (await shows.Get(id)).Id,
+ async slug => (await shows.Get(slug)).Id
);
- await _collections.AddShow(collectionId, showId);
+ await collections.AddShow(collectionId, showId);
return NoContent();
}
@@ -151,9 +139,9 @@ public class CollectionApi : CrudThumbsApi
{
Guid collectionId = await identifier.Match(
id => Task.FromResult(id),
- async slug => (await _libraryManager.Collections.Get(slug)).Id
+ async slug => (await collections.Get(slug)).Id
);
- ICollection resources = await _items.GetAllOfCollection(
+ ICollection resources = await items.GetAllOfCollection(
collectionId,
filter,
sortBy == new Sort.Default()
@@ -165,8 +153,7 @@ public class CollectionApi : CrudThumbsApi
if (
!resources.Any()
- && await _libraryManager.Collections.GetOrDefault(identifier.IsSame())
- == null
+ && await collections.GetOrDefault(identifier.IsSame()) == null
)
return NotFound();
return Page(resources, pagination.Limit);
@@ -200,7 +187,7 @@ public class CollectionApi : CrudThumbsApi
[FromQuery] Include? fields
)
{
- ICollection resources = await _libraryManager.Shows.GetAll(
+ ICollection resources = await shows.GetAll(
Filter.And(filter, identifier.IsContainedIn(x => x.Collections)),
sortBy == new Sort.Default() ? new Sort.By(x => x.AirDate) : sortBy,
fields,
@@ -209,8 +196,7 @@ public class CollectionApi : CrudThumbsApi
if (
!resources.Any()
- && await _libraryManager.Collections.GetOrDefault(identifier.IsSame())
- == null
+ && await collections.GetOrDefault(identifier.IsSame()) == null
)
return NotFound();
return Page(resources, pagination.Limit);
@@ -244,7 +230,7 @@ public class CollectionApi : CrudThumbsApi
[FromQuery] Include? fields
)
{
- ICollection resources = await _libraryManager.Movies.GetAll(
+ ICollection resources = await movies.GetAll(
Filter.And(filter, identifier.IsContainedIn(x => x.Collections)),
sortBy == new Sort.Default() ? new Sort.By(x => x.AirDate) : sortBy,
fields,
@@ -253,8 +239,7 @@ public class CollectionApi : CrudThumbsApi
if (
!resources.Any()
- && await _libraryManager.Collections.GetOrDefault(identifier.IsSame())
- == null
+ && await collections.GetOrDefault(identifier.IsSame()) == null
)
return NotFound();
return Page(resources, pagination.Limit);
diff --git a/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs b/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs
index f44cdf6e..93b3063c 100644
--- a/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs
+++ b/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs
@@ -38,8 +38,8 @@ namespace Kyoo.Core.Api;
[ApiController]
[PartialPermission(nameof(Episode))]
[ApiDefinition("Episodes", Group = ResourcesGroup)]
-public class EpisodeApi(ILibraryManager libraryManager, IThumbnailsManager thumbnails)
- : TranscoderApi(libraryManager.Episodes, thumbnails)
+public class EpisodeApi(ILibraryManager libraryManager)
+ : TranscoderApi(libraryManager.Episodes)
{
///
/// Get episode's show
diff --git a/back/src/Kyoo.Core/Views/Resources/LibraryItemApi.cs b/back/src/Kyoo.Core/Views/Resources/LibraryItemApi.cs
index 9e203375..37f28c26 100644
--- a/back/src/Kyoo.Core/Views/Resources/LibraryItemApi.cs
+++ b/back/src/Kyoo.Core/Views/Resources/LibraryItemApi.cs
@@ -34,23 +34,5 @@ namespace Kyoo.Core.Api;
[ApiController]
[PartialPermission("LibraryItem")]
[ApiDefinition("Items", Group = ResourcesGroup)]
-public class LibraryItemApi : CrudThumbsApi
-{
- ///
- /// The library item repository used to modify or retrieve information in the data store.
- ///
- private readonly IRepository _libraryItems;
-
- ///