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"
|
MEILI_MASTER_KEY="ghvjkgisbgkbgskegblfqbgjkebbhgwkjfb"
|
||||||
|
|
||||||
RABBITMQ_HOST=rabbitmq
|
RABBITMQ_HOST=rabbitmq
|
||||||
|
RABBITMQ_PORT=5672
|
||||||
RABBITMQ_DEFAULT_USER=kyoo
|
RABBITMQ_DEFAULT_USER=kyoo
|
||||||
RABBITMQ_DEFAULT_PASS=aohohunuhouhuhhoahothonseuhaoensuthoaentsuhha
|
RABBITMQ_DEFAULT_PASS=aohohunuhouhuhhoahothonseuhaoensuthoaentsuhha
|
||||||
|
@ -46,6 +46,7 @@ def main():
|
|||||||
connection = pika.BlockingConnection(
|
connection = pika.BlockingConnection(
|
||||||
pika.ConnectionParameters(
|
pika.ConnectionParameters(
|
||||||
host=os.environ.get("RABBITMQ_HOST", "rabbitmq"),
|
host=os.environ.get("RABBITMQ_HOST", "rabbitmq"),
|
||||||
|
port=os.environ.get("RABBITMQ_PORT", 5672),
|
||||||
credentials=pika.credentials.PlainCredentials(
|
credentials=pika.credentials.PlainCredentials(
|
||||||
os.environ.get("RABBITMQ_DEFAULT_USER", "guest"),
|
os.environ.get("RABBITMQ_DEFAULT_USER", "guest"),
|
||||||
os.environ.get("RABBITMQ_DEFAULT_PASS", "guest"),
|
os.environ.get("RABBITMQ_DEFAULT_PASS", "guest"),
|
||||||
|
@ -56,7 +56,7 @@ class Simkl(Service):
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
headers={
|
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,
|
"simkl-api-key": self._api_key,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -85,7 +85,7 @@ class Simkl(Service):
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
headers={
|
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,
|
"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
|
RUN apt-get update && apt-get install -y curl
|
||||||
COPY --from=builder /app /app
|
COPY --from=builder /app /app
|
||||||
|
|
||||||
WORKDIR /kyoo
|
WORKDIR /app
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
# The back can take a long time to start if meilisearch is initializing
|
# 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
|
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
|
COPY src/Kyoo.Swagger/Kyoo.Swagger.csproj src/Kyoo.Swagger/Kyoo.Swagger.csproj
|
||||||
RUN dotnet restore
|
RUN dotnet restore
|
||||||
|
|
||||||
WORKDIR /kyoo
|
WORKDIR /app
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
ENV DOTNET_USE_POLLING_FILE_WATCHER 1
|
ENV DOTNET_USE_POLLING_FILE_WATCHER 1
|
||||||
# HEALTHCHECK --interval=5s CMD curl --fail http://localhost:5000/health || exit
|
# HEALTHCHECK --interval=5s CMD curl --fail http://localhost:5000/health || exit
|
||||||
|
@ -19,7 +19,10 @@ RUN dotnet restore -a $TARGETARCH
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN dotnet build
|
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
|
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0
|
||||||
COPY --from=builder /app/migrate /app/migrate
|
COPY --from=builder /app/migrate /app/migrate
|
||||||
|
@ -28,6 +28,11 @@
|
|||||||
<CheckCodingStyle Condition="$(CheckCodingStyle) == ''">true</CheckCodingStyle>
|
<CheckCodingStyle Condition="$(CheckCodingStyle) == ''">true</CheckCodingStyle>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<BaseIntermediateOutputPath>$(MsBuildThisFileDirectory)/../out/obj/$(MSBuildProjectName)</BaseIntermediateOutputPath>
|
||||||
|
<BaseOutputPath>$(MsBuildThisFileDirectory)/../out/bin/$(MSBuildProjectName)</BaseOutputPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="$(CheckCodingStyle) == true">
|
<ItemGroup Condition="$(CheckCodingStyle) == true">
|
||||||
<None Include="$(MSBuildThisFileDirectory)../.editorconfig" Link=".editorconfig" Visible="false" />
|
<None Include="$(MSBuildThisFileDirectory)../.editorconfig" Link=".editorconfig" Visible="false" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -23,56 +23,19 @@ using Kyoo.Abstractions.Models;
|
|||||||
|
|
||||||
namespace Kyoo.Abstractions.Controllers;
|
namespace Kyoo.Abstractions.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Download images and retrieve the path of those images for a resource.
|
|
||||||
/// </summary>
|
|
||||||
public interface IThumbnailsManager
|
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)
|
Task DownloadImages<T>(T item)
|
||||||
where T : IThumbnails;
|
where T : IThumbnails;
|
||||||
|
|
||||||
/// <summary>
|
Task DownloadImage(Image? image, string what);
|
||||||
/// Retrieve the local path of an image of the given item.
|
|
||||||
/// </summary>
|
string GetImagePath(Guid imageId, ImageQuality quality);
|
||||||
/// <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;
|
|
||||||
|
|
||||||
/// <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)
|
Task DeleteImages<T>(T item)
|
||||||
where T : IThumbnails;
|
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);
|
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);
|
Task SetUserImage(Guid userId, Stream? image);
|
||||||
}
|
}
|
||||||
|
@ -94,7 +94,7 @@ public class PermissionAttribute : Attribute, IFilterFactory
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The group of this permission.
|
/// The group of this permission.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Group Group { get; }
|
public Group Group { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ask a permission to run an action.
|
/// Ask a permission to run an action.
|
||||||
|
@ -17,12 +17,9 @@
|
|||||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Globalization;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Kyoo.Abstractions.Models.Attributes;
|
|
||||||
|
|
||||||
namespace Kyoo.Abstractions.Models;
|
namespace Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
@ -49,9 +46,13 @@ public interface IThumbnails
|
|||||||
}
|
}
|
||||||
|
|
||||||
[JsonConverter(typeof(ImageConvertor))]
|
[JsonConverter(typeof(ImageConvertor))]
|
||||||
[SqlFirstColumn(nameof(Source))]
|
|
||||||
public class Image
|
public class Image
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A unique identifier for the image. Used for proper http caches.
|
||||||
|
/// </summary>
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The original image from another server.
|
/// The original image from another server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -63,6 +64,21 @@ public class Image
|
|||||||
[MaxLength(32)]
|
[MaxLength(32)]
|
||||||
public string Blurhash { get; set; }
|
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() { }
|
public Image() { }
|
||||||
|
|
||||||
[JsonConstructor]
|
[JsonConstructor]
|
||||||
@ -72,6 +88,7 @@ public class Image
|
|||||||
Blurhash = blurhash ?? "000000";
|
Blurhash = blurhash ?? "000000";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
public class ImageConvertor : JsonConverter<Image>
|
public class ImageConvertor : JsonConverter<Image>
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -84,7 +101,10 @@ public class Image
|
|||||||
if (reader.TokenType == JsonTokenType.String && reader.GetString() is string source)
|
if (reader.TokenType == JsonTokenType.String && reader.GetString() is string source)
|
||||||
return new Image(source);
|
return new Image(source);
|
||||||
using JsonDocument document = JsonDocument.ParseValue(ref reader);
|
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 />
|
/// <inheritdoc />
|
||||||
@ -97,6 +117,9 @@ public class Image
|
|||||||
writer.WriteStartObject();
|
writer.WriteStartObject();
|
||||||
writer.WriteString("source", value.Source);
|
writer.WriteString("source", value.Source);
|
||||||
writer.WriteString("blurhash", value.Blurhash);
|
writer.WriteString("blurhash", value.Blurhash);
|
||||||
|
writer.WriteString("low", value.Low);
|
||||||
|
writer.WriteString("medium", value.Medium);
|
||||||
|
writer.WriteString("high", value.High);
|
||||||
writer.WriteEndObject();
|
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.
|
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints used by admins.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string AdminGroup = "4:Admin";
|
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 System.Text.Json.Serialization.Metadata;
|
||||||
using Kyoo.Abstractions.Models;
|
using Kyoo.Abstractions.Models;
|
||||||
using Kyoo.Abstractions.Models.Attributes;
|
using Kyoo.Abstractions.Models.Attributes;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using static System.Text.Json.JsonNamingPolicy;
|
using static System.Text.Json.JsonNamingPolicy;
|
||||||
|
|
||||||
namespace Kyoo.Utils;
|
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;
|
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>
|
/// <summary>
|
||||||
/// Get the generic definition of <paramref name="genericType"/>.
|
/// Get the generic definition of <paramref name="genericType"/>.
|
||||||
/// For example, calling this function with List<string> and typeof(IEnumerable<>) will return IEnumerable<string>
|
/// 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);
|
.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>
|
/// <summary>
|
||||||
/// Convert a dictionary to a query string.
|
/// Convert a dictionary to a query string.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// A local repository to handle collections
|
/// A local repository to handle collections
|
||||||
/// </summary>
|
/// </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 />
|
/// <inheritdoc />
|
||||||
public override async Task<ICollection<Collection>> Search(
|
public override async Task<ICollection<Collection>> Search(
|
||||||
string query,
|
string query,
|
||||||
Include<Collection>? include = default
|
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}%"))
|
.Where(x => EF.Functions.ILike(x.Name + " " + x.Slug, $"%{query}%"))
|
||||||
.Take(20)
|
.Take(20)
|
||||||
.ToListAsync();
|
.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 />
|
/// <inheritdoc />
|
||||||
protected override async Task Validate(Collection resource)
|
protected override async Task Validate(Collection resource)
|
||||||
{
|
{
|
||||||
@ -78,25 +53,18 @@ public class CollectionRepository : LocalRepository<Collection>
|
|||||||
|
|
||||||
if (string.IsNullOrEmpty(resource.Name))
|
if (string.IsNullOrEmpty(resource.Name))
|
||||||
throw new ArgumentException("The collection's name must be set and not empty");
|
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)
|
public async Task AddMovie(Guid id, Guid movieId)
|
||||||
{
|
{
|
||||||
_database.AddLinks<Collection, Movie>(id, movieId);
|
Database.AddLinks<Collection, Movie>(id, movieId);
|
||||||
await _database.SaveChangesAsync();
|
await Database.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddShow(Guid id, Guid showId)
|
public async Task AddShow(Guid id, Guid showId)
|
||||||
{
|
{
|
||||||
_database.AddLinks<Collection, Show>(id, showId);
|
Database.AddLinks<Collection, Show>(id, showId);
|
||||||
await _database.SaveChangesAsync();
|
await Database.SaveChangesAsync();
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override async Task Delete(Collection obj)
|
|
||||||
{
|
|
||||||
_database.Entry(obj).State = EntityState.Deleted;
|
|
||||||
await _database.SaveChangesAsync();
|
|
||||||
await base.Delete(obj);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -252,7 +252,7 @@ public static class DapperHelper
|
|||||||
this IDbConnection db,
|
this IDbConnection db,
|
||||||
FormattableString command,
|
FormattableString command,
|
||||||
Dictionary<string, Type> config,
|
Dictionary<string, Type> config,
|
||||||
Func<List<object?>, T> mapper,
|
Func<IList<object?>, T> mapper,
|
||||||
Func<Guid, Task<T>> get,
|
Func<Guid, Task<T>> get,
|
||||||
SqlVariableContext context,
|
SqlVariableContext context,
|
||||||
Include<T>? include,
|
Include<T>? include,
|
||||||
@ -327,23 +327,6 @@ public static class DapperHelper
|
|||||||
? ExpendProjections(typeV, prefix, include)
|
? ExpendProjections(typeV, prefix, include)
|
||||||
: null;
|
: 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))
|
if (string.IsNullOrEmpty(projection))
|
||||||
return leadingComa;
|
return leadingComa;
|
||||||
return $", {projection}{leadingComa}";
|
return $", {projection}{leadingComa}";
|
||||||
@ -355,19 +338,7 @@ public static class DapperHelper
|
|||||||
types.ToArray(),
|
types.ToArray(),
|
||||||
items =>
|
items =>
|
||||||
{
|
{
|
||||||
List<object?> nItems = new(items.Length);
|
return mapIncludes(mapper(items), items.Skip(config.Count));
|
||||||
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));
|
|
||||||
},
|
},
|
||||||
ParametersDictionary.LoadFrom(cmd),
|
ParametersDictionary.LoadFrom(cmd),
|
||||||
splitOn: string.Join(
|
splitOn: string.Join(
|
||||||
@ -384,7 +355,7 @@ public static class DapperHelper
|
|||||||
this IDbConnection db,
|
this IDbConnection db,
|
||||||
FormattableString command,
|
FormattableString command,
|
||||||
Dictionary<string, Type> config,
|
Dictionary<string, Type> config,
|
||||||
Func<List<object?>, T> mapper,
|
Func<IList<object?>, T> mapper,
|
||||||
SqlVariableContext context,
|
SqlVariableContext context,
|
||||||
Include<T>? include,
|
Include<T>? include,
|
||||||
Filter<T>? filter,
|
Filter<T>? filter,
|
||||||
|
@ -37,7 +37,7 @@ public abstract class DapperRepository<T> : IRepository<T>
|
|||||||
|
|
||||||
protected abstract Dictionary<string, Type> Config { get; }
|
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; }
|
protected DbConnection Database { get; init; }
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Kyoo.Abstractions.Controllers;
|
using Kyoo.Abstractions.Controllers;
|
||||||
@ -35,8 +36,8 @@ namespace Kyoo.Core.Controllers;
|
|||||||
public class EpisodeRepository(
|
public class EpisodeRepository(
|
||||||
DatabaseContext database,
|
DatabaseContext database,
|
||||||
IRepository<Show> shows,
|
IRepository<Show> shows,
|
||||||
IThumbnailsManager thumbs
|
IThumbnailsManager thumbnails
|
||||||
) : LocalRepository<Episode>(database, thumbs)
|
) : GenericRepository<Episode>(database)
|
||||||
{
|
{
|
||||||
static EpisodeRepository()
|
static EpisodeRepository()
|
||||||
{
|
{
|
||||||
@ -64,70 +65,77 @@ public class EpisodeRepository(
|
|||||||
Include<Episode>? include = default
|
Include<Episode>? include = default
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
return await AddIncludes(database.Episodes, include)
|
return await AddIncludes(Database.Episodes, include)
|
||||||
.Where(x => EF.Functions.ILike(x.Name!, $"%{query}%"))
|
.Where(x => EF.Functions.ILike(x.Name!, $"%{query}%"))
|
||||||
.Take(20)
|
.Take(20)
|
||||||
.ToListAsync();
|
.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 />
|
/// <inheritdoc />
|
||||||
public override async Task<Episode> Create(Episode obj)
|
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;
|
obj.ShowSlug = obj.Show?.Slug ?? (await shows.Get(obj.ShowId)).Slug;
|
||||||
await base.Create(obj);
|
return await base.Create(obj);
|
||||||
database.Entry(obj).State = EntityState.Added;
|
|
||||||
await database.SaveChangesAsync(() => GetDuplicated(obj));
|
|
||||||
await IRepository<Episode>.OnResourceCreated(obj);
|
|
||||||
return obj;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override async Task Validate(Episode resource)
|
protected override async Task Validate(Episode resource)
|
||||||
{
|
{
|
||||||
await base.Validate(resource);
|
await base.Validate(resource);
|
||||||
|
resource.Show = null;
|
||||||
if (resource.ShowId == Guid.Empty)
|
if (resource.ShowId == Guid.Empty)
|
||||||
{
|
throw new ValidationException("Missing show id");
|
||||||
if (resource.Show == null)
|
resource.Season = null;
|
||||||
{
|
|
||||||
throw new ArgumentException(
|
|
||||||
$"Can't store an episode not related "
|
|
||||||
+ $"to any show (showID: {resource.ShowId})."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
resource.ShowId = resource.Show.Id;
|
|
||||||
}
|
|
||||||
if (resource.SeasonId == null && resource.SeasonNumber != null)
|
if (resource.SeasonId == null && resource.SeasonNumber != null)
|
||||||
{
|
{
|
||||||
resource.Season = await database.Seasons.FirstOrDefaultAsync(x =>
|
resource.SeasonId = await Database
|
||||||
x.ShowId == resource.ShowId && x.SeasonNumber == resource.SeasonNumber
|
.Seasons.Where(x =>
|
||||||
);
|
x.ShowId == resource.ShowId && x.SeasonNumber == resource.SeasonNumber
|
||||||
|
)
|
||||||
|
.Select(x => x.Id)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
}
|
}
|
||||||
|
await thumbnails.DownloadImages(resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async Task Delete(Episode obj)
|
public override async Task Delete(Episode obj)
|
||||||
{
|
{
|
||||||
int epCount = await database
|
int epCount = await Database
|
||||||
.Episodes.Where(x => x.ShowId == obj.ShowId)
|
.Episodes.Where(x => x.ShowId == obj.ShowId)
|
||||||
.Take(2)
|
.Take(2)
|
||||||
.CountAsync();
|
.CountAsync();
|
||||||
database.Entry(obj).State = EntityState.Deleted;
|
|
||||||
await database.SaveChangesAsync();
|
|
||||||
await base.Delete(obj);
|
|
||||||
if (epCount == 1)
|
if (epCount == 1)
|
||||||
await shows.Delete(obj.ShowId);
|
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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
@ -29,38 +30,14 @@ using Kyoo.Abstractions.Models.Attributes;
|
|||||||
using Kyoo.Abstractions.Models.Exceptions;
|
using Kyoo.Abstractions.Models.Exceptions;
|
||||||
using Kyoo.Abstractions.Models.Utils;
|
using Kyoo.Abstractions.Models.Utils;
|
||||||
using Kyoo.Postgresql;
|
using Kyoo.Postgresql;
|
||||||
using Kyoo.Utils;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Kyoo.Core.Controllers;
|
namespace Kyoo.Core.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
public abstract class GenericRepository<T>(DatabaseContext database) : IRepository<T>
|
||||||
/// 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>
|
|
||||||
where T : class, IResource, IQuery
|
where T : class, IResource, IQuery
|
||||||
{
|
{
|
||||||
/// <summary>
|
public DatabaseContext Database => database;
|
||||||
/// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public Type RepositoryType => typeof(T);
|
public Type RepositoryType => typeof(T);
|
||||||
@ -127,12 +104,6 @@ public abstract class LocalRepository<T> : IRepository<T>
|
|||||||
return query;
|
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)
|
protected virtual async Task<T> GetWithTracking(Guid id)
|
||||||
{
|
{
|
||||||
T? ret = await Database.Set<T>().AsTracking().FirstOrDefaultAsync(x => x.Id == 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;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual Task<T?> GetDuplicated(T item)
|
|
||||||
{
|
|
||||||
return GetOrDefault(item.Slug);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual Task<T?> GetOrDefault(Guid id, Include<T>? include = default)
|
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)
|
public virtual async Task<T> Create(T obj)
|
||||||
{
|
{
|
||||||
await Validate(obj);
|
await Validate(obj);
|
||||||
if (obj is IThumbnails thumbs)
|
Database.Add(obj);
|
||||||
{
|
await Database.SaveChangesAsync(() => Get(obj.Slug));
|
||||||
try
|
await IRepository<T>.OnResourceCreated(obj);
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -346,27 +295,11 @@ public abstract class LocalRepository<T> : IRepository<T>
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public virtual async Task<T> Edit(T edited)
|
public virtual async Task<T> Edit(T edited)
|
||||||
{
|
{
|
||||||
bool lazyLoading = Database.ChangeTracker.LazyLoadingEnabled;
|
await Validate(edited);
|
||||||
Database.ChangeTracker.LazyLoadingEnabled = false;
|
Database.Update(edited);
|
||||||
try
|
await Database.SaveChangesAsync();
|
||||||
{
|
await IRepository<T>.OnResourceEdited(edited);
|
||||||
T old = await GetWithTracking(edited.Id);
|
return edited;
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@ -391,39 +324,9 @@ public abstract class LocalRepository<T> : IRepository<T>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <exception cref="ValidationException">
|
||||||
/// 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">
|
|
||||||
/// You can throw this if the resource is illegal and should not be saved.
|
/// You can throw this if the resource is illegal and should not be saved.
|
||||||
/// </exception>
|
/// </exception>
|
||||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
|
||||||
protected virtual Task Validate(T resource)
|
protected virtual Task Validate(T resource)
|
||||||
{
|
{
|
||||||
if (
|
if (
|
||||||
@ -432,26 +335,9 @@ public abstract class LocalRepository<T> : IRepository<T>
|
|||||||
)
|
)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
if (string.IsNullOrEmpty(resource.Slug))
|
if (string.IsNullOrEmpty(resource.Slug))
|
||||||
throw new ArgumentException("Resource can't have null as a slug.");
|
throw new ValidationException("Resource can't have null as a slug.");
|
||||||
if (int.TryParse(resource.Slug, out int _) || resource.Slug == "random")
|
if (resource.Slug == "random")
|
||||||
{
|
throw new ValidationException("Resources slug can't be the literal \"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\"."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -470,18 +356,20 @@ public abstract class LocalRepository<T> : IRepository<T>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public virtual Task Delete(T obj)
|
public virtual async Task Delete(T obj)
|
||||||
{
|
{
|
||||||
IRepository<T>.OnResourceDeleted(obj);
|
await Database.Set<T>().Where(x => x.Id == obj.Id).ExecuteDeleteAsync();
|
||||||
if (obj is IThumbnails thumbs)
|
await IRepository<T>.OnResourceDeleted(obj);
|
||||||
return _thumbs.DeleteImages(thumbs);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async Task DeleteAll(Filter<T> filter)
|
public virtual async Task DeleteAll(Filter<T> filter)
|
||||||
{
|
{
|
||||||
foreach (T resource in await GetAll(filter))
|
ICollection<T> items = await GetAll(filter);
|
||||||
await Delete(resource);
|
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>
|
/// <summary>
|
||||||
/// A local repository to handle library items.
|
/// A local repository to handle library items.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LibraryItemRepository : DapperRepository<ILibraryItem>
|
public class LibraryItemRepository(DbConnection database, SqlVariableContext context)
|
||||||
|
: DapperRepository<ILibraryItem>(database, context)
|
||||||
{
|
{
|
||||||
// language=PostgreSQL
|
// language=PostgreSQL
|
||||||
protected override FormattableString Sql =>
|
protected override FormattableString Sql =>
|
||||||
@ -67,7 +68,7 @@ public class LibraryItemRepository : DapperRepository<ILibraryItem>
|
|||||||
{ "c", typeof(Collection) }
|
{ "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)
|
if (items[0] is Show show && show.Id != Guid.Empty)
|
||||||
return show;
|
return show;
|
||||||
@ -78,9 +79,6 @@ public class LibraryItemRepository : DapperRepository<ILibraryItem>
|
|||||||
throw new InvalidDataException();
|
throw new InvalidDataException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public LibraryItemRepository(DbConnection database, SqlVariableContext context)
|
|
||||||
: base(database, context) { }
|
|
||||||
|
|
||||||
public async Task<ICollection<ILibraryItem>> GetAllOfCollection(
|
public async Task<ICollection<ILibraryItem>> GetAllOfCollection(
|
||||||
Guid collectionId,
|
Guid collectionId,
|
||||||
Filter<ILibraryItem>? filter = default,
|
Filter<ILibraryItem>? filter = default,
|
||||||
|
@ -21,58 +21,48 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Kyoo.Abstractions.Controllers;
|
using Kyoo.Abstractions.Controllers;
|
||||||
using Kyoo.Abstractions.Models;
|
using Kyoo.Abstractions.Models;
|
||||||
|
using Kyoo.Abstractions.Models.Exceptions;
|
||||||
using Kyoo.Abstractions.Models.Utils;
|
using Kyoo.Abstractions.Models.Utils;
|
||||||
using Kyoo.Postgresql;
|
using Kyoo.Postgresql;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Kyoo.Core.Controllers;
|
namespace Kyoo.Core.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
public class MovieRepository(
|
||||||
/// A local repository to handle shows
|
DatabaseContext database,
|
||||||
/// </summary>
|
IRepository<Studio> studios,
|
||||||
public class MovieRepository : LocalRepository<Movie>
|
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 />
|
/// <inheritdoc />
|
||||||
public override async Task<ICollection<Movie>> Search(
|
public override async Task<ICollection<Movie>> Search(
|
||||||
string query,
|
string query,
|
||||||
Include<Movie>? include = default
|
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}%"))
|
.Where(x => EF.Functions.ILike(x.Name + " " + x.Slug, $"%{query}%"))
|
||||||
.Take(20)
|
.Take(20)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async Task<Movie> Create(Movie obj)
|
public override Task<Movie> Create(Movie obj)
|
||||||
{
|
{
|
||||||
await base.Create(obj);
|
try
|
||||||
_database.Entry(obj).State = EntityState.Added;
|
{
|
||||||
await _database.SaveChangesAsync(() => Get(obj.Slug));
|
return base.Create(obj);
|
||||||
await IRepository<Movie>.OnResourceCreated(obj);
|
}
|
||||||
return 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 />
|
/// <inheritdoc />
|
||||||
@ -81,28 +71,9 @@ public class MovieRepository : LocalRepository<Movie>
|
|||||||
await base.Validate(resource);
|
await base.Validate(resource);
|
||||||
if (resource.Studio != null)
|
if (resource.Studio != null)
|
||||||
{
|
{
|
||||||
resource.Studio = await _studios.CreateIfNotExists(resource.Studio);
|
resource.StudioId = (await studios.CreateIfNotExists(resource.Studio)).Id;
|
||||||
resource.StudioId = resource.Studio.Id;
|
resource.Studio = null;
|
||||||
}
|
}
|
||||||
}
|
await thumbnails.DownloadImages(resource);
|
||||||
|
|
||||||
/// <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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ public class NewsRepository : DapperRepository<INews>
|
|||||||
protected override Dictionary<string, Type> Config =>
|
protected override Dictionary<string, Type> Config =>
|
||||||
new() { { "e", typeof(Episode) }, { "m", typeof(Movie) }, };
|
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)
|
if (items[0] is Episode episode && episode.Id != Guid.Empty)
|
||||||
return episode;
|
return episode;
|
||||||
|
@ -31,16 +31,9 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
|
|
||||||
namespace Kyoo.Core.Controllers;
|
namespace Kyoo.Core.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
public class SeasonRepository(DatabaseContext database, IThumbnailsManager thumbnails)
|
||||||
/// A local repository to handle seasons.
|
: GenericRepository<Season>(database)
|
||||||
/// </summary>
|
|
||||||
public class SeasonRepository : LocalRepository<Season>
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// The database handle
|
|
||||||
/// </summary>
|
|
||||||
private readonly DatabaseContext _database;
|
|
||||||
|
|
||||||
static SeasonRepository()
|
static SeasonRepository()
|
||||||
{
|
{
|
||||||
// Edit seasons slugs when the show's slug changes.
|
// 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/>
|
/// <inheritdoc/>
|
||||||
public override async Task<ICollection<Season>> Search(
|
public override async Task<ICollection<Season>> Search(
|
||||||
string query,
|
string query,
|
||||||
Include<Season>? include = default
|
Include<Season>? include = default
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
return await AddIncludes(_database.Seasons, include)
|
return await AddIncludes(Database.Seasons, include)
|
||||||
.Where(x => EF.Functions.ILike(x.Name!, $"%{query}%"))
|
.Where(x => EF.Functions.ILike(x.Name!, $"%{query}%"))
|
||||||
.Take(20)
|
.Take(20)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
@ -94,38 +69,20 @@ public class SeasonRepository : LocalRepository<Season>
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override async Task<Season> Create(Season obj)
|
public override async Task<Season> Create(Season obj)
|
||||||
{
|
{
|
||||||
await base.Create(obj);
|
// Set it for the OnResourceCreated event and the return value.
|
||||||
obj.ShowSlug =
|
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}");
|
?? throw new ItemNotFoundException($"No show found with ID {obj.ShowId}");
|
||||||
_database.Entry(obj).State = EntityState.Added;
|
return await base.Create(obj);
|
||||||
await _database.SaveChangesAsync(() => GetDuplicated(obj));
|
|
||||||
await IRepository<Season>.OnResourceCreated(obj);
|
|
||||||
return obj;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
protected override async Task Validate(Season resource)
|
protected override async Task Validate(Season resource)
|
||||||
{
|
{
|
||||||
await base.Validate(resource);
|
await base.Validate(resource);
|
||||||
|
resource.Show = null;
|
||||||
if (resource.ShowId == Guid.Empty)
|
if (resource.ShowId == Guid.Empty)
|
||||||
{
|
throw new ValidationException("Missing show id");
|
||||||
if (resource.Show == null)
|
await thumbnails.DownloadImages(resource);
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,59 +21,48 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Kyoo.Abstractions.Controllers;
|
using Kyoo.Abstractions.Controllers;
|
||||||
using Kyoo.Abstractions.Models;
|
using Kyoo.Abstractions.Models;
|
||||||
|
using Kyoo.Abstractions.Models.Exceptions;
|
||||||
using Kyoo.Abstractions.Models.Utils;
|
using Kyoo.Abstractions.Models.Utils;
|
||||||
using Kyoo.Postgresql;
|
using Kyoo.Postgresql;
|
||||||
using Kyoo.Utils;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Kyoo.Core.Controllers;
|
namespace Kyoo.Core.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
public class ShowRepository(
|
||||||
/// A local repository to handle shows
|
DatabaseContext database,
|
||||||
/// </summary>
|
IRepository<Studio> studios,
|
||||||
public class ShowRepository : LocalRepository<Show>
|
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 />
|
/// <inheritdoc />
|
||||||
public override async Task<ICollection<Show>> Search(
|
public override async Task<ICollection<Show>> Search(
|
||||||
string query,
|
string query,
|
||||||
Include<Show>? include = default
|
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}%"))
|
.Where(x => EF.Functions.ILike(x.Name + " " + x.Slug, $"%{query}%"))
|
||||||
.Take(20)
|
.Take(20)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async Task<Show> Create(Show obj)
|
public override Task<Show> Create(Show obj)
|
||||||
{
|
{
|
||||||
await base.Create(obj);
|
try
|
||||||
_database.Entry(obj).State = EntityState.Added;
|
{
|
||||||
await _database.SaveChangesAsync(() => Get(obj.Slug));
|
return base.Create(obj);
|
||||||
await IRepository<Show>.OnResourceCreated(obj);
|
}
|
||||||
return 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 />
|
/// <inheritdoc />
|
||||||
@ -82,28 +71,9 @@ public class ShowRepository : LocalRepository<Show>
|
|||||||
await base.Validate(resource);
|
await base.Validate(resource);
|
||||||
if (resource.Studio != null)
|
if (resource.Studio != null)
|
||||||
{
|
{
|
||||||
resource.Studio = await _studios.CreateIfNotExists(resource.Studio);
|
resource.StudioId = (await studios.CreateIfNotExists(resource.Studio)).Id;
|
||||||
resource.StudioId = resource.Studio.Id;
|
resource.Studio = null;
|
||||||
}
|
}
|
||||||
}
|
await thumbnails.DownloadImages(resource);
|
||||||
|
|
||||||
/// <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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,11 +19,9 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Kyoo.Abstractions.Controllers;
|
|
||||||
using Kyoo.Abstractions.Models;
|
using Kyoo.Abstractions.Models;
|
||||||
using Kyoo.Abstractions.Models.Utils;
|
using Kyoo.Abstractions.Models.Utils;
|
||||||
using Kyoo.Postgresql;
|
using Kyoo.Postgresql;
|
||||||
using Kyoo.Utils;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Kyoo.Core.Controllers;
|
namespace Kyoo.Core.Controllers;
|
||||||
@ -31,51 +29,17 @@ namespace Kyoo.Core.Controllers;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A local repository to handle studios
|
/// A local repository to handle studios
|
||||||
/// </summary>
|
/// </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 />
|
/// <inheritdoc />
|
||||||
public override async Task<ICollection<Studio>> Search(
|
public override async Task<ICollection<Studio>> Search(
|
||||||
string query,
|
string query,
|
||||||
Include<Studio>? include = default
|
Include<Studio>? include = default
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
return await AddIncludes(_database.Studios, include)
|
return await AddIncludes(Database.Studios, include)
|
||||||
.Where(x => EF.Functions.ILike(x.Name, $"%{query}%"))
|
.Where(x => EF.Functions.ILike(x.Name, $"%{query}%"))
|
||||||
.Take(20)
|
.Take(20)
|
||||||
.ToListAsync();
|
.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,
|
DatabaseContext database,
|
||||||
DbConnection db,
|
DbConnection db,
|
||||||
SqlVariableContext context,
|
SqlVariableContext context,
|
||||||
IThumbnailsManager thumbs,
|
|
||||||
PermissionOption options
|
PermissionOption options
|
||||||
) : LocalRepository<User>(database, thumbs), IUserRepository
|
) : GenericRepository<User>(database), IUserRepository
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async Task<ICollection<User>> Search(
|
public override async Task<ICollection<User>> Search(
|
||||||
@ -50,7 +49,7 @@ public class UserRepository(
|
|||||||
Include<User>? include = default
|
Include<User>? include = default
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
return await AddIncludes(database.Users, include)
|
return await AddIncludes(Database.Users, include)
|
||||||
.Where(x => EF.Functions.ILike(x.Username, $"%{query}%"))
|
.Where(x => EF.Functions.ILike(x.Username, $"%{query}%"))
|
||||||
.Take(20)
|
.Take(20)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
@ -60,26 +59,14 @@ public class UserRepository(
|
|||||||
public override async Task<User> Create(User obj)
|
public override async Task<User> Create(User obj)
|
||||||
{
|
{
|
||||||
// If no users exists, the new one will be an admin. Give it every permissions.
|
// 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;
|
obj.Permissions = PermissionOption.Admin;
|
||||||
else if (!options.RequireVerification)
|
else if (!options.RequireVerification)
|
||||||
obj.Permissions = options.NewUser;
|
obj.Permissions = options.NewUser;
|
||||||
else
|
else
|
||||||
obj.Permissions = Array.Empty<string>();
|
obj.Permissions = Array.Empty<string>();
|
||||||
|
|
||||||
await base.Create(obj);
|
return 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<User?> GetByExternalId(string provider, string id)
|
public Task<User?> GetByExternalId(string provider, string id)
|
||||||
@ -109,8 +96,8 @@ public class UserRepository(
|
|||||||
User user = await GetWithTracking(userId);
|
User user = await GetWithTracking(userId);
|
||||||
user.ExternalId[provider] = token;
|
user.ExternalId[provider] = token;
|
||||||
// without that, the change tracker does not find the modification. /shrug
|
// without that, the change tracker does not find the modification. /shrug
|
||||||
database.Entry(user).Property(x => x.ExternalId).IsModified = true;
|
Database.Entry(user).Property(x => x.ExternalId).IsModified = true;
|
||||||
await database.SaveChangesAsync();
|
await Database.SaveChangesAsync();
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,8 +106,8 @@ public class UserRepository(
|
|||||||
User user = await GetWithTracking(userId);
|
User user = await GetWithTracking(userId);
|
||||||
user.ExternalId.Remove(provider);
|
user.ExternalId.Remove(provider);
|
||||||
// without that, the change tracker does not find the modification. /shrug
|
// without that, the change tracker does not find the modification. /shrug
|
||||||
database.Entry(user).Property(x => x.ExternalId).IsModified = true;
|
Database.Entry(user).Property(x => x.ExternalId).IsModified = true;
|
||||||
await database.SaveChangesAsync();
|
await Database.SaveChangesAsync();
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -135,7 +135,7 @@ public class WatchStatusRepository(
|
|||||||
{ "_mw", typeof(MovieWatchStatus) },
|
{ "_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)
|
if (items[0] is Show show && show.Id != Guid.Empty)
|
||||||
{
|
{
|
||||||
|
@ -42,8 +42,6 @@ public class ThumbnailsManager(
|
|||||||
Lazy<IRepository<User>> users
|
Lazy<IRepository<User>> users
|
||||||
) : IThumbnailsManager
|
) : IThumbnailsManager
|
||||||
{
|
{
|
||||||
private static readonly Dictionary<string, TaskCompletionSource<object>> _downloading = [];
|
|
||||||
|
|
||||||
private static async Task _WriteTo(SKBitmap bitmap, string path, int quality)
|
private static async Task _WriteTo(SKBitmap bitmap, string path, int quality)
|
||||||
{
|
{
|
||||||
SKData data = bitmap.Encode(SKEncodedImageFormat.Webp, quality);
|
SKData data = bitmap.Encode(SKEncodedImageFormat.Webp, quality);
|
||||||
@ -52,12 +50,15 @@ public class ThumbnailsManager(
|
|||||||
await reader.CopyToAsync(file);
|
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)
|
if (image == null)
|
||||||
return;
|
return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (image.Id == Guid.Empty)
|
||||||
|
image.Id = Guid.NewGuid();
|
||||||
|
|
||||||
logger.LogInformation("Downloading image {What}", what);
|
logger.LogInformation("Downloading image {What}", what);
|
||||||
|
|
||||||
HttpClient client = clientFactory.CreateClient();
|
HttpClient client = clientFactory.CreateClient();
|
||||||
@ -79,31 +80,19 @@ public class ThumbnailsManager(
|
|||||||
new SKSizeI(original.Width, original.Height),
|
new SKSizeI(original.Width, original.Height),
|
||||||
SKFilterQuality.High
|
SKFilterQuality.High
|
||||||
);
|
);
|
||||||
await _WriteTo(
|
await _WriteTo(original, GetImagePath(image.Id, ImageQuality.High), 90);
|
||||||
original,
|
|
||||||
$"{localPath}.{ImageQuality.High.ToString().ToLowerInvariant()}.webp",
|
|
||||||
90
|
|
||||||
);
|
|
||||||
|
|
||||||
using SKBitmap medium = high.Resize(
|
using SKBitmap medium = high.Resize(
|
||||||
new SKSizeI((int)(high.Width / 1.5), (int)(high.Height / 1.5)),
|
new SKSizeI((int)(high.Width / 1.5), (int)(high.Height / 1.5)),
|
||||||
SKFilterQuality.Medium
|
SKFilterQuality.Medium
|
||||||
);
|
);
|
||||||
await _WriteTo(
|
await _WriteTo(medium, GetImagePath(image.Id, ImageQuality.Medium), 75);
|
||||||
medium,
|
|
||||||
$"{localPath}.{ImageQuality.Medium.ToString().ToLowerInvariant()}.webp",
|
|
||||||
75
|
|
||||||
);
|
|
||||||
|
|
||||||
using SKBitmap low = medium.Resize(
|
using SKBitmap low = medium.Resize(
|
||||||
new SKSizeI(original.Width / 2, original.Height / 2),
|
new SKSizeI(original.Width / 2, original.Height / 2),
|
||||||
SKFilterQuality.Low
|
SKFilterQuality.Low
|
||||||
);
|
);
|
||||||
await _WriteTo(
|
await _WriteTo(low, GetImagePath(image.Id, ImageQuality.Low), 50);
|
||||||
low,
|
|
||||||
$"{localPath}.{ImageQuality.Low.ToString().ToLowerInvariant()}.webp",
|
|
||||||
50
|
|
||||||
);
|
|
||||||
|
|
||||||
image.Blurhash = Blurhasher.Encode(low, 4, 3);
|
image.Blurhash = Blurhasher.Encode(low, 4, 3);
|
||||||
}
|
}
|
||||||
@ -119,86 +108,24 @@ public class ThumbnailsManager(
|
|||||||
{
|
{
|
||||||
string name = item is IResource res ? res.Slug : "???";
|
string name = item is IResource res ? res.Slug : "???";
|
||||||
|
|
||||||
string posterPath =
|
await DownloadImage(item.Poster, $"The poster of {name}");
|
||||||
$"{_GetBaseImagePath(item, "poster")}.{ImageQuality.High.ToString().ToLowerInvariant()}.webp";
|
await DownloadImage(item.Thumbnail, $"The thumbnail of {name}");
|
||||||
bool duplicated = false;
|
await DownloadImage(item.Logo, $"The logo of {name}");
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string GetImagePath<T>(T item, string image, ImageQuality quality)
|
public string GetImagePath(Guid imageId, ImageQuality quality)
|
||||||
where T : IThumbnails
|
|
||||||
{
|
{
|
||||||
return $"{_GetBaseImagePath(item, image)}.{quality.ToString().ToLowerInvariant()}.webp";
|
return $"/metadata/{imageId}.{quality.ToString().ToLowerInvariant()}.webp";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task DeleteImages<T>(T item)
|
public Task DeleteImages<T>(T item)
|
||||||
where T : IThumbnails
|
where T : IThumbnails
|
||||||
{
|
{
|
||||||
IEnumerable<string> images = new[] { "poster", "thumbnail", "logo" }
|
IEnumerable<string> images = new[] { item.Poster?.Id, item.Thumbnail?.Id, item.Logo?.Id }
|
||||||
.SelectMany(x => _GetBaseImagePath(item, x))
|
.Where(x => x is not null)
|
||||||
|
.SelectMany(x => $"/metadata/{x}")
|
||||||
.SelectMany(x =>
|
.SelectMany(x =>
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
|
@ -63,5 +63,6 @@ public static class CoreModule
|
|||||||
);
|
);
|
||||||
builder.Services.AddScoped<IIssueRepository, IssueRepository>();
|
builder.Services.AddScoped<IIssueRepository, IssueRepository>();
|
||||||
builder.Services.AddScoped<SqlVariableContext>();
|
builder.Services.AddScoped<SqlVariableContext>();
|
||||||
|
builder.Services.AddScoped<MiscRepository>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,13 +16,12 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Linq.Expressions;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using AspNetCore.Proxy;
|
using AspNetCore.Proxy;
|
||||||
using Kyoo.Abstractions.Models.Utils;
|
using Kyoo.Abstractions.Models.Utils;
|
||||||
|
using Kyoo.Authentication;
|
||||||
using Kyoo.Core.Api;
|
using Kyoo.Core.Api;
|
||||||
using Kyoo.Core.Controllers;
|
using Kyoo.Core.Controllers;
|
||||||
using Kyoo.Utils;
|
using Kyoo.Utils;
|
||||||
@ -47,6 +46,8 @@ public static class ServiceExtensions
|
|||||||
options.ModelBinderProviders.Insert(0, new IncludeBinder.Provider());
|
options.ModelBinderProviders.Insert(0, new IncludeBinder.Provider());
|
||||||
options.ModelBinderProviders.Insert(0, new FilterBinder.Provider());
|
options.ModelBinderProviders.Insert(0, new FilterBinder.Provider());
|
||||||
})
|
})
|
||||||
|
.AddApplicationPart(typeof(CoreModule).Assembly)
|
||||||
|
.AddApplicationPart(typeof(AuthenticationModule).Assembly)
|
||||||
.AddJsonOptions(x =>
|
.AddJsonOptions(x =>
|
||||||
{
|
{
|
||||||
x.JsonSerializerOptions.TypeInfoResolver = new JsonKindResolver()
|
x.JsonSerializerOptions.TypeInfoResolver = new JsonKindResolver()
|
||||||
|
@ -17,9 +17,9 @@
|
|||||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
|
||||||
using Kyoo.Authentication;
|
using Kyoo.Authentication;
|
||||||
using Kyoo.Core;
|
using Kyoo.Core;
|
||||||
|
using Kyoo.Core.Controllers;
|
||||||
using Kyoo.Core.Extensions;
|
using Kyoo.Core.Extensions;
|
||||||
using Kyoo.Meiliseach;
|
using Kyoo.Meiliseach;
|
||||||
using Kyoo.Postgresql;
|
using Kyoo.Postgresql;
|
||||||
@ -70,11 +70,6 @@ AppDomain.CurrentDomain.UnhandledException += (_, ex) =>
|
|||||||
Log.Fatal(ex.ExceptionObject as Exception, "Unhandled exception");
|
Log.Fatal(ex.ExceptionObject as Exception, "Unhandled exception");
|
||||||
builder.Host.UseSerilog();
|
builder.Host.UseSerilog();
|
||||||
|
|
||||||
builder
|
|
||||||
.Services.AddMvcCore()
|
|
||||||
.AddApplicationPart(typeof(CoreModule).Assembly)
|
|
||||||
.AddApplicationPart(typeof(AuthenticationModule).Assembly);
|
|
||||||
|
|
||||||
builder.Services.ConfigureMvc();
|
builder.Services.ConfigureMvc();
|
||||||
builder.Services.ConfigureOpenApi();
|
builder.Services.ConfigureOpenApi();
|
||||||
builder.ConfigureKyoo();
|
builder.ConfigureKyoo();
|
||||||
@ -93,42 +88,6 @@ app.UseRouting();
|
|||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.MapControllers();
|
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
|
// Activate services that always run in the background
|
||||||
app.Services.GetRequiredService<MeiliSync>();
|
app.Services.GetRequiredService<MeiliSync>();
|
||||||
app.Services.GetRequiredService<RabbitProducer>();
|
app.Services.GetRequiredService<RabbitProducer>();
|
||||||
@ -138,4 +97,7 @@ await using (AsyncServiceScope scope = app.Services.CreateAsyncScope())
|
|||||||
await MeilisearchModule.Initialize(scope.ServiceProvider);
|
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")]
|
[Route("health")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[ApiDefinition("Health")]
|
[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>
|
/// <summary>
|
||||||
/// Check if the api is ready to accept requests.
|
/// Check if the api is ready to accept requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -57,7 +46,7 @@ public class Health : BaseApi
|
|||||||
headers.Pragma = "no-cache";
|
headers.Pragma = "no-cache";
|
||||||
headers.Expires = "Thu, 01 Jan 1970 00:00:00 GMT";
|
headers.Expires = "Thu, 01 Jan 1970 00:00:00 GMT";
|
||||||
|
|
||||||
HealthReport result = await _healthCheckService.CheckHealthAsync();
|
HealthReport result = await healthCheckService.CheckHealthAsync();
|
||||||
return result.Status switch
|
return result.Status switch
|
||||||
{
|
{
|
||||||
HealthStatus.Healthy => Ok(new HealthResult("Healthy")),
|
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);
|
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>
|
/// <summary>
|
||||||
/// Patch
|
/// Patch
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
using System.IO;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Kyoo.Abstractions.Controllers;
|
using Kyoo.Abstractions.Controllers;
|
||||||
using Kyoo.Abstractions.Models;
|
using Kyoo.Abstractions.Models;
|
||||||
@ -28,14 +28,8 @@ using static Kyoo.Abstractions.Models.Utils.Constants;
|
|||||||
|
|
||||||
namespace Kyoo.Core.Api;
|
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]
|
[ApiController]
|
||||||
public class CrudThumbsApi<T>(IRepository<T> repository, IThumbnailsManager thumbs)
|
public class CrudThumbsApi<T>(IRepository<T> repository) : CrudApi<T>(repository)
|
||||||
: CrudApi<T>(repository)
|
|
||||||
where T : class, IResource, IThumbnails, IQuery
|
where T : class, IResource, IThumbnails, IQuery
|
||||||
{
|
{
|
||||||
private async Task<IActionResult> _GetImage(
|
private async Task<IActionResult> _GetImage(
|
||||||
@ -50,18 +44,19 @@ public class CrudThumbsApi<T>(IRepository<T> repository, IThumbnailsManager thum
|
|||||||
);
|
);
|
||||||
if (resource == null)
|
if (resource == null)
|
||||||
return NotFound();
|
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();
|
return NotFound();
|
||||||
|
|
||||||
if (!identifier.Match(id => false, slug => slug == "random"))
|
// TODO: Remove the /api and use a proxy rewrite instead.
|
||||||
{
|
return Redirect($"/api/thumbnails/{img.Id}");
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -78,7 +73,7 @@ public class CrudThumbsApi<T>(IRepository<T> repository, IThumbnailsManager thum
|
|||||||
/// </response>
|
/// </response>
|
||||||
[HttpGet("{identifier:id}/poster")]
|
[HttpGet("{identifier:id}/poster")]
|
||||||
[PartialPermission(Kind.Read)]
|
[PartialPermission(Kind.Read)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status302Found)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public Task<IActionResult> GetPoster(Identifier identifier, [FromQuery] ImageQuality? quality)
|
public Task<IActionResult> GetPoster(Identifier identifier, [FromQuery] ImageQuality? quality)
|
||||||
{
|
{
|
||||||
@ -99,7 +94,7 @@ public class CrudThumbsApi<T>(IRepository<T> repository, IThumbnailsManager thum
|
|||||||
/// </response>
|
/// </response>
|
||||||
[HttpGet("{identifier:id}/logo")]
|
[HttpGet("{identifier:id}/logo")]
|
||||||
[PartialPermission(Kind.Read)]
|
[PartialPermission(Kind.Read)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status302Found)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public Task<IActionResult> GetLogo(Identifier identifier, [FromQuery] ImageQuality? quality)
|
public Task<IActionResult> GetLogo(Identifier identifier, [FromQuery] ImageQuality? quality)
|
||||||
{
|
{
|
||||||
@ -120,6 +115,8 @@ public class CrudThumbsApi<T>(IRepository<T> repository, IThumbnailsManager thum
|
|||||||
/// </response>
|
/// </response>
|
||||||
[HttpGet("{identifier:id}/thumbnail")]
|
[HttpGet("{identifier:id}/thumbnail")]
|
||||||
[HttpGet("{identifier:id}/backdrop", Order = AlternativeRoute)]
|
[HttpGet("{identifier:id}/backdrop", Order = AlternativeRoute)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status302Found)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public Task<IActionResult> GetBackdrop(Identifier identifier, [FromQuery] ImageQuality? quality)
|
public Task<IActionResult> GetBackdrop(Identifier identifier, [FromQuery] ImageQuality? quality)
|
||||||
{
|
{
|
||||||
return _GetImage(identifier, "thumbnail", quality);
|
return _GetImage(identifier, "thumbnail", quality);
|
||||||
|
@ -35,8 +35,7 @@ public static class Transcoder
|
|||||||
Environment.GetEnvironmentVariable("TRANSCODER_URL") ?? "http://transcoder:7666";
|
Environment.GetEnvironmentVariable("TRANSCODER_URL") ?? "http://transcoder:7666";
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class TranscoderApi<T>(IRepository<T> repository, IThumbnailsManager thumbs)
|
public abstract class TranscoderApi<T>(IRepository<T> repository) : CrudThumbsApi<T>(repository)
|
||||||
: CrudThumbsApi<T>(repository, thumbs)
|
|
||||||
where T : class, IResource, IThumbnails, IQuery
|
where T : class, IResource, IThumbnails, IQuery
|
||||||
{
|
{
|
||||||
private Task _Proxy(string route, (string path, string route) info)
|
private Task _Proxy(string route, (string path, string route) info)
|
||||||
|
@ -40,25 +40,13 @@ namespace Kyoo.Core.Api;
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
[PartialPermission(nameof(Collection))]
|
[PartialPermission(nameof(Collection))]
|
||||||
[ApiDefinition("Collections", Group = ResourcesGroup)]
|
[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>
|
/// <summary>
|
||||||
/// Add a movie
|
/// Add a movie
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -79,14 +67,14 @@ public class CollectionApi : CrudThumbsApi<Collection>
|
|||||||
public async Task<ActionResult> AddMovie(Identifier identifier, Identifier movie)
|
public async Task<ActionResult> AddMovie(Identifier identifier, Identifier movie)
|
||||||
{
|
{
|
||||||
Guid collectionId = await identifier.Match(
|
Guid collectionId = await identifier.Match(
|
||||||
async id => (await _libraryManager.Collections.Get(id)).Id,
|
async id => (await collections.Get(id)).Id,
|
||||||
async slug => (await _libraryManager.Collections.Get(slug)).Id
|
async slug => (await collections.Get(slug)).Id
|
||||||
);
|
);
|
||||||
Guid movieId = await movie.Match(
|
Guid movieId = await movie.Match(
|
||||||
async id => (await _libraryManager.Movies.Get(id)).Id,
|
async id => (await movies.Get(id)).Id,
|
||||||
async slug => (await _libraryManager.Movies.Get(slug)).Id
|
async slug => (await movies.Get(slug)).Id
|
||||||
);
|
);
|
||||||
await _collections.AddMovie(collectionId, movieId);
|
await collections.AddMovie(collectionId, movieId);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,14 +98,14 @@ public class CollectionApi : CrudThumbsApi<Collection>
|
|||||||
public async Task<ActionResult> AddShow(Identifier identifier, Identifier show)
|
public async Task<ActionResult> AddShow(Identifier identifier, Identifier show)
|
||||||
{
|
{
|
||||||
Guid collectionId = await identifier.Match(
|
Guid collectionId = await identifier.Match(
|
||||||
async id => (await _libraryManager.Collections.Get(id)).Id,
|
async id => (await collections.Get(id)).Id,
|
||||||
async slug => (await _libraryManager.Collections.Get(slug)).Id
|
async slug => (await collections.Get(slug)).Id
|
||||||
);
|
);
|
||||||
Guid showId = await show.Match(
|
Guid showId = await show.Match(
|
||||||
async id => (await _libraryManager.Shows.Get(id)).Id,
|
async id => (await shows.Get(id)).Id,
|
||||||
async slug => (await _libraryManager.Shows.Get(slug)).Id
|
async slug => (await shows.Get(slug)).Id
|
||||||
);
|
);
|
||||||
await _collections.AddShow(collectionId, showId);
|
await collections.AddShow(collectionId, showId);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,9 +139,9 @@ public class CollectionApi : CrudThumbsApi<Collection>
|
|||||||
{
|
{
|
||||||
Guid collectionId = await identifier.Match(
|
Guid collectionId = await identifier.Match(
|
||||||
id => Task.FromResult(id),
|
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,
|
collectionId,
|
||||||
filter,
|
filter,
|
||||||
sortBy == new Sort<ILibraryItem>.Default()
|
sortBy == new Sort<ILibraryItem>.Default()
|
||||||
@ -165,8 +153,7 @@ public class CollectionApi : CrudThumbsApi<Collection>
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!resources.Any()
|
!resources.Any()
|
||||||
&& await _libraryManager.Collections.GetOrDefault(identifier.IsSame<Collection>())
|
&& await collections.GetOrDefault(identifier.IsSame<Collection>()) == null
|
||||||
== null
|
|
||||||
)
|
)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
return Page(resources, pagination.Limit);
|
return Page(resources, pagination.Limit);
|
||||||
@ -200,7 +187,7 @@ public class CollectionApi : CrudThumbsApi<Collection>
|
|||||||
[FromQuery] Include<Show>? fields
|
[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)),
|
Filter.And(filter, identifier.IsContainedIn<Show, Collection>(x => x.Collections)),
|
||||||
sortBy == new Sort<Show>.Default() ? new Sort<Show>.By(x => x.AirDate) : sortBy,
|
sortBy == new Sort<Show>.Default() ? new Sort<Show>.By(x => x.AirDate) : sortBy,
|
||||||
fields,
|
fields,
|
||||||
@ -209,8 +196,7 @@ public class CollectionApi : CrudThumbsApi<Collection>
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!resources.Any()
|
!resources.Any()
|
||||||
&& await _libraryManager.Collections.GetOrDefault(identifier.IsSame<Collection>())
|
&& await collections.GetOrDefault(identifier.IsSame<Collection>()) == null
|
||||||
== null
|
|
||||||
)
|
)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
return Page(resources, pagination.Limit);
|
return Page(resources, pagination.Limit);
|
||||||
@ -244,7 +230,7 @@ public class CollectionApi : CrudThumbsApi<Collection>
|
|||||||
[FromQuery] Include<Movie>? fields
|
[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)),
|
Filter.And(filter, identifier.IsContainedIn<Movie, Collection>(x => x.Collections)),
|
||||||
sortBy == new Sort<Movie>.Default() ? new Sort<Movie>.By(x => x.AirDate) : sortBy,
|
sortBy == new Sort<Movie>.Default() ? new Sort<Movie>.By(x => x.AirDate) : sortBy,
|
||||||
fields,
|
fields,
|
||||||
@ -253,8 +239,7 @@ public class CollectionApi : CrudThumbsApi<Collection>
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!resources.Any()
|
!resources.Any()
|
||||||
&& await _libraryManager.Collections.GetOrDefault(identifier.IsSame<Collection>())
|
&& await collections.GetOrDefault(identifier.IsSame<Collection>()) == null
|
||||||
== null
|
|
||||||
)
|
)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
return Page(resources, pagination.Limit);
|
return Page(resources, pagination.Limit);
|
||||||
|
@ -38,8 +38,8 @@ namespace Kyoo.Core.Api;
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
[PartialPermission(nameof(Episode))]
|
[PartialPermission(nameof(Episode))]
|
||||||
[ApiDefinition("Episodes", Group = ResourcesGroup)]
|
[ApiDefinition("Episodes", Group = ResourcesGroup)]
|
||||||
public class EpisodeApi(ILibraryManager libraryManager, IThumbnailsManager thumbnails)
|
public class EpisodeApi(ILibraryManager libraryManager)
|
||||||
: TranscoderApi<Episode>(libraryManager.Episodes, thumbnails)
|
: TranscoderApi<Episode>(libraryManager.Episodes)
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get episode's show
|
/// Get episode's show
|
||||||
|
@ -34,23 +34,5 @@ namespace Kyoo.Core.Api;
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
[PartialPermission("LibraryItem")]
|
[PartialPermission("LibraryItem")]
|
||||||
[ApiDefinition("Items", Group = ResourcesGroup)]
|
[ApiDefinition("Items", Group = ResourcesGroup)]
|
||||||
public class LibraryItemApi : CrudThumbsApi<ILibraryItem>
|
public class LibraryItemApi(IRepository<ILibraryItem> libraryItems)
|
||||||
{
|
: CrudThumbsApi<ILibraryItem>(libraryItems) { }
|
||||||
/// <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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -40,8 +40,7 @@ namespace Kyoo.Core.Api;
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
[PartialPermission(nameof(Show))]
|
[PartialPermission(nameof(Show))]
|
||||||
[ApiDefinition("Shows", Group = ResourcesGroup)]
|
[ApiDefinition("Shows", Group = ResourcesGroup)]
|
||||||
public class MovieApi(ILibraryManager libraryManager, IThumbnailsManager thumbs)
|
public class MovieApi(ILibraryManager libraryManager) : TranscoderApi<Movie>(libraryManager.Movies)
|
||||||
: TranscoderApi<Movie>(libraryManager.Movies, thumbs)
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get studio that made the show
|
/// Get studio that made the show
|
||||||
|
@ -33,8 +33,4 @@ namespace Kyoo.Core.Api;
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
[PartialPermission("LibraryItem")]
|
[PartialPermission("LibraryItem")]
|
||||||
[ApiDefinition("News", Group = ResourcesGroup)]
|
[ApiDefinition("News", Group = ResourcesGroup)]
|
||||||
public class NewsApi : CrudThumbsApi<INews>
|
public class NewsApi(IRepository<INews> news) : CrudThumbsApi<INews>(news) { }
|
||||||
{
|
|
||||||
public NewsApi(IRepository<INews> news, IThumbnailsManager thumbs)
|
|
||||||
: base(news, thumbs) { }
|
|
||||||
}
|
|
||||||
|
@ -38,26 +38,9 @@ namespace Kyoo.Core.Api;
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
[PartialPermission(nameof(Season))]
|
[PartialPermission(nameof(Season))]
|
||||||
[ApiDefinition("Seasons", Group = ResourcesGroup)]
|
[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>
|
/// <summary>
|
||||||
/// Get episodes in the season
|
/// Get episodes in the season
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -86,7 +69,7 @@ public class SeasonApi : CrudThumbsApi<Season>
|
|||||||
[FromQuery] Include<Episode> fields
|
[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)),
|
Filter.And(filter, identifier.Matcher<Episode>(x => x.SeasonId, x => x.Season!.Slug)),
|
||||||
sortBy,
|
sortBy,
|
||||||
fields,
|
fields,
|
||||||
@ -95,7 +78,7 @@ public class SeasonApi : CrudThumbsApi<Season>
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!resources.Any()
|
!resources.Any()
|
||||||
&& await _libraryManager.Seasons.GetOrDefault(identifier.IsSame<Season>()) == null
|
&& await libraryManager.Seasons.GetOrDefault(identifier.IsSame<Season>()) == null
|
||||||
)
|
)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
return Page(resources, pagination.Limit);
|
return Page(resources, pagination.Limit);
|
||||||
@ -120,7 +103,7 @@ public class SeasonApi : CrudThumbsApi<Season>
|
|||||||
[FromQuery] Include<Show> fields
|
[FromQuery] Include<Show> fields
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Show? ret = await _libraryManager.Shows.GetOrDefault(
|
Show? ret = await libraryManager.Shows.GetOrDefault(
|
||||||
identifier.IsContainedIn<Show, Season>(x => x.Seasons!),
|
identifier.IsContainedIn<Show, Season>(x => x.Seasons!),
|
||||||
fields
|
fields
|
||||||
);
|
);
|
||||||
|
@ -40,26 +40,8 @@ namespace Kyoo.Core.Api;
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
[PartialPermission(nameof(Show))]
|
[PartialPermission(nameof(Show))]
|
||||||
[ApiDefinition("Shows", Group = ResourcesGroup)]
|
[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>
|
/// <summary>
|
||||||
/// Get seasons of this show
|
/// Get seasons of this show
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -88,7 +70,7 @@ public class ShowApi : CrudThumbsApi<Show>
|
|||||||
[FromQuery] Include<Season> fields
|
[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)),
|
Filter.And(filter, identifier.Matcher<Season>(x => x.ShowId, x => x.Show!.Slug)),
|
||||||
sortBy,
|
sortBy,
|
||||||
fields,
|
fields,
|
||||||
@ -97,7 +79,7 @@ public class ShowApi : CrudThumbsApi<Show>
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!resources.Any()
|
!resources.Any()
|
||||||
&& await _libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null
|
&& await libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null
|
||||||
)
|
)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
return Page(resources, pagination.Limit);
|
return Page(resources, pagination.Limit);
|
||||||
@ -131,7 +113,7 @@ public class ShowApi : CrudThumbsApi<Show>
|
|||||||
[FromQuery] Include<Episode> fields
|
[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)),
|
Filter.And(filter, identifier.Matcher<Episode>(x => x.ShowId, x => x.Show!.Slug)),
|
||||||
sortBy,
|
sortBy,
|
||||||
fields,
|
fields,
|
||||||
@ -140,7 +122,7 @@ public class ShowApi : CrudThumbsApi<Show>
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!resources.Any()
|
!resources.Any()
|
||||||
&& await _libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null
|
&& await libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null
|
||||||
)
|
)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
return Page(resources, pagination.Limit);
|
return Page(resources, pagination.Limit);
|
||||||
@ -165,7 +147,7 @@ public class ShowApi : CrudThumbsApi<Show>
|
|||||||
[FromQuery] Include<Studio> fields
|
[FromQuery] Include<Studio> fields
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
return await _libraryManager.Studios.Get(
|
return await libraryManager.Studios.Get(
|
||||||
identifier.IsContainedIn<Studio, Show>(x => x.Shows!),
|
identifier.IsContainedIn<Studio, Show>(x => x.Shows!),
|
||||||
fields
|
fields
|
||||||
);
|
);
|
||||||
@ -199,7 +181,7 @@ public class ShowApi : CrudThumbsApi<Show>
|
|||||||
[FromQuery] Include<Collection> fields
|
[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!)),
|
Filter.And(filter, identifier.IsContainedIn<Collection, Show>(x => x.Shows!)),
|
||||||
sortBy,
|
sortBy,
|
||||||
fields,
|
fields,
|
||||||
@ -208,7 +190,7 @@ public class ShowApi : CrudThumbsApi<Show>
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!resources.Any()
|
!resources.Any()
|
||||||
&& await _libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null
|
&& await libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null
|
||||||
)
|
)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
return Page(resources, pagination.Limit);
|
return Page(resources, pagination.Limit);
|
||||||
@ -233,9 +215,9 @@ public class ShowApi : CrudThumbsApi<Show>
|
|||||||
{
|
{
|
||||||
Guid id = await identifier.Match(
|
Guid id = await identifier.Match(
|
||||||
id => Task.FromResult(id),
|
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>
|
/// <summary>
|
||||||
@ -260,9 +242,9 @@ public class ShowApi : CrudThumbsApi<Show>
|
|||||||
{
|
{
|
||||||
Guid id = await identifier.Match(
|
Guid id = await identifier.Match(
|
||||||
id => Task.FromResult(id),
|
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>
|
/// <summary>
|
||||||
@ -283,8 +265,8 @@ public class ShowApi : CrudThumbsApi<Show>
|
|||||||
{
|
{
|
||||||
Guid id = await identifier.Match(
|
Guid id = await identifier.Match(
|
||||||
id => Task.FromResult(id),
|
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
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
// 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.Controllers;
|
||||||
using Kyoo.Abstractions.Models;
|
using Kyoo.Abstractions.Models;
|
||||||
using Meilisearch;
|
using Meilisearch;
|
||||||
|
@ -23,7 +23,6 @@ using System.Linq.Expressions;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Kyoo.Abstractions.Controllers;
|
|
||||||
using Kyoo.Abstractions.Models;
|
using Kyoo.Abstractions.Models;
|
||||||
using Kyoo.Abstractions.Models.Exceptions;
|
using Kyoo.Abstractions.Models.Exceptions;
|
||||||
using Kyoo.Authentication;
|
using Kyoo.Authentication;
|
||||||
@ -33,13 +32,6 @@ using Microsoft.EntityFrameworkCore.ChangeTracking;
|
|||||||
|
|
||||||
namespace Kyoo.Postgresql;
|
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
|
public abstract class DatabaseContext : DbContext
|
||||||
{
|
{
|
||||||
private readonly IHttpContextAccessor _accessor;
|
private readonly IHttpContextAccessor _accessor;
|
||||||
@ -53,39 +45,18 @@ public abstract class DatabaseContext : DbContext
|
|||||||
|
|
||||||
public Guid? CurrentUserId => _accessor.HttpContext?.User.GetId();
|
public Guid? CurrentUserId => _accessor.HttpContext?.User.GetId();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// All collections of Kyoo. See <see cref="Collection"/>.
|
|
||||||
/// </summary>
|
|
||||||
public DbSet<Collection> Collections { get; set; }
|
public DbSet<Collection> Collections { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// All movies of Kyoo. See <see cref="Movie"/>.
|
|
||||||
/// </summary>
|
|
||||||
public DbSet<Movie> Movies { get; set; }
|
public DbSet<Movie> Movies { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// All shows of Kyoo. See <see cref="Show"/>.
|
|
||||||
/// </summary>
|
|
||||||
public DbSet<Show> Shows { get; set; }
|
public DbSet<Show> Shows { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// All seasons of Kyoo. See <see cref="Season"/>.
|
|
||||||
/// </summary>
|
|
||||||
public DbSet<Season> Seasons { get; set; }
|
public DbSet<Season> Seasons { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// All episodes of Kyoo. See <see cref="Episode"/>.
|
|
||||||
/// </summary>
|
|
||||||
public DbSet<Episode> Episodes { get; set; }
|
public DbSet<Episode> Episodes { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// All studios of Kyoo. See <see cref="Studio"/>.
|
|
||||||
/// </summary>
|
|
||||||
public DbSet<Studio> Studios { get; set; }
|
public DbSet<Studio> Studios { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The list of registered users.
|
|
||||||
/// </summary>
|
|
||||||
public DbSet<User> Users { get; set; }
|
public DbSet<User> Users { get; set; }
|
||||||
|
|
||||||
public DbSet<MovieWatchStatus> MovieWatchStatus { get; set; }
|
public DbSet<MovieWatchStatus> MovieWatchStatus { get; set; }
|
||||||
@ -129,28 +100,13 @@ public abstract class DatabaseContext : DbContext
|
|||||||
_accessor = accessor;
|
_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>()
|
protected abstract string LinkName<T, T2>()
|
||||||
where T : IResource
|
where T : IResource
|
||||||
where T2 : 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>()
|
protected abstract string LinkNameFk<T>()
|
||||||
where T : IResource;
|
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)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
base.OnConfiguring(optionsBuilder);
|
base.OnConfiguring(optionsBuilder);
|
||||||
@ -201,9 +157,9 @@ public abstract class DatabaseContext : DbContext
|
|||||||
private static void _HasImages<T>(ModelBuilder modelBuilder)
|
private static void _HasImages<T>(ModelBuilder modelBuilder)
|
||||||
where T : class, IThumbnails
|
where T : class, IThumbnails
|
||||||
{
|
{
|
||||||
modelBuilder.Entity<T>().OwnsOne(x => x.Poster);
|
modelBuilder.Entity<T>().OwnsOne(x => x.Poster, x => x.ToJson());
|
||||||
modelBuilder.Entity<T>().OwnsOne(x => x.Thumbnail);
|
modelBuilder.Entity<T>().OwnsOne(x => x.Thumbnail, x => x.ToJson());
|
||||||
modelBuilder.Entity<T>().OwnsOne(x => x.Logo);
|
modelBuilder.Entity<T>().OwnsOne(x => x.Logo, x => x.ToJson());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void _HasAddedDate<T>(ModelBuilder modelBuilder)
|
private static void _HasAddedDate<T>(ModelBuilder modelBuilder)
|
||||||
@ -227,15 +183,6 @@ public abstract class DatabaseContext : DbContext
|
|||||||
.ValueGeneratedOnAdd();
|
.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>(
|
private void _HasManyToMany<T, T2>(
|
||||||
ModelBuilder modelBuilder,
|
ModelBuilder modelBuilder,
|
||||||
Expression<Func<T, IEnumerable<T2>?>> firstNavigation,
|
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)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
@ -412,28 +355,6 @@ public abstract class DatabaseContext : DbContext
|
|||||||
_HasJson<Issue, object>(modelBuilder, x => x.Extra);
|
_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()
|
public override int SaveChanges()
|
||||||
{
|
{
|
||||||
try
|
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)
|
public override int SaveChanges(bool acceptAllChangesOnSuccess)
|
||||||
{
|
{
|
||||||
try
|
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(
|
public override async Task<int> SaveChangesAsync(
|
||||||
bool acceptAllChangesOnSuccess,
|
bool acceptAllChangesOnSuccess,
|
||||||
CancellationToken cancellationToken = default
|
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)
|
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
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>(
|
public async Task<int> SaveChangesAsync<T>(
|
||||||
Func<Task<T>> getExisting,
|
Func<Task<T>> getExisting,
|
||||||
CancellationToken cancellationToken = default
|
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)
|
public async Task<int> SaveIfNoDuplicates(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
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)
|
public T? LocalEntity<T>(string slug)
|
||||||
where T : class, IResource
|
where T : class, IResource
|
||||||
{
|
{
|
||||||
return ChangeTracker.Entries<T>().FirstOrDefault(x => x.Entity.Slug == slug)?.Entity;
|
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);
|
protected abstract bool IsDuplicateException(Exception ex);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Delete every changes that are on this context.
|
|
||||||
/// </summary>
|
|
||||||
public void DiscardChanges()
|
public void DiscardChanges()
|
||||||
{
|
{
|
||||||
foreach (
|
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 =>
|
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 =>
|
||||||
{
|
{
|
||||||
b1.Property<Guid>("CollectionId")
|
b1.Property<Guid>("CollectionId")
|
||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid");
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b1.Property<string>("Blurhash")
|
b1.Property<string>("Blurhash")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(32)
|
.HasMaxLength(32)
|
||||||
.HasColumnType("character varying(32)")
|
.HasColumnType("character varying(32)");
|
||||||
.HasColumnName("logo_blurhash");
|
|
||||||
|
b1.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b1.Property<string>("Source")
|
b1.Property<string>("Source")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text")
|
.HasColumnType("text");
|
||||||
.HasColumnName("logo_source");
|
|
||||||
|
|
||||||
b1.HasKey("CollectionId");
|
b1.HasKey("CollectionId");
|
||||||
|
|
||||||
b1.ToTable("collections");
|
b1.ToTable("collections");
|
||||||
|
|
||||||
|
b1.ToJson("logo");
|
||||||
|
|
||||||
b1.WithOwner()
|
b1.WithOwner()
|
||||||
.HasForeignKey("CollectionId")
|
.HasForeignKey("CollectionId")
|
||||||
.HasConstraintName("fk_collections_collections_id");
|
.HasConstraintName("fk_collections_collections_id");
|
||||||
@ -757,50 +759,55 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 =>
|
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 =>
|
||||||
{
|
{
|
||||||
b1.Property<Guid>("CollectionId")
|
b1.Property<Guid>("CollectionId")
|
||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid");
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b1.Property<string>("Blurhash")
|
b1.Property<string>("Blurhash")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(32)
|
.HasMaxLength(32)
|
||||||
.HasColumnType("character varying(32)")
|
.HasColumnType("character varying(32)");
|
||||||
.HasColumnName("poster_blurhash");
|
|
||||||
|
b1.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b1.Property<string>("Source")
|
b1.Property<string>("Source")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text")
|
.HasColumnType("text");
|
||||||
.HasColumnName("poster_source");
|
|
||||||
|
|
||||||
b1.HasKey("CollectionId");
|
b1.HasKey("CollectionId")
|
||||||
|
.HasName("pk_collections");
|
||||||
|
|
||||||
b1.ToTable("collections");
|
b1.ToTable("collections");
|
||||||
|
|
||||||
|
b1.ToJson("poster");
|
||||||
|
|
||||||
b1.WithOwner()
|
b1.WithOwner()
|
||||||
.HasForeignKey("CollectionId")
|
.HasForeignKey("CollectionId")
|
||||||
.HasConstraintName("fk_collections_collections_id");
|
.HasConstraintName("fk_collections_collections_collection_id");
|
||||||
});
|
});
|
||||||
|
|
||||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 =>
|
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 =>
|
||||||
{
|
{
|
||||||
b1.Property<Guid>("CollectionId")
|
b1.Property<Guid>("CollectionId")
|
||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid");
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b1.Property<string>("Blurhash")
|
b1.Property<string>("Blurhash")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(32)
|
.HasMaxLength(32)
|
||||||
.HasColumnType("character varying(32)")
|
.HasColumnType("character varying(32)");
|
||||||
.HasColumnName("thumbnail_blurhash");
|
|
||||||
|
b1.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b1.Property<string>("Source")
|
b1.Property<string>("Source")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text")
|
.HasColumnType("text");
|
||||||
.HasColumnName("thumbnail_source");
|
|
||||||
|
|
||||||
b1.HasKey("CollectionId");
|
b1.HasKey("CollectionId");
|
||||||
|
|
||||||
b1.ToTable("collections");
|
b1.ToTable("collections");
|
||||||
|
|
||||||
|
b1.ToJson("thumbnail");
|
||||||
|
|
||||||
b1.WithOwner()
|
b1.WithOwner()
|
||||||
.HasForeignKey("CollectionId")
|
.HasForeignKey("CollectionId")
|
||||||
.HasConstraintName("fk_collections_collections_id");
|
.HasConstraintName("fk_collections_collections_id");
|
||||||
@ -831,24 +838,26 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 =>
|
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 =>
|
||||||
{
|
{
|
||||||
b1.Property<Guid>("EpisodeId")
|
b1.Property<Guid>("EpisodeId")
|
||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid");
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b1.Property<string>("Blurhash")
|
b1.Property<string>("Blurhash")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(32)
|
.HasMaxLength(32)
|
||||||
.HasColumnType("character varying(32)")
|
.HasColumnType("character varying(32)");
|
||||||
.HasColumnName("logo_blurhash");
|
|
||||||
|
b1.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b1.Property<string>("Source")
|
b1.Property<string>("Source")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text")
|
.HasColumnType("text");
|
||||||
.HasColumnName("logo_source");
|
|
||||||
|
|
||||||
b1.HasKey("EpisodeId");
|
b1.HasKey("EpisodeId");
|
||||||
|
|
||||||
b1.ToTable("episodes");
|
b1.ToTable("episodes");
|
||||||
|
|
||||||
|
b1.ToJson("logo");
|
||||||
|
|
||||||
b1.WithOwner()
|
b1.WithOwner()
|
||||||
.HasForeignKey("EpisodeId")
|
.HasForeignKey("EpisodeId")
|
||||||
.HasConstraintName("fk_episodes_episodes_id");
|
.HasConstraintName("fk_episodes_episodes_id");
|
||||||
@ -857,24 +866,26 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 =>
|
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 =>
|
||||||
{
|
{
|
||||||
b1.Property<Guid>("EpisodeId")
|
b1.Property<Guid>("EpisodeId")
|
||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid");
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b1.Property<string>("Blurhash")
|
b1.Property<string>("Blurhash")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(32)
|
.HasMaxLength(32)
|
||||||
.HasColumnType("character varying(32)")
|
.HasColumnType("character varying(32)");
|
||||||
.HasColumnName("poster_blurhash");
|
|
||||||
|
b1.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b1.Property<string>("Source")
|
b1.Property<string>("Source")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text")
|
.HasColumnType("text");
|
||||||
.HasColumnName("poster_source");
|
|
||||||
|
|
||||||
b1.HasKey("EpisodeId");
|
b1.HasKey("EpisodeId");
|
||||||
|
|
||||||
b1.ToTable("episodes");
|
b1.ToTable("episodes");
|
||||||
|
|
||||||
|
b1.ToJson("poster");
|
||||||
|
|
||||||
b1.WithOwner()
|
b1.WithOwner()
|
||||||
.HasForeignKey("EpisodeId")
|
.HasForeignKey("EpisodeId")
|
||||||
.HasConstraintName("fk_episodes_episodes_id");
|
.HasConstraintName("fk_episodes_episodes_id");
|
||||||
@ -883,24 +894,26 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 =>
|
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 =>
|
||||||
{
|
{
|
||||||
b1.Property<Guid>("EpisodeId")
|
b1.Property<Guid>("EpisodeId")
|
||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid");
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b1.Property<string>("Blurhash")
|
b1.Property<string>("Blurhash")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(32)
|
.HasMaxLength(32)
|
||||||
.HasColumnType("character varying(32)")
|
.HasColumnType("character varying(32)");
|
||||||
.HasColumnName("thumbnail_blurhash");
|
|
||||||
|
b1.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b1.Property<string>("Source")
|
b1.Property<string>("Source")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text")
|
.HasColumnType("text");
|
||||||
.HasColumnName("thumbnail_source");
|
|
||||||
|
|
||||||
b1.HasKey("EpisodeId");
|
b1.HasKey("EpisodeId");
|
||||||
|
|
||||||
b1.ToTable("episodes");
|
b1.ToTable("episodes");
|
||||||
|
|
||||||
|
b1.ToJson("thumbnail");
|
||||||
|
|
||||||
b1.WithOwner()
|
b1.WithOwner()
|
||||||
.HasForeignKey("EpisodeId")
|
.HasForeignKey("EpisodeId")
|
||||||
.HasConstraintName("fk_episodes_episodes_id");
|
.HasConstraintName("fk_episodes_episodes_id");
|
||||||
@ -949,24 +962,26 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 =>
|
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 =>
|
||||||
{
|
{
|
||||||
b1.Property<Guid>("MovieId")
|
b1.Property<Guid>("MovieId")
|
||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid");
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b1.Property<string>("Blurhash")
|
b1.Property<string>("Blurhash")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(32)
|
.HasMaxLength(32)
|
||||||
.HasColumnType("character varying(32)")
|
.HasColumnType("character varying(32)");
|
||||||
.HasColumnName("logo_blurhash");
|
|
||||||
|
b1.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b1.Property<string>("Source")
|
b1.Property<string>("Source")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text")
|
.HasColumnType("text");
|
||||||
.HasColumnName("logo_source");
|
|
||||||
|
|
||||||
b1.HasKey("MovieId");
|
b1.HasKey("MovieId");
|
||||||
|
|
||||||
b1.ToTable("movies");
|
b1.ToTable("movies");
|
||||||
|
|
||||||
|
b1.ToJson("logo");
|
||||||
|
|
||||||
b1.WithOwner()
|
b1.WithOwner()
|
||||||
.HasForeignKey("MovieId")
|
.HasForeignKey("MovieId")
|
||||||
.HasConstraintName("fk_movies_movies_id");
|
.HasConstraintName("fk_movies_movies_id");
|
||||||
@ -975,24 +990,26 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 =>
|
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 =>
|
||||||
{
|
{
|
||||||
b1.Property<Guid>("MovieId")
|
b1.Property<Guid>("MovieId")
|
||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid");
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b1.Property<string>("Blurhash")
|
b1.Property<string>("Blurhash")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(32)
|
.HasMaxLength(32)
|
||||||
.HasColumnType("character varying(32)")
|
.HasColumnType("character varying(32)");
|
||||||
.HasColumnName("poster_blurhash");
|
|
||||||
|
b1.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b1.Property<string>("Source")
|
b1.Property<string>("Source")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text")
|
.HasColumnType("text");
|
||||||
.HasColumnName("poster_source");
|
|
||||||
|
|
||||||
b1.HasKey("MovieId");
|
b1.HasKey("MovieId");
|
||||||
|
|
||||||
b1.ToTable("movies");
|
b1.ToTable("movies");
|
||||||
|
|
||||||
|
b1.ToJson("poster");
|
||||||
|
|
||||||
b1.WithOwner()
|
b1.WithOwner()
|
||||||
.HasForeignKey("MovieId")
|
.HasForeignKey("MovieId")
|
||||||
.HasConstraintName("fk_movies_movies_id");
|
.HasConstraintName("fk_movies_movies_id");
|
||||||
@ -1001,24 +1018,26 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 =>
|
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 =>
|
||||||
{
|
{
|
||||||
b1.Property<Guid>("MovieId")
|
b1.Property<Guid>("MovieId")
|
||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid");
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b1.Property<string>("Blurhash")
|
b1.Property<string>("Blurhash")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(32)
|
.HasMaxLength(32)
|
||||||
.HasColumnType("character varying(32)")
|
.HasColumnType("character varying(32)");
|
||||||
.HasColumnName("thumbnail_blurhash");
|
|
||||||
|
b1.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b1.Property<string>("Source")
|
b1.Property<string>("Source")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text")
|
.HasColumnType("text");
|
||||||
.HasColumnName("thumbnail_source");
|
|
||||||
|
|
||||||
b1.HasKey("MovieId");
|
b1.HasKey("MovieId");
|
||||||
|
|
||||||
b1.ToTable("movies");
|
b1.ToTable("movies");
|
||||||
|
|
||||||
|
b1.ToJson("thumbnail");
|
||||||
|
|
||||||
b1.WithOwner()
|
b1.WithOwner()
|
||||||
.HasForeignKey("MovieId")
|
.HasForeignKey("MovieId")
|
||||||
.HasConstraintName("fk_movies_movies_id");
|
.HasConstraintName("fk_movies_movies_id");
|
||||||
@ -1066,24 +1085,26 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 =>
|
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 =>
|
||||||
{
|
{
|
||||||
b1.Property<Guid>("SeasonId")
|
b1.Property<Guid>("SeasonId")
|
||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid");
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b1.Property<string>("Blurhash")
|
b1.Property<string>("Blurhash")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(32)
|
.HasMaxLength(32)
|
||||||
.HasColumnType("character varying(32)")
|
.HasColumnType("character varying(32)");
|
||||||
.HasColumnName("logo_blurhash");
|
|
||||||
|
b1.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b1.Property<string>("Source")
|
b1.Property<string>("Source")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text")
|
.HasColumnType("text");
|
||||||
.HasColumnName("logo_source");
|
|
||||||
|
|
||||||
b1.HasKey("SeasonId");
|
b1.HasKey("SeasonId");
|
||||||
|
|
||||||
b1.ToTable("seasons");
|
b1.ToTable("seasons");
|
||||||
|
|
||||||
|
b1.ToJson("logo");
|
||||||
|
|
||||||
b1.WithOwner()
|
b1.WithOwner()
|
||||||
.HasForeignKey("SeasonId")
|
.HasForeignKey("SeasonId")
|
||||||
.HasConstraintName("fk_seasons_seasons_id");
|
.HasConstraintName("fk_seasons_seasons_id");
|
||||||
@ -1092,24 +1113,26 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 =>
|
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 =>
|
||||||
{
|
{
|
||||||
b1.Property<Guid>("SeasonId")
|
b1.Property<Guid>("SeasonId")
|
||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid");
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b1.Property<string>("Blurhash")
|
b1.Property<string>("Blurhash")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(32)
|
.HasMaxLength(32)
|
||||||
.HasColumnType("character varying(32)")
|
.HasColumnType("character varying(32)");
|
||||||
.HasColumnName("poster_blurhash");
|
|
||||||
|
b1.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b1.Property<string>("Source")
|
b1.Property<string>("Source")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text")
|
.HasColumnType("text");
|
||||||
.HasColumnName("poster_source");
|
|
||||||
|
|
||||||
b1.HasKey("SeasonId");
|
b1.HasKey("SeasonId");
|
||||||
|
|
||||||
b1.ToTable("seasons");
|
b1.ToTable("seasons");
|
||||||
|
|
||||||
|
b1.ToJson("poster");
|
||||||
|
|
||||||
b1.WithOwner()
|
b1.WithOwner()
|
||||||
.HasForeignKey("SeasonId")
|
.HasForeignKey("SeasonId")
|
||||||
.HasConstraintName("fk_seasons_seasons_id");
|
.HasConstraintName("fk_seasons_seasons_id");
|
||||||
@ -1118,24 +1141,26 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 =>
|
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 =>
|
||||||
{
|
{
|
||||||
b1.Property<Guid>("SeasonId")
|
b1.Property<Guid>("SeasonId")
|
||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid");
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b1.Property<string>("Blurhash")
|
b1.Property<string>("Blurhash")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(32)
|
.HasMaxLength(32)
|
||||||
.HasColumnType("character varying(32)")
|
.HasColumnType("character varying(32)");
|
||||||
.HasColumnName("thumbnail_blurhash");
|
|
||||||
|
b1.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b1.Property<string>("Source")
|
b1.Property<string>("Source")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text")
|
.HasColumnType("text");
|
||||||
.HasColumnName("thumbnail_source");
|
|
||||||
|
|
||||||
b1.HasKey("SeasonId");
|
b1.HasKey("SeasonId");
|
||||||
|
|
||||||
b1.ToTable("seasons");
|
b1.ToTable("seasons");
|
||||||
|
|
||||||
|
b1.ToJson("thumbnail");
|
||||||
|
|
||||||
b1.WithOwner()
|
b1.WithOwner()
|
||||||
.HasForeignKey("SeasonId")
|
.HasForeignKey("SeasonId")
|
||||||
.HasConstraintName("fk_seasons_seasons_id");
|
.HasConstraintName("fk_seasons_seasons_id");
|
||||||
@ -1161,24 +1186,26 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 =>
|
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 =>
|
||||||
{
|
{
|
||||||
b1.Property<Guid>("ShowId")
|
b1.Property<Guid>("ShowId")
|
||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid");
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b1.Property<string>("Blurhash")
|
b1.Property<string>("Blurhash")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(32)
|
.HasMaxLength(32)
|
||||||
.HasColumnType("character varying(32)")
|
.HasColumnType("character varying(32)");
|
||||||
.HasColumnName("logo_blurhash");
|
|
||||||
|
b1.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b1.Property<string>("Source")
|
b1.Property<string>("Source")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text")
|
.HasColumnType("text");
|
||||||
.HasColumnName("logo_source");
|
|
||||||
|
|
||||||
b1.HasKey("ShowId");
|
b1.HasKey("ShowId");
|
||||||
|
|
||||||
b1.ToTable("shows");
|
b1.ToTable("shows");
|
||||||
|
|
||||||
|
b1.ToJson("logo");
|
||||||
|
|
||||||
b1.WithOwner()
|
b1.WithOwner()
|
||||||
.HasForeignKey("ShowId")
|
.HasForeignKey("ShowId")
|
||||||
.HasConstraintName("fk_shows_shows_id");
|
.HasConstraintName("fk_shows_shows_id");
|
||||||
@ -1187,24 +1214,26 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 =>
|
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 =>
|
||||||
{
|
{
|
||||||
b1.Property<Guid>("ShowId")
|
b1.Property<Guid>("ShowId")
|
||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid");
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b1.Property<string>("Blurhash")
|
b1.Property<string>("Blurhash")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(32)
|
.HasMaxLength(32)
|
||||||
.HasColumnType("character varying(32)")
|
.HasColumnType("character varying(32)");
|
||||||
.HasColumnName("poster_blurhash");
|
|
||||||
|
b1.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b1.Property<string>("Source")
|
b1.Property<string>("Source")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text")
|
.HasColumnType("text");
|
||||||
.HasColumnName("poster_source");
|
|
||||||
|
|
||||||
b1.HasKey("ShowId");
|
b1.HasKey("ShowId");
|
||||||
|
|
||||||
b1.ToTable("shows");
|
b1.ToTable("shows");
|
||||||
|
|
||||||
|
b1.ToJson("poster");
|
||||||
|
|
||||||
b1.WithOwner()
|
b1.WithOwner()
|
||||||
.HasForeignKey("ShowId")
|
.HasForeignKey("ShowId")
|
||||||
.HasConstraintName("fk_shows_shows_id");
|
.HasConstraintName("fk_shows_shows_id");
|
||||||
@ -1213,24 +1242,26 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 =>
|
b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 =>
|
||||||
{
|
{
|
||||||
b1.Property<Guid>("ShowId")
|
b1.Property<Guid>("ShowId")
|
||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid");
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b1.Property<string>("Blurhash")
|
b1.Property<string>("Blurhash")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(32)
|
.HasMaxLength(32)
|
||||||
.HasColumnType("character varying(32)")
|
.HasColumnType("character varying(32)");
|
||||||
.HasColumnName("thumbnail_blurhash");
|
|
||||||
|
b1.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b1.Property<string>("Source")
|
b1.Property<string>("Source")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text")
|
.HasColumnType("text");
|
||||||
.HasColumnName("thumbnail_source");
|
|
||||||
|
|
||||||
b1.HasKey("ShowId");
|
b1.HasKey("ShowId");
|
||||||
|
|
||||||
b1.ToTable("shows");
|
b1.ToTable("shows");
|
||||||
|
|
||||||
|
b1.ToJson("thumbnail");
|
||||||
|
|
||||||
b1.WithOwner()
|
b1.WithOwner()
|
||||||
.HasForeignKey("ShowId")
|
.HasForeignKey("ShowId")
|
||||||
.HasConstraintName("fk_shows_shows_id");
|
.HasConstraintName("fk_shows_shows_id");
|
||||||
|
@ -94,6 +94,7 @@ public class PostgresContext(DbContextOptions options, IHttpContextAccessor acce
|
|||||||
typeof(Dictionary<string, ExternalToken>),
|
typeof(Dictionary<string, ExternalToken>),
|
||||||
new JsonTypeHandler<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<string>), new ListTypeHandler<string>());
|
||||||
SqlMapper.AddTypeHandler(typeof(List<Genre>), new ListTypeHandler<Genre>());
|
SqlMapper.AddTypeHandler(typeof(List<Genre>), new ListTypeHandler<Genre>());
|
||||||
SqlMapper.AddTypeHandler(typeof(Wrapper), new Wrapper.Handler());
|
SqlMapper.AddTypeHandler(typeof(Wrapper), new Wrapper.Handler());
|
||||||
|
@ -35,7 +35,7 @@ public static class RabbitMqModule
|
|||||||
UserName = builder.Configuration.GetValue("RABBITMQ_DEFAULT_USER", "guest"),
|
UserName = builder.Configuration.GetValue("RABBITMQ_DEFAULT_USER", "guest"),
|
||||||
Password = builder.Configuration.GetValue("RABBITMQ_DEFAULT_PASS", "guest"),
|
Password = builder.Configuration.GetValue("RABBITMQ_DEFAULT_PASS", "guest"),
|
||||||
HostName = builder.Configuration.GetValue("RABBITMQ_HOST", "rabbitmq"),
|
HostName = builder.Configuration.GetValue("RABBITMQ_HOST", "rabbitmq"),
|
||||||
Port = 5672,
|
Port = builder.Configuration.GetValue("RABBITMQ_Port", 5672),
|
||||||
};
|
};
|
||||||
|
|
||||||
return factory.CreateConnection();
|
return factory.CreateConnection();
|
||||||
|
@ -30,7 +30,7 @@ services:
|
|||||||
migrations:
|
migrations:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
volumes:
|
volumes:
|
||||||
- kyoo:/kyoo
|
- kyoo:/metadata
|
||||||
|
|
||||||
migrations:
|
migrations:
|
||||||
build:
|
build:
|
||||||
|
@ -42,7 +42,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./back:/app
|
- ./back:/app
|
||||||
- /app/out/
|
- /app/out/
|
||||||
- kyoo:/kyoo
|
- kyoo:/metadata
|
||||||
|
|
||||||
migrations:
|
migrations:
|
||||||
build:
|
build:
|
||||||
|
@ -31,7 +31,7 @@ services:
|
|||||||
migrations:
|
migrations:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
volumes:
|
volumes:
|
||||||
- kyoo:/kyoo
|
- kyoo:/metadata
|
||||||
|
|
||||||
migrations:
|
migrations:
|
||||||
image: zoriya/kyoo_migrations:latest
|
image: zoriya/kyoo_migrations:latest
|
||||||
|
@ -19,10 +19,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { withImages, ResourceP } from "../traits";
|
import { ImagesP, ResourceP } from "../traits";
|
||||||
|
|
||||||
export const CollectionP = withImages(
|
export const CollectionP = ResourceP("collection")
|
||||||
ResourceP("collection").extend({
|
.merge(ImagesP)
|
||||||
|
.extend({
|
||||||
/**
|
/**
|
||||||
* The title of this collection.
|
* The title of this collection.
|
||||||
*/
|
*/
|
||||||
@ -31,11 +32,11 @@ export const CollectionP = withImages(
|
|||||||
* The summary of this show.
|
* The summary of this show.
|
||||||
*/
|
*/
|
||||||
overview: z.string().nullable(),
|
overview: z.string().nullable(),
|
||||||
}),
|
})
|
||||||
).transform((x) => ({
|
.transform((x) => ({
|
||||||
...x,
|
...x,
|
||||||
href: `/collection/${x.slug}`,
|
href: `/collection/${x.slug}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class representing collections of show or movies.
|
* A class representing collections of show or movies.
|
||||||
|
@ -20,11 +20,12 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { zdate } from "../utils";
|
import { zdate } from "../utils";
|
||||||
import { withImages, imageFn } from "../traits";
|
import { ImagesP, imageFn } from "../traits";
|
||||||
import { ResourceP } from "../traits/resource";
|
import { ResourceP } from "../traits/resource";
|
||||||
|
|
||||||
export const BaseEpisodeP = withImages(
|
export const BaseEpisodeP = ResourceP("episode")
|
||||||
ResourceP("episode").extend({
|
.merge(ImagesP)
|
||||||
|
.extend({
|
||||||
/**
|
/**
|
||||||
* The season in witch this episode is in.
|
* The season in witch this episode is in.
|
||||||
*/
|
*/
|
||||||
@ -71,8 +72,7 @@ export const BaseEpisodeP = withImages(
|
|||||||
* The id of the show containing this episode
|
* The id of the show containing this episode
|
||||||
*/
|
*/
|
||||||
showId: z.string(),
|
showId: z.string(),
|
||||||
}),
|
})
|
||||||
)
|
|
||||||
.transform((x) => ({
|
.transform((x) => ({
|
||||||
...x,
|
...x,
|
||||||
runtime: x.runtime === 0 ? null : x.runtime,
|
runtime: x.runtime === 0 ? null : x.runtime,
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { zdate } from "../utils";
|
import { zdate } from "../utils";
|
||||||
import { withImages, ResourceP, imageFn } from "../traits";
|
import { ImagesP, ResourceP, imageFn } from "../traits";
|
||||||
import { Genre } from "./genre";
|
import { Genre } from "./genre";
|
||||||
import { StudioP } from "./studio";
|
import { StudioP } from "./studio";
|
||||||
import { Status } from "./show";
|
import { Status } from "./show";
|
||||||
@ -28,8 +28,9 @@ import { CollectionP } from "./collection";
|
|||||||
import { MetadataP } from "./metadata";
|
import { MetadataP } from "./metadata";
|
||||||
import { WatchStatusP } from "./watch-status";
|
import { WatchStatusP } from "./watch-status";
|
||||||
|
|
||||||
export const MovieP = withImages(
|
export const MovieP = ResourceP("movie")
|
||||||
ResourceP("movie").extend({
|
.merge(ImagesP)
|
||||||
|
.extend({
|
||||||
/**
|
/**
|
||||||
* The title of this movie.
|
* The title of this movie.
|
||||||
*/
|
*/
|
||||||
@ -104,8 +105,7 @@ export const MovieP = withImages(
|
|||||||
* Metadata of what an user as started/planned to watch.
|
* Metadata of what an user as started/planned to watch.
|
||||||
*/
|
*/
|
||||||
watchStatus: WatchStatusP.optional().nullable(),
|
watchStatus: WatchStatusP.optional().nullable(),
|
||||||
}),
|
})
|
||||||
)
|
|
||||||
.transform((x) => ({
|
.transform((x) => ({
|
||||||
...x,
|
...x,
|
||||||
runtime: x.runtime === 0 ? null : x.runtime,
|
runtime: x.runtime === 0 ? null : x.runtime,
|
||||||
|
@ -19,28 +19,25 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { withImages } from "../traits";
|
import { ImagesP, ResourceP } from "../traits";
|
||||||
import { ResourceP } from "../traits/resource";
|
|
||||||
|
|
||||||
export const PersonP = withImages(
|
export const PersonP = ResourceP("people").merge(ImagesP).extend({
|
||||||
ResourceP("people").extend({
|
/**
|
||||||
/**
|
* The name of this person.
|
||||||
* The name of this person.
|
*/
|
||||||
*/
|
name: z.string(),
|
||||||
name: z.string(),
|
/**
|
||||||
/**
|
* The type of work the person has done for the show. That can be something like "Actor",
|
||||||
* The type of work the person has done for the show. That can be something like "Actor",
|
* "Writer", "Music", "Voice Actor"...
|
||||||
* "Writer", "Music", "Voice Actor"...
|
*/
|
||||||
*/
|
type: z.string().optional(),
|
||||||
type: z.string().optional(),
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The role the People played. This is mostly used to inform witch character was played for actor
|
* The role the People played. This is mostly used to inform witch character was played for actor
|
||||||
* and voice actors.
|
* and voice actors.
|
||||||
*/
|
*/
|
||||||
role: z.string().optional(),
|
role: z.string().optional(),
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A studio that make shows.
|
* A studio that make shows.
|
||||||
|
@ -20,37 +20,34 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { zdate } from "../utils";
|
import { zdate } from "../utils";
|
||||||
import { withImages } from "../traits";
|
import { ImagesP, ResourceP } from "../traits";
|
||||||
import { ResourceP } from "../traits/resource";
|
|
||||||
|
|
||||||
export const SeasonP = withImages(
|
export const SeasonP = ResourceP("season").merge(ImagesP).extend({
|
||||||
ResourceP("season").extend({
|
/**
|
||||||
/**
|
* The name of this season.
|
||||||
* The name of this season.
|
*/
|
||||||
*/
|
name: z.string(),
|
||||||
name: z.string(),
|
/**
|
||||||
/**
|
* The number of this season. This can be set to 0 to indicate specials.
|
||||||
* The number of this season. This can be set to 0 to indicate specials.
|
*/
|
||||||
*/
|
seasonNumber: z.number(),
|
||||||
seasonNumber: z.number(),
|
/**
|
||||||
/**
|
* A quick overview of this season.
|
||||||
* A quick overview of this season.
|
*/
|
||||||
*/
|
overview: z.string().nullable(),
|
||||||
overview: z.string().nullable(),
|
/**
|
||||||
/**
|
* The starting air date of this season.
|
||||||
* The starting air date of this season.
|
*/
|
||||||
*/
|
startDate: zdate().nullable(),
|
||||||
startDate: zdate().nullable(),
|
/**
|
||||||
/**
|
* The ending date of this season.
|
||||||
* The ending date of this season.
|
*/
|
||||||
*/
|
endDate: zdate().nullable(),
|
||||||
endDate: zdate().nullable(),
|
/**
|
||||||
/**
|
* The number of episodes available on kyoo of this season.
|
||||||
* The number of episodes available on kyoo of this season.
|
*/
|
||||||
*/
|
episodesCount: z.number(),
|
||||||
episodesCount: z.number(),
|
});
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A season of a Show.
|
* A season of a Show.
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { zdate } from "../utils";
|
import { zdate } from "../utils";
|
||||||
import { withImages, ResourceP } from "../traits";
|
import { ImagesP, ResourceP } from "../traits";
|
||||||
import { Genre } from "./genre";
|
import { Genre } from "./genre";
|
||||||
import { StudioP } from "./studio";
|
import { StudioP } from "./studio";
|
||||||
import { BaseEpisodeP } from "./episode.base";
|
import { BaseEpisodeP } from "./episode.base";
|
||||||
@ -37,8 +37,9 @@ export enum Status {
|
|||||||
Planned = "Planned",
|
Planned = "Planned",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowP = withImages(
|
export const ShowP = ResourceP("show")
|
||||||
ResourceP("show").extend({
|
.merge(ImagesP)
|
||||||
|
.extend({
|
||||||
/**
|
/**
|
||||||
* The title of this show.
|
* The title of this show.
|
||||||
*/
|
*/
|
||||||
@ -103,8 +104,7 @@ export const ShowP = withImages(
|
|||||||
* The number of episodes in this show.
|
* The number of episodes in this show.
|
||||||
*/
|
*/
|
||||||
episodesCount: z.number().int().gte(0).optional(),
|
episodesCount: z.number().int().gte(0).optional(),
|
||||||
}),
|
})
|
||||||
)
|
|
||||||
.transform((x) => {
|
.transform((x) => {
|
||||||
if (!x.thumbnail && x.poster) {
|
if (!x.thumbnail && x.poster) {
|
||||||
x.thumbnail = { ...x.poster };
|
x.thumbnail = { ...x.poster };
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { ZodObject, ZodRawShape, z } from "zod";
|
import { z } from "zod";
|
||||||
import { lastUsedUrl } from "..";
|
import { lastUsedUrl } from "..";
|
||||||
|
|
||||||
export const imageFn = (url: string) =>
|
export const imageFn = (url: string) =>
|
||||||
@ -28,9 +28,12 @@ export const imageFn = (url: string) =>
|
|||||||
export const Img = z.object({
|
export const Img = z.object({
|
||||||
source: z.string(),
|
source: z.string(),
|
||||||
blurhash: 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
|
* 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
|
* 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(),
|
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.
|
* 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 "??:??";
|
return "??:??";
|
||||||
const h = Math.floor(timer / 3600);
|
const h = Math.floor(timer / 3600);
|
||||||
const min = Math.floor((timer / 60) % 60);
|
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");
|
const fmt = (n: number) => n.toString().padStart(2, "0");
|
||||||
|
|
||||||
if (duration >= 3600) return `${fmt(h)}:${fmt(min)}:${fmt(sec)}`;
|
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:
|
async with KyooClient() as kyoo, Subscriber() as sub:
|
||||||
provider = Provider.get_default(kyoo.client)
|
provider = Provider.get_default(kyoo.client)
|
||||||
scanner = Matcher(kyoo, provider)
|
matcher = Matcher(kyoo, provider)
|
||||||
await sub.listen(scanner)
|
await sub.listen(matcher)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from typing import Literal
|
||||||
import asyncio
|
import asyncio
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from providers.provider import Provider, ProviderError
|
from providers.provider import Provider, ProviderError
|
||||||
@ -165,3 +166,32 @@ class Matcher:
|
|||||||
return await self._client.post("seasons", data=season.to_kyoo())
|
return await self._client.post("seasons", data=season.to_kyoo())
|
||||||
|
|
||||||
return await create_season(show_id, season_number)
|
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
|
import asyncio
|
||||||
from dataclasses import dataclass
|
from typing import Union, Literal
|
||||||
from dataclasses_json import DataClassJsonMixin
|
from msgspec import Struct, json
|
||||||
from typing import Literal
|
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
from aio_pika import connect_robust
|
from aio_pika import connect_robust
|
||||||
@ -12,18 +11,33 @@ from matcher.matcher import Matcher
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
class Message(Struct, tag_field="action", tag=str.lower):
|
||||||
class Message(DataClassJsonMixin):
|
pass
|
||||||
action: Literal["scan", "delete"]
|
|
||||||
|
|
||||||
|
class Scan(Message):
|
||||||
path: str
|
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:
|
class Subscriber:
|
||||||
QUEUE = "scanner"
|
QUEUE = "scanner"
|
||||||
|
|
||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
self._con = await connect_robust(
|
self._con = await connect_robust(
|
||||||
host=os.environ.get("RABBITMQ_HOST", "rabbitmq"),
|
host=os.environ.get("RABBITMQ_HOST", "rabbitmq"),
|
||||||
|
port=int(os.environ.get("RABBITMQ_PORT", "5672")),
|
||||||
login=os.environ.get("RABBITMQ_DEFAULT_USER", "guest"),
|
login=os.environ.get("RABBITMQ_DEFAULT_USER", "guest"),
|
||||||
password=os.environ.get("RABBITMQ_DEFAULT_PASS", "guest"),
|
password=os.environ.get("RABBITMQ_DEFAULT_PASS", "guest"),
|
||||||
)
|
)
|
||||||
@ -36,18 +50,24 @@ class Subscriber:
|
|||||||
|
|
||||||
async def listen(self, scanner: Matcher):
|
async def listen(self, scanner: Matcher):
|
||||||
async def on_message(message: AbstractIncomingMessage):
|
async def on_message(message: AbstractIncomingMessage):
|
||||||
msg = Message.from_json(message.body)
|
try:
|
||||||
ack = False
|
msg = decoder.decode(message.body)
|
||||||
match msg.action:
|
ack = False
|
||||||
case "scan":
|
match msg:
|
||||||
ack = await scanner.identify(msg.path)
|
case Scan(path):
|
||||||
case "delete":
|
ack = await scanner.identify(path)
|
||||||
ack = await scanner.delete(msg.path)
|
case Delete(path):
|
||||||
case _:
|
ack = await scanner.delete(path)
|
||||||
logger.error(f"Invalid action: {msg.action}")
|
case Refresh(kind, id):
|
||||||
if ack:
|
ack = await scanner.refresh(kind, id)
|
||||||
await message.ack()
|
case _:
|
||||||
else:
|
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()
|
await message.reject()
|
||||||
|
|
||||||
# Allow up to 20 scan requests to run in parallel on the same listener.
|
# Allow up to 20 scan requests to run in parallel on the same listener.
|
||||||
|
@ -36,25 +36,12 @@ class KyooClient:
|
|||||||
await self.client.close()
|
await self.client.close()
|
||||||
|
|
||||||
async def get_registered_paths(self) -> List[str]:
|
async def get_registered_paths(self) -> List[str]:
|
||||||
paths = None
|
|
||||||
async with self.client.get(
|
async with self.client.get(
|
||||||
f"{self._url}/episodes",
|
f"{self._url}/paths",
|
||||||
params={"limit": 0},
|
|
||||||
headers={"X-API-Key": self._api_key},
|
headers={"X-API-Key": self._api_key},
|
||||||
) as r:
|
) as r:
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
ret = await r.json()
|
return 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
|
|
||||||
|
|
||||||
async def create_issue(self, path: str, issue: str, extra: dict | None = None):
|
async def create_issue(self, path: str, issue: str, extra: dict | None = None):
|
||||||
async with self.client.post(
|
async with self.client.post(
|
||||||
@ -112,20 +99,6 @@ class KyooClient:
|
|||||||
logger.error(f"Request error: {await r.text()}")
|
logger.error(f"Request error: {await r.text()}")
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
ret = await r.json()
|
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"]
|
return ret["id"]
|
||||||
|
|
||||||
async def delete(
|
async def delete(
|
||||||
@ -154,3 +127,35 @@ class KyooClient:
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
await self.delete_issue(path)
|
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
|
jsons
|
||||||
watchfiles
|
watchfiles
|
||||||
aio-pika
|
aio-pika
|
||||||
dataclasses-json
|
msgspec
|
||||||
|
@ -7,7 +7,7 @@ logger = getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
async def monitor(path: str, publisher: Publisher):
|
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:
|
for event, file in changes:
|
||||||
if event == Change.added:
|
if event == Change.added:
|
||||||
await publisher.add(file)
|
await publisher.add(file)
|
||||||
|
@ -9,6 +9,7 @@ class Publisher:
|
|||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
self._con = await connect_robust(
|
self._con = await connect_robust(
|
||||||
host=os.environ.get("RABBITMQ_HOST", "rabbitmq"),
|
host=os.environ.get("RABBITMQ_HOST", "rabbitmq"),
|
||||||
|
port=int(os.environ.get("RABBITMQ_PORT", "5672")),
|
||||||
login=os.environ.get("RABBITMQ_DEFAULT_USER", "guest"),
|
login=os.environ.get("RABBITMQ_DEFAULT_USER", "guest"),
|
||||||
password=os.environ.get("RABBITMQ_DEFAULT_PASS", "guest"),
|
password=os.environ.get("RABBITMQ_DEFAULT_PASS", "guest"),
|
||||||
)
|
)
|
||||||
|
@ -1 +0,0 @@
|
|||||||
aio-pika
|
|
Loading…
x
Reference in New Issue
Block a user