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;
|
||||||
using API.Schema.ActionsContext.Actions;
|
using API.Schema.ActionsContext.Actions;
|
||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
@@ -5,6 +6,7 @@ using Microsoft.AspNetCore.Http.HttpResults;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||||
|
using ActionRecord = API.Controllers.DTOs.ActionRecord;
|
||||||
|
|
||||||
namespace API.Controllers;
|
namespace API.Controllers;
|
||||||
|
|
||||||
@@ -26,34 +28,66 @@ public class ActionsController(ActionsContext context) : Controller
|
|||||||
|
|
||||||
public sealed record Interval(DateTime Start, DateTime End);
|
public sealed record Interval(DateTime Start, DateTime End);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns <see cref="ActionRecord"/> performed in <see cref="Interval"/>
|
/// Returns <see cref="Schema.ActionsContext.ActionRecord"/> performed in <see cref="Interval"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="200">List of performed actions</response>
|
/// <response code="200">List of performed actions</response>
|
||||||
/// <response code="500">Database error</response>
|
/// <response code="500">Database error</response>
|
||||||
[HttpPost("Interval")]
|
[HttpPost("Interval")]
|
||||||
[ProducesResponseType<List<ActionRecord>>(Status200OK, "application/json")]
|
[ProducesResponseType<IEnumerable<ActionRecord>>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType(Status500InternalServerError)]
|
[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)
|
if (await context.Actions.Where(a => a.PerformedAt >= interval.Start && a.PerformedAt <= interval.End)
|
||||||
.ToListAsync(HttpContext.RequestAborted) is not { } actions)
|
.ToListAsync(HttpContext.RequestAborted) is not { } actions)
|
||||||
return TypedResults.InternalServerError();
|
return TypedResults.InternalServerError();
|
||||||
return TypedResults.Ok(actions);
|
return TypedResults.Ok(actions.Select(a => new ActionRecord(a)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <response code="200">List of performed actions</response>
|
/// <response code="200">List of performed actions</response>
|
||||||
/// <response code="500">Database error</response>
|
/// <response code="500">Database error</response>
|
||||||
[HttpGet("Type/{Type}")]
|
[HttpGet("Type/{Type}")]
|
||||||
[ProducesResponseType<List<ActionRecord>>(Status200OK, "application/json")]
|
[ProducesResponseType<IEnumerable<ActionRecord>>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType(Status500InternalServerError)]
|
[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)
|
if (await context.Actions.Where(a => a.Action == Type)
|
||||||
.ToListAsync(HttpContext.RequestAborted) is not { } actions)
|
.ToListAsync(HttpContext.RequestAborted) is not { } actions)
|
||||||
return TypedResults.InternalServerError();
|
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;
|
using API.Schema.MangaContext;
|
||||||
|
|
||||||
namespace API.Schema.ActionsContext.Actions;
|
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) { }
|
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;
|
||||||
|
using API.Schema.ActionsContext.Actions.Generic;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace API.Schema.ActionsContext;
|
namespace API.Schema.ActionsContext;
|
||||||
|
@@ -50,13 +50,15 @@ public class DownloadCoverFromMangaconnectorWorker(MangaConnectorId<Manga> mcId,
|
|||||||
Log.Error($"Could not get Cover for MangaConnectorId {mangaConnectorId}.");
|
Log.Error($"Could not get Cover for MangaConnectorId {mangaConnectorId}.");
|
||||||
return [];
|
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)
|
if(await MangaContext.Sync(CancellationToken, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false } mangaContextException)
|
||||||
Log.Error($"Failed to save database changes: {mangaContextException.exceptionMessage}");
|
Log.Error($"Failed to save database changes: {mangaContextException.exceptionMessage}");
|
||||||
|
|
||||||
ActionsContext.Actions.Add(new CoverDownloadedActionRecord(mcId.Obj, coverFileName));
|
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}");
|
Log.Error($"Failed to save database changes: {actionsContextException.exceptionMessage}");
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
|
@@ -92,7 +92,7 @@ public class RetrieveMangaChaptersFromMangaconnectorWorker(MangaConnectorId<Mang
|
|||||||
Log.Error($"Failed to save database changes: {mangaContextException.exceptionMessage}");
|
Log.Error($"Failed to save database changes: {mangaContextException.exceptionMessage}");
|
||||||
|
|
||||||
ActionsContext.Actions.Add(new ChaptersRetrievedActionRecord(manga));
|
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}");
|
Log.Error($"Failed to save database changes: {actionsContextException.exceptionMessage}");
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
|
@@ -31,7 +31,10 @@ public class CleanupMangaconnectorIdsWithoutConnector : BaseWorkerWithContexts
|
|||||||
int deletedMangaIds = await MangaContext.MangaConnectorToManga.Where(mcId => connectorNames.All(name => name != mcId.MangaConnectorName)).ExecuteDeleteAsync(CancellationToken);
|
int deletedMangaIds = await MangaContext.MangaConnectorToManga.Where(mcId => connectorNames.All(name => name != mcId.MangaConnectorName)).ExecuteDeleteAsync(CancellationToken);
|
||||||
Log.Info($"Deleted {deletedMangaIds} mangaIds.");
|
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 [];
|
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}": {
|
"/v2/Chapters/{MangaId}": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -3189,7 +3261,9 @@
|
|||||||
"schemas": {
|
"schemas": {
|
||||||
"ActionRecord": {
|
"ActionRecord": {
|
||||||
"required": [
|
"required": [
|
||||||
"key"
|
"action",
|
||||||
|
"key",
|
||||||
|
"performedAt"
|
||||||
],
|
],
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -3198,13 +3272,24 @@
|
|||||||
},
|
},
|
||||||
"performedAt": {
|
"performedAt": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "UTC Time when Action was performed",
|
"description": "<inheritdoc cref=\"P:API.Schema.ActionsContext.ActionRecord.PerformedAt\" />",
|
||||||
"format": "date-time"
|
"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": {
|
"key": {
|
||||||
"maxLength": 64,
|
"maxLength": 64,
|
||||||
"minLength": 16,
|
"minLength": 16,
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"description": "Unique Identifier of the DTO"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
Reference in New Issue
Block a user