From 53276e858bc8b860debfc8e0211d6ed121d67fe2 Mon Sep 17 00:00:00 2001 From: glax Date: Thu, 16 Oct 2025 02:52:04 +0200 Subject: [PATCH] Actions initial commit --- API/Controllers/ActionsController.cs | 63 +++++++ API/Controllers/MangaController.cs | 34 ++-- .../20251016005257_Actions.Designer.cs | 156 ++++++++++++++++++ .../Actions/20251016005257_Actions.cs | 42 +++++ .../Actions/ActionsContextModelSnapshot.cs | 153 +++++++++++++++++ API/Program.cs | 13 ++ API/Schema/ActionsContext/ActionRecord.cs | 19 +++ .../Actions/ChapterDownloadedActionRecord.cs | 17 ++ .../Actions/ChaptersRetrievedActionRecord.cs | 12 ++ .../Actions/CoverDownloadedActionRecord.cs | 19 +++ .../Actions/DataMovedActionRecord.cs | 22 +++ .../Actions/Generic/ActionWithMangaRecord.cs | 15 ++ .../Actions/LibraryMovedActionRecord.cs | 18 ++ .../Actions/MetadataUpdatedActionRecord.cs | 20 +++ .../Actions/StartupActionRecord.cs | 8 + API/Schema/ActionsContext/ActionsContext.cs | 22 +++ API/Tranga.cs | 20 +-- API/Workers/BaseWorkerWithContext.cs | 24 --- API/Workers/BaseWorkerWithContexts.cs | 28 ++++ ...DownloadChapterFromMangaconnectorWorker.cs | 42 +++-- .../DownloadCoverFromMangaconnectorWorker.cs | 37 ++++- ...veMangaChaptersFromMangaconnectorWorker.cs | 37 ++++- API/Workers/MoveFileOrFolderWorker.cs | 26 ++- API/Workers/MoveMangaLibraryWorker.cs | 47 ------ .../CheckForNewChaptersWorker.cs | 14 +- .../CleanupMangaCoversWorker.cs | 13 +- ...leanupMangaconnectorIdsWithoutConnector.cs | 20 ++- .../RemoveOldNotificationsWorker.cs | 15 +- .../SendNotificationsWorker.cs | 21 ++- .../StartNewChapterDownloadsWorker.cs | 14 +- .../UpdateChaptersDownloadedWorker.cs | 18 +- .../PeriodicWorkers/UpdateCoversWorker.cs | 14 +- .../PeriodicWorkers/UpdateMetadataWorker.cs | 28 +++- API/Workers/RefreshLibrariesWorker.cs | 15 +- API/openapi/API_v2.json | 106 ++++++++++++ CreateMigrations.sh | 10 ++ 36 files changed, 1013 insertions(+), 169 deletions(-) create mode 100644 API/Controllers/ActionsController.cs create mode 100644 API/Migrations/Actions/20251016005257_Actions.Designer.cs create mode 100644 API/Migrations/Actions/20251016005257_Actions.cs create mode 100644 API/Migrations/Actions/ActionsContextModelSnapshot.cs create mode 100644 API/Schema/ActionsContext/ActionRecord.cs create mode 100644 API/Schema/ActionsContext/Actions/ChapterDownloadedActionRecord.cs create mode 100644 API/Schema/ActionsContext/Actions/ChaptersRetrievedActionRecord.cs create mode 100644 API/Schema/ActionsContext/Actions/CoverDownloadedActionRecord.cs create mode 100644 API/Schema/ActionsContext/Actions/DataMovedActionRecord.cs create mode 100644 API/Schema/ActionsContext/Actions/Generic/ActionWithMangaRecord.cs create mode 100644 API/Schema/ActionsContext/Actions/LibraryMovedActionRecord.cs create mode 100644 API/Schema/ActionsContext/Actions/MetadataUpdatedActionRecord.cs create mode 100644 API/Schema/ActionsContext/Actions/StartupActionRecord.cs create mode 100644 API/Schema/ActionsContext/ActionsContext.cs delete mode 100644 API/Workers/BaseWorkerWithContext.cs create mode 100644 API/Workers/BaseWorkerWithContexts.cs delete mode 100644 API/Workers/MoveMangaLibraryWorker.cs create mode 100755 CreateMigrations.sh 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