mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Rework images storing (#428)
This commit is contained in:
commit
41130cab1c
@ -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
|
||||
|
@ -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"),
|
||||
|
@ -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,
|
||||
},
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -28,6 +28,11 @@
|
||||
<CheckCodingStyle Condition="$(CheckCodingStyle) == ''">true</CheckCodingStyle>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<BaseIntermediateOutputPath>$(MsBuildThisFileDirectory)/../out/obj/$(MSBuildProjectName)</BaseIntermediateOutputPath>
|
||||
<BaseOutputPath>$(MsBuildThisFileDirectory)/../out/bin/$(MSBuildProjectName)</BaseOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition="$(CheckCodingStyle) == true">
|
||||
<None Include="$(MSBuildThisFileDirectory)../.editorconfig" Link=".editorconfig" Visible="false" />
|
||||
</ItemGroup>
|
||||
|
@ -23,56 +23,19 @@ using Kyoo.Abstractions.Models;
|
||||
|
||||
namespace Kyoo.Abstractions.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Download images and retrieve the path of those images for a resource.
|
||||
/// </summary>
|
||||
public interface IThumbnailsManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Download images of a specified item.
|
||||
/// If no images is available to download, do nothing and silently return.
|
||||
/// </summary>
|
||||
/// <param name="item">
|
||||
/// The item to cache images.
|
||||
/// </param>
|
||||
/// <typeparam name="T">The type of the item</typeparam>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
Task DownloadImages<T>(T item)
|
||||
where T : IThumbnails;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve the local path of an image of the given item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to retrieve the poster from.</param>
|
||||
/// <param name="image">The ID of the image.</param>
|
||||
/// <param name="quality">The quality of the image</param>
|
||||
/// <typeparam name="T">The type of the item</typeparam>
|
||||
/// <returns>The path of the image for the given resource or null if it does not exists.</returns>
|
||||
string GetImagePath<T>(T item, string image, ImageQuality quality)
|
||||
where T : IThumbnails;
|
||||
Task DownloadImage(Image? image, string what);
|
||||
|
||||
string GetImagePath(Guid imageId, ImageQuality quality);
|
||||
|
||||
/// <summary>
|
||||
/// Delete images associated with the item.
|
||||
/// </summary>
|
||||
/// <param name="item">
|
||||
/// The item with cached images.
|
||||
/// </param>
|
||||
/// <typeparam name="T">The type of the item</typeparam>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
Task DeleteImages<T>(T item)
|
||||
where T : IThumbnails;
|
||||
|
||||
/// <summary>
|
||||
/// Set the user's profile picture
|
||||
/// </summary>
|
||||
/// <param name="userId">The id of the user. </param>
|
||||
/// <returns>The byte stream of the image. Null if no image exist.</returns>
|
||||
Task<Stream> GetUserImage(Guid userId);
|
||||
|
||||
/// <summary>
|
||||
/// Set the user's profile picture
|
||||
/// </summary>
|
||||
/// <param name="userId">The id of the user. </param>
|
||||
/// <param name="image">The byte stream of the image. Null to delete the image.</param>
|
||||
Task SetUserImage(Guid userId, Stream? image);
|
||||
}
|
||||
|
@ -94,7 +94,7 @@ public class PermissionAttribute : Attribute, IFilterFactory
|
||||
/// <summary>
|
||||
/// The group of this permission.
|
||||
/// </summary>
|
||||
public Group Group { get; }
|
||||
public Group Group { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ask a permission to run an action.
|
||||
|
@ -17,12 +17,9 @@
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// A unique identifier for the image. Used for proper http caches.
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The original image from another server.
|
||||
/// </summary>
|
||||
@ -63,6 +64,21 @@ public class Image
|
||||
[MaxLength(32)]
|
||||
public string Blurhash { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The url to access the image in low quality.
|
||||
/// </summary>
|
||||
public string Low => $"/thumbnails/{Id}?quality=low";
|
||||
|
||||
/// <summary>
|
||||
/// The url to access the image in medium quality.
|
||||
/// </summary>
|
||||
public string Medium => $"/thumbnails/{Id}?quality=medium";
|
||||
|
||||
/// <summary>
|
||||
/// The url to access the image in high quality.
|
||||
/// </summary>
|
||||
public string High => $"/thumbnails/{Id}?quality=high";
|
||||
|
||||
public Image() { }
|
||||
|
||||
[JsonConstructor]
|
||||
@ -72,6 +88,7 @@ public class Image
|
||||
Blurhash = blurhash ?? "000000";
|
||||
}
|
||||
|
||||
//
|
||||
public class ImageConvertor : JsonConverter<Image>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
@ -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<Image>();
|
||||
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 };
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -56,4 +56,5 @@ public static class Constants
|
||||
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints used by admins.
|
||||
/// </summary>
|
||||
public const string AdminGroup = "4:Admin";
|
||||
public const string OtherGroup = "5:Other";
|
||||
}
|
||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Kyoo.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// A set of extensions class for enumerable.
|
||||
/// </summary>
|
||||
public static class EnumerableExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// If the enumerable is empty, execute an action.
|
||||
/// </summary>
|
||||
/// <param name="self">The enumerable to check</param>
|
||||
/// <param name="action">The action to execute is the list is empty</param>
|
||||
/// <typeparam name="T">The type of items inside the list</typeparam>
|
||||
/// <returns>The iterator proxied, there is no dual iterations.</returns>
|
||||
public static IEnumerable<T> IfEmpty<T>(this IEnumerable<T> self, Action action)
|
||||
{
|
||||
static IEnumerable<T> Generator(IEnumerable<T> self, Action action)
|
||||
{
|
||||
using IEnumerator<T> enumerator = self.GetEnumerator();
|
||||
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
action();
|
||||
yield break;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
yield return enumerator.Current;
|
||||
} while (enumerator.MoveNext());
|
||||
}
|
||||
|
||||
return Generator(self, action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A foreach used as a function with a little specificity: the list can be null.
|
||||
/// </summary>
|
||||
/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
|
||||
/// <param name="action">The action to execute for each arguments</param>
|
||||
/// <typeparam name="T">The type of items in the list</typeparam>
|
||||
public static void ForEach<T>(this IEnumerable<T>? self, Action<T> action)
|
||||
{
|
||||
if (self == null)
|
||||
return;
|
||||
foreach (T i in self)
|
||||
action(i);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Kyoo.Abstractions.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// A class containing helper methods to merge objects.
|
||||
/// </summary>
|
||||
public static class Merger
|
||||
{
|
||||
/// <summary>
|
||||
/// Merge two dictionary, if the same key is found on both dictionary, the values of the second one is kept.
|
||||
/// </summary>
|
||||
/// <param name="first">The first dictionary to merge</param>
|
||||
/// <param name="second">The second dictionary to merge</param>
|
||||
/// <param name="hasChanged">
|
||||
/// <c>true</c> if a new items has been added to the dictionary, <c>false</c> otherwise.
|
||||
/// </param>
|
||||
/// <typeparam name="T">The type of the keys in dictionaries</typeparam>
|
||||
/// <typeparam name="T2">The type of values in the dictionaries</typeparam>
|
||||
/// <returns>
|
||||
/// A dictionary with the missing elements of <paramref name="second"/>
|
||||
/// set to those of <paramref name="first"/>.
|
||||
/// </returns>
|
||||
public static IDictionary<T, T2>? CompleteDictionaries<T, T2>(
|
||||
IDictionary<T, T2>? first,
|
||||
IDictionary<T, T2>? 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="IOnMerge"/>
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "foo"}
|
||||
/// </example>
|
||||
/// <param name="first">
|
||||
/// The object to complete
|
||||
/// </param>
|
||||
/// <param name="second">
|
||||
/// Missing fields of first will be completed by fields of this item. If second is null, the function no-op.
|
||||
/// </param>
|
||||
/// <param name="where">
|
||||
/// Filter fields that will be merged
|
||||
/// </param>
|
||||
/// <typeparam name="T">Fields of T will be completed</typeparam>
|
||||
/// <returns><paramref name="first"/></returns>
|
||||
public static T Complete<T>(T first, T? second, Func<PropertyInfo, bool>? where = null)
|
||||
{
|
||||
if (second == null)
|
||||
return first;
|
||||
|
||||
Type type = typeof(T);
|
||||
IEnumerable<PropertyInfo> 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<object>(
|
||||
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;
|
||||
}
|
||||
}
|
@ -177,25 +177,6 @@ public static class Utility
|
||||
yield return type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if <paramref name="type"/> inherit from a generic type <paramref name="genericType"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to check</param>
|
||||
/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable<>).</param>
|
||||
/// <returns>True if obj inherit from genericType. False otherwise</returns>
|
||||
public static bool IsOfGenericType(Type type, Type genericType)
|
||||
{
|
||||
if (!genericType.IsGenericType)
|
||||
throw new ArgumentException($"{nameof(genericType)} is not a generic type.");
|
||||
|
||||
IEnumerable<Type> types = genericType.IsInterface
|
||||
? type.GetInterfaces()
|
||||
: type.GetInheritanceTree();
|
||||
return types
|
||||
.Prepend(type)
|
||||
.Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the generic definition of <paramref name="genericType"/>.
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a method from an <see cref="Type"/> with the given name and respect the
|
||||
/// amount of parameters and generic parameters. This works for polymorphic methods.
|
||||
/// </summary>
|
||||
/// <param name="type">
|
||||
/// The type owning the method. For non static methods, this is the <c>this</c>.
|
||||
/// </param>
|
||||
/// <param name="flag">
|
||||
/// The binding flags of the method. This allow you to specify public/private and so on.
|
||||
/// </param>
|
||||
/// <param name="name">
|
||||
/// The name of the method.
|
||||
/// </param>
|
||||
/// <param name="generics">
|
||||
/// The list of generic parameters.
|
||||
/// </param>
|
||||
/// <param name="args">
|
||||
/// The list of parameters.
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentException">No method match the given constraints.</exception>
|
||||
/// <returns>The method handle of the matching method.</returns>
|
||||
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<T> 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."
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run a generic static method for a runtime <see cref="Type"/>.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// To run Merger.MergeLists{T} for a List where you don't know the type at compile type,
|
||||
/// you could do:
|
||||
/// <code lang="C#">
|
||||
/// Utility.RunGenericMethod<object>(
|
||||
/// typeof(Utility),
|
||||
/// nameof(MergeLists),
|
||||
/// enumerableType,
|
||||
/// oldValue, newValue, equalityComparer)
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// <param name="owner">The type that owns the method. For non static methods, the type of <c>this</c>.</param>
|
||||
/// <param name="methodName">The name of the method. You should use the <c>nameof</c> keyword.</param>
|
||||
/// <param name="type">The generic type to run the method with.</param>
|
||||
/// <param name="args">The list of arguments of the method</param>
|
||||
/// <typeparam name="T">
|
||||
/// The return type of the method. You can put <see cref="object"/> for an unknown one.
|
||||
/// </typeparam>
|
||||
/// <exception cref="ArgumentException">No method match the given constraints.</exception>
|
||||
/// <returns>The return of the method you wanted to run.</returns>
|
||||
/// <seealso cref="RunGenericMethod{T}(System.Type,string,System.Type[],object[])"/>
|
||||
public static T? RunGenericMethod<T>(
|
||||
Type owner,
|
||||
string methodName,
|
||||
Type type,
|
||||
params object[] args
|
||||
)
|
||||
{
|
||||
return RunGenericMethod<T>(owner, methodName, new[] { type }, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run a generic static method for a multiple runtime <see cref="Type"/>.
|
||||
/// If your generic method only needs one type, see
|
||||
/// <see cref="RunGenericMethod{T}(System.Type,string,System.Type,object[])"/>
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// To run Merger.MergeLists{T} for a List where you don't know the type at compile type,
|
||||
/// you could do:
|
||||
/// <code>
|
||||
/// Utility.RunGenericMethod<object>(
|
||||
/// typeof(Utility),
|
||||
/// nameof(MergeLists),
|
||||
/// enumerableType,
|
||||
/// oldValue, newValue, equalityComparer)
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// <param name="owner">The type that owns the method. For non static methods, the type of <c>this</c>.</param>
|
||||
/// <param name="methodName">The name of the method. You should use the <c>nameof</c> keyword.</param>
|
||||
/// <param name="types">The list of generic types to run the method with.</param>
|
||||
/// <param name="args">The list of arguments of the method</param>
|
||||
/// <typeparam name="T">
|
||||
/// The return type of the method. You can put <see cref="object"/> for an unknown one.
|
||||
/// </typeparam>
|
||||
/// <exception cref="ArgumentException">No method match the given constraints.</exception>
|
||||
/// <returns>The return of the method you wanted to run.</returns>
|
||||
/// <seealso cref="RunGenericMethod{T}(System.Type,string,System.Type,object[])"/>
|
||||
public static T? RunGenericMethod<T>(
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a dictionary to a query string.
|
||||
/// </summary>
|
||||
|
85
back/src/Kyoo.Core/Controllers/MiscRepository.cs
Normal file
85
back/src/Kyoo.Core/Controllers/MiscRepository.cs
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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<MiscRepository>().DownloadMissingImages();
|
||||
}
|
||||
|
||||
private async Task<ICollection<Image>> _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<Image?> ret = await database.QueryAsync<Image?>(sql);
|
||||
return ret.Where(x => x != null).ToArray() as Image[];
|
||||
}
|
||||
|
||||
public async Task DownloadMissingImages()
|
||||
{
|
||||
ICollection<Image> images = await _GetAllImages();
|
||||
IEnumerable<Task> 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<Task> batch in tasks.Chunk(30))
|
||||
await Task.WhenAll(batch);
|
||||
}
|
||||
|
||||
public async Task<ICollection<string>> GetRegisteredPaths()
|
||||
{
|
||||
return await context
|
||||
.Episodes.Select(x => x.Path)
|
||||
.Concat(context.Movies.Select(x => x.Path))
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
@ -31,46 +31,21 @@ namespace Kyoo.Core.Controllers;
|
||||
/// <summary>
|
||||
/// A local repository to handle collections
|
||||
/// </summary>
|
||||
public class CollectionRepository : LocalRepository<Collection>
|
||||
public class CollectionRepository(DatabaseContext database, IThumbnailsManager thumbnails)
|
||||
: GenericRepository<Collection>(database)
|
||||
{
|
||||
/// <summary>
|
||||
/// The database handle
|
||||
/// </summary>
|
||||
private readonly DatabaseContext _database;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="CollectionRepository"/>.
|
||||
/// </summary>
|
||||
/// <param name="database">The database handle to use</param>
|
||||
/// <param name="thumbs">The thumbnail manager used to store images.</param>
|
||||
public CollectionRepository(DatabaseContext database, IThumbnailsManager thumbs)
|
||||
: base(database, thumbs)
|
||||
{
|
||||
_database = database;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<ICollection<Collection>> Search(
|
||||
string query,
|
||||
Include<Collection>? 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();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<Collection> Create(Collection obj)
|
||||
{
|
||||
await base.Create(obj);
|
||||
_database.Entry(obj).State = EntityState.Added;
|
||||
await _database.SaveChangesAsync(() => Get(obj.Slug));
|
||||
await IRepository<Collection>.OnResourceCreated(obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task Validate(Collection resource)
|
||||
{
|
||||
@ -78,25 +53,18 @@ public class CollectionRepository : LocalRepository<Collection>
|
||||
|
||||
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<Collection, Movie>(id, movieId);
|
||||
await _database.SaveChangesAsync();
|
||||
Database.AddLinks<Collection, Movie>(id, movieId);
|
||||
await Database.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task AddShow(Guid id, Guid showId)
|
||||
{
|
||||
_database.AddLinks<Collection, Show>(id, showId);
|
||||
await _database.SaveChangesAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task Delete(Collection obj)
|
||||
{
|
||||
_database.Entry(obj).State = EntityState.Deleted;
|
||||
await _database.SaveChangesAsync();
|
||||
await base.Delete(obj);
|
||||
Database.AddLinks<Collection, Show>(id, showId);
|
||||
await Database.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
@ -252,7 +252,7 @@ public static class DapperHelper
|
||||
this IDbConnection db,
|
||||
FormattableString command,
|
||||
Dictionary<string, Type> config,
|
||||
Func<List<object?>, T> mapper,
|
||||
Func<IList<object?>, T> mapper,
|
||||
Func<Guid, Task<T>> get,
|
||||
SqlVariableContext context,
|
||||
Include<T>? 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<object?> 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<string, Type> config,
|
||||
Func<List<object?>, T> mapper,
|
||||
Func<IList<object?>, T> mapper,
|
||||
SqlVariableContext context,
|
||||
Include<T>? include,
|
||||
Filter<T>? filter,
|
||||
|
@ -37,7 +37,7 @@ public abstract class DapperRepository<T> : IRepository<T>
|
||||
|
||||
protected abstract Dictionary<string, Type> Config { get; }
|
||||
|
||||
protected abstract T Mapper(List<object?> items);
|
||||
protected abstract T Mapper(IList<object?> items);
|
||||
|
||||
protected DbConnection Database { get; init; }
|
||||
|
||||
|
@ -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<Show> shows,
|
||||
IThumbnailsManager thumbs
|
||||
) : LocalRepository<Episode>(database, thumbs)
|
||||
IThumbnailsManager thumbnails
|
||||
) : GenericRepository<Episode>(database)
|
||||
{
|
||||
static EpisodeRepository()
|
||||
{
|
||||
@ -64,70 +65,77 @@ public class EpisodeRepository(
|
||||
Include<Episode>? 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<Episode?> 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
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<Episode> 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<Episode>.OnResourceCreated(obj);
|
||||
return obj;
|
||||
return await base.Create(obj);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task DeleteAll(Filter<Episode> filter)
|
||||
{
|
||||
ICollection<Episode> items = await GetAll(filter);
|
||||
Guid[] ids = items.Select(x => x.Id).ToArray();
|
||||
|
||||
await Database.Set<Episode>().Where(x => ids.Contains(x.Id)).ExecuteDeleteAsync();
|
||||
foreach (Episode resource in items)
|
||||
await IRepository<Episode>.OnResourceDeleted(resource);
|
||||
|
||||
Guid[] showIds = await Database
|
||||
.Set<Episode>()
|
||||
.Where(filter.ToEfLambda())
|
||||
.Select(x => x.Show!)
|
||||
.Where(x => !x.Episodes!.Any())
|
||||
.Select(x => x.Id)
|
||||
.ToArrayAsync();
|
||||
|
||||
if (!showIds.Any())
|
||||
return;
|
||||
|
||||
Filter<Show>[] showFilters = showIds
|
||||
.Select(x => new Filter<Show>.Eq(nameof(Show.Id), x))
|
||||
.ToArray();
|
||||
await shows.DeleteAll(Filter.Or(showFilters)!);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// A base class to create repositories using Entity Framework.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of this repository</typeparam>
|
||||
public abstract class LocalRepository<T> : IRepository<T>
|
||||
public abstract class GenericRepository<T>(DatabaseContext database) : IRepository<T>
|
||||
where T : class, IResource, IQuery
|
||||
{
|
||||
/// <summary>
|
||||
/// The Entity Framework's Database handle.
|
||||
/// </summary>
|
||||
protected DbContext Database { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The thumbnail manager used to store images.
|
||||
/// </summary>
|
||||
private readonly IThumbnailsManager _thumbs;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new base <see cref="LocalRepository{T}"/> with the given database handle.
|
||||
/// </summary>
|
||||
/// <param name="database">A database connection to load resources of type <typeparamref name="T"/></param>
|
||||
/// <param name="thumbs">The thumbnail manager used to store images.</param>
|
||||
protected LocalRepository(DbContext database, IThumbnailsManager thumbs)
|
||||
{
|
||||
Database = database;
|
||||
_thumbs = thumbs;
|
||||
}
|
||||
public DatabaseContext Database => database;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Type RepositoryType => typeof(T);
|
||||
@ -127,12 +104,6 @@ public abstract class LocalRepository<T> : IRepository<T>
|
||||
return query;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a resource from it's ID and make the <see cref="Database"/> instance track it.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the resource</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||
/// <returns>The tracked resource with the given ID</returns>
|
||||
protected virtual async Task<T> GetWithTracking(Guid id)
|
||||
{
|
||||
T? ret = await Database.Set<T>().AsTracking().FirstOrDefaultAsync(x => x.Id == id);
|
||||
@ -174,11 +145,6 @@ public abstract class LocalRepository<T> : IRepository<T>
|
||||
return ret;
|
||||
}
|
||||
|
||||
protected virtual Task<T?> GetDuplicated(T item)
|
||||
{
|
||||
return GetOrDefault(item.Slug);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task<T?> GetOrDefault(Guid id, Include<T>? include = default)
|
||||
{
|
||||
@ -303,26 +269,9 @@ public abstract class LocalRepository<T> : IRepository<T>
|
||||
public virtual async Task<T> 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<T>.OnResourceCreated(obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
@ -346,27 +295,11 @@ public abstract class LocalRepository<T> : IRepository<T>
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task<T> 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<LoadableRelationAttribute>() == null
|
||||
);
|
||||
await EditRelations(old, edited);
|
||||
await Database.SaveChangesAsync();
|
||||
await IRepository<T>.OnResourceEdited(old);
|
||||
return old;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Database.ChangeTracker.LazyLoadingEnabled = lazyLoading;
|
||||
Database.ChangeTracker.Clear();
|
||||
}
|
||||
await Validate(edited);
|
||||
Database.Update(edited);
|
||||
await Database.SaveChangesAsync();
|
||||
await IRepository<T>.OnResourceEdited(edited);
|
||||
return edited;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@ -391,39 +324,9 @@ public abstract class LocalRepository<T> : IRepository<T>
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An overridable method to edit relation of a resource.
|
||||
/// </summary>
|
||||
/// <param name="resource">
|
||||
/// The non edited resource
|
||||
/// </param>
|
||||
/// <param name="changed">
|
||||
/// The new version of <paramref name="resource"/>.
|
||||
/// This item will be saved on the database and replace <paramref name="resource"/>
|
||||
/// </param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A method called just before saving a new resource to the database.
|
||||
/// It is also called on the default implementation of <see cref="EditRelations"/>
|
||||
/// </summary>
|
||||
/// <param name="resource">The resource that will be saved</param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <exception cref="ValidationException">
|
||||
/// You can throw this if the resource is illegal and should not be saved.
|
||||
/// </exception>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
protected virtual Task Validate(T resource)
|
||||
{
|
||||
if (
|
||||
@ -432,26 +335,9 @@ public abstract class LocalRepository<T> : IRepository<T>
|
||||
)
|
||||
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<T> : IRepository<T>
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual Task Delete(T obj)
|
||||
public virtual async Task Delete(T obj)
|
||||
{
|
||||
IRepository<T>.OnResourceDeleted(obj);
|
||||
if (obj is IThumbnails thumbs)
|
||||
return _thumbs.DeleteImages(thumbs);
|
||||
return Task.CompletedTask;
|
||||
await Database.Set<T>().Where(x => x.Id == obj.Id).ExecuteDeleteAsync();
|
||||
await IRepository<T>.OnResourceDeleted(obj);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task DeleteAll(Filter<T> filter)
|
||||
public virtual async Task DeleteAll(Filter<T> filter)
|
||||
{
|
||||
foreach (T resource in await GetAll(filter))
|
||||
await Delete(resource);
|
||||
ICollection<T> items = await GetAll(filter);
|
||||
Guid[] ids = items.Select(x => x.Id).ToArray();
|
||||
await Database.Set<T>().Where(x => ids.Contains(x.Id)).ExecuteDeleteAsync();
|
||||
|
||||
foreach (T resource in items)
|
||||
await IRepository<T>.OnResourceDeleted(resource);
|
||||
}
|
||||
}
|
@ -30,7 +30,8 @@ namespace Kyoo.Core.Controllers;
|
||||
/// <summary>
|
||||
/// A local repository to handle library items.
|
||||
/// </summary>
|
||||
public class LibraryItemRepository : DapperRepository<ILibraryItem>
|
||||
public class LibraryItemRepository(DbConnection database, SqlVariableContext context)
|
||||
: DapperRepository<ILibraryItem>(database, context)
|
||||
{
|
||||
// language=PostgreSQL
|
||||
protected override FormattableString Sql =>
|
||||
@ -67,7 +68,7 @@ public class LibraryItemRepository : DapperRepository<ILibraryItem>
|
||||
{ "c", typeof(Collection) }
|
||||
};
|
||||
|
||||
protected override ILibraryItem Mapper(List<object?> items)
|
||||
protected override ILibraryItem Mapper(IList<object?> items)
|
||||
{
|
||||
if (items[0] is Show show && show.Id != Guid.Empty)
|
||||
return show;
|
||||
@ -78,9 +79,6 @@ public class LibraryItemRepository : DapperRepository<ILibraryItem>
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
|
||||
public LibraryItemRepository(DbConnection database, SqlVariableContext context)
|
||||
: base(database, context) { }
|
||||
|
||||
public async Task<ICollection<ILibraryItem>> GetAllOfCollection(
|
||||
Guid collectionId,
|
||||
Filter<ILibraryItem>? filter = default,
|
||||
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// A local repository to handle shows
|
||||
/// </summary>
|
||||
public class MovieRepository : LocalRepository<Movie>
|
||||
public class MovieRepository(
|
||||
DatabaseContext database,
|
||||
IRepository<Studio> studios,
|
||||
IThumbnailsManager thumbnails
|
||||
) : GenericRepository<Movie>(database)
|
||||
{
|
||||
/// <summary>
|
||||
/// The database handle
|
||||
/// </summary>
|
||||
private readonly DatabaseContext _database;
|
||||
|
||||
/// <summary>
|
||||
/// A studio repository to handle creation/validation of related studios.
|
||||
/// </summary>
|
||||
private readonly IRepository<Studio> _studios;
|
||||
|
||||
public MovieRepository(
|
||||
DatabaseContext database,
|
||||
IRepository<Studio> studios,
|
||||
IThumbnailsManager thumbs
|
||||
)
|
||||
: base(database, thumbs)
|
||||
{
|
||||
_database = database;
|
||||
_studios = studios;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<ICollection<Movie>> Search(
|
||||
string query,
|
||||
Include<Movie>? 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();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<Movie> Create(Movie obj)
|
||||
public override Task<Movie> Create(Movie obj)
|
||||
{
|
||||
await base.Create(obj);
|
||||
_database.Entry(obj).State = EntityState.Added;
|
||||
await _database.SaveChangesAsync(() => Get(obj.Slug));
|
||||
await IRepository<Movie>.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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -81,28 +71,9 @@ public class MovieRepository : LocalRepository<Movie>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task Delete(Movie obj)
|
||||
{
|
||||
_database.Remove(obj);
|
||||
await _database.SaveChangesAsync();
|
||||
await base.Delete(obj);
|
||||
await thumbnails.DownloadImages(resource);
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ public class NewsRepository : DapperRepository<INews>
|
||||
protected override Dictionary<string, Type> Config =>
|
||||
new() { { "e", typeof(Episode) }, { "m", typeof(Movie) }, };
|
||||
|
||||
protected override INews Mapper(List<object?> items)
|
||||
protected override INews Mapper(IList<object?> items)
|
||||
{
|
||||
if (items[0] is Episode episode && episode.Id != Guid.Empty)
|
||||
return episode;
|
||||
|
@ -31,16 +31,9 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Kyoo.Core.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// A local repository to handle seasons.
|
||||
/// </summary>
|
||||
public class SeasonRepository : LocalRepository<Season>
|
||||
public class SeasonRepository(DatabaseContext database, IThumbnailsManager thumbnails)
|
||||
: GenericRepository<Season>(database)
|
||||
{
|
||||
/// <summary>
|
||||
/// The database handle
|
||||
/// </summary>
|
||||
private readonly DatabaseContext _database;
|
||||
|
||||
static SeasonRepository()
|
||||
{
|
||||
// Edit seasons slugs when the show's slug changes.
|
||||
@ -61,31 +54,13 @@ public class SeasonRepository : LocalRepository<Season>
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="SeasonRepository"/>.
|
||||
/// </summary>
|
||||
/// <param name="database">The database handle that will be used</param>
|
||||
/// <param name="thumbs">The thumbnail manager used to store images.</param>
|
||||
public SeasonRepository(DatabaseContext database, IThumbnailsManager thumbs)
|
||||
: base(database, thumbs)
|
||||
{
|
||||
_database = database;
|
||||
}
|
||||
|
||||
protected override Task<Season?> GetDuplicated(Season item)
|
||||
{
|
||||
return _database.Seasons.FirstOrDefaultAsync(x =>
|
||||
x.ShowId == item.ShowId && x.SeasonNumber == item.SeasonNumber
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<ICollection<Season>> Search(
|
||||
string query,
|
||||
Include<Season>? 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<Season>
|
||||
/// <inheritdoc/>
|
||||
public override async Task<Season> 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<Season>.OnResourceCreated(obj);
|
||||
return obj;
|
||||
return await base.Create(obj);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// A local repository to handle shows
|
||||
/// </summary>
|
||||
public class ShowRepository : LocalRepository<Show>
|
||||
public class ShowRepository(
|
||||
DatabaseContext database,
|
||||
IRepository<Studio> studios,
|
||||
IThumbnailsManager thumbnails
|
||||
) : GenericRepository<Show>(database)
|
||||
{
|
||||
/// <summary>
|
||||
/// The database handle
|
||||
/// </summary>
|
||||
private readonly DatabaseContext _database;
|
||||
|
||||
/// <summary>
|
||||
/// A studio repository to handle creation/validation of related studios.
|
||||
/// </summary>
|
||||
private readonly IRepository<Studio> _studios;
|
||||
|
||||
public ShowRepository(
|
||||
DatabaseContext database,
|
||||
IRepository<Studio> studios,
|
||||
IThumbnailsManager thumbs
|
||||
)
|
||||
: base(database, thumbs)
|
||||
{
|
||||
_database = database;
|
||||
_studios = studios;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<ICollection<Show>> Search(
|
||||
string query,
|
||||
Include<Show>? 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();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<Show> Create(Show obj)
|
||||
public override Task<Show> Create(Show obj)
|
||||
{
|
||||
await base.Create(obj);
|
||||
_database.Entry(obj).State = EntityState.Added;
|
||||
await _database.SaveChangesAsync(() => Get(obj.Slug));
|
||||
await IRepository<Show>.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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -82,28 +71,9 @@ public class ShowRepository : LocalRepository<Show>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task Delete(Show obj)
|
||||
{
|
||||
_database.Remove(obj);
|
||||
await _database.SaveChangesAsync();
|
||||
await base.Delete(obj);
|
||||
await thumbnails.DownloadImages(resource);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
/// <summary>
|
||||
/// A local repository to handle studios
|
||||
/// </summary>
|
||||
public class StudioRepository : LocalRepository<Studio>
|
||||
public class StudioRepository(DatabaseContext database) : GenericRepository<Studio>(database)
|
||||
{
|
||||
/// <summary>
|
||||
/// The database handle
|
||||
/// </summary>
|
||||
private readonly DatabaseContext _database;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="StudioRepository"/>.
|
||||
/// </summary>
|
||||
/// <param name="database">The database handle</param>
|
||||
/// <param name="thumbs">The thumbnail manager used to store images.</param>
|
||||
public StudioRepository(DatabaseContext database, IThumbnailsManager thumbs)
|
||||
: base(database, thumbs)
|
||||
{
|
||||
_database = database;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<ICollection<Studio>> Search(
|
||||
string query,
|
||||
Include<Studio>? 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();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<Studio> Create(Studio obj)
|
||||
{
|
||||
await base.Create(obj);
|
||||
_database.Entry(obj).State = EntityState.Added;
|
||||
await _database.SaveChangesAsync(() => Get(obj.Slug));
|
||||
await IRepository<Studio>.OnResourceCreated(obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task Delete(Studio obj)
|
||||
{
|
||||
_database.Entry(obj).State = EntityState.Deleted;
|
||||
await _database.SaveChangesAsync();
|
||||
await base.Delete(obj);
|
||||
}
|
||||
}
|
||||
|
@ -40,9 +40,8 @@ public class UserRepository(
|
||||
DatabaseContext database,
|
||||
DbConnection db,
|
||||
SqlVariableContext context,
|
||||
IThumbnailsManager thumbs,
|
||||
PermissionOption options
|
||||
) : LocalRepository<User>(database, thumbs), IUserRepository
|
||||
) : GenericRepository<User>(database), IUserRepository
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override async Task<ICollection<User>> Search(
|
||||
@ -50,7 +49,7 @@ public class UserRepository(
|
||||
Include<User>? 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<User> 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<string>();
|
||||
|
||||
await base.Create(obj);
|
||||
database.Entry(obj).State = EntityState.Added;
|
||||
await database.SaveChangesAsync(() => Get(obj.Slug));
|
||||
await IRepository<User>.OnResourceCreated(obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<User?> 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;
|
||||
}
|
||||
}
|
||||
|
@ -135,7 +135,7 @@ public class WatchStatusRepository(
|
||||
{ "_mw", typeof(MovieWatchStatus) },
|
||||
};
|
||||
|
||||
protected IWatchlist Mapper(List<object?> items)
|
||||
protected IWatchlist Mapper(IList<object?> items)
|
||||
{
|
||||
if (items[0] is Show show && show.Id != Guid.Empty)
|
||||
{
|
||||
|
@ -42,8 +42,6 @@ public class ThumbnailsManager(
|
||||
Lazy<IRepository<User>> users
|
||||
) : IThumbnailsManager
|
||||
{
|
||||
private static readonly Dictionary<string, TaskCompletionSource<object>> _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<object>? 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>(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}");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetImagePath<T>(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";
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DeleteImages<T>(T item)
|
||||
where T : IThumbnails
|
||||
{
|
||||
IEnumerable<string> images = new[] { "poster", "thumbnail", "logo" }
|
||||
.SelectMany(x => _GetBaseImagePath(item, x))
|
||||
IEnumerable<string> images = new[] { item.Poster?.Id, item.Thumbnail?.Id, item.Logo?.Id }
|
||||
.Where(x => x is not null)
|
||||
.SelectMany(x => $"/metadata/{x}")
|
||||
.SelectMany(x =>
|
||||
new[]
|
||||
{
|
||||
|
@ -63,5 +63,6 @@ public static class CoreModule
|
||||
);
|
||||
builder.Services.AddScoped<IIssueRepository, IssueRepository>();
|
||||
builder.Services.AddScoped<SqlVariableContext>();
|
||||
builder.Services.AddScoped<MiscRepository>();
|
||||
}
|
||||
}
|
||||
|
@ -16,13 +16,12 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using 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()
|
||||
|
@ -17,9 +17,9 @@
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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<MeiliSync>();
|
||||
app.Services.GetRequiredService<RabbitProducer>();
|
||||
@ -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");
|
||||
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Health"/>.
|
||||
/// </summary>
|
||||
/// <param name="healthCheckService">The service to check health.</param>
|
||||
public Health(HealthCheckService healthCheckService)
|
||||
{
|
||||
_healthCheckService = healthCheckService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the api is ready to accept requests.
|
||||
/// </summary>
|
||||
@ -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")),
|
45
back/src/Kyoo.Core/Views/Admin/Misc.cs
Normal file
45
back/src/Kyoo.Core/Views/Admin/Misc.cs
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Private APIs only used for other services. Can change at any time without notice.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Permission(nameof(Misc), Kind.Read, Group = Group.Admin)]
|
||||
public class Misc(MiscRepository repo) : BaseApi
|
||||
{
|
||||
/// <summary>
|
||||
/// List all registered paths.
|
||||
/// </summary>
|
||||
/// <returns>The list of paths known to Kyoo.</returns>
|
||||
[HttpGet("/paths")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public Task<ICollection<string>> GetAllPaths()
|
||||
{
|
||||
return repo.GetRegisteredPaths();
|
||||
}
|
||||
}
|
67
back/src/Kyoo.Core/Views/Content/ThumbnailsApi.cs
Normal file
67
back/src/Kyoo.Core/Views/Content/ThumbnailsApi.cs
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Retrive images.
|
||||
/// </summary>
|
||||
[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
|
||||
{
|
||||
/// <summary>
|
||||
/// Get Image
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Get an image from it's id. You can select a specefic quality.
|
||||
/// </remarks>
|
||||
/// <param name="id">The ID of the image to retrive.</param>
|
||||
/// <param name="quality">The quality of the image to retrieve.</param>
|
||||
/// <returns>The image asked.</returns>
|
||||
/// <response code="404">
|
||||
/// The image does not exists on kyoo.
|
||||
/// </response>
|
||||
[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);
|
||||
}
|
||||
}
|
@ -170,6 +170,33 @@ public class CrudApi<T> : BaseApi
|
||||
return await Repository.Edit(resource);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Edit
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
/// <param name="identifier">The id or slug of the resource.</param>
|
||||
/// <param name="resource">The resource to edit.</param>
|
||||
/// <returns>The edited resource.</returns>
|
||||
/// <response code="400">The resource in the request body is invalid.</response>
|
||||
/// <response code="404">No item found with the specified ID (or slug).</response>
|
||||
[HttpPut("{identifier:id}")]
|
||||
[PartialPermission(Kind.Write)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<T>> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patch
|
||||
/// </summary>
|
||||
|
@ -16,7 +16,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System.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;
|
||||
|
||||
/// <summary>
|
||||
/// A base class to handle CRUD operations and services thumbnails for
|
||||
/// a specific resource type <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of resource to make CRUD and thumbnails apis for.</typeparam>
|
||||
[ApiController]
|
||||
public class CrudThumbsApi<T>(IRepository<T> repository, IThumbnailsManager thumbs)
|
||||
: CrudApi<T>(repository)
|
||||
public class CrudThumbsApi<T>(IRepository<T> repository) : CrudApi<T>(repository)
|
||||
where T : class, IResource, IThumbnails, IQuery
|
||||
{
|
||||
private async Task<IActionResult> _GetImage(
|
||||
@ -50,18 +44,19 @@ public class CrudThumbsApi<T>(IRepository<T> 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}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -78,7 +73,7 @@ public class CrudThumbsApi<T>(IRepository<T> repository, IThumbnailsManager thum
|
||||
/// </response>
|
||||
[HttpGet("{identifier:id}/poster")]
|
||||
[PartialPermission(Kind.Read)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status302Found)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public Task<IActionResult> GetPoster(Identifier identifier, [FromQuery] ImageQuality? quality)
|
||||
{
|
||||
@ -99,7 +94,7 @@ public class CrudThumbsApi<T>(IRepository<T> repository, IThumbnailsManager thum
|
||||
/// </response>
|
||||
[HttpGet("{identifier:id}/logo")]
|
||||
[PartialPermission(Kind.Read)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status302Found)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public Task<IActionResult> GetLogo(Identifier identifier, [FromQuery] ImageQuality? quality)
|
||||
{
|
||||
@ -120,6 +115,8 @@ public class CrudThumbsApi<T>(IRepository<T> repository, IThumbnailsManager thum
|
||||
/// </response>
|
||||
[HttpGet("{identifier:id}/thumbnail")]
|
||||
[HttpGet("{identifier:id}/backdrop", Order = AlternativeRoute)]
|
||||
[ProducesResponseType(StatusCodes.Status302Found)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public Task<IActionResult> GetBackdrop(Identifier identifier, [FromQuery] ImageQuality? quality)
|
||||
{
|
||||
return _GetImage(identifier, "thumbnail", quality);
|
||||
|
@ -35,8 +35,7 @@ public static class Transcoder
|
||||
Environment.GetEnvironmentVariable("TRANSCODER_URL") ?? "http://transcoder:7666";
|
||||
}
|
||||
|
||||
public abstract class TranscoderApi<T>(IRepository<T> repository, IThumbnailsManager thumbs)
|
||||
: CrudThumbsApi<T>(repository, thumbs)
|
||||
public abstract class TranscoderApi<T>(IRepository<T> repository) : CrudThumbsApi<T>(repository)
|
||||
where T : class, IResource, IThumbnails, IQuery
|
||||
{
|
||||
private Task _Proxy(string route, (string path, string route) info)
|
||||
|
@ -40,25 +40,13 @@ namespace Kyoo.Core.Api;
|
||||
[ApiController]
|
||||
[PartialPermission(nameof(Collection))]
|
||||
[ApiDefinition("Collections", Group = ResourcesGroup)]
|
||||
public class CollectionApi : CrudThumbsApi<Collection>
|
||||
public class CollectionApi(
|
||||
IRepository<Movie> movies,
|
||||
IRepository<Show> shows,
|
||||
CollectionRepository collections,
|
||||
LibraryItemRepository items
|
||||
) : CrudThumbsApi<Collection>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a movie
|
||||
/// </summary>
|
||||
@ -79,14 +67,14 @@ public class CollectionApi : CrudThumbsApi<Collection>
|
||||
public async Task<ActionResult> 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<Collection>
|
||||
public async Task<ActionResult> 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<Collection>
|
||||
{
|
||||
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<ILibraryItem> resources = await _items.GetAllOfCollection(
|
||||
ICollection<ILibraryItem> resources = await items.GetAllOfCollection(
|
||||
collectionId,
|
||||
filter,
|
||||
sortBy == new Sort<ILibraryItem>.Default()
|
||||
@ -165,8 +153,7 @@ public class CollectionApi : CrudThumbsApi<Collection>
|
||||
|
||||
if (
|
||||
!resources.Any()
|
||||
&& await _libraryManager.Collections.GetOrDefault(identifier.IsSame<Collection>())
|
||||
== null
|
||||
&& await collections.GetOrDefault(identifier.IsSame<Collection>()) == null
|
||||
)
|
||||
return NotFound();
|
||||
return Page(resources, pagination.Limit);
|
||||
@ -200,7 +187,7 @@ public class CollectionApi : CrudThumbsApi<Collection>
|
||||
[FromQuery] Include<Show>? fields
|
||||
)
|
||||
{
|
||||
ICollection<Show> resources = await _libraryManager.Shows.GetAll(
|
||||
ICollection<Show> resources = await shows.GetAll(
|
||||
Filter.And(filter, identifier.IsContainedIn<Show, Collection>(x => x.Collections)),
|
||||
sortBy == new Sort<Show>.Default() ? new Sort<Show>.By(x => x.AirDate) : sortBy,
|
||||
fields,
|
||||
@ -209,8 +196,7 @@ public class CollectionApi : CrudThumbsApi<Collection>
|
||||
|
||||
if (
|
||||
!resources.Any()
|
||||
&& await _libraryManager.Collections.GetOrDefault(identifier.IsSame<Collection>())
|
||||
== null
|
||||
&& await collections.GetOrDefault(identifier.IsSame<Collection>()) == null
|
||||
)
|
||||
return NotFound();
|
||||
return Page(resources, pagination.Limit);
|
||||
@ -244,7 +230,7 @@ public class CollectionApi : CrudThumbsApi<Collection>
|
||||
[FromQuery] Include<Movie>? fields
|
||||
)
|
||||
{
|
||||
ICollection<Movie> resources = await _libraryManager.Movies.GetAll(
|
||||
ICollection<Movie> resources = await movies.GetAll(
|
||||
Filter.And(filter, identifier.IsContainedIn<Movie, Collection>(x => x.Collections)),
|
||||
sortBy == new Sort<Movie>.Default() ? new Sort<Movie>.By(x => x.AirDate) : sortBy,
|
||||
fields,
|
||||
@ -253,8 +239,7 @@ public class CollectionApi : CrudThumbsApi<Collection>
|
||||
|
||||
if (
|
||||
!resources.Any()
|
||||
&& await _libraryManager.Collections.GetOrDefault(identifier.IsSame<Collection>())
|
||||
== null
|
||||
&& await collections.GetOrDefault(identifier.IsSame<Collection>()) == null
|
||||
)
|
||||
return NotFound();
|
||||
return Page(resources, pagination.Limit);
|
||||
|
@ -38,8 +38,8 @@ namespace Kyoo.Core.Api;
|
||||
[ApiController]
|
||||
[PartialPermission(nameof(Episode))]
|
||||
[ApiDefinition("Episodes", Group = ResourcesGroup)]
|
||||
public class EpisodeApi(ILibraryManager libraryManager, IThumbnailsManager thumbnails)
|
||||
: TranscoderApi<Episode>(libraryManager.Episodes, thumbnails)
|
||||
public class EpisodeApi(ILibraryManager libraryManager)
|
||||
: TranscoderApi<Episode>(libraryManager.Episodes)
|
||||
{
|
||||
/// <summary>
|
||||
/// Get episode's show
|
||||
|
@ -34,23 +34,5 @@ namespace Kyoo.Core.Api;
|
||||
[ApiController]
|
||||
[PartialPermission("LibraryItem")]
|
||||
[ApiDefinition("Items", Group = ResourcesGroup)]
|
||||
public class LibraryItemApi : CrudThumbsApi<ILibraryItem>
|
||||
{
|
||||
/// <summary>
|
||||
/// The library item repository used to modify or retrieve information in the data store.
|
||||
/// </summary>
|
||||
private readonly IRepository<ILibraryItem> _libraryItems;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="LibraryItemApi"/>.
|
||||
/// </summary>
|
||||
/// <param name="libraryItems">
|
||||
/// The library item repository used to modify or retrieve information in the data store.
|
||||
/// </param>
|
||||
/// <param name="thumbs">Thumbnail manager to retrieve images.</param>
|
||||
public LibraryItemApi(IRepository<ILibraryItem> libraryItems, IThumbnailsManager thumbs)
|
||||
: base(libraryItems, thumbs)
|
||||
{
|
||||
_libraryItems = libraryItems;
|
||||
}
|
||||
}
|
||||
public class LibraryItemApi(IRepository<ILibraryItem> libraryItems)
|
||||
: CrudThumbsApi<ILibraryItem>(libraryItems) { }
|
||||
|
@ -40,8 +40,7 @@ namespace Kyoo.Core.Api;
|
||||
[ApiController]
|
||||
[PartialPermission(nameof(Show))]
|
||||
[ApiDefinition("Shows", Group = ResourcesGroup)]
|
||||
public class MovieApi(ILibraryManager libraryManager, IThumbnailsManager thumbs)
|
||||
: TranscoderApi<Movie>(libraryManager.Movies, thumbs)
|
||||
public class MovieApi(ILibraryManager libraryManager) : TranscoderApi<Movie>(libraryManager.Movies)
|
||||
{
|
||||
/// <summary>
|
||||
/// Get studio that made the show
|
||||
|
@ -33,8 +33,4 @@ namespace Kyoo.Core.Api;
|
||||
[ApiController]
|
||||
[PartialPermission("LibraryItem")]
|
||||
[ApiDefinition("News", Group = ResourcesGroup)]
|
||||
public class NewsApi : CrudThumbsApi<INews>
|
||||
{
|
||||
public NewsApi(IRepository<INews> news, IThumbnailsManager thumbs)
|
||||
: base(news, thumbs) { }
|
||||
}
|
||||
public class NewsApi(IRepository<INews> news) : CrudThumbsApi<INews>(news) { }
|
||||
|
@ -38,26 +38,9 @@ namespace Kyoo.Core.Api;
|
||||
[ApiController]
|
||||
[PartialPermission(nameof(Season))]
|
||||
[ApiDefinition("Seasons", Group = ResourcesGroup)]
|
||||
public class SeasonApi : CrudThumbsApi<Season>
|
||||
public class SeasonApi(ILibraryManager libraryManager)
|
||||
: CrudThumbsApi<Season>(libraryManager.Seasons)
|
||||
{
|
||||
/// <summary>
|
||||
/// The library manager used to modify or retrieve information in the data store.
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="SeasonApi"/>.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">
|
||||
/// The library manager used to modify or retrieve information in the data store.
|
||||
/// </param>
|
||||
/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
|
||||
public SeasonApi(ILibraryManager libraryManager, IThumbnailsManager thumbs)
|
||||
: base(libraryManager.Seasons, thumbs)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get episodes in the season
|
||||
/// </summary>
|
||||
@ -86,7 +69,7 @@ public class SeasonApi : CrudThumbsApi<Season>
|
||||
[FromQuery] Include<Episode> fields
|
||||
)
|
||||
{
|
||||
ICollection<Episode> resources = await _libraryManager.Episodes.GetAll(
|
||||
ICollection<Episode> resources = await libraryManager.Episodes.GetAll(
|
||||
Filter.And(filter, identifier.Matcher<Episode>(x => x.SeasonId, x => x.Season!.Slug)),
|
||||
sortBy,
|
||||
fields,
|
||||
@ -95,7 +78,7 @@ public class SeasonApi : CrudThumbsApi<Season>
|
||||
|
||||
if (
|
||||
!resources.Any()
|
||||
&& await _libraryManager.Seasons.GetOrDefault(identifier.IsSame<Season>()) == null
|
||||
&& await libraryManager.Seasons.GetOrDefault(identifier.IsSame<Season>()) == null
|
||||
)
|
||||
return NotFound();
|
||||
return Page(resources, pagination.Limit);
|
||||
@ -120,7 +103,7 @@ public class SeasonApi : CrudThumbsApi<Season>
|
||||
[FromQuery] Include<Show> fields
|
||||
)
|
||||
{
|
||||
Show? ret = await _libraryManager.Shows.GetOrDefault(
|
||||
Show? ret = await libraryManager.Shows.GetOrDefault(
|
||||
identifier.IsContainedIn<Show, Season>(x => x.Seasons!),
|
||||
fields
|
||||
);
|
||||
|
@ -40,26 +40,8 @@ namespace Kyoo.Core.Api;
|
||||
[ApiController]
|
||||
[PartialPermission(nameof(Show))]
|
||||
[ApiDefinition("Shows", Group = ResourcesGroup)]
|
||||
public class ShowApi : CrudThumbsApi<Show>
|
||||
public class ShowApi(ILibraryManager libraryManager) : CrudThumbsApi<Show>(libraryManager.Shows)
|
||||
{
|
||||
/// <summary>
|
||||
/// The library manager used to modify or retrieve information in the data store.
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="ShowApi"/>.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">
|
||||
/// The library manager used to modify or retrieve information about the data store.
|
||||
/// </param>
|
||||
/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
|
||||
public ShowApi(ILibraryManager libraryManager, IThumbnailsManager thumbs)
|
||||
: base(libraryManager.Shows, thumbs)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get seasons of this show
|
||||
/// </summary>
|
||||
@ -88,7 +70,7 @@ public class ShowApi : CrudThumbsApi<Show>
|
||||
[FromQuery] Include<Season> fields
|
||||
)
|
||||
{
|
||||
ICollection<Season> resources = await _libraryManager.Seasons.GetAll(
|
||||
ICollection<Season> resources = await libraryManager.Seasons.GetAll(
|
||||
Filter.And(filter, identifier.Matcher<Season>(x => x.ShowId, x => x.Show!.Slug)),
|
||||
sortBy,
|
||||
fields,
|
||||
@ -97,7 +79,7 @@ public class ShowApi : CrudThumbsApi<Show>
|
||||
|
||||
if (
|
||||
!resources.Any()
|
||||
&& await _libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null
|
||||
&& await libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null
|
||||
)
|
||||
return NotFound();
|
||||
return Page(resources, pagination.Limit);
|
||||
@ -131,7 +113,7 @@ public class ShowApi : CrudThumbsApi<Show>
|
||||
[FromQuery] Include<Episode> fields
|
||||
)
|
||||
{
|
||||
ICollection<Episode> resources = await _libraryManager.Episodes.GetAll(
|
||||
ICollection<Episode> resources = await libraryManager.Episodes.GetAll(
|
||||
Filter.And(filter, identifier.Matcher<Episode>(x => x.ShowId, x => x.Show!.Slug)),
|
||||
sortBy,
|
||||
fields,
|
||||
@ -140,7 +122,7 @@ public class ShowApi : CrudThumbsApi<Show>
|
||||
|
||||
if (
|
||||
!resources.Any()
|
||||
&& await _libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null
|
||||
&& await libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null
|
||||
)
|
||||
return NotFound();
|
||||
return Page(resources, pagination.Limit);
|
||||
@ -165,7 +147,7 @@ public class ShowApi : CrudThumbsApi<Show>
|
||||
[FromQuery] Include<Studio> fields
|
||||
)
|
||||
{
|
||||
return await _libraryManager.Studios.Get(
|
||||
return await libraryManager.Studios.Get(
|
||||
identifier.IsContainedIn<Studio, Show>(x => x.Shows!),
|
||||
fields
|
||||
);
|
||||
@ -199,7 +181,7 @@ public class ShowApi : CrudThumbsApi<Show>
|
||||
[FromQuery] Include<Collection> fields
|
||||
)
|
||||
{
|
||||
ICollection<Collection> resources = await _libraryManager.Collections.GetAll(
|
||||
ICollection<Collection> resources = await libraryManager.Collections.GetAll(
|
||||
Filter.And(filter, identifier.IsContainedIn<Collection, Show>(x => x.Shows!)),
|
||||
sortBy,
|
||||
fields,
|
||||
@ -208,7 +190,7 @@ public class ShowApi : CrudThumbsApi<Show>
|
||||
|
||||
if (
|
||||
!resources.Any()
|
||||
&& await _libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null
|
||||
&& await libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null
|
||||
)
|
||||
return NotFound();
|
||||
return Page(resources, pagination.Limit);
|
||||
@ -233,9 +215,9 @@ public class ShowApi : CrudThumbsApi<Show>
|
||||
{
|
||||
Guid id = await identifier.Match(
|
||||
id => Task.FromResult(id),
|
||||
async slug => (await _libraryManager.Shows.Get(slug)).Id
|
||||
async slug => (await libraryManager.Shows.Get(slug)).Id
|
||||
);
|
||||
return await _libraryManager.WatchStatus.GetShowStatus(id, User.GetIdOrThrow());
|
||||
return await libraryManager.WatchStatus.GetShowStatus(id, User.GetIdOrThrow());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -260,9 +242,9 @@ public class ShowApi : CrudThumbsApi<Show>
|
||||
{
|
||||
Guid id = await identifier.Match(
|
||||
id => Task.FromResult(id),
|
||||
async slug => (await _libraryManager.Shows.Get(slug)).Id
|
||||
async slug => (await libraryManager.Shows.Get(slug)).Id
|
||||
);
|
||||
return await _libraryManager.WatchStatus.SetShowStatus(id, User.GetIdOrThrow(), status);
|
||||
return await libraryManager.WatchStatus.SetShowStatus(id, User.GetIdOrThrow(), status);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -283,8 +265,8 @@ public class ShowApi : CrudThumbsApi<Show>
|
||||
{
|
||||
Guid id = await identifier.Match(
|
||||
id => Task.FromResult(id),
|
||||
async slug => (await _libraryManager.Shows.Get(slug)).Id
|
||||
async slug => (await libraryManager.Shows.Get(slug)).Id
|
||||
);
|
||||
await _libraryManager.WatchStatus.DeleteShowStatus(id, User.GetIdOrThrow());
|
||||
await libraryManager.WatchStatus.DeleteShowStatus(id, User.GetIdOrThrow());
|
||||
}
|
||||
}
|
||||
|
@ -16,9 +16,6 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.Abstractions.Controllers;
|
||||
using Kyoo.Abstractions.Models;
|
||||
using Meilisearch;
|
||||
|
@ -23,7 +23,6 @@ using System.Linq.Expressions;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.Abstractions.Controllers;
|
||||
using Kyoo.Abstractions.Models;
|
||||
using Kyoo.Abstractions.Models.Exceptions;
|
||||
using Kyoo.Authentication;
|
||||
@ -33,13 +32,6 @@ using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
|
||||
namespace Kyoo.Postgresql;
|
||||
|
||||
/// <summary>
|
||||
/// The database handle used for all local repositories.
|
||||
/// This is an abstract class. It is meant to be implemented by plugins. This allow the core to be database agnostic.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It should not be used directly, to access the database use a <see cref="ILibraryManager"/> or repositories.
|
||||
/// </remarks>
|
||||
public abstract class DatabaseContext : DbContext
|
||||
{
|
||||
private readonly IHttpContextAccessor _accessor;
|
||||
@ -53,39 +45,18 @@ public abstract class DatabaseContext : DbContext
|
||||
|
||||
public Guid? CurrentUserId => _accessor.HttpContext?.User.GetId();
|
||||
|
||||
/// <summary>
|
||||
/// All collections of Kyoo. See <see cref="Collection"/>.
|
||||
/// </summary>
|
||||
public DbSet<Collection> Collections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// All movies of Kyoo. See <see cref="Movie"/>.
|
||||
/// </summary>
|
||||
public DbSet<Movie> Movies { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// All shows of Kyoo. See <see cref="Show"/>.
|
||||
/// </summary>
|
||||
public DbSet<Show> Shows { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// All seasons of Kyoo. See <see cref="Season"/>.
|
||||
/// </summary>
|
||||
public DbSet<Season> Seasons { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// All episodes of Kyoo. See <see cref="Episode"/>.
|
||||
/// </summary>
|
||||
public DbSet<Episode> Episodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// All studios of Kyoo. See <see cref="Studio"/>.
|
||||
/// </summary>
|
||||
public DbSet<Studio> Studios { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of registered users.
|
||||
/// </summary>
|
||||
public DbSet<User> Users { get; set; }
|
||||
|
||||
public DbSet<MovieWatchStatus> MovieWatchStatus { get; set; }
|
||||
@ -129,28 +100,13 @@ public abstract class DatabaseContext : DbContext
|
||||
_accessor = accessor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the name of the link table of the two given types.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The owner type of the relation</typeparam>
|
||||
/// <typeparam name="T2">The child type of the relation</typeparam>
|
||||
/// <returns>The name of the table containing the links.</returns>
|
||||
protected abstract string LinkName<T, T2>()
|
||||
where T : IResource
|
||||
where T2 : IResource;
|
||||
|
||||
/// <summary>
|
||||
/// Get the name of a link's foreign key.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type that will be accessible via the navigation</typeparam>
|
||||
/// <returns>The name of the foreign key for the given resource.</returns>
|
||||
protected abstract string LinkNameFk<T>()
|
||||
where T : IResource;
|
||||
|
||||
/// <summary>
|
||||
/// Set basic configurations (like preventing query tracking)
|
||||
/// </summary>
|
||||
/// <param name="optionsBuilder">An option builder to fill.</param>
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
@ -201,9 +157,9 @@ public abstract class DatabaseContext : DbContext
|
||||
private static void _HasImages<T>(ModelBuilder modelBuilder)
|
||||
where T : class, IThumbnails
|
||||
{
|
||||
modelBuilder.Entity<T>().OwnsOne(x => x.Poster);
|
||||
modelBuilder.Entity<T>().OwnsOne(x => x.Thumbnail);
|
||||
modelBuilder.Entity<T>().OwnsOne(x => x.Logo);
|
||||
modelBuilder.Entity<T>().OwnsOne(x => x.Poster, x => x.ToJson());
|
||||
modelBuilder.Entity<T>().OwnsOne(x => x.Thumbnail, x => x.ToJson());
|
||||
modelBuilder.Entity<T>().OwnsOne(x => x.Logo, x => x.ToJson());
|
||||
}
|
||||
|
||||
private static void _HasAddedDate<T>(ModelBuilder modelBuilder)
|
||||
@ -227,15 +183,6 @@ public abstract class DatabaseContext : DbContext
|
||||
.ValueGeneratedOnAdd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a many to many relationship between the two entities.
|
||||
/// The resulting relationship will have an available <see cref="AddLinks{T1,T2}"/> method.
|
||||
/// </summary>
|
||||
/// <param name="modelBuilder">The database model builder</param>
|
||||
/// <param name="firstNavigation">The first navigation expression from T to T2</param>
|
||||
/// <param name="secondNavigation">The second navigation expression from T2 to T</param>
|
||||
/// <typeparam name="T">The owning type of the relationship</typeparam>
|
||||
/// <typeparam name="T2">The owned type of the relationship</typeparam>
|
||||
private void _HasManyToMany<T, T2>(
|
||||
ModelBuilder modelBuilder,
|
||||
Expression<Func<T, IEnumerable<T2>?>> firstNavigation,
|
||||
@ -263,10 +210,6 @@ public abstract class DatabaseContext : DbContext
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set database parameters to support every types of Kyoo.
|
||||
/// </summary>
|
||||
/// <param name="modelBuilder">The database's model builder.</param>
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
@ -412,28 +355,6 @@ public abstract class DatabaseContext : DbContext
|
||||
_HasJson<Issue, object>(modelBuilder, x => x.Extra);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a new or an in cache temporary object wih the same ID as the one given
|
||||
/// </summary>
|
||||
/// <param name="model">If a resource with the same ID is found in the database, it will be used.
|
||||
/// <paramref name="model"/> will be used otherwise</param>
|
||||
/// <typeparam name="T">The type of the resource</typeparam>
|
||||
/// <returns>A resource that is now tracked by this context.</returns>
|
||||
public T GetTemporaryObject<T>(T model)
|
||||
where T : class, IResource
|
||||
{
|
||||
T? tmp = Set<T>().Local.FirstOrDefault(x => x.Id == model.Id);
|
||||
if (tmp != null)
|
||||
return tmp;
|
||||
Entry(model).State = EntityState.Unchanged;
|
||||
return model;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save changes that are applied to this context.
|
||||
/// </summary>
|
||||
/// <exception cref="DuplicatedItemException">A duplicated item has been found.</exception>
|
||||
/// <returns>The number of state entries written to the database.</returns>
|
||||
public override int SaveChanges()
|
||||
{
|
||||
try
|
||||
@ -449,13 +370,6 @@ public abstract class DatabaseContext : DbContext
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save changes that are applied to this context.
|
||||
/// </summary>
|
||||
/// <param name="acceptAllChangesOnSuccess">Indicates whether AcceptAllChanges() is called after the changes
|
||||
/// have been sent successfully to the database.</param>
|
||||
/// <exception cref="DuplicatedItemException">A duplicated item has been found.</exception>
|
||||
/// <returns>The number of state entries written to the database.</returns>
|
||||
public override int SaveChanges(bool acceptAllChangesOnSuccess)
|
||||
{
|
||||
try
|
||||
@ -471,14 +385,6 @@ public abstract class DatabaseContext : DbContext
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save changes that are applied to this context.
|
||||
/// </summary>
|
||||
/// <param name="acceptAllChangesOnSuccess">Indicates whether AcceptAllChanges() is called after the changes
|
||||
/// have been sent successfully to the database.</param>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete</param>
|
||||
/// <exception cref="DuplicatedItemException">A duplicated item has been found.</exception>
|
||||
/// <returns>The number of state entries written to the database.</returns>
|
||||
public override async Task<int> SaveChangesAsync(
|
||||
bool acceptAllChangesOnSuccess,
|
||||
CancellationToken cancellationToken = default
|
||||
@ -497,12 +403,6 @@ public abstract class DatabaseContext : DbContext
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save changes that are applied to this context.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete</param>
|
||||
/// <exception cref="DuplicatedItemException">A duplicated item has been found.</exception>
|
||||
/// <returns>The number of state entries written to the database.</returns>
|
||||
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
@ -518,14 +418,6 @@ public abstract class DatabaseContext : DbContext
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save changes that are applied to this context.
|
||||
/// </summary>
|
||||
/// <param name="getExisting">How to retrieve the conflicting item.</param>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete</param>
|
||||
/// <exception cref="DuplicatedItemException">A duplicated item has been found.</exception>
|
||||
/// <typeparam name="T">The type of the potential duplicate (this is unused).</typeparam>
|
||||
/// <returns>The number of state entries written to the database.</returns>
|
||||
public async Task<int> SaveChangesAsync<T>(
|
||||
Func<Task<T>> getExisting,
|
||||
CancellationToken cancellationToken = default
|
||||
@ -548,12 +440,6 @@ public abstract class DatabaseContext : DbContext
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save changes if no duplicates are found. If one is found, no change are saved but the current changes are no discarded.
|
||||
/// The current context will still hold those invalid changes.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete</param>
|
||||
/// <returns>The number of state entries written to the database or -1 if a duplicate exist.</returns>
|
||||
public async Task<int> SaveIfNoDuplicates(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
@ -566,30 +452,14 @@ public abstract class DatabaseContext : DbContext
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the first resource with the given slug that is currently tracked by this context.
|
||||
/// This allow one to limit redundant calls to <see cref="IRepository{T}.CreateIfNotExists"/> during the
|
||||
/// same transaction and prevent fails from EF when two same entities are being tracked.
|
||||
/// </summary>
|
||||
/// <param name="slug">The slug of the resource to check</param>
|
||||
/// <typeparam name="T">The type of entity to check</typeparam>
|
||||
/// <returns>The local entity representing the resource with the given slug if it exists or null.</returns>
|
||||
public T? LocalEntity<T>(string slug)
|
||||
where T : class, IResource
|
||||
{
|
||||
return ChangeTracker.Entries<T>().FirstOrDefault(x => x.Entity.Slug == slug)?.Entity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the exception is a duplicated exception.
|
||||
/// </summary>
|
||||
/// <param name="ex">The exception to check</param>
|
||||
/// <returns>True if the exception is a duplicate exception. False otherwise</returns>
|
||||
protected abstract bool IsDuplicateException(Exception ex);
|
||||
|
||||
/// <summary>
|
||||
/// Delete every changes that are on this context.
|
||||
/// </summary>
|
||||
public void DiscardChanges()
|
||||
{
|
||||
foreach (
|
||||
|
1375
back/src/Kyoo.Postgresql/Migrations/20240420124608_ReworkImages.Designer.cs
generated
Normal file
1375
back/src/Kyoo.Postgresql/Migrations/20240420124608_ReworkImages.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,464 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Kyoo.Postgresql.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ReworkImages : Migration
|
||||
{
|
||||
private void MigrateImage(MigrationBuilder migrationBuilder, string table, string type)
|
||||
{
|
||||
migrationBuilder.Sql(
|
||||
$"""
|
||||
update {table} as r set {type} = json_build_object(
|
||||
'Id', gen_random_uuid(),
|
||||
'Source', r.{type}_source,
|
||||
'Blurhash', r.{type}_blurhash
|
||||
)
|
||||
where r.{type}_source is not null
|
||||
"""
|
||||
);
|
||||
}
|
||||
|
||||
private void UnMigrateImage(MigrationBuilder migrationBuilder, string table, string type)
|
||||
{
|
||||
migrationBuilder.Sql(
|
||||
$"""
|
||||
update {table} as r
|
||||
set {type}_source = r.{type}->>'Source',
|
||||
{type}_blurhash = r.{type}->>'Blurhash'
|
||||
"""
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "logo",
|
||||
table: "shows",
|
||||
type: "jsonb",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "poster",
|
||||
table: "shows",
|
||||
type: "jsonb",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "thumbnail",
|
||||
table: "shows",
|
||||
type: "jsonb",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "logo",
|
||||
table: "seasons",
|
||||
type: "jsonb",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "poster",
|
||||
table: "seasons",
|
||||
type: "jsonb",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "thumbnail",
|
||||
table: "seasons",
|
||||
type: "jsonb",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "logo",
|
||||
table: "movies",
|
||||
type: "jsonb",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "poster",
|
||||
table: "movies",
|
||||
type: "jsonb",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "thumbnail",
|
||||
table: "movies",
|
||||
type: "jsonb",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "logo",
|
||||
table: "episodes",
|
||||
type: "jsonb",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "poster",
|
||||
table: "episodes",
|
||||
type: "jsonb",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "thumbnail",
|
||||
table: "episodes",
|
||||
type: "jsonb",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "logo",
|
||||
table: "collections",
|
||||
type: "jsonb",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "poster",
|
||||
table: "collections",
|
||||
type: "jsonb",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "thumbnail",
|
||||
table: "collections",
|
||||
type: "jsonb",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
MigrateImage(migrationBuilder, "shows", "logo");
|
||||
MigrateImage(migrationBuilder, "shows", "poster");
|
||||
MigrateImage(migrationBuilder, "shows", "thumbnail");
|
||||
|
||||
MigrateImage(migrationBuilder, "seasons", "logo");
|
||||
MigrateImage(migrationBuilder, "seasons", "poster");
|
||||
MigrateImage(migrationBuilder, "seasons", "thumbnail");
|
||||
|
||||
MigrateImage(migrationBuilder, "movies", "logo");
|
||||
MigrateImage(migrationBuilder, "movies", "poster");
|
||||
MigrateImage(migrationBuilder, "movies", "thumbnail");
|
||||
|
||||
MigrateImage(migrationBuilder, "episodes", "logo");
|
||||
MigrateImage(migrationBuilder, "episodes", "poster");
|
||||
MigrateImage(migrationBuilder, "episodes", "thumbnail");
|
||||
|
||||
MigrateImage(migrationBuilder, "collections", "logo");
|
||||
MigrateImage(migrationBuilder, "collections", "poster");
|
||||
MigrateImage(migrationBuilder, "collections", "thumbnail");
|
||||
|
||||
migrationBuilder.DropColumn(name: "logo_blurhash", table: "shows");
|
||||
migrationBuilder.DropColumn(name: "logo_source", table: "shows");
|
||||
migrationBuilder.DropColumn(name: "poster_blurhash", table: "shows");
|
||||
migrationBuilder.DropColumn(name: "poster_source", table: "shows");
|
||||
migrationBuilder.DropColumn(name: "thumbnail_blurhash", table: "shows");
|
||||
migrationBuilder.DropColumn(name: "thumbnail_source", table: "shows");
|
||||
|
||||
migrationBuilder.DropColumn(name: "logo_blurhash", table: "seasons");
|
||||
migrationBuilder.DropColumn(name: "logo_source", table: "seasons");
|
||||
migrationBuilder.DropColumn(name: "poster_blurhash", table: "seasons");
|
||||
migrationBuilder.DropColumn(name: "poster_source", table: "seasons");
|
||||
migrationBuilder.DropColumn(name: "thumbnail_blurhash", table: "seasons");
|
||||
migrationBuilder.DropColumn(name: "thumbnail_source", table: "seasons");
|
||||
|
||||
migrationBuilder.DropColumn(name: "logo_blurhash", table: "movies");
|
||||
migrationBuilder.DropColumn(name: "logo_source", table: "movies");
|
||||
migrationBuilder.DropColumn(name: "poster_blurhash", table: "movies");
|
||||
migrationBuilder.DropColumn(name: "poster_source", table: "movies");
|
||||
migrationBuilder.DropColumn(name: "thumbnail_blurhash", table: "movies");
|
||||
migrationBuilder.DropColumn(name: "thumbnail_source", table: "movies");
|
||||
|
||||
migrationBuilder.DropColumn(name: "logo_blurhash", table: "episodes");
|
||||
migrationBuilder.DropColumn(name: "logo_source", table: "episodes");
|
||||
migrationBuilder.DropColumn(name: "poster_blurhash", table: "episodes");
|
||||
migrationBuilder.DropColumn(name: "poster_source", table: "episodes");
|
||||
migrationBuilder.DropColumn(name: "thumbnail_blurhash", table: "episodes");
|
||||
migrationBuilder.DropColumn(name: "thumbnail_source", table: "episodes");
|
||||
|
||||
migrationBuilder.DropColumn(name: "logo_blurhash", table: "collections");
|
||||
migrationBuilder.DropColumn(name: "logo_source", table: "collections");
|
||||
migrationBuilder.DropColumn(name: "poster_blurhash", table: "collections");
|
||||
migrationBuilder.DropColumn(name: "poster_source", table: "collections");
|
||||
migrationBuilder.DropColumn(name: "thumbnail_blurhash", table: "collections");
|
||||
migrationBuilder.DropColumn(name: "thumbnail_source", table: "collections");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "logo_blurhash",
|
||||
table: "shows",
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "logo_source",
|
||||
table: "shows",
|
||||
type: "text",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "poster_blurhash",
|
||||
table: "shows",
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "poster_source",
|
||||
table: "shows",
|
||||
type: "text",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "thumbnail_blurhash",
|
||||
table: "shows",
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "thumbnail_source",
|
||||
table: "shows",
|
||||
type: "text",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "logo_blurhash",
|
||||
table: "seasons",
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "logo_source",
|
||||
table: "seasons",
|
||||
type: "text",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "poster_blurhash",
|
||||
table: "seasons",
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "poster_source",
|
||||
table: "seasons",
|
||||
type: "text",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "thumbnail_blurhash",
|
||||
table: "seasons",
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "thumbnail_source",
|
||||
table: "seasons",
|
||||
type: "text",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "logo_blurhash",
|
||||
table: "movies",
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "logo_source",
|
||||
table: "movies",
|
||||
type: "text",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "poster_blurhash",
|
||||
table: "movies",
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "poster_source",
|
||||
table: "movies",
|
||||
type: "text",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "thumbnail_blurhash",
|
||||
table: "movies",
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "thumbnail_source",
|
||||
table: "movies",
|
||||
type: "text",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "logo_blurhash",
|
||||
table: "episodes",
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "logo_source",
|
||||
table: "episodes",
|
||||
type: "text",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "poster_blurhash",
|
||||
table: "episodes",
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "poster_source",
|
||||
table: "episodes",
|
||||
type: "text",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "thumbnail_blurhash",
|
||||
table: "episodes",
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "thumbnail_source",
|
||||
table: "episodes",
|
||||
type: "text",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "logo_blurhash",
|
||||
table: "collections",
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "logo_source",
|
||||
table: "collections",
|
||||
type: "text",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "poster_blurhash",
|
||||
table: "collections",
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "poster_source",
|
||||
table: "collections",
|
||||
type: "text",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "thumbnail_blurhash",
|
||||
table: "collections",
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "thumbnail_source",
|
||||
table: "collections",
|
||||
type: "text",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
UnMigrateImage(migrationBuilder, "shows", "logo");
|
||||
UnMigrateImage(migrationBuilder, "shows", "poster");
|
||||
UnMigrateImage(migrationBuilder, "shows", "thumbnail");
|
||||
|
||||
UnMigrateImage(migrationBuilder, "seasons", "logo");
|
||||
UnMigrateImage(migrationBuilder, "seasons", "poster");
|
||||
UnMigrateImage(migrationBuilder, "seasons", "thumbnail");
|
||||
|
||||
UnMigrateImage(migrationBuilder, "movies", "logo");
|
||||
UnMigrateImage(migrationBuilder, "movies", "poster");
|
||||
UnMigrateImage(migrationBuilder, "movies", "thumbnail");
|
||||
|
||||
UnMigrateImage(migrationBuilder, "episodes", "logo");
|
||||
UnMigrateImage(migrationBuilder, "episodes", "poster");
|
||||
UnMigrateImage(migrationBuilder, "episodes", "thumbnail");
|
||||
|
||||
UnMigrateImage(migrationBuilder, "collections", "logo");
|
||||
UnMigrateImage(migrationBuilder, "collections", "poster");
|
||||
UnMigrateImage(migrationBuilder, "collections", "thumbnail");
|
||||
|
||||
migrationBuilder.DropColumn(name: "logo", table: "shows");
|
||||
migrationBuilder.DropColumn(name: "poster", table: "shows");
|
||||
migrationBuilder.DropColumn(name: "thumbnail", table: "shows");
|
||||
migrationBuilder.DropColumn(name: "logo", table: "seasons");
|
||||
migrationBuilder.DropColumn(name: "poster", table: "seasons");
|
||||
migrationBuilder.DropColumn(name: "thumbnail", table: "seasons");
|
||||
migrationBuilder.DropColumn(name: "logo", table: "movies");
|
||||
migrationBuilder.DropColumn(name: "poster", table: "movies");
|
||||
migrationBuilder.DropColumn(name: "thumbnail", table: "movies");
|
||||
migrationBuilder.DropColumn(name: "logo", table: "episodes");
|
||||
migrationBuilder.DropColumn(name: "poster", table: "episodes");
|
||||
migrationBuilder.DropColumn(name: "thumbnail", table: "episodes");
|
||||
migrationBuilder.DropColumn(name: "logo", table: "collections");
|
||||
migrationBuilder.DropColumn(name: "poster", table: "collections");
|
||||
migrationBuilder.DropColumn(name: "thumbnail", table: "collections");
|
||||
}
|
||||
}
|
||||
}
|
@ -731,24 +731,26 @@ namespace Kyoo.Postgresql.Migrations
|
||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("CollectionId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Blurhash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("logo_blurhash");
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b1.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("logo_source");
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.HasKey("CollectionId");
|
||||
|
||||
b1.ToTable("collections");
|
||||
|
||||
b1.ToJson("logo");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("CollectionId")
|
||||
.HasConstraintName("fk_collections_collections_id");
|
||||
@ -757,50 +759,55 @@ namespace Kyoo.Postgresql.Migrations
|
||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("CollectionId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Blurhash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("poster_blurhash");
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b1.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("poster_source");
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.HasKey("CollectionId");
|
||||
b1.HasKey("CollectionId")
|
||||
.HasName("pk_collections");
|
||||
|
||||
b1.ToTable("collections");
|
||||
|
||||
b1.ToJson("poster");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("CollectionId")
|
||||
.HasConstraintName("fk_collections_collections_id");
|
||||
.HasConstraintName("fk_collections_collections_collection_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("CollectionId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Blurhash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("thumbnail_blurhash");
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b1.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("thumbnail_source");
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.HasKey("CollectionId");
|
||||
|
||||
b1.ToTable("collections");
|
||||
|
||||
b1.ToJson("thumbnail");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("CollectionId")
|
||||
.HasConstraintName("fk_collections_collections_id");
|
||||
@ -831,24 +838,26 @@ namespace Kyoo.Postgresql.Migrations
|
||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("EpisodeId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Blurhash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("logo_blurhash");
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b1.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("logo_source");
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.HasKey("EpisodeId");
|
||||
|
||||
b1.ToTable("episodes");
|
||||
|
||||
b1.ToJson("logo");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("EpisodeId")
|
||||
.HasConstraintName("fk_episodes_episodes_id");
|
||||
@ -857,24 +866,26 @@ namespace Kyoo.Postgresql.Migrations
|
||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("EpisodeId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Blurhash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("poster_blurhash");
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b1.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("poster_source");
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.HasKey("EpisodeId");
|
||||
|
||||
b1.ToTable("episodes");
|
||||
|
||||
b1.ToJson("poster");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("EpisodeId")
|
||||
.HasConstraintName("fk_episodes_episodes_id");
|
||||
@ -883,24 +894,26 @@ namespace Kyoo.Postgresql.Migrations
|
||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("EpisodeId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Blurhash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("thumbnail_blurhash");
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b1.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("thumbnail_source");
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.HasKey("EpisodeId");
|
||||
|
||||
b1.ToTable("episodes");
|
||||
|
||||
b1.ToJson("thumbnail");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("EpisodeId")
|
||||
.HasConstraintName("fk_episodes_episodes_id");
|
||||
@ -949,24 +962,26 @@ namespace Kyoo.Postgresql.Migrations
|
||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("MovieId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Blurhash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("logo_blurhash");
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b1.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("logo_source");
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.HasKey("MovieId");
|
||||
|
||||
b1.ToTable("movies");
|
||||
|
||||
b1.ToJson("logo");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MovieId")
|
||||
.HasConstraintName("fk_movies_movies_id");
|
||||
@ -975,24 +990,26 @@ namespace Kyoo.Postgresql.Migrations
|
||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("MovieId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Blurhash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("poster_blurhash");
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b1.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("poster_source");
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.HasKey("MovieId");
|
||||
|
||||
b1.ToTable("movies");
|
||||
|
||||
b1.ToJson("poster");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MovieId")
|
||||
.HasConstraintName("fk_movies_movies_id");
|
||||
@ -1001,24 +1018,26 @@ namespace Kyoo.Postgresql.Migrations
|
||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("MovieId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Blurhash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("thumbnail_blurhash");
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b1.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("thumbnail_source");
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.HasKey("MovieId");
|
||||
|
||||
b1.ToTable("movies");
|
||||
|
||||
b1.ToJson("thumbnail");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MovieId")
|
||||
.HasConstraintName("fk_movies_movies_id");
|
||||
@ -1066,24 +1085,26 @@ namespace Kyoo.Postgresql.Migrations
|
||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("SeasonId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Blurhash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("logo_blurhash");
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b1.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("logo_source");
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.HasKey("SeasonId");
|
||||
|
||||
b1.ToTable("seasons");
|
||||
|
||||
b1.ToJson("logo");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("SeasonId")
|
||||
.HasConstraintName("fk_seasons_seasons_id");
|
||||
@ -1092,24 +1113,26 @@ namespace Kyoo.Postgresql.Migrations
|
||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("SeasonId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Blurhash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("poster_blurhash");
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b1.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("poster_source");
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.HasKey("SeasonId");
|
||||
|
||||
b1.ToTable("seasons");
|
||||
|
||||
b1.ToJson("poster");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("SeasonId")
|
||||
.HasConstraintName("fk_seasons_seasons_id");
|
||||
@ -1118,24 +1141,26 @@ namespace Kyoo.Postgresql.Migrations
|
||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("SeasonId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Blurhash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("thumbnail_blurhash");
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b1.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("thumbnail_source");
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.HasKey("SeasonId");
|
||||
|
||||
b1.ToTable("seasons");
|
||||
|
||||
b1.ToJson("thumbnail");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("SeasonId")
|
||||
.HasConstraintName("fk_seasons_seasons_id");
|
||||
@ -1161,24 +1186,26 @@ namespace Kyoo.Postgresql.Migrations
|
||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("ShowId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Blurhash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("logo_blurhash");
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b1.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("logo_source");
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.HasKey("ShowId");
|
||||
|
||||
b1.ToTable("shows");
|
||||
|
||||
b1.ToJson("logo");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ShowId")
|
||||
.HasConstraintName("fk_shows_shows_id");
|
||||
@ -1187,24 +1214,26 @@ namespace Kyoo.Postgresql.Migrations
|
||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("ShowId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Blurhash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("poster_blurhash");
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b1.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("poster_source");
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.HasKey("ShowId");
|
||||
|
||||
b1.ToTable("shows");
|
||||
|
||||
b1.ToJson("poster");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ShowId")
|
||||
.HasConstraintName("fk_shows_shows_id");
|
||||
@ -1213,24 +1242,26 @@ namespace Kyoo.Postgresql.Migrations
|
||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("ShowId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Blurhash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("thumbnail_blurhash");
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b1.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("thumbnail_source");
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.HasKey("ShowId");
|
||||
|
||||
b1.ToTable("shows");
|
||||
|
||||
b1.ToJson("thumbnail");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ShowId")
|
||||
.HasConstraintName("fk_shows_shows_id");
|
||||
|
@ -94,6 +94,7 @@ public class PostgresContext(DbContextOptions options, IHttpContextAccessor acce
|
||||
typeof(Dictionary<string, ExternalToken>),
|
||||
new JsonTypeHandler<Dictionary<string, ExternalToken>>()
|
||||
);
|
||||
SqlMapper.AddTypeHandler(typeof(Image), new JsonTypeHandler<Image>());
|
||||
SqlMapper.AddTypeHandler(typeof(List<string>), new ListTypeHandler<string>());
|
||||
SqlMapper.AddTypeHandler(typeof(List<Genre>), new ListTypeHandler<Genre>());
|
||||
SqlMapper.AddTypeHandler(typeof(Wrapper), new Wrapper.Handler());
|
||||
|
@ -35,7 +35,7 @@ public static class RabbitMqModule
|
||||
UserName = builder.Configuration.GetValue("RABBITMQ_DEFAULT_USER", "guest"),
|
||||
Password = builder.Configuration.GetValue("RABBITMQ_DEFAULT_PASS", "guest"),
|
||||
HostName = builder.Configuration.GetValue("RABBITMQ_HOST", "rabbitmq"),
|
||||
Port = 5672,
|
||||
Port = builder.Configuration.GetValue("RABBITMQ_Port", 5672),
|
||||
};
|
||||
|
||||
return factory.CreateConnection();
|
||||
|
@ -30,7 +30,7 @@ services:
|
||||
migrations:
|
||||
condition: service_completed_successfully
|
||||
volumes:
|
||||
- kyoo:/kyoo
|
||||
- kyoo:/metadata
|
||||
|
||||
migrations:
|
||||
build:
|
||||
|
@ -42,7 +42,7 @@ services:
|
||||
volumes:
|
||||
- ./back:/app
|
||||
- /app/out/
|
||||
- kyoo:/kyoo
|
||||
- kyoo:/metadata
|
||||
|
||||
migrations:
|
||||
build:
|
||||
|
@ -31,7 +31,7 @@ services:
|
||||
migrations:
|
||||
condition: service_completed_successfully
|
||||
volumes:
|
||||
- kyoo:/kyoo
|
||||
- kyoo:/metadata
|
||||
|
||||
migrations:
|
||||
image: zoriya/kyoo_migrations:latest
|
||||
|
@ -19,10 +19,11 @@
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import { withImages, ResourceP } from "../traits";
|
||||
import { ImagesP, ResourceP } from "../traits";
|
||||
|
||||
export const CollectionP = withImages(
|
||||
ResourceP("collection").extend({
|
||||
export const CollectionP = ResourceP("collection")
|
||||
.merge(ImagesP)
|
||||
.extend({
|
||||
/**
|
||||
* The title of this collection.
|
||||
*/
|
||||
@ -31,11 +32,11 @@ export const CollectionP = withImages(
|
||||
* The summary of this show.
|
||||
*/
|
||||
overview: z.string().nullable(),
|
||||
}),
|
||||
).transform((x) => ({
|
||||
...x,
|
||||
href: `/collection/${x.slug}`,
|
||||
}));
|
||||
})
|
||||
.transform((x) => ({
|
||||
...x,
|
||||
href: `/collection/${x.slug}`,
|
||||
}));
|
||||
|
||||
/**
|
||||
* A class representing collections of show or movies.
|
||||
|
@ -20,11 +20,12 @@
|
||||
|
||||
import { z } from "zod";
|
||||
import { zdate } from "../utils";
|
||||
import { withImages, imageFn } from "../traits";
|
||||
import { ImagesP, imageFn } from "../traits";
|
||||
import { ResourceP } from "../traits/resource";
|
||||
|
||||
export const BaseEpisodeP = withImages(
|
||||
ResourceP("episode").extend({
|
||||
export const BaseEpisodeP = ResourceP("episode")
|
||||
.merge(ImagesP)
|
||||
.extend({
|
||||
/**
|
||||
* The season in witch this episode is in.
|
||||
*/
|
||||
@ -71,8 +72,7 @@ export const BaseEpisodeP = withImages(
|
||||
* The id of the show containing this episode
|
||||
*/
|
||||
showId: z.string(),
|
||||
}),
|
||||
)
|
||||
})
|
||||
.transform((x) => ({
|
||||
...x,
|
||||
runtime: x.runtime === 0 ? null : x.runtime,
|
||||
|
@ -20,7 +20,7 @@
|
||||
|
||||
import { z } from "zod";
|
||||
import { zdate } from "../utils";
|
||||
import { withImages, ResourceP, imageFn } from "../traits";
|
||||
import { ImagesP, ResourceP, imageFn } from "../traits";
|
||||
import { Genre } from "./genre";
|
||||
import { StudioP } from "./studio";
|
||||
import { Status } from "./show";
|
||||
@ -28,8 +28,9 @@ import { CollectionP } from "./collection";
|
||||
import { MetadataP } from "./metadata";
|
||||
import { WatchStatusP } from "./watch-status";
|
||||
|
||||
export const MovieP = withImages(
|
||||
ResourceP("movie").extend({
|
||||
export const MovieP = ResourceP("movie")
|
||||
.merge(ImagesP)
|
||||
.extend({
|
||||
/**
|
||||
* The title of this movie.
|
||||
*/
|
||||
@ -104,8 +105,7 @@ export const MovieP = withImages(
|
||||
* Metadata of what an user as started/planned to watch.
|
||||
*/
|
||||
watchStatus: WatchStatusP.optional().nullable(),
|
||||
}),
|
||||
)
|
||||
})
|
||||
.transform((x) => ({
|
||||
...x,
|
||||
runtime: x.runtime === 0 ? null : x.runtime,
|
||||
|
@ -19,28 +19,25 @@
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import { withImages } from "../traits";
|
||||
import { ResourceP } from "../traits/resource";
|
||||
import { ImagesP, ResourceP } from "../traits";
|
||||
|
||||
export const PersonP = withImages(
|
||||
ResourceP("people").extend({
|
||||
/**
|
||||
* The name of this person.
|
||||
*/
|
||||
name: z.string(),
|
||||
/**
|
||||
* The type of work the person has done for the show. That can be something like "Actor",
|
||||
* "Writer", "Music", "Voice Actor"...
|
||||
*/
|
||||
type: z.string().optional(),
|
||||
export const PersonP = ResourceP("people").merge(ImagesP).extend({
|
||||
/**
|
||||
* The name of this person.
|
||||
*/
|
||||
name: z.string(),
|
||||
/**
|
||||
* The type of work the person has done for the show. That can be something like "Actor",
|
||||
* "Writer", "Music", "Voice Actor"...
|
||||
*/
|
||||
type: z.string().optional(),
|
||||
|
||||
/**
|
||||
* The role the People played. This is mostly used to inform witch character was played for actor
|
||||
* and voice actors.
|
||||
*/
|
||||
role: z.string().optional(),
|
||||
}),
|
||||
);
|
||||
/**
|
||||
* The role the People played. This is mostly used to inform witch character was played for actor
|
||||
* and voice actors.
|
||||
*/
|
||||
role: z.string().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* A studio that make shows.
|
||||
|
@ -20,37 +20,34 @@
|
||||
|
||||
import { z } from "zod";
|
||||
import { zdate } from "../utils";
|
||||
import { withImages } from "../traits";
|
||||
import { ResourceP } from "../traits/resource";
|
||||
import { ImagesP, ResourceP } from "../traits";
|
||||
|
||||
export const SeasonP = withImages(
|
||||
ResourceP("season").extend({
|
||||
/**
|
||||
* The name of this season.
|
||||
*/
|
||||
name: z.string(),
|
||||
/**
|
||||
* The number of this season. This can be set to 0 to indicate specials.
|
||||
*/
|
||||
seasonNumber: z.number(),
|
||||
/**
|
||||
* A quick overview of this season.
|
||||
*/
|
||||
overview: z.string().nullable(),
|
||||
/**
|
||||
* The starting air date of this season.
|
||||
*/
|
||||
startDate: zdate().nullable(),
|
||||
/**
|
||||
* The ending date of this season.
|
||||
*/
|
||||
endDate: zdate().nullable(),
|
||||
/**
|
||||
* The number of episodes available on kyoo of this season.
|
||||
*/
|
||||
episodesCount: z.number(),
|
||||
}),
|
||||
);
|
||||
export const SeasonP = ResourceP("season").merge(ImagesP).extend({
|
||||
/**
|
||||
* The name of this season.
|
||||
*/
|
||||
name: z.string(),
|
||||
/**
|
||||
* The number of this season. This can be set to 0 to indicate specials.
|
||||
*/
|
||||
seasonNumber: z.number(),
|
||||
/**
|
||||
* A quick overview of this season.
|
||||
*/
|
||||
overview: z.string().nullable(),
|
||||
/**
|
||||
* The starting air date of this season.
|
||||
*/
|
||||
startDate: zdate().nullable(),
|
||||
/**
|
||||
* The ending date of this season.
|
||||
*/
|
||||
endDate: zdate().nullable(),
|
||||
/**
|
||||
* The number of episodes available on kyoo of this season.
|
||||
*/
|
||||
episodesCount: z.number(),
|
||||
});
|
||||
|
||||
/**
|
||||
* A season of a Show.
|
||||
|
@ -20,7 +20,7 @@
|
||||
|
||||
import { z } from "zod";
|
||||
import { zdate } from "../utils";
|
||||
import { withImages, ResourceP } from "../traits";
|
||||
import { ImagesP, ResourceP } from "../traits";
|
||||
import { Genre } from "./genre";
|
||||
import { StudioP } from "./studio";
|
||||
import { BaseEpisodeP } from "./episode.base";
|
||||
@ -37,8 +37,9 @@ export enum Status {
|
||||
Planned = "Planned",
|
||||
}
|
||||
|
||||
export const ShowP = withImages(
|
||||
ResourceP("show").extend({
|
||||
export const ShowP = ResourceP("show")
|
||||
.merge(ImagesP)
|
||||
.extend({
|
||||
/**
|
||||
* The title of this show.
|
||||
*/
|
||||
@ -103,8 +104,7 @@ export const ShowP = withImages(
|
||||
* The number of episodes in this show.
|
||||
*/
|
||||
episodesCount: z.number().int().gte(0).optional(),
|
||||
}),
|
||||
)
|
||||
})
|
||||
.transform((x) => {
|
||||
if (!x.thumbnail && x.poster) {
|
||||
x.thumbnail = { ...x.poster };
|
||||
|
@ -19,7 +19,7 @@
|
||||
*/
|
||||
|
||||
import { Platform } from "react-native";
|
||||
import { ZodObject, ZodRawShape, z } from "zod";
|
||||
import { z } from "zod";
|
||||
import { lastUsedUrl } from "..";
|
||||
|
||||
export const imageFn = (url: string) =>
|
||||
@ -28,9 +28,12 @@ export const imageFn = (url: string) =>
|
||||
export const Img = z.object({
|
||||
source: z.string(),
|
||||
blurhash: z.string(),
|
||||
low: z.string().transform(imageFn),
|
||||
medium: z.string().transform(imageFn),
|
||||
high: z.string().transform(imageFn),
|
||||
});
|
||||
|
||||
const ImagesP = z.object({
|
||||
export const ImagesP = z.object({
|
||||
/**
|
||||
* An url to the poster of this resource. If this resource does not have an image, the link will
|
||||
* be null. If the kyoo's instance is not capable of handling this kind of image for the specific
|
||||
@ -53,28 +56,7 @@ const ImagesP = z.object({
|
||||
logo: Img.nullable(),
|
||||
});
|
||||
|
||||
const addQualities = (x: object | null | undefined, href: string) => {
|
||||
if (x === null) return null;
|
||||
return {
|
||||
...x,
|
||||
low: imageFn(`${href}?quality=low`),
|
||||
medium: imageFn(`${href}?quality=medium`),
|
||||
high: imageFn(`${href}?quality=high`),
|
||||
};
|
||||
};
|
||||
|
||||
export const withImages = <T extends ZodRawShape>(parser: ZodObject<T>) => {
|
||||
return parser.merge(ImagesP).transform((x) => {
|
||||
return {
|
||||
...x,
|
||||
poster: addQualities(x.poster, `/${x.kind}/${x.slug}/poster`),
|
||||
thumbnail: addQualities(x.thumbnail, `/${x.kind}/${x.slug}/thumbnail`),
|
||||
logo: addQualities(x.logo, `/${x.kind}/${x.slug}/logo`),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Base traits for items that has image resources.
|
||||
*/
|
||||
export type KyooImage = z.infer<typeof Img> & { low: string; medium: string; high: string };
|
||||
export type KyooImage = z.infer<typeof Img>;
|
||||
|
@ -208,7 +208,7 @@ export const toTimerString = (timer?: number, duration?: number) => {
|
||||
return "??:??";
|
||||
const h = Math.floor(timer / 3600);
|
||||
const min = Math.floor((timer / 60) % 60);
|
||||
const sec = Math.round(timer % 60);
|
||||
const sec = Math.floor(timer % 60);
|
||||
const fmt = (n: number) => n.toString().padStart(2, "0");
|
||||
|
||||
if (duration >= 3600) return `${fmt(h)}:${fmt(min)}:${fmt(sec)}`;
|
||||
|
@ -14,5 +14,5 @@ async def main():
|
||||
|
||||
async with KyooClient() as kyoo, Subscriber() as sub:
|
||||
provider = Provider.get_default(kyoo.client)
|
||||
scanner = Matcher(kyoo, provider)
|
||||
await sub.listen(scanner)
|
||||
matcher = Matcher(kyoo, provider)
|
||||
await sub.listen(matcher)
|
||||
|
@ -1,4 +1,5 @@
|
||||
from datetime import timedelta
|
||||
from typing import Literal
|
||||
import asyncio
|
||||
from logging import getLogger
|
||||
from providers.provider import Provider, ProviderError
|
||||
@ -165,3 +166,32 @@ class Matcher:
|
||||
return await self._client.post("seasons", data=season.to_kyoo())
|
||||
|
||||
return await create_season(show_id, season_number)
|
||||
|
||||
async def refresh(
|
||||
self,
|
||||
kind: Literal["collection", "movie", "episode", "show", "season"],
|
||||
kyoo_id: str,
|
||||
):
|
||||
identify_table = {
|
||||
"collection": lambda _, id: self._provider.identify_collection(
|
||||
id["dataId"]
|
||||
),
|
||||
"movie": lambda _, id: self._provider.identify_movie(id["dataId"]),
|
||||
"show": lambda _, id: self._provider.identify_show(id["dataId"]),
|
||||
"season": lambda season, id: self._provider.identify_season(
|
||||
id["dataId"], season["seasonNumber"]
|
||||
),
|
||||
"episode": lambda episode, id: self._provider.identify_episode(
|
||||
id["showId"], id["season"], id["episode"], episode["absoluteNumber"]
|
||||
),
|
||||
}
|
||||
current = await self._client.get(kind, kyoo_id)
|
||||
if self._provider.name not in current["externalId"]:
|
||||
logger.error(
|
||||
f"Could not refresh metadata of {kind}/{kyoo_id}. Missisg provider id."
|
||||
)
|
||||
return False
|
||||
provider_id = current["externalId"][self._provider.name]
|
||||
new_value = await identify_table[kind](current, provider_id)
|
||||
await self._client.put(f"{kind}/{kyoo_id}", data=new_value.to_kyoo())
|
||||
return True
|
||||
|
@ -1,7 +1,6 @@
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from dataclasses_json import DataClassJsonMixin
|
||||
from typing import Literal
|
||||
from typing import Union, Literal
|
||||
from msgspec import Struct, json
|
||||
import os
|
||||
import logging
|
||||
from aio_pika import connect_robust
|
||||
@ -12,18 +11,33 @@ from matcher.matcher import Matcher
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Message(DataClassJsonMixin):
|
||||
action: Literal["scan", "delete"]
|
||||
class Message(Struct, tag_field="action", tag=str.lower):
|
||||
pass
|
||||
|
||||
|
||||
class Scan(Message):
|
||||
path: str
|
||||
|
||||
|
||||
class Delete(Message):
|
||||
path: str
|
||||
|
||||
|
||||
class Refresh(Message):
|
||||
kind: Literal["collection", "show", "movie", "season", "episode"]
|
||||
id: str
|
||||
|
||||
|
||||
decoder = json.Decoder(Union[Scan, Delete, Refresh])
|
||||
|
||||
|
||||
class Subscriber:
|
||||
QUEUE = "scanner"
|
||||
|
||||
async def __aenter__(self):
|
||||
self._con = await connect_robust(
|
||||
host=os.environ.get("RABBITMQ_HOST", "rabbitmq"),
|
||||
port=int(os.environ.get("RABBITMQ_PORT", "5672")),
|
||||
login=os.environ.get("RABBITMQ_DEFAULT_USER", "guest"),
|
||||
password=os.environ.get("RABBITMQ_DEFAULT_PASS", "guest"),
|
||||
)
|
||||
@ -36,18 +50,24 @@ class Subscriber:
|
||||
|
||||
async def listen(self, scanner: Matcher):
|
||||
async def on_message(message: AbstractIncomingMessage):
|
||||
msg = Message.from_json(message.body)
|
||||
ack = False
|
||||
match msg.action:
|
||||
case "scan":
|
||||
ack = await scanner.identify(msg.path)
|
||||
case "delete":
|
||||
ack = await scanner.delete(msg.path)
|
||||
case _:
|
||||
logger.error(f"Invalid action: {msg.action}")
|
||||
if ack:
|
||||
await message.ack()
|
||||
else:
|
||||
try:
|
||||
msg = decoder.decode(message.body)
|
||||
ack = False
|
||||
match msg:
|
||||
case Scan(path):
|
||||
ack = await scanner.identify(path)
|
||||
case Delete(path):
|
||||
ack = await scanner.delete(path)
|
||||
case Refresh(kind, id):
|
||||
ack = await scanner.refresh(kind, id)
|
||||
case _:
|
||||
logger.error(f"Invalid action: {msg.action}")
|
||||
if ack:
|
||||
await message.ack()
|
||||
else:
|
||||
await message.reject()
|
||||
except Exception as e:
|
||||
logger.exception("Unhandled error", exc_info=e)
|
||||
await message.reject()
|
||||
|
||||
# Allow up to 20 scan requests to run in parallel on the same listener.
|
||||
|
@ -36,25 +36,12 @@ class KyooClient:
|
||||
await self.client.close()
|
||||
|
||||
async def get_registered_paths(self) -> List[str]:
|
||||
paths = None
|
||||
async with self.client.get(
|
||||
f"{self._url}/episodes",
|
||||
params={"limit": 0},
|
||||
f"{self._url}/paths",
|
||||
headers={"X-API-Key": self._api_key},
|
||||
) as r:
|
||||
r.raise_for_status()
|
||||
ret = await r.json()
|
||||
paths = list(x["path"] for x in ret["items"])
|
||||
|
||||
async with self.client.get(
|
||||
f"{self._url}/movies",
|
||||
params={"limit": 0},
|
||||
headers={"X-API-Key": self._api_key},
|
||||
) as r:
|
||||
r.raise_for_status()
|
||||
ret = await r.json()
|
||||
paths += list(x["path"] for x in ret["items"])
|
||||
return paths
|
||||
return await r.json()
|
||||
|
||||
async def create_issue(self, path: str, issue: str, extra: dict | None = None):
|
||||
async with self.client.post(
|
||||
@ -112,20 +99,6 @@ class KyooClient:
|
||||
logger.error(f"Request error: {await r.text()}")
|
||||
r.raise_for_status()
|
||||
ret = await r.json()
|
||||
|
||||
if r.status == 409 and (
|
||||
(path == "shows" and ret["startAir"][:4] != str(data["start_air"].year))
|
||||
or (
|
||||
path == "movies"
|
||||
and ret["airDate"][:4] != str(data["air_date"].year)
|
||||
)
|
||||
):
|
||||
logger.info(
|
||||
f"Found a {path} with the same slug ({ret['slug']}) and a different date, using the date as part of the slug"
|
||||
)
|
||||
year = (data["start_air"] if path == "movie" else data["air_date"]).year
|
||||
data["slug"] = f"{ret['slug']}-{year}"
|
||||
return await self.post(path, data=data)
|
||||
return ret["id"]
|
||||
|
||||
async def delete(
|
||||
@ -154,3 +127,35 @@ class KyooClient:
|
||||
r.raise_for_status()
|
||||
|
||||
await self.delete_issue(path)
|
||||
|
||||
async def get(
|
||||
self, kind: Literal["movie", "show", "season", "episode", "collection"], id: str
|
||||
):
|
||||
async with self.client.get(
|
||||
f"{self._url}/{kind}/{id}",
|
||||
headers={"X-API-Key": self._api_key},
|
||||
) as r:
|
||||
if not r.ok:
|
||||
logger.error(f"Request error: {await r.text()}")
|
||||
r.raise_for_status()
|
||||
return await r.json()
|
||||
|
||||
async def put(self, path: str, *, data: dict[str, Any]):
|
||||
logger.debug(
|
||||
"Sending %s: %s",
|
||||
path,
|
||||
jsons.dumps(
|
||||
data,
|
||||
key_transformer=jsons.KEY_TRANSFORMER_CAMELCASE,
|
||||
jdkwargs={"indent": 4},
|
||||
),
|
||||
)
|
||||
async with self.client.put(
|
||||
f"{self._url}/{path}",
|
||||
json=data,
|
||||
headers={"X-API-Key": self._api_key},
|
||||
) as r:
|
||||
# Allow 409 and continue as if it worked.
|
||||
if not r.ok and r.status != 409:
|
||||
logger.error(f"Request error: {await r.text()}")
|
||||
r.raise_for_status()
|
||||
|
@ -3,4 +3,4 @@ aiohttp
|
||||
jsons
|
||||
watchfiles
|
||||
aio-pika
|
||||
dataclasses-json
|
||||
msgspec
|
||||
|
@ -7,7 +7,7 @@ logger = getLogger(__name__)
|
||||
|
||||
|
||||
async def monitor(path: str, publisher: Publisher):
|
||||
async for changes in awatch(path):
|
||||
async for changes in awatch(path, ignore_permission_denied=True):
|
||||
for event, file in changes:
|
||||
if event == Change.added:
|
||||
await publisher.add(file)
|
||||
|
@ -9,6 +9,7 @@ class Publisher:
|
||||
async def __aenter__(self):
|
||||
self._con = await connect_robust(
|
||||
host=os.environ.get("RABBITMQ_HOST", "rabbitmq"),
|
||||
port=int(os.environ.get("RABBITMQ_PORT", "5672")),
|
||||
login=os.environ.get("RABBITMQ_DEFAULT_USER", "guest"),
|
||||
password=os.environ.get("RABBITMQ_DEFAULT_PASS", "guest"),
|
||||
)
|
||||
|
@ -1 +0,0 @@
|
||||
aio-pika
|
Loading…
x
Reference in New Issue
Block a user