Allow requests to be cancelled.

Make workers have a CancellationTokenSource
This commit is contained in:
2025-09-01 23:26:49 +02:00
parent 6c61869e66
commit 3b8570cf57
31 changed files with 296 additions and 251 deletions

View File

@@ -1,6 +1,7 @@
using API.Schema.MangaContext;
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
@@ -15,11 +16,14 @@ public class FileLibraryController(MangaContext context) : Controller
/// Returns all <see cref="FileLibrary"/>
/// </summary>
/// <response code="200"></response>
/// <response code="500">Error during Database Operation</response>
[HttpGet]
[ProducesResponseType<FileLibrary[]>(Status200OK, "application/json")]
public IActionResult GetFileLibraries()
public async Task<IActionResult> GetFileLibraries ()
{
return Ok(context.FileLibraries.ToArray());
if(await context.FileLibraries.ToArrayAsync(HttpContext.RequestAborted) is not { } result)
return StatusCode(Status500InternalServerError);
return Ok(result);
}
/// <summary>
@@ -31,9 +35,9 @@ public class FileLibraryController(MangaContext context) : Controller
[HttpGet("{FileLibraryId}")]
[ProducesResponseType<FileLibrary>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetFileLibrary(string FileLibraryId)
public async Task<IActionResult> GetFileLibrary (string FileLibraryId)
{
if (context.FileLibraries.Find(FileLibraryId) is not { } library)
if(await context.FileLibraries.FirstOrDefaultAsync(l => l.Key == FileLibraryId, HttpContext.RequestAborted) is not { } library)
return NotFound();
return Ok(library);
@@ -51,15 +55,15 @@ public class FileLibraryController(MangaContext context) : Controller
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult ChangeLibraryBasePath(string FileLibraryId, [FromBody]string newBasePath)
public async Task<IActionResult> ChangeLibraryBasePath (string FileLibraryId, [FromBody]string newBasePath)
{
if (context.FileLibraries.Find(FileLibraryId) is not { } library)
if(await context.FileLibraries.FirstOrDefaultAsync(l => l.Key == FileLibraryId, HttpContext.RequestAborted) is not { } library)
return NotFound();
//TODO Path check
library.BasePath = newBasePath;
if(context.Sync() is { success: false } result)
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Ok();
}
@@ -77,21 +81,21 @@ public class FileLibraryController(MangaContext context) : Controller
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult ChangeLibraryName(string FileLibraryId, [FromBody] string newName)
public async Task<IActionResult> ChangeLibraryName (string FileLibraryId, [FromBody] string newName)
{
if (context.FileLibraries.Find(FileLibraryId) is not { } library)
if(await context.FileLibraries.FirstOrDefaultAsync(l => l.Key == FileLibraryId, HttpContext.RequestAborted) is not { } library)
return NotFound();
//TODO Name check
library.LibraryName = newName;
if(context.Sync() is { success: false } result)
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Ok();
}
/// <summary>
/// Creates new <see cref="FileLibraryId"/>
/// Creates new <see cref="FileLibrary"/>
/// </summary>
/// <param name="library">New <see cref="FileLibrary"/> to add</param>
/// <response code="200"></response>
@@ -99,13 +103,12 @@ public class FileLibraryController(MangaContext context) : Controller
[HttpPut]
[ProducesResponseType(Status201Created)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateNewLibrary([FromBody]FileLibrary library)
public async Task<IActionResult> CreateNewLibrary ([FromBody]FileLibrary library)
{
//TODO Parameter check
context.FileLibraries.Add(library);
if(context.Sync() is { success: false } result)
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Created();
}
@@ -120,14 +123,14 @@ public class FileLibraryController(MangaContext context) : Controller
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult DeleteLocalLibrary(string FileLibraryId)
public async Task<IActionResult> DeleteLocalLibrary (string FileLibraryId)
{
if (context.FileLibraries.Find(FileLibraryId) is not { } library)
if(await context.FileLibraries.FirstOrDefaultAsync(l => l.Key == FileLibraryId, HttpContext.RequestAborted) is not { } library)
return NotFound();
context.FileLibraries.Remove(library);
if(context.Sync() is { success: false } result)
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Ok();
}

View File

@@ -2,6 +2,7 @@
using API.Schema.LibraryContext.LibraryConnectors;
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
@@ -16,11 +17,13 @@ public class LibraryConnectorController(LibraryContext context) : Controller
/// Gets all configured <see cref="LibraryConnector"/>
/// </summary>
/// <response code="200"></response>
/// <response code="500">Error during Database Operation</response>
[HttpGet]
[ProducesResponseType<LibraryConnector[]>(Status200OK, "application/json")]
public IActionResult GetAllConnectors()
public async Task<IActionResult> GetAllConnectors ()
{
LibraryConnector[] connectors = context.LibraryConnectors.ToArray();
if (await context.LibraryConnectors.ToArrayAsync(HttpContext.RequestAborted) is not { } connectors)
return StatusCode(Status500InternalServerError);
return Ok(connectors);
}
@@ -34,9 +37,9 @@ public class LibraryConnectorController(LibraryContext context) : Controller
[HttpGet("{LibraryConnectorId}")]
[ProducesResponseType<LibraryConnector>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetConnector(string LibraryConnectorId)
public async Task<IActionResult> GetConnector (string LibraryConnectorId)
{
if (context.LibraryConnectors.Find(LibraryConnectorId) is not { } connector)
if (await context.LibraryConnectors.FirstOrDefaultAsync(l => l.Key == LibraryConnectorId) is not { } connector)
return NotFound();
return Ok(connector);
@@ -51,12 +54,12 @@ public class LibraryConnectorController(LibraryContext context) : Controller
[HttpPut]
[ProducesResponseType(Status201Created)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateConnector([FromBody]LibraryConnector libraryConnector)
public async Task<IActionResult> CreateConnector ([FromBody]LibraryConnector libraryConnector)
{
context.LibraryConnectors.Add(libraryConnector);
if(context.Sync() is { success: false } result)
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Created();
}
@@ -66,20 +69,20 @@ public class LibraryConnectorController(LibraryContext context) : Controller
/// </summary>
/// <param name="LibraryConnectorId">ToFileLibrary-Connector-ID</param>
/// <response code="200"></response>
/// <response code="404"><see cref="LibraryConnector"/> with <<paramref name="LibraryConnectorId"/> not found.</response>
/// <response code="404"><see cref="LibraryConnector"/> with <paramref name="LibraryConnectorId"/> not found.</response>
/// <response code="500">Error during Database Operation</response>
[HttpDelete("{LibraryConnectorId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult DeleteConnector(string LibraryConnectorId)
public async Task<IActionResult> DeleteConnector (string LibraryConnectorId)
{
if (context.LibraryConnectors.Find(LibraryConnectorId) is not { } connector)
if (await context.LibraryConnectors.FirstOrDefaultAsync(l => l.Key == LibraryConnectorId) is not { } connector)
return NotFound();
context.LibraryConnectors.Remove(connector);
if(context.Sync() is { success: false } result)
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Ok();
}

View File

@@ -21,15 +21,17 @@ public class MaintenanceController(MangaContext mangaContext) : Controller
[HttpPost("CleanupNoDownloadManga")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CleanupNoDownloadManga()
public async Task<IActionResult> CleanupNoDownloadManga()
{
Manga[] noDownloads = mangaContext.Mangas
.Include(m => m.MangaConnectorIds)
.Where(m => !m.MangaConnectorIds.Any(id => id.UseForDownload))
.ToArray();
if (await mangaContext.Mangas
.Include(m => m.MangaConnectorIds)
.Where(m => !m.MangaConnectorIds.Any(id => id.UseForDownload))
.ToArrayAsync(HttpContext.RequestAborted) is not { } noDownloads)
return StatusCode(Status500InternalServerError);
mangaContext.Mangas.RemoveRange(noDownloads);
if(mangaContext.Sync() is { success: false } result)
if(await mangaContext.Sync(HttpContext.RequestAborted) is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Ok();
}

View File

@@ -75,14 +75,14 @@ public class MangaConnectorController(MangaContext context) : Controller
[ProducesResponseType(Status202Accepted)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult SetEnabled(string MangaConnectorName, bool Enabled)
public async Task<IActionResult> SetEnabled(string MangaConnectorName, bool Enabled)
{
if(Tranga.MangaConnectors.FirstOrDefault(c => c.Name.Equals(MangaConnectorName, StringComparison.InvariantCultureIgnoreCase)) is not { } connector)
return NotFound();
connector.Enabled = Enabled;
if(context.Sync() is { success: false } result)
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Accepted();
}

View File

@@ -25,49 +25,65 @@ public class MangaController(MangaContext context) : Controller
/// Returns all cached <see cref="Manga"/>
/// </summary>
/// <response code="200"></response>
/// <response code="500">Error during Database Operation</response>
[HttpGet]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
public IActionResult GetAllManga()
public async Task<IActionResult> GetAllManga ()
{
return Ok(context.Mangas.ToArray());
if(await context.Mangas.ToArrayAsync(HttpContext.RequestAborted) is not { } result)
return StatusCode(Status500InternalServerError);
return Ok(result);
}
/// <summary>
/// Returns all cached <see cref="Manga"/>.Keys
/// </summary>
/// <response code="200"><see cref="Manga"/> Keys/IDs</response>
/// <response code="500">Error during Database Operation</response>
[HttpGet("Keys")]
[ProducesResponseType<string[]>(Status200OK, "application/json")]
public IActionResult GetAllMangaKeys()
public async Task<IActionResult> GetAllMangaKeys ()
{
return Ok(context.Mangas.Select(m => m.Key).ToArray());
if(await context.Mangas.Select(m => m.Key).ToArrayAsync(HttpContext.RequestAborted) is not { } result)
return StatusCode(Status500InternalServerError);
return Ok(result);
}
/// <summary>
/// Returns all <see cref="Manga"/> that are being downloaded from at least one <see cref="MangaConnector"/>
/// </summary>
/// <response code="200"></response>
/// <response code="500">Error during Database Operation</response>
[HttpGet("Downloading")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
public IActionResult GetMangaDownloading()
public async Task<IActionResult> GetMangaDownloading ()
{
Manga[] ret = context.MangaIncludeAll()
.Where(m => m.MangaConnectorIds.Any(id => id.UseForDownload))
.ToArray();
return Ok(ret);
if(await context.MangaIncludeAll()
.Where(m => m.MangaConnectorIds.Any(id => id.UseForDownload))
.ToArrayAsync(HttpContext.RequestAborted) is not { } result)
return StatusCode(Status500InternalServerError);
return Ok(result);
}
/// <summary>
/// Returns all cached <see cref="Manga"/> with <paramref name="MangaIds"/>
/// </summary>
/// <param name="MangaIds">Array of <<see cref="Manga"/>.Key</param>
/// <param name="MangaIds">Array of <see cref="Manga"/>.Key</param>
/// <response code="200"></response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("WithIDs")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
public IActionResult GetManga([FromBody]string[] MangaIds)
public async Task<IActionResult> GetManga ([FromBody]string[] MangaIds)
{
Manga[] ret = context.MangaIncludeAll().Where(m => MangaIds.Contains(m.Key)).ToArray();
return Ok(ret);
if(await context.MangaIncludeAll()
.Where(m => MangaIds.Contains(m.Key))
.ToArrayAsync(HttpContext.RequestAborted) is not { } result)
return StatusCode(Status500InternalServerError);
return Ok(result);
}
/// <summary>
@@ -79,10 +95,11 @@ public class MangaController(MangaContext context) : Controller
[HttpGet("{MangaId}")]
[ProducesResponseType<Manga>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetManga(string MangaId)
public async Task<IActionResult> GetManga (string MangaId)
{
if (context.MangaIncludeAll().FirstOrDefault(m => m.Key == MangaId) is not { } manga)
if (await context.MangaIncludeAll().FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return NotFound(nameof(MangaId));
return Ok(manga);
}
@@ -97,14 +114,14 @@ public class MangaController(MangaContext context) : Controller
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult DeleteManga(string MangaId)
public async Task<IActionResult> DeleteManga (string MangaId)
{
if (context.Mangas.Find(MangaId) is not { } manga)
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return NotFound(nameof(MangaId));
context.Mangas.Remove(manga);
if(context.Sync() is { success: false } result)
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Ok();
}
@@ -120,21 +137,20 @@ public class MangaController(MangaContext context) : Controller
[HttpPatch("{MangaIdFrom}/MergeInto/{MangaIdInto}")]
[ProducesResponseType<byte[]>(Status200OK,"image/jpeg")]
[ProducesResponseType(Status404NotFound)]
public IActionResult MergeIntoManga(string MangaIdFrom, string MangaIdInto)
public async Task<IActionResult> MergeIntoManga (string MangaIdFrom, string MangaIdInto)
{
if (context.Mangas.Find(MangaIdFrom) is not { } from)
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaIdFrom, HttpContext.RequestAborted) is not { } from)
return NotFound(nameof(MangaIdFrom));
if (context.Mangas.Find(MangaIdInto) is not { } into)
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaIdInto, HttpContext.RequestAborted) is not { } into)
return NotFound(nameof(MangaIdInto));
foreach (CollectionEntry collectionEntry in context.Entry(from).Collections)
collectionEntry.Load();
context.Entry(from).Navigation(nameof(Manga.Library)).Load();
await collectionEntry.LoadAsync(HttpContext.RequestAborted);
await context.Entry(from).Navigation(nameof(Manga.Library)).LoadAsync(HttpContext.RequestAborted);
foreach (CollectionEntry collectionEntry in context.Entry(into).Collections)
collectionEntry.Load();
context.Entry(into).Navigation(nameof(Manga.Library)).Load();
await collectionEntry.LoadAsync(HttpContext.RequestAborted);
await context.Entry(into).Navigation(nameof(Manga.Library)).LoadAsync(HttpContext.RequestAborted);
BaseWorker[] newJobs = into.MergeFrom(from, context);
Tranga.AddWorkers(newJobs);
@@ -159,9 +175,9 @@ public class MangaController(MangaContext context) : Controller
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
public IActionResult GetCover(string MangaId, [FromQuery]int? width, [FromQuery]int? height)
public async Task<IActionResult> GetCover (string MangaId, [FromQuery]int? width, [FromQuery]int? height)
{
if (context.Mangas.Find(MangaId) is not { } manga)
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return NotFound(nameof(MangaId));
if (!System.IO.File.Exists(manga.CoverFileNameInCache))
@@ -175,7 +191,7 @@ public class MangaController(MangaContext context) : Controller
return NoContent();
}
Image image = Image.Load(manga.CoverFileNameInCache);
Image image = await Image.LoadAsync(manga.CoverFileNameInCache, HttpContext.RequestAborted);
if (width is { } w && height is { } h)
{
@@ -189,7 +205,7 @@ public class MangaController(MangaContext context) : Controller
}
using MemoryStream ms = new();
image.Save(ms, new JpegEncoder(){Quality = 100});
await image.SaveAsync(ms, new JpegEncoder(){Quality = 100}, HttpContext.RequestAborted);
DateTime lastModified = new FileInfo(manga.CoverFileNameInCache).LastWriteTime;
HttpContext.Response.Headers.CacheControl = "public";
return File(ms.GetBuffer(), "image/jpeg", new DateTimeOffset(lastModified), EntityTagHeaderValue.Parse($"\"{lastModified.Ticks}\""));
@@ -204,12 +220,12 @@ public class MangaController(MangaContext context) : Controller
[HttpGet("{MangaId}/Chapters")]
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetChapters(string MangaId)
public async Task<IActionResult> GetChapters (string MangaId)
{
if (context.Mangas.Find(MangaId) is not { } manga)
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return NotFound(nameof(MangaId));
context.Entry(manga).Collection(m => m.Chapters).Load();
await context.Entry(manga).Collection(m => m.Chapters).LoadAsync();
Chapter[] chapters = manga.Chapters.ToArray();
return Ok(chapters);
@@ -226,12 +242,12 @@ public class MangaController(MangaContext context) : Controller
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
[ProducesResponseType(Status204NoContent)]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetChaptersDownloaded(string MangaId)
public async Task<IActionResult> GetChaptersDownloaded (string MangaId)
{
if (context.Mangas.Find(MangaId) is not { } manga)
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return NotFound(nameof(MangaId));
context.Entry(manga).Collection(m => m.Chapters).Load();
await context.Entry(manga).Collection(m => m.Chapters).LoadAsync();
List<Chapter> chapters = manga.Chapters.Where(c => c.Downloaded).ToList();
if (chapters.Count == 0)
@@ -251,12 +267,12 @@ public class MangaController(MangaContext context) : Controller
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
[ProducesResponseType(Status204NoContent)]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetChaptersNotDownloaded(string MangaId)
public async Task<IActionResult> GetChaptersNotDownloaded (string MangaId)
{
if (context.Mangas.Find(MangaId) is not { } manga)
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return NotFound(nameof(MangaId));
context.Entry(manga).Collection(m => m.Chapters).Load();
await context.Entry(manga).Collection(m => m.Chapters).LoadAsync(HttpContext.RequestAborted);
List<Chapter> chapters = manga.Chapters.Where(c => c.Downloaded == false).ToList();
if (chapters.Count == 0)
@@ -280,12 +296,12 @@ public class MangaController(MangaContext context) : Controller
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
public IActionResult GetLatestChapter(string MangaId)
public async Task<IActionResult> GetLatestChapter (string MangaId)
{
if (context.Mangas.Find(MangaId) is not { } manga)
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return NotFound(nameof(MangaId));
context.Entry(manga).Collection(m => m.Chapters).Load();
await context.Entry(manga).Collection(m => m.Chapters).LoadAsync(HttpContext.RequestAborted);
List<Chapter> chapters = manga.Chapters.ToList();
if (chapters.Count == 0)
@@ -303,8 +319,8 @@ public class MangaController(MangaContext context) : Controller
return StatusCode(Status500InternalServerError, "Max chapter could not be found");
foreach (CollectionEntry collectionEntry in context.Entry(max).Collections)
collectionEntry.Load();
context.Entry(max).Navigation(nameof(Chapter.ParentManga)).Load();
await collectionEntry.LoadAsync(HttpContext.RequestAborted);
await context.Entry(max).Navigation(nameof(Chapter.ParentManga)).LoadAsync(HttpContext.RequestAborted);
return Ok(max);
}
@@ -324,12 +340,12 @@ public class MangaController(MangaContext context) : Controller
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
public IActionResult GetLatestChapterDownloaded(string MangaId)
public async Task<IActionResult> GetLatestChapterDownloaded (string MangaId)
{
if (context.Mangas.Find(MangaId) is not { } manga)
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return NotFound(nameof(MangaId));
context.Entry(manga).Collection(m => m.Chapters).Load();
await context.Entry(manga).Collection(m => m.Chapters).LoadAsync(HttpContext.RequestAborted);
List<Chapter> chapters = manga.Chapters.ToList();
if (chapters.Count == 0)
@@ -347,8 +363,8 @@ public class MangaController(MangaContext context) : Controller
return StatusCode(Status412PreconditionFailed, "Max chapter could not be found");
foreach (CollectionEntry collectionEntry in context.Entry(max).Collections)
collectionEntry.Load();
context.Entry(max).Navigation(nameof(Chapter.ParentManga)).Load();
await collectionEntry.LoadAsync(HttpContext.RequestAborted);
await context.Entry(max).Navigation(nameof(Chapter.ParentManga)).LoadAsync(HttpContext.RequestAborted);
return Ok(max);
}
@@ -365,13 +381,13 @@ public class MangaController(MangaContext context) : Controller
[ProducesResponseType(Status202Accepted)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult IgnoreChaptersBefore(string MangaId, [FromBody]float chapterThreshold)
public async Task<IActionResult> IgnoreChaptersBefore (string MangaId, [FromBody]float chapterThreshold)
{
if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound();
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return NotFound(nameof(MangaId));
manga.IgnoreChaptersBefore = chapterThreshold;
if(context.Sync() is { success: false } result)
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Accepted();
@@ -387,16 +403,16 @@ public class MangaController(MangaContext context) : Controller
[HttpPost("{MangaId}/ChangeLibrary/{LibraryId}")]
[ProducesResponseType(Status202Accepted)]
[ProducesResponseType(Status404NotFound)]
public IActionResult ChangeLibrary(string MangaId, string LibraryId)
public async Task<IActionResult> ChangeLibrary (string MangaId, string LibraryId)
{
if (context.Mangas.Find(MangaId) is not { } manga)
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return NotFound(nameof(MangaId));
if(context.FileLibraries.Find(LibraryId) is not { } library)
if (await context.FileLibraries.FirstOrDefaultAsync(l => l.Key == LibraryId, HttpContext.RequestAborted) is not { } library)
return NotFound(nameof(LibraryId));
foreach (CollectionEntry collectionEntry in context.Entry(manga).Collections)
collectionEntry.Load();
context.Entry(manga).Navigation(nameof(Manga.Library)).Load();
await collectionEntry.LoadAsync(HttpContext.RequestAborted);
await context.Entry(manga).Navigation(nameof(Manga.Library)).LoadAsync(HttpContext.RequestAborted);
MoveMangaLibraryWorker moveLibrary = new(manga, library);
@@ -422,11 +438,11 @@ public class MangaController(MangaContext context) : Controller
[ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
[ProducesResponseType<string>(Status428PreconditionRequired, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult MarkAsRequested(string MangaId, string MangaConnectorName, bool IsRequested)
public async Task<IActionResult> MarkAsRequested (string MangaId, string MangaConnectorName, bool IsRequested)
{
if (context.Mangas.Find(MangaId) is null)
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } _)
return NotFound(nameof(MangaId));
if(!Tranga.TryGetMangaConnector(MangaConnectorName, out MangaConnector? mangaConnector))
if(!Tranga.TryGetMangaConnector(MangaConnectorName, out MangaConnector? _))
return NotFound(nameof(MangaConnectorName));
if (context.MangaConnectorToManga
@@ -440,7 +456,7 @@ public class MangaController(MangaContext context) : Controller
}
mcId.UseForDownload = IsRequested;
if(context.Sync() is { success: false } result)
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
@@ -463,9 +479,9 @@ public class MangaController(MangaContext context) : Controller
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status406NotAcceptable)]
public IActionResult SearchOnDifferentConnector(string MangaId, string MangaConnectorName)
public async Task<IActionResult> SearchOnDifferentConnector (string MangaId, string MangaConnectorName)
{
if (context.Mangas.Find(MangaId) is not { } manga)
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return NotFound(nameof(MangaId));
return new SearchController(context).SearchManga(MangaConnectorName, manga.Name);
@@ -479,10 +495,10 @@ public class MangaController(MangaContext context) : Controller
/// <response code="404"><see cref="Author"/> with <paramref name="AuthorId"/></response>
[HttpGet("WithAuthorId/{AuthorId}")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
public IActionResult GetMangaWithAuthorIds(string AuthorId)
public async Task<IActionResult> GetMangaWithAuthorIds (string AuthorId)
{
if (context.Authors.Find(AuthorId) is not { } author)
return NotFound();
if (await context.Authors.FirstOrDefaultAsync(a => a.Key == AuthorId, HttpContext.RequestAborted) is not { } author)
return NotFound(nameof(AuthorId));
return Ok(context.Mangas.Where(m => m.Authors.Contains(author)));
}
@@ -495,10 +511,10 @@ public class MangaController(MangaContext context) : Controller
/// <response code="404"><see cref="Tag"/> not found</response>
[HttpGet("WithTag/{Tag}")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
public IActionResult GetMangasWithTag(string Tag)
public async Task<IActionResult> GetMangasWithTag (string Tag)
{
if (context.Tags.Find(Tag) is not { } tag)
return NotFound();
if (await context.Tags.FirstOrDefaultAsync(t => t.Tag == Tag, HttpContext.RequestAborted) is not { } tag)
return NotFound(nameof(Tag));
return Ok(context.Mangas.Where(m => m.MangaTags.Contains(tag)));
}

View File

@@ -3,6 +3,7 @@ using API.Schema.MangaContext.MetadataFetchers;
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.EntityFrameworkCore;
using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
@@ -19,7 +20,7 @@ public class MetadataFetcherController(MangaContext context) : Controller
/// <response code="200">Names of <see cref="MetadataFetcher"/> (Metadata-Sites)</response>
[HttpGet]
[ProducesResponseType<string[]>(Status200OK, "application/json")]
public IActionResult GetConnectors()
public IActionResult GetConnectors ()
{
return Ok(Tranga.MetadataFetchers.Select(m => m.Name).ToArray());
}
@@ -28,11 +29,15 @@ public class MetadataFetcherController(MangaContext context) : Controller
/// Returns all <see cref="MetadataEntry"/>
/// </summary>
/// <response code="200"></response>
/// <response code="500">Error during Database Operation</response>
[HttpGet("Links")]
[ProducesResponseType<MetadataEntry[]>(Status200OK, "application/json")]
public IActionResult GetLinkedEntries()
public async Task<IActionResult> GetLinkedEntries ()
{
return Ok(context.MetadataEntries.ToArray());
if (await context.MetadataEntries.ToArrayAsync() is not { } result)
return StatusCode(Status500InternalServerError);
return Ok(result);
}
/// <summary>
@@ -48,10 +53,10 @@ public class MetadataFetcherController(MangaContext context) : Controller
[ProducesResponseType<MetadataSearchResult[]>(Status200OK, "application/json")]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)]
public IActionResult SearchMangaMetadata(string MangaId, string MetadataFetcherName, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)]string? searchTerm = null)
public async Task<IActionResult> SearchMangaMetadata(string MangaId, string MetadataFetcherName, [FromBody (EmptyBodyBehavior = EmptyBodyBehavior.Allow)]string? searchTerm = null)
{
if(context.Mangas.Find(MangaId) is not { } manga)
return NotFound();
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return NotFound(nameof(MangaId));
if(Tranga.MetadataFetchers.FirstOrDefault(f => f.Name == MetadataFetcherName) is not { } fetcher)
return BadRequest();
@@ -74,18 +79,18 @@ public class MetadataFetcherController(MangaContext context) : Controller
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult LinkMangaMetadata(string MangaId, string MetadataFetcherName, [FromBody]string Identifier)
public async Task<IActionResult> LinkMangaMetadata (string MangaId, string MetadataFetcherName, [FromBody]string Identifier)
{
if(context.Mangas.Find(MangaId) is not { } manga)
return NotFound();
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return NotFound(nameof(MangaId));
if(Tranga.MetadataFetchers.FirstOrDefault(f => f.Name == MetadataFetcherName) is not { } fetcher)
return BadRequest();
MetadataEntry entry = fetcher.CreateMetadataEntry(manga, Identifier);
context.MetadataEntries.Add(entry);
if(context.Sync() is { } errorMessage)
return StatusCode(Status500InternalServerError, errorMessage);
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Ok(entry);
}
@@ -103,10 +108,10 @@ public class MetadataFetcherController(MangaContext context) : Controller
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult UnlinkMangaMetadata(string MangaId, string MetadataFetcherName)
public async Task<IActionResult> UnlinkMangaMetadata (string MangaId, string MetadataFetcherName)
{
if(context.Mangas.Find(MangaId) is null)
return NotFound();
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } _)
return NotFound(nameof(MangaId));
if(Tranga.MetadataFetchers.FirstOrDefault(f => f.Name == MetadataFetcherName) is null)
return BadRequest();
if(context.MetadataEntries.FirstOrDefault(e => e.MangaId == MangaId && e.MetadataFetcherName == MetadataFetcherName) is not { } entry)
@@ -114,7 +119,7 @@ public class MetadataFetcherController(MangaContext context) : Controller
context.Remove(entry);
if(context.Sync() is { success: false } result)
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Ok();
}

View File

@@ -4,6 +4,7 @@ using API.Schema.NotificationsContext;
using API.Schema.NotificationsContext.NotificationConnectors;
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
@@ -19,12 +20,15 @@ public class NotificationConnectorController(NotificationsContext context) : Con
/// Gets all configured <see cref="NotificationConnector"/>
/// </summary>
/// <response code="200"></response>
/// <response code="500">Error during Database Operation</response>
[HttpGet]
[ProducesResponseType<NotificationConnector[]>(Status200OK, "application/json")]
public IActionResult GetAllConnectors()
public async Task<IActionResult> GetAllConnectors ()
{
if(await context.NotificationConnectors.ToArrayAsync(HttpContext.RequestAborted) is not { } result)
return StatusCode(Status500InternalServerError);
return Ok(context.NotificationConnectors.ToArray());
return Ok(result);
}
/// <summary>
@@ -36,10 +40,10 @@ public class NotificationConnectorController(NotificationsContext context) : Con
[HttpGet("{Name}")]
[ProducesResponseType<NotificationConnector>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetConnector(string Name)
public async Task<IActionResult> GetConnector (string Name)
{
if(context.NotificationConnectors.Find(Name) is not { } connector)
return NotFound();
if (await context.NotificationConnectors.FirstOrDefaultAsync(c => c.Name == Name, HttpContext.RequestAborted) is not { } connector)
return NotFound(nameof(Name));
return Ok(connector);
}
@@ -53,12 +57,12 @@ public class NotificationConnectorController(NotificationsContext context) : Con
[HttpPut]
[ProducesResponseType<string>(Status200OK, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateConnector([FromBody]NotificationConnector notificationConnector)
public async Task<IActionResult> CreateConnector ([FromBody]NotificationConnector notificationConnector)
{
context.NotificationConnectors.Add(notificationConnector);
context.Notifications.Add(new ("Added new Notification Connector!", notificationConnector.Name, NotificationUrgency.High));
if(context.Sync() is { success: false } result)
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Ok(notificationConnector.Name);
}
@@ -72,7 +76,7 @@ public class NotificationConnectorController(NotificationsContext context) : Con
[HttpPut("Gotify")]
[ProducesResponseType<string>(Status200OK, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateGotifyConnector([FromBody]GotifyRecord gotifyData)
public async Task<IActionResult> CreateGotifyConnector ([FromBody]GotifyRecord gotifyData)
{
//TODO Validate Data
@@ -81,7 +85,7 @@ public class NotificationConnectorController(NotificationsContext context) : Con
new Dictionary<string, string>() { { "X-Gotify-Key", gotifyData.AppToken } },
"POST",
$"{{\"message\": \"%text\", \"title\": \"%title\", \"Priority\": {gotifyData.Priority}}}");
return CreateConnector(gotifyConnector);
return await CreateConnector(gotifyConnector);
}
/// <summary>
@@ -93,7 +97,7 @@ public class NotificationConnectorController(NotificationsContext context) : Con
[HttpPut("Ntfy")]
[ProducesResponseType<string>(Status200OK, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateNtfyConnector([FromBody]NtfyRecord ntfyRecord)
public async Task<IActionResult> CreateNtfyConnector ([FromBody]NtfyRecord ntfyRecord)
{
//TODO Validate Data
@@ -108,7 +112,7 @@ public class NotificationConnectorController(NotificationsContext context) : Con
},
"POST",
$"{{\"message\": \"%text\", \"title\": \"%title\", \"Priority\": {ntfyRecord.Priority} \"Topic\": \"{ntfyRecord.Topic}\"}}");
return CreateConnector(ntfyConnector);
return await CreateConnector(ntfyConnector);
}
/// <summary>
@@ -120,7 +124,7 @@ public class NotificationConnectorController(NotificationsContext context) : Con
[HttpPut("Pushover")]
[ProducesResponseType<string>(Status200OK, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreatePushoverConnector([FromBody]PushoverRecord pushoverRecord)
public async Task<IActionResult> CreatePushoverConnector ([FromBody]PushoverRecord pushoverRecord)
{
//TODO Validate Data
@@ -129,7 +133,7 @@ public class NotificationConnectorController(NotificationsContext context) : Con
new Dictionary<string, string>(),
"POST",
$"{{\"token\": \"{pushoverRecord.AppToken}\", \"user\": \"{pushoverRecord.User}\", \"message:\":\"%text\", \"%title\" }}");
return CreateConnector(pushoverConnector);
return await CreateConnector(pushoverConnector);
}
/// <summary>
@@ -143,14 +147,14 @@ public class NotificationConnectorController(NotificationsContext context) : Con
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult DeleteConnector(string Name)
public async Task<IActionResult> DeleteConnector (string Name)
{
if(context.NotificationConnectors.Find(Name) is not { } connector)
return NotFound();
if (await context.NotificationConnectors.FirstOrDefaultAsync(c => c.Name == Name, HttpContext.RequestAborted) is not { } connector)
return NotFound(nameof(Name));
context.NotificationConnectors.Remove(connector);
if(context.Sync() is { success: false } result)
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Ok();
}

View File

@@ -1,7 +1,7 @@
using API.MangaConnectors;
using API.Schema.MangaContext;
using API.Schema.MangaContext;
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Soenneker.Utils.String.NeedlemanWunsch;
using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
@@ -22,10 +22,10 @@ public class QueryController(MangaContext context) : Controller
[HttpGet("Author/{AuthorId}")]
[ProducesResponseType<Author>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetAuthor(string AuthorId)
public async Task<IActionResult> GetAuthor (string AuthorId)
{
if (context.Authors.Find(AuthorId) is not { } author)
return NotFound();
if (await context.Authors.FirstOrDefaultAsync(a => a.Key == AuthorId, HttpContext.RequestAborted) is not { } author)
return NotFound(nameof(AuthorId));
return Ok(author);
}
@@ -39,10 +39,10 @@ public class QueryController(MangaContext context) : Controller
[HttpGet("Chapter/{ChapterId}")]
[ProducesResponseType<Chapter>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetChapter(string ChapterId)
public async Task<IActionResult> GetChapter (string ChapterId)
{
if (context.Chapters.Find(ChapterId) is not { } chapter)
return NotFound();
if (await context.Chapters.FirstOrDefaultAsync(c => c.Key == ChapterId, HttpContext.RequestAborted) is not { } chapter)
return NotFound(nameof(ChapterId));
return Ok(chapter);
}
@@ -56,10 +56,10 @@ public class QueryController(MangaContext context) : Controller
[HttpGet("Manga/MangaConnectorId/{MangaConnectorIdId}")]
[ProducesResponseType<MangaConnectorId<Manga>>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetMangaMangaConnectorId(string MangaConnectorIdId)
public async Task<IActionResult> GetMangaMangaConnectorId (string MangaConnectorIdId)
{
if(context.MangaConnectorToManga.Find(MangaConnectorIdId) is not { } mcIdManga)
return NotFound();
if (await context.MangaConnectorToManga.FirstOrDefaultAsync(c => c.Key == MangaConnectorIdId, HttpContext.RequestAborted) is not { } mcIdManga)
return NotFound(nameof(MangaConnectorIdId));
return Ok(mcIdManga);
}
@@ -70,18 +70,24 @@ public class QueryController(MangaContext context) : Controller
/// <param name="MangaId">Key of <see cref="Manga"/></param>
/// <response code="200"></response>
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
/// <response code="500">Error during Database Operation</response>
[HttpGet("Manga/{MangaId}/SimilarName")]
[ProducesResponseType<string[]>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetSimilarManga(string MangaId)
public async Task<IActionResult> GetSimilarManga (string MangaId)
{
if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound();
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return NotFound(nameof(MangaId));
string name = manga.Name;
Dictionary<string, string> mangaNames = context.Mangas.Where(m => m.Key != MangaId).ToDictionary(m => m.Key, m => m.Name);
if(await context.Mangas.Where(m => m.Key != MangaId).ToDictionaryAsync(m => m.Key, m => m.Name, HttpContext.RequestAborted) is not { } mangaNames)
return StatusCode(Status500InternalServerError);
string[] similarIds = mangaNames
.Where(kv => NeedlemanWunschStringUtil.CalculateSimilarityPercentage(name, kv.Value) > 0.8)
.Select(kv => kv.Key).ToArray();
return Ok(similarIds);
}
@@ -94,10 +100,10 @@ public class QueryController(MangaContext context) : Controller
[HttpGet("Chapter/MangaConnectorId/{MangaConnectorIdId}")]
[ProducesResponseType<MangaConnectorId<Chapter>>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetChapterMangaConnectorId(string MangaConnectorIdId)
public async Task<IActionResult> GetChapterMangaConnectorId (string MangaConnectorIdId)
{
if(context.MangaConnectorToChapter.Find(MangaConnectorIdId) is not { } mcIdChapter)
return NotFound();
if (await context.MangaConnectorToManga.FirstOrDefaultAsync(c => c.Key == MangaConnectorIdId, HttpContext.RequestAborted) is not { } mcIdChapter)
return NotFound(nameof(MangaConnectorIdId));
return Ok(mcIdChapter);
}

View File

@@ -24,7 +24,7 @@ public class SearchController(MangaContext context) : Controller
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status406NotAcceptable)]
public IActionResult SearchManga(string MangaConnectorName, string Query)
public IActionResult SearchManga (string MangaConnectorName, string Query)
{
if(Tranga.MangaConnectors.FirstOrDefault(c => c.Name.Equals(MangaConnectorName, StringComparison.InvariantCultureIgnoreCase)) is not { } connector)
return NotFound();
@@ -35,7 +35,7 @@ public class SearchController(MangaContext context) : Controller
List<Manga> retMangas = new();
foreach ((Manga manga, MangaConnectorId<Manga> mcId) manga in mangas)
{
if(Tranga.AddMangaToContext(manga, context, out Manga? add))
if(Tranga.AddMangaToContext(manga, context, out Manga? add, HttpContext.RequestAborted))
retMangas.Add(add);
}
@@ -54,7 +54,7 @@ public class SearchController(MangaContext context) : Controller
[ProducesResponseType<Manga>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status500InternalServerError)]
public IActionResult GetMangaFromUrl([FromBody]string url)
public IActionResult GetMangaFromUrl ([FromBody]string url)
{
if(Tranga.MangaConnectors.FirstOrDefault(c => c.Name.Equals("Global", StringComparison.InvariantCultureIgnoreCase)) is not { } connector)
return StatusCode(Status500InternalServerError, "Could not find Global Connector.");
@@ -62,7 +62,7 @@ public class SearchController(MangaContext context) : Controller
if(connector.GetMangaFromUrl(url) is not { } manga)
return NotFound();
if(Tranga.AddMangaToContext(manga, context, out Manga? add) == false)
if(Tranga.AddMangaToContext(manga, context, out Manga? add, HttpContext.RequestAborted) == false)
return StatusCode(Status500InternalServerError);
return Ok(add);

View File

@@ -1,5 +1,4 @@
using API.APIEndpointRecords;
using API.Workers;
using API.Workers;
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes;