From cb14a7c31fbc9dfb82fbe3eb89deb925bb2f4e4c Mon Sep 17 00:00:00 2001 From: glax Date: Tue, 2 Sep 2025 22:26:50 +0200 Subject: [PATCH] Use DTOs to return API requests instead of Database Schema types. Make use of IHttpStatusCodeResults --- API/Controllers/DTOs/AltTitle.cs | 24 ++ API/Controllers/DTOs/Author.cs | 17 + API/Controllers/DTOs/Chapter.cs | 52 +++ API/Controllers/DTOs/Identifiable.cs | 18 + API/Controllers/DTOs/Link.cs | 25 ++ API/Controllers/DTOs/Manga.cs | 66 +++ API/Controllers/DTOs/MangaConnectorId.cs | 38 ++ API/Controllers/DTOs/MinimalManga.cs | 34 +- API/Controllers/DTOs/PeriodicWorker.cs | 33 ++ API/Controllers/DTOs/Worker.cs | 39 ++ API/Controllers/FileLibraryController.cs | 61 +-- API/Controllers/LibraryConnectorController.cs | 38 +- API/Controllers/MaintenanceController.cs | 9 +- API/Controllers/MangaConnectorController.cs | 41 +- API/Controllers/MangaController.cs | 396 ++++++++++-------- API/Controllers/MetadataFetcherController.cs | 59 +-- .../NotificationConnectorController.cs | 42 +- API/Controllers/QueryController.cs | 80 ++-- API/Controllers/SearchController.cs | 59 +-- API/Controllers/SettingsController.cs | 85 ++-- API/Controllers/WorkerController.cs | 75 ++-- API/Schema/MangaContext/Author.cs | 1 - API/Schema/MangaContext/Chapter.cs | 13 +- API/Schema/MangaContext/Link.cs | 2 - API/Schema/MangaContext/Manga.cs | 25 +- API/Schema/MangaContext/MangaConnectorId.cs | 9 +- API/Workers/BaseWorker.cs | 13 +- 27 files changed, 887 insertions(+), 467 deletions(-) create mode 100644 API/Controllers/DTOs/AltTitle.cs create mode 100644 API/Controllers/DTOs/Author.cs create mode 100644 API/Controllers/DTOs/Chapter.cs create mode 100644 API/Controllers/DTOs/Identifiable.cs create mode 100644 API/Controllers/DTOs/Link.cs create mode 100644 API/Controllers/DTOs/Manga.cs create mode 100644 API/Controllers/DTOs/MangaConnectorId.cs create mode 100644 API/Controllers/DTOs/PeriodicWorker.cs create mode 100644 API/Controllers/DTOs/Worker.cs diff --git a/API/Controllers/DTOs/AltTitle.cs b/API/Controllers/DTOs/AltTitle.cs new file mode 100644 index 0000000..bb06267 --- /dev/null +++ b/API/Controllers/DTOs/AltTitle.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace API.Controllers.DTOs; + +/// +/// DTO +/// +public sealed record AltTitle(string Language, string Title) +{ + /// + /// Language of the Title + /// + [Required] + [Description("Language of the Title")] + public string Language { get; init; } = Language; + + /// + /// Title + /// + [Required] + [Description("Title")] + public string Title { get; init; } = Title; +} \ No newline at end of file diff --git a/API/Controllers/DTOs/Author.cs b/API/Controllers/DTOs/Author.cs new file mode 100644 index 0000000..0e2403b --- /dev/null +++ b/API/Controllers/DTOs/Author.cs @@ -0,0 +1,17 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace API.Controllers.DTOs; + +/// +/// The DTO +/// +public record Author(string Key, string Name) : Identifiable(Key) +{ + /// + /// Name of the Author. + /// + [Required] + [Description("Name of the Author.")] + public string Name { get; init; } = Name; +} \ No newline at end of file diff --git a/API/Controllers/DTOs/Chapter.cs b/API/Controllers/DTOs/Chapter.cs new file mode 100644 index 0000000..aab2860 --- /dev/null +++ b/API/Controllers/DTOs/Chapter.cs @@ -0,0 +1,52 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace API.Controllers.DTOs; + +/// +/// DTO +/// +public record Chapter(string Key, string MangaId, int? Volume, string ChapterNumber, string? Title, IEnumerable MangaConnectorIds, bool Downloaded) : Identifiable(Key) +{ + /// + /// Identifier of the Manga this Chapter belongs to + /// + [Required] + [Description("Identifier of the Manga this Chapter belongs to")] + public string MangaId { get; init; } = MangaId; + + /// + /// Volume number + /// + [Required] + [Description("Volume number")] + public int? Volume { get; init; } = Volume; + + /// + /// Chapter number + /// + [Required] + [Description("Chapter number")] + public string ChapterNumber { get; init; } = ChapterNumber; + + /// + /// Title of the Chapter + /// + [Required] + [Description("Title of the Chapter")] + public string? Title { get; init; } = Title; + + /// + /// Whether Chapter is Downloaded (on disk) + /// + [Required] + [Description("Whether Chapter is Downloaded (on disk)")] + public bool Downloaded { get; init; } = Downloaded; + + /// + /// Ids of the Manga on MangaConnectors + /// + [Required] + [Description("Ids of the Manga on MangaConnectors")] + public IEnumerable MangaConnectorIds { get; init; } = MangaConnectorIds; +} \ No newline at end of file diff --git a/API/Controllers/DTOs/Identifiable.cs b/API/Controllers/DTOs/Identifiable.cs new file mode 100644 index 0000000..4d20ab2 --- /dev/null +++ b/API/Controllers/DTOs/Identifiable.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace API.Controllers.DTOs; + +/// +/// +/// +public record Identifiable(string Key) +{ + /// + /// Unique Identifier of the DTO + /// + [Required] + [Description("Unique Identifier of the DTO")] + [StringLength(TokenGen.MaximumLength, MinimumLength = TokenGen.MinimumLength)] + public string Key { get; init; } = Key; +} \ No newline at end of file diff --git a/API/Controllers/DTOs/Link.cs b/API/Controllers/DTOs/Link.cs new file mode 100644 index 0000000..0ed1439 --- /dev/null +++ b/API/Controllers/DTOs/Link.cs @@ -0,0 +1,25 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace API.Controllers.DTOs; + + +/// +/// DTO +/// +public sealed record Link(string Key, string Provider, string Url) : Identifiable(Key) +{ + /// + /// Name of the Provider + /// + [Required] + [Description("Name of the Provider")] + public string Provider { get; init; } = Provider; + + /// + /// Url + /// + [Required] + [Description("Url")] + public string Url { get; init; } = Url; +} \ No newline at end of file diff --git a/API/Controllers/DTOs/Manga.cs b/API/Controllers/DTOs/Manga.cs new file mode 100644 index 0000000..587e1b2 --- /dev/null +++ b/API/Controllers/DTOs/Manga.cs @@ -0,0 +1,66 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using API.Schema.MangaContext; + +namespace API.Controllers.DTOs; + +/// +/// DTO +/// +public record Manga(string Key, string Name, string Description, MangaReleaseStatus ReleaseStatus, IEnumerable MangaConnectorIds, float IgnoreChaptersBefore, uint? Year, string? OriginalLanguage, IEnumerable ChapterIds, IEnumerable Authors, IEnumerable Tags, IEnumerable Links, IEnumerable AltTitles) + : MinimalManga(Key, Name, Description, ReleaseStatus, MangaConnectorIds) +{ + /// + /// Chapter cutoff for Downloads (Chapters before this will not be downloaded) + /// + [Required] + [Description("Chapter cutoff for Downloads (Chapters before this will not be downloaded)")] + public float IgnoreChaptersBefore { get; init; } = IgnoreChaptersBefore; + + /// + /// Release Year + /// + [Description("Release Year")] + public uint? Year { get; init; } = Year; + + /// + /// Release Language + /// + [Description("Release Language")] + public string? OriginalLanguage { get; init; } = OriginalLanguage; + + /// + /// Keys of ChapterDTOs + /// + [Required] + [Description("Keys of ChapterDTOs")] + public IEnumerable ChapterIds { get; init; } = ChapterIds; + + /// + /// Author-names + /// + [Required] + [Description("Author-names")] + public IEnumerable Authors { get; init; } = Authors; + + /// + /// Manga Tags + /// + [Required] + [Description("Manga Tags")] + public IEnumerable Tags { get; init; } = Tags; + + /// + /// Links for more Metadata + /// + [Required] + [Description("Links for more Metadata")] + public IEnumerable Links { get; init; } = Links; + + /// + /// Alt Titles of Manga + /// + [Required] + [Description("Alt Titles of Manga")] + public IEnumerable AltTitles { get; init; } = AltTitles; +} \ No newline at end of file diff --git a/API/Controllers/DTOs/MangaConnectorId.cs b/API/Controllers/DTOs/MangaConnectorId.cs new file mode 100644 index 0000000..b9c4892 --- /dev/null +++ b/API/Controllers/DTOs/MangaConnectorId.cs @@ -0,0 +1,38 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using API.Schema.MangaContext; + +namespace API.Controllers.DTOs; + +/// +/// DTO +/// +public sealed record MangaConnectorId(string Key, string MangaConnectorName, string ForeignKey, string? WebsiteUrl, bool UseForDownload) : Identifiable(Key) +{ + /// + /// Name of the Connector + /// + [Required] + [Description("Name of the Connector")] + public string MangaConnectorName { get; init; } = MangaConnectorName; + + /// + /// Key of the referenced DTO + /// + [Required] + [Description("Key of the referenced DTO")] + public string ForeignKey { get; init; } = ForeignKey; + + /// + /// Website Link for reference, if any + /// + [Description("Website Link for reference, if any")] + public string? WebsiteUrl { get; init; } = WebsiteUrl; + + /// + /// Whether this Link is used for downloads + /// + [Required] + [Description("Whether this Link is used for downloads")] + public bool UseForDownload { get; init; } = UseForDownload; +} \ No newline at end of file diff --git a/API/Controllers/DTOs/MinimalManga.cs b/API/Controllers/DTOs/MinimalManga.cs index 32e4ad8..72e778a 100644 --- a/API/Controllers/DTOs/MinimalManga.cs +++ b/API/Controllers/DTOs/MinimalManga.cs @@ -1,21 +1,39 @@ +using System.ComponentModel; using System.ComponentModel.DataAnnotations; using API.Schema.MangaContext; -using Newtonsoft.Json; namespace API.Controllers.DTOs; -public sealed record MinimalManga(string Key, string Name, string Description, MangaReleaseStatus ReleaseStatus, IEnumerable>? MangaConnectorIds = null) +/// +/// Shortened Version of +/// +public record MinimalManga(string Key, string Name, string Description, MangaReleaseStatus ReleaseStatus, IEnumerable MangaConnectorIds) : Identifiable(Key) { - [Required] [StringLength(TokenGen.MaximumLength, MinimumLength = TokenGen.MinimumLength)] - public string Key { get; init; } = Key; + /// + /// Name of the Manga + /// [Required] - [JsonRequired] + [Description("Name of the Manga")] public string Name { get; init; } = Name; + + /// + /// Description of the Manga + /// [Required] - [JsonRequired] + [Description("Description of the Manga")] public string Description { get; init; } = Description; + + /// + /// ReleaseStatus of the Manga + /// [Required] - [JsonRequired] + [Description("ReleaseStatus of the Manga")] public MangaReleaseStatus ReleaseStatus { get; init; } = ReleaseStatus; - public IEnumerable>? MangaConnectorIds { get; init; } = MangaConnectorIds; + + /// + /// Ids of the Manga on MangaConnectors + /// + [Required] + [Description("Ids of the Manga on MangaConnectors")] + public IEnumerable MangaConnectorIds { get; init; } = MangaConnectorIds; } \ No newline at end of file diff --git a/API/Controllers/DTOs/PeriodicWorker.cs b/API/Controllers/DTOs/PeriodicWorker.cs new file mode 100644 index 0000000..4433618 --- /dev/null +++ b/API/Controllers/DTOs/PeriodicWorker.cs @@ -0,0 +1,33 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using API.Workers; + +namespace API.Controllers.DTOs; + +/// +/// DTO ( ) +/// +public sealed record PeriodicWorker(string Key, IEnumerable Dependencies, IEnumerable MissingDependencies, bool DependenciesFulfilled, WorkerExecutionState State, DateTime LastExecution, TimeSpan Interval, DateTime NextExecution) + : Worker(Key, Dependencies, MissingDependencies, DependenciesFulfilled, State) +{ + /// + /// Timestamp when Worker executed last. + /// + [Required] + [Description("Timestamp when Worker executed last.")] + public DateTime LastExecution { get; init; } = LastExecution; + + /// + /// Interval at which Worker runs. + /// + [Required] + [Description("Interval at which Worker runs.")] + public TimeSpan Interval { get; init; } = Interval; + + /// + /// Timestamp when Worker is scheduled to execute next. + /// + [Required] + [Description("Timestamp when Worker is scheduled to execute next.")] + public DateTime NextExecution { get; init; } = LastExecution; +} \ No newline at end of file diff --git a/API/Controllers/DTOs/Worker.cs b/API/Controllers/DTOs/Worker.cs new file mode 100644 index 0000000..8f9292c --- /dev/null +++ b/API/Controllers/DTOs/Worker.cs @@ -0,0 +1,39 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using API.Workers; + +namespace API.Controllers.DTOs; + +/// +/// DTO +/// +public record Worker(string Key, IEnumerable Dependencies, IEnumerable MissingDependencies, bool DependenciesFulfilled, WorkerExecutionState State) : Identifiable(Key) +{ + /// + /// Workers this worker depends on having ran. + /// + [Required] + [Description("Workers this worker depends on having ran.")] + public IEnumerable Dependencies { get; init; } = Dependencies; + + /// + /// Workers that have not yet ran, that need to run for this Worker to run. + /// + [Required] + [Description("Workers that have not yet ran, that need to run for this Worker to run.")] + public IEnumerable MissingDependencies { get; init; } = MissingDependencies; + + /// + /// Worker can run. + /// + [Required] + [Description("Worker can run.")] + public bool DependenciesFulfilled { get; init; } = DependenciesFulfilled; + + /// + /// Execution state of the Worker. + /// + [Required] + [Description("Execution state of the Worker.")] + public WorkerExecutionState State { get; init; } = State; +} \ No newline at end of file diff --git a/API/Controllers/FileLibraryController.cs b/API/Controllers/FileLibraryController.cs index bd9df1d..fba8910 100644 --- a/API/Controllers/FileLibraryController.cs +++ b/API/Controllers/FileLibraryController.cs @@ -1,5 +1,6 @@ using API.Schema.MangaContext; using Asp.Versioning; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using static Microsoft.AspNetCore.Http.StatusCodes; @@ -19,11 +20,12 @@ public class FileLibraryController(MangaContext context) : Controller /// Error during Database Operation [HttpGet] [ProducesResponseType(Status200OK, "application/json")] - public async Task GetFileLibraries () + [ProducesResponseType(Status500InternalServerError)] + public async Task>, InternalServerError>> GetFileLibraries () { - if(await context.FileLibraries.ToArrayAsync(HttpContext.RequestAborted) is not { } result) - return StatusCode(Status500InternalServerError); - return Ok(result); + if (await context.FileLibraries.ToListAsync(HttpContext.RequestAborted) is not { } result) + return TypedResults.InternalServerError(); + return TypedResults.Ok(result); } /// @@ -34,13 +36,13 @@ public class FileLibraryController(MangaContext context) : Controller /// with not found. [HttpGet("{FileLibraryId}")] [ProducesResponseType(Status200OK, "application/json")] - [ProducesResponseType(Status404NotFound)] - public async Task GetFileLibrary (string FileLibraryId) + [ProducesResponseType(Status404NotFound, "text/plain")] + public async Task, NotFound>> GetFileLibrary (string FileLibraryId) { if(await context.FileLibraries.FirstOrDefaultAsync(l => l.Key == FileLibraryId, HttpContext.RequestAborted) is not { } library) - return NotFound(); + return TypedResults.NotFound(nameof(FileLibraryId)); - return Ok(library); + return TypedResults.Ok(library); } /// @@ -53,19 +55,19 @@ public class FileLibraryController(MangaContext context) : Controller /// Error during Database Operation [HttpPatch("{FileLibraryId}/ChangeBasePath")] [ProducesResponseType(Status200OK)] - [ProducesResponseType(Status404NotFound)] + [ProducesResponseType(Status404NotFound, "text/plain")] [ProducesResponseType(Status500InternalServerError, "text/plain")] - public async Task ChangeLibraryBasePath (string FileLibraryId, [FromBody]string newBasePath) + public async Task, InternalServerError>> ChangeLibraryBasePath (string FileLibraryId, [FromBody]string newBasePath) { if(await context.FileLibraries.FirstOrDefaultAsync(l => l.Key == FileLibraryId, HttpContext.RequestAborted) is not { } library) - return NotFound(); + return TypedResults.NotFound(nameof(FileLibraryId)); //TODO Path check library.BasePath = newBasePath; if(await context.Sync(HttpContext.RequestAborted) is { success: false } result) - return StatusCode(Status500InternalServerError, result.exceptionMessage); - return Ok(); + return TypedResults.InternalServerError(result.exceptionMessage); + return TypedResults.Ok(); } /// @@ -78,39 +80,39 @@ public class FileLibraryController(MangaContext context) : Controller /// Error during Database Operation [HttpPatch("{FileLibraryId}/ChangeName")] [ProducesResponseType(Status200OK)] - [ProducesResponseType(Status404NotFound)] - [ProducesResponseType(Status400BadRequest)] + [ProducesResponseType(Status404NotFound, "text/plain")] [ProducesResponseType(Status500InternalServerError, "text/plain")] - public async Task ChangeLibraryName (string FileLibraryId, [FromBody] string newName) + public async Task, InternalServerError>> ChangeLibraryName (string FileLibraryId, [FromBody] string newName) { if(await context.FileLibraries.FirstOrDefaultAsync(l => l.Key == FileLibraryId, HttpContext.RequestAborted) is not { } library) - return NotFound(); + return TypedResults.NotFound(nameof(FileLibraryId)); //TODO Name check library.LibraryName = newName; if(await context.Sync(HttpContext.RequestAborted) is { success: false } result) - return StatusCode(Status500InternalServerError, result.exceptionMessage); - return Ok(); + return TypedResults.InternalServerError(result.exceptionMessage); + return TypedResults.Ok(); } /// /// Creates new /// /// New to add - /// + /// Key of new Library /// Error during Database Operation [HttpPut] - [ProducesResponseType(Status201Created)] + [ProducesResponseType(Status201Created, "text/plain")] [ProducesResponseType(Status500InternalServerError, "text/plain")] - public async Task CreateNewLibrary ([FromBody]FileLibrary library) + public async Task, InternalServerError>> CreateNewLibrary ([FromBody]FileLibrary library) { //TODO Parameter check context.FileLibraries.Add(library); if(await context.Sync(HttpContext.RequestAborted) is { success: false } result) - return StatusCode(Status500InternalServerError, result.exceptionMessage); - return Created(); + return TypedResults.InternalServerError(result.exceptionMessage); + + return TypedResults.Created(string.Empty, library.Key); } /// @@ -118,20 +120,21 @@ public class FileLibraryController(MangaContext context) : Controller /// /// .Key /// + /// with not found. /// Error during Database Operation [HttpDelete("{FileLibraryId}")] [ProducesResponseType(Status200OK)] - [ProducesResponseType(Status404NotFound)] + [ProducesResponseType(Status404NotFound, "text/plain")] [ProducesResponseType(Status500InternalServerError, "text/plain")] - public async Task DeleteLocalLibrary (string FileLibraryId) + public async Task, InternalServerError>> DeleteLocalLibrary (string FileLibraryId) { if(await context.FileLibraries.FirstOrDefaultAsync(l => l.Key == FileLibraryId, HttpContext.RequestAborted) is not { } library) - return NotFound(); + return TypedResults.NotFound(nameof(FileLibraryId)); context.FileLibraries.Remove(library); if(await context.Sync(HttpContext.RequestAborted) is { success: false } result) - return StatusCode(Status500InternalServerError, result.exceptionMessage); - return Ok(); + return TypedResults.InternalServerError(result.exceptionMessage); + return TypedResults.Ok(); } } \ No newline at end of file diff --git a/API/Controllers/LibraryConnectorController.cs b/API/Controllers/LibraryConnectorController.cs index f2865fa..389771b 100644 --- a/API/Controllers/LibraryConnectorController.cs +++ b/API/Controllers/LibraryConnectorController.cs @@ -1,6 +1,7 @@ using API.Schema.LibraryContext; using API.Schema.LibraryContext.LibraryConnectors; using Asp.Versioning; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using static Microsoft.AspNetCore.Http.StatusCodes; @@ -19,13 +20,13 @@ public class LibraryConnectorController(LibraryContext context) : Controller /// /// Error during Database Operation [HttpGet] - [ProducesResponseType(Status200OK, "application/json")] - public async Task GetAllConnectors () + [ProducesResponseType>(Status200OK, "application/json")] + public async Task>, InternalServerError>> GetAllConnectors () { - if (await context.LibraryConnectors.ToArrayAsync(HttpContext.RequestAborted) is not { } connectors) - return StatusCode(Status500InternalServerError); + if (await context.LibraryConnectors.ToListAsync(HttpContext.RequestAborted) is not { } connectors) + return TypedResults.InternalServerError(); - return Ok(connectors); + return TypedResults.Ok(connectors); } /// @@ -36,13 +37,13 @@ public class LibraryConnectorController(LibraryContext context) : Controller /// with not found. [HttpGet("{LibraryConnectorId}")] [ProducesResponseType(Status200OK, "application/json")] - [ProducesResponseType(Status404NotFound)] - public async Task GetConnector (string LibraryConnectorId) + [ProducesResponseType(Status404NotFound, "text/plain")] + public async Task, NotFound>> GetConnector (string LibraryConnectorId) { if (await context.LibraryConnectors.FirstOrDefaultAsync(l => l.Key == LibraryConnectorId) is not { } connector) - return NotFound(); + return TypedResults.NotFound(nameof(LibraryConnectorId)); - return Ok(connector); + return TypedResults.Ok(connector); } /// @@ -52,16 +53,15 @@ public class LibraryConnectorController(LibraryContext context) : Controller /// /// Error during Database Operation [HttpPut] - [ProducesResponseType(Status201Created)] + [ProducesResponseType(Status201Created, "text/plain")] [ProducesResponseType(Status500InternalServerError, "text/plain")] - public async Task CreateConnector ([FromBody]LibraryConnector libraryConnector) + public async Task, InternalServerError>> CreateConnector ([FromBody]LibraryConnector libraryConnector) { - context.LibraryConnectors.Add(libraryConnector); if(await context.Sync(HttpContext.RequestAborted) is { success: false } result) - return StatusCode(Status500InternalServerError, result.exceptionMessage); - return Created(); + return TypedResults.InternalServerError(result.exceptionMessage); + return TypedResults.Created(string.Empty, libraryConnector.Key); } /// @@ -73,17 +73,17 @@ public class LibraryConnectorController(LibraryContext context) : Controller /// Error during Database Operation [HttpDelete("{LibraryConnectorId}")] [ProducesResponseType(Status200OK)] - [ProducesResponseType(Status404NotFound)] + [ProducesResponseType(Status404NotFound, "text/plain")] [ProducesResponseType(Status500InternalServerError, "text/plain")] - public async Task DeleteConnector (string LibraryConnectorId) + public async Task, InternalServerError>> DeleteConnector (string LibraryConnectorId) { if (await context.LibraryConnectors.FirstOrDefaultAsync(l => l.Key == LibraryConnectorId) is not { } connector) - return NotFound(); + return TypedResults.NotFound(nameof(LibraryConnectorId)); context.LibraryConnectors.Remove(connector); if(await context.Sync(HttpContext.RequestAborted) is { success: false } result) - return StatusCode(Status500InternalServerError, result.exceptionMessage); - return Ok(); + return TypedResults.InternalServerError(result.exceptionMessage); + return TypedResults.Ok(); } } \ No newline at end of file diff --git a/API/Controllers/MaintenanceController.cs b/API/Controllers/MaintenanceController.cs index caab816..2611984 100644 --- a/API/Controllers/MaintenanceController.cs +++ b/API/Controllers/MaintenanceController.cs @@ -1,6 +1,7 @@ using API.MangaConnectors; using API.Schema.MangaContext; using Asp.Versioning; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using static Microsoft.AspNetCore.Http.StatusCodes; @@ -21,18 +22,18 @@ public class MaintenanceController(MangaContext mangaContext) : Controller [HttpPost("CleanupNoDownloadManga")] [ProducesResponseType(Status200OK)] [ProducesResponseType(Status500InternalServerError, "text/plain")] - public async Task CleanupNoDownloadManga() + public async Task>> CleanupNoDownloadManga() { 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); + return TypedResults.InternalServerError("Could not fetch Manga"); mangaContext.Mangas.RemoveRange(noDownloads); if(await mangaContext.Sync(HttpContext.RequestAborted) is { success: false } result) - return StatusCode(Status500InternalServerError, result.exceptionMessage); - return Ok(); + return TypedResults.InternalServerError(result.exceptionMessage); + return TypedResults.Ok(); } } \ No newline at end of file diff --git a/API/Controllers/MangaConnectorController.cs b/API/Controllers/MangaConnectorController.cs index b04a0f6..faf7fa9 100644 --- a/API/Controllers/MangaConnectorController.cs +++ b/API/Controllers/MangaConnectorController.cs @@ -1,6 +1,7 @@ using API.MangaConnectors; using API.Schema.MangaContext; using Asp.Versioning; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using static Microsoft.AspNetCore.Http.StatusCodes; // ReSharper disable InconsistentNaming @@ -17,10 +18,10 @@ public class MangaConnectorController(MangaContext context) : Controller /// /// Names of (Scanlation-Sites) [HttpGet] - [ProducesResponseType(Status200OK, "application/json")] - public IActionResult GetConnectors() + [ProducesResponseType>(Status200OK, "application/json")] + public Ok> GetConnectors() { - return Ok(Tranga.MangaConnectors.ToArray()); + return TypedResults.Ok(Tranga.MangaConnectors.ToList()); } /// @@ -31,13 +32,13 @@ public class MangaConnectorController(MangaContext context) : Controller /// (Scanlation-Sites) with Name not found. [HttpGet("{MangaConnectorName}")] [ProducesResponseType(Status200OK, "application/json")] - [ProducesResponseType(Status404NotFound)] - public IActionResult GetConnector(string MangaConnectorName) + [ProducesResponseType(Status404NotFound, "text/plain")] + public Results, NotFound> GetConnector(string MangaConnectorName) { if(Tranga.MangaConnectors.FirstOrDefault(c => c.Name.Equals(MangaConnectorName, StringComparison.InvariantCultureIgnoreCase)) is not { } connector) - return NotFound(); + return TypedResults.NotFound(nameof(MangaConnectorName)); - return Ok(connector); + return TypedResults.Ok(connector); } /// @@ -45,10 +46,10 @@ public class MangaConnectorController(MangaContext context) : Controller /// /// [HttpGet("Enabled")] - [ProducesResponseType(Status200OK, "application/json")] - public IActionResult GetEnabledConnectors() + [ProducesResponseType>(Status200OK, "application/json")] + public Ok> GetEnabledConnectors() { - return Ok(Tranga.MangaConnectors.Where(c => c.Enabled).ToArray()); + return TypedResults.Ok(Tranga.MangaConnectors.Where(c => c.Enabled).ToList()); } /// @@ -56,11 +57,11 @@ public class MangaConnectorController(MangaContext context) : Controller /// /// [HttpGet("Disabled")] - [ProducesResponseType(Status200OK, "application/json")] - public IActionResult GetDisabledConnectors() + [ProducesResponseType>(Status200OK, "application/json")] + public Ok> GetDisabledConnectors() { - return Ok(Tranga.MangaConnectors.Where(c => c.Enabled == false).ToArray()); + return TypedResults.Ok(Tranga.MangaConnectors.Where(c => c.Enabled == false).ToList()); } /// @@ -68,22 +69,22 @@ public class MangaConnectorController(MangaContext context) : Controller /// /// .Name /// Set true to enable, false to disable - /// + /// /// (Scanlation-Sites) with Name not found. /// Error during Database Operation [HttpPatch("{MangaConnectorName}/SetEnabled/{Enabled}")] - [ProducesResponseType(Status202Accepted)] - [ProducesResponseType(Status404NotFound)] + [ProducesResponseType(Status200OK)] + [ProducesResponseType(Status404NotFound, "text/plain")] [ProducesResponseType(Status500InternalServerError, "text/plain")] - public async Task SetEnabled(string MangaConnectorName, bool Enabled) + public async Task, InternalServerError>> SetEnabled(string MangaConnectorName, bool Enabled) { if(Tranga.MangaConnectors.FirstOrDefault(c => c.Name.Equals(MangaConnectorName, StringComparison.InvariantCultureIgnoreCase)) is not { } connector) - return NotFound(); + return TypedResults.NotFound(nameof(MangaConnectorName)); connector.Enabled = Enabled; if(await context.Sync(HttpContext.RequestAborted) is { success: false } result) - return StatusCode(Status500InternalServerError, result.exceptionMessage); - return Accepted(); + return TypedResults.InternalServerError(result.exceptionMessage); + return TypedResults.Ok(); } } \ No newline at end of file diff --git a/API/Controllers/MangaController.cs b/API/Controllers/MangaController.cs index 006ff2d..f6b5180 100644 --- a/API/Controllers/MangaController.cs +++ b/API/Controllers/MangaController.cs @@ -3,6 +3,7 @@ using API.MangaConnectors; using API.Schema.MangaContext; using API.Workers; using Asp.Versioning; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; @@ -12,6 +13,12 @@ using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Transforms; using static Microsoft.AspNetCore.Http.StatusCodes; +using AltTitle = API.Controllers.DTOs.AltTitle; +using Author = API.Controllers.DTOs.Author; +using Chapter = API.Controllers.DTOs.Chapter; +using Link = API.Controllers.DTOs.Link; +using Manga = API.Controllers.DTOs.Manga; + // ReSharper disable InconsistentNaming namespace API.Controllers; @@ -23,86 +30,114 @@ public class MangaController(MangaContext context) : Controller { /// - /// Returns all cached + /// Returns all cached /// - /// exert of . Use for more information + /// exert of . Use for more information /// Error during Database Operation [HttpGet] - [ProducesResponseType(Status200OK, "application/json")] - public async Task GetAllManga () + [ProducesResponseType>(Status200OK, "application/json")] + [ProducesResponseType(Status500InternalServerError)] + public async Task>, InternalServerError>> GetAllManga () { - if(await context.Mangas.ToArrayAsync(HttpContext.RequestAborted) is not { } result) - return StatusCode(Status500InternalServerError); + if (await context.Mangas.Include(m => m.MangaConnectorIds).ToArrayAsync(HttpContext.RequestAborted) is not + { } result) + return TypedResults.InternalServerError(); - return Ok(result.Select(m => new MinimalManga(m.Key, m.Name, m.Description, m.ReleaseStatus))); + return TypedResults.Ok(result.Select(m => + { + IEnumerable ids = m.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload)); + return new MinimalManga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids); + }).ToList()); } /// - /// Returns all cached .Keys + /// Returns all cached .Keys /// - /// Keys/IDs + /// Keys/IDs /// Error during Database Operation [HttpGet("Keys")] [ProducesResponseType(Status200OK, "application/json")] - public async Task GetAllMangaKeys () + [ProducesResponseType(Status500InternalServerError)] + public async Task, InternalServerError>> GetAllMangaKeys () { - if(await context.Mangas.Select(m => m.Key).ToArrayAsync(HttpContext.RequestAborted) is not { } result) - return StatusCode(Status500InternalServerError); + if (await context.Mangas.Select(m => m.Key).ToArrayAsync(HttpContext.RequestAborted) is not { } result) + return TypedResults.InternalServerError(); - return Ok(result); + return TypedResults.Ok(result); } /// - /// Returns all that are being downloaded from at least one + /// Returns all that are being downloaded from at least one /// - /// exert of . Use for more information + /// exert of . Use for more information /// Error during Database Operation [HttpGet("Downloading")] [ProducesResponseType(Status200OK, "application/json")] - public async Task GetMangaDownloading () + [ProducesResponseType(Status500InternalServerError)] + public async Task>, InternalServerError>> GetMangaDownloading() { - if(await context.Mangas - .Include(m => m.MangaConnectorIds) - .Where(m => m.MangaConnectorIds.Any(id => id.UseForDownload)) - .ToArrayAsync(HttpContext.RequestAborted) is not { } result) - return StatusCode(Status500InternalServerError); + if (await context.Mangas + .Include(m => m.MangaConnectorIds) + .Where(m => m.MangaConnectorIds.Any(id => id.UseForDownload)) + .ToArrayAsync(HttpContext.RequestAborted) is not { } result) + return TypedResults.InternalServerError(); - return Ok(result.Select(m => new MinimalManga(m.Key, m.Name, m.Description, m.ReleaseStatus, m.MangaConnectorIds))); + return TypedResults.Ok(result.Select(m => + { + IEnumerable ids = m.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload)); + return new MinimalManga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids); + }).ToList()); } /// - /// Returns all cached with + /// Returns all cached with /// - /// Array of .Key - /// + /// Array of .Key + /// /// Error during Database Operation [HttpPost("WithIDs")] - [ProducesResponseType(Status200OK, "application/json")] - public async Task GetMangaWithIds ([FromBody]string[] MangaIds) + [ProducesResponseType>(Status200OK, "application/json")] + [ProducesResponseType(Status500InternalServerError)] + public async Task>, InternalServerError>> GetMangaWithIds ([FromBody]string[] MangaIds) { - if(await context.MangaIncludeAll() - .Where(m => MangaIds.Contains(m.Key)) - .ToArrayAsync(HttpContext.RequestAborted) is not { } result) - return StatusCode(Status500InternalServerError); + if (await context.MangaIncludeAll() + .Where(m => MangaIds.Contains(m.Key)) + .ToArrayAsync(HttpContext.RequestAborted) is not { } result) + return TypedResults.InternalServerError(); - return Ok(result); + return TypedResults.Ok(result.Select(m => + { + IEnumerable ids = m.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload)); + IEnumerable authors = m.Authors.Select(a => new Author(a.Key, a.AuthorName)); + IEnumerable tags = m.MangaTags.Select(t => t.Tag); + IEnumerable links = m.Links.Select(l => new Link(l.Key, l.LinkProvider, l.LinkUrl)); + IEnumerable altTitles = m.AltTitles.Select(a => new AltTitle(a.Language, a.Title)); + return new Manga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids, m.IgnoreChaptersBefore, m.Year, m.OriginalLanguage, m.ChapterIds, authors, tags, links, altTitles); + }).ToList()); } /// - /// Return with + /// Return with /// - /// .Key + /// .Key /// /// with not found [HttpGet("{MangaId}")] [ProducesResponseType(Status200OK, "application/json")] - [ProducesResponseType(Status404NotFound)] - public async Task GetManga (string MangaId) + [ProducesResponseType(Status404NotFound, "text/plain")] + public async Task, NotFound>> GetManga (string MangaId) { if (await context.MangaIncludeAll().FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) - return NotFound(nameof(MangaId)); + return TypedResults.NotFound(nameof(MangaId)); - return Ok(manga); + IEnumerable ids = manga.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload)); + IEnumerable authors = manga.Authors.Select(a => new Author(a.Key, a.AuthorName)); + IEnumerable tags = manga.MangaTags.Select(t => t.Tag); + IEnumerable links = manga.Links.Select(l => new Link(l.Key, l.LinkProvider, l.LinkUrl)); + IEnumerable altTitles = manga.AltTitles.Select(a => new AltTitle(a.Language, a.Title)); + Manga result = new (manga.Key, manga.Name, manga.Description, manga.ReleaseStatus, ids, manga.IgnoreChaptersBefore, manga.Year, manga.OriginalLanguage, manga.ChapterIds, authors, tags, links, altTitles); + + return TypedResults.Ok(result); } /// @@ -114,18 +149,18 @@ public class MangaController(MangaContext context) : Controller /// Error during Database Operation [HttpDelete("{MangaId}")] [ProducesResponseType(Status200OK)] - [ProducesResponseType(Status404NotFound)] + [ProducesResponseType(Status404NotFound, "text/plain")] [ProducesResponseType(Status500InternalServerError, "text/plain")] - public async Task DeleteManga (string MangaId) + public async Task, InternalServerError>> DeleteManga (string MangaId) { if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) - return NotFound(nameof(MangaId)); + return TypedResults.NotFound(nameof(MangaId)); context.Mangas.Remove(manga); if(await context.Sync(HttpContext.RequestAborted) is { success: false } result) - return StatusCode(Status500InternalServerError, result.exceptionMessage); - return Ok(); + return TypedResults.InternalServerError(result.exceptionMessage); + return TypedResults.Ok(); } @@ -137,27 +172,19 @@ public class MangaController(MangaContext context) : Controller /// /// with or not found [HttpPatch("{MangaIdFrom}/MergeInto/{MangaIdInto}")] - [ProducesResponseType(Status200OK,"image/jpeg")] - [ProducesResponseType(Status404NotFound)] - public async Task MergeIntoManga (string MangaIdFrom, string MangaIdInto) + [ProducesResponseType(Status200OK)] + [ProducesResponseType(Status404NotFound, "text/plain")] + public async Task>> MergeIntoManga (string MangaIdFrom, string MangaIdInto) { - if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaIdFrom, HttpContext.RequestAborted) is not { } from) - return NotFound(nameof(MangaIdFrom)); - 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) - await collectionEntry.LoadAsync(HttpContext.RequestAborted); - await context.Entry(from).Navigation(nameof(Manga.Library)).LoadAsync(HttpContext.RequestAborted); - - foreach (CollectionEntry collectionEntry in context.Entry(into).Collections) - await collectionEntry.LoadAsync(HttpContext.RequestAborted); - await context.Entry(into).Navigation(nameof(Manga.Library)).LoadAsync(HttpContext.RequestAborted); + if (await context.MangaIncludeAll().FirstOrDefaultAsync(m => m.Key == MangaIdFrom, HttpContext.RequestAborted) is not { } from) + return TypedResults.NotFound(nameof(MangaIdFrom)); + if (await context.MangaIncludeAll().FirstOrDefaultAsync(m => m.Key == MangaIdInto, HttpContext.RequestAborted) is not { } into) + return TypedResults.NotFound(nameof(MangaIdInto)); BaseWorker[] newJobs = into.MergeFrom(from, context); Tranga.AddWorkers(newJobs); - return Ok(); + return TypedResults.Ok(); } /// @@ -175,22 +202,22 @@ public class MangaController(MangaContext context) : Controller [ProducesResponseType(Status200OK,"image/jpeg")] [ProducesResponseType(Status204NoContent)] [ProducesResponseType(Status400BadRequest)] - [ProducesResponseType(Status404NotFound)] - [ProducesResponseType(Status503ServiceUnavailable, "text/plain")] - public async Task GetCover (string MangaId, [FromQuery]int? width, [FromQuery]int? height) + [ProducesResponseType(Status404NotFound, "text/plain")] + [ProducesResponseType(Status503ServiceUnavailable)] + public async Task, StatusCodeHttpResult>> GetCover (string MangaId, [FromQuery]int? width, [FromQuery]int? height) { if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) - return NotFound(nameof(MangaId)); + return TypedResults.NotFound(nameof(MangaId)); if (!System.IO.File.Exists(manga.CoverFileNameInCache)) { if (Tranga.GetRunningWorkers().Any(worker => worker is DownloadCoverFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId)) { Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}"); - return StatusCode(Status503ServiceUnavailable, Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000); + return TypedResults.StatusCode(Status503ServiceUnavailable); } else - return NoContent(); + return TypedResults.NoContent(); } Image image = await Image.LoadAsync(manga.CoverFileNameInCache, HttpContext.RequestAborted); @@ -198,7 +225,7 @@ public class MangaController(MangaContext context) : Controller if (width is { } w && height is { } h) { if (width < 10 || height < 10 || width > 65535 || height > 65535) - return BadRequest(); + return TypedResults.BadRequest(); image.Mutate(i => i.ApplyProcessor(new ResizeProcessor(new ResizeOptions() { Mode = ResizeMode.Max, @@ -210,7 +237,7 @@ public class MangaController(MangaContext context) : Controller 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}\"")); + return TypedResults.File(ms.GetBuffer(), "image/jpeg", lastModified: new DateTimeOffset(lastModified), entityTag: EntityTagHeaderValue.Parse($"\"{lastModified.Ticks}\"")); } /// @@ -220,17 +247,20 @@ public class MangaController(MangaContext context) : Controller /// /// with not found [HttpGet("{MangaId}/Chapters")] - [ProducesResponseType(Status200OK, "application/json")] + [ProducesResponseType>(Status200OK, "application/json")] [ProducesResponseType(Status404NotFound)] - public async Task GetChapters (string MangaId) + public async Task>, NotFound>> GetChapters(string MangaId) { - if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) - return NotFound(nameof(MangaId)); + if (await context.Mangas.Include(m => m.Chapters).ThenInclude(c => c.MangaConnectorIds).FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) + return TypedResults.NotFound(nameof(MangaId)); - await context.Entry(manga).Collection(m => m.Chapters).LoadAsync(); - - Chapter[] chapters = manga.Chapters.ToArray(); - return Ok(chapters); + List chapters = manga.Chapters.Select(c => + { + IEnumerable ids = c.MangaConnectorIds.Select(id => + new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload)); + return new Chapter(c.Key, c.ParentMangaId, c.VolumeNumber, c.ChapterNumber, c.Title, ids, c.Downloaded); + }).ToList(); + return TypedResults.Ok(chapters); } /// @@ -243,19 +273,22 @@ public class MangaController(MangaContext context) : Controller [HttpGet("{MangaId}/Chapters/Downloaded")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status204NoContent)] - [ProducesResponseType(Status404NotFound)] - public async Task GetChaptersDownloaded (string MangaId) + [ProducesResponseType(Status404NotFound, "text/plain")] + public async Task>, NoContent, NotFound>> GetChaptersDownloaded(string MangaId) { - if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) - return NotFound(nameof(MangaId)); + if (await context.Mangas.Include(m => m.Chapters).FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) + return TypedResults.NotFound(nameof(MangaId)); - await context.Entry(manga).Collection(m => m.Chapters).LoadAsync(); - - List chapters = manga.Chapters.Where(c => c.Downloaded).ToList(); + List chapters = manga.Chapters.Where(c => c.Downloaded).Select(c => + { + IEnumerable ids = c.MangaConnectorIds.Select(id => + new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload)); + return new Chapter(c.Key, c.ParentMangaId, c.VolumeNumber, c.ChapterNumber, c.Title, ids, c.Downloaded); + }).ToList(); if (chapters.Count == 0) - return NoContent(); + return TypedResults.NoContent(); - return Ok(chapters); + return TypedResults.Ok(chapters); } /// @@ -266,21 +299,24 @@ public class MangaController(MangaContext context) : Controller /// No available chapters /// with not found. [HttpGet("{MangaId}/Chapters/NotDownloaded")] - [ProducesResponseType(Status200OK, "application/json")] + [ProducesResponseType>(Status200OK, "application/json")] [ProducesResponseType(Status204NoContent)] - [ProducesResponseType(Status404NotFound)] - public async Task GetChaptersNotDownloaded (string MangaId) + [ProducesResponseType(Status404NotFound, "text/plain")] + public async Task>, NoContent, NotFound>> GetChaptersNotDownloaded(string MangaId) { - if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) - return NotFound(nameof(MangaId)); + if (await context.Mangas.Include(m => m.Chapters).FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) + return TypedResults.NotFound(nameof(MangaId)); - await context.Entry(manga).Collection(m => m.Chapters).LoadAsync(HttpContext.RequestAborted); - - List chapters = manga.Chapters.Where(c => c.Downloaded == false).ToList(); + List chapters = manga.Chapters.Where(c => c.Downloaded == false).Select(c => + { + IEnumerable ids = c.MangaConnectorIds.Select(id => + new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload)); + return new Chapter(c.Key, c.ParentMangaId, c.VolumeNumber, c.ChapterNumber, c.Title, ids, c.Downloaded); + }).ToList(); if (chapters.Count == 0) - return NoContent(); + return TypedResults.NoContent(); - return Ok(chapters); + return TypedResults.Ok(chapters); } /// @@ -293,38 +329,41 @@ public class MangaController(MangaContext context) : Controller /// Could not retrieve the maximum chapter-number /// Retry after timeout, updating value [HttpGet("{MangaId}/Chapter/LatestAvailable")] - [ProducesResponseType(Status200OK, "application/json")] + [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status204NoContent)] [ProducesResponseType(Status404NotFound, "text/plain")] - [ProducesResponseType(Status500InternalServerError, "text/plain")] - [ProducesResponseType(Status503ServiceUnavailable, "text/plain")] - public async Task GetLatestChapter (string MangaId) + [ProducesResponseType(Status500InternalServerError)] + [ProducesResponseType(Status503ServiceUnavailable)] + public async Task, NoContent, InternalServerError, NotFound, StatusCodeHttpResult>> GetLatestChapter(string MangaId) { - if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) - return NotFound(nameof(MangaId)); + if (await context.Mangas.Include(m => m.Chapters).FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) + return TypedResults.NotFound(nameof(MangaId)); - await context.Entry(manga).Collection(m => m.Chapters).LoadAsync(HttpContext.RequestAborted); - - List chapters = manga.Chapters.ToList(); + List chapters = manga.Chapters.ToList(); if (chapters.Count == 0) { - if (Tranga.GetRunningWorkers().Any(worker => worker is RetrieveMangaChaptersFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId && w.State < WorkerExecutionState.Completed)) + if (Tranga.GetRunningWorkers().Any(worker => + worker is RetrieveMangaChaptersFromMangaconnectorWorker w && + context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId && + w.State < WorkerExecutionState.Completed)) { Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}"); - return StatusCode(Status503ServiceUnavailable, Tranga.Settings.WorkCycleTimeoutMs * 2/ 1000); - }else - return Ok(0); + return TypedResults.StatusCode(Status503ServiceUnavailable); + } + else + return TypedResults.NoContent(); } - Chapter? max = chapters.Max(); + API.Schema.MangaContext.Chapter? max = chapters.Max(); if (max is null) - return StatusCode(Status500InternalServerError, "Max chapter could not be found"); + return TypedResults.InternalServerError(); foreach (CollectionEntry collectionEntry in context.Entry(max).Collections) await collectionEntry.LoadAsync(HttpContext.RequestAborted); - await context.Entry(max).Navigation(nameof(Chapter.ParentManga)).LoadAsync(HttpContext.RequestAborted); - return Ok(max); + IEnumerable ids = max.MangaConnectorIds.Select(id => + new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload)); + return TypedResults.Ok(new Chapter(max.Key, max.ParentMangaId, max.VolumeNumber, max.ChapterNumber, max.Title,ids, max.Downloaded)); } /// @@ -339,36 +378,34 @@ public class MangaController(MangaContext context) : Controller [HttpGet("{MangaId}/Chapter/LatestDownloaded")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status204NoContent)] - [ProducesResponseType(Status404NotFound)] - [ProducesResponseType(Status412PreconditionFailed, "text/plain")] - [ProducesResponseType(Status503ServiceUnavailable, "text/plain")] - public async Task GetLatestChapterDownloaded (string MangaId) + [ProducesResponseType(Status404NotFound, "text/plain")] + [ProducesResponseType(Status412PreconditionFailed)] + [ProducesResponseType(Status503ServiceUnavailable)] + public async Task, NoContent, NotFound, StatusCodeHttpResult>> GetLatestChapterDownloaded(string MangaId) { if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) - return NotFound(nameof(MangaId)); + return TypedResults.NotFound(nameof(MangaId)); await context.Entry(manga).Collection(m => m.Chapters).LoadAsync(HttpContext.RequestAborted); - List chapters = manga.Chapters.ToList(); + List chapters = manga.Chapters.ToList(); if (chapters.Count == 0) { if (Tranga.GetRunningWorkers().Any(worker => worker is RetrieveMangaChaptersFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId && w.State < WorkerExecutionState.Completed)) { Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}"); - return StatusCode(Status503ServiceUnavailable, Tranga.Settings.WorkCycleTimeoutMs * 2/ 1000); + return TypedResults.StatusCode(Status503ServiceUnavailable); }else - return NoContent(); + return TypedResults.NoContent(); } - Chapter? max = chapters.Max(); + API.Schema.MangaContext.Chapter? max = chapters.Max(); if (max is null) - return StatusCode(Status412PreconditionFailed, "Max chapter could not be found"); + return TypedResults.StatusCode(Status412PreconditionFailed); - foreach (CollectionEntry collectionEntry in context.Entry(max).Collections) - await collectionEntry.LoadAsync(HttpContext.RequestAborted); - await context.Entry(max).Navigation(nameof(Chapter.ParentManga)).LoadAsync(HttpContext.RequestAborted); - - return Ok(max); + IEnumerable ids = max.MangaConnectorIds.Select(id => + new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload)); + return TypedResults.Ok(new Chapter(max.Key, max.ParentMangaId, max.VolumeNumber, max.ChapterNumber, max.Title,ids, max.Downloaded)); } /// @@ -380,19 +417,19 @@ public class MangaController(MangaContext context) : Controller /// with not found. /// Error during Database Operation [HttpPatch("{MangaId}/IgnoreChaptersBefore")] - [ProducesResponseType(Status202Accepted)] - [ProducesResponseType(Status404NotFound)] + [ProducesResponseType(Status200OK)] + [ProducesResponseType(Status404NotFound, "text/plain")] [ProducesResponseType(Status500InternalServerError, "text/plain")] - public async Task IgnoreChaptersBefore (string MangaId, [FromBody]float chapterThreshold) + public async Task, InternalServerError>> IgnoreChaptersBefore(string MangaId, [FromBody]float chapterThreshold) { if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) - return NotFound(nameof(MangaId)); + return TypedResults.NotFound(nameof(MangaId)); manga.IgnoreChaptersBefore = chapterThreshold; if(await context.Sync(HttpContext.RequestAborted) is { success: false } result) - return StatusCode(Status500InternalServerError, result.exceptionMessage); + return TypedResults.InternalServerError(result.exceptionMessage); - return Accepted(); + return TypedResults.Ok(); } /// @@ -403,24 +440,20 @@ public class MangaController(MangaContext context) : Controller /// Folder is going to be moved /// or not found [HttpPost("{MangaId}/ChangeLibrary/{LibraryId}")] - [ProducesResponseType(Status202Accepted)] - [ProducesResponseType(Status404NotFound)] - public async Task ChangeLibrary (string MangaId, string LibraryId) + [ProducesResponseType(Status200OK)] + [ProducesResponseType(Status404NotFound, "text/plain")] + public async Task>> ChangeLibrary(string MangaId, string LibraryId) { - if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) - return NotFound(nameof(MangaId)); + if (await context.MangaIncludeAll().FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) + return TypedResults.NotFound(nameof(MangaId)); 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) - await collectionEntry.LoadAsync(HttpContext.RequestAborted); - await context.Entry(manga).Navigation(nameof(Manga.Library)).LoadAsync(HttpContext.RequestAborted); + return TypedResults.NotFound(nameof(LibraryId)); MoveMangaLibraryWorker moveLibrary = new(manga, library); Tranga.AddWorkers([moveLibrary]); - return Accepted(); + return TypedResults.Ok(); } /// @@ -440,51 +473,50 @@ public class MangaController(MangaContext context) : Controller [ProducesResponseType(Status412PreconditionFailed, "text/plain")] [ProducesResponseType(Status428PreconditionRequired, "text/plain")] [ProducesResponseType(Status500InternalServerError, "text/plain")] - public async Task MarkAsRequested (string MangaId, string MangaConnectorName, bool IsRequested) + public async Task, StatusCodeHttpResult, InternalServerError>> MarkAsRequested(string MangaId, string MangaConnectorName, bool IsRequested) { if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } _) - return NotFound(nameof(MangaId)); + return TypedResults.NotFound(nameof(MangaId)); if(!Tranga.TryGetMangaConnector(MangaConnectorName, out MangaConnector? _)) - return NotFound(nameof(MangaConnectorName)); + return TypedResults.NotFound(nameof(MangaConnectorName)); if (context.MangaConnectorToManga .FirstOrDefault(id => id.MangaConnectorName == MangaConnectorName && id.ObjId == MangaId) is not { } mcId) { if(IsRequested) - return StatusCode(Status428PreconditionRequired, "Don't know how to download this Manga from MangaConnector"); + return TypedResults.StatusCode(Status428PreconditionRequired); else - return StatusCode(Status412PreconditionFailed, "Not linked anyways."); + return TypedResults.StatusCode(Status412PreconditionFailed); } mcId.UseForDownload = IsRequested; if(await context.Sync(HttpContext.RequestAborted) is { success: false } result) - return StatusCode(Status500InternalServerError, result.exceptionMessage); - + return TypedResults.InternalServerError(result.exceptionMessage); DownloadCoverFromMangaconnectorWorker downloadCover = new(mcId); RetrieveMangaChaptersFromMangaconnectorWorker retrieveChapters = new(mcId, Tranga.Settings.DownloadLanguage); Tranga.AddWorkers([downloadCover, retrieveChapters]); - return Ok(); + return TypedResults.Ok(); } /// - /// Initiate a search for on a different + /// Initiate a search for on a different /// - /// with + /// with /// .Name - /// + /// exert of /// with Name not found /// with Name is disabled [HttpPost("{MangaId}/SearchOn/{MangaConnectorName}")] - [ProducesResponseType(Status200OK, "application/json")] - [ProducesResponseType(Status404NotFound)] + [ProducesResponseType>(Status200OK, "application/json")] + [ProducesResponseType(Status404NotFound, "text/plain")] [ProducesResponseType(Status406NotAcceptable)] - public async Task SearchOnDifferentConnector (string MangaId, string MangaConnectorName) + public async Task>, NotFound, StatusCodeHttpResult>> SearchOnDifferentConnector (string MangaId, string MangaConnectorName) { if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) - return NotFound(nameof(MangaId)); + return TypedResults.NotFound(nameof(MangaId)); return new SearchController(context).SearchManga(MangaConnectorName, manga.Name); } @@ -495,14 +527,27 @@ public class MangaController(MangaContext context) : Controller /// .Key /// /// with + /// /// Error during Database Operation [HttpGet("WithAuthorId/{AuthorId}")] - [ProducesResponseType(Status200OK, "application/json")] - public async Task GetMangaWithAuthorIds (string AuthorId) + [ProducesResponseType>(Status200OK, "application/json")] + [ProducesResponseType(Status404NotFound, "text/plain")] + public async Task>, NotFound, InternalServerError>> GetMangaWithAuthorIds (string AuthorId) { - 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))); + if (await context.Authors.FirstOrDefaultAsync(a => a.Key == AuthorId, HttpContext.RequestAborted) is not { } _) + return TypedResults.NotFound(nameof(AuthorId)); + + if (await context.MangaIncludeAll().Where(m => m.Authors.Any(a => a.Key == AuthorId)).ToListAsync(HttpContext.RequestAborted) is not { } result) + return TypedResults.InternalServerError(); + + return TypedResults.Ok(result.Select(m => + { + IEnumerable ids = m.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload)); + IEnumerable authors = m.Authors.Select(a => new Author(a.Key, a.AuthorName)); + IEnumerable tags = m.MangaTags.Select(t => t.Tag); + IEnumerable links = m.Links.Select(l => new Link(l.Key, l.LinkProvider, l.LinkUrl)); + IEnumerable altTitles = m.AltTitles.Select(a => new AltTitle(a.Language, a.Title)); + return new Manga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids, m.IgnoreChaptersBefore, m.Year, m.OriginalLanguage, m.ChapterIds, authors, tags, links, altTitles); + }).ToList()); } /// @@ -511,13 +556,28 @@ public class MangaController(MangaContext context) : Controller /// .Tag /// /// not found + /// Error during Database Operation [HttpGet("WithTag/{Tag}")] [ProducesResponseType(Status200OK, "application/json")] - public async Task GetMangasWithTag (string Tag) + [ProducesResponseType(Status404NotFound, "text/plain")] + [ProducesResponseType(Status500InternalServerError)] + public async Task>, NotFound, InternalServerError>> GetMangasWithTag (string Tag) { 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))); + return TypedResults.NotFound(nameof(Tag)); + + + if (await context.MangaIncludeAll().Where(m => m.MangaTags.Any(t => t.Tag.Equals(tag))) .ToListAsync(HttpContext.RequestAborted) is not { } result) + return TypedResults.InternalServerError(); + + return TypedResults.Ok(result.Select(m => + { + IEnumerable ids = m.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload)); + IEnumerable authors = m.Authors.Select(a => new Author(a.Key, a.AuthorName)); + IEnumerable tags = m.MangaTags.Select(t => t.Tag); + IEnumerable links = m.Links.Select(l => new Link(l.Key, l.LinkProvider, l.LinkUrl)); + IEnumerable altTitles = m.AltTitles.Select(a => new AltTitle(a.Language, a.Title)); + return new Manga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids, m.IgnoreChaptersBefore, m.Year, m.OriginalLanguage, m.ChapterIds, authors, tags, links, altTitles); + }).ToList()); } } \ No newline at end of file diff --git a/API/Controllers/MetadataFetcherController.cs b/API/Controllers/MetadataFetcherController.cs index c215b7a..90dc0e9 100644 --- a/API/Controllers/MetadataFetcherController.cs +++ b/API/Controllers/MetadataFetcherController.cs @@ -1,6 +1,7 @@ using API.Schema.MangaContext; using API.Schema.MangaContext.MetadataFetchers; using Asp.Versioning; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.EntityFrameworkCore; @@ -19,10 +20,10 @@ public class MetadataFetcherController(MangaContext context) : Controller /// /// Names of (Metadata-Sites) [HttpGet] - [ProducesResponseType(Status200OK, "application/json")] - public IActionResult GetConnectors () + [ProducesResponseType>(Status200OK, "application/json")] + public Ok> GetConnectors () { - return Ok(Tranga.MetadataFetchers.Select(m => m.Name).ToArray()); + return TypedResults.Ok(Tranga.MetadataFetchers.Select(m => m.Name).ToList()); } /// @@ -31,13 +32,14 @@ public class MetadataFetcherController(MangaContext context) : Controller /// /// Error during Database Operation [HttpGet("Links")] - [ProducesResponseType(Status200OK, "application/json")] - public async Task GetLinkedEntries () + [ProducesResponseType>(Status200OK, "application/json")] + [ProducesResponseType(Status500InternalServerError)] + public async Task>, InternalServerError>> GetLinkedEntries () { - if (await context.MetadataEntries.ToArrayAsync() is not { } result) - return StatusCode(Status500InternalServerError); + if (await context.MetadataEntries.ToListAsync() is not { } result) + return TypedResults.InternalServerError(); - return Ok(result); + return TypedResults.Ok(result); } /// @@ -52,16 +54,16 @@ public class MetadataFetcherController(MangaContext context) : Controller [HttpPost("{MetadataFetcherName}/SearchManga/{MangaId}")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status400BadRequest)] - [ProducesResponseType(Status404NotFound)] - public async Task SearchMangaMetadata(string MangaId, string MetadataFetcherName, [FromBody (EmptyBodyBehavior = EmptyBodyBehavior.Allow)]string? searchTerm = null) + [ProducesResponseType(Status404NotFound, "text/plain")] + public async Task>, BadRequest, NotFound>> SearchMangaMetadata(string MangaId, string MetadataFetcherName, [FromBody (EmptyBodyBehavior = EmptyBodyBehavior.Allow)]string? searchTerm = null) { if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) - return NotFound(nameof(MangaId)); + return TypedResults.NotFound(nameof(MangaId)); if(Tranga.MetadataFetchers.FirstOrDefault(f => f.Name == MetadataFetcherName) is not { } fetcher) - return BadRequest(); + return TypedResults.BadRequest(); MetadataSearchResult[] searchResults = searchTerm is null ? fetcher.SearchMetadataEntry(manga) : fetcher.SearchMetadataEntry(searchTerm); - return Ok(searchResults); + return TypedResults.Ok(searchResults.ToList()); } /// @@ -77,21 +79,21 @@ public class MetadataFetcherController(MangaContext context) : Controller [HttpPost("{MetadataFetcherName}/Link/{MangaId}")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status400BadRequest)] - [ProducesResponseType(Status404NotFound)] + [ProducesResponseType(Status404NotFound, "text/plain")] [ProducesResponseType(Status500InternalServerError, "text/plain")] - public async Task LinkMangaMetadata (string MangaId, string MetadataFetcherName, [FromBody]string Identifier) + public async Task, BadRequest, NotFound, InternalServerError>> LinkMangaMetadata (string MangaId, string MetadataFetcherName, [FromBody]string Identifier) { if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) - return NotFound(nameof(MangaId)); + return TypedResults.NotFound(nameof(MangaId)); if(Tranga.MetadataFetchers.FirstOrDefault(f => f.Name == MetadataFetcherName) is not { } fetcher) - return BadRequest(); + return TypedResults.BadRequest(); MetadataEntry entry = fetcher.CreateMetadataEntry(manga, Identifier); context.MetadataEntries.Add(entry); if(await context.Sync(HttpContext.RequestAborted) is { success: false } result) - return StatusCode(Status500InternalServerError, result.exceptionMessage); - return Ok(entry); + return TypedResults.InternalServerError(result.exceptionMessage); + return TypedResults.Ok(entry); } /// @@ -105,22 +107,23 @@ public class MetadataFetcherController(MangaContext context) : Controller [HttpPost("{MetadataFetcherName}/Unlink/{MangaId}")] [ProducesResponseType(Status200OK)] [ProducesResponseType(Status400BadRequest)] - [ProducesResponseType(Status404NotFound)] - [ProducesResponseType(Status412PreconditionFailed, "text/plain")] + [ProducesResponseType(Status404NotFound, "text/plain")] + [ProducesResponseType(Status412PreconditionFailed)] [ProducesResponseType(Status500InternalServerError, "text/plain")] - public async Task UnlinkMangaMetadata (string MangaId, string MetadataFetcherName) + public async Task, InternalServerError, StatusCodeHttpResult>> UnlinkMangaMetadata (string MangaId, string MetadataFetcherName) { if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } _) - return NotFound(nameof(MangaId)); + return TypedResults.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) - return StatusCode(Status412PreconditionFailed, "No entry found"); + return TypedResults.BadRequest(); + if (context.MetadataEntries.FirstOrDefault(e => + e.MangaId == MangaId && e.MetadataFetcherName == MetadataFetcherName) is not { } entry) + return TypedResults.StatusCode(Status412PreconditionFailed); context.Remove(entry); if(await context.Sync(HttpContext.RequestAborted) is { success: false } result) - return StatusCode(Status500InternalServerError, result.exceptionMessage); - return Ok(); + return TypedResults.InternalServerError(result.exceptionMessage); + return TypedResults.Ok(); } } \ No newline at end of file diff --git a/API/Controllers/NotificationConnectorController.cs b/API/Controllers/NotificationConnectorController.cs index 95a98c6..4e3cdbd 100644 --- a/API/Controllers/NotificationConnectorController.cs +++ b/API/Controllers/NotificationConnectorController.cs @@ -3,6 +3,7 @@ using API.APIEndpointRecords; using API.Schema.NotificationsContext; using API.Schema.NotificationsContext.NotificationConnectors; using Asp.Versioning; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using static Microsoft.AspNetCore.Http.StatusCodes; @@ -22,13 +23,14 @@ public class NotificationConnectorController(NotificationsContext context) : Con /// /// Error during Database Operation [HttpGet] - [ProducesResponseType(Status200OK, "application/json")] - public async Task GetAllConnectors () + [ProducesResponseType>(Status200OK, "application/json")] + [ProducesResponseType(Status500InternalServerError)] + public async Task>, InternalServerError>> GetAllConnectors () { - if(await context.NotificationConnectors.ToArrayAsync(HttpContext.RequestAborted) is not { } result) - return StatusCode(Status500InternalServerError); + if(await context.NotificationConnectors.ToListAsync(HttpContext.RequestAborted) is not { } result) + return TypedResults.InternalServerError(); - return Ok(result); + return TypedResults.Ok(result); } /// @@ -39,13 +41,13 @@ public class NotificationConnectorController(NotificationsContext context) : Con /// with not found [HttpGet("{Name}")] [ProducesResponseType(Status200OK, "application/json")] - [ProducesResponseType(Status404NotFound)] - public async Task GetConnector (string Name) + [ProducesResponseType(Status404NotFound, "text/plain")] + public async Task, NotFound>> GetConnector (string Name) { if (await context.NotificationConnectors.FirstOrDefaultAsync(c => c.Name == Name, HttpContext.RequestAborted) is not { } connector) - return NotFound(nameof(Name)); + return TypedResults.NotFound(nameof(Name)); - return Ok(connector); + return TypedResults.Ok(connector); } /// @@ -57,14 +59,14 @@ public class NotificationConnectorController(NotificationsContext context) : Con [HttpPut] [ProducesResponseType(Status200OK, "text/plain")] [ProducesResponseType(Status500InternalServerError, "text/plain")] - public async Task CreateConnector ([FromBody]NotificationConnector notificationConnector) + public async Task, InternalServerError>> CreateConnector ([FromBody]NotificationConnector notificationConnector) { context.NotificationConnectors.Add(notificationConnector); context.Notifications.Add(new ("Added new Notification Connector!", notificationConnector.Name, NotificationUrgency.High)); if(await context.Sync(HttpContext.RequestAborted) is { success: false } result) - return StatusCode(Status500InternalServerError, result.exceptionMessage); - return Ok(notificationConnector.Name); + return TypedResults.InternalServerError(result.exceptionMessage); + return TypedResults.Ok(notificationConnector.Name); } /// @@ -76,7 +78,7 @@ public class NotificationConnectorController(NotificationsContext context) : Con [HttpPut("Gotify")] [ProducesResponseType(Status200OK, "text/plain")] [ProducesResponseType(Status500InternalServerError, "text/plain")] - public async Task CreateGotifyConnector ([FromBody]GotifyRecord gotifyData) + public async Task, InternalServerError>> CreateGotifyConnector ([FromBody]GotifyRecord gotifyData) { //TODO Validate Data @@ -97,7 +99,7 @@ public class NotificationConnectorController(NotificationsContext context) : Con [HttpPut("Ntfy")] [ProducesResponseType(Status200OK, "text/plain")] [ProducesResponseType(Status500InternalServerError, "text/plain")] - public async Task CreateNtfyConnector ([FromBody]NtfyRecord ntfyRecord) + public async Task, InternalServerError>> CreateNtfyConnector ([FromBody]NtfyRecord ntfyRecord) { //TODO Validate Data @@ -124,7 +126,7 @@ public class NotificationConnectorController(NotificationsContext context) : Con [HttpPut("Pushover")] [ProducesResponseType(Status200OK, "text/plain")] [ProducesResponseType(Status500InternalServerError, "text/plain")] - public async Task CreatePushoverConnector ([FromBody]PushoverRecord pushoverRecord) + public async Task, InternalServerError>> CreatePushoverConnector ([FromBody]PushoverRecord pushoverRecord) { //TODO Validate Data @@ -145,17 +147,17 @@ public class NotificationConnectorController(NotificationsContext context) : Con /// Error during Database Operation [HttpDelete("{Name}")] [ProducesResponseType(Status200OK)] - [ProducesResponseType(Status404NotFound)] + [ProducesResponseType(Status404NotFound, "text/plain")] [ProducesResponseType(Status500InternalServerError, "text/plain")] - public async Task DeleteConnector (string Name) + public async Task, InternalServerError>> DeleteConnector (string Name) { if (await context.NotificationConnectors.FirstOrDefaultAsync(c => c.Name == Name, HttpContext.RequestAborted) is not { } connector) - return NotFound(nameof(Name)); + return TypedResults.NotFound(nameof(Name)); context.NotificationConnectors.Remove(connector); if(await context.Sync(HttpContext.RequestAborted) is { success: false } result) - return StatusCode(Status500InternalServerError, result.exceptionMessage); - return Ok(); + return TypedResults.InternalServerError(result.exceptionMessage); + return TypedResults.Ok(); } } \ No newline at end of file diff --git a/API/Controllers/QueryController.cs b/API/Controllers/QueryController.cs index 0318b5d..ba3fa7c 100644 --- a/API/Controllers/QueryController.cs +++ b/API/Controllers/QueryController.cs @@ -1,9 +1,14 @@ -using API.Schema.MangaContext; +using API.Controllers.DTOs; +using API.Schema.MangaContext; using Asp.Versioning; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Soenneker.Utils.String.NeedlemanWunsch; using static Microsoft.AspNetCore.Http.StatusCodes; +using Author = API.Controllers.DTOs.Author; +using Chapter = API.Controllers.DTOs.Chapter; + // ReSharper disable InconsistentNaming namespace API.Controllers; @@ -21,13 +26,13 @@ public class QueryController(MangaContext context) : Controller /// with not found [HttpGet("Author/{AuthorId}")] [ProducesResponseType(Status200OK, "application/json")] - [ProducesResponseType(Status404NotFound)] - public async Task GetAuthor (string AuthorId) + [ProducesResponseType(Status404NotFound, "text/plain")] + public async Task, NotFound>> GetAuthor (string AuthorId) { if (await context.Authors.FirstOrDefaultAsync(a => a.Key == AuthorId, HttpContext.RequestAborted) is not { } author) - return NotFound(nameof(AuthorId)); + return TypedResults.NotFound(nameof(AuthorId)); - return Ok(author); + return TypedResults.Ok(new Author(author.Key, author.AuthorName)); } /// @@ -38,13 +43,15 @@ public class QueryController(MangaContext context) : Controller /// with not found [HttpGet("Chapter/{ChapterId}")] [ProducesResponseType(Status200OK, "application/json")] - [ProducesResponseType(Status404NotFound)] - public async Task GetChapter (string ChapterId) + [ProducesResponseType(Status404NotFound, "text/plain")] + public async Task, NotFound>> GetChapter (string ChapterId) { if (await context.Chapters.FirstOrDefaultAsync(c => c.Key == ChapterId, HttpContext.RequestAborted) is not { } chapter) - return NotFound(nameof(ChapterId)); + return TypedResults.NotFound(nameof(ChapterId)); - return Ok(chapter); + IEnumerable ids = chapter.MangaConnectorIds.Select(id => + new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload)); + return TypedResults.Ok(new Chapter(chapter.Key, chapter.ParentMangaId, chapter.VolumeNumber, chapter.ChapterNumber, chapter.Title,ids, chapter.Downloaded)); } /// @@ -54,41 +61,46 @@ public class QueryController(MangaContext context) : Controller /// /// with not found [HttpGet("Manga/MangaConnectorId/{MangaConnectorIdId}")] - [ProducesResponseType>(Status200OK, "application/json")] - [ProducesResponseType(Status404NotFound)] - public async Task GetMangaMangaConnectorId (string MangaConnectorIdId) + [ProducesResponseType(Status200OK, "application/json")] + [ProducesResponseType(Status404NotFound, "text/plain")] + public async Task, NotFound>> GetMangaMangaConnectorId (string MangaConnectorIdId) { if (await context.MangaConnectorToManga.FirstOrDefaultAsync(c => c.Key == MangaConnectorIdId, HttpContext.RequestAborted) is not { } mcIdManga) - return NotFound(nameof(MangaConnectorIdId)); + return TypedResults.NotFound(nameof(MangaConnectorIdId)); + + MangaConnectorId result = new (mcIdManga.Key, mcIdManga.MangaConnectorName, mcIdManga.ObjId, mcIdManga.WebsiteUrl, mcIdManga.UseForDownload); - return Ok(mcIdManga); + return TypedResults.Ok(result); } /// - /// Returns with names similar to (identified by ) + /// Returns with names similar to (identified by ) /// - /// Key of + /// Key of /// - /// with not found + /// with not found /// Error during Database Operation [HttpGet("Manga/{MangaId}/SimilarName")] - [ProducesResponseType(Status200OK, "application/json")] - [ProducesResponseType(Status404NotFound)] - public async Task GetSimilarManga (string MangaId) + [ProducesResponseType>(Status200OK, "application/json")] + [ProducesResponseType(Status404NotFound, "text/plain")] + [ProducesResponseType(Status500InternalServerError)] + public async Task>, NotFound, InternalServerError>> GetSimilarManga (string MangaId) { if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) - return NotFound(nameof(MangaId)); + return TypedResults.NotFound(nameof(MangaId)); string name = manga.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 + + if (await context.Mangas.Where(m => m.Key != MangaId) + .ToDictionaryAsync(m => m.Key, m => m.Name, HttpContext.RequestAborted) is not { } mangaNames) + return TypedResults.InternalServerError(); + + List similarIds = mangaNames .Where(kv => NeedlemanWunschStringUtil.CalculateSimilarityPercentage(name, kv.Value) > 0.8) - .Select(kv => kv.Key).ToArray(); + .Select(kv => kv.Key) + .ToList(); - return Ok(similarIds); + return TypedResults.Ok(similarIds); } /// @@ -98,13 +110,15 @@ public class QueryController(MangaContext context) : Controller /// /// with not found [HttpGet("Chapter/MangaConnectorId/{MangaConnectorIdId}")] - [ProducesResponseType>(Status200OK, "application/json")] - [ProducesResponseType(Status404NotFound)] - public async Task GetChapterMangaConnectorId (string MangaConnectorIdId) + [ProducesResponseType(Status200OK, "application/json")] + [ProducesResponseType(Status404NotFound, "text/plain")] + public async Task, NotFound>> GetChapterMangaConnectorId (string MangaConnectorIdId) { if (await context.MangaConnectorToManga.FirstOrDefaultAsync(c => c.Key == MangaConnectorIdId, HttpContext.RequestAborted) is not { } mcIdChapter) - return NotFound(nameof(MangaConnectorIdId)); + return TypedResults.NotFound(nameof(MangaConnectorIdId)); - return Ok(mcIdChapter); + MangaConnectorId result = new(mcIdChapter.Key, mcIdChapter.MangaConnectorName, mcIdChapter.ObjId, mcIdChapter.WebsiteUrl, mcIdChapter.UseForDownload); + + return TypedResults.Ok(result); } } \ No newline at end of file diff --git a/API/Controllers/SearchController.cs b/API/Controllers/SearchController.cs index eb02f41..d337e42 100644 --- a/API/Controllers/SearchController.cs +++ b/API/Controllers/SearchController.cs @@ -2,8 +2,11 @@ using API.Controllers.DTOs; using API.MangaConnectors; using API.Schema.MangaContext; using Asp.Versioning; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using static Microsoft.AspNetCore.Http.StatusCodes; +using Manga = API.Schema.MangaContext.Manga; + // ReSharper disable InconsistentNaming namespace API.Controllers; @@ -14,58 +17,62 @@ namespace API.Controllers; public class SearchController(MangaContext context) : Controller { /// - /// Initiate a search for a on with searchTerm + /// Initiate a search for a on with searchTerm /// /// .Name /// searchTerm - /// exert of . Use for more information + /// exert of /// with Name not found /// with Name is disabled [HttpGet("{MangaConnectorName}/{Query}")] - [ProducesResponseType(Status200OK, "application/json")] - [ProducesResponseType(Status404NotFound)] + [ProducesResponseType>(Status200OK, "application/json")] + [ProducesResponseType(Status404NotFound, "text/plain")] [ProducesResponseType(Status406NotAcceptable)] - public IActionResult SearchManga (string MangaConnectorName, string Query) + public Results>, NotFound, StatusCodeHttpResult> SearchManga (string MangaConnectorName, string Query) { if(Tranga.MangaConnectors.FirstOrDefault(c => c.Name.Equals(MangaConnectorName, StringComparison.InvariantCultureIgnoreCase)) is not { } connector) - return NotFound(); + return TypedResults.NotFound(nameof(MangaConnectorName)); if (connector.Enabled is false) - return StatusCode(Status412PreconditionFailed); + return TypedResults.StatusCode(Status412PreconditionFailed); - (Manga, MangaConnectorId)[] mangas = connector.SearchManga(Query); - List retMangas = new(); - foreach ((Manga manga, MangaConnectorId mcId) manga in mangas) - { - if(Tranga.AddMangaToContext(manga, context, out Manga? add, HttpContext.RequestAborted)) - retMangas.Add(add); - } + (Manga manga, MangaConnectorId id)[] mangas = connector.SearchManga(Query); - return Ok(retMangas.Select(m => new MinimalManga(m.Key, m.Name, m.Description, m.ReleaseStatus, m.MangaConnectorIds))); + IEnumerable result = mangas.Select(manga => manga.manga).Select(m => + { + IEnumerable ids = m.MangaConnectorIds.Select(id => + new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload)); + return new MinimalManga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids); + }); + + return TypedResults.Ok(result.ToList()); } /// - /// Returns from the associated with + /// Returns from the associated with /// /// - /// exert of . Use for more information - /// Multiple found for URL + /// exert of . /// not found /// Error during Database Operation [HttpPost("Url")] [ProducesResponseType(Status200OK, "application/json")] - [ProducesResponseType(Status404NotFound)] - [ProducesResponseType(Status500InternalServerError)] - public IActionResult GetMangaFromUrl ([FromBody]string url) + [ProducesResponseType(Status404NotFound, "text/plain")] + [ProducesResponseType(Status500InternalServerError, "text/plain")] + public Results, NotFound, InternalServerError> 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."); + return TypedResults.InternalServerError("Could not find Global Connector."); if(connector.GetMangaFromUrl(url) is not { } manga) - return NotFound(); + return TypedResults.NotFound("Could not retrieve Manga"); - if(Tranga.AddMangaToContext(manga, context, out Manga? add, HttpContext.RequestAborted) == false) - return StatusCode(Status500InternalServerError); + if(Tranga.AddMangaToContext(manga, context, out Manga? m, HttpContext.RequestAborted) == false) + return TypedResults.InternalServerError("Could not add Manga to context"); - return Ok(new MinimalManga(add.Key, add.Name, add.Description, add.ReleaseStatus, add.MangaConnectorIds)); + IEnumerable ids = m.MangaConnectorIds.Select(id => + new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload)); + MinimalManga result = new (m.Key, m.Name, m.Description, m.ReleaseStatus, ids); + + return TypedResults.Ok(result); } } \ No newline at end of file diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs index 72a8bd4..c7c9d05 100644 --- a/API/Controllers/SettingsController.cs +++ b/API/Controllers/SettingsController.cs @@ -1,5 +1,6 @@ using API.MangaDownloadClients; using Asp.Versioning; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using static Microsoft.AspNetCore.Http.StatusCodes; // ReSharper disable InconsistentNaming @@ -17,9 +18,9 @@ public class SettingsController() : Controller /// [HttpGet] [ProducesResponseType(Status200OK, "application/json")] - public IActionResult GetSettings() + public Ok GetSettings() { - return Ok(Tranga.Settings); + return TypedResults.Ok(Tranga.Settings); } /// @@ -28,9 +29,9 @@ public class SettingsController() : Controller /// [HttpGet("UserAgent")] [ProducesResponseType(Status200OK, "text/plain")] - public IActionResult GetUserAgent() + public Ok GetUserAgent() { - return Ok(Tranga.Settings.UserAgent); + return TypedResults.Ok(Tranga.Settings.UserAgent); } /// @@ -39,11 +40,11 @@ public class SettingsController() : Controller /// [HttpPatch("UserAgent")] [ProducesResponseType(Status200OK)] - public IActionResult SetUserAgent([FromBody]string userAgent) + public Ok SetUserAgent([FromBody]string userAgent) { //TODO Validate Tranga.Settings.SetUserAgent(userAgent); - return Ok(); + return TypedResults.Ok(); } /// @@ -52,10 +53,10 @@ public class SettingsController() : Controller /// [HttpDelete("UserAgent")] [ProducesResponseType(Status200OK)] - public IActionResult ResetUserAgent() + public Ok ResetUserAgent() { Tranga.Settings.SetUserAgent(TrangaSettings.DefaultUserAgent); - return Ok(); + return TypedResults.Ok(); } /// @@ -64,9 +65,9 @@ public class SettingsController() : Controller /// [HttpGet("RequestLimits")] [ProducesResponseType>(Status200OK, "application/json")] - public IActionResult GetRequestLimits() + public Ok> GetRequestLimits() { - return Ok(Tranga.Settings.RequestLimits); + return TypedResults.Ok(Tranga.Settings.RequestLimits); } /// @@ -75,9 +76,9 @@ public class SettingsController() : Controller ///

NOT IMPLEMENTED

[HttpPatch("RequestLimits")] [ProducesResponseType(Status501NotImplemented)] - public IActionResult SetRequestLimits() + public StatusCodeHttpResult SetRequestLimits() { - return StatusCode(501); + return TypedResults.StatusCode(Status501NotImplemented); } /// @@ -90,12 +91,12 @@ public class SettingsController() : Controller [HttpPatch("RequestLimits/{RequestType}")] [ProducesResponseType(Status200OK)] [ProducesResponseType(Status400BadRequest)] - public IActionResult SetRequestLimit(RequestType RequestType, [FromBody]int requestLimit) + public Results SetRequestLimit(RequestType RequestType, [FromBody]int requestLimit) { if (requestLimit <= 0) - return BadRequest(); + return TypedResults.BadRequest(); Tranga.Settings.SetRequestLimit(RequestType, requestLimit); - return Ok(); + return TypedResults.Ok(); } /// @@ -104,10 +105,10 @@ public class SettingsController() : Controller /// [HttpDelete("RequestLimits/{RequestType}")] [ProducesResponseType(Status200OK)] - public IActionResult ResetRequestLimits(RequestType RequestType) + public Ok ResetRequestLimits(RequestType RequestType) { Tranga.Settings.SetRequestLimit(RequestType, TrangaSettings.DefaultRequestLimits[RequestType]); - return Ok(); + return TypedResults.Ok(); } /// @@ -116,10 +117,10 @@ public class SettingsController() : Controller /// [HttpDelete("RequestLimits")] [ProducesResponseType(Status200OK)] - public IActionResult ResetRequestLimits() + public Ok ResetRequestLimits() { Tranga.Settings.ResetRequestLimits(); - return Ok(); + return TypedResults.Ok(); } /// @@ -128,9 +129,9 @@ public class SettingsController() : Controller /// JPEG ImageCompression-level as Integer [HttpGet("ImageCompressionLevel")] [ProducesResponseType(Status200OK, "text/plain")] - public IActionResult GetImageCompression() + public Ok GetImageCompression() { - return Ok(Tranga.Settings.ImageCompression); + return TypedResults.Ok(Tranga.Settings.ImageCompression); } /// @@ -142,12 +143,12 @@ public class SettingsController() : Controller [HttpPatch("ImageCompressionLevel/{level}")] [ProducesResponseType(Status200OK)] [ProducesResponseType(Status400BadRequest)] - public IActionResult SetImageCompression(int level) + public Results SetImageCompression(int level) { if (level < 1 || level > 100) - return BadRequest(); + return TypedResults.BadRequest(); Tranga.Settings.UpdateImageCompression(level); - return Ok(); + return TypedResults.Ok(); } /// @@ -156,9 +157,9 @@ public class SettingsController() : Controller /// True if enabled [HttpGet("BWImages")] [ProducesResponseType(Status200OK, "text/plain")] - public IActionResult GetBwImagesToggle() + public Ok GetBwImagesToggle() { - return Ok(Tranga.Settings.BlackWhiteImages); + return TypedResults.Ok(Tranga.Settings.BlackWhiteImages); } /// @@ -168,10 +169,10 @@ public class SettingsController() : Controller /// [HttpPatch("BWImages/{enabled}")] [ProducesResponseType(Status200OK)] - public IActionResult SetBwImagesToggle(bool enabled) + public Ok SetBwImagesToggle(bool enabled) { Tranga.Settings.SetBlackWhiteImageEnabled(enabled); - return Ok(); + return TypedResults.Ok(); } /// @@ -194,9 +195,9 @@ public class SettingsController() : Controller /// [HttpGet("ChapterNamingScheme")] [ProducesResponseType(Status200OK, "text/plain")] - public IActionResult GetCustomNamingScheme() + public Ok GetCustomNamingScheme() { - return Ok(Tranga.Settings.ChapterNamingScheme); + return TypedResults.Ok(Tranga.Settings.ChapterNamingScheme); } /// @@ -217,12 +218,12 @@ public class SettingsController() : Controller /// [HttpPatch("ChapterNamingScheme")] [ProducesResponseType(Status200OK)] - public IActionResult SetCustomNamingScheme([FromBody]string namingScheme) + public Ok SetCustomNamingScheme([FromBody]string namingScheme) { //TODO Move old Chapters Tranga.Settings.SetChapterNamingScheme(namingScheme); - return Ok(); + return TypedResults.Ok(); } /// @@ -232,10 +233,10 @@ public class SettingsController() : Controller /// [HttpPost("FlareSolverr/Url")] [ProducesResponseType(Status200OK)] - public IActionResult SetFlareSolverrUrl([FromBody]string flareSolverrUrl) + public Ok SetFlareSolverrUrl([FromBody]string flareSolverrUrl) { Tranga.Settings.SetFlareSolverrUrl(flareSolverrUrl); - return Ok(); + return TypedResults.Ok(); } /// @@ -244,10 +245,10 @@ public class SettingsController() : Controller /// [HttpDelete("FlareSolverr/Url")] [ProducesResponseType(Status200OK)] - public IActionResult ClearFlareSolverrUrl() + public Ok ClearFlareSolverrUrl() { Tranga.Settings.SetFlareSolverrUrl(string.Empty); - return Ok(); + return TypedResults.Ok(); } /// @@ -258,12 +259,12 @@ public class SettingsController() : Controller [HttpPost("FlareSolverr/Test")] [ProducesResponseType(Status200OK)] [ProducesResponseType(Status500InternalServerError)] - public IActionResult TestFlareSolverrReachable() + public Results TestFlareSolverrReachable() { const string knownProtectedUrl = "https://prowlarr.servarr.com/v1/ping"; FlareSolverrDownloadClient client = new(); RequestResult result = client.MakeRequestInternal(knownProtectedUrl); - return (int)result.statusCode >= 200 && (int)result.statusCode < 300 ? Ok() : StatusCode(500, result.statusCode); + return (int)result.statusCode >= 200 && (int)result.statusCode < 300 ? TypedResults.Ok() : TypedResults.InternalServerError(); } /// @@ -272,9 +273,9 @@ public class SettingsController() : Controller /// [HttpGet("DownloadLanguage")] [ProducesResponseType(Status200OK, "text/plain")] - public IActionResult GetDownloadLanguage() + public Ok GetDownloadLanguage() { - return Ok(Tranga.Settings.DownloadLanguage); + return TypedResults.Ok(Tranga.Settings.DownloadLanguage); } /// @@ -283,10 +284,10 @@ public class SettingsController() : Controller /// [HttpPatch("DownloadLanguage/{Language}")] [ProducesResponseType(Status200OK)] - public IActionResult SetDownloadLanguage(string Language) + public Ok SetDownloadLanguage(string Language) { //TODO Validation Tranga.Settings.SetDownloadLanguage(Language); - return Ok(); + return TypedResults.Ok(); } } \ No newline at end of file diff --git a/API/Controllers/WorkerController.cs b/API/Controllers/WorkerController.cs index 871df70..25cf761 100644 --- a/API/Controllers/WorkerController.cs +++ b/API/Controllers/WorkerController.cs @@ -1,5 +1,7 @@ -using API.Workers; +using API.Controllers.DTOs; +using API.Workers; using Asp.Versioning; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using static Microsoft.AspNetCore.Http.StatusCodes; // ReSharper disable InconsistentNaming @@ -14,12 +16,14 @@ public class WorkerController() : Controller /// /// Returns all /// - /// + /// [HttpGet] - [ProducesResponseType(Status200OK, "application/json")] - public IActionResult GetWorkers() + [ProducesResponseType>(Status200OK, "application/json")] + public Ok> GetWorkers() { - return Ok(Tranga.GetRunningWorkers().ToArray()); + IEnumerable result = Tranga.GetRunningWorkers().Select(w => + new Worker(w.Key, w.AllDependencies.Select(d => d.Key), w.MissingDependencies.Select(d => d.Key), w.AllDependenciesFulfilled, w.State)); + return TypedResults.Ok(result.ToList()); } /// @@ -28,9 +32,9 @@ public class WorkerController() : Controller /// [HttpGet("Keys")] [ProducesResponseType(Status200OK, "application/json")] - public IActionResult GetWorkerIds() + public Ok> GetWorkerIds() { - return Ok(Tranga.GetRunningWorkers().Select(w => w.Key).ToArray()); + return TypedResults.Ok(Tranga.GetRunningWorkers().Select(w => w.Key).ToList()); } /// @@ -39,10 +43,12 @@ public class WorkerController() : Controller /// Requested /// [HttpGet("State/{State}")] - [ProducesResponseType(Status200OK, "application/json")] - public IActionResult GetWorkersInState(WorkerExecutionState State) + [ProducesResponseType>(Status200OK, "application/json")] + public Ok> GetWorkersInState(WorkerExecutionState State) { - return Ok(Tranga.GetRunningWorkers().Where(worker => worker.State == State).ToArray()); + IEnumerable result = Tranga.GetRunningWorkers().Where(worker => worker.State == State).Select(w => + new Worker(w.Key, w.AllDependencies.Select(d => d.Key), w.MissingDependencies.Select(d => d.Key), w.AllDependenciesFulfilled, w.State)); + return TypedResults.Ok(result.ToList()); } /// @@ -52,13 +58,16 @@ public class WorkerController() : Controller /// /// with could not be found [HttpGet("{WorkerId}")] - [ProducesResponseType(Status200OK, "application/json")] - [ProducesResponseType(Status404NotFound)] - public IActionResult GetWorker(string WorkerId) + [ProducesResponseType(Status200OK, "application/json")] + [ProducesResponseType(Status404NotFound, "text/plain")] + public Results, NotFound> GetWorker(string WorkerId) { - if(Tranga.GetRunningWorkers().FirstOrDefault(w => w.Key == WorkerId) is not { } worker) - return NotFound(nameof(WorkerId)); - return Ok(worker); + if(Tranga.GetRunningWorkers().FirstOrDefault(w => w.Key == WorkerId) is not { } w) + return TypedResults.NotFound(nameof(WorkerId)); + + Worker result = new (w.Key, w.AllDependencies.Select(d => d.Key), w.MissingDependencies.Select(d => d.Key), w.AllDependenciesFulfilled, w.State); + + return TypedResults.Ok(result); } /// @@ -69,13 +78,13 @@ public class WorkerController() : Controller /// with could not be found [HttpDelete("{WorkerId}")] [ProducesResponseType(Status200OK)] - [ProducesResponseType(Status404NotFound)] - public IActionResult DeleteWorker(string WorkerId) + [ProducesResponseType(Status404NotFound, "text/plain")] + public Results> DeleteWorker(string WorkerId) { if(Tranga.GetRunningWorkers().FirstOrDefault(w => w.Key == WorkerId) is not { } worker) - return NotFound(nameof(WorkerId)); + return TypedResults.NotFound(nameof(WorkerId)); Tranga.StopWorker(worker); - return Ok(); + return TypedResults.Ok(); } /// @@ -87,18 +96,18 @@ public class WorkerController() : Controller /// was already running [HttpPost("{WorkerId}/Start")] [ProducesResponseType(Status202Accepted)] - [ProducesResponseType(Status404NotFound)] - [ProducesResponseType(Status412PreconditionFailed, "text/plain")] - public IActionResult StartWorker(string WorkerId) + [ProducesResponseType(Status404NotFound, "text/plain")] + [ProducesResponseType(Status412PreconditionFailed)] + public Results, StatusCodeHttpResult> StartWorker(string WorkerId) { if(Tranga.GetRunningWorkers().FirstOrDefault(w => w.Key == WorkerId) is not { } worker) - return NotFound(nameof(WorkerId)); + return TypedResults.NotFound(nameof(WorkerId)); if (worker.State >= WorkerExecutionState.Waiting) - return StatusCode(Status412PreconditionFailed, "Already running"); + return TypedResults.StatusCode(Status412PreconditionFailed); Tranga.StartWorker(worker); - return Ok(); + return TypedResults.Ok(); } /// @@ -107,18 +116,20 @@ public class WorkerController() : Controller /// .Key /// /// with could not be found - /// was not running + /// was already not running [HttpPost("{WorkerId}/Stop")] - [ProducesResponseType(Status501NotImplemented)] - public IActionResult StopWorker(string WorkerId) + [ProducesResponseType(Status202Accepted)] + [ProducesResponseType(Status404NotFound, "text/plain")] + [ProducesResponseType(Status412PreconditionFailed)] + public Results, StatusCodeHttpResult> StopWorker(string WorkerId) { if(Tranga.GetRunningWorkers().FirstOrDefault(w => w.Key == WorkerId) is not { } worker) - return NotFound(nameof(WorkerId)); + return TypedResults.NotFound(nameof(WorkerId)); if(worker.State is < WorkerExecutionState.Running or >= WorkerExecutionState.Completed) - return StatusCode(Status208AlreadyReported, "Not running"); + return TypedResults.StatusCode(Status412PreconditionFailed); Tranga.StopWorker(worker); - return Ok(); + return TypedResults.Ok(); } } \ No newline at end of file diff --git a/API/Schema/MangaContext/Author.cs b/API/Schema/MangaContext/Author.cs index 6529f1c..6dbcd83 100644 --- a/API/Schema/MangaContext/Author.cs +++ b/API/Schema/MangaContext/Author.cs @@ -7,7 +7,6 @@ namespace API.Schema.MangaContext; public class Author(string authorName) : Identifiable(TokenGen.CreateToken(typeof(Author), authorName)) { [StringLength(128)] - [Required] public string AuthorName { get; init; } = authorName; public override string ToString() => $"{base.ToString()} {AuthorName}"; diff --git a/API/Schema/MangaContext/Chapter.cs b/API/Schema/MangaContext/Chapter.cs index ba59338..ce84cc9 100644 --- a/API/Schema/MangaContext/Chapter.cs +++ b/API/Schema/MangaContext/Chapter.cs @@ -4,28 +4,27 @@ using System.Text; using System.Text.RegularExpressions; using System.Xml.Linq; using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json; namespace API.Schema.MangaContext; [PrimaryKey("Key")] public class Chapter : Identifiable, IComparable { - [StringLength(64)] [Required] public string ParentMangaId { get; init; } = null!; - [JsonIgnore] public Manga ParentManga = null!; + [StringLength(64)] public string ParentMangaId { get; init; } = null!; + public Manga ParentManga = null!; [NotMapped] public Dictionary IdsOnMangaConnectors => MangaConnectorIds.ToDictionary(id => id.MangaConnectorName, id => id.IdOnConnectorSite); - [JsonIgnore] public ICollection> MangaConnectorIds = null!; + public ICollection> MangaConnectorIds = null!; public int? VolumeNumber { get; private set; } - [StringLength(10)] [Required] public string ChapterNumber { get; private set; } + [StringLength(10)] public string ChapterNumber { get; private set; } [StringLength(256)] public string? Title { get; private set; } - [StringLength(256)] [Required] public string FileName { get; private set; } + [StringLength(256)] public string FileName { get; private set; } - [Required] public bool Downloaded { get; internal set; } + public bool Downloaded { get; internal set; } [NotMapped] public string FullArchiveFilePath => Path.Join(ParentManga.FullDirectoryPath, FileName); public Chapter(Manga parentManga, string chapterNumber, diff --git a/API/Schema/MangaContext/Link.cs b/API/Schema/MangaContext/Link.cs index 871b6e1..c9e34a5 100644 --- a/API/Schema/MangaContext/Link.cs +++ b/API/Schema/MangaContext/Link.cs @@ -7,10 +7,8 @@ namespace API.Schema.MangaContext; public class Link(string linkProvider, string linkUrl) : Identifiable(TokenGen.CreateToken(typeof(Link), linkProvider, linkUrl)) { [StringLength(64)] - [Required] public string LinkProvider { get; init; } = linkProvider; [StringLength(2048)] - [Required] [Url] public string LinkUrl { get; init; } = linkUrl; diff --git a/API/Schema/MangaContext/Manga.cs b/API/Schema/MangaContext/Manga.cs index b606f16..66b25c0 100644 --- a/API/Schema/MangaContext/Manga.cs +++ b/API/Schema/MangaContext/Manga.cs @@ -4,8 +4,6 @@ using System.Runtime.InteropServices; using System.Text; using API.Workers; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Newtonsoft.Json; using static System.IO.UnixFileMode; namespace API.Schema.MangaContext; @@ -13,34 +11,31 @@ namespace API.Schema.MangaContext; [PrimaryKey("Key")] public class Manga : Identifiable { - [StringLength(512)] [Required] public string Name { get; internal set; } + [StringLength(512)] public string Name { get; internal set; } [Required] public string Description { get; internal set; } - [JsonIgnore] [Url] [StringLength(512)] public string CoverUrl { get; internal set; } - [Required] public MangaReleaseStatus ReleaseStatus { get; internal set; } + [Url] [StringLength(512)] public string CoverUrl { get; internal set; } + public MangaReleaseStatus ReleaseStatus { get; internal set; } [StringLength(64)] public string? LibraryId { get; private set; } - [JsonIgnore] public FileLibrary? Library = null!; + public FileLibrary? Library = null!; public ICollection Authors { get; internal set; } = null!; public ICollection MangaTags { get; internal set; } = null!; public ICollection Links { get; internal set; } = null!; public ICollection AltTitles { get; internal set; } = null!; - [Required] public float IgnoreChaptersBefore { get; internal set; } + public float IgnoreChaptersBefore { get; internal set; } [StringLength(1024)] [Required] public string DirectoryName { get; private set; } - [JsonIgnore] [StringLength(512)] public string? CoverFileNameInCache { get; internal set; } + [StringLength(512)] public string? CoverFileNameInCache { get; internal set; } public uint? Year { get; internal init; } [StringLength(8)] public string? OriginalLanguage { get; internal init; } - [JsonIgnore] - [NotMapped] - public string? FullDirectoryPath => Library is not null ? Path.Join(Library.BasePath, DirectoryName) : null; + [NotMapped] public string? FullDirectoryPath => Library is not null ? Path.Join(Library.BasePath, DirectoryName) : null; [NotMapped] public ICollection ChapterIds => Chapters.Select(c => c.Key).ToList(); - [JsonIgnore] public ICollection Chapters = null!; + public ICollection Chapters = null!; - [NotMapped] public Dictionary IdsOnMangaConnectors => - MangaConnectorIds.ToDictionary(id => id.MangaConnectorName, id => id.IdOnConnectorSite); + [NotMapped] public Dictionary IdsOnMangaConnectors => MangaConnectorIds.ToDictionary(id => id.MangaConnectorName, id => id.IdOnConnectorSite); [NotMapped] public ICollection MangaConnectorIdsIds => MangaConnectorIds.Select(id => id.Key).ToList(); - [JsonIgnore] public ICollection> MangaConnectorIds = null!; + public ICollection> MangaConnectorIds = null!; public Manga(string name, string description, string coverUrl, MangaReleaseStatus releaseStatus, ICollection authors, ICollection mangaTags, ICollection links, ICollection altTitles, diff --git a/API/Schema/MangaContext/MangaConnectorId.cs b/API/Schema/MangaContext/MangaConnectorId.cs index ee1508b..5a9c07d 100644 --- a/API/Schema/MangaContext/MangaConnectorId.cs +++ b/API/Schema/MangaContext/MangaConnectorId.cs @@ -1,19 +1,18 @@ using System.ComponentModel.DataAnnotations; using API.MangaConnectors; using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json; namespace API.Schema.MangaContext; [PrimaryKey("Key")] public class MangaConnectorId : Identifiable where T : Identifiable { - [StringLength(64)] [Required] public string ObjId { get; internal set; } - [JsonIgnore] public T Obj = null!; + public T Obj = null!; + [StringLength(64)] public string ObjId { get; internal set; } - [StringLength(32)] [Required] public string MangaConnectorName { get; private set; } + [StringLength(32)] public string MangaConnectorName { get; private set; } - [StringLength(256)] [Required] public string IdOnConnectorSite { get; init; } + [StringLength(256)] public string IdOnConnectorSite { get; init; } [Url] [StringLength(512)] public string? WebsiteUrl { get; internal init; } public bool UseForDownload { get; internal set; } diff --git a/API/Workers/BaseWorker.cs b/API/Workers/BaseWorker.cs index ef7e01c..b75d23b 100644 --- a/API/Workers/BaseWorker.cs +++ b/API/Workers/BaseWorker.cs @@ -1,6 +1,5 @@ using API.Schema; using log4net; -using Newtonsoft.Json; namespace API.Workers; @@ -9,21 +8,19 @@ public abstract class BaseWorker : Identifiable /// /// Workers this Worker depends on being completed before running. /// - public BaseWorker[] DependsOn { get; init; } + private BaseWorker[] DependsOn { get; init; } /// /// Dependencies and dependencies of dependencies. See also . /// - [JsonIgnore] - public IEnumerable AllDependencies => DependsOn.Select(d => d.AllDependencies).SelectMany(x => x); + internal IEnumerable AllDependencies => DependsOn.Select(d => d.AllDependencies).SelectMany(x => x); /// /// and Self. /// - [JsonIgnore] - public IEnumerable DependenciesAndSelf => AllDependencies.Append(this); + internal IEnumerable DependenciesAndSelf => AllDependencies.Append(this); /// /// where is less than Completed. /// - public IEnumerable MissingDependencies => DependsOn.Where(d => d.State < WorkerExecutionState.Completed); + internal IEnumerable MissingDependencies => DependsOn.Where(d => d.State < WorkerExecutionState.Completed); public bool AllDependenciesFulfilled => DependsOn.All(d => d.State >= WorkerExecutionState.Completed); internal WorkerExecutionState State { get; private set; } private CancellationTokenSource _cancellationTokenSource = new (); @@ -50,7 +47,7 @@ public abstract class BaseWorker : Identifiable _cancellationTokenSource.Cancel(); } - public BaseWorker(IEnumerable? dependsOn = null) + protected BaseWorker(IEnumerable? dependsOn = null) { this.DependsOn = dependsOn?.ToArray() ?? []; this.Log = LogManager.GetLogger(GetType());