Refactor Controllers

SettingsController.cs

SearchController.cs

QueryController.cs

NotificationConnectorController.cs

MetadataFetcherController.cs

MangaConnectorController.cs

FileLibraryController

LibraryConnectors

WorkerController
This commit is contained in:
2025-07-02 19:54:44 +02:00
parent 57bb87120a
commit 91c91e4989
28 changed files with 527 additions and 753 deletions

View File

@ -1,14 +1,14 @@
namespace API.APIEndpointRecords; namespace API.APIEndpointRecords;
public record GotifyRecord(string endpoint, string appToken, int priority) public record GotifyRecord(string Name, string Endpoint, string AppToken, int Priority)
{ {
public bool Validate() public bool Validate()
{ {
if (endpoint == string.Empty) if (Endpoint == string.Empty)
return false; return false;
if (appToken == string.Empty) if (AppToken == string.Empty)
return false; return false;
if (priority < 0 || priority > 10) if (Priority < 0 || Priority > 10)
return false; return false;
return true; return true;

View File

@ -1,3 +0,0 @@
namespace API.APIEndpointRecords;
public record ModifyJobRecord(ulong? RecurrenceMs, bool? Enabled);

View File

@ -0,0 +1,3 @@
namespace API.APIEndpointRecords;
public record ModifyWorkerRecord(ulong? IntervalMs);

View File

@ -1,13 +0,0 @@
namespace API.APIEndpointRecords;
public record NewLibraryRecord(string path, string name)
{
public bool Validate()
{
if (path.Length < 1) //TODO Better Path validation
return false;
if (name.Length < 1)
return false;
return true;
}
}

View File

@ -1,16 +1,16 @@
namespace API.APIEndpointRecords; namespace API.APIEndpointRecords;
public record NtfyRecord(string endpoint, string username, string password, string topic, int priority) public record NtfyRecord(string Name, string Endpoint, string Username, string Password, string Topic, int Priority)
{ {
public bool Validate() public bool Validate()
{ {
if (endpoint == string.Empty) if (Endpoint == string.Empty)
return false; return false;
if (username == string.Empty) if (Username == string.Empty)
return false; return false;
if (password == string.Empty) if (Password == string.Empty)
return false; return false;
if (priority < 1 || priority > 5) if (Priority < 1 || Priority > 5)
return false; return false;
return true; return true;
} }

View File

@ -1,12 +1,12 @@
namespace API.APIEndpointRecords; namespace API.APIEndpointRecords;
public record PushoverRecord(string apptoken, string user) public record PushoverRecord(string Name, string AppToken, string User)
{ {
public bool Validate() public bool Validate()
{ {
if (apptoken == string.Empty) if (AppToken == string.Empty)
return false; return false;
if (user == string.Empty) if (User == string.Empty)
return false; return false;
return true; return true;
} }

View File

@ -0,0 +1,141 @@
using API.Schema.MangaContext;
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{v:apiVersion}/[controller]")]
public class FileLibraryController(IServiceScope scope) : Controller
{
/// <summary>
/// Returns all <see cref="FileLibrary"/>
/// </summary>
/// <response code="200"></response>
[HttpGet]
[ProducesResponseType<FileLibrary[]>(Status200OK, "application/json")]
public IActionResult GetFileLibraries()
{
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
return Ok(context.FileLibraries.ToArray());
}
/// <summary>
/// Returns <see cref="FileLibrary"/> with <paramref name="FileLibraryId"/>
/// </summary>
/// <param name="FileLibraryId"><see cref="FileLibrary"/>.Key</param>
/// <response code="200"></response>
/// <response code="404"><see cref="FileLibrary"/> with <paramref name="FileLibraryId"/> not found.</response>
[HttpGet("{FileLibraryId}")]
[ProducesResponseType<FileLibrary>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetFileLibrary(string FileLibraryId)
{
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
if (context.FileLibraries.Find(FileLibraryId) is not { } library)
return NotFound();
return Ok(library);
}
/// <summary>
/// Changes the <see cref="FileLibraryId"/>.BasePath with <paramref name="FileLibraryId"/>
/// </summary>
/// <param name="FileLibraryId"><see cref="FileLibrary"/>.Key</param>
/// <param name="newBasePath">New <see cref="FileLibraryId"/>.BasePath</param>
/// <response code="200"></response>
/// <response code="404"><see cref="FileLibrary"/> with <paramref name="FileLibraryId"/> not found.</response>
/// <response code="500">Error during Database Operation</response>
[HttpPatch("{FileLibraryId}/ChangeBasePath")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult ChangeLibraryBasePath(string FileLibraryId, [FromBody]string newBasePath)
{
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
if (context.FileLibraries.Find(FileLibraryId) is not { } library)
return NotFound();
//TODO Path check
library.BasePath = newBasePath;
if(context.Sync().Result is { } errorMessage)
return StatusCode(Status500InternalServerError, errorMessage);
return Ok();
}
/// <summary>
/// Changes the <see cref="FileLibraryId"/>.LibraryName with <paramref name="FileLibraryId"/>
/// </summary>
/// <param name="FileLibraryId"><see cref="FileLibrary"/>.Key</param>
/// <param name="newName">New <see cref="FileLibraryId"/>.LibraryName</param>
/// <response code="200"></response>
/// <response code="404"><see cref="FileLibrary"/> with <paramref name="FileLibraryId"/> not found.</response>
/// <response code="500">Error during Database Operation</response>
[HttpPatch("{FileLibraryId}/ChangeName")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult ChangeLibraryName(string FileLibraryId, [FromBody] string newName)
{
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
if (context.FileLibraries.Find(FileLibraryId) is not { } library)
return NotFound();
//TODO Name check
library.LibraryName = newName;
if(context.Sync().Result is { } errorMessage)
return StatusCode(Status500InternalServerError, errorMessage);
return Ok();
}
/// <summary>
/// Creates new <see cref="FileLibraryId"/>
/// </summary>
/// <param name="library">New <see cref="FileLibrary"/> to add</param>
/// <response code="200"></response>
/// <response code="500">Error during Database Operation</response>
[HttpPut]
[ProducesResponseType(Status201Created)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateNewLibrary([FromBody]FileLibrary library)
{
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
//TODO Parameter check
context.FileLibraries.Add(library);
if(context.Sync().Result is { } errorMessage)
return StatusCode(Status500InternalServerError, errorMessage);
return Created();
}
/// <summary>
/// Deletes the <see cref="FileLibraryId"/>.LibraryName with <paramref name="FileLibraryId"/>
/// </summary>
/// <param name="FileLibraryId"><see cref="FileLibrary"/>.Key</param>
/// <response code="200"></response>
/// <response code="500">Error during Database Operation</response>
[HttpDelete("{FileLibraryId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult DeleteLocalLibrary(string FileLibraryId)
{
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
if (context.FileLibraries.Find(FileLibraryId) is not { } library)
return NotFound();
context.FileLibraries.Remove(library);
if(context.Sync().Result is { } errorMessage)
return StatusCode(Status500InternalServerError, errorMessage);
return Ok();
}
}

View File

@ -1,52 +1,54 @@
using API.Schema.LibraryContext; using API.Schema.LibraryContext;
using API.Schema.LibraryContext.LibraryConnectors; using API.Schema.LibraryContext.LibraryConnectors;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
namespace API.Controllers; namespace API.Controllers;
[ApiVersion(2)] [ApiVersion(2)]
[ApiController] [ApiController]
[Route("v{v:apiVersion}/[controller]")] [Route("v{v:apiVersion}/[controller]")]
public class LibraryConnectorController(LibraryContext context, ILog Log) : Controller public class LibraryConnectorController(IServiceScope scope) : Controller
{ {
/// <summary> /// <summary>
/// Gets all configured ToFileLibrary-Connectors /// Gets all configured <see cref="LibraryConnector"/>
/// </summary> /// </summary>
/// <response code="200"></response> /// <response code="200"></response>
[HttpGet] [HttpGet]
[ProducesResponseType<LibraryConnector[]>(Status200OK, "application/json")] [ProducesResponseType<LibraryConnector[]>(Status200OK, "application/json")]
public IActionResult GetAllConnectors() public IActionResult GetAllConnectors()
{ {
LibraryContext context = scope.ServiceProvider.GetRequiredService<LibraryContext>();
LibraryConnector[] connectors = context.LibraryConnectors.ToArray(); LibraryConnector[] connectors = context.LibraryConnectors.ToArray();
return Ok(connectors); return Ok(connectors);
} }
/// <summary> /// <summary>
/// Returns ToFileLibrary-Connector with requested ID /// Returns <see cref="LibraryConnector"/> with <paramref name="LibraryConnectorId"/>
/// </summary> /// </summary>
/// <param name="LibraryControllerId">ToFileLibrary-Connector-ID</param> /// <param name="LibraryConnectorId"><see cref="LibraryConnector"/>.Key</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">Connector with ID not found.</response> /// <response code="404"><see cref="LibraryConnector"/> with <paramref name="LibraryConnectorId"/> not found.</response>
[HttpGet("{LibraryControllerId}")] [HttpGet("{LibraryConnectorId}")]
[ProducesResponseType<LibraryConnector>(Status200OK, "application/json")] [ProducesResponseType<LibraryConnector>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult GetConnector(string LibraryControllerId) public IActionResult GetConnector(string LibraryConnectorId)
{ {
LibraryConnector? ret = context.LibraryConnectors.Find(LibraryControllerId); LibraryContext context = scope.ServiceProvider.GetRequiredService<LibraryContext>();
return (ret is not null) switch if (context.LibraryConnectors.Find(LibraryConnectorId) is not { } connector)
{ return NotFound();
true => Ok(ret),
false => NotFound() return Ok(connector);
};
} }
/// <summary> /// <summary>
/// Creates a new ToFileLibrary-Connector /// Creates a new <see cref="LibraryConnector"/>
/// </summary> /// </summary>
/// <param name="libraryConnector">ToFileLibrary-Connector</param> /// <param name="libraryConnector"></param>
/// <response code="201"></response> /// <response code="201"></response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut] [HttpPut]
@ -54,46 +56,36 @@ public class LibraryConnectorController(LibraryContext context, ILog Log) : Cont
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateConnector([FromBody]LibraryConnector libraryConnector) public IActionResult CreateConnector([FromBody]LibraryConnector libraryConnector)
{ {
try LibraryContext context = scope.ServiceProvider.GetRequiredService<LibraryContext>();
{
context.LibraryConnectors.Add(libraryConnector); context.LibraryConnectors.Add(libraryConnector);
context.SaveChanges();
if(context.Sync().Result is { } errorMessage)
return StatusCode(Status500InternalServerError, errorMessage);
return Created(); return Created();
} }
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
/// <summary> /// <summary>
/// Deletes the ToFileLibrary-Connector with the requested ID /// Deletes <see cref="LibraryConnector"/> with <paramref name="LibraryConnectorId"/>
/// </summary> /// </summary>
/// <param name="LibraryControllerId">ToFileLibrary-Connector-ID</param> /// <param name="LibraryConnectorId">ToFileLibrary-Connector-ID</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">Connector with ID not found.</response> /// <response code="404"><see cref="LibraryConnector"/> with <<paramref name="LibraryConnectorId"/> not found.</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpDelete("{LibraryControllerId}")] [HttpDelete("{LibraryConnectorId}")]
[ProducesResponseType(Status200OK)] [ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult DeleteConnector(string LibraryControllerId) public IActionResult DeleteConnector(string LibraryConnectorId)
{ {
try LibraryContext context = scope.ServiceProvider.GetRequiredService<LibraryContext>();
{ if (context.LibraryConnectors.Find(LibraryConnectorId) is not { } connector)
LibraryConnector? ret = context.LibraryConnectors.Find(LibraryControllerId);
if (ret is null)
return NotFound(); return NotFound();
context.Remove(ret); context.LibraryConnectors.Remove(connector);
context.SaveChanges();
if(context.Sync().Result is { } errorMessage)
return StatusCode(Status500InternalServerError, errorMessage);
return Ok(); return Ok();
} }
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
} }

View File

@ -1,163 +0,0 @@
using API.APIEndpointRecords;
using API.Schema.MangaContext;
using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes;
namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{v:apiVersion}/[controller]")]
public class LocalLibrariesController(MangaContext context, ILog Log) : Controller
{
[HttpGet]
[ProducesResponseType<FileLibrary[]>(Status200OK, "application/json")]
public IActionResult GetLocalLibraries()
{
return Ok(context.LocalLibraries);
}
[HttpGet("{LibraryId}")]
[ProducesResponseType<FileLibrary>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetLocalLibrary(string LibraryId)
{
FileLibrary? library = context.LocalLibraries.Find(LibraryId);
if (library is null)
return NotFound();
return Ok(library);
}
[HttpPatch("{LibraryId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult UpdateLocalLibrary(string LibraryId, [FromBody]NewLibraryRecord record)
{
FileLibrary? library = context.LocalLibraries.Find(LibraryId);
if (library is null)
return NotFound();
if (record.Validate() == false)
return BadRequest();
try
{
library.LibraryName = record.name;
library.BasePath = record.path;
context.SaveChanges();
return Ok();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
[HttpPatch("{LibraryId}/ChangeBasePath")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult ChangeLibraryBasePath(string LibraryId, [FromBody] string newBasePath)
{
try
{
FileLibrary? library = context.LocalLibraries.Find(LibraryId);
if (library is null)
return NotFound();
if (false) //TODO implement path check
return BadRequest();
library.BasePath = newBasePath;
context.SaveChanges();
return Ok();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
[HttpPatch("{LibraryId}/ChangeName")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult ChangeLibraryName(string LibraryId, [FromBody] string newName)
{
try
{
FileLibrary? library = context.LocalLibraries.Find(LibraryId);
if (library is null)
return NotFound();
if(newName.Length < 1)
return BadRequest();
library.LibraryName = newName;
context.SaveChanges();
return Ok();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
[HttpPut]
[ProducesResponseType<FileLibrary>(Status200OK, "application/json")]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateNewLibrary([FromBody]NewLibraryRecord library)
{
if (library.Validate() == false)
return BadRequest();
try
{
FileLibrary newFileLibrary = new (library.path, library.name);
context.LocalLibraries.Add(newFileLibrary);
context.SaveChanges();
return Ok(newFileLibrary);
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
[HttpDelete("{LibraryId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult DeleteLocalLibrary(string LibraryId)
{
try
{
FileLibrary? library = context.LocalLibraries.Find(LibraryId);
if (library is null)
return NotFound();
context.Remove(library);
context.SaveChanges();
return Ok();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
}

View File

@ -1,107 +1,95 @@
using API.Schema.MangaContext; using API.Schema.MangaContext;
using API.Schema.MangaContext.MangaConnectors; using API.Schema.MangaContext.MangaConnectors;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
namespace API.Controllers; namespace API.Controllers;
[ApiVersion(2)] [ApiVersion(2)]
[ApiController] [ApiController]
[Route("v{v:apiVersion}/[controller]")] [Route("v{v:apiVersion}/[controller]")]
public class MangaConnectorController(MangaContext context, ILog Log) : Controller public class MangaConnectorController(IServiceScope scope) : Controller
{ {
/// <summary> /// <summary>
/// Get all available Connectors (Scanlation-Sites) /// Get all <see cref="MangaConnector"/> (Scanlation-Sites)
/// </summary> /// </summary>
/// <response code="200"></response> /// <response code="200">Names of <see cref="MangaConnector"/> (Scanlation-Sites)</response>
[HttpGet] [HttpGet]
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")] [ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
public IActionResult GetConnectors() public IActionResult GetConnectors()
{ {
MangaConnector[] connectors = context.MangaConnectors.ToArray(); MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
return Ok(connectors); return Ok(context.MangaConnectors.Select(c => c.Name).ToArray());
} }
/// <summary> /// <summary>
/// Returns the MangaConnector with the requested Name /// Returns the <see cref="MangaConnector"/> (Scanlation-Sites) with the requested Name
/// </summary> /// </summary>
/// <param name="MangaConnectorName"></param> /// <param name="MangaConnectorName"><see cref="MangaConnector"/>.Name</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">Connector with ID not found.</response> /// <response code="404"><see cref="MangaConnector"/> (Scanlation-Sites) with Name not found.</response>
/// <response code="500">Error during Database Operation</response>
[HttpGet("{MangaConnectorName}")] [HttpGet("{MangaConnectorName}")]
[ProducesResponseType<MangaConnector>(Status200OK, "application/json")] [ProducesResponseType<MangaConnector>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetConnector(string MangaConnectorName) public IActionResult GetConnector(string MangaConnectorName)
{ {
try MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
{
if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector) if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector)
return NotFound(); return NotFound();
return Ok(connector); return Ok(connector);
} }
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
/// <summary> /// <summary>
/// Get all enabled Connectors (Scanlation-Sites) /// Get all enabled <see cref="MangaConnector"/> (Scanlation-Sites)
/// </summary> /// </summary>
/// <response code="200"></response> /// <response code="200"></response>
[HttpGet("enabled")] [HttpGet("Enabled")]
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")] [ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
public IActionResult GetEnabledConnectors() public IActionResult GetEnabledConnectors()
{ {
MangaConnector[] connectors = context.MangaConnectors.Where(c => c.Enabled == true).ToArray(); MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
return Ok(connectors);
return Ok(context.MangaConnectors.Where(c => c.Enabled).ToArray());
} }
/// <summary> /// <summary>
/// Get all disabled Connectors (Scanlation-Sites) /// Get all disabled <see cref="MangaConnector"/> (Scanlation-Sites)
/// </summary> /// </summary>
/// <response code="200"></response> /// <response code="200"></response>
[HttpGet("disabled")] [HttpGet("Disabled")]
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")] [ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
public IActionResult GetDisabledConnectors() public IActionResult GetDisabledConnectors()
{ {
MangaConnector[] connectors = context.MangaConnectors.Where(c => c.Enabled == false).ToArray(); MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
return Ok(connectors);
return Ok(context.MangaConnectors.Where(c => c.Enabled == false).ToArray());
} }
/// <summary> /// <summary>
/// Enabled or disables a Connector /// Enabled or disables <see cref="MangaConnector"/> (Scanlation-Sites) with Name
/// </summary> /// </summary>
/// <param name="MangaConnectorName">ID of the connector</param> /// <param name="MangaConnectorName"><see cref="MangaConnector"/>.Name</param>
/// <param name="enabled">Set true to enable</param> /// <param name="Enabled">Set true to enable, false to disable</param>
/// <response code="200"></response> /// <response code="202"></response>
/// <response code="404">Connector with ID not found.</response> /// <response code="404"><see cref="MangaConnector"/> (Scanlation-Sites) with Name not found.</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPatch("{MangaConnectorName}/SetEnabled/{enabled}")] [HttpPatch("{MangaConnectorName}/SetEnabled/{Enabled}")]
[ProducesResponseType(Status200OK)] [ProducesResponseType(Status202Accepted)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult SetEnabled(string MangaConnectorName, bool enabled) public IActionResult SetEnabled(string MangaConnectorName, bool Enabled)
{ {
try MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
{ if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector)
MangaConnector? connector = context.MangaConnectors.Find(MangaConnectorName);
if (connector is null)
return NotFound(); return NotFound();
connector.Enabled = enabled; connector.Enabled = Enabled;
context.SaveChanges();
return Ok(); if(context.Sync().Result is { } errorMessage)
} return StatusCode(Status500InternalServerError, errorMessage);
catch (Exception e) return Accepted();
{
Log.Error(e);
return StatusCode(500, e.Message);
}
} }
} }

View File

@ -2,9 +2,7 @@
using API.Schema.MangaContext.MangaConnectors; using API.Schema.MangaContext.MangaConnectors;
using API.Workers; using API.Workers;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
@ -352,7 +350,7 @@ public class MangaController(IServiceScope scope) : Controller
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>(); MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
if (context.Mangas.Find(MangaId) is not { } manga) if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(nameof(MangaId)); return NotFound(nameof(MangaId));
if(context.LocalLibraries.Find(LibraryId) is not { } library) if(context.FileLibraries.Find(LibraryId) is not { } library)
return NotFound(nameof(LibraryId)); return NotFound(nameof(LibraryId));
MoveMangaLibraryWorker moveLibrary = new(manga, library, scope); MoveMangaLibraryWorker moveLibrary = new(manga, library, scope);

View File

@ -1,7 +1,6 @@
using API.Schema.MangaContext; using API.Schema.MangaContext;
using API.Schema.MangaContext.MetadataFetchers; using API.Schema.MangaContext.MetadataFetchers;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
@ -12,47 +11,51 @@ namespace API.Controllers;
[ApiVersion(2)] [ApiVersion(2)]
[ApiController] [ApiController]
[Route("v{v:apiVersion}/[controller]")] [Route("v{v:apiVersion}/[controller]")]
public class MetadataFetcherController(MangaContext context, ILog Log) : Controller public class MetadataFetcherController(IServiceScope scope) : Controller
{ {
/// <summary> /// <summary>
/// Get all available Connectors (Metadata-Sites) /// Get all <see cref="MetadataFetcher"/> (Metadata-Sites)
/// </summary> /// </summary>
/// <response code="200">Names of Metadata-Fetchers</response> /// <response code="200">Names of <see cref="MetadataFetcher"/> (Metadata-Sites)</response>
[HttpGet] [HttpGet]
[ProducesResponseType<string[]>(Status200OK, "application/json")] [ProducesResponseType<string[]>(Status200OK, "application/json")]
public IActionResult GetConnectors() public IActionResult GetConnectors()
{ {
string[] connectors = Tranga.MetadataFetchers.Select(f => f.MetadataFetcherName).ToArray(); return Ok(Tranga.MetadataFetchers.Select(m => m.Name).ToArray());
return Ok(connectors);
} }
/// <summary> /// <summary>
/// Returns all Mangas which have a linked Metadata-Provider /// Returns all <see cref="MetadataEntry"/>
/// </summary> /// </summary>
/// <response code="200"></response> /// <response code="200"></response>
[HttpGet("Links")] [HttpGet("Links")]
[ProducesResponseType<MetadataEntry>(Status200OK, "application/json")] [ProducesResponseType<MetadataEntry[]>(Status200OK, "application/json")]
public IActionResult GetLinkedEntries() public IActionResult GetLinkedEntries()
{ {
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
return Ok(context.MetadataEntries.ToArray()); return Ok(context.MetadataEntries.ToArray());
} }
/// <summary> /// <summary>
/// Searches Metadata-Provider for Manga-Metadata /// Searches <see cref="MetadataFetcher"/> (Metadata-Sites) for Manga-Metadata
/// </summary> /// </summary>
/// <param name="searchTerm">Instead of using the Manga for search, use a specific term</param> /// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <param name="MetadataFetcherName"><see cref="MetadataFetcher"/>.Name</param>
/// <param name="searchTerm">Instead of using the <paramref name="MangaId"/> for search on Website, use a specific term</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="400">Metadata-fetcher with Name does not exist</response> /// <response code="400"><see cref="MetadataFetcher"/> (Metadata-Sites) with <paramref name="MetadataFetcherName"/> does not exist</response>
/// <response code="404">Manga with ID not found</response> /// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
[HttpPost("{MetadataFetcherName}/SearchManga/{MangaId}")] [HttpPost("{MetadataFetcherName}/SearchManga/{MangaId}")]
[ProducesResponseType<MetadataSearchResult[]>(Status200OK, "application/json")] [ProducesResponseType<MetadataSearchResult[]>(Status200OK, "application/json")]
[ProducesResponseType(Status400BadRequest)] [ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult SearchMangaMetadata(string MangaId, string MetadataFetcherName, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)]string? searchTerm = null) public IActionResult SearchMangaMetadata(string MangaId, string MetadataFetcherName, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)]string? searchTerm = null)
{ {
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
if(context.Mangas.Find(MangaId) is not { } manga) if(context.Mangas.Find(MangaId) is not { } manga)
return NotFound(); return NotFound();
if(Tranga.MetadataFetchers.FirstOrDefault(f => f.MetadataFetcherName == MetadataFetcherName) is not { } fetcher) if(Tranga.MetadataFetchers.FirstOrDefault(f => f.Name == MetadataFetcherName) is not { } fetcher)
return BadRequest(); return BadRequest();
MetadataSearchResult[] searchResults = searchTerm is null ? fetcher.SearchMetadataEntry(manga) : fetcher.SearchMetadataEntry(searchTerm); MetadataSearchResult[] searchResults = searchTerm is null ? fetcher.SearchMetadataEntry(manga) : fetcher.SearchMetadataEntry(searchTerm);
@ -60,45 +63,43 @@ public class MetadataFetcherController(MangaContext context, ILog Log) : Control
} }
/// <summary> /// <summary>
/// Links Metadata-Provider using Provider-Specific Identifier to Manga /// Links <see cref="MetadataFetcher"/> (Metadata-Sites) using Provider-Specific Identifier to <see cref="Manga"/>
/// </summary> /// </summary>
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <param name="MetadataFetcherName"><see cref="MetadataFetcher"/>.Name</param>
/// <param name="Identifier"><see cref="MetadataFetcherName"/>-Specific ID</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="400">Metadata-fetcher with Name does not exist</response> /// <response code="400"><see cref="MetadataFetcher"/> (Metadata-Sites) with <paramref name="MetadataFetcherName"/> does not exist</response>
/// <response code="404">Manga with ID not found</response> /// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPost("{MetadataFetcherName}/Link/{MangaId}")] [HttpPost("{MetadataFetcherName}/Link/{MangaId}")]
[ProducesResponseType(Status200OK)] [ProducesResponseType<MetadataEntry>(Status200OK, "application/json")]
[ProducesResponseType(Status400BadRequest)] [ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult LinkMangaMetadata(string MangaId, string MetadataFetcherName, [FromBody]string Identifier) public IActionResult LinkMangaMetadata(string MangaId, string MetadataFetcherName, [FromBody]string Identifier)
{ {
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
if(context.Mangas.Find(MangaId) is not { } manga) if(context.Mangas.Find(MangaId) is not { } manga)
return NotFound(); return NotFound();
if(Tranga.MetadataFetchers.FirstOrDefault(f => f.MetadataFetcherName == MetadataFetcherName) is not { } fetcher) if(Tranga.MetadataFetchers.FirstOrDefault(f => f.Name == MetadataFetcherName) is not { } fetcher)
return BadRequest(); return BadRequest();
MetadataEntry entry = fetcher.CreateMetadataEntry(manga, Identifier);
try MetadataEntry entry = fetcher.CreateMetadataEntry(manga, Identifier);
{
context.MetadataEntries.Add(entry); context.MetadataEntries.Add(entry);
context.SaveChanges();
} if(context.Sync().Result is { } errorMessage)
catch (Exception e) return StatusCode(Status500InternalServerError, errorMessage);
{ return Ok(entry);
Log.Error(e);
return StatusCode(500, e.Message);
}
return Ok();
} }
/// <summary> /// <summary>
/// Un-Links Metadata-Provider using Provider-Specific Identifier to Manga /// Un-Links <see cref="MetadataFetcher"/> (Metadata-Sites) from <see cref="Manga"/>
/// </summary> /// </summary>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="400">Metadata-fetcher with Name does not exist</response> /// <response code="400"><see cref="MetadataFetcher"/> (Metadata-Sites) with <paramref name="MetadataFetcherName"/> does not exist</response>
/// <response code="404">Manga with ID not found</response> /// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
/// <response code="412">No Entry linking Manga and Metadata-Provider found</response> /// <response code="412">No <see cref="MetadataEntry"/> linking <see cref="Manga"/> and <see cref="MetadataFetcher"/> found</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPost("{MetadataFetcherName}/Unlink/{MangaId}")] [HttpPost("{MetadataFetcherName}/Unlink/{MangaId}")]
[ProducesResponseType(Status200OK)] [ProducesResponseType(Status200OK)]
@ -108,58 +109,18 @@ public class MetadataFetcherController(MangaContext context, ILog Log) : Control
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult UnlinkMangaMetadata(string MangaId, string MetadataFetcherName) public IActionResult UnlinkMangaMetadata(string MangaId, string MetadataFetcherName)
{ {
if(context.Mangas.Find(MangaId) is not { } manga) MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
if(context.Mangas.Find(MangaId) is null)
return NotFound(); return NotFound();
if(Tranga.MetadataFetchers.FirstOrDefault(f => f.MetadataFetcherName == MetadataFetcherName) is not { } fetcher) if(Tranga.MetadataFetchers.FirstOrDefault(f => f.Name == MetadataFetcherName) is null)
return BadRequest(); return BadRequest();
MetadataEntry? entry = context.MetadataEntries.FirstOrDefault(e => e.MangaId == MangaId && e.MetadataFetcherName == MetadataFetcherName); if(context.MetadataEntries.FirstOrDefault(e => e.MangaId == MangaId && e.MetadataFetcherName == MetadataFetcherName) is not { } entry)
if (entry is null)
return StatusCode(Status412PreconditionFailed, "No entry found"); return StatusCode(Status412PreconditionFailed, "No entry found");
try
{
context.MetadataEntries.Remove(entry);
context.SaveChanges();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
return Ok();
}
/// <summary> context.Remove(entry);
/// Tries linking a Manga to a Metadata-Provider-Site
/// </summary> if(context.Sync().Result is { } errorMessage)
/// <response code="200"></response> return StatusCode(Status500InternalServerError, errorMessage);
/// <response code="400">MetadataFetcher Name is invalid</response>
/// <response code="404">Manga has no linked entry with MetadataFetcher</response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("{MetadataFetcherName}/{MangaId}/UpdateMetadata")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult UpdateMetadata(string MangaId, string MetadataFetcherName)
{
if(Tranga.MetadataFetchers
.FirstOrDefault(f =>
f.MetadataFetcherName.Equals(MetadataFetcherName, StringComparison.InvariantCultureIgnoreCase)) is not { } fetcher)
return BadRequest();
MetadataEntry? entry = context.MetadataEntries
.FirstOrDefault(e =>
e.MangaId == MangaId && e.MetadataFetcherName.Equals(MetadataFetcherName, StringComparison.InvariantCultureIgnoreCase));
if (entry is null)
return NotFound();
try
{
fetcher.UpdateMetadata(entry, context);
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
return Ok(); return Ok();
} }
} }

View File

@ -3,9 +3,9 @@ using API.APIEndpointRecords;
using API.Schema.NotificationsContext; using API.Schema.NotificationsContext;
using API.Schema.NotificationsContext.NotificationConnectors; using API.Schema.NotificationsContext.NotificationConnectors;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
namespace API.Controllers; namespace API.Controllers;
@ -13,121 +13,103 @@ namespace API.Controllers;
[ApiController] [ApiController]
[Produces("application/json")] [Produces("application/json")]
[Route("v{v:apiVersion}/[controller]")] [Route("v{v:apiVersion}/[controller]")]
public class NotificationConnectorController(NotificationsContext context, ILog Log) : Controller public class NotificationConnectorController(IServiceScope scope) : Controller
{ {
/// <summary> /// <summary>
/// Gets all configured Notification-Connectors /// Gets all configured <see cref="NotificationConnector"/>
/// </summary> /// </summary>
/// <response code="200"></response> /// <response code="200"></response>
[HttpGet] [HttpGet]
[ProducesResponseType<NotificationConnector[]>(Status200OK, "application/json")] [ProducesResponseType<NotificationConnector[]>(Status200OK, "application/json")]
public IActionResult GetAllConnectors() public IActionResult GetAllConnectors()
{ {
NotificationConnector[] ret = context.NotificationConnectors.ToArray(); NotificationsContext context = scope.ServiceProvider.GetRequiredService<NotificationsContext>();
return Ok(ret);
return Ok(context.NotificationConnectors.ToArray());
} }
/// <summary> /// <summary>
/// Returns Notification-Connector with requested ID /// Returns <see cref="NotificationConnector"/> with requested Name
/// </summary> /// </summary>
/// <param name="NotificationConnectorId">Notification-Connector-ID</param> /// <param name="Name"><see cref="NotificationConnector"/>.Name</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">NotificationConnector with ID not found</response> /// <response code="404"><see cref="NotificationConnector"/> with <paramref name="Name"/> not found</response>
[HttpGet("{NotificationConnectorId}")] [HttpGet("{Name}")]
[ProducesResponseType<NotificationConnector>(Status200OK, "application/json")] [ProducesResponseType<NotificationConnector>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult GetConnector(string NotificationConnectorId) public IActionResult GetConnector(string Name)
{ {
NotificationConnector? ret = context.NotificationConnectors.Find(NotificationConnectorId); NotificationsContext context = scope.ServiceProvider.GetRequiredService<NotificationsContext>();
return (ret is not null) switch if(context.NotificationConnectors.Find(Name) is not { } connector)
{ return NotFound();
true => Ok(ret),
false => NotFound() return Ok(connector);
};
} }
/// <summary> /// <summary>
/// Creates a new REST-Notification-Connector /// Creates a new <see cref="NotificationConnector"/>
/// </summary> /// </summary>
/// <remarks>Formatting placeholders: "%title" and "%text" can be placed in url, header-values and body and will be replaced when notifications are sent</remarks> /// <remarks>Formatting placeholders: "%title" and "%text" can be placed in url, header-values and body and will be replaced when notifications are sent</remarks>
/// <param name="notificationConnector">Notification-Connector</param> /// <response code="201"></response>
/// <response code="201">ID of new connector</response>
/// <response code="409">A NotificationConnector with name already exists</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut] [HttpPut]
[ProducesResponseType<string>(Status201Created, "application/json")] [ProducesResponseType(Status201Created)]
[ProducesResponseType(Status409Conflict)] [ProducesResponseType(Status409Conflict)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateConnector([FromBody]NotificationConnector notificationConnector) public IActionResult CreateConnector([FromBody]NotificationConnector notificationConnector)
{ {
if (context.NotificationConnectors.Find(notificationConnector.Name) is not null) NotificationsContext context = scope.ServiceProvider.GetRequiredService<NotificationsContext>();
return Conflict();
try
{
context.NotificationConnectors.Add(notificationConnector); context.NotificationConnectors.Add(notificationConnector);
context.SaveChanges();
return Created(notificationConnector.Name, notificationConnector); if(context.Sync().Result is { } errorMessage)
} return StatusCode(Status500InternalServerError, errorMessage);
catch (Exception e) return Created();
{
Log.Error(e);
return StatusCode(500, e.Message);
}
} }
/// <summary> /// <summary>
/// Creates a new Gotify-Notification-Connector /// Creates a new Gotify-<see cref="NotificationConnector"/>
/// </summary> /// </summary>
/// <remarks>Priority needs to be between 0 and 10</remarks> /// <remarks>Priority needs to be between 0 and 10</remarks>
/// <response code="201">ID of new connector</response> /// <response code="201"></response>
/// <response code="400"></response>
/// <response code="409">A NotificationConnector with name already exists</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut("Gotify")] [HttpPut("Gotify")]
[ProducesResponseType<string>(Status201Created, "application/json")] [ProducesResponseType<string>(Status201Created, "application/json")]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status409Conflict)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateGotifyConnector([FromBody]GotifyRecord gotifyData) public IActionResult CreateGotifyConnector([FromBody]GotifyRecord gotifyData)
{ {
if(!gotifyData.Validate()) //TODO Validate Data
return BadRequest();
NotificationConnector gotifyConnector = new NotificationConnector(TokenGen.CreateToken("Gotify"), NotificationConnector gotifyConnector = new (gotifyData.Name,
gotifyData.endpoint, gotifyData.Endpoint,
new Dictionary<string, string>() { { "X-Gotify-IDOnConnector", gotifyData.appToken } }, new Dictionary<string, string>() { { "X-Gotify-IDOnConnector", gotifyData.AppToken } },
"POST", "POST",
$"{{\"message\": \"%text\", \"title\": \"%title\", \"priority\": {gotifyData.priority}}}"); $"{{\"message\": \"%text\", \"title\": \"%title\", \"Priority\": {gotifyData.Priority}}}");
return CreateConnector(gotifyConnector); return CreateConnector(gotifyConnector);
} }
/// <summary> /// <summary>
/// Creates a new Ntfy-Notification-Connector /// Creates a new Ntfy-<see cref="NotificationConnector"/>
/// </summary> /// </summary>
/// <remarks>Priority needs to be between 1 and 5</remarks> /// <remarks>Priority needs to be between 1 and 5</remarks>
/// <response code="201">ID of new connector</response> /// <response code="201"></response>
/// <response code="400"></response>
/// <response code="409">A NotificationConnector with name already exists</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut("Ntfy")] [HttpPut("Ntfy")]
[ProducesResponseType<string>(Status201Created, "application/json")] [ProducesResponseType<string>(Status201Created, "application/json")]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status409Conflict)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateNtfyConnector([FromBody]NtfyRecord ntfyRecord) public IActionResult CreateNtfyConnector([FromBody]NtfyRecord ntfyRecord)
{ {
if(!ntfyRecord.Validate()) //TODO Validate Data
return BadRequest();
string authHeader = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{ntfyRecord.username}:{ntfyRecord.password}")); string authHeader = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{ntfyRecord.Username}:{ntfyRecord.Password}"));
string auth = Convert.ToBase64String(Encoding.UTF8.GetBytes(authHeader)).Replace("=",""); string auth = Convert.ToBase64String(Encoding.UTF8.GetBytes(authHeader)).Replace("=","");
NotificationConnector ntfyConnector = new (TokenGen.CreateToken("Ntfy"), NotificationConnector ntfyConnector = new (ntfyRecord.Name,
$"{ntfyRecord.endpoint}?auth={auth}", $"{ntfyRecord.Endpoint}/{ntfyRecord.Topic}?auth={auth}",
new Dictionary<string, string>() new Dictionary<string, string>()
{ {
{"Title", "%title"}, {"Title", "%title"},
{"Priority", ntfyRecord.priority.ToString()}, {"Priority", ntfyRecord.Priority.ToString()},
}, },
"POST", "POST",
"%text"); "%text");
@ -135,58 +117,47 @@ public class NotificationConnectorController(NotificationsContext context, ILog
} }
/// <summary> /// <summary>
/// Creates a new Pushover-Notification-Connector /// Creates a new Pushover-<see cref="NotificationConnector"/>
/// </summary> /// </summary>
/// <remarks>https://pushover.net/api</remarks> /// <remarks>https://pushover.net/api</remarks>
/// <response code="201">ID of new connector</response> /// <response code="201">ID of new connector</response>
/// <response code="400"></response>
/// <response code="409">A NotificationConnector with name already exists</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut("Pushover")] [HttpPut("Pushover")]
[ProducesResponseType<string>(Status201Created, "application/json")] [ProducesResponseType<string>(Status201Created, "application/json")]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status409Conflict)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreatePushoverConnector([FromBody]PushoverRecord pushoverRecord) public IActionResult CreatePushoverConnector([FromBody]PushoverRecord pushoverRecord)
{ {
if(!pushoverRecord.Validate()) //TODO Validate Data
return BadRequest();
NotificationConnector pushoverConnector = new (TokenGen.CreateToken("Pushover"), NotificationConnector pushoverConnector = new (pushoverRecord.Name,
$"https://api.pushover.net/1/messages.json", $"https://api.pushover.net/1/messages.json",
new Dictionary<string, string>(), new Dictionary<string, string>(),
"POST", "POST",
$"{{\"token\": \"{pushoverRecord.apptoken}\", \"user\": \"{pushoverRecord.user}\", \"message:\":\"%text\", \"%title\" }}"); $"{{\"token\": \"{pushoverRecord.AppToken}\", \"user\": \"{pushoverRecord.User}\", \"message:\":\"%text\", \"%title\" }}");
return CreateConnector(pushoverConnector); return CreateConnector(pushoverConnector);
} }
/// <summary> /// <summary>
/// Deletes the Notification-Connector with the requested ID /// Deletes the <see cref="NotificationConnector"/> with the requested Name
/// </summary> /// </summary>
/// <param name="NotificationConnectorId">Notification-Connector-ID</param> /// <param name="Name"><see cref="NotificationConnector"/>.Name</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">NotificationConnector with ID not found</response> /// <response code="404"><see cref="NotificationConnector"/> with Name not found</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpDelete("{NotificationConnectorId}")] [HttpDelete("{Name}")]
[ProducesResponseType(Status200OK)] [ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult DeleteConnector(string NotificationConnectorId) public IActionResult DeleteConnector(string Name)
{ {
try NotificationsContext context = scope.ServiceProvider.GetRequiredService<NotificationsContext>();
{ if(context.NotificationConnectors.Find(Name) is not { } connector)
NotificationConnector? ret = context.NotificationConnectors.Find(NotificationConnectorId);
if(ret is null)
return NotFound(); return NotFound();
context.Remove(ret); context.NotificationConnectors.Remove(connector);
context.SaveChanges();
return Ok(); if(context.Sync().Result is { } errorMessage)
} return StatusCode(Status500InternalServerError, errorMessage);
catch (Exception e) return Created();
{
Log.Error(e);
return StatusCode(500, e.Message);
}
} }
} }

View File

@ -1,6 +1,5 @@
using API.Schema.MangaContext; using API.Schema.MangaContext;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
@ -10,102 +9,74 @@ namespace API.Controllers;
[ApiVersion(2)] [ApiVersion(2)]
[ApiController] [ApiController]
[Route("v{v:apiVersion}/[controller]")] [Route("v{v:apiVersion}/[controller]")]
public class QueryController(MangaContext context, ILog Log) : Controller public class QueryController(IServiceScope scope) : Controller
{ {
/// <summary> /// <summary>
/// Returns the Author-Information for Author-ID /// Returns the <see cref="Author"/> with <paramref name="AuthorId"/>
/// </summary> /// </summary>
/// <param name="AuthorId">Author-Id</param> /// <param name="AuthorId"><see cref="Author"/>.Key</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">Author with ID not found</response> /// <response code="404"><see cref="Author"/> with <paramref name="AuthorId"/> not found</response>
[HttpGet("Author/{AuthorId}")] [HttpGet("Author/{AuthorId}")]
[ProducesResponseType<Author>(Status200OK, "application/json")] [ProducesResponseType<Author>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult GetAuthor(string AuthorId) public IActionResult GetAuthor(string AuthorId)
{ {
Author? ret = context.Authors.Find(AuthorId); MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
if (ret is null) if (context.Authors.Find(AuthorId) is not { } author)
return NotFound(); return NotFound();
return Ok(ret);
return Ok(author);
} }
/// <summary> /// <summary>
/// Returns all Mangas which where Authored by Author with AuthorId /// Returns all <see cref="Manga"/> which where Authored by <see cref="Author"/> with <paramref name="AuthorId"/>
/// </summary> /// </summary>
/// <param name="AuthorId">Author-ID</param> /// <param name="AuthorId"><see cref="Author"/>.Key</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">Author not found</response> /// <response code="404"><see cref="Author"/> with <paramref name="AuthorId"/></response>
[HttpGet("Mangas/WithAuthorId/{AuthorId}")] [HttpGet("Mangas/WithAuthorId/{AuthorId}")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")] [ProducesResponseType<Manga[]>(Status200OK, "application/json")]
public IActionResult GetMangaWithAuthorIds(string AuthorId) public IActionResult GetMangaWithAuthorIds(string AuthorId)
{ {
if(context.Authors.Find(AuthorId) is not { } a) MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
if (context.Authors.Find(AuthorId) is not { } author)
return NotFound(); return NotFound();
return Ok(context.Mangas.Where(m => m.Authors.Contains(a)));
} return Ok(context.Mangas.Where(m => m.Authors.Contains(author)));
/*
/// <summary>
/// Returns Link-Information for Link-Id
/// </summary>
/// <param name="LinkId"></param>
/// <response code="200"></response>
/// <response code="404">Link with ID not found</response>
[HttpGet("Link/{LinkId}")]
[ProducesResponseType<Link>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetLink(string LinkId)
{
Link? ret = context.Links.Find(LinkId);
if (ret is null)
return NotFound();
return Ok(ret);
} }
/// <summary> /// <summary>
/// Returns AltTitle-Information for AltTitle-Id /// Returns all <see cref="Manga"/> with <see cref="Tag"/>
/// </summary> /// </summary>
/// <param name="AltTitleId"></param> /// <param name="Tag"><see cref="Tag"/>.Tag</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">AltTitle with ID not found</response> /// <response code="404"><see cref="Tag"/> not found</response>
[HttpGet("AltTitle/{AltTitleId}")]
[ProducesResponseType<AltTitle>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetAltTitle(string AltTitleId)
{
AltTitle? ret = context.AltTitles.Find(AltTitleId);
if (ret is null)
return NotFound();
return Ok(ret);
}*/
/// <summary>
/// Returns all Obj with Tag
/// </summary>
/// <param name="Tag"></param>
/// <response code="200"></response>
/// <response code="404">Tag not found</response>
[HttpGet("Mangas/WithTag/{Tag}")] [HttpGet("Mangas/WithTag/{Tag}")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")] [ProducesResponseType<Manga[]>(Status200OK, "application/json")]
public IActionResult GetMangasWithTag(string Tag) public IActionResult GetMangasWithTag(string Tag)
{ {
if(context.Tags.Find(Tag) is not { } t) MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
if (context.Tags.Find(Tag) is not { } tag)
return NotFound(); return NotFound();
return Ok(context.Mangas.Where(m => m.MangaTags.Contains(t)));
return Ok(context.Mangas.Where(m => m.MangaTags.Contains(tag)));
} }
/// <summary> /// <summary>
/// Returns Chapter-Information for Chapter-Id /// Returns <see cref="Chapter"/> with <paramref name="ChapterId"/>
/// </summary> /// </summary>
/// <param name="ChapterId"></param> /// <param name="ChapterId"><see cref="Chapter"/>.Key</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">Chapter with ID not found</response> /// <response code="404"><see cref="Chapter"/> with <paramref name="ChapterId"/> not found</response>
[HttpGet("Chapter/{ChapterId}")] [HttpGet("Chapter/{ChapterId}")]
[ProducesResponseType<Chapter>(Status200OK, "application/json")] [ProducesResponseType<Chapter>(Status200OK, "application/json")]
public IActionResult GetChapter(string ChapterId) public IActionResult GetChapter(string ChapterId)
{ {
Chapter? ret = context.Chapters.Find(ChapterId); MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
if (ret is null) if (context.Chapters.Find(ChapterId) is not { } chapter)
return NotFound(); return NotFound();
return Ok(ret);
return Ok(chapter);
} }
} }

View File

@ -1,9 +1,7 @@
using API.Schema.MangaContext; using API.Schema.MangaContext;
using API.Schema.MangaContext.MangaConnectors;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Soenneker.Utils.String.NeedlemanWunsch;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
@ -12,98 +10,69 @@ namespace API.Controllers;
[ApiVersion(2)] [ApiVersion(2)]
[ApiController] [ApiController]
[Route("v{v:apiVersion}/[controller]")] [Route("v{v:apiVersion}/[controller]")]
public class SearchController(MangaContext context, ILog Log) : Controller public class SearchController(IServiceScope scope) : Controller
{ {
/// <summary> /// <summary>
/// Initiate a search for a Obj on a specific Connector /// Initiate a search for a <see cref="Manga"/> on <see cref="MangaConnector"/> with searchTerm
/// </summary> /// </summary>
/// <param name="MangaConnectorName"></param> /// <param name="MangaConnectorName"><see cref="MangaConnector"/>.Name</param>
/// <param name="Query"></param> /// <param name="Query">searchTerm</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">MangaConnector with ID not found</response> /// <response code="404"><see cref="MangaConnector"/> with Name not found</response>
/// <response code="406">MangaConnector with ID is disabled</response> /// <response code="412"><see cref="MangaConnector"/> with Name is disabled</response>
/// <response code="500">Error during Database Operation</response>
[HttpGet("{MangaConnectorName}/{Query}")] [HttpGet("{MangaConnectorName}/{Query}")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")] [ProducesResponseType<Manga[]>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status406NotAcceptable)] [ProducesResponseType(Status406NotAcceptable)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult SearchManga(string MangaConnectorName, string Query) public IActionResult SearchManga(string MangaConnectorName, string Query)
{ {
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector) if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector)
return NotFound(); return NotFound();
else if (connector.Enabled is false) if (connector.Enabled is false)
return StatusCode(Status406NotAcceptable); return StatusCode(Status412PreconditionFailed);
(Manga, MangaConnectorId<Manga>)[] mangas = connector.SearchManga(Query); (Manga, MangaConnectorId<Manga>)[] mangas = connector.SearchManga(Query);
List<Manga> retMangas = new(); List<Manga> retMangas = new();
foreach ((Manga manga, MangaConnectorId<Manga> mcId) manga in mangas) foreach ((Manga manga, MangaConnectorId<Manga> mcId) manga in mangas)
{ {
try if(AddMangaToContext(manga, context) is { } add)
{
if(AddMangaToContext(manga) is { } add)
retMangas.Add(add); retMangas.Add(add);
} }
catch (DbUpdateException e)
{
Log.Error(e);
return StatusCode(Status500InternalServerError, e.Message);
}
}
return Ok(retMangas.ToArray()); return Ok(retMangas.ToArray());
} }
/// <summary> /// <summary>
/// Search for a known Obj /// Returns <see cref="Manga"/> from the <see cref="MangaConnector"/> associated with <paramref name="url"/>
/// </summary> /// </summary>
/// <param name="Query"></param> /// <param name="url"></param>
/// <response code="200"></response> /// <response code="200"></response>
[HttpGet("Local/{Query}")] /// <response code="300">Multiple <see cref="MangaConnector"/> found for URL</response>
[ProducesResponseType<Manga[]>(Status200OK, "application/json")] /// <response code="404"><see cref="Manga"/> not found</response>
public IActionResult SearchMangaLocally(string Query)
{
Dictionary<Manga, double> distance = context.Mangas
.ToArray()
.ToDictionary(m => m, m => NeedlemanWunschStringUtil.CalculateSimilarityPercentage(Query, m.Name));
return Ok(distance.Where(kv => kv.Value > 50).OrderByDescending(kv => kv.Value).Select(kv => kv.Key).ToArray());
}
/// <summary>
/// Returns Obj from MangaConnector associated with URL
/// </summary>
/// <param name="url">Obj-Page URL</param>
/// <response code="200"></response>
/// <response code="300">Multiple connectors found for URL</response>
/// <response code="404">Obj not found</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPost("Url")] [HttpPost("Url")]
[ProducesResponseType<Manga>(Status200OK, "application/json")] [ProducesResponseType<Manga>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType(Status500InternalServerError)]
public IActionResult GetMangaFromUrl([FromBody]string url) public IActionResult GetMangaFromUrl([FromBody]string url)
{ {
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
if (context.MangaConnectors.Find("Global") is not { } connector) if (context.MangaConnectors.Find("Global") is not { } connector)
return StatusCode(Status500InternalServerError, "Could not find Global Connector."); return StatusCode(Status500InternalServerError, "Could not find Global Connector.");
if(connector.GetMangaFromUrl(url) is not { } manga) if(connector.GetMangaFromUrl(url) is not { } manga)
return NotFound(); return NotFound();
try
{ if(AddMangaToContext(manga, context) is not { } add)
if(AddMangaToContext(manga) is { } add)
return Ok(add);
return StatusCode(Status500InternalServerError); return StatusCode(Status500InternalServerError);
}
catch (DbUpdateException e) return Ok(add);
{
Log.Error(e);
return StatusCode(Status500InternalServerError, e.Message);
}
} }
private Manga? AddMangaToContext((Manga, MangaConnectorId<Manga>) manga) => AddMangaToContext(manga.Item1, manga.Item2, context); private Manga? AddMangaToContext((Manga, MangaConnectorId<Manga>) manga, MangaContext context) => AddMangaToContext(manga.Item1, manga.Item2, context);
internal static Manga? AddMangaToContext(Manga addManga, MangaConnectorId<Manga> addMcId, MangaContext context) private static Manga? AddMangaToContext(Manga addManga, MangaConnectorId<Manga> addMcId, MangaContext context)
{ {
Manga manga = context.Mangas.Find(addManga.Key) ?? addManga; Manga manga = context.Mangas.Find(addManga.Key) ?? addManga;
MangaConnectorId<Manga> mcId = context.MangaConnectorToManga.Find(addMcId.Key) ?? addMcId; MangaConnectorId<Manga> mcId = context.MangaConnectorToManga.Find(addMcId.Key) ?? addMcId;
@ -123,16 +92,12 @@ public class SearchController(MangaContext context, ILog Log) : Controller
}); });
manga.Authors = mergedAuthors.ToList(); manga.Authors = mergedAuthors.ToList();
try
{
if(context.MangaConnectorToManga.Find(addMcId.Key) is null) if(context.MangaConnectorToManga.Find(addMcId.Key) is null)
context.MangaConnectorToManga.Add(mcId); context.MangaConnectorToManga.Add(mcId);
context.SaveChanges();
} if (context.Sync().Result is not null)
catch (DbUpdateException e)
{
return null; return null;
}
return manga; return manga;
} }
} }

View File

@ -1,18 +1,18 @@
using API.MangaDownloadClients; using API.MangaDownloadClients;
using API.Schema.JobsContext.Jobs;
using API.Schema.MangaContext; using API.Schema.MangaContext;
using API.Workers;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
namespace API.Controllers; namespace API.Controllers;
[ApiVersion(2)] [ApiVersion(2)]
[ApiController] [ApiController]
[Route("v{v:apiVersion}/[controller]")] [Route("v{v:apiVersion}/[controller]")]
public class SettingsController(MangaContext context, ILog Log) : Controller public class SettingsController(IServiceScope scope) : Controller
{ {
/// <summary> /// <summary>
/// Get all Settings /// Get all Settings
@ -237,58 +237,25 @@ public class SettingsController(MangaContext context, ILog Log) : Controller
/// %C Chapter /// %C Chapter
/// %T Title /// %T Title
/// %A Author (first in list) /// %A Author (first in list)
/// %I Chapter Internal ID
/// %i Obj Internal ID
/// %Y Year (Obj) /// %Y Year (Obj)
/// ///
/// ?_(...) replace _ with a value from above: /// ?_(...) replace _ with a value from above:
/// Everything inside the braces will only be added if the value of %_ is not null /// Everything inside the braces will only be added if the value of %_ is not null
/// </remarks> /// </remarks>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="500">Error during Database Operation</response>
[HttpPatch("ChapterNamingScheme")] [HttpPatch("ChapterNamingScheme")]
[ProducesResponseType(Status200OK)] [ProducesResponseType(Status200OK)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult SetCustomNamingScheme([FromBody]string namingScheme) public IActionResult SetCustomNamingScheme([FromBody]string namingScheme)
{ {
try MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
{
Dictionary<Chapter, string> oldPaths = context.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath); Dictionary<Chapter, string> oldPaths = context.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
TrangaSettings.UpdateChapterNamingScheme(namingScheme); TrangaSettings.UpdateChapterNamingScheme(namingScheme);
MoveFileOrFolderJob[] newJobs = oldPaths MoveFileOrFolderWorker[] newJobs = oldPaths
.Select(kv => new MoveFileOrFolderJob(kv.Value, kv.Key.FullArchiveFilePath)).ToArray(); .Select(kv => new MoveFileOrFolderWorker(kv.Value, kv.Key.FullArchiveFilePath)).ToArray();
context.Jobs.AddRange(newJobs); Tranga.AddWorkers(newJobs);
return Ok();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e);
}
}
/// <summary> return Ok();
/// Creates a UpdateCoverJob for all Obj
/// </summary>
/// <response code="200">Array of JobIds</response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("CleanupCovers")]
[ProducesResponseType<string[]>(Status200OK)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CleanupCovers()
{
try
{
Tranga.RemoveStaleFiles(context);
List<UpdateCoverJob> newJobs = context.Mangas.ToList().Select(m => new UpdateCoverJob(m, 0)).ToList();
context.Jobs.AddRange(newJobs);
return Ok(newJobs.Select(j => j.Key));
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e);
}
} }
/// <summary> /// <summary>

View File

@ -84,110 +84,73 @@ public class WorkerController(ILog Log) : Controller
} }
/// <summary> /// <summary>
/// Modify Job with ID /// Modify <see cref="BaseWorker"/> with <paramref name="WorkerId"/>
/// </summary> /// </summary>
/// <param name="JobId">Job-ID</param> /// <param name="WorkerId"><see cref="BaseWorker"/>.Key</param>
/// <param name="modifyJobRecord">Fields to modify, set to null to keep previous value</param> /// <param name="modifyWorkerRecord">Fields to modify, set to null to keep previous value</param>
/// <response code="202">Job modified</response> /// <response code="202"></response>
/// <response code="400">Malformed request</response> /// <response code="400"></response>
/// <response code="404">Job with ID not found</response> /// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
/// <response code="500">Error during Database Operation</response> /// <response code="409"><see cref="BaseWorker"/> is not <see cref="IPeriodic"/>, can not modify <paramref name="modifyWorkerRecord.IntervalMs"/></response>
[HttpPatch("{JobId}")] [HttpPatch("{WorkerId}")]
[ProducesResponseType<Job>(Status202Accepted, "application/json")] [ProducesResponseType<BaseWorker>(Status202Accepted, "application/json")]
[ProducesResponseType(Status400BadRequest)] [ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status409Conflict, "text/plain")]
public IActionResult ModifyJob(string JobId, [FromBody]ModifyJobRecord modifyJobRecord) public IActionResult ModifyJob(string WorkerId, [FromBody]ModifyWorkerRecord modifyWorkerRecord)
{ {
try if(Tranga.Workers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
{ return NotFound(nameof(WorkerId));
Job? ret = context.Jobs.Find(JobId);
if(ret is null)
return NotFound();
ret.RecurrenceMs = modifyJobRecord.RecurrenceMs ?? ret.RecurrenceMs; if(modifyWorkerRecord.IntervalMs is not null && worker is not IPeriodic)
ret.Enabled = modifyJobRecord.Enabled ?? ret.Enabled; return Conflict("Can not modify Interval of non-Periodic worker");
else if(modifyWorkerRecord.IntervalMs is not null && worker is IPeriodic periodic)
periodic.Interval = TimeSpan.FromMilliseconds((long)modifyWorkerRecord.IntervalMs);
context.SaveChanges(); return Accepted(worker);
return new AcceptedResult(ret.Key, ret);
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
} }
/// <summary> /// <summary>
/// Starts the Job with the requested ID /// Starts <see cref="BaseWorker"/> with <paramref name="WorkerId"/>
/// </summary> /// </summary>
/// <param name="JobId">Job-ID</param> /// <param name="WorkerId"><see cref="BaseWorker"/>.Key</param>
/// <param name="startDependencies">Start Jobs necessary for execution</param> /// <response code="200"></response>
/// <response code="202">Job started</response> /// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
/// <response code="404">Job with ID not found</response> /// <response code="412"><see cref="BaseWorker"/> was already running</response>
/// <response code="409">Job was already running</response> [HttpPost("{WorkerId}/Start")]
/// <response code="500">Error during Database Operation</response>
[HttpPost("{JobId}/Start")]
[ProducesResponseType(Status202Accepted)] [ProducesResponseType(Status202Accepted)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status409Conflict)] [ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] public IActionResult StartJob(string WorkerId)
public IActionResult StartJob(string JobId, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)]bool startDependencies = false)
{ {
Job? ret = context.Jobs.Find(JobId); if(Tranga.Workers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
if (ret is null) return NotFound(nameof(WorkerId));
return NotFound();
List<Job> dependencies = startDependencies ? ret.GetDependenciesAndSelf() : [ret];
try if (worker.State >= WorkerExecutionState.Waiting)
{ return StatusCode(Status412PreconditionFailed, "Already running");
if(dependencies.Any(d => d.state >= JobState.Running && d.state < JobState.Completed))
return new ConflictResult();
dependencies.ForEach(d =>
{
d.LastExecution = DateTime.UnixEpoch;
d.state = JobState.CompletedWaiting;
});
context.SaveChanges();
return Accepted();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
/// <summary> Tranga.StartWorker(worker);
/// Stops the Job with the requested ID
/// </summary>
/// <param name="JobId">Job-ID</param>
/// <remarks><h1>NOT IMPLEMENTED</h1></remarks>
[HttpPost("{JobId}/Stop")]
[ProducesResponseType(Status501NotImplemented)]
public IActionResult StopJob(string JobId)
{
return StatusCode(Status501NotImplemented);
}
/// <summary>
/// Removes failed and completed Jobs (that are not recurring)
/// </summary>
/// <response code="202">Job started</response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("Cleanup")]
public IActionResult CleanupJobs()
{
try
{
context.Jobs.RemoveRange(context.Jobs.Where(j => j.state == JobState.Failed || j.state == JobState.Completed));
context.SaveChanges();
return Ok(); return Ok();
} }
catch (Exception e)
/// <summary>
/// Stops <see cref="BaseWorker"/> with <paramref name="WorkerId"/>
/// </summary>
/// <param name="WorkerId"><see cref="BaseWorker"/>.Key</param>
/// <response code="200"></response>
/// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
/// <response code="208"><see cref="BaseWorker"/> was not running</response>
[HttpPost("{WorkerId}/Stop")]
[ProducesResponseType(Status501NotImplemented)]
public IActionResult StopJob(string WorkerId)
{ {
Log.Error(e); if(Tranga.Workers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
return StatusCode(500, e.Message); return NotFound(nameof(WorkerId));
}
if(worker.State is < WorkerExecutionState.Running or >= WorkerExecutionState.Completed)
return StatusCode(Status208AlreadyReported, "Not running");
Tranga.StopWorker(worker);
return Ok();
} }
} }

View File

@ -118,8 +118,8 @@ using (IServiceScope scope = app.Services.CreateScope())
]; ];
MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray(); MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray();
context.MangaConnectors.AddRange(newConnectors); context.MangaConnectors.AddRange(newConnectors);
if (!context.LocalLibraries.Any()) if (!context.FileLibraries.Any())
context.LocalLibraries.Add(new FileLibrary(TrangaSettings.downloadLocation, "Default FileLibrary")); context.FileLibraries.Add(new FileLibrary(TrangaSettings.downloadLocation, "Default FileLibrary"));
context.Sync(); context.Sync();
} }

View File

@ -6,7 +6,7 @@ namespace API.Schema.LibraryContext.LibraryConnectors;
public class Kavita : LibraryConnector public class Kavita : LibraryConnector
{ {
public Kavita(string baseUrl, string auth) : base(TokenGen.CreateToken(typeof(Kavita), baseUrl), LibraryType.Kavita, baseUrl, auth) public Kavita(string baseUrl, string auth) : base(LibraryType.Kavita, baseUrl, auth)
{ {
} }

View File

@ -5,8 +5,7 @@ namespace API.Schema.LibraryContext.LibraryConnectors;
public class Komga : LibraryConnector public class Komga : LibraryConnector
{ {
public Komga(string baseUrl, string auth) : base(TokenGen.CreateToken(typeof(Komga), baseUrl), LibraryType.Komga, public Komga(string baseUrl, string auth) : base(LibraryType.Komga, baseUrl, auth)
baseUrl, auth)
{ {
} }

View File

@ -7,26 +7,51 @@ using Newtonsoft.Json;
namespace API.Schema.LibraryContext.LibraryConnectors; namespace API.Schema.LibraryContext.LibraryConnectors;
[PrimaryKey("LibraryConnectorId")] [PrimaryKey("LibraryConnectorId")]
public abstract class LibraryConnector(string libraryConnectorId, LibraryType libraryType, string baseUrl, string auth) public abstract class LibraryConnector : Identifiable
{ {
[StringLength(64)]
[Required] [Required]
public string LibraryConnectorId { get; } = libraryConnectorId; public LibraryType LibraryType { get; init; }
[Required]
public LibraryType LibraryType { get; init; } = libraryType;
[StringLength(256)] [StringLength(256)]
[Required] [Required]
[Url] [Url]
public string BaseUrl { get; init; } = baseUrl; public string BaseUrl { get; init; }
[StringLength(256)] [StringLength(256)]
[Required] [Required]
public string Auth { get; init; } = auth; public string Auth { get; init; }
[JsonIgnore] [JsonIgnore]
[NotMapped] [NotMapped]
protected ILog Log { get; init; } = LogManager.GetLogger($"{libraryType.ToString()} {baseUrl}"); protected ILog Log { get; init; }
protected LibraryConnector(LibraryType libraryType, string baseUrl, string auth)
: base()
{
this.LibraryType = libraryType;
this.BaseUrl = baseUrl;
this.Auth = auth;
this.Log = LogManager.GetLogger(GetType());
}
/// <summary>
/// EF CORE ONLY!!!!
/// </summary>
internal LibraryConnector(string key, LibraryType libraryType, string baseUrl, string auth)
: base(key)
{
this.LibraryType = libraryType;
this.BaseUrl = baseUrl;
this.Auth = auth;
this.Log = LogManager.GetLogger(GetType());
}
public override string ToString() => $"{base.ToString()} {this.LibraryType} {this.BaseUrl}";
protected abstract void UpdateLibraryInternal(); protected abstract void UpdateLibraryInternal();
internal abstract bool Test(); internal abstract bool Test();
} }
public enum LibraryType : byte
{
Komga = 0,
Kavita = 1
}

View File

@ -1,7 +0,0 @@
namespace API.Schema.LibraryContext.LibraryConnectors;
public enum LibraryType : byte
{
Komga = 0,
Kavita = 1
}

View File

@ -8,7 +8,7 @@ public class MangaContext(DbContextOptions<MangaContext> options) : TrangaBaseCo
{ {
public DbSet<MangaConnector> MangaConnectors { get; set; } public DbSet<MangaConnector> MangaConnectors { get; set; }
public DbSet<Manga> Mangas { get; set; } public DbSet<Manga> Mangas { get; set; }
public DbSet<FileLibrary> LocalLibraries { get; set; } public DbSet<FileLibrary> FileLibraries { get; set; }
public DbSet<Chapter> Chapters { get; set; } public DbSet<Chapter> Chapters { get; set; }
public DbSet<Author> Authors { get; set; } public DbSet<Author> Authors { get; set; }
public DbSet<MangaTag> Tags { get; set; } public DbSet<MangaTag> Tags { get; set; }

View File

@ -3,7 +3,7 @@ using Newtonsoft.Json;
namespace API.Schema.MangaContext.MetadataFetchers; namespace API.Schema.MangaContext.MetadataFetchers;
[PrimaryKey("MetadataFetcherName", "Identifier")] [PrimaryKey("Name", "Identifier")]
public class MetadataEntry public class MetadataEntry
{ {
[JsonIgnore] [JsonIgnore]
@ -19,7 +19,7 @@ public class MetadataEntry
this.Manga = manga; this.Manga = manga;
this.MangaId = manga.Key; this.MangaId = manga.Key;
this.MetadataFetcher = fetcher; this.MetadataFetcher = fetcher;
this.MetadataFetcherName = fetcher.MetadataFetcherName; this.MetadataFetcherName = fetcher.Name;
this.Identifier = identifier; this.Identifier = identifier;
} }

View File

@ -2,23 +2,23 @@ using Microsoft.EntityFrameworkCore;
namespace API.Schema.MangaContext.MetadataFetchers; namespace API.Schema.MangaContext.MetadataFetchers;
[PrimaryKey("MetadataFetcherName")] [PrimaryKey("Name")]
public abstract class MetadataFetcher public abstract class MetadataFetcher
{ {
// ReSharper disable once EntityFramework.ModelValidation.UnlimitedStringLength // ReSharper disable once EntityFramework.ModelValidation.UnlimitedStringLength
public string MetadataFetcherName { get; init; } public string Name { get; init; }
protected MetadataFetcher() protected MetadataFetcher()
{ {
this.MetadataFetcherName = this.GetType().Name; this.Name = this.GetType().Name;
} }
/// <summary> /// <summary>
/// EFCORE ONLY!!! /// EFCORE ONLY!!!
/// </summary> /// </summary>
internal MetadataFetcher(string metadataFetcherName) internal MetadataFetcher(string name)
{ {
this.MetadataFetcherName = metadataFetcherName; this.Name = name;
} }
internal MetadataEntry CreateMetadataEntry(Manga manga, string identifier) => internal MetadataEntry CreateMetadataEntry(Manga manga, string identifier) =>

View File

@ -71,4 +71,14 @@ public static class Tranga
Thread.Sleep(TrangaSettings.workCycleTimeout); Thread.Sleep(TrangaSettings.workCycleTimeout);
} }
} }
internal static void StartWorker(BaseWorker worker)
{
throw new NotImplementedException();
}
internal static void StopWorker(BaseWorker worker)
{
throw new NotImplementedException();
}
} }

View File

@ -13,7 +13,12 @@ public abstract class BaseWorker : Identifiable
internal WorkerExecutionState State { get; set; } internal WorkerExecutionState State { get; set; }
private static readonly CancellationTokenSource CancellationTokenSource = new(TimeSpan.FromMinutes(10)); private static readonly CancellationTokenSource CancellationTokenSource = new(TimeSpan.FromMinutes(10));
protected ILog Log { get; init; } protected ILog Log { get; init; }
public void Cancel() => CancellationTokenSource.Cancel();
public void Cancel()
{
this.State = WorkerExecutionState.Cancelled;
CancellationTokenSource.Cancel();
}
protected void Fail() => this.State = WorkerExecutionState.Failed; protected void Fail() => this.State = WorkerExecutionState.Failed;
public BaseWorker(IEnumerable<BaseWorker>? dependsOn = null) public BaseWorker(IEnumerable<BaseWorker>? dependsOn = null)
@ -58,5 +63,6 @@ public enum WorkerExecutionState
Created = 64, Created = 64,
Waiting = 96, Waiting = 96,
Running = 128, Running = 128,
Completed = 192 Completed = 192,
Cancelled = 193
} }

View File

@ -1,9 +1,9 @@
namespace API.Workers; namespace API.Workers;
public interface IPeriodic<T> where T : BaseWorker public interface IPeriodic
{ {
protected DateTime LastExecution { get; set; } protected DateTime LastExecution { get; set; }
protected TimeSpan Interval { get; set; } public TimeSpan Interval { get; set; }
public DateTime NextExecution => LastExecution.Add(Interval); public DateTime NextExecution => LastExecution.Add(Interval);
public bool IsDue => NextExecution <= DateTime.UtcNow;
} }