mirror of
https://github.com/C9Glax/tranga.git
synced 2025-10-17 10:50:45 +02:00
Add ActionRecord DTO
Include all Data in ActionRecord return Add endpoints for returning actions related to manga and chapter Fix wrong syncs for ActionsContext
This commit is contained in:
@@ -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);
|
||||
/// <summary>
|
||||
/// Returns <see cref="ActionRecord"/> performed in <see cref="Interval"/>
|
||||
/// Returns <see cref="Schema.ActionsContext.ActionRecord"/> performed in <see cref="Interval"/>
|
||||
/// </summary>
|
||||
/// <response code="200">List of performed actions</response>
|
||||
/// <response code="500">Database error</response>
|
||||
[HttpPost("Interval")]
|
||||
[ProducesResponseType<List<ActionRecord>>(Status200OK, "application/json")]
|
||||
[ProducesResponseType<IEnumerable<ActionRecord>>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status500InternalServerError)]
|
||||
public async Task<Results<Ok<List<ActionRecord>>, InternalServerError>> GetActionsInterval([FromBody]Interval interval)
|
||||
public async Task<Results<Ok<IEnumerable<ActionRecord>>, 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)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see cref="ActionRecord"/> with <paramref name="Type"/> <see cref="ActionsEnum"/>
|
||||
/// Returns <see cref="Schema.ActionsContext.ActionRecord"/> with <paramref name="Type"/> <see cref="ActionsEnum"/>
|
||||
/// </summary>
|
||||
/// <response code="200">List of performed actions</response>
|
||||
/// <response code="500">Database error</response>
|
||||
[HttpGet("Type/{Type}")]
|
||||
[ProducesResponseType<List<ActionRecord>>(Status200OK, "application/json")]
|
||||
[ProducesResponseType<IEnumerable<ActionRecord>>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status500InternalServerError)]
|
||||
public async Task<Results<Ok<List<ActionRecord>>, InternalServerError>> GetActionsWithType(ActionsEnum Type)
|
||||
public async Task<Results<Ok<IEnumerable<ActionRecord>>, 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)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see cref="Schema.ActionsContext.ActionRecord"/> related to <see cref="Manga"/>
|
||||
/// </summary>
|
||||
/// <response code="200">List of performed actions</response>
|
||||
/// <response code="500">Database error</response>
|
||||
[HttpGet("RelatedTo/Manga/{MangaId}")]
|
||||
[ProducesResponseType<IEnumerable<ActionRecord>>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status500InternalServerError)]
|
||||
public async Task<Results<Ok<IEnumerable<ActionRecord>>, 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)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see cref="Schema.ActionsContext.ActionRecord"/> related to <see cref="Chapter"/>
|
||||
/// </summary>
|
||||
/// <response code="200">List of performed actions</response>
|
||||
/// <response code="500">Database error</response>
|
||||
[HttpGet("RelatedTo/Chapter/{ChapterId}")]
|
||||
[ProducesResponseType<IEnumerable<ActionRecord>>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status500InternalServerError)]
|
||||
public async Task<Results<Ok<IEnumerable<ActionRecord>>, 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)));
|
||||
}
|
||||
}
|
41
API/Controllers/DTOs/ActionRecord.cs
Normal file
41
API/Controllers/DTOs/ActionRecord.cs
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="Schema.ActionsContext.ActionRecord.Action" />
|
||||
/// </summary>
|
||||
[Required]
|
||||
public ActionsEnum Action { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="Schema.ActionsContext.ActionRecord.PerformedAt" />
|
||||
/// </summary>
|
||||
[Required]
|
||||
public DateTime PerformedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// MangaId if Record is <see cref="ActionWithMangaRecord"/>
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? MangaId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// ChapterId if Record is <see cref="ActionWithMangaRecord"/>
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? ChapterId { get; init; }
|
||||
}
|
@@ -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) { }
|
||||
|
||||
/// <summary>
|
||||
/// Chapter that was downloaded
|
||||
/// </summary>
|
||||
[StringLength(64)]
|
||||
public string ChapterId { get; init; } = chapterId;
|
||||
}
|
@@ -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) { }
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Schema.MangaContext.Manga"/> for which the cover was downloaded
|
||||
/// </summary>
|
||||
[StringLength(64)]
|
||||
public string ChapterId { get; init; } = chapterId;
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
using API.Schema.ActionsContext.Actions;
|
||||
using API.Schema.ActionsContext.Actions.Generic;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Schema.ActionsContext;
|
||||
|
@@ -50,13 +50,15 @@ public class DownloadCoverFromMangaconnectorWorker(MangaConnectorId<Manga> 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 [];
|
||||
|
@@ -92,7 +92,7 @@ public class RetrieveMangaChaptersFromMangaconnectorWorker(MangaConnectorId<Mang
|
||||
Log.Error($"Failed to save database changes: {mangaContextException.exceptionMessage}");
|
||||
|
||||
ActionsContext.Actions.Add(new ChaptersRetrievedActionRecord(manga));
|
||||
if(await MangaContext.Sync(CancellationToken, GetType(), "Chapters retrieved") is { success: false } actionsContextException)
|
||||
if(await ActionsContext.Sync(CancellationToken, GetType(), "Chapters retrieved") is { success: false } actionsContextException)
|
||||
Log.Error($"Failed to save database changes: {actionsContextException.exceptionMessage}");
|
||||
|
||||
return [];
|
||||
|
@@ -31,7 +31,10 @@ public class CleanupMangaconnectorIdsWithoutConnector : BaseWorkerWithContexts
|
||||
int deletedMangaIds = await MangaContext.MangaConnectorToManga.Where(mcId => 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 [];
|
||||
}
|
||||
}
|
@@ -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": "<inheritdoc cref=\"P:API.Schema.ActionsContext.ActionRecord.PerformedAt\" />",
|
||||
"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
|
||||
|
Reference in New Issue
Block a user