Delete the task system

This commit is contained in:
Zoe Roux 2023-03-19 00:53:53 +09:00
parent dca91feff8
commit 13d79c5338
33 changed files with 7 additions and 2889 deletions

View File

@ -1,55 +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.Threading.Tasks;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions;
namespace Kyoo.Abstractions.Controllers
{
/// <summary>
/// An interface to identify episodes, shows and metadata based on the episode file.
/// </summary>
public interface IIdentifier
{
/// <summary>
/// Identify a path and return the parsed metadata.
/// </summary>
/// <param name="path">The path of the episode file to parse.</param>
/// <exception cref="IdentificationFailedException">
/// The identifier could not work for the given path.
/// </exception>
/// <returns>
/// A tuple of models representing parsed metadata.
/// If no metadata could be parsed for a type, null can be returned.
/// </returns>
Task<(Collection, Show, Season, Episode)> Identify(string path);
/// <summary>
/// Identify an external subtitle or track file from it's path and return the parsed metadata.
/// </summary>
/// <param name="path">The path of the external track file to parse.</param>
/// <exception cref="IdentificationFailedException">
/// The identifier could not work for the given path.
/// </exception>
/// <returns>
/// The metadata of the track identified.
/// </returns>
Task<Track> IdentifyTrack(string path);
}
}

View File

@ -1,96 +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.Collections.Generic;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Kyoo.Abstractions.Models;
namespace Kyoo.Abstractions.Controllers
{
/// <summary>
/// An interface to automatically retrieve metadata from external providers.
/// </summary>
public interface IMetadataProvider
{
/// <summary>
/// The <see cref="Provider"/> corresponding to this provider.
/// This allow to map metadata to a provider, keep metadata links and
/// know witch <see cref="IMetadataProvider"/> is used for a specific <see cref="Library"/>.
/// </summary>
Provider Provider { get; }
/// <summary>
/// Return a new item with metadata from your provider.
/// </summary>
/// <param name="item">
/// The item to retrieve metadata from. Most of the time, only the name will be available but other
/// properties may be filed by other providers before a call to this method. This can allow you to identify
/// the collection on your provider.
/// </param>
/// <remarks>
/// You must not use metadata from the given <paramref name="item"/>.
/// Merging metadata is the job of Kyoo, a complex <typeparamref name="T"/> is given
/// to make a precise search and give you every available properties, not to discard properties.
/// </remarks>
/// <typeparam name="T">The type of resource to retrieve metadata for.</typeparam>
/// <returns>A new <typeparamref name="T"/> containing metadata from your provider or null</returns>
[ItemCanBeNull]
Task<T> Get<T>([NotNull] T item)
where T : class, IResource;
/// <summary>
/// Search for a specific type of items with a given query.
/// </summary>
/// <param name="query">The search query to use.</param>
/// <typeparam name="T">The type of resource to search metadata for.</typeparam>
/// <returns>The list of items that could be found on this specific provider.</returns>
[ItemNotNull]
Task<ICollection<T>> Search<T>(string query)
where T : class, IResource;
}
/// <summary>
/// A special <see cref="IMetadataProvider"/> that merge results.
/// This interface exists to specify witch provider to use but it can be used like any other metadata provider.
/// </summary>
public abstract class AProviderComposite : IMetadataProvider
{
/// <inheritdoc />
[ItemNotNull]
public abstract Task<T> Get<T>(T item)
where T : class, IResource;
/// <inheritdoc />
public abstract Task<ICollection<T>> Search<T>(string query)
where T : class, IResource;
/// <summary>
/// Since this is a composite and not a real provider, no metadata is available.
/// It is not meant to be stored or selected. This class will handle merge based on what is required.
/// </summary>
public Provider Provider => null;
/// <summary>
/// Select witch providers to use.
/// The <see cref="IMetadataProvider"/> associated with the given <see cref="Provider"/> will be used.
/// </summary>
/// <param name="providers">The list of providers to use</param>
public abstract void UseProviders(IEnumerable<Provider> providers);
}
}

View File

@ -34,32 +34,11 @@ namespace Kyoo.Abstractions.Controllers
[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)]
public interface IPlugin
{
/// <summary>
/// A slug to identify this plugin in queries.
/// </summary>
string Slug { get; }
/// <summary>
/// The name of the plugin
/// </summary>
string Name { get; }
/// <summary>
/// The description of this plugin. This will be displayed on the "installed plugins" page.
/// </summary>
string Description { get; }
/// <summary>
/// <c>true</c> if the plugin should be enabled, <c>false</c> otherwise.
/// If a plugin is not enabled, no configure method will be called.
/// This allow one to enable a plugin if a specific configuration value is set or if the environment contains
/// the right settings.
/// </summary>
/// <remarks>
/// By default, a plugin is always enabled. This method can be overriden to change this behavior.
/// </remarks>
virtual bool Enabled => true;
/// <summary>
/// A list of types that will be available via the IOptions interfaces and will be listed inside
/// an IConfiguration.
@ -73,7 +52,7 @@ namespace Kyoo.Abstractions.Controllers
/// An optional configuration step to allow a plugin to change asp net configurations.
/// </summary>
/// <seealso cref="SA"/>
virtual IEnumerable<IStartupAction> ConfigureSteps => ArraySegment<IStartupAction>.Empty;
IEnumerable<IStartupAction> ConfigureSteps => ArraySegment<IStartupAction>.Empty;
/// <summary>
/// A configure method that will be run on plugin's startup.
@ -94,15 +73,5 @@ namespace Kyoo.Abstractions.Controllers
{
// Skipped
}
/// <summary>
/// An optional function to execute and initialize your plugin.
/// It can be used to initialize a database connection, fill initial data or anything.
/// </summary>
/// <param name="provider">A service provider to request services</param>
void Initialize(IServiceProvider provider)
{
// Skipped
}
}
}

View File

@ -1,212 +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.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions;
namespace Kyoo.Abstractions.Controllers
{
/// <summary>
/// A common interface that tasks should implement.
/// </summary>
public interface ITask
{
/// <summary>
/// The list of parameters
/// </summary>
/// <returns>
/// All parameters that this task as. Every one of them will be given to the run function with a value.
/// </returns>
public TaskParameters GetParameters();
/// <summary>
/// Start this task.
/// </summary>
/// <param name="arguments">
/// The list of parameters.
/// </param>
/// <param name="progress">
/// The progress reporter. Used to inform the sender the percentage of completion of this task
/// .</param>
/// <param name="cancellationToken">A token to request the task's cancellation.
/// If this task is not cancelled quickly, it might be killed by the runner.
/// </param>
/// <exception cref="TaskFailedException">
/// An exception meaning that the task has failed for handled reasons like invalid arguments,
/// invalid environment, missing plugins or failures not related to a default in the code.
/// This exception allow the task to display a failure message to the end user while others exceptions
/// will be displayed as unhandled exceptions and display a stack trace.
/// </exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public Task Run([NotNull] TaskParameters arguments,
[NotNull] IProgress<float> progress,
CancellationToken cancellationToken);
}
/// <summary>
/// A single task parameter. This struct contains metadata to display and utility functions to get them in the task.
/// </summary>
/// <remarks>This struct will be used to generate the swagger documentation of the task.</remarks>
public record TaskParameter
{
/// <summary>
/// The name of this parameter.
/// </summary>
public string Name { get; init; }
/// <summary>
/// The description of this parameter.
/// </summary>
public string Description { get; init; }
/// <summary>
/// The type of this parameter.
/// </summary>
public Type Type { get; init; }
/// <summary>
/// Is this parameter required or can it be ignored?
/// </summary>
public bool IsRequired { get; init; }
/// <summary>
/// The default value of this object.
/// </summary>
public object DefaultValue { get; init; }
/// <summary>
/// The value of the parameter.
/// </summary>
private object _Value { get; init; }
/// <summary>
/// Create a new task parameter.
/// </summary>
/// <param name="name">The name of the parameter</param>
/// <param name="description">The description of the parameter</param>
/// <typeparam name="T">The type of the parameter.</typeparam>
/// <returns>A new task parameter.</returns>
public static TaskParameter Create<T>(string name, string description)
{
return new TaskParameter
{
Name = name,
Description = description,
Type = typeof(T)
};
}
/// <summary>
/// Create a new required task parameter.
/// </summary>
/// <param name="name">The name of the parameter</param>
/// <param name="description">The description of the parameter</param>
/// <typeparam name="T">The type of the parameter.</typeparam>
/// <returns>A new task parameter.</returns>
public static TaskParameter CreateRequired<T>(string name, string description)
{
return new TaskParameter
{
Name = name,
Description = description,
Type = typeof(T),
IsRequired = true
};
}
/// <summary>
/// Create a parameter's value to give to a task.
/// </summary>
/// <param name="name">The name of the parameter</param>
/// <param name="value">The value of the parameter. It's type will be used as parameter's type.</param>
/// <typeparam name="T">The type of the parameter</typeparam>
/// <returns>A TaskParameter that can be used as value.</returns>
public static TaskParameter CreateValue<T>(string name, T value)
{
return new()
{
Name = name,
Type = typeof(T),
_Value = value
};
}
/// <summary>
/// Create a parameter's value for the current parameter.
/// </summary>
/// <param name="value">The value to use</param>
/// <returns>A new parameter's value for this current parameter</returns>
public TaskParameter CreateValue(object value)
{
return this with { _Value = value };
}
/// <summary>
/// Get the value of this parameter. If the value is of the wrong type, it will be converted.
/// </summary>
/// <typeparam name="T">The type of this parameter</typeparam>
/// <returns>The value of this parameter.</returns>
public T As<T>()
{
if (typeof(T) == typeof(object))
return (T)_Value;
if (_Value is IResource resource)
{
if (typeof(T) == typeof(string))
return (T)(object)resource.Slug;
if (typeof(T) == typeof(int))
return (T)(object)resource.ID;
}
return (T)Convert.ChangeType(_Value, typeof(T));
}
}
/// <summary>
/// A parameters container implementing an indexer to allow simple usage of parameters.
/// </summary>
public class TaskParameters : List<TaskParameter>
{
/// <summary>
/// An indexer that return the parameter with the specified name.
/// </summary>
/// <param name="name">The name of the task (case sensitive)</param>
public TaskParameter this[string name] => this.FirstOrDefault(x => x.Name == name);
/// <summary>
/// Create a new, empty, <see cref="TaskParameters"/>
/// </summary>
public TaskParameters() { }
/// <summary>
/// Create a <see cref="TaskParameters"/> with an initial parameters content.
/// </summary>
/// <param name="parameters">The list of parameters</param>
public TaskParameters(IEnumerable<TaskParameter> parameters)
{
AddRange(parameters);
}
}
}

View File

@ -1,100 +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.Diagnostics.CodeAnalysis;
using System.Threading;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Exceptions;
namespace Kyoo.Abstractions.Controllers
{
/// <summary>
/// A service to handle long running tasks.
/// </summary>
/// <remarks>The concurrent number of running tasks is implementation dependent.</remarks>
public interface ITaskManager
{
/// <summary>
/// Start a new task (or queue it).
/// </summary>
/// <param name="taskSlug">
/// The slug of the task to run.
/// </param>
/// <param name="progress">
/// A progress reporter to know the percentage of completion of the task.
/// </param>
/// <param name="arguments">
/// A list of arguments to pass to the task. An automatic conversion will be made if arguments to not fit.
/// </param>
/// <param name="cancellationToken">
/// A custom cancellation token for the task.
/// </param>
/// <exception cref="ArgumentException">
/// If the number of arguments is invalid, if an argument can't be converted or if the task finds the argument
/// invalid.
/// </exception>
/// <exception cref="ItemNotFoundException">
/// The task could not be found.
/// </exception>
void StartTask(string taskSlug,
[NotNull] IProgress<float> progress,
Dictionary<string, object> arguments = null,
CancellationToken? cancellationToken = null);
/// <summary>
/// Start a new task (or queue it).
/// </summary>
/// <param name="progress">
/// A progress reporter to know the percentage of completion of the task.
/// </param>
/// <param name="arguments">
/// A list of arguments to pass to the task. An automatic conversion will be made if arguments to not fit.
/// </param>
/// <typeparam name="T">
/// The type of the task to start.
/// </typeparam>
/// <param name="cancellationToken">
/// A custom cancellation token for the task.
/// </param>
/// <exception cref="ArgumentException">
/// If the number of arguments is invalid, if an argument can't be converted or if the task finds the argument
/// invalid.
/// </exception>
/// <exception cref="ItemNotFoundException">
/// The task could not be found.
/// </exception>
void StartTask<T>([NotNull] IProgress<float> progress,
Dictionary<string, object> arguments = null,
CancellationToken? cancellationToken = null)
where T : ITask;
/// <summary>
/// Get all currently running tasks
/// </summary>
/// <returns>A list of currently running tasks.</returns>
ICollection<(TaskMetadataAttribute, ITask)> GetRunningTasks();
/// <summary>
/// Get all available tasks
/// </summary>
/// <returns>A list of every tasks that this instance know.</returns>
ICollection<TaskMetadataAttribute> GetAllTasks();
}
}

View File

@ -1,55 +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.Runtime.Serialization;
using Kyoo.Abstractions.Controllers;
namespace Kyoo.Abstractions.Models.Exceptions
{
/// <summary>
/// An exception raised when an <see cref="IIdentifier"/> failed.
/// </summary>
[Serializable]
public class IdentificationFailedException : Exception
{
/// <summary>
/// Create a new <see cref="IdentificationFailedException"/> with a default message.
/// </summary>
public IdentificationFailedException()
: base("An identification failed.")
{ }
/// <summary>
/// Create a new <see cref="IdentificationFailedException"/> with a custom message.
/// </summary>
/// <param name="message">The message to use.</param>
public IdentificationFailedException(string message)
: base(message)
{ }
/// <summary>
/// The serialization constructor
/// </summary>
/// <param name="info">Serialization infos</param>
/// <param name="context">The serialization context</param>
protected IdentificationFailedException(SerializationInfo info, StreamingContext context)
: base(info, context)
{ }
}
}

View File

@ -1,63 +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.Runtime.Serialization;
using Kyoo.Abstractions.Controllers;
namespace Kyoo.Abstractions.Models.Exceptions
{
/// <summary>
/// An exception raised when an <see cref="ITask"/> failed.
/// </summary>
[Serializable]
public class TaskFailedException : AggregateException
{
/// <summary>
/// Create a new <see cref="TaskFailedException"/> with a default message.
/// </summary>
public TaskFailedException()
: base("A task failed.")
{ }
/// <summary>
/// Create a new <see cref="TaskFailedException"/> with a custom message.
/// </summary>
/// <param name="message">The message to use.</param>
public TaskFailedException(string message)
: base(message)
{ }
/// <summary>
/// Create a new <see cref="TaskFailedException"/> wrapping another exception.
/// </summary>
/// <param name="exception">The exception to wrap.</param>
public TaskFailedException(Exception exception)
: base(exception)
{ }
/// <summary>
/// The serialization constructor
/// </summary>
/// <param name="info">Serialization infos</param>
/// <param name="context">The serialization context</param>
protected TaskFailedException(SerializationInfo info, StreamingContext context)
: base(info, context)
{ }
}
}

View File

@ -16,12 +16,10 @@
// 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 Autofac;
using Autofac.Builder;
using Kyoo.Abstractions.Controllers;
using Kyoo.Utils;
using Microsoft.Extensions.Configuration;
namespace Kyoo.Abstractions
{
@ -30,32 +28,6 @@ namespace Kyoo.Abstractions
/// </summary>
public static class Module
{
/// <summary>
/// Register a new task to the container.
/// </summary>
/// <param name="builder">The container</param>
/// <typeparam name="T">The type of the task</typeparam>
/// <returns>The registration builder of this new task. That can be used to edit the registration.</returns>
public static IRegistrationBuilder<T, ConcreteReflectionActivatorData, SingleRegistrationStyle>
RegisterTask<T>(this ContainerBuilder builder)
where T : class, ITask
{
return builder.RegisterType<T>().As<ITask>();
}
/// <summary>
/// Register a new metadata provider to the container.
/// </summary>
/// <param name="builder">The container</param>
/// <typeparam name="T">The type of the task</typeparam>
/// <returns>The registration builder of this new provider. That can be used to edit the registration.</returns>
public static IRegistrationBuilder<T, ConcreteReflectionActivatorData, SingleRegistrationStyle>
RegisterProvider<T>(this ContainerBuilder builder)
where T : class, IMetadataProvider
{
return builder.RegisterType<T>().As<IMetadataProvider>().InstancePerLifetimeScope();
}
/// <summary>
/// Register a new repository to the container.
/// </summary>

View File

@ -35,15 +35,9 @@ namespace Kyoo.Authentication
/// </summary>
public class AuthenticationModule : IPlugin
{
/// <inheritdoc />
public string Slug => "auth";
/// <inheritdoc />
public string Name => "Authentication";
/// <inheritdoc />
public string Description => "Enable an authentication/permission system for Kyoo (via Jwt or ApiKeys).";
/// <inheritdoc />
public Dictionary<string, Type> Configuration => new()
{

View File

@ -1,121 +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.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Utils;
using Microsoft.Extensions.Logging;
namespace Kyoo.Core.Controllers
{
/// <summary>
/// A metadata provider composite that merge results from all available providers.
/// </summary>
public class ProviderComposite : AProviderComposite
{
/// <summary>
/// The list of metadata providers
/// </summary>
private readonly ICollection<IMetadataProvider> _providers;
/// <summary>
/// The logger used to print errors.
/// </summary>
private readonly ILogger<ProviderComposite> _logger;
/// <summary>
/// The list of selected providers. If no provider has been selected, this is null.
/// </summary>
private ICollection<Provider> _selectedProviders;
/// <summary>
/// Create a new <see cref="ProviderComposite"/> with a list of available providers.
/// </summary>
/// <param name="providers">The list of providers to merge.</param>
/// <param name="logger">The logger used to print errors.</param>
public ProviderComposite(IEnumerable<IMetadataProvider> providers, ILogger<ProviderComposite> logger)
{
_providers = providers.ToArray();
_logger = logger;
}
/// <inheritdoc />
public override void UseProviders(IEnumerable<Provider> providers)
{
_selectedProviders = providers.ToArray();
}
/// <summary>
/// Return the list of providers that should be used for queries.
/// </summary>
/// <returns>The list of providers to use, respecting the <see cref="UseProviders"/>.</returns>
private IEnumerable<IMetadataProvider> _GetProviders()
{
return _selectedProviders?
.Select(x => _providers.FirstOrDefault(y => y.Provider.Slug == x.Slug))
.Where(x => x != null)
?? _providers;
}
/// <inheritdoc />
public override async Task<T> Get<T>(T item)
{
T ret = item;
foreach (IMetadataProvider provider in _GetProviders())
{
try
{
ret = Merger.Merge(ret, await provider.Get(ret));
}
catch (Exception ex)
{
_logger.LogError(ex, "The provider {Provider} could not get a {Type}",
provider.Provider.Name, typeof(T).Name);
}
}
return ret;
}
/// <inheritdoc />
public override async Task<ICollection<T>> Search<T>(string query)
{
List<T> ret = new();
foreach (IMetadataProvider provider in _GetProviders())
{
try
{
ret.AddRange(await provider.Search<T>(query));
}
catch (Exception ex)
{
_logger.LogError(ex, "The provider {Provider} could not search for {Type}",
provider.Provider.Name, typeof(T).Name);
}
}
return ret;
}
}
}

View File

@ -1,160 +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.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Core.Models;
using Kyoo.Core.Models.Options;
using Kyoo.Utils;
using Microsoft.Extensions.Options;
namespace Kyoo.Core.Controllers
{
/// <summary>
/// An identifier that use a regex to extract basics metadata.
/// </summary>
public class RegexIdentifier : IIdentifier
{
/// <summary>
/// The configuration of kyoo to retrieve the identifier regex.
/// </summary>
private readonly IOptionsMonitor<MediaOptions> _configuration;
/// <summary>
/// The library manager used to retrieve libraries paths.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Create a new <see cref="RegexIdentifier"/>.
/// </summary>
/// <param name="configuration">The regex patterns to use.</param>
/// <param name="libraryManager">The library manager used to retrieve libraries paths.</param>
public RegexIdentifier(IOptionsMonitor<MediaOptions> configuration, ILibraryManager libraryManager)
{
_configuration = configuration;
_libraryManager = libraryManager;
}
/// <summary>
/// Retrieve the relative path of an episode or subtitle.
/// </summary>
/// <param name="path">The full path of the episode</param>
/// <returns>The path relative to the library root.</returns>
private async Task<string> _GetRelativePath(string path)
{
string libraryPath = (await _libraryManager.GetAll<Library>())
.SelectMany(x => x.Paths)
.Where(path.StartsWith)
.MaxBy(x => x.Length);
return path[(libraryPath?.Length ?? 0)..];
}
/// <inheritdoc />
public async Task<(Collection, Show, Season, Episode)> Identify(string path)
{
string relativePath = await _GetRelativePath(path);
Match match = _configuration.CurrentValue.Regex
.Select(x => new Regex(x, RegexOptions.IgnoreCase | RegexOptions.Compiled))
.Select(x => x.Match(relativePath))
.FirstOrDefault(x => x.Success);
if (match == null)
throw new IdentificationFailedException($"The episode at {path} does not match the episode's regex.");
(Collection collection, Show show, Season season, Episode episode) ret = (
collection: new Collection
{
Slug = Utility.ToSlug(match.Groups["Collection"].Value),
Name = match.Groups["Collection"].Value
},
show: new Show
{
Slug = Utility.ToSlug(match.Groups["Show"].Value),
Title = match.Groups["Show"].Value,
Path = Path.GetDirectoryName(path),
StartAir = match.Groups["StartYear"].Success
? new DateTime(int.Parse(match.Groups["StartYear"].Value), 1, 1)
: null
},
season: null,
episode: new Episode
{
SeasonNumber = match.Groups["Season"].Success
? int.Parse(match.Groups["Season"].Value)
: null,
EpisodeNumber = match.Groups["Episode"].Success
? int.Parse(match.Groups["Episode"].Value)
: null,
AbsoluteNumber = match.Groups["Absolute"].Success
? int.Parse(match.Groups["Absolute"].Value)
: null,
Path = path
}
);
if (ret.episode.SeasonNumber.HasValue)
ret.season = new Season { SeasonNumber = ret.episode.SeasonNumber.Value };
if (ret.episode.SeasonNumber == null && ret.episode.EpisodeNumber == null
&& ret.episode.AbsoluteNumber == null)
{
ret.show.IsMovie = true;
ret.episode.Title = ret.show.Title;
}
return ret;
}
/// <inheritdoc />
public Task<Track> IdentifyTrack(string path)
{
Match match = _configuration.CurrentValue.SubtitleRegex
.Select(x => new Regex(x, RegexOptions.IgnoreCase | RegexOptions.Compiled))
.Select(x => x.Match(path))
.FirstOrDefault(x => x.Success);
if (match == null)
throw new IdentificationFailedException($"The subtitle at {path} does not match the subtitle's regex.");
string episodePath = match.Groups["Episode"].Value;
string extension = Path.GetExtension(path);
return Task.FromResult(new Track
{
Type = StreamType.Subtitle,
Language = match.Groups["Language"].Value,
IsDefault = match.Groups["Default"].Success,
IsForced = match.Groups["Forced"].Success,
Codec = FileExtensions.SubtitleExtensions.GetValueOrDefault(extension, extension[1..]),
IsExternal = true,
Path = path,
Episode = new Episode
{
Path = episodePath
}
});
}
}
}

View File

@ -24,15 +24,12 @@ using Kyoo.Abstractions;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models.Utils;
using Kyoo.Core.Controllers;
using Kyoo.Core.Models.Options;
using Kyoo.Core.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using IMetadataProvider = Kyoo.Abstractions.Controllers.IMetadataProvider;
using JsonOptions = Kyoo.Core.Api.JsonOptions;
namespace Kyoo.Core
@ -42,22 +39,13 @@ namespace Kyoo.Core
/// </summary>
public class CoreModule : IPlugin
{
/// <inheritdoc />
public string Slug => "core";
/// <inheritdoc />
public string Name => "Core";
/// <inheritdoc />
public string Description => "The core module containing default implementations.";
/// <inheritdoc />
public Dictionary<string, Type> Configuration => new()
{
{ TaskOptions.Path, typeof(TaskOptions) },
{ MediaOptions.Path, typeof(MediaOptions) },
{ "database", null },
{ "logging", null }
};
/// <inheritdoc />
@ -69,17 +57,6 @@ namespace Kyoo.Core
builder.RegisterType<Transcoder>().As<ITranscoder>().SingleInstance();
builder.RegisterType<ThumbnailsManager>().As<IThumbnailsManager>().InstancePerLifetimeScope();
builder.RegisterType<LibraryManager>().As<ILibraryManager>().InstancePerLifetimeScope();
builder.RegisterType<RegexIdentifier>().As<IIdentifier>().SingleInstance();
builder.RegisterComposite<ProviderComposite, IMetadataProvider>();
builder.Register(x => (AProviderComposite)x.Resolve<IMetadataProvider>());
builder.RegisterTask<Crawler>();
builder.RegisterTask<Housekeeping>();
builder.RegisterTask<RegisterEpisode>();
builder.RegisterTask<RegisterSubtitle>();
builder.RegisterTask<MetadataProviderLoader>();
builder.RegisterTask<LibraryCreator>();
builder.RegisterRepository<ILibraryRepository, LibraryRepository>();
builder.RegisterRepository<ILibraryItemRepository, LibraryItemRepository>();

View File

@ -1,89 +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.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
namespace Kyoo.Core.Models
{
/// <summary>
/// A static class allowing one to identify files extensions.
/// </summary>
public static class FileExtensions
{
/// <summary>
/// The list of known video extensions
/// </summary>
public static readonly ImmutableArray<string> VideoExtensions = ImmutableArray.Create(
".webm",
".mkv",
".flv",
".vob",
".ogg",
".ogv",
".avi",
".mts",
".m2ts",
".ts",
".mov",
".qt",
".asf",
".mp4",
".m4p",
".m4v",
".mpg",
".mp2",
".mpeg",
".mpe",
".mpv",
".m2v",
".3gp",
".3g2"
);
/// <summary>
/// Check if a file represent a video file (only by checking the extension of the file)
/// </summary>
/// <param name="filePath">The path of the file to check</param>
/// <returns><c>true</c> if the file is a video file, <c>false</c> otherwise.</returns>
public static bool IsVideo(string filePath)
{
return VideoExtensions.Contains(Path.GetExtension(filePath));
}
/// <summary>
/// The dictionary of known subtitles extensions and the name of the subtitle codec.
/// </summary>
public static readonly ImmutableDictionary<string, string> SubtitleExtensions = new Dictionary<string, string>
{
{ ".ass", "ass" },
{ ".str", "subrip" }
}.ToImmutableDictionary();
/// <summary>
/// Check if a file represent a subtitle file (only by checking the extension of the file)
/// </summary>
/// <param name="filePath">The path of the file to check</param>
/// <returns><c>true</c> if the file is a subtitle file, <c>false</c> otherwise.</returns>
public static bool IsSubtitle(string filePath)
{
return SubtitleExtensions.ContainsKey(Path.GetExtension(filePath));
}
}
}

View File

@ -28,11 +28,6 @@ namespace Kyoo.Core.Models.Options
/// </summary>
public const string Path = "Basics";
/// <summary>
/// The path of the plugin directory.
/// </summary>
public string PluginPath { get; set; } = "plugins/";
/// <summary>
/// The temporary folder to cache transmuxed file.
/// </summary>

View File

@ -1,41 +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/>.
namespace Kyoo.Core.Models.Options
{
/// <summary>
/// Options for media registering.
/// </summary>
public class MediaOptions
{
/// <summary>
/// The path of this options
/// </summary>
public const string Path = "Media";
/// <summary>
/// A regex for episodes
/// </summary>
public string[] Regex { get; set; }
/// <summary>
/// A regex for subtitles
/// </summary>
public string[] SubtitleRegex { get; set; }
}
}

View File

@ -1,46 +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 JetBrains.Annotations;
namespace Kyoo.Core.Models.Options
{
/// <summary>
/// Options related to tasks
/// </summary>
public class TaskOptions
{
/// <summary>
/// The path of this options
/// </summary>
public const string Path = "Tasks";
/// <summary>
/// The number of tasks that can be run concurrently.
/// </summary>
public int Parallels { get; set; }
/// <summary>
/// The delay of tasks that should be automatically started at fixed times.
/// </summary>
[UsedImplicitly]
public Dictionary<string, TimeSpan> Scheduled { get; set; } = new();
}
}

View File

@ -1,189 +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.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Core.Models;
using Microsoft.Extensions.Logging;
namespace Kyoo.Core.Tasks
{
/// <summary>
/// A task to add new video files.
/// </summary>
[TaskMetadata("scan", "Scan libraries", "Scan your libraries and load data for new shows.", RunOnStartup = true)]
public class Crawler : ITask
{
/// <summary>
/// The library manager used to get libraries and providers to use.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// The file manager used walk inside directories and check they existences.
/// </summary>
private readonly IFileSystem _fileSystem;
/// <summary>
/// A task manager used to create sub tasks for each episode to add to the database.
/// </summary>
private readonly ITaskManager _taskManager;
/// <summary>
/// The logger used to inform the current status to the console.
/// </summary>
private readonly ILogger<Crawler> _logger;
/// <summary>
/// Create a new <see cref="Crawler"/>.
/// </summary>
/// <param name="libraryManager">The library manager to retrieve existing episodes/library/tracks</param>
/// <param name="fileSystem">The file system to glob files</param>
/// <param name="taskManager">The task manager used to start <see cref="RegisterEpisode"/>.</param>
/// <param name="logger">The logger used print messages.</param>
public Crawler(ILibraryManager libraryManager,
IFileSystem fileSystem,
ITaskManager taskManager,
ILogger<Crawler> logger)
{
_libraryManager = libraryManager;
_fileSystem = fileSystem;
_taskManager = taskManager;
_logger = logger;
}
/// <inheritdoc />
public TaskParameters GetParameters()
{
return new()
{
TaskParameter.Create<string>("slug", "A library slug to restrict the scan to this library.")
};
}
/// <inheritdoc />
public async Task Run(TaskParameters arguments, IProgress<float> progress, CancellationToken cancellationToken)
{
string argument = arguments["slug"].As<string>();
ICollection<Library> libraries = argument == null
? await _libraryManager.GetAll<Library>()
: new[] { await _libraryManager.GetOrDefault<Library>(argument) };
if (argument != null && libraries.First() == null)
throw new ArgumentException($"No library found with the name {argument}");
foreach (Library library in libraries)
await _libraryManager.Load(library, x => x.Providers);
progress.Report(0);
float percent = 0;
ICollection<Episode> episodes = await _libraryManager.GetAll<Episode>();
ICollection<Track> tracks = await _libraryManager.GetAll<Track>();
foreach (Library library in libraries)
{
IProgress<float> reporter = new Progress<float>(x =>
{
// ReSharper disable once AccessToModifiedClosure
progress.Report(percent + (x / libraries.Count));
});
await _Scan(library, episodes, tracks, reporter, cancellationToken);
percent += 100f / libraries.Count;
if (cancellationToken.IsCancellationRequested)
return;
}
progress.Report(100);
}
private async Task _Scan(Library library,
IEnumerable<Episode> episodes,
IEnumerable<Track> tracks,
IProgress<float> progress,
CancellationToken cancellationToken)
{
_logger.LogInformation("Scanning library {Library} at {Paths}", library.Name, library.Paths);
foreach (string path in library.Paths)
{
ICollection<string> files = await _fileSystem.ListFiles(path, SearchOption.AllDirectories);
if (cancellationToken.IsCancellationRequested)
return;
// We try to group episodes by shows to register one episode of each show first.
// This speeds up the scan process because further episodes of a show are registered when all metadata
// of the show has already been fetched.
List<IGrouping<string, string>> shows = files
.Where(FileExtensions.IsVideo)
.Where(x => !Path.GetFileName(x).StartsWith('.')) // ignore hidden files.
.Where(x => episodes.All(y => y.Path != x))
.GroupBy(Path.GetDirectoryName)
.ToList();
string[] paths = shows.Select(x => x.First())
.Concat(shows.SelectMany(x => x.Skip(1)))
.ToArray();
float percent = 0;
IProgress<float> reporter = new Progress<float>(x =>
{
// ReSharper disable once AccessToModifiedClosure
progress.Report((percent + (x / paths.Length) - 10) / library.Paths.Length);
});
foreach (string episodePath in paths)
{
_taskManager.StartTask<RegisterEpisode>(reporter, new Dictionary<string, object>
{
["path"] = episodePath,
["library"] = library
}, cancellationToken);
percent += 100f / paths.Length;
}
string[] subtitles = files
.Where(FileExtensions.IsSubtitle)
.Where(x => !x.Contains("Extra"))
.Where(x => tracks.All(y => y.Path != x))
.ToArray();
percent = 0;
reporter = new Progress<float>(x =>
{
// ReSharper disable once AccessToModifiedClosure
progress.Report((90 + (percent + (x / subtitles.Length))) / library.Paths.Length);
});
foreach (string trackPath in subtitles)
{
_taskManager.StartTask<RegisterSubtitle>(reporter, new Dictionary<string, object>
{
["path"] = trackPath
}, cancellationToken);
percent += 100f / subtitles.Length;
}
}
}
}
}

View File

@ -1,138 +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.Threading;
// using System.Threading.Tasks;
// using Kyoo.Controllers;
// using Kyoo.Models;
// using Microsoft.Extensions.DependencyInjection;
//
// namespace Kyoo.Tasks
// {
// public class ExtractMetadata : ITask
// {
// public string Slug => "extract";
// public string Name => "Metadata Extractor";
// public string Description => "Extract subtitles or download thumbnails for a show/episode.";
// public string HelpMessage => null;
// public bool RunOnStartup => false;
// public int Priority => 0;
//
//
// private ILibraryManager _library;
// private IThumbnailsManager _thumbnails;
// private ITranscoder _transcoder;
//
// public async Task Run(IServiceProvider serviceProvider, CancellationToken token, string arguments = null)
// {
// string[] args = arguments?.Split('/');
//
// if (args == null || args.Length < 2)
// return;
//
// string slug = args[1];
// bool thumbs = args.Length < 3 || string.Equals(args[2], "thumbnails", StringComparison.InvariantCultureIgnoreCase);
// bool subs = args.Length < 3 || string.Equals(args[2], "subs", StringComparison.InvariantCultureIgnoreCase);
//
// using IServiceScope serviceScope = serviceProvider.CreateScope();
// _library = serviceScope.ServiceProvider.GetService<ILibraryManager>();
// _thumbnails = serviceScope.ServiceProvider.GetService<IThumbnailsManager>();
// _transcoder = serviceScope.ServiceProvider.GetService<ITranscoder>();
// int id;
//
// switch (args[0].ToLowerInvariant())
// {
// case "show":
// case "shows":
// Show show = await (int.TryParse(slug, out id)
// ? _library!.Get<Show>(id)
// : _library!.Get<Show>(slug));
// await ExtractShow(show, thumbs, subs, token);
// break;
// case "season":
// case "seasons":
// Season season = await (int.TryParse(slug, out id)
// ? _library!.Get<Season>(id)
// : _library!.Get<Season>(slug));
// await ExtractSeason(season, thumbs, subs, token);
// break;
// case "episode":
// case "episodes":
// Episode episode = await (int.TryParse(slug, out id)
// ? _library!.Get<Episode>(id)
// : _library!.Get<Episode>(slug));
// await ExtractEpisode(episode, thumbs, subs);
// break;
// }
// }
//
// private async Task ExtractShow(Show show, bool thumbs, bool subs, CancellationToken token)
// {
// if (thumbs)
// await _thumbnails!.Validate(show, true);
// await _library.Load(show, x => x.Seasons);
// foreach (Season season in show.Seasons)
// {
// if (token.IsCancellationRequested)
// return;
// await ExtractSeason(season, thumbs, subs, token);
// }
// }
//
// private async Task ExtractSeason(Season season, bool thumbs, bool subs, CancellationToken token)
// {
// if (thumbs)
// await _thumbnails!.Validate(season, true);
// await _library.Load(season, x => x.Episodes);
// foreach (Episode episode in season.Episodes)
// {
// if (token.IsCancellationRequested)
// return;
// await ExtractEpisode(episode, thumbs, subs);
// }
// }
//
// private async Task ExtractEpisode(Episode episode, bool thumbs, bool subs)
// {
// if (thumbs)
// await _thumbnails!.Validate(episode, true);
// if (subs)
// {
// await _library.Load(episode, x => x.Tracks);
// episode.Tracks = (await _transcoder!.ExtractInfos(episode, true))
// .Where(x => x.Type != StreamType.Attachment)
// .Concat(episode.Tracks.Where(x => x.IsExternal))
// .ToList();
// await _library.Edit(episode, false);
// }
// }
//
// public Task<IEnumerable<string>> GetPossibleParameters()
// {
// return Task.FromResult<IEnumerable<string>>(null);
// }
//
// public int? Progress()
// {
// return null;
// }
// }
// }

View File

@ -1,103 +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.Threading;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Microsoft.Extensions.Logging;
namespace Kyoo.Core.Tasks
{
/// <summary>
/// A task to remove orphaned episode and series.
/// </summary>
[TaskMetadata("housekeeping", "Housekeeping", "Remove orphaned episode and series.", RunOnStartup = true)]
public class Housekeeping : ITask
{
/// <summary>
/// The library manager used to get libraries or remove deleted episodes.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// The file manager used walk inside directories and check they existences.
/// </summary>
private readonly IFileSystem _fileSystem;
/// <summary>
/// The logger used to inform the user that episodes has been removed.
/// </summary>
private readonly ILogger<Housekeeping> _logger;
/// <summary>
/// Create a new <see cref="Housekeeping"/> task.
/// </summary>
/// <param name="libraryManager">The library manager used to get libraries or remove deleted episodes.</param>
/// <param name="fileSystem">The file manager used walk inside directories and check they existences.</param>
/// <param name="logger">The logger used to inform the user that episodes has been removed.</param>
public Housekeeping(ILibraryManager libraryManager, IFileSystem fileSystem, ILogger<Housekeeping> logger)
{
_libraryManager = libraryManager;
_fileSystem = fileSystem;
_logger = logger;
}
/// <inheritdoc />
public TaskParameters GetParameters()
{
return new();
}
/// <inheritdoc />
public async Task Run(TaskParameters arguments, IProgress<float> progress, CancellationToken cancellationToken)
{
int count = 0;
int delCount = await _libraryManager.GetCount<Show>() + await _libraryManager.GetCount<Episode>();
progress.Report(0);
foreach (Show show in await _libraryManager.GetAll<Show>())
{
progress.Report(count / delCount * 100);
count++;
if (await _fileSystem.Exists(show.Path))
continue;
_logger.LogWarning("Show {Name}'s folder has been deleted (was {Path}), removing it from kyoo",
show.Title, show.Path);
await _libraryManager.Delete(show);
}
foreach (Episode episode in await _libraryManager.GetAll<Episode>())
{
progress.Report(count / delCount * 100);
count++;
if (await _fileSystem.Exists(episode.Path))
continue;
_logger.LogWarning("Episode {Slug}'s file has been deleted (was {Path}), removing it from kyoo",
episode.Slug, episode.Path);
await _libraryManager.Delete(episode);
}
progress.Report(100);
}
}
}

View File

@ -1,80 +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.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Utils;
namespace Kyoo.Core.Tasks
{
/// <summary>
/// A task to add new video files.
/// </summary>
[TaskMetadata("library-creator", "Create libraries", "Create libraries on the library root folder.",
RunOnStartup = true, Priority = 500)]
public class LibraryCreator : ITask
{
/// <summary>
/// The library manager used to get libraries and providers to use.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Create a new <see cref="Crawler"/>.
/// </summary>
/// <param name="libraryManager">The library manager to retrieve existing episodes/library/tracks</param>
public LibraryCreator(ILibraryManager libraryManager)
{
_libraryManager = libraryManager;
}
/// <inheritdoc />
public TaskParameters GetParameters()
{
return new();
}
/// <inheritdoc />
public async Task Run(TaskParameters arguments, IProgress<float> progress, CancellationToken cancellationToken)
{
ICollection<Provider> providers = await _libraryManager.GetAll<Provider>();
ICollection<string> existings = (await _libraryManager.GetAll<Library>()).SelectMany(x => x.Paths).ToArray();
IEnumerable<Library> newLibraries = Directory.GetDirectories(Environment.GetEnvironmentVariable("KYOO_LIBRARY_ROOT") ?? "/video")
.Where(x => !existings.Contains(x))
.Select(x => new Library
{
Slug = Utility.ToSlug(Path.GetFileName(x)),
Name = Path.GetFileName(x),
Paths = new string[] { x },
Providers = providers,
});
foreach (Library library in newLibraries)
{
await _libraryManager.Create(library);
}
}
}
}

View File

@ -1,96 +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.Threading;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Exceptions;
namespace Kyoo.Core.Tasks
{
/// <summary>
/// A task that download metadata providers images.
/// </summary>
[TaskMetadata("reload-metadata", "Reload Metadata Providers", "Add every loaded metadata provider to the database.",
RunOnStartup = true, Priority = 1000, IsHidden = true)]
public class MetadataProviderLoader : ITask
{
/// <summary>
/// The provider repository used to create in-db providers from metadata providers.
/// </summary>
private readonly IProviderRepository _providers;
/// <summary>
/// The thumbnail manager used to download providers logo.
/// </summary>
private readonly IThumbnailsManager _thumbnails;
/// <summary>
/// The list of metadata providers to register.
/// </summary>
private readonly ICollection<IMetadataProvider> _metadataProviders;
/// <summary>
/// Create a new <see cref="MetadataProviderLoader"/> task.
/// </summary>
/// <param name="providers">
/// The provider repository used to create in-db providers from metadata providers.
/// </param>
/// <param name="thumbnails">
/// The thumbnail manager used to download providers logo.
/// </param>
/// <param name="metadataProviders">
/// The list of metadata providers to register.
/// </param>
public MetadataProviderLoader(IProviderRepository providers,
IThumbnailsManager thumbnails,
ICollection<IMetadataProvider> metadataProviders)
{
_providers = providers;
_thumbnails = thumbnails;
_metadataProviders = metadataProviders;
}
/// <inheritdoc />
public TaskParameters GetParameters()
{
return new TaskParameters();
}
/// <inheritdoc />
public async Task Run(TaskParameters arguments, IProgress<float> progress, CancellationToken cancellationToken)
{
float percent = 0;
progress.Report(0);
foreach (IMetadataProvider provider in _metadataProviders)
{
if (string.IsNullOrEmpty(provider.Provider.Slug))
throw new TaskFailedException($"Empty provider slug (name: {provider.Provider.Name}).");
await _providers.CreateIfNotExists(provider.Provider);
await _thumbnails.DownloadImages(provider.Provider);
percent += 100f / _metadataProviders.Count;
progress.Report(percent);
}
progress.Report(100);
}
}
}

View File

@ -1,145 +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.Threading;
// using System.Threading.Tasks;
// using Kyoo.Controllers;
// using Kyoo.Models;
// using Microsoft.Extensions.DependencyInjection;
//
// namespace Kyoo.Tasks
// {
// public class ReScan: ITask
// {
// public string Slug => "re-scan";
// public string Name => "ReScan";
// public string Description => "Re download metadata of an item using it's external ids.";
// public string HelpMessage => null;
// public bool RunOnStartup => false;
// public int Priority => 0;
//
//
// private IServiceProvider _serviceProvider;
// private IThumbnailsManager _thumbnailsManager;
// private IProviderManager _providerManager;
// private DatabaseContext _database;
//
// public async Task Run(IServiceProvider serviceProvider, CancellationToken cancellationToken, string arguments = null)
// {
// using IServiceScope serviceScope = serviceProvider.CreateScope();
// _serviceProvider = serviceProvider;
// _thumbnailsManager = serviceProvider.GetService<IThumbnailsManager>();
// _providerManager = serviceProvider.GetService<IProviderManager>();
// _database = serviceScope.ServiceProvider.GetService<DatabaseContext>();
//
// if (arguments == null || !arguments.Contains('/'))
// return;
//
// string slug = arguments.Substring(arguments.IndexOf('/') + 1);
// switch (arguments.Substring(0, arguments.IndexOf('/')))
// {
// case "show":
// await ReScanShow(slug);
// break;
// case "season":
// await ReScanSeason(slug);
// break;
// }
// }
//
// private async Task ReScanShow(string slug)
// {
// Show old;
//
// using (IServiceScope serviceScope = _serviceProvider.CreateScope())
// {
// ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>();
// old = _database.Shows.FirstOrDefault(x => x.Slug == slug);
// if (old == null)
// return;
// Library library = _database.LibraryLinks.First(x => x.Show == old && x.Library != null).Library;
// Show edited = await _providerManager.CompleteShow(old, library);
// edited.ID = old.ID;
// edited.Slug = old.Slug;
// edited.Path = old.Path;
// await libraryManager.EditShow(edited, true);
// await _thumbnailsManager.Validate(edited, true);
// }
// if (old.Seasons != null)
// await Task.WhenAll(old.Seasons.Select(x => ReScanSeason(old, x)));
// IEnumerable<Episode> orphans = old.Episodes.Where(x => x.Season == null).ToList();
// if (orphans.Any())
// await Task.WhenAll(orphans.Select(x => ReScanEpisode(old, x)));
// }
//
// private async Task ReScanSeason(string seasonSlug)
// {
// string[] infos = seasonSlug.Split('-');
// if (infos.Length != 2 || int.TryParse(infos[1], out int seasonNumber))
// return;
// string slug = infos[0];
// Show show = _database.Shows.FirstOrDefault(x => x.Slug == slug);
// if (show == null)
// return;
// Season old = _database.Seasons.FirstOrDefault(x => x.SeasonNumber == seasonNumber && x.Show.ID == show.ID);
// if (old == null)
// return;
// await ReScanSeason(show, old);
// }
//
// private async Task ReScanSeason(Show show, Season old)
// {
// using (IServiceScope serviceScope = _serviceProvider.CreateScope())
// {
// ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>();
// Library library = _database.LibraryLinks.First(x => x.Show == show && x.Library != null).Library;
// Season edited = await _providerManager.GetSeason(show, old.SeasonNumber, library);
// edited.ID = old.ID;
// await libraryManager.EditSeason(edited, true);
// await _thumbnailsManager.Validate(edited, true);
// }
// if (old.Episodes != null)
// await Task.WhenAll(old.Episodes.Select(x => ReScanEpisode(show, x)));
// }
//
// private async Task ReScanEpisode(Show show, Episode old)
// {
// using IServiceScope serviceScope = _serviceProvider.CreateScope();
// ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>();
//
// Library library = _database.LibraryLinks.First(x => x.Show == show && x.Library != null).Library;
// Episode edited = await _providerManager.GetEpisode(show, old.Path, old.SeasonNumber, old.EpisodeNumber, old.AbsoluteNumber, library);
// edited.ID = old.ID;
// await libraryManager.EditEpisode(edited, true);
// await _thumbnailsManager.Validate(edited, true);
// }
//
// public Task<IEnumerable<string>> GetPossibleParameters()
// {
// return Task.FromResult<IEnumerable<string>>(null);
// }
//
// public int? Progress()
// {
// return null;
// }
// }
// }

View File

@ -1,210 +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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Exceptions;
namespace Kyoo.Core.Tasks
{
/// <summary>
/// A task to register a new episode
/// </summary>
[TaskMetadata("register", "Register episode", "Register a new episode")]
public class RegisterEpisode : ITask
{
/// <summary>
/// An identifier to extract metadata from paths.
/// </summary>
private readonly IIdentifier _identifier;
/// <summary>
/// The library manager used to register the episode.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// A metadata provider to retrieve the metadata of the new episode (and related items if they do not exist).
/// </summary>
private readonly AProviderComposite _metadataProvider;
/// <summary>
/// The thumbnail manager used to download images.
/// </summary>
private readonly IThumbnailsManager _thumbnailsManager;
/// <summary>
/// The transcoder used to extract subtitles and metadata.
/// </summary>
private readonly IFileSystem _transcoder;
/// <summary>
/// Create a new <see cref="RegisterEpisode"/> task.
/// </summary>
/// <param name="identifier">
/// An identifier to extract metadata from paths.
/// </param>
/// <param name="libraryManager">
/// The library manager used to register the episode.
/// </param>
/// <param name="metadataProvider">
/// A metadata provider to retrieve the metadata of the new episode (and related items if they do not exist).
/// </param>
/// <param name="thumbnailsManager">
/// The thumbnail manager used to download images.
/// </param>
/// <param name="transcoder">
/// The file manager used to retrieve episodes metadata.
/// </param>
public RegisterEpisode(IIdentifier identifier,
ILibraryManager libraryManager,
AProviderComposite metadataProvider,
IThumbnailsManager thumbnailsManager,
IFileSystem transcoder)
{
_identifier = identifier;
_libraryManager = libraryManager;
_metadataProvider = metadataProvider;
_thumbnailsManager = thumbnailsManager;
_transcoder = transcoder;
}
/// <inheritdoc />
public TaskParameters GetParameters()
{
return new()
{
TaskParameter.CreateRequired<string>("path", "The path of the episode file"),
TaskParameter.CreateRequired<Library>("library", "The library in witch the episode is")
};
}
/// <inheritdoc />
public async Task Run(TaskParameters arguments, IProgress<float> progress, CancellationToken cancellationToken)
{
string path = arguments["path"].As<string>();
Library library = arguments["library"].As<Library>();
progress.Report(0);
if (library != null)
{
if (library.Providers == null)
await _libraryManager.Load(library, x => x.Providers);
_metadataProvider.UseProviders(library.Providers);
}
try
{
(Collection collection, Show show, Season season, Episode episode) = await _identifier.Identify(path);
progress.Report(15);
collection = await _RegisterAndFill(collection);
progress.Report(20);
Show registeredShow = await _RegisterAndFill(show);
if (registeredShow.Path != show.Path)
{
if (show.StartAir.HasValue)
{
show.Slug += $"-{show.StartAir.Value.Year}";
show = await _libraryManager.Create(show);
}
else
{
throw new TaskFailedException($"Duplicated show found ({show.Slug}) " +
$"at {registeredShow.Path} and {show.Path}");
}
}
else
show = registeredShow;
progress.Report(50);
if (season != null)
season.Show = show;
season = await _RegisterAndFill(season);
progress.Report(60);
episode.Show = show;
episode.Season = season;
if (!show.IsMovie)
episode = await _metadataProvider.Get(episode);
progress.Report(70);
episode.Tracks = await _transcoder.ExtractInfos(episode, false);
await _thumbnailsManager.DownloadImages(episode);
progress.Report(90);
await _libraryManager.Create(episode);
progress.Report(95);
await _libraryManager.AddShowLink(show, library, collection);
progress.Report(100);
}
catch (IdentificationFailedException ex)
{
throw new TaskFailedException(ex);
}
catch (DuplicatedItemException ex)
{
throw new TaskFailedException(ex);
}
}
/// <summary>
/// Retrieve the equivalent item if it already exists in the database,
/// if it does not, fill metadata using the metadata provider, download images and register the item to the
/// database.
/// </summary>
/// <param name="item">The item to retrieve or fill and register</param>
/// <typeparam name="T">The type of the item</typeparam>
/// <returns>The existing or filled item.</returns>
private async Task<T> _RegisterAndFill<T>(T item)
where T : class, IResource, IThumbnails, IMetadata
{
if (item == null || string.IsNullOrEmpty(item.Slug))
return null;
T existing = await _libraryManager.GetOrDefault<T>(item.Slug);
if (existing != null)
{
await _libraryManager.Load(existing, x => x.ExternalIDs);
return existing;
}
item = await _metadataProvider.Get(item);
await _thumbnailsManager.DownloadImages(item);
switch (item)
{
case Show show when show.People != null:
foreach (PeopleRole role in show.People)
await _thumbnailsManager.DownloadImages(role.People);
break;
case Season season:
season.Title ??= $"Season {season.SeasonNumber}";
break;
}
return await _libraryManager.CreateIfNotExists(item);
}
}
}

View File

@ -1,102 +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.Threading;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Exceptions;
namespace Kyoo.Core.Tasks
{
/// <summary>
/// A task to register a new episode
/// </summary>
[TaskMetadata("register-sub", "Register subtitle", "Register a new subtitle")]
public class RegisterSubtitle : ITask
{
/// <summary>
/// An identifier to extract metadata from paths.
/// </summary>
private readonly IIdentifier _identifier;
/// <summary>
/// The library manager used to register the episode.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Create a new <see cref="RegisterSubtitle"/> task.
/// </summary>
/// <param name="identifier">An identifier to extract metadata from paths.</param>
/// <param name="libraryManager">The library manager used to register the episode.</param>
public RegisterSubtitle(IIdentifier identifier, ILibraryManager libraryManager)
{
_identifier = identifier;
_libraryManager = libraryManager;
}
/// <inheritdoc />
public TaskParameters GetParameters()
{
return new()
{
TaskParameter.CreateRequired<string>("path", "The path of the subtitle file")
};
}
/// <inheritdoc />
public async Task Run(TaskParameters arguments, IProgress<float> progress, CancellationToken cancellationToken)
{
string path = arguments["path"].As<string>();
try
{
progress.Report(0);
Track track = await _identifier.IdentifyTrack(path);
progress.Report(25);
if (track.Episode == null)
throw new TaskFailedException($"No episode identified for the track at {path}");
if (track.Episode.ID == 0)
{
if (track.Episode.Slug != null)
track.Episode = await _libraryManager.Get<Episode>(track.Episode.Slug);
else if (track.Episode.Path != null)
{
track.Episode = await _libraryManager.GetOrDefault<Episode>(x => x.Path.StartsWith(track.Episode.Path));
if (track.Episode == null)
throw new TaskFailedException($"No episode found for the track at: {path}.");
}
else
throw new TaskFailedException($"No episode identified for the track at {path}");
}
progress.Report(50);
await _libraryManager.Create(track);
progress.Report(100);
}
catch (IdentificationFailedException ex)
{
throw new TaskFailedException(ex);
}
}
}
}

View File

@ -1,96 +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 Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api
{
/// <summary>
/// An endpoint to list and run tasks in the background.
/// </summary>
[Route("tasks")]
[Route("task", Order = AlternativeRoute)]
[ApiController]
[ResourceView]
[PartialPermission("Task", Group = Group.Admin)]
[ApiDefinition("Tasks", Group = AdminGroup)]
public class TaskApi : ControllerBase
{
/// <summary>
/// The task manager used to retrieve and start tasks.
/// </summary>
private readonly ITaskManager _taskManager;
/// <summary>
/// Create a new <see cref="TaskApi"/>.
/// </summary>
/// <param name="taskManager">The task manager used to start tasks.</param>
public TaskApi(ITaskManager taskManager)
{
_taskManager = taskManager;
}
/// <summary>
/// Get all tasks
/// </summary>
/// <remarks>
/// Retrieve all tasks available in this instance of Kyoo.
/// </remarks>
/// <returns>A list of every tasks that this instance know.</returns>
[HttpGet]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<ICollection<ITask>> GetTasks()
{
return Ok(_taskManager.GetAllTasks());
}
/// <summary>
/// Start task
/// </summary>
/// <remarks>
/// Start a task with the given arguments. If a task is already running, it may be queued and started only when
/// a runner become available.
/// </remarks>
/// <param name="taskSlug">The slug of the task to start.</param>
/// <param name="args">The list of arguments to give to the task.</param>
/// <returns>The task has been started or is queued.</returns>
/// <response code="400">The task misses an argument or an argument is invalid.</response>
/// <response code="404">No task could be found with the given slug.</response>
[HttpPut("{taskSlug}")]
[HttpGet("{taskSlug}", Order = AlternativeRoute)]
[PartialPermission(Kind.Create)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult RunTask(string taskSlug,
[FromQuery] Dictionary<string, object> args)
{
_taskManager.StartTask(taskSlug, new Progress<float>(), args);
return Ok();
}
}
}

View File

@ -40,18 +40,13 @@ namespace Kyoo.Host
/// Hosts of kyoo (main functions) generally only create a new <see cref="Application"/>
/// and return <see cref="Start(string[])"/>.
/// </summary>
public class Application : IDisposable
public class Application
{
/// <summary>
/// The environment in witch Kyoo will run (ether "Production" or "Development").
/// </summary>
private readonly string _environment;
/// <summary>
/// The cancellation token source used to allow the app to be shutdown or restarted.
/// </summary>
private CancellationTokenSource _tokenSource;
/// <summary>
/// The logger used for startup and error messages.
/// </summary>
@ -109,8 +104,7 @@ namespace Kyoo.Host
.ConfigureContainer(configure)
.Build();
_tokenSource = new CancellationTokenSource();
await _StartWithHost(host, _tokenSource.Token);
await _StartWithHost(host);
}
/// <summary>
@ -118,7 +112,7 @@ namespace Kyoo.Host
/// </summary>
/// <param name="host">The host to start.</param>
/// <param name="cancellationToken">A token to allow one to stop the host.</param>
private async Task _StartWithHost(IHost host, CancellationToken cancellationToken)
private async Task _StartWithHost(IHost host, CancellationToken cancellationToken = default)
{
try
{
@ -187,12 +181,5 @@ namespace Kyoo.Host
.Enrich.WithThreadId()
.Enrich.FromLogContext();
}
/// <inheritdoc/>
public void Dispose()
{
_tokenSource.Dispose();
GC.SuppressFinalize(this);
}
}
}

View File

@ -18,10 +18,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Kyoo.Abstractions.Controllers;
using Kyoo.Core.Models.Options;
using Microsoft.Extensions.DependencyInjection;
@ -89,51 +86,11 @@ namespace Kyoo.Host.Controllers
return _plugins;
}
/// <summary>
/// Load a single plugin and return all IPlugin implementations contained in the Assembly.
/// </summary>
/// <param name="path">The path of the dll</param>
/// <returns>The list of dlls in hte assembly</returns>
private IPlugin[] _LoadPlugin(string path)
{
path = Path.GetFullPath(path);
try
{
PluginDependencyLoader loader = new(path);
Assembly assembly = loader.LoadFromAssemblyPath(path);
return assembly.GetTypes()
.Where(x => typeof(IPlugin).IsAssignableFrom(x))
.Where(x => _plugins.All(y => y.GetType() != x))
.Select(x => (IPlugin)ActivatorUtilities.CreateInstance(_provider, x))
.ToArray();
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not load the plugin at {Path}", path);
return Array.Empty<IPlugin>();
}
}
/// <inheritdoc />
public void LoadPlugins(ICollection<IPlugin> plugins)
{
string pluginFolder = _options.Value.PluginPath;
if (!Directory.Exists(pluginFolder))
Directory.CreateDirectory(pluginFolder);
_logger.LogTrace("Loading new plugins...");
string[] pluginsPaths = Directory.GetFiles(pluginFolder, "*.dll", SearchOption.AllDirectories);
_plugins.AddRange(plugins
.Concat(pluginsPaths.SelectMany(_LoadPlugin))
.Where(x => x.Enabled)
.GroupBy(x => x.Name)
.Select(x => x.First())
);
if (!_plugins.Any())
_logger.LogInformation("No plugin enabled");
else
_logger.LogInformation("Plugin enabled: {Plugins}", _plugins.Select(x => x.Name));
_plugins.AddRange(plugins);
_logger.LogInformation("Modules enabled: {Plugins}", _plugins.Select(x => x.Name));
}
/// <inheritdoc />
@ -144,52 +101,5 @@ namespace Kyoo.Host.Controllers
.ToArray()
);
}
/// <summary>
/// A custom <see cref="AssemblyLoadContext"/> to load plugin's dependency if they are on the same folder.
/// </summary>
private class PluginDependencyLoader : AssemblyLoadContext
{
/// <summary>
/// The basic resolver that will be used to load dlls.
/// </summary>
private readonly AssemblyDependencyResolver _resolver;
/// <summary>
/// Create a new <see cref="PluginDependencyLoader"/> for the given path.
/// </summary>
/// <param name="pluginPath">The path of the plugin and it's dependencies</param>
public PluginDependencyLoader(string pluginPath)
{
_resolver = new AssemblyDependencyResolver(pluginPath);
}
/// <inheritdoc />
protected override Assembly Load(AssemblyName assemblyName)
{
Assembly existing = AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(x =>
{
AssemblyName name = x.GetName();
return name.Name == assemblyName.Name && name.Version == assemblyName.Version;
});
if (existing != null)
return existing;
// TODO load the assembly from the common folder if the file exists (this would allow shared libraries)
string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
if (assemblyPath != null)
return LoadFromAssemblyPath(assemblyPath);
return base.Load(assemblyName);
}
/// <inheritdoc />
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (libraryPath != null)
return LoadUnmanagedDllFromPath(libraryPath);
return base.LoadUnmanagedDll(unmanagedDllName);
}
}
}
}

View File

@ -1,346 +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 System.Threading;
using System.Threading.Tasks;
using Autofac.Features.Metadata;
using Autofac.Features.OwnedInstances;
using JetBrains.Annotations;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Core.Models.Options;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Kyoo.Host.Controllers
{
/// <summary>
/// A service to handle long running tasks and a background runner.
/// </summary>
/// <remarks>Task will be queued, only one can run simultaneously.</remarks>
public class TaskManager : BackgroundService, ITaskManager
{
/// <summary>
/// The class representing task under this <see cref="TaskManager"/> jurisdiction.
/// </summary>
private class ManagedTask
{
/// <summary>
/// The metadata for this task (the slug, and other useful information).
/// </summary>
public TaskMetadataAttribute Metadata { get; set; }
/// <summary>
/// The function used to create the task object.
/// </summary>
public Func<Owned<ITask>> Factory { get; init; }
/// <summary>
/// The next scheduled date for this task
/// </summary>
public DateTime ScheduledDate { get; set; }
}
/// <summary>
/// A class representing a task inside the <see cref="TaskManager._queuedTasks"/> list.
/// </summary>
private class QueuedTask
{
/// <summary>
/// The task currently queued.
/// </summary>
public ManagedTask Task { get; init; }
/// <summary>
/// The progress reporter that this task should use.
/// </summary>
public IProgress<float> ProgressReporter { get; init; }
/// <summary>
/// The arguments to give to run the task with.
/// </summary>
public Dictionary<string, object> Arguments { get; init; }
/// <summary>
/// A token informing the task that it should be cancelled or not.
/// </summary>
public CancellationToken? CancellationToken { get; init; }
}
/// <summary>
/// The configuration instance used to get schedule information
/// </summary>
private readonly IOptionsMonitor<TaskOptions> _options;
/// <summary>
/// The logger instance.
/// </summary>
private readonly ILogger<TaskManager> _logger;
/// <summary>
/// The list of tasks and their next scheduled run.
/// </summary>
private readonly List<ManagedTask> _tasks;
/// <summary>
/// The queue of tasks that should be run as soon as possible.
/// </summary>
private readonly Queue<QueuedTask> _queuedTasks = new();
/// <summary>
/// The cancellation token used to cancel the running task when the runner should shutdown.
/// </summary>
private readonly CancellationTokenSource _taskToken = new();
/// <summary>
/// The currently running task.
/// </summary>
private (TaskMetadataAttribute, ITask)? _runningTask;
/// <summary>
/// Create a new <see cref="TaskManager"/>.
/// </summary>
/// <param name="tasks">The list of tasks to manage with their metadata</param>
/// <param name="options">The configuration to load schedule information.</param>
/// <param name="logger">The logger.</param>
public TaskManager(IEnumerable<Meta<Func<Owned<ITask>>, TaskMetadataAttribute>> tasks,
IOptionsMonitor<TaskOptions> options,
ILogger<TaskManager> logger)
{
_options = options;
_logger = logger;
_tasks = tasks.Select(x => new ManagedTask
{
Factory = x.Value,
Metadata = x.Metadata,
ScheduledDate = _GetNextTaskDate(x.Metadata.Slug)
}).ToList();
if (_tasks.Any())
_logger.LogTrace("Task manager initiated with: {Tasks}", _tasks.Select(x => x.Metadata.Name));
else
_logger.LogInformation("Task manager initiated without any tasks");
}
/// <summary>
/// Triggered when the application host is ready to start the service.
/// </summary>
/// <remarks>Start the runner in another thread.</remarks>
/// <param name="cancellationToken">Indicates that the start process has been aborted.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public override Task StartAsync(CancellationToken cancellationToken)
{
Task.Run(() => base.StartAsync(cancellationToken), CancellationToken.None);
return Task.CompletedTask;
}
/// <inheritdoc />
public override Task StopAsync(CancellationToken cancellationToken)
{
_taskToken.Cancel();
return base.StopAsync(cancellationToken);
}
/// <summary>
/// The runner that will host tasks and run queued tasks.
/// </summary>
/// <param name="cancellationToken">A token to stop the runner</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
_EnqueueStartupTasks();
while (!cancellationToken.IsCancellationRequested)
{
if (_queuedTasks.Any())
{
QueuedTask task = _queuedTasks.Dequeue();
try
{
await _RunTask(task.Task, task.ProgressReporter, task.Arguments, task.CancellationToken);
}
catch (TaskFailedException ex)
{
_logger.LogWarning("The task \"{Task}\" failed: {Message}",
task.Task.Metadata.Name, ex.Message);
}
catch (Exception e)
{
_logger.LogError(e, "An unhandled exception occured while running the task {Task}",
task.Task.Metadata.Name);
}
}
else
{
await Task.Delay(1000, cancellationToken);
_QueueScheduledTasks();
}
}
}
/// <summary>
/// Parse parameters, inject a task and run it.
/// </summary>
/// <param name="task">The task to run</param>
/// <param name="progress">A progress reporter to know the percentage of completion of the task.</param>
/// <param name="arguments">The arguments to pass to the function</param>
/// <param name="cancellationToken">An optional cancellation token that will be passed to the task.</param>
/// <exception cref="ArgumentException">
/// If the number of arguments is invalid, if an argument can't be converted or if the task finds the argument
/// invalid.
/// </exception>
private async Task _RunTask(ManagedTask task,
[NotNull] IProgress<float> progress,
Dictionary<string, object> arguments,
CancellationToken? cancellationToken = null)
{
using (_logger.BeginScope("Task: {Task}", task.Metadata.Name))
{
await using Owned<ITask> taskObj = task.Factory.Invoke();
ICollection<TaskParameter> all = taskObj.Value.GetParameters();
_runningTask = (task.Metadata, taskObj.Value);
ICollection<string> invalids = arguments.Keys
.Where(x => all.All(y => x != y.Name))
.ToArray();
if (invalids.Any())
{
throw new ArgumentException($"{string.Join(", ", invalids)} are " +
$"invalid arguments for the task {task.Metadata.Name}");
}
TaskParameters args = new(all
.Select(x =>
{
object value = arguments
.FirstOrDefault(y => string.Equals(y.Key, x.Name, StringComparison.OrdinalIgnoreCase))
.Value;
if (value == null && x.IsRequired)
{
throw new ArgumentException($"The argument {x.Name} is required to run " +
$"{task.Metadata.Name} but it was not specified.");
}
return x.CreateValue(value ?? x.DefaultValue);
}));
_logger.LogInformation("Task starting: {Task} ({Parameters})",
task.Metadata.Name, args.ToDictionary(x => x.Name, x => x.As<object>()));
CancellationToken token = cancellationToken != null
? CancellationTokenSource.CreateLinkedTokenSource(_taskToken.Token, cancellationToken.Value).Token
: _taskToken.Token;
await taskObj.Value.Run(args, progress, token);
_logger.LogInformation("Task finished: {Task}", task.Metadata.Name);
_runningTask = null;
}
}
/// <summary>
/// Start tasks that are scheduled for start.
/// </summary>
private void _QueueScheduledTasks()
{
IEnumerable<string> tasksToQueue = _tasks.Where(x => x.ScheduledDate <= DateTime.Now)
.Select(x => x.Metadata.Slug);
foreach (string task in tasksToQueue)
{
_logger.LogDebug("Queuing task scheduled for running: {Task}", task);
StartTask(task, new Progress<float>(), new Dictionary<string, object>());
}
}
/// <summary>
/// Queue startup tasks with respect to the priority rules.
/// </summary>
private void _EnqueueStartupTasks()
{
IEnumerable<string> startupTasks = _tasks
.Where(x => x.Metadata.RunOnStartup)
.OrderByDescending(x => x.Metadata.Priority)
.Select(x => x.Metadata.Slug);
foreach (string task in startupTasks)
StartTask(task, new Progress<float>(), new Dictionary<string, object>());
}
/// <inheritdoc />
public void StartTask(string taskSlug,
IProgress<float> progress,
Dictionary<string, object> arguments = null,
CancellationToken? cancellationToken = null)
{
arguments ??= new Dictionary<string, object>();
int index = _tasks.FindIndex(x => x.Metadata.Slug == taskSlug);
if (index == -1)
throw new ItemNotFoundException($"No task found with the slug {taskSlug}");
_queuedTasks.Enqueue(new QueuedTask
{
Task = _tasks[index],
ProgressReporter = progress,
Arguments = arguments,
CancellationToken = cancellationToken
});
_tasks[index].ScheduledDate = _GetNextTaskDate(taskSlug);
}
/// <inheritdoc />
public void StartTask<T>(IProgress<float> progress,
Dictionary<string, object> arguments = null,
CancellationToken? cancellationToken = null)
where T : ITask
{
TaskMetadataAttribute metadata = typeof(T).GetCustomAttribute<TaskMetadataAttribute>();
if (metadata == null)
throw new ArgumentException($"No metadata found on the given task (type: {typeof(T).Name}).");
StartTask(metadata.Slug, progress, arguments, cancellationToken);
}
/// <summary>
/// Get the next date of the execution of the given task.
/// </summary>
/// <param name="taskSlug">The slug of the task</param>
/// <returns>The next date.</returns>
private DateTime _GetNextTaskDate(string taskSlug)
{
if (_options.CurrentValue.Scheduled.TryGetValue(taskSlug, out TimeSpan delay))
return DateTime.Now + delay;
return DateTime.MaxValue;
}
/// <inheritdoc />
public ICollection<(TaskMetadataAttribute, ITask)> GetRunningTasks()
{
return _runningTask == null
? ArraySegment<(TaskMetadataAttribute, ITask)>.Empty
: new[] { _runningTask.Value };
}
/// <inheritdoc />
public ICollection<TaskMetadataAttribute> GetAllTasks()
{
return _tasks.Select(x => x.Metadata).ToArray();
}
}
}

View File

@ -20,13 +20,10 @@ using System;
using System.Collections.Generic;
using Autofac;
using Autofac.Extras.AttributeMetadata;
using Kyoo.Abstractions;
using Kyoo.Abstractions.Controllers;
using Kyoo.Core.Models.Options;
using Kyoo.Core.Tasks;
using Kyoo.Host.Controllers;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;
using Serilog;
namespace Kyoo.Host
@ -36,15 +33,9 @@ namespace Kyoo.Host
/// </summary>
public class HostModule : IPlugin
{
/// <inheritdoc />
public string Slug => "host";
/// <inheritdoc />
public string Name => "Host";
/// <inheritdoc />
public string Description => "A module that registers host controllers and other needed things.";
/// <inheritdoc />
public Dictionary<string, Type> Configuration => new()
{
@ -71,8 +62,6 @@ namespace Kyoo.Host
builder.RegisterModule<AttributedMetadataModule>();
builder.RegisterInstance(_plugins).As<IPluginManager>().ExternallyOwned();
builder.RegisterComposite<FileSystemComposite, IFileSystem>().InstancePerLifetimeScope();
builder.RegisterType<TaskManager>().As<ITaskManager>().As<IHostedService>().SingleInstance();
builder.RegisterTask<PluginInitializer>();
}
/// <inheritdoc />

View File

@ -1,81 +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.Threading;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models.Attributes;
namespace Kyoo.Core.Tasks
{
/// <summary>
/// A task run on Kyoo's startup to initialize plugins
/// </summary>
[TaskMetadata("plugin-init", "Plugin Initializer", "A task to initialize plugins.",
RunOnStartup = true, Priority = int.MaxValue, IsHidden = true)]
public class PluginInitializer : ITask
{
/// <summary>
/// The plugin manager used to retrieve plugins to initialize them.
/// </summary>
private readonly IPluginManager _pluginManager;
/// <summary>
/// The service provider given to each <see cref="IPlugin.Initialize"/> method.
/// </summary>
private readonly IServiceProvider _provider;
/// <summary>
/// Create a new <see cref="PluginInitializer"/> task
/// </summary>
/// <param name="pluginManager">The plugin manager used to retrieve plugins to initialize them.</param>
/// <param name="provider">The service provider given to each <see cref="IPlugin.Initialize"/> method.</param>
public PluginInitializer(IPluginManager pluginManager, IServiceProvider provider)
{
_pluginManager = pluginManager;
_provider = provider;
}
/// <inheritdoc />
public TaskParameters GetParameters()
{
return new();
}
/// <inheritdoc />
public Task Run(TaskParameters arguments, IProgress<float> progress, CancellationToken cancellationToken)
{
ICollection<IPlugin> plugins = _pluginManager.GetAllPlugins();
int count = 0;
progress.Report(0);
foreach (IPlugin plugin in plugins)
{
plugin.Initialize(_provider);
progress.Report(count / plugins.Count * 100);
count++;
}
progress.Report(100);
return Task.CompletedTask;
}
}
}

View File

@ -1,29 +1,10 @@
{
"basics": {
"pluginsPath": "plugins/",
"transmuxPath": "cached/transmux",
"transcodePath": "cached/transcode",
"metadataPath": "metadata/"
},
"tasks": {
"parallels": "1",
"scheduled": {
"scan": "24:00:00"
}
},
"media": {
"regex": [
"^[\\/\\\\]*(?<Collection>.+)?[\\/\\\\]+(?<Show>.+?)(?: \\((?<StartYear>\\d+)\\))?[\\/\\\\]+\\k<Show>(?: \\(\\d+\\))? S(?<Season>\\d+)E(?<Episode>\\d+)\\..*$",
"^[\\/\\\\]*(?<Collection>.+)?[\\/\\\\]+(?<Show>.+?)(?: \\((?<StartYear>\\d+)\\))?[\\/\\\\]+\\k<Show>(?: \\(\\d+\\))? (?<Absolute>\\d+)\\..*$",
"^[\\/\\\\]*(?<Collection>.+)?[\\/\\\\]+(?<Show>.+?)(?: \\((?<StartYear>\\d+)\\))?[\\/\\\\]+\\k<Show>(?: \\(\\d+\\))?\\..*$"
],
"subtitleRegex": [
"^(?<Episode>.+)\\.(?<Language>\\w{1,3})\\.(?<Default>default\\.)?(?<Forced>forced\\.)?.*$"
]
},
"authentication": {
"permissions": {
"default": ["overall.read", "overall.write"],

View File

@ -34,21 +34,12 @@ namespace Kyoo.Postgresql
/// </summary>
public class PostgresModule : IPlugin
{
/// <inheritdoc />
public string Slug => "postgresql";
/// <inheritdoc />
public string Name => "Postgresql";
/// <inheritdoc />
public string Description => "A database context for postgresql.";
/// <inheritdoc />
public Dictionary<string, Type> Configuration => new();
/// <inheritdoc />
public bool Enabled => true;
/// <summary>
/// The configuration to use. The database connection string is pulled from it.
/// </summary>

View File

@ -18,14 +18,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models.Utils;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using NJsonSchema;
using NJsonSchema.Generation.TypeMappers;
@ -40,32 +37,12 @@ namespace Kyoo.Swagger
/// </summary>
public class SwaggerModule : IPlugin
{
/// <inheritdoc />
public string Slug => "swagger";
/// <inheritdoc />
public string Name => "Swagger";
/// <inheritdoc />
public string Description => "A swagger interface and an OpenAPI endpoint to document Kyoo.";
/// <inheritdoc />
public Dictionary<string, Type> Configuration => new();
/// <summary>
/// The configuration instance used to retrieve the server's public url.
/// </summary>
private readonly IConfiguration _configuration;
/// <summary>
/// Create a new <see cref="SwaggerModule"/>.
/// </summary>
/// <param name="configuration">The configuration instance used to retrieve the server's public url.</param>
public SwaggerModule(IConfiguration configuration)
{
_configuration = configuration;
}
/// <inheritdoc />
public void Configure(IServiceCollection services)
{
@ -133,7 +110,7 @@ namespace Kyoo.Swagger
SA.New<IApplicationBuilder>(app => app.UseReDoc(x =>
{
x.Path = "/doc";
x.TransformToExternalPath = (internalUiRoute, request) => "/api" + internalUiRoute;
x.TransformToExternalPath = (internalUiRoute, _) => "/api" + internalUiRoute;
x.AdditionalSettings["theme"] = new
{
colors = new { primary = new { main = "#e13e13" } }