diff --git a/API/Controllers/ActionsController.cs b/API/Controllers/ActionsController.cs
index b80a6c9..f0408df 100644
--- a/API/Controllers/ActionsController.cs
+++ b/API/Controllers/ActionsController.cs
@@ -1,3 +1,4 @@
+using API.Controllers.DTOs;
using API.Schema.ActionsContext;
using API.Schema.ActionsContext.Actions;
using Asp.Versioning;
@@ -5,6 +6,7 @@ using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using static Microsoft.AspNetCore.Http.StatusCodes;
+using ActionRecord = API.Controllers.DTOs.ActionRecord;
namespace API.Controllers;
@@ -26,34 +28,66 @@ public class ActionsController(ActionsContext context) : Controller
public sealed record Interval(DateTime Start, DateTime End);
///
- /// Returns performed in
+ /// Returns performed in
///
/// List of performed actions
/// Database error
[HttpPost("Interval")]
- [ProducesResponseType>(Status200OK, "application/json")]
+ [ProducesResponseType>(Status200OK, "application/json")]
[ProducesResponseType(Status500InternalServerError)]
- public async Task>, InternalServerError>> GetActionsInterval([FromBody]Interval interval)
+ public async Task>, InternalServerError>> GetActionsInterval([FromBody]Interval interval)
{
if (await context.Actions.Where(a => a.PerformedAt >= interval.Start && a.PerformedAt <= interval.End)
.ToListAsync(HttpContext.RequestAborted) is not { } actions)
return TypedResults.InternalServerError();
- return TypedResults.Ok(actions);
+ return TypedResults.Ok(actions.Select(a => new ActionRecord(a)));
}
///
- /// Returns with
+ /// Returns with
///
/// List of performed actions
/// Database error
[HttpGet("Type/{Type}")]
- [ProducesResponseType>(Status200OK, "application/json")]
+ [ProducesResponseType>(Status200OK, "application/json")]
[ProducesResponseType(Status500InternalServerError)]
- public async Task>, InternalServerError>> GetActionsWithType(ActionsEnum Type)
+ public async Task>, InternalServerError>> GetActionsWithType(ActionsEnum Type)
{
if (await context.Actions.Where(a => a.Action == Type)
.ToListAsync(HttpContext.RequestAborted) is not { } actions)
return TypedResults.InternalServerError();
- return TypedResults.Ok(actions);
+ return TypedResults.Ok(actions.Select(a => new ActionRecord(a)));
+ }
+
+ ///
+ /// Returns related to
+ ///
+ /// List of performed actions
+ /// Database error
+ [HttpGet("RelatedTo/Manga/{MangaId}")]
+ [ProducesResponseType>(Status200OK, "application/json")]
+ [ProducesResponseType(Status500InternalServerError)]
+ public async Task>, InternalServerError>> GetActionsRelatedToManga(string MangaId)
+ {
+ if(await context.Actions.FromSqlInterpolated($"""SELECT * FROM public."Actions" WHERE "MangaId" = {MangaId}""").ToListAsync() is not { } actions)
+ return TypedResults.InternalServerError();
+
+ return TypedResults.Ok(actions.Select(a => new ActionRecord(a)));
+ }
+
+ ///
+ /// Returns related to
+ ///
+ /// List of performed actions
+ /// Database error
+ [HttpGet("RelatedTo/Chapter/{ChapterId}")]
+ [ProducesResponseType>(Status200OK, "application/json")]
+ [ProducesResponseType(Status500InternalServerError)]
+ public async Task>, InternalServerError>> GetActionsRelatedToChapter(string ChapterId)
+ {
+ if(await context.Actions.FromSqlInterpolated($"""SELECT * FROM public."Actions" WHERE "ChapterId" = {ChapterId}""").ToListAsync() is not { } actions)
+ return TypedResults.InternalServerError();
+
+ return TypedResults.Ok(actions.Select(a => new ActionRecord(a)));
}
}
\ No newline at end of file
diff --git a/API/Controllers/DTOs/ActionRecord.cs b/API/Controllers/DTOs/ActionRecord.cs
new file mode 100644
index 0000000..961e93b
--- /dev/null
+++ b/API/Controllers/DTOs/ActionRecord.cs
@@ -0,0 +1,41 @@
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Serialization;
+using API.Schema.ActionsContext.Actions;
+using API.Schema.ActionsContext.Actions.Generic;
+
+namespace API.Controllers.DTOs;
+
+public sealed record ActionRecord : Identifiable
+{
+ public ActionRecord(Schema.ActionsContext.ActionRecord actionRecord) : base(actionRecord.Key)
+ {
+ Action = actionRecord.Action;
+ PerformedAt = actionRecord.PerformedAt;
+ MangaId = actionRecord is ActionWithMangaRecord m ? m.MangaId : null;
+ ChapterId = actionRecord is ActionWithChapterRecord c ? c.ChapterId : null;
+ }
+
+ ///
+ ///
+ ///
+ [Required]
+ public ActionsEnum Action { get; init; }
+
+ ///
+ ///
+ ///
+ [Required]
+ public DateTime PerformedAt { get; init; }
+
+ ///
+ /// MangaId if Record is
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? MangaId { get; init; }
+
+ ///
+ /// ChapterId if Record is
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? ChapterId { get; init; }
+}
\ No newline at end of file
diff --git a/API/Schema/ActionsContext/Actions/ChapterDownloadedActionRecord.cs b/API/Schema/ActionsContext/Actions/ChapterDownloadedActionRecord.cs
index 81331ec..eeef8f3 100644
--- a/API/Schema/ActionsContext/Actions/ChapterDownloadedActionRecord.cs
+++ b/API/Schema/ActionsContext/Actions/ChapterDownloadedActionRecord.cs
@@ -1,15 +1,9 @@
-using System.ComponentModel.DataAnnotations;
+using API.Schema.ActionsContext.Actions.Generic;
using API.Schema.MangaContext;
namespace API.Schema.ActionsContext.Actions;
-public sealed class ChapterDownloadedActionRecord(ActionsEnum action, DateTime performedAt, string chapterId) : ActionRecord(action, performedAt)
+public sealed class ChapterDownloadedActionRecord(ActionsEnum action, DateTime performedAt, string chapterId) : ActionWithChapterRecord(action, performedAt, chapterId)
{
public ChapterDownloadedActionRecord(Chapter chapter) : this(ActionsEnum.ChapterDownloaded, DateTime.UtcNow, chapter.Key) { }
-
- ///
- /// Chapter that was downloaded
- ///
- [StringLength(64)]
- public string ChapterId { get; init; } = chapterId;
}
\ No newline at end of file
diff --git a/API/Schema/ActionsContext/Actions/Generic/ActionWithChapterRecord.cs b/API/Schema/ActionsContext/Actions/Generic/ActionWithChapterRecord.cs
new file mode 100644
index 0000000..41d84bf
--- /dev/null
+++ b/API/Schema/ActionsContext/Actions/Generic/ActionWithChapterRecord.cs
@@ -0,0 +1,15 @@
+using System.ComponentModel.DataAnnotations;
+using API.Schema.MangaContext;
+
+namespace API.Schema.ActionsContext.Actions.Generic;
+
+public abstract class ActionWithChapterRecord(ActionsEnum action, DateTime performedAt, string chapterId) : ActionRecord(action, performedAt)
+{
+ protected ActionWithChapterRecord(ActionsEnum action, DateTime performedAt, Chapter chapter) : this(action, performedAt, chapter.Key) { }
+
+ ///
+ /// for which the cover was downloaded
+ ///
+ [StringLength(64)]
+ public string ChapterId { get; init; } = chapterId;
+}
\ No newline at end of file
diff --git a/API/Schema/ActionsContext/ActionsContext.cs b/API/Schema/ActionsContext/ActionsContext.cs
index 801b18e..908e7a7 100644
--- a/API/Schema/ActionsContext/ActionsContext.cs
+++ b/API/Schema/ActionsContext/ActionsContext.cs
@@ -1,4 +1,5 @@
using API.Schema.ActionsContext.Actions;
+using API.Schema.ActionsContext.Actions.Generic;
using Microsoft.EntityFrameworkCore;
namespace API.Schema.ActionsContext;
diff --git a/API/Workers/MangaDownloadWorkers/DownloadCoverFromMangaconnectorWorker.cs b/API/Workers/MangaDownloadWorkers/DownloadCoverFromMangaconnectorWorker.cs
index 718bffc..0e57b8d 100644
--- a/API/Workers/MangaDownloadWorkers/DownloadCoverFromMangaconnectorWorker.cs
+++ b/API/Workers/MangaDownloadWorkers/DownloadCoverFromMangaconnectorWorker.cs
@@ -50,13 +50,15 @@ public class DownloadCoverFromMangaconnectorWorker(MangaConnectorId mcId,
Log.Error($"Could not get Cover for MangaConnectorId {mangaConnectorId}.");
return [];
}
- MangaContext.Entry(mangaConnectorId.Obj).Property(m => m.CoverFileNameInCache).CurrentValue = coverFileName;
+
+ await MangaContext.Entry(mangaConnectorId).Reference(m => m.Obj).LoadAsync(CancellationToken);
+ mangaConnectorId.Obj.CoverFileNameInCache = coverFileName;
if(await MangaContext.Sync(CancellationToken, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false } mangaContextException)
Log.Error($"Failed to save database changes: {mangaContextException.exceptionMessage}");
ActionsContext.Actions.Add(new CoverDownloadedActionRecord(mcId.Obj, coverFileName));
- if(await MangaContext.Sync(CancellationToken, GetType(), "Download complete") is { success: false } actionsContextException)
+ if(await ActionsContext.Sync(CancellationToken, GetType(), "Download complete") is { success: false } actionsContextException)
Log.Error($"Failed to save database changes: {actionsContextException.exceptionMessage}");
return [];
diff --git a/API/Workers/MangaDownloadWorkers/RetrieveMangaChaptersFromMangaconnectorWorker.cs b/API/Workers/MangaDownloadWorkers/RetrieveMangaChaptersFromMangaconnectorWorker.cs
index 8985b4e..398561f 100644
--- a/API/Workers/MangaDownloadWorkers/RetrieveMangaChaptersFromMangaconnectorWorker.cs
+++ b/API/Workers/MangaDownloadWorkers/RetrieveMangaChaptersFromMangaconnectorWorker.cs
@@ -92,7 +92,7 @@ public class RetrieveMangaChaptersFromMangaconnectorWorker(MangaConnectorId connectorNames.All(name => name != mcId.MangaConnectorName)).ExecuteDeleteAsync(CancellationToken);
Log.Info($"Deleted {deletedMangaIds} mangaIds.");
- await MangaContext.SaveChangesAsync(CancellationToken);
+
+ if(await MangaContext.Sync(CancellationToken, GetType(), "Cleanup done") is { success: false } e)
+ Log.Error($"Failed to save database changes: {e.exceptionMessage}");
+
return [];
}
}
\ No newline at end of file
diff --git a/API/openapi/API_v2.json b/API/openapi/API_v2.json
index c225dc1..aea45a2 100644
--- a/API/openapi/API_v2.json
+++ b/API/openapi/API_v2.json
@@ -114,6 +114,78 @@
}
}
},
+ "/v2/Actions/RelatedTo/Manga/{MangaId}": {
+ "get": {
+ "tags": [
+ "Actions"
+ ],
+ "summary": "Returns API.Schema.ActionsContext.ActionRecord related to API.Controllers.DTOs.Manga",
+ "parameters": [
+ {
+ "name": "MangaId",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "List of performed actions",
+ "content": {
+ "application/json; x-version=2.0": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/ActionRecord"
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Database error"
+ }
+ }
+ }
+ },
+ "/v2/Actions/RelatedTo/Chapter/{ChapterId}": {
+ "get": {
+ "tags": [
+ "Actions"
+ ],
+ "summary": "Returns API.Schema.ActionsContext.ActionRecord related to API.Controllers.DTOs.Chapter",
+ "parameters": [
+ {
+ "name": "ChapterId",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "List of performed actions",
+ "content": {
+ "application/json; x-version=2.0": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/ActionRecord"
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Database error"
+ }
+ }
+ }
+ },
"/v2/Chapters/{MangaId}": {
"get": {
"tags": [
@@ -3189,7 +3261,9 @@
"schemas": {
"ActionRecord": {
"required": [
- "key"
+ "action",
+ "key",
+ "performedAt"
],
"type": "object",
"properties": {
@@ -3198,13 +3272,24 @@
},
"performedAt": {
"type": "string",
- "description": "UTC Time when Action was performed",
+ "description": "",
"format": "date-time"
},
+ "mangaId": {
+ "type": "string",
+ "description": "MangaId if Record is API.Schema.ActionsContext.Actions.Generic.ActionWithMangaRecord",
+ "nullable": true
+ },
+ "chapterId": {
+ "type": "string",
+ "description": "ChapterId if Record is API.Schema.ActionsContext.Actions.Generic.ActionWithMangaRecord",
+ "nullable": true
+ },
"key": {
"maxLength": 64,
"minLength": 16,
- "type": "string"
+ "type": "string",
+ "description": "Unique Identifier of the DTO"
}
},
"additionalProperties": false