diff --git a/API/Controllers/FileLibraryController.cs b/API/Controllers/FileLibraryController.cs index 4611e4f..ff0d535 100644 --- a/API/Controllers/FileLibraryController.cs +++ b/API/Controllers/FileLibraryController.cs @@ -63,8 +63,8 @@ public class FileLibraryController(IServiceScope scope) : Controller //TODO Path check library.BasePath = newBasePath; - if(context.Sync().Result is { } errorMessage) - return StatusCode(Status500InternalServerError, errorMessage); + if(context.Sync().Result is { success: false } result) + return StatusCode(Status500InternalServerError, result.exceptionMessage); return Ok(); } @@ -90,8 +90,8 @@ public class FileLibraryController(IServiceScope scope) : Controller //TODO Name check library.LibraryName = newName; - if(context.Sync().Result is { } errorMessage) - return StatusCode(Status500InternalServerError, errorMessage); + if(context.Sync().Result is { success: false } result) + return StatusCode(Status500InternalServerError, result.exceptionMessage); return Ok(); } @@ -111,8 +111,8 @@ public class FileLibraryController(IServiceScope scope) : Controller //TODO Parameter check context.FileLibraries.Add(library); - if(context.Sync().Result is { } errorMessage) - return StatusCode(Status500InternalServerError, errorMessage); + if(context.Sync().Result is { success: false } result) + return StatusCode(Status500InternalServerError, result.exceptionMessage); return Created(); } @@ -134,8 +134,8 @@ public class FileLibraryController(IServiceScope scope) : Controller context.FileLibraries.Remove(library); - if(context.Sync().Result is { } errorMessage) - return StatusCode(Status500InternalServerError, errorMessage); + if(context.Sync().Result is { success: false } result) + return StatusCode(Status500InternalServerError, result.exceptionMessage); return Ok(); } } \ No newline at end of file diff --git a/API/Controllers/LibraryConnectorController.cs b/API/Controllers/LibraryConnectorController.cs index f947ebc..4aa30ae 100644 --- a/API/Controllers/LibraryConnectorController.cs +++ b/API/Controllers/LibraryConnectorController.cs @@ -60,8 +60,8 @@ public class LibraryConnectorController(IServiceScope scope) : Controller context.LibraryConnectors.Add(libraryConnector); - if(context.Sync().Result is { } errorMessage) - return StatusCode(Status500InternalServerError, errorMessage); + if(context.Sync().Result is { success: false } result) + return StatusCode(Status500InternalServerError, result.exceptionMessage); return Created(); } @@ -84,8 +84,8 @@ public class LibraryConnectorController(IServiceScope scope) : Controller context.LibraryConnectors.Remove(connector); - if(context.Sync().Result is { } errorMessage) - return StatusCode(Status500InternalServerError, errorMessage); + if(context.Sync().Result is { success: false } result) + return StatusCode(Status500InternalServerError, result.exceptionMessage); return Ok(); } } \ No newline at end of file diff --git a/API/Controllers/MangaConnectorController.cs b/API/Controllers/MangaConnectorController.cs index 9e2ad72..0487e6a 100644 --- a/API/Controllers/MangaConnectorController.cs +++ b/API/Controllers/MangaConnectorController.cs @@ -88,8 +88,8 @@ public class MangaConnectorController(IServiceScope scope) : Controller connector.Enabled = Enabled; - if(context.Sync().Result is { } errorMessage) - return StatusCode(Status500InternalServerError, errorMessage); + if(context.Sync().Result is { success: false } result) + return StatusCode(Status500InternalServerError, result.exceptionMessage); return Accepted(); } } \ No newline at end of file diff --git a/API/Controllers/MangaController.cs b/API/Controllers/MangaController.cs index 45b6bba..7d46472 100644 --- a/API/Controllers/MangaController.cs +++ b/API/Controllers/MangaController.cs @@ -81,8 +81,8 @@ public class MangaController(IServiceScope scope) : Controller context.Mangas.Remove(manga); - if(context.Sync().Result is { } errorMessage) - return StatusCode(Status500InternalServerError, errorMessage); + if(context.Sync().Result is { success: false } result) + return StatusCode(Status500InternalServerError, result.exceptionMessage); return Ok(); } @@ -329,8 +329,8 @@ public class MangaController(IServiceScope scope) : Controller return NotFound(); manga.IgnoreChaptersBefore = chapterThreshold; - if(context.Sync().Result is { } errorMessage) - return StatusCode(Status500InternalServerError, errorMessage); + if(context.Sync().Result is { success: false } result) + return StatusCode(Status500InternalServerError, result.exceptionMessage); return Accepted(); } @@ -354,7 +354,7 @@ public class MangaController(IServiceScope scope) : Controller return NotFound(nameof(LibraryId)); MoveMangaLibraryWorker moveLibrary = new(manga, library, scope); - UpdateChaptersDownloadedWorker updateDownloadedFiles = new(manga, scope, [moveLibrary]); + UpdateChaptersDownloadedWorker updateDownloadedFiles = new(manga, [moveLibrary]); Tranga.AddWorkers([moveLibrary, updateDownloadedFiles]); diff --git a/API/Controllers/MetadataFetcherController.cs b/API/Controllers/MetadataFetcherController.cs index 071bdb8..6785df7 100644 --- a/API/Controllers/MetadataFetcherController.cs +++ b/API/Controllers/MetadataFetcherController.cs @@ -119,8 +119,8 @@ public class MetadataFetcherController(IServiceScope scope) : Controller context.Remove(entry); - if(context.Sync().Result is { } errorMessage) - return StatusCode(Status500InternalServerError, errorMessage); + if(context.Sync().Result is { success: false } result) + return StatusCode(Status500InternalServerError, result.exceptionMessage); return Ok(); } } \ No newline at end of file diff --git a/API/Controllers/NotificationConnectorController.cs b/API/Controllers/NotificationConnectorController.cs index 1da22be..cac14e4 100644 --- a/API/Controllers/NotificationConnectorController.cs +++ b/API/Controllers/NotificationConnectorController.cs @@ -62,8 +62,8 @@ public class NotificationConnectorController(IServiceScope scope) : Controller context.NotificationConnectors.Add(notificationConnector); - if(context.Sync().Result is { } errorMessage) - return StatusCode(Status500InternalServerError, errorMessage); + if(context.Sync().Result is { success: false } result) + return StatusCode(Status500InternalServerError, result.exceptionMessage); return Created(); } @@ -156,8 +156,8 @@ public class NotificationConnectorController(IServiceScope scope) : Controller context.NotificationConnectors.Remove(connector); - if(context.Sync().Result is { } errorMessage) - return StatusCode(Status500InternalServerError, errorMessage); + if(context.Sync().Result is { success: false } result) + return StatusCode(Status500InternalServerError, result.exceptionMessage); return Created(); } } \ No newline at end of file diff --git a/API/Controllers/SearchController.cs b/API/Controllers/SearchController.cs index 258c72b..1fc1c4b 100644 --- a/API/Controllers/SearchController.cs +++ b/API/Controllers/SearchController.cs @@ -96,7 +96,7 @@ public class SearchController(IServiceScope scope) : Controller if(context.MangaConnectorToManga.Find(addMcId.Key) is null) context.MangaConnectorToManga.Add(mcId); - if (context.Sync().Result is not null) + if (context.Sync().Result is { success: false } ) return null; return manga; } diff --git a/API/Controllers/WorkerController.cs b/API/Controllers/WorkerController.cs index 0b3816e..a0f66af 100644 --- a/API/Controllers/WorkerController.cs +++ b/API/Controllers/WorkerController.cs @@ -1,10 +1,8 @@ using API.APIEndpointRecords; -using API.Schema.MangaContext; using API.Workers; using Asp.Versioning; using log4net; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; using static Microsoft.AspNetCore.Http.StatusCodes; // ReSharper disable InconsistentNaming @@ -129,7 +127,7 @@ public class WorkerController(ILog Log) : Controller if (worker.State >= WorkerExecutionState.Waiting) return StatusCode(Status412PreconditionFailed, "Already running"); - Tranga.StartWorker(worker); + Tranga.MarkWorkerForStart(worker); return Ok(); } diff --git a/API/Schema/TrangaBaseContext.cs b/API/Schema/TrangaBaseContext.cs index 5ad8947..1c67445 100644 --- a/API/Schema/TrangaBaseContext.cs +++ b/API/Schema/TrangaBaseContext.cs @@ -22,17 +22,17 @@ public abstract class TrangaBaseContext : DbContext where T : DbContext }, Array.Empty(), LogLevel.Warning, DbContextLoggerOptions.Level | DbContextLoggerOptions.Category | DbContextLoggerOptions.UtcTime); } - internal async Task Sync() + internal async Task<(bool success, string? exceptionMessage)> Sync() { try { await this.SaveChangesAsync(); - return null; + return (true, null); } catch (Exception e) { Log.Error(null, e); - return e.Message; + return (false, e.Message); } } } \ No newline at end of file diff --git a/API/Tranga.cs b/API/Tranga.cs index 7a8c6c1..12c39b2 100644 --- a/API/Tranga.cs +++ b/API/Tranga.cs @@ -2,6 +2,7 @@ using API.Workers; using log4net; using log4net.Config; +using Microsoft.EntityFrameworkCore; namespace API; @@ -54,6 +55,7 @@ public static class Tranga private static readonly Dictionary RunningWorkers = new(); public static BaseWorker[] GetRunningWorkers() => RunningWorkers.Keys.ToArray(); + private static readonly HashSet StartWorkers = new(); private static void WorkerStarter(object? serviceProviderObj) { Log.Info("WorkerStarter Thread running."); @@ -66,19 +68,26 @@ public static class Tranga while (true) { - using IServiceScope scope = serviceProvider.CreateScope(); - + foreach (BaseWorker startWorker in StartWorkers) + { + IServiceScope scope = serviceProvider.CreateScope(); + StartWorker(startWorker, scope); + } Thread.Sleep(TrangaSettings.workCycleTimeout); } } - internal static void StartWorker(BaseWorker worker) + internal static void MarkWorkerForStart(BaseWorker worker) => StartWorkers.Add(worker); + + private static void StartWorker(BaseWorker worker, IServiceScope scope) { - throw new NotImplementedException(); + if(worker is BaseWorkerWithContext w) + w.SetScope(scope); + worker.DoWork(); } internal static void StopWorker(BaseWorker worker) { - throw new NotImplementedException(); + worker.Cancel(); } } \ No newline at end of file diff --git a/API/Workers/BaseWorker.cs b/API/Workers/BaseWorker.cs index 49b6dca..0a1792d 100644 --- a/API/Workers/BaseWorker.cs +++ b/API/Workers/BaseWorker.cs @@ -5,21 +5,44 @@ namespace API.Workers; public abstract class BaseWorker : Identifiable { + /// + /// Workers this Worker depends on being completed before running. + /// public BaseWorker[] DependsOn { get; init; } + /// + /// Dependencies and dependencies of dependencies. See also . + /// public IEnumerable AllDependencies => DependsOn.Select(d => d.AllDependencies).SelectMany(x => x); + /// + /// and Self. + /// public IEnumerable DependenciesAndSelf => AllDependencies.Append(this); + /// + /// where is less than Completed. + /// public IEnumerable MissingDependencies => DependsOn.Where(d => d.State < WorkerExecutionState.Completed); - public bool DependenciesFulfilled => DependsOn.All(d => d.State >= WorkerExecutionState.Completed); - internal WorkerExecutionState State { get; set; } + public bool AllDependenciesFulfilled => DependsOn.All(d => d.State >= WorkerExecutionState.Completed); + internal WorkerExecutionState State { get; private set; } private static readonly CancellationTokenSource CancellationTokenSource = new(TimeSpan.FromMinutes(10)); protected ILog Log { get; init; } + /// + /// Stops worker, and marks as .Cancelled + /// public void Cancel() { this.State = WorkerExecutionState.Cancelled; CancellationTokenSource.Cancel(); } - protected void Fail() => this.State = WorkerExecutionState.Failed; + + /// + /// Stops worker, and marks as .Failed + /// + protected void Fail() + { + this.State = WorkerExecutionState.Failed; + CancellationTokenSource.Cancel(); + } public BaseWorker(IEnumerable? dependsOn = null) { @@ -27,6 +50,22 @@ public abstract class BaseWorker : Identifiable this.Log = LogManager.GetLogger(GetType()); } + /// + /// Sets States during worker-run. + /// States: + /// + /// .Waiting when waiting for + /// .Running when running + /// .Completed after finished + /// + /// + /// + /// + /// If has , missing dependencies. + /// If are .Running, itself after waiting for dependencies. + /// If has run, additional . + /// + /// public Task DoWork() { this.State = WorkerExecutionState.Waiting; @@ -60,9 +99,9 @@ public abstract class BaseWorker : Identifiable public enum WorkerExecutionState { Failed = 0, + Cancelled = 32, Created = 64, Waiting = 96, Running = 128, - Completed = 192, - Cancelled = 193 + Completed = 192 } \ No newline at end of file diff --git a/API/Workers/BaseWorkerWithContext.cs b/API/Workers/BaseWorkerWithContext.cs index 4a58779..8877c3b 100644 --- a/API/Workers/BaseWorkerWithContext.cs +++ b/API/Workers/BaseWorkerWithContext.cs @@ -1,8 +1,18 @@ +using System.Configuration; using Microsoft.EntityFrameworkCore; namespace API.Workers; -public abstract class BaseWorkerWithContext(IServiceScope scope, IEnumerable? dependsOn = null) : BaseWorker(dependsOn) where T : DbContext +public abstract class BaseWorkerWithContext(IEnumerable? dependsOn = null) : BaseWorker(dependsOn) where T : DbContext { - protected T DbContext { get; init; } = scope.ServiceProvider.GetRequiredService(); + protected T? DbContext = null; + public void SetScope(IServiceScope scope) => 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/MaintenanceWorkers/CleanupMangaCoversWorker.cs b/API/Workers/MaintenanceWorkers/CleanupMangaCoversWorker.cs index 6d51e0e..6eb4d2e 100644 --- a/API/Workers/MaintenanceWorkers/CleanupMangaCoversWorker.cs +++ b/API/Workers/MaintenanceWorkers/CleanupMangaCoversWorker.cs @@ -2,7 +2,7 @@ using API.Schema.MangaContext; namespace API.Workers.MaintenanceWorkers; -public class CleanupMangaCoversWorker(IServiceScope scope, IEnumerable? dependsOn = null) : BaseWorkerWithContext(scope, dependsOn), IPeriodic +public class CleanupMangaCoversWorker(IEnumerable? dependsOn = null) : BaseWorkerWithContext(dependsOn), IPeriodic { public DateTime LastExecution { get; set; } = DateTime.UtcNow; public TimeSpan Interval { get; set; } = TimeSpan.FromMinutes(60); diff --git a/API/Workers/MaintenanceWorkers/UpdateChaptersDownloadedWorker.cs b/API/Workers/MaintenanceWorkers/UpdateChaptersDownloadedWorker.cs index 245d164..36b0ab7 100644 --- a/API/Workers/MaintenanceWorkers/UpdateChaptersDownloadedWorker.cs +++ b/API/Workers/MaintenanceWorkers/UpdateChaptersDownloadedWorker.cs @@ -1,10 +1,8 @@ using API.Schema.MangaContext; -using Microsoft.EntityFrameworkCore; - namespace API.Workers; -public class UpdateChaptersDownloadedWorker(Manga manga, IServiceScope scope, IEnumerable? dependsOn = null) - : BaseWorkerWithContext(scope, dependsOn), IPeriodic +public class UpdateChaptersDownloadedWorker(Manga manga, IEnumerable? dependsOn = null) + : BaseWorkerWithContext(dependsOn), IPeriodic { public DateTime LastExecution { get; set; } = DateTime.UtcNow; public TimeSpan Interval { get; set; } = TimeSpan.FromMinutes(60); @@ -15,14 +13,7 @@ public class UpdateChaptersDownloadedWorker(Manga manga, IServiceScope scope, IE mangaChapter.Downloaded = mangaChapter.CheckDownloaded(); } - try - { - DbContext.SaveChanges(); - } - catch (DbUpdateException e) - { - Log.Error(e); - } + DbContext.Sync(); return []; } } \ No newline at end of file diff --git a/API/Workers/MangaDownloadWorkers/DownloadChapterFromMangaconnectorWorker.cs b/API/Workers/MangaDownloadWorkers/DownloadChapterFromMangaconnectorWorker.cs index 5fe2edd..528fd97 100644 --- a/API/Workers/MangaDownloadWorkers/DownloadChapterFromMangaconnectorWorker.cs +++ b/API/Workers/MangaDownloadWorkers/DownloadChapterFromMangaconnectorWorker.cs @@ -10,8 +10,8 @@ using static System.IO.UnixFileMode; namespace API.Workers; -public class DownloadChapterFromMangaconnectorWorker(Chapter chapter, IServiceScope scope, IEnumerable? dependsOn = null) - : BaseWorkerWithContext(scope, dependsOn) +public class DownloadChapterFromMangaconnectorWorker(Chapter chapter, IEnumerable? dependsOn = null) + : BaseWorkerWithContext(dependsOn) { protected override BaseWorker[] DoWorkInternal() { @@ -89,7 +89,7 @@ public class DownloadChapterFromMangaconnectorWorker(Chapter chapter, IServiceSc Directory.Delete(tempFolder, true); //Cleanup chapter.Downloaded = true; - DbContext.SaveChanges(); + DbContext.Sync(); return []; } diff --git a/API/Workers/MangaDownloadWorkers/DownloadCoverFromMangaconnectorWorker.cs b/API/Workers/MangaDownloadWorkers/DownloadCoverFromMangaconnectorWorker.cs index 2f5e839..3aa5216 100644 --- a/API/Workers/MangaDownloadWorkers/DownloadCoverFromMangaconnectorWorker.cs +++ b/API/Workers/MangaDownloadWorkers/DownloadCoverFromMangaconnectorWorker.cs @@ -1,26 +1,20 @@ using API.Schema.MangaContext; using API.Schema.MangaContext.MangaConnectors; -using Microsoft.EntityFrameworkCore; namespace API.Workers; -public class DownloadCoverFromMangaconnectorWorker(MangaConnectorId mcId, IServiceScope scope, IEnumerable? dependsOn = null) - : BaseWorkerWithContext(scope, dependsOn) +public class DownloadCoverFromMangaconnectorWorker(MangaConnectorId mcId, IEnumerable? dependsOn = null) + : BaseWorkerWithContext(dependsOn) { public MangaConnectorId MangaConnectorId { get; init; } = mcId; protected override BaseWorker[] DoWorkInternal() { MangaConnector mangaConnector = MangaConnectorId.MangaConnector; Manga manga = MangaConnectorId.Obj; - try - { - manga.CoverFileNameInCache = mangaConnector.SaveCoverImageToCache(MangaConnectorId); - DbContext.SaveChanges(); - } - catch (DbUpdateException e) - { - Log.Error(e); - } + + manga.CoverFileNameInCache = mangaConnector.SaveCoverImageToCache(MangaConnectorId); + + DbContext.Sync(); return []; } } \ No newline at end of file diff --git a/API/Workers/MangaDownloadWorkers/RetrieveMangaChaptersFromMangaconnectorWorker.cs b/API/Workers/MangaDownloadWorkers/RetrieveMangaChaptersFromMangaconnectorWorker.cs index c1af2b2..e2448f9 100644 --- a/API/Workers/MangaDownloadWorkers/RetrieveMangaChaptersFromMangaconnectorWorker.cs +++ b/API/Workers/MangaDownloadWorkers/RetrieveMangaChaptersFromMangaconnectorWorker.cs @@ -1,11 +1,10 @@ using API.Schema.MangaContext; using API.Schema.MangaContext.MangaConnectors; -using Microsoft.EntityFrameworkCore; namespace API.Workers; -public class RetrieveMangaChaptersFromMangaconnectorWorker(MangaConnectorId mcId, string language, IServiceScope scope, IEnumerable? dependsOn = null) - : BaseWorkerWithContext(scope, dependsOn) +public class RetrieveMangaChaptersFromMangaconnectorWorker(MangaConnectorId mcId, string language, IEnumerable? dependsOn = null) + : BaseWorkerWithContext(dependsOn) { public MangaConnectorId MangaConnectorId { get; init; } = mcId; protected override BaseWorker[] DoWorkInternal() @@ -19,20 +18,13 @@ public class RetrieveMangaChaptersFromMangaconnectorWorker(MangaConnectorId chapter.Item1.Key == ch.Key && ch.Downloaded) == false).ToArray(); Log.Info($"{manga.Chapters.Count} existing + {newChapters.Length} new chapters."); - try + foreach ((Chapter chapter, MangaConnectorId mcId) newChapter in newChapters) { - foreach ((Chapter chapter, MangaConnectorId mcId) newChapter in newChapters) - { - manga.Chapters.Add(newChapter.chapter); - DbContext.MangaConnectorToChapter.Add(newChapter.mcId); - } + manga.Chapters.Add(newChapter.chapter); + DbContext.MangaConnectorToChapter.Add(newChapter.mcId); + } - DbContext.SaveChanges(); - } - catch (DbUpdateException e) - { - Log.Error(e); - } + DbContext.Sync(); return []; } diff --git a/API/Workers/MoveMangaLibraryWorker.cs b/API/Workers/MoveMangaLibraryWorker.cs index 6205271..7371cfe 100644 --- a/API/Workers/MoveMangaLibraryWorker.cs +++ b/API/Workers/MoveMangaLibraryWorker.cs @@ -1,24 +1,17 @@ using API.Schema.MangaContext; -using Microsoft.EntityFrameworkCore; namespace API.Workers; public class MoveMangaLibraryWorker(Manga manga, FileLibrary toLibrary, IServiceScope scope, IEnumerable? dependsOn = null) - : BaseWorkerWithContext(scope, dependsOn) + : BaseWorkerWithContext(dependsOn) { protected override BaseWorker[] DoWorkInternal() { Dictionary oldPath = manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath); manga.Library = toLibrary; - try - { - DbContext.SaveChanges(); - } - catch (DbUpdateException e) - { - Log.Error(e); + + if (DbContext.Sync().Result is { success: false }) return []; - } return manga.Chapters.Select(c => new MoveFileOrFolderWorker(c.FullArchiveFilePath, oldPath[c])).ToArray(); } diff --git a/API/Workers/SendNotificationsWorker.cs b/API/Workers/SendNotificationsWorker.cs index b3b74ef..8c12643 100644 --- a/API/Workers/SendNotificationsWorker.cs +++ b/API/Workers/SendNotificationsWorker.cs @@ -2,8 +2,8 @@ using API.Schema.NotificationsContext; namespace API.Workers; -public class SendNotificationsWorker(IServiceScope scope, IEnumerable? dependsOn = null) - : BaseWorkerWithContext(scope, dependsOn), IPeriodic +public class SendNotificationsWorker(IEnumerable? dependsOn = null) + : BaseWorkerWithContext(dependsOn), IPeriodic { public DateTime LastExecution { get; set; } = DateTime.UtcNow; public TimeSpan Interval { get; set; } = TimeSpan.FromMinutes(1);