diff --git a/API/Controllers/ActionsController.cs b/API/Controllers/ActionsController.cs
new file mode 100644
index 0000000..755e360
--- /dev/null
+++ b/API/Controllers/ActionsController.cs
@@ -0,0 +1,63 @@
+using API.Schema.ActionsContext;
+using Asp.Versioning;
+using Microsoft.AspNetCore.Http.HttpResults;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using static Microsoft.AspNetCore.Http.StatusCodes;
+
+namespace API.Controllers;
+
+[ApiVersion(2)]
+[ApiController]
+[Route("v{v:apiVersion}/[controller]")]
+public class ActionsController(ActionsContext context) : Controller
+{
+ ///
+ /// Returns the available Action Types () performed by Tranga
+ ///
+ /// List of performed action-types
+ /// Database error
+ [HttpGet("Types")]
+ [ProducesResponseType(Status200OK)]
+ [ProducesResponseType(Status500InternalServerError)]
+ public async Task>, InternalServerError>> GetAvailableActions()
+ {
+ if (await context.Actions.Select(a => a.Action).Distinct().ToListAsync(HttpContext.RequestAborted) is not
+ { } actions)
+ return TypedResults.InternalServerError();
+ return TypedResults.Ok(actions);
+ }
+
+ public sealed record Interval(DateTime Start, DateTime End);
+ ///
+ /// Returns performed in
+ ///
+ /// List of performed actions
+ /// Database error
+ [HttpPost("Interval")]
+ [ProducesResponseType(Status200OK)]
+ [ProducesResponseType(Status500InternalServerError)]
+ 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);
+ }
+
+ ///
+ /// Returns with type
+ ///
+ /// List of performed actions
+ /// Database error
+ [HttpGet("Type/{Type}")]
+ [ProducesResponseType(Status200OK)]
+ [ProducesResponseType(Status500InternalServerError)]
+ public async Task>, InternalServerError>> GetActionsWithType(string Type)
+ {
+ if (await context.Actions.Where(a => a.Action == Type)
+ .ToListAsync(HttpContext.RequestAborted) is not { } actions)
+ return TypedResults.InternalServerError();
+ return TypedResults.Ok(actions);
+ }
+}
\ No newline at end of file
diff --git a/API/Controllers/MangaController.cs b/API/Controllers/MangaController.cs
index 5e13853..6ea3e17 100644
--- a/API/Controllers/MangaController.cs
+++ b/API/Controllers/MangaController.cs
@@ -1,4 +1,6 @@
using API.Controllers.DTOs;
+using API.Schema.ActionsContext;
+using API.Schema.ActionsContext.Actions;
using API.Schema.MangaContext;
using API.Workers;
using API.Workers.MangaDownloadWorkers;
@@ -11,6 +13,7 @@ using Soenneker.Utils.String.NeedlemanWunsch;
using static Microsoft.AspNetCore.Http.StatusCodes;
using AltTitle = API.Controllers.DTOs.AltTitle;
using Author = API.Controllers.DTOs.Author;
+using Chapter = API.Schema.MangaContext.Chapter;
using Link = API.Controllers.DTOs.Link;
using Manga = API.Controllers.DTOs.Manga;
@@ -21,7 +24,7 @@ namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{v:apiVersion}/[controller]")]
-public class MangaController(MangaContext context) : Controller
+public class MangaController(MangaContext context, ActionsContext actionsContext) : Controller
{
///
@@ -175,12 +178,7 @@ public class MangaController(MangaContext context) : Controller
if (await manga.GetCoverImage(cache, HttpContext.RequestAborted) is not { } data)
{
- if (Tranga.GetRunningWorkers().Any(worker => worker is DownloadCoverFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId))
- {
- Response.Headers.Append("Retry-After","2");
- return TypedResults.StatusCode(Status503ServiceUnavailable);
- }
- return TypedResults.NoContent();
+ return TypedResults.NotFound("Image not in cache");
}
DateTime lastModified = data.fileInfo.LastWriteTime;
@@ -199,12 +197,17 @@ public class MangaController(MangaContext context) : Controller
/// .Key
/// Folder is going to be moved
/// or not found
+ /// Error during Database Operation
[HttpPost("{MangaId}/ChangeLibrary/{LibraryId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound, "text/plain")]
- public async Task>> ChangeLibrary(string MangaId, string LibraryId)
+ [ProducesResponseType(Status500InternalServerError, "text/plain")]
+ public async Task, InternalServerError>> ChangeLibrary(string MangaId, string LibraryId)
{
- if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
+ if (await context.Mangas
+ .Include(m => m.Library)
+ .Include(m => m.Chapters)
+ .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 TypedResults.NotFound(nameof(LibraryId));
@@ -212,9 +215,18 @@ public class MangaController(MangaContext context) : Controller
if(manga.LibraryId == library.Key)
return TypedResults.Ok();
- MoveMangaLibraryWorker moveLibrary = new(manga, library);
+ Dictionary oldPaths = manga.Chapters.Where(ch => ch.Downloaded).ToDictionary(ch => ch, ch => ch.FullArchiveFilePath);
+ manga.Library = library;
+ Dictionary newPaths = oldPaths.ToDictionary(kv => kv.Key, kv => kv.Key.FullArchiveFilePath);
+ IEnumerable workers = oldPaths.Select(kv => new MoveFileOrFolderWorker(newPaths[kv.Key]!, kv.Value!));
+ Tranga.AddWorkers(workers);
- Tranga.AddWorkers([moveLibrary]);
+ if(await context.Sync(HttpContext.RequestAborted, GetType(), "Move Manga") is { success: false } mangaContextResult)
+ return TypedResults.InternalServerError(mangaContextResult.exceptionMessage);
+
+ actionsContext.Actions.Add(new LibraryMovedActionRecord(manga, library));
+ if(await actionsContext.Sync(HttpContext.RequestAborted, GetType(), "Move Manga") is { success: false } actionsContextResult)
+ return TypedResults.InternalServerError(actionsContextResult.exceptionMessage);
return TypedResults.Ok();
}
diff --git a/API/Migrations/Actions/20251016005257_Actions.Designer.cs b/API/Migrations/Actions/20251016005257_Actions.Designer.cs
new file mode 100644
index 0000000..6644771
--- /dev/null
+++ b/API/Migrations/Actions/20251016005257_Actions.Designer.cs
@@ -0,0 +1,156 @@
+//
+using System;
+using API.Schema.ActionsContext;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations.Actions
+{
+ [DbContext(typeof(ActionsContext))]
+ [Migration("20251016005257_Actions")]
+ partial class Actions
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.9")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("API.Schema.ActionsContext.ActionRecord", b =>
+ {
+ b.Property("Key")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("Action")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.Property("PerformedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Key");
+
+ b.ToTable("Actions");
+
+ b.HasDiscriminator("Action").HasValue("ActionRecord");
+
+ b.UseTphMappingStrategy();
+ });
+
+ modelBuilder.Entity("API.Schema.ActionsContext.Actions.ChapterDownloadedActionRecord", b =>
+ {
+ b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
+
+ b.Property("ChapterId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasDiscriminator().HasValue("Chapter.Downloaded");
+ });
+
+ modelBuilder.Entity("API.Schema.ActionsContext.Actions.ChaptersRetrievedActionRecord", b =>
+ {
+ b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
+
+ b.Property("MangaId")
+ .IsRequired()
+ .ValueGeneratedOnUpdateSometimes()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasDiscriminator().HasValue("Manga.ChaptersRetrieved");
+ });
+
+ modelBuilder.Entity("API.Schema.ActionsContext.Actions.CoverDownloadedActionRecord", b =>
+ {
+ b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
+
+ b.Property("Filename")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("character varying(1024)");
+
+ b.Property("MangaId")
+ .IsRequired()
+ .ValueGeneratedOnUpdateSometimes()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasDiscriminator().HasValue("Manga.CoverDownloaded");
+ });
+
+ modelBuilder.Entity("API.Schema.ActionsContext.Actions.DataMovedActionRecord", b =>
+ {
+ b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
+
+ b.Property("From")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b.Property("To")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b.HasDiscriminator().HasValue("Tranga.DataMoved");
+ });
+
+ modelBuilder.Entity("API.Schema.ActionsContext.Actions.LibraryMovedActionRecord", b =>
+ {
+ b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
+
+ b.Property("FileLibraryId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("MangaId")
+ .IsRequired()
+ .ValueGeneratedOnUpdateSometimes()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasDiscriminator().HasValue("Manga.LibraryMoved");
+ });
+
+ modelBuilder.Entity("API.Schema.ActionsContext.Actions.MetadataUpdatedActionRecord", b =>
+ {
+ b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
+
+ b.Property("MangaId")
+ .IsRequired()
+ .ValueGeneratedOnUpdateSometimes()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("MetadataFetcher")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("character varying(1024)");
+
+ b.HasDiscriminator().HasValue("Manga.MetadataUpdated");
+ });
+
+ modelBuilder.Entity("API.Schema.ActionsContext.Actions.StartupActionRecord", b =>
+ {
+ b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
+
+ b.HasDiscriminator().HasValue("Tranga.Started");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/API/Migrations/Actions/20251016005257_Actions.cs b/API/Migrations/Actions/20251016005257_Actions.cs
new file mode 100644
index 0000000..f15825b
--- /dev/null
+++ b/API/Migrations/Actions/20251016005257_Actions.cs
@@ -0,0 +1,42 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace API.Migrations.Actions
+{
+ ///
+ public partial class Actions : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "Actions",
+ columns: table => new
+ {
+ Key = table.Column(type: "character varying(64)", maxLength: 64, nullable: false),
+ Action = table.Column(type: "character varying(128)", maxLength: 128, nullable: false),
+ PerformedAt = table.Column(type: "timestamp with time zone", nullable: false),
+ ChapterId = table.Column(type: "character varying(64)", maxLength: 64, nullable: true),
+ MangaId = table.Column(type: "character varying(64)", maxLength: 64, nullable: true),
+ Filename = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true),
+ From = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true),
+ To = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true),
+ FileLibraryId = table.Column(type: "character varying(64)", maxLength: 64, nullable: true),
+ MetadataFetcher = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Actions", x => x.Key);
+ });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "Actions");
+ }
+ }
+}
diff --git a/API/Migrations/Actions/ActionsContextModelSnapshot.cs b/API/Migrations/Actions/ActionsContextModelSnapshot.cs
new file mode 100644
index 0000000..1dcee0e
--- /dev/null
+++ b/API/Migrations/Actions/ActionsContextModelSnapshot.cs
@@ -0,0 +1,153 @@
+//
+using System;
+using API.Schema.ActionsContext;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations.Actions
+{
+ [DbContext(typeof(ActionsContext))]
+ partial class ActionsContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.9")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("API.Schema.ActionsContext.ActionRecord", b =>
+ {
+ b.Property("Key")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("Action")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.Property("PerformedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Key");
+
+ b.ToTable("Actions");
+
+ b.HasDiscriminator("Action").HasValue("ActionRecord");
+
+ b.UseTphMappingStrategy();
+ });
+
+ modelBuilder.Entity("API.Schema.ActionsContext.Actions.ChapterDownloadedActionRecord", b =>
+ {
+ b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
+
+ b.Property("ChapterId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasDiscriminator().HasValue("Chapter.Downloaded");
+ });
+
+ modelBuilder.Entity("API.Schema.ActionsContext.Actions.ChaptersRetrievedActionRecord", b =>
+ {
+ b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
+
+ b.Property("MangaId")
+ .IsRequired()
+ .ValueGeneratedOnUpdateSometimes()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasDiscriminator().HasValue("Manga.ChaptersRetrieved");
+ });
+
+ modelBuilder.Entity("API.Schema.ActionsContext.Actions.CoverDownloadedActionRecord", b =>
+ {
+ b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
+
+ b.Property("Filename")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("character varying(1024)");
+
+ b.Property("MangaId")
+ .IsRequired()
+ .ValueGeneratedOnUpdateSometimes()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasDiscriminator().HasValue("Manga.CoverDownloaded");
+ });
+
+ modelBuilder.Entity("API.Schema.ActionsContext.Actions.DataMovedActionRecord", b =>
+ {
+ b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
+
+ b.Property("From")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b.Property("To")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b.HasDiscriminator().HasValue("Tranga.DataMoved");
+ });
+
+ modelBuilder.Entity("API.Schema.ActionsContext.Actions.LibraryMovedActionRecord", b =>
+ {
+ b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
+
+ b.Property("FileLibraryId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("MangaId")
+ .IsRequired()
+ .ValueGeneratedOnUpdateSometimes()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasDiscriminator().HasValue("Manga.LibraryMoved");
+ });
+
+ modelBuilder.Entity("API.Schema.ActionsContext.Actions.MetadataUpdatedActionRecord", b =>
+ {
+ b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
+
+ b.Property("MangaId")
+ .IsRequired()
+ .ValueGeneratedOnUpdateSometimes()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("MetadataFetcher")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("character varying(1024)");
+
+ b.HasDiscriminator().HasValue("Manga.MetadataUpdated");
+ });
+
+ modelBuilder.Entity("API.Schema.ActionsContext.Actions.StartupActionRecord", b =>
+ {
+ b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
+
+ b.HasDiscriminator().HasValue("Tranga.Started");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/API/Program.cs b/API/Program.cs
index e184399..5ce35ce 100644
--- a/API/Program.cs
+++ b/API/Program.cs
@@ -1,5 +1,7 @@
using System.Reflection;
using API;
+using API.Schema.ActionsContext;
+using API.Schema.ActionsContext.Actions;
using API.Schema.LibraryContext;
using API.Schema.MangaContext;
using API.Schema.NotificationsContext;
@@ -92,6 +94,8 @@ builder.Services.AddDbContext(options =>
options.UseNpgsql(connectionStringBuilder.ConnectionString));
builder.Services.AddDbContext(options =>
options.UseNpgsql(connectionStringBuilder.ConnectionString));
+builder.Services.AddDbContext(options =>
+ options.UseNpgsql(connectionStringBuilder.ConnectionString));
builder.Services.Configure(options =>
{
@@ -180,6 +184,15 @@ try //Connect to DB and apply migrations
await context.Sync(CancellationToken.None, reason: "Startup library");
}
+
+ using (IServiceScope scope = app.Services.CreateScope())
+ {
+ ActionsContext context = scope.ServiceProvider.GetRequiredService();
+ await context.Database.MigrateAsync(CancellationToken.None);
+ context.Actions.Add(new StartupActionRecord());
+
+ await context.Sync(CancellationToken.None, reason: "Startup actions");
+ }
}
catch (Exception e)
{
diff --git a/API/Schema/ActionsContext/ActionRecord.cs b/API/Schema/ActionsContext/ActionRecord.cs
new file mode 100644
index 0000000..ac448e3
--- /dev/null
+++ b/API/Schema/ActionsContext/ActionRecord.cs
@@ -0,0 +1,19 @@
+using System.ComponentModel.DataAnnotations;
+using Microsoft.EntityFrameworkCore;
+
+namespace API.Schema.ActionsContext;
+
+[PrimaryKey("Key")]
+public abstract class ActionRecord(string action, DateTime performedAt) : Identifiable
+{
+ ///
+ /// Constant string that describes the performed Action
+ ///
+ [StringLength(128)]
+ public string Action { get; init; } = action;
+
+ ///
+ /// UTC Time when Action was performed
+ ///
+ public DateTime PerformedAt { get; init; } = performedAt;
+}
\ No newline at end of file
diff --git a/API/Schema/ActionsContext/Actions/ChapterDownloadedActionRecord.cs b/API/Schema/ActionsContext/Actions/ChapterDownloadedActionRecord.cs
new file mode 100644
index 0000000..5b2420c
--- /dev/null
+++ b/API/Schema/ActionsContext/Actions/ChapterDownloadedActionRecord.cs
@@ -0,0 +1,17 @@
+using System.ComponentModel.DataAnnotations;
+using API.Schema.MangaContext;
+
+namespace API.Schema.ActionsContext.Actions;
+
+public sealed class ChapterDownloadedActionRecord(string action, DateTime performedAt, string chapterId) : ActionRecord(action, performedAt)
+{
+ public ChapterDownloadedActionRecord(Chapter chapter) : this(ChapterDownloadedAction, DateTime.UtcNow, chapter.Key) { }
+
+ ///
+ /// Chapter that was downloaded
+ ///
+ [StringLength(64)]
+ public string ChapterId { get; init; } = chapterId;
+
+ public const string ChapterDownloadedAction = "Chapter.Downloaded";
+}
\ No newline at end of file
diff --git a/API/Schema/ActionsContext/Actions/ChaptersRetrievedActionRecord.cs b/API/Schema/ActionsContext/Actions/ChaptersRetrievedActionRecord.cs
new file mode 100644
index 0000000..0dbf854
--- /dev/null
+++ b/API/Schema/ActionsContext/Actions/ChaptersRetrievedActionRecord.cs
@@ -0,0 +1,12 @@
+using API.Schema.ActionsContext.Actions.Generic;
+using API.Schema.MangaContext;
+
+namespace API.Schema.ActionsContext.Actions;
+
+public sealed class ChaptersRetrievedActionRecord(string action, DateTime performedAt, string mangaId)
+ : ActionWithMangaRecord(action, performedAt, mangaId)
+{
+ public ChaptersRetrievedActionRecord(Manga manga) : this(ChaptersRetrievedAction, DateTime.UtcNow, manga.Key) { }
+
+ public const string ChaptersRetrievedAction = "Manga.ChaptersRetrieved";
+}
\ No newline at end of file
diff --git a/API/Schema/ActionsContext/Actions/CoverDownloadedActionRecord.cs b/API/Schema/ActionsContext/Actions/CoverDownloadedActionRecord.cs
new file mode 100644
index 0000000..3568c95
--- /dev/null
+++ b/API/Schema/ActionsContext/Actions/CoverDownloadedActionRecord.cs
@@ -0,0 +1,19 @@
+using System.ComponentModel.DataAnnotations;
+using API.Schema.ActionsContext.Actions.Generic;
+using API.Schema.MangaContext;
+
+namespace API.Schema.ActionsContext.Actions;
+
+public sealed class CoverDownloadedActionRecord(string action, DateTime performedAt, string mangaId, string filename)
+ : ActionWithMangaRecord(action, performedAt, mangaId)
+{
+ public CoverDownloadedActionRecord(Manga manga, string filename) : this(CoverDownloadedAction, DateTime.UtcNow, manga.Key, filename) { }
+
+ ///
+ /// Filename on disk
+ ///
+ [StringLength(1024)]
+ public string Filename { get; init; } = filename;
+
+ public const string CoverDownloadedAction = "Manga.CoverDownloaded";
+}
\ No newline at end of file
diff --git a/API/Schema/ActionsContext/Actions/DataMovedActionRecord.cs b/API/Schema/ActionsContext/Actions/DataMovedActionRecord.cs
new file mode 100644
index 0000000..b2618a5
--- /dev/null
+++ b/API/Schema/ActionsContext/Actions/DataMovedActionRecord.cs
@@ -0,0 +1,22 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace API.Schema.ActionsContext.Actions;
+
+public sealed class DataMovedActionRecord(string action, DateTime performedAt, string from, string to) : ActionRecord(action, performedAt)
+{
+ public DataMovedActionRecord(string from, string to) : this(DataMovedAction, DateTime.UtcNow, from, to) { }
+
+ ///
+ /// From path
+ ///
+ [StringLength(2048)]
+ public string From { get; init; } = from;
+
+ ///
+ /// To path
+ ///
+ [StringLength(2048)]
+ public string To { get; init; } = to;
+
+ public const string DataMovedAction = "Tranga.DataMoved";
+}
\ No newline at end of file
diff --git a/API/Schema/ActionsContext/Actions/Generic/ActionWithMangaRecord.cs b/API/Schema/ActionsContext/Actions/Generic/ActionWithMangaRecord.cs
new file mode 100644
index 0000000..ad569d8
--- /dev/null
+++ b/API/Schema/ActionsContext/Actions/Generic/ActionWithMangaRecord.cs
@@ -0,0 +1,15 @@
+using System.ComponentModel.DataAnnotations;
+using API.Schema.MangaContext;
+
+namespace API.Schema.ActionsContext.Actions.Generic;
+
+public abstract class ActionWithMangaRecord(string action, DateTime performedAt, string mangaId) : ActionRecord(action, performedAt)
+{
+ protected ActionWithMangaRecord(string action, DateTime performedAt, Manga manga) : this(action, performedAt, manga.Key) { }
+
+ ///
+ /// for which the cover was downloaded
+ ///
+ [StringLength(64)]
+ public string MangaId { get; init; } = mangaId;
+}
\ No newline at end of file
diff --git a/API/Schema/ActionsContext/Actions/LibraryMovedActionRecord.cs b/API/Schema/ActionsContext/Actions/LibraryMovedActionRecord.cs
new file mode 100644
index 0000000..5230021
--- /dev/null
+++ b/API/Schema/ActionsContext/Actions/LibraryMovedActionRecord.cs
@@ -0,0 +1,18 @@
+using System.ComponentModel.DataAnnotations;
+using API.Schema.ActionsContext.Actions.Generic;
+using API.Schema.MangaContext;
+
+namespace API.Schema.ActionsContext.Actions;
+
+public sealed class LibraryMovedActionRecord(string action, DateTime performedAt, string mangaId, string fileLibraryId) : ActionWithMangaRecord(action, performedAt, mangaId)
+{
+ public LibraryMovedActionRecord(Manga manga, FileLibrary library) : this(LibraryMovedAction, DateTime.UtcNow, manga.Key, library.Key) { }
+
+ ///
+ /// for which the cover was downloaded
+ ///
+ [StringLength(64)]
+ public string FileLibraryId { get; init; } = fileLibraryId;
+
+ public const string LibraryMovedAction = "Manga.LibraryMoved";
+}
\ No newline at end of file
diff --git a/API/Schema/ActionsContext/Actions/MetadataUpdatedActionRecord.cs b/API/Schema/ActionsContext/Actions/MetadataUpdatedActionRecord.cs
new file mode 100644
index 0000000..af8f488
--- /dev/null
+++ b/API/Schema/ActionsContext/Actions/MetadataUpdatedActionRecord.cs
@@ -0,0 +1,20 @@
+using System.ComponentModel.DataAnnotations;
+using API.Schema.ActionsContext.Actions.Generic;
+using API.Schema.MangaContext;
+using API.Schema.MangaContext.MetadataFetchers;
+
+namespace API.Schema.ActionsContext.Actions;
+
+public sealed class MetadataUpdatedActionRecord(string action, DateTime performedAt, string mangaId, string metadataFetcher)
+ : ActionWithMangaRecord(action, performedAt, mangaId)
+{
+ public MetadataUpdatedActionRecord(Manga manga, MetadataFetcher fetcher) : this(MetadataUpdatedAction, DateTime.UtcNow, manga.Key, fetcher.Name) { }
+
+ ///
+ /// Filename on disk
+ ///
+ [StringLength(1024)]
+ public string MetadataFetcher { get; init; } = metadataFetcher;
+
+ public const string MetadataUpdatedAction = "Manga.MetadataUpdated";
+}
\ No newline at end of file
diff --git a/API/Schema/ActionsContext/Actions/StartupActionRecord.cs b/API/Schema/ActionsContext/Actions/StartupActionRecord.cs
new file mode 100644
index 0000000..776a596
--- /dev/null
+++ b/API/Schema/ActionsContext/Actions/StartupActionRecord.cs
@@ -0,0 +1,8 @@
+namespace API.Schema.ActionsContext.Actions;
+
+public sealed class StartupActionRecord(string action, DateTime performedAt) : ActionRecord(action, performedAt)
+{
+ public StartupActionRecord() : this(StartupAction, DateTime.UtcNow) { }
+
+ public const string StartupAction = "Tranga.Started";
+}
\ No newline at end of file
diff --git a/API/Schema/ActionsContext/ActionsContext.cs b/API/Schema/ActionsContext/ActionsContext.cs
new file mode 100644
index 0000000..5456d9d
--- /dev/null
+++ b/API/Schema/ActionsContext/ActionsContext.cs
@@ -0,0 +1,22 @@
+using API.Schema.ActionsContext.Actions;
+using Microsoft.EntityFrameworkCore;
+
+namespace API.Schema.ActionsContext;
+
+public class ActionsContext(DbContextOptions options) : TrangaBaseContext(options)
+{
+ public DbSet Actions { get; set; }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity()
+ .HasDiscriminator(a => a.Action)
+ .HasValue(ChapterDownloadedActionRecord.ChapterDownloadedAction)
+ .HasValue(CoverDownloadedActionRecord.CoverDownloadedAction)
+ .HasValue(ChaptersRetrievedActionRecord.ChaptersRetrievedAction)
+ .HasValue(MetadataUpdatedActionRecord.MetadataUpdatedAction)
+ .HasValue(DataMovedActionRecord.DataMovedAction)
+ .HasValue(LibraryMovedActionRecord.LibraryMovedAction)
+ .HasValue(StartupActionRecord.StartupAction);
+ }
+}
\ No newline at end of file
diff --git a/API/Tranga.cs b/API/Tranga.cs
index 2506646..4beaa65 100644
--- a/API/Tranga.cs
+++ b/API/Tranga.cs
@@ -2,10 +2,8 @@
using System.Diagnostics.CodeAnalysis;
using API.MangaConnectors;
using API.MangaDownloadClients;
-using API.Schema.LibraryContext;
using API.Schema.MangaContext;
using API.Schema.MangaContext.MetadataFetchers;
-using API.Schema.NotificationsContext;
using API.Workers;
using API.Workers.MangaDownloadWorkers;
using API.Workers.PeriodicWorkers;
@@ -146,21 +144,15 @@ public static class Tranga
Log.Warn($"{worker}: Max worker concurrency reached ({Settings.MaxConcurrentWorkers})! Waiting {Settings.WorkCycleTimeoutMs}ms...");
Thread.Sleep(Settings.WorkCycleTimeoutMs);
}
-
- if (worker is BaseWorkerWithContext mangaContextWorker)
+
+ if (worker is BaseWorkerWithContexts withContexts)
{
- mangaContextWorker.SetScope(ServiceProvider.CreateScope());
- RunningWorkers.TryAdd(mangaContextWorker, mangaContextWorker.DoWork(afterWorkCallback));
- }else if (worker is BaseWorkerWithContext notificationContextWorker)
+ RunningWorkers.TryAdd(withContexts, withContexts.DoWork(ServiceProvider.CreateScope(), afterWorkCallback));
+ }
+ else
{
- notificationContextWorker.SetScope(ServiceProvider.CreateScope());
- RunningWorkers.TryAdd(notificationContextWorker, notificationContextWorker.DoWork(afterWorkCallback));
- }else if (worker is BaseWorkerWithContext libraryContextWorker)
- {
- libraryContextWorker.SetScope(ServiceProvider.CreateScope());
- RunningWorkers.TryAdd(libraryContextWorker, libraryContextWorker.DoWork(afterWorkCallback));
- }else
RunningWorkers.TryAdd(worker, worker.DoWork(afterWorkCallback));
+ }
}
private static Action DefaultAfterWork(BaseWorker worker, Action? callback = null) => () =>
diff --git a/API/Workers/BaseWorkerWithContext.cs b/API/Workers/BaseWorkerWithContext.cs
deleted file mode 100644
index 9dc233b..0000000
--- a/API/Workers/BaseWorkerWithContext.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using System.Configuration;
-using Microsoft.EntityFrameworkCore;
-
-namespace API.Workers;
-
-public abstract class BaseWorkerWithContext(IEnumerable? dependsOn = null) : BaseWorker(dependsOn) where T : DbContext
-{
- protected T DbContext = null!;
- private IServiceScope? _scope;
-
- public void SetScope(IServiceScope scope)
- {
- this._scope = scope;
- this.DbContext = scope.ServiceProvider.GetRequiredService();
- }
-
- /// Scope has not been set.
- public new Task DoWork()
- {
- if (DbContext is null)
- throw new ConfigurationErrorsException("Scope has not been set.");
- return base.DoWork();
- }
-}
\ No newline at end of file
diff --git a/API/Workers/BaseWorkerWithContexts.cs b/API/Workers/BaseWorkerWithContexts.cs
new file mode 100644
index 0000000..1f12693
--- /dev/null
+++ b/API/Workers/BaseWorkerWithContexts.cs
@@ -0,0 +1,28 @@
+using Microsoft.EntityFrameworkCore;
+
+namespace API.Workers;
+
+public abstract class BaseWorkerWithContexts(IEnumerable? dependsOn = null) : BaseWorker(dependsOn)
+{
+ ///
+ /// Returns the context of requested type
+ ///
+ ///
+ /// Type of
+ /// Context in scope
+ /// Scope not set
+ protected T GetContext(IServiceScope scope) where T : DbContext
+ {
+ if (scope is not { } serviceScope)
+ throw new Exception("Scope not set!");
+ return serviceScope.ServiceProvider.GetRequiredService();
+ }
+
+ protected abstract void SetContexts(IServiceScope serviceScope);
+
+ public new Task DoWork(IServiceScope serviceScope, Action? callback = null)
+ {
+ SetContexts(serviceScope);
+ return base.DoWork(callback);
+ }
+}
\ No newline at end of file
diff --git a/API/Workers/MangaDownloadWorkers/DownloadChapterFromMangaconnectorWorker.cs b/API/Workers/MangaDownloadWorkers/DownloadChapterFromMangaconnectorWorker.cs
index 554e382..ad98dcf 100644
--- a/API/Workers/MangaDownloadWorkers/DownloadChapterFromMangaconnectorWorker.cs
+++ b/API/Workers/MangaDownloadWorkers/DownloadChapterFromMangaconnectorWorker.cs
@@ -1,8 +1,10 @@
+using System.Diagnostics.CodeAnalysis;
using System.IO.Compression;
using System.Runtime.InteropServices;
using System.Text;
using API.MangaConnectors;
-using API.MangaDownloadClients;
+using API.Schema.ActionsContext;
+using API.Schema.ActionsContext.Actions;
using API.Schema.MangaContext;
using API.Workers.PeriodicWorkers;
using Microsoft.EntityFrameworkCore;
@@ -21,14 +23,26 @@ namespace API.Workers.MangaDownloadWorkers;
///
///
public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId chId, IEnumerable? dependsOn = null)
- : BaseWorkerWithContext(dependsOn)
+ : BaseWorkerWithContexts(dependsOn)
{
private readonly string _mangaConnectorIdId = chId.Key;
+
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ private MangaContext MangaContext = null!;
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ private ActionsContext ActionsContext = null!;
+
+ protected override void SetContexts(IServiceScope serviceScope)
+ {
+ MangaContext = GetContext(serviceScope);
+ ActionsContext = GetContext(serviceScope);
+ }
+
protected override async Task DoWorkInternal()
{
Log.Debug($"Downloading chapter for MangaConnectorId {_mangaConnectorIdId}...");
// Getting MangaConnector info
- if (await DbContext.MangaConnectorToChapter
+ if (await MangaContext.MangaConnectorToChapter
.Include(id => id.Obj)
.ThenInclude(c => c.ParentManga)
.ThenInclude(m => m.Library)
@@ -39,7 +53,7 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId c
}
// Check if Chapter already exists...
- if (await mangaConnectorId.Obj.CheckDownloaded(DbContext, CancellationToken))
+ if (await mangaConnectorId.Obj.CheckDownloaded(MangaContext, CancellationToken))
{
Log.Warn("Chapter already exists!");
return [];
@@ -66,7 +80,7 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId c
{
Log.Info($"No imageUrls for chapter {chapter}");
mangaConnectorId.UseForDownload = false; // Do not try to download from this again
- if(await DbContext.Sync(CancellationToken, GetType(), "Disable Id") is { success: false } result)
+ if(await MangaContext.Sync(CancellationToken, GetType(), "Disable Id") is { success: false } result)
Log.Error(result.exceptionMessage);
return [];
}
@@ -121,7 +135,7 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId c
await CopyCoverFromCacheToDownloadLocation(chapter.ParentManga);
Log.Debug($"Loading collections {chapter}");
- foreach (CollectionEntry collectionEntry in DbContext.Entry(chapter.ParentManga).Collections)
+ foreach (CollectionEntry collectionEntry in MangaContext.Entry(chapter.ParentManga).Collections)
await collectionEntry.LoadAsync(CancellationToken);
if (File.Exists(saveArchiveFilePath))
@@ -173,11 +187,15 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId c
chapter.Downloaded = true;
chapter.FileName = new FileInfo(saveArchiveFilePath).Name;
- if(await DbContext.Sync(CancellationToken, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false } e)
- Log.Error($"Failed to save database changes: {e.exceptionMessage}");
+ if(await MangaContext.Sync(CancellationToken, GetType(), "Downloading complete") is { success: false } chapterContextException)
+ Log.Error($"Failed to save database changes: {chapterContextException.exceptionMessage}");
Log.Debug($"Downloaded chapter {chapter}.");
+ ActionsContext.Actions.Add(new ChapterDownloadedActionRecord(chapter));
+ if(await ActionsContext.Sync(CancellationToken, GetType(), "Download complete") is { success: false } actionsContextException)
+ Log.Error($"Failed to save database changes: {actionsContextException.exceptionMessage}");
+
bool refreshLibrary = await CheckLibraryRefresh();
if(refreshLibrary)
Log.Info($"Condition {Tranga.Settings.LibraryRefreshSetting} met.");
@@ -188,12 +206,12 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId c
private async Task CheckLibraryRefresh() => Tranga.Settings.LibraryRefreshSetting switch
{
LibraryRefreshSetting.AfterAllFinished => await AllDownloadsFinished(),
- LibraryRefreshSetting.AfterMangaFinished => await DbContext.MangaConnectorToChapter.Include(chId => chId.Obj).Where(chId => chId.UseForDownload).AllAsync(chId => chId.Obj.Downloaded, CancellationToken),
+ LibraryRefreshSetting.AfterMangaFinished => await MangaContext.MangaConnectorToChapter.Include(chId => chId.Obj).Where(chId => chId.UseForDownload).AllAsync(chId => chId.Obj.Downloaded, CancellationToken),
LibraryRefreshSetting.AfterEveryChapter => true,
LibraryRefreshSetting.WhileDownloading => await AllDownloadsFinished() || DateTime.UtcNow.Subtract(RefreshLibrariesWorker.LastRefresh).TotalMinutes > Tranga.Settings.RefreshLibraryWhileDownloadingEveryMinutes,
_ => true
};
- private async Task AllDownloadsFinished() => (await StartNewChapterDownloadsWorker.GetMissingChapters(DbContext, CancellationToken)).Count == 0;
+ private async Task AllDownloadsFinished() => (await StartNewChapterDownloadsWorker.GetMissingChapters(MangaContext, CancellationToken)).Count == 0;
private async Task ProcessImage(Stream imageStream, CancellationToken? cancellationToken = null)
{
@@ -244,7 +262,7 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId c
{
Log.Debug($"Copying cover for {manga}");
- manga = await DbContext.MangaIncludeAll().FirstAsync(m => m.Key == manga.Key, CancellationToken);
+ manga = await MangaContext.MangaIncludeAll().FirstAsync(m => m.Key == manga.Key, CancellationToken);
string publicationFolder;
try
{
@@ -278,7 +296,7 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId c
coverFileNameInCache = mangaConnector.SaveCoverImageToCache(mangaConnectorId);
manga.CoverFileNameInCache = coverFileNameInCache;
- if (await DbContext.Sync(CancellationToken, reason: "Update cover filename") is { success: false } result)
+ if (await MangaContext.Sync(CancellationToken, reason: "Update cover filename") is { success: false } result)
Log.Error($"Couldn't update cover filename {result.exceptionMessage}");
}
if (coverFileNameInCache is null)
diff --git a/API/Workers/MangaDownloadWorkers/DownloadCoverFromMangaconnectorWorker.cs b/API/Workers/MangaDownloadWorkers/DownloadCoverFromMangaconnectorWorker.cs
index 5b5d864..718bffc 100644
--- a/API/Workers/MangaDownloadWorkers/DownloadCoverFromMangaconnectorWorker.cs
+++ b/API/Workers/MangaDownloadWorkers/DownloadCoverFromMangaconnectorWorker.cs
@@ -1,4 +1,7 @@
+using System.Diagnostics.CodeAnalysis;
using API.MangaConnectors;
+using API.Schema.ActionsContext;
+using API.Schema.ActionsContext.Actions;
using API.Schema.MangaContext;
using Microsoft.EntityFrameworkCore;
@@ -8,16 +11,28 @@ namespace API.Workers.MangaDownloadWorkers;
/// Downloads the cover for Manga from Mangaconnector
///
public class DownloadCoverFromMangaconnectorWorker(MangaConnectorId mcId, IEnumerable? dependsOn = null)
- : BaseWorkerWithContext(dependsOn)
+ : BaseWorkerWithContexts(dependsOn)
{
- internal readonly string MangaConnectorIdId = mcId.Key;
+ private readonly string _mangaConnectorIdId = mcId.Key;
+
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ private MangaContext MangaContext = null!;
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ private ActionsContext ActionsContext = null!;
+
+ protected override void SetContexts(IServiceScope serviceScope)
+ {
+ MangaContext = GetContext(serviceScope);
+ ActionsContext = GetContext(serviceScope);
+ }
+
protected override async Task DoWorkInternal()
{
- Log.Debug($"Getting Cover for MangaConnectorId {MangaConnectorIdId}...");
+ Log.Debug($"Getting Cover for MangaConnectorId {_mangaConnectorIdId}...");
// Getting MangaConnector info
- if (await DbContext.MangaConnectorToManga
+ if (await MangaContext.MangaConnectorToManga
.Include(id => id.Obj)
- .FirstOrDefaultAsync(c => c.Key == MangaConnectorIdId, CancellationToken) is not { } mangaConnectorId)
+ .FirstOrDefaultAsync(c => c.Key == _mangaConnectorIdId, CancellationToken) is not { } mangaConnectorId)
{
Log.Error("Could not get MangaConnectorId.");
return []; //TODO Exception?
@@ -35,13 +50,17 @@ public class DownloadCoverFromMangaconnectorWorker(MangaConnectorId mcId,
Log.Error($"Could not get Cover for MangaConnectorId {mangaConnectorId}.");
return [];
}
- DbContext.Entry(mangaConnectorId.Obj).Property(m => m.CoverFileNameInCache).CurrentValue = coverFileName;
+ MangaContext.Entry(mangaConnectorId.Obj).Property(m => m.CoverFileNameInCache).CurrentValue = coverFileName;
- if(await DbContext.Sync(CancellationToken, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false } e)
- Log.Error($"Failed to save database changes: {e.exceptionMessage}");
+ 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)
+ Log.Error($"Failed to save database changes: {actionsContextException.exceptionMessage}");
return [];
}
- public override string ToString() => $"{base.ToString()} {MangaConnectorIdId}";
+ public override string ToString() => $"{base.ToString()} {_mangaConnectorIdId}";
}
\ No newline at end of file
diff --git a/API/Workers/MangaDownloadWorkers/RetrieveMangaChaptersFromMangaconnectorWorker.cs b/API/Workers/MangaDownloadWorkers/RetrieveMangaChaptersFromMangaconnectorWorker.cs
index c481538..8985b4e 100644
--- a/API/Workers/MangaDownloadWorkers/RetrieveMangaChaptersFromMangaconnectorWorker.cs
+++ b/API/Workers/MangaDownloadWorkers/RetrieveMangaChaptersFromMangaconnectorWorker.cs
@@ -1,4 +1,7 @@
+using System.Diagnostics.CodeAnalysis;
using API.MangaConnectors;
+using API.Schema.ActionsContext;
+using API.Schema.ActionsContext.Actions;
using API.Schema.MangaContext;
using Microsoft.EntityFrameworkCore;
@@ -11,18 +14,30 @@ namespace API.Workers.MangaDownloadWorkers;
///
///
public class RetrieveMangaChaptersFromMangaconnectorWorker(MangaConnectorId mcId, string language, IEnumerable? dependsOn = null)
- : BaseWorkerWithContext(dependsOn)
+ : BaseWorkerWithContexts(dependsOn)
{
- internal readonly string MangaConnectorIdId = mcId.Key;
+ private readonly string _mangaConnectorIdId = mcId.Key;
+
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ private MangaContext MangaContext = null!;
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ private ActionsContext ActionsContext = null!;
+
+ protected override void SetContexts(IServiceScope serviceScope)
+ {
+ MangaContext = GetContext(serviceScope);
+ ActionsContext = GetContext(serviceScope);
+ }
+
protected override async Task DoWorkInternal()
{
- Log.Debug($"Getting Chapters for MangaConnectorId {MangaConnectorIdId}...");
+ Log.Debug($"Getting Chapters for MangaConnectorId {_mangaConnectorIdId}...");
// Getting MangaConnector info
- if (await DbContext.MangaConnectorToManga
+ if (await MangaContext.MangaConnectorToManga
.Include(id => id.Obj)
.ThenInclude(m => m.Chapters)
.ThenInclude(ch => ch.MangaConnectorIds)
- .FirstOrDefaultAsync(c => c.Key == MangaConnectorIdId, CancellationToken) is not { } mangaConnectorId)
+ .FirstOrDefaultAsync(c => c.Key == _mangaConnectorIdId, CancellationToken) is not { } mangaConnectorId)
{
Log.Error("Could not get MangaConnectorId.");
return []; //TODO Exception?
@@ -62,7 +77,7 @@ public class RetrieveMangaChaptersFromMangaconnectorWorker(MangaConnectorId $"{base.ToString()} {MangaConnectorIdId}";
+ public override string ToString() => $"{base.ToString()} {_mangaConnectorIdId}";
}
\ No newline at end of file
diff --git a/API/Workers/MoveFileOrFolderWorker.cs b/API/Workers/MoveFileOrFolderWorker.cs
index acd8ff8..1d9b656 100644
--- a/API/Workers/MoveFileOrFolderWorker.cs
+++ b/API/Workers/MoveFileOrFolderWorker.cs
@@ -1,12 +1,24 @@
+using System.Diagnostics.CodeAnalysis;
+using API.Schema.ActionsContext;
+using API.Schema.ActionsContext.Actions;
+
namespace API.Workers;
public class MoveFileOrFolderWorker(string toLocation, string fromLocation, IEnumerable? dependsOn = null)
- : BaseWorker(dependsOn)
+ : BaseWorkerWithContexts(dependsOn)
{
public readonly string FromLocation = fromLocation;
public readonly string ToLocation = toLocation;
+
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ private ActionsContext ActionsContext = null!;
- protected override Task DoWorkInternal()
+ protected override void SetContexts(IServiceScope serviceScope)
+ {
+ ActionsContext = GetContext(serviceScope);
+ }
+
+ protected override async Task DoWorkInternal()
{
try
{
@@ -14,13 +26,13 @@ public class MoveFileOrFolderWorker(string toLocation, string fromLocation, IEnu
if (!fi.Exists)
{
Log.Error($"File does not exist at {FromLocation}");
- return new Task(() => []);
+ return [];
}
if (File.Exists(ToLocation))//Do not override existing
{
Log.Error($"File already exists at {ToLocation}");
- return new Task(() => []);
+ return [];
}
if(fi.Attributes.HasFlag(FileAttributes.Directory))
MoveDirectory(fi, ToLocation);
@@ -32,7 +44,11 @@ public class MoveFileOrFolderWorker(string toLocation, string fromLocation, IEnu
Log.Error(e);
}
- return new Task(() => []);
+ ActionsContext.Actions.Add(new DataMovedActionRecord(FromLocation, ToLocation));
+ if(await ActionsContext.Sync(CancellationToken, GetType(), "Library Moved") is { success: false } actionsContextException)
+ Log.Error($"Failed to save database changes: {actionsContextException.exceptionMessage}");
+
+ return [];
}
private void MoveDirectory(FileInfo from, string toLocation)
diff --git a/API/Workers/MoveMangaLibraryWorker.cs b/API/Workers/MoveMangaLibraryWorker.cs
deleted file mode 100644
index bab52d0..0000000
--- a/API/Workers/MoveMangaLibraryWorker.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using API.Schema.MangaContext;
-using Microsoft.EntityFrameworkCore;
-
-namespace API.Workers;
-
-///
-/// Moves a Manga to a different Library
-///
-public class MoveMangaLibraryWorker(Manga manga, FileLibrary toLibrary, IEnumerable? dependsOn = null)
- : BaseWorkerWithContext(dependsOn)
-{
- internal readonly string MangaId = manga.Key;
- internal readonly string LibraryId = toLibrary.Key;
- protected override async Task DoWorkInternal()
- {
- Log.Debug("Moving Manga...");
- // Get Manga (with and Library)
- if (await DbContext.Mangas
- .Include(m => m.Library)
- .Include(m => m.Chapters)
- .FirstOrDefaultAsync(m => m.Key == MangaId, CancellationToken) is not { } manga)
- {
- Log.Error("Could not find Manga.");
- return [];
- }
-
- // Get new Library
- if (await DbContext.FileLibraries.FirstOrDefaultAsync(l => l.Key == LibraryId, CancellationToken) is not { } toLibrary)
- {
- Log.Error("Could not find Library.");
- return [];
- }
-
- // Save old Path (to later move chapters)
- Dictionary oldPath = manga.Chapters.Where(c => c.FileName != null).ToDictionary(c => c.Key, c => c.FullArchiveFilePath)!;
- // Set new Path
- manga.Library = toLibrary;
-
- if (await DbContext.Sync(CancellationToken, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false })
- return [];
-
- // Create Jobs to move chapters from old to new Path
- return oldPath.Select(kv => new MoveFileOrFolderWorker(manga.Chapters.First(ch => ch.Key == kv.Key).FullArchiveFilePath!, kv.Value)).ToArray();
- }
-
- public override string ToString() => $"{base.ToString()} {MangaId} {LibraryId}";
-}
\ No newline at end of file
diff --git a/API/Workers/PeriodicWorkers/CheckForNewChaptersWorker.cs b/API/Workers/PeriodicWorkers/CheckForNewChaptersWorker.cs
index e7b81a6..d2093b6 100644
--- a/API/Workers/PeriodicWorkers/CheckForNewChaptersWorker.cs
+++ b/API/Workers/PeriodicWorkers/CheckForNewChaptersWorker.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using API.Schema.MangaContext;
using API.Workers.MangaDownloadWorkers;
using Microsoft.EntityFrameworkCore;
@@ -8,15 +9,23 @@ namespace API.Workers.PeriodicWorkers;
/// Creates Jobs to update available Chapters for all Manga that are marked for Download
///
public class CheckForNewChaptersWorker(TimeSpan? interval = null, IEnumerable? dependsOn = null)
- : BaseWorkerWithContext(dependsOn), IPeriodic
+ : BaseWorkerWithContexts(dependsOn), IPeriodic
{
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
public TimeSpan Interval { get; set; } = interval??TimeSpan.FromMinutes(60);
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ private MangaContext MangaContext = null!;
+
+ protected override void SetContexts(IServiceScope serviceScope)
+ {
+ MangaContext = GetContext(serviceScope);
+ }
+
protected override async Task DoWorkInternal()
{
Log.Debug("Checking for new chapters...");
- List> connectorIdsManga = await DbContext.MangaConnectorToManga
+ List> connectorIdsManga = await MangaContext.MangaConnectorToManga
.Include(id => id.Obj)
.Where(id => id.UseForDownload)
.ToListAsync(CancellationToken);
@@ -27,5 +36,4 @@ public class CheckForNewChaptersWorker(TimeSpan? interval = null, IEnumerable? dependsOn = null)
- : BaseWorkerWithContext(dependsOn), IPeriodic
+ : BaseWorkerWithContexts(dependsOn), IPeriodic
{
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(24);
+
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ private MangaContext MangaContext = null!;
+
+ protected override void SetContexts(IServiceScope serviceScope)
+ {
+ MangaContext = GetContext(serviceScope);
+ }
protected override async Task DoWorkInternal()
{
Log.Info("Removing stale files...");
- string[] usedFiles = await DbContext.Mangas.Where(m => m.CoverFileNameInCache != null).Select(m => m.CoverFileNameInCache!).ToArrayAsync(CancellationToken);
+ string[] usedFiles = await MangaContext.Mangas.Where(m => m.CoverFileNameInCache != null).Select(m => m.CoverFileNameInCache!).ToArrayAsync(CancellationToken);
CleanupImageCache(usedFiles, TrangaSettings.CoverImageCacheOriginal);
CleanupImageCache(usedFiles, TrangaSettings.CoverImageCacheLarge);
CleanupImageCache(usedFiles, TrangaSettings.CoverImageCacheMedium);
diff --git a/API/Workers/PeriodicWorkers/MaintenanceWorkers/CleanupMangaconnectorIdsWithoutConnector.cs b/API/Workers/PeriodicWorkers/MaintenanceWorkers/CleanupMangaconnectorIdsWithoutConnector.cs
index c1da9f2..5b27b5d 100644
--- a/API/Workers/PeriodicWorkers/MaintenanceWorkers/CleanupMangaconnectorIdsWithoutConnector.cs
+++ b/API/Workers/PeriodicWorkers/MaintenanceWorkers/CleanupMangaconnectorIdsWithoutConnector.cs
@@ -1,29 +1,37 @@
-using System.Text;
+using System.Diagnostics.CodeAnalysis;
using API.Schema.MangaContext;
using Microsoft.EntityFrameworkCore;
namespace API.Workers.PeriodicWorkers.MaintenanceWorkers;
-public class CleanupMangaconnectorIdsWithoutConnector : BaseWorkerWithContext
+public class CleanupMangaconnectorIdsWithoutConnector : BaseWorkerWithContexts
{
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ private MangaContext MangaContext = null!;
+
+ protected override void SetContexts(IServiceScope serviceScope)
+ {
+ MangaContext = GetContext(serviceScope);
+ }
+
protected override async Task DoWorkInternal()
{
Log.Info("Cleaning up old connector-data");
string[] connectorNames = Tranga.MangaConnectors.Select(c => c.Name).ToArray();
- int deletedChapterIds = await DbContext.MangaConnectorToChapter.Where(chId => connectorNames.All(n => n != chId.MangaConnectorName)).ExecuteDeleteAsync(CancellationToken);
+ int deletedChapterIds = await MangaContext.MangaConnectorToChapter.Where(chId => connectorNames.All(n => n != chId.MangaConnectorName)).ExecuteDeleteAsync(CancellationToken);
Log.Info($"Deleted {deletedChapterIds} chapterIds.");
// Manga without Connector get printed to file, to not lose data...
- if (await DbContext.MangaConnectorToManga.Include(id => id.Obj) .Where(mcId => connectorNames.All(name => name != mcId.MangaConnectorName)).ToListAsync() is { Count: > 0 } list)
+ if (await MangaContext.MangaConnectorToManga.Include(id => id.Obj) .Where(mcId => connectorNames.All(name => name != mcId.MangaConnectorName)).ToListAsync() is { Count: > 0 } list)
{
string filePath = Path.Join(TrangaSettings.WorkingDirectory, $"deletedManga-{DateTime.UtcNow.Ticks}.txt");
Log.Debug($"Writing deleted manga to {filePath}.");
await File.WriteAllLinesAsync(filePath, list.Select(id => string.Join('-', id.MangaConnectorName, id.IdOnConnectorSite, id.Obj.Name, id.WebsiteUrl)), CancellationToken);
}
- int deletedMangaIds = await DbContext.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.");
- await DbContext.SaveChangesAsync(CancellationToken);
+ await MangaContext.SaveChangesAsync(CancellationToken);
return [];
}
}
\ No newline at end of file
diff --git a/API/Workers/PeriodicWorkers/MaintenanceWorkers/RemoveOldNotificationsWorker.cs b/API/Workers/PeriodicWorkers/MaintenanceWorkers/RemoveOldNotificationsWorker.cs
index 6d20bee..390f035 100644
--- a/API/Workers/PeriodicWorkers/MaintenanceWorkers/RemoveOldNotificationsWorker.cs
+++ b/API/Workers/PeriodicWorkers/MaintenanceWorkers/RemoveOldNotificationsWorker.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using API.Schema.NotificationsContext;
using Microsoft.EntityFrameworkCore;
@@ -7,18 +8,26 @@ namespace API.Workers.PeriodicWorkers.MaintenanceWorkers;
/// Removes sent notifications from database
///
public class RemoveOldNotificationsWorker(TimeSpan? interval = null, IEnumerable? dependsOn = null)
- : BaseWorkerWithContext(dependsOn), IPeriodic
+ : BaseWorkerWithContexts(dependsOn), IPeriodic
{
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(1);
+
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ private NotificationsContext NotificationsContext = null!;
+
+ protected override void SetContexts(IServiceScope serviceScope)
+ {
+ NotificationsContext = GetContext(serviceScope);
+ }
protected override async Task DoWorkInternal()
{
Log.Debug("Removing old notifications...");
- int removed = await DbContext.Notifications.Where(n => n.IsSent).ExecuteDeleteAsync(CancellationToken);
+ int removed = await NotificationsContext.Notifications.Where(n => n.IsSent).ExecuteDeleteAsync(CancellationToken);
Log.Debug($"Removed {removed} old notifications...");
- if(await DbContext.Sync(CancellationToken, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false } e)
+ if(await NotificationsContext.Sync(CancellationToken, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false } e)
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
return [];
diff --git a/API/Workers/PeriodicWorkers/SendNotificationsWorker.cs b/API/Workers/PeriodicWorkers/SendNotificationsWorker.cs
index 9fcb405..0476904 100644
--- a/API/Workers/PeriodicWorkers/SendNotificationsWorker.cs
+++ b/API/Workers/PeriodicWorkers/SendNotificationsWorker.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using API.Schema.NotificationsContext;
using API.Schema.NotificationsContext.NotificationConnectors;
using Microsoft.EntityFrameworkCore;
@@ -10,15 +11,24 @@ namespace API.Workers.PeriodicWorkers;
///
///
public class SendNotificationsWorker(TimeSpan? interval = null, IEnumerable? dependsOn = null)
- : BaseWorkerWithContext(dependsOn), IPeriodic
+ : BaseWorkerWithContexts(dependsOn), IPeriodic
{
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
public TimeSpan Interval { get; set; } = interval??TimeSpan.FromMinutes(1);
+
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ private NotificationsContext NotificationsContext = null!;
+
+ protected override void SetContexts(IServiceScope serviceScope)
+ {
+ NotificationsContext = GetContext(serviceScope);
+ }
+
protected override async Task DoWorkInternal()
{
Log.Debug("Sending notifications...");
- List connectors = await DbContext.NotificationConnectors.ToListAsync(CancellationToken);
- List unsentNotifications = await DbContext.Notifications.Where(n => n.IsSent == false).ToListAsync(CancellationToken);
+ List connectors = await NotificationsContext.NotificationConnectors.ToListAsync(CancellationToken);
+ List unsentNotifications = await NotificationsContext.Notifications.Where(n => n.IsSent == false).ToListAsync(CancellationToken);
Log.Debug($"Sending {unsentNotifications.Count} notifications to {connectors.Count} connectors...");
@@ -27,16 +37,15 @@ public class SendNotificationsWorker(TimeSpan? interval = null, IEnumerable
{
connector.SendNotification(notification.Title, notification.Message);
- DbContext.Entry(notification).Property(n => n.IsSent).CurrentValue = true;
+ NotificationsContext.Entry(notification).Property(n => n.IsSent).CurrentValue = true;
});
});
Log.Debug("Notifications sent.");
- if(await DbContext.Sync(CancellationToken, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false } e)
+ if(await NotificationsContext.Sync(CancellationToken, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false } e)
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
return [];
}
-
}
\ No newline at end of file
diff --git a/API/Workers/PeriodicWorkers/StartNewChapterDownloadsWorker.cs b/API/Workers/PeriodicWorkers/StartNewChapterDownloadsWorker.cs
index 3aef412..8b729db 100644
--- a/API/Workers/PeriodicWorkers/StartNewChapterDownloadsWorker.cs
+++ b/API/Workers/PeriodicWorkers/StartNewChapterDownloadsWorker.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using API.Schema.MangaContext;
using API.Workers.MangaDownloadWorkers;
using Microsoft.EntityFrameworkCore;
@@ -8,17 +9,26 @@ namespace API.Workers.PeriodicWorkers;
/// Create new Workers for Chapters on Manga marked for Download, that havent been downloaded yet.
///
public class StartNewChapterDownloadsWorker(TimeSpan? interval = null, IEnumerable? dependsOn = null)
- : BaseWorkerWithContext(dependsOn), IPeriodic
+ : BaseWorkerWithContexts(dependsOn), IPeriodic
{
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromMinutes(1);
+
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ private MangaContext MangaContext = null!;
+
+ protected override void SetContexts(IServiceScope serviceScope)
+ {
+ MangaContext = GetContext(serviceScope);
+ }
+
protected override async Task DoWorkInternal()
{
Log.Debug("Checking for missing chapters...");
// Get missing chapters
- List> missingChapters = await GetMissingChapters(DbContext, CancellationToken);
+ List> missingChapters = await GetMissingChapters(MangaContext, CancellationToken);
Log.Debug($"Found {missingChapters.Count} missing downloads.");
diff --git a/API/Workers/PeriodicWorkers/UpdateChaptersDownloadedWorker.cs b/API/Workers/PeriodicWorkers/UpdateChaptersDownloadedWorker.cs
index dd8725a..628eaea 100644
--- a/API/Workers/PeriodicWorkers/UpdateChaptersDownloadedWorker.cs
+++ b/API/Workers/PeriodicWorkers/UpdateChaptersDownloadedWorker.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using API.Schema.MangaContext;
using Microsoft.EntityFrameworkCore;
@@ -7,20 +8,29 @@ namespace API.Workers.PeriodicWorkers;
/// Updates the database to reflect changes made on disk
///
public class UpdateChaptersDownloadedWorker(TimeSpan? interval = null, IEnumerable? dependsOn = null)
- : BaseWorkerWithContext(dependsOn), IPeriodic
+ : BaseWorkerWithContexts(dependsOn), IPeriodic
{
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
public TimeSpan Interval { get; set; } = interval??TimeSpan.FromDays(1);
+
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ private MangaContext MangaContext = null!;
+
+ protected override void SetContexts(IServiceScope serviceScope)
+ {
+ MangaContext = GetContext(serviceScope);
+ }
+
protected override async Task DoWorkInternal()
{
Log.Debug("Checking chapter files...");
- List chapters = await DbContext.Chapters.ToListAsync(CancellationToken);
+ List chapters = await MangaContext.Chapters.ToListAsync(CancellationToken);
Log.Debug($"Checking {chapters.Count} chapters...");
foreach (Chapter chapter in chapters)
{
try
{
- chapter.Downloaded = await chapter.CheckDownloaded(DbContext, CancellationToken);
+ chapter.Downloaded = await chapter.CheckDownloaded(MangaContext, CancellationToken);
}
catch (Exception exception)
{
@@ -28,7 +38,7 @@ public class UpdateChaptersDownloadedWorker(TimeSpan? interval = null, IEnumerab
}
}
- if(await DbContext.Sync(CancellationToken, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false } e)
+ if(await MangaContext.Sync(CancellationToken, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false } e)
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
return [];
diff --git a/API/Workers/PeriodicWorkers/UpdateCoversWorker.cs b/API/Workers/PeriodicWorkers/UpdateCoversWorker.cs
index 0743969..2b85f81 100644
--- a/API/Workers/PeriodicWorkers/UpdateCoversWorker.cs
+++ b/API/Workers/PeriodicWorkers/UpdateCoversWorker.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using API.Schema.MangaContext;
using API.Workers.MangaDownloadWorkers;
using Microsoft.EntityFrameworkCore;
@@ -10,15 +11,22 @@ namespace API.Workers.PeriodicWorkers;
///
///
public class UpdateCoversWorker(TimeSpan? interval = null, IEnumerable? dependsOn = null)
- : BaseWorkerWithContext(dependsOn), IPeriodic
+ : BaseWorkerWithContexts(dependsOn), IPeriodic
{
-
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(6);
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ private MangaContext MangaContext = null!;
+
+ protected override void SetContexts(IServiceScope serviceScope)
+ {
+ MangaContext = GetContext(serviceScope);
+ }
+
protected override async Task DoWorkInternal()
{
- List> manga = await DbContext.MangaConnectorToManga.Where(mcId => mcId.UseForDownload).ToListAsync(CancellationToken);
+ List> manga = await MangaContext.MangaConnectorToManga.Where(mcId => mcId.UseForDownload).ToListAsync(CancellationToken);
List newWorkers = manga.Select(m => new DownloadCoverFromMangaconnectorWorker(m)).ToList();
return newWorkers.ToArray();
}
diff --git a/API/Workers/PeriodicWorkers/UpdateMetadataWorker.cs b/API/Workers/PeriodicWorkers/UpdateMetadataWorker.cs
index aae078f..355ef0e 100644
--- a/API/Workers/PeriodicWorkers/UpdateMetadataWorker.cs
+++ b/API/Workers/PeriodicWorkers/UpdateMetadataWorker.cs
@@ -1,3 +1,6 @@
+using System.Diagnostics.CodeAnalysis;
+using API.Schema.ActionsContext;
+using API.Schema.ActionsContext.Actions;
using API.Schema.MangaContext;
using API.Schema.MangaContext.MetadataFetchers;
using Microsoft.EntityFrameworkCore;
@@ -10,20 +13,31 @@ namespace API.Workers.PeriodicWorkers;
///
///
public class UpdateMetadataWorker(TimeSpan? interval = null, IEnumerable? dependsOn = null)
- : BaseWorkerWithContext(dependsOn), IPeriodic
+ : BaseWorkerWithContexts(dependsOn), IPeriodic
{
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(12);
+
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ private MangaContext MangaContext = null!;
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ private ActionsContext ActionsContext = null!;
+
+ protected override void SetContexts(IServiceScope serviceScope)
+ {
+ MangaContext = GetContext(serviceScope);
+ ActionsContext = GetContext(serviceScope);
+ }
protected override async Task DoWorkInternal()
{
Log.Debug("Updating metadata...");
// Get MetadataEntries of Manga marked for download
- List metadataEntriesToUpdate = await DbContext.MangaConnectorToManga
+ List metadataEntriesToUpdate = await MangaContext.MangaConnectorToManga
.Where(m => m.UseForDownload) // Get marked Manga
.Join(
- DbContext.MetadataEntries.Include(e => e.MetadataFetcher).Include(e => e.Manga),
+ MangaContext.MetadataEntries.Include(e => e.MetadataFetcher).Include(e => e.Manga),
mcId => mcId.ObjId,
e => e.MangaId,
(mcId, e) => e) // return MetadataEntry
@@ -33,12 +47,16 @@ public class UpdateMetadataWorker(TimeSpan? interval = null, IEnumerable? dependsOn = null) : BaseWorkerWithContext(dependsOn)
+public class RefreshLibrariesWorker(IEnumerable? dependsOn = null) : BaseWorkerWithContexts(dependsOn)
{
public static DateTime LastRefresh { get; set; } = DateTime.UnixEpoch;
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ private LibraryContext LibraryContext = null!;
+
+ protected override void SetContexts(IServiceScope serviceScope)
+ {
+ LibraryContext = GetContext(serviceScope);
+ }
+
protected override async Task DoWorkInternal()
{
Log.Debug("Refreshing libraries...");
LastRefresh = DateTime.UtcNow;
- List libraries = await DbContext.LibraryConnectors.ToListAsync(CancellationToken);
+ List libraries = await LibraryContext.LibraryConnectors.ToListAsync(CancellationToken);
foreach (LibraryConnector connector in libraries)
await connector.UpdateLibrary(CancellationToken);
Log.Debug("Libraries Refreshed...");
diff --git a/API/openapi/API_v2.json b/API/openapi/API_v2.json
index 81d75c3..c661751 100644
--- a/API/openapi/API_v2.json
+++ b/API/openapi/API_v2.json
@@ -5,6 +5,88 @@
"version": "2.0"
},
"paths": {
+ "/v2/Actions/Types": {
+ "get": {
+ "tags": [
+ "Actions"
+ ],
+ "summary": "Returns the available Action Types (API.Schema.ActionsContext.ActionRecord.Action) performed by Tranga",
+ "responses": {
+ "200": {
+ "description": "List of performed action-types"
+ },
+ "500": {
+ "description": "Database error"
+ }
+ }
+ }
+ },
+ "/v2/Actions/Interval": {
+ "post": {
+ "tags": [
+ "Actions"
+ ],
+ "summary": "Returns API.Schema.ActionsContext.ActionRecord performed in API.Controllers.ActionsController.Interval",
+ "requestBody": {
+ "content": {
+ "application/json-patch+json; x-version=2.0": {
+ "schema": {
+ "$ref": "#/components/schemas/Interval"
+ }
+ },
+ "application/json; x-version=2.0": {
+ "schema": {
+ "$ref": "#/components/schemas/Interval"
+ }
+ },
+ "text/json; x-version=2.0": {
+ "schema": {
+ "$ref": "#/components/schemas/Interval"
+ }
+ },
+ "application/*+json; x-version=2.0": {
+ "schema": {
+ "$ref": "#/components/schemas/Interval"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "List of performed actions"
+ },
+ "500": {
+ "description": "Database error"
+ }
+ }
+ }
+ },
+ "/v2/Actions/Type/{Type}": {
+ "get": {
+ "tags": [
+ "Actions"
+ ],
+ "summary": "Returns API.Schema.ActionsContext.ActionRecord with type Type",
+ "parameters": [
+ {
+ "name": "Type",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "List of performed actions"
+ },
+ "500": {
+ "description": "Database error"
+ }
+ }
+ }
+ },
"/v2/Chapters/{MangaId}": {
"get": {
"tags": [
@@ -1128,6 +1210,16 @@
}
}
},
+ "500": {
+ "description": "Error during Database Operation",
+ "content": {
+ "text/plain; x-version=2.0": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
"202": {
"description": "Folder is going to be moved"
}
@@ -3401,6 +3493,20 @@
},
"additionalProperties": false
},
+ "Interval": {
+ "type": "object",
+ "properties": {
+ "start": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "end": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "additionalProperties": false
+ },
"LibraryConnector": {
"required": [
"baseUrl",
diff --git a/CreateMigrations.sh b/CreateMigrations.sh
new file mode 100755
index 0000000..de30453
--- /dev/null
+++ b/CreateMigrations.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+if [ "$#" -ne 1 ]; then
+ echo "Usage: $0 "
+ exit 1
+fi
+cd API || exit
+dotnet ef migrations add $1 --context MangaContext --output-dir Migrations/Manga
+dotnet ef migrations add $1 --context LibraryContext --output-dir Migrations/Library
+dotnet ef migrations add $1 --context NotificationsContext --output-dir Migrations/Notifications
+dotnet ef migrations add $1 --context ActionsContext --output-dir Migrations/Actions
\ No newline at end of file