From bd189984a9e59a13a7d054aca5d6fbf65d47cfff Mon Sep 17 00:00:00 2001 From: glax Date: Mon, 5 Jun 2023 00:35:57 +0200 Subject: [PATCH] Rewrote entire Task-Structure: TrangaTask now only contains essentials, derived classes contain specific information such as connectorName, publication, chapter, etc. Removed taskQueue system, instead all tasks are kept in _allTasks. Progress is being tracked in TrangaTask resolves #36 resolves #32 Added new TrangaTask: DownloadChapter to download single chapters. #35 Fixed duplicate file-access when writing settings. --- Tranga-API/Program.cs | 52 +--- Tranga-CLI/Tranga_Cli.cs | 138 ++++++--- Tranga/Connector.cs | 9 +- Tranga/Connectors/MangaDex.cs | 3 +- Tranga/Connectors/Manganato.cs | 3 +- Tranga/TaskManager.cs | 280 +++++++++++------- Tranga/TrangaTask.cs | 36 +-- Tranga/TrangaTasks/DownloadChapterTask.cs | 31 ++ Tranga/TrangaTasks/DownloadNewChaptersTask.cs | 32 +- Tranga/TrangaTasks/UpdateLibrariesTask.cs | 3 +- Website/interaction.js | 11 +- Website/style.css | 2 +- 12 files changed, 373 insertions(+), 227 deletions(-) create mode 100644 Tranga/TrangaTasks/DownloadChapterTask.cs diff --git a/Tranga-API/Program.cs b/Tranga-API/Program.cs index 5d42e70..f2ed2b9 100644 --- a/Tranga-API/Program.cs +++ b/Tranga-API/Program.cs @@ -73,9 +73,8 @@ app.MapGet("/Tasks/GetTaskTypes", () => Enum.GetNames(typeof(TrangaTask.Task))); app.MapPost("/Tasks/Create", (string taskType, string? connectorName, string? publicationId, string reoccurrenceTime, string? language) => { - Publication? publication = taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == publicationId); TrangaTask.Task task = Enum.Parse(taskType); - taskManager.AddTask(task, connectorName, publication, TimeSpan.Parse(reoccurrenceTime), language??""); + taskManager.AddTask(task, connectorName, publicationId, TimeSpan.Parse(reoccurrenceTime), language??""); }); app.MapDelete("/Tasks/Delete", (string taskType, string? connectorName, string? publicationId) => @@ -90,12 +89,7 @@ app.MapGet("/Tasks/Get", (string taskType, string? connectorName, string? search try { TrangaTask.Task task = Enum.Parse(taskType); - if (searchString is null || connectorName is null) - return taskManager.GetAllTasks().Where(tTask => tTask.task == task); - else - return taskManager.GetAllTasks().Where(tTask => - tTask.task == task && tTask.connectorName == connectorName && tTask.ToString() - .Contains(searchString, StringComparison.InvariantCultureIgnoreCase)); + return taskManager.GetTasksMatching(task, connectorName:connectorName, searchString:searchString); } catch (ArgumentException) { @@ -108,15 +102,9 @@ app.MapGet("/Tasks/GetTaskProgress", (string taskType, string? connectorName, st try { TrangaTask.Task pTask = Enum.Parse(taskType); - TrangaTask? task = null; - if (connectorName is null || publicationId is null) - task = taskManager.GetAllTasks().FirstOrDefault(tTask => - tTask.task == pTask); - else - task = taskManager.GetAllTasks().FirstOrDefault(tTask => - tTask.task == pTask && tTask.publication?.internalId == publicationId && - tTask.connectorName == connectorName); - + TrangaTask? task = taskManager + .GetTasksMatching(pTask, connectorName: connectorName, publicationId: publicationId)?.First(); + if (task is null) return -1f; @@ -133,14 +121,8 @@ app.MapPost("/Tasks/Start", (string taskType, string? connectorName, string? pub try { TrangaTask.Task pTask = Enum.Parse(taskType); - TrangaTask? task = null; - if (connectorName is null || publicationId is null) - task = taskManager.GetAllTasks().FirstOrDefault(tTask => - tTask.task == pTask); - else - task = taskManager.GetAllTasks().FirstOrDefault(tTask => - tTask.task == pTask && tTask.publication?.internalId == publicationId && - tTask.connectorName == connectorName); + TrangaTask? task = taskManager + .GetTasksMatching(pTask, connectorName: connectorName, publicationId: publicationId)?.First(); if (task is null) return; @@ -164,14 +146,8 @@ app.MapPost("/Queue/Enqueue", (string taskType, string? connectorName, string? p try { TrangaTask.Task pTask = Enum.Parse(taskType); - TrangaTask? task = null; - if (connectorName is null || publicationId is null) - task = taskManager.GetAllTasks().FirstOrDefault(tTask => - tTask.task == pTask); - else - task = taskManager.GetAllTasks().FirstOrDefault(tTask => - tTask.task == pTask && tTask.publication?.internalId == publicationId && - tTask.connectorName == connectorName); + TrangaTask? task = taskManager + .GetTasksMatching(pTask, connectorName: connectorName, publicationId: publicationId)?.First(); if (task is null) return; @@ -188,14 +164,8 @@ app.MapDelete("/Queue/Dequeue", (string taskType, string? connectorName, string? try { TrangaTask.Task pTask = Enum.Parse(taskType); - TrangaTask? task = null; - if (connectorName is null || publicationId is null) - task = taskManager.GetAllTasks().FirstOrDefault(tTask => - tTask.task == pTask); - else - task = taskManager.GetAllTasks().FirstOrDefault(tTask => - tTask.task == pTask && tTask.publication?.internalId == publicationId && - tTask.connectorName == connectorName); + TrangaTask? task = taskManager + .GetTasksMatching(pTask, connectorName: connectorName, publicationId: publicationId)?.First(); if (task is null) return; diff --git a/Tranga-CLI/Tranga_Cli.cs b/Tranga-CLI/Tranga_Cli.cs index 060ec06..bc5b21d 100644 --- a/Tranga-CLI/Tranga_Cli.cs +++ b/Tranga-CLI/Tranga_Cli.cs @@ -2,6 +2,7 @@ using Logging; using Tranga; using Tranga.LibraryManagers; +using Tranga.TrangaTasks; namespace Tranga_CLI; @@ -239,18 +240,18 @@ public static class Tranga_Cli int tIndex = 0; Console.WriteLine($"Tasks (Running/Queue/Total): {taskRunningCount}/{taskEnqueuedCount}/{taskCount}"); string header = - $"{"",-5}{"Task",-20} | {"Last Executed",-20} | {"Reoccurrence",-12} | {"State",-10} | {"Connector",-15} | {"Progress",-9} | Publication/Manga"; + $"{"",-5}{"Task",-20} | {"Last Executed",-20} | {"Reoccurrence",-12} | {"State",-10} | {"Progress",-9} | {"Connector",-15} | Publication/Manga "; Console.WriteLine(header); Console.WriteLine(new string('-', header.Length)); foreach (TrangaTask trangaTask in tasks) { string[] taskSplit = trangaTask.ToString().Split(", "); - Console.WriteLine($"{tIndex++:000}: {taskSplit[0],-20} | {taskSplit[1],-20} | {taskSplit[2],-12} | {taskSplit[3],-10} | {(taskSplit.Length > 4 ? taskSplit[4] : ""),-15} | {(taskSplit.Length > 5 ? taskSplit[5] : ""),-9} |{(taskSplit.Length > 6 ? taskSplit[6] : "")}"); + Console.WriteLine($"{tIndex++:000}: {taskSplit[0],-20} | {taskSplit[1],-20} | {taskSplit[2],-12} | {taskSplit[3],-10} | {(taskSplit.Length > 4 ? taskSplit[4] : ""),-9} | {(taskSplit.Length > 5 ? taskSplit[5] : ""),-15} | {(taskSplit.Length > 6 ? taskSplit[6] : "")} {(taskSplit.Length > 7 ? taskSplit[7] : "")} {(taskSplit.Length > 8 ? taskSplit[8] : "")}"); } } - private static TrangaTask? SelectTask(TrangaTask[] tasks, Logger logger) + private static TrangaTask[] SelectTasks(TrangaTask[] tasks, Logger logger) { logger.WriteLine("Tranga_CLI", "Menu: Select task"); if (tasks.Length < 1) @@ -258,13 +259,13 @@ public static class Tranga_Cli Console.Clear(); Console.WriteLine("There are no available Tasks."); logger.WriteLine("Tranga_CLI", "No available Tasks."); - return null; + return Array.Empty(); } PrintTasks(tasks, logger); logger.WriteLine("Tranga_CLI", "Selecting Task to Remove (from queue)"); Console.WriteLine("Enter q to abort"); - Console.WriteLine($"Select Task (0-{tasks.Length - 1}):"); + Console.WriteLine($"Select Task(s) (0-{tasks.Length - 1}):"); string? selectedTask = Console.ReadLine(); while(selectedTask is null || selectedTask.Length < 1) @@ -275,21 +276,20 @@ public static class Tranga_Cli Console.Clear(); Console.WriteLine("aborted."); logger.WriteLine("Tranga_CLI", "aborted"); - return null; - } - - try - { - int selectedTaskIndex = Convert.ToInt32(selectedTask); - return tasks[selectedTaskIndex]; - } - catch (Exception e) - { - Console.WriteLine($"Exception: {e.Message}"); - logger.WriteLine("Tranga_CLI", e.Message); + return Array.Empty(); } - return null; + if (selectedTask.Contains('-')) + { + int start = Convert.ToInt32(selectedTask.Split('-')[0]); + int end = Convert.ToInt32(selectedTask.Split('-')[1]); + return tasks[start..end]; + } + else + { + int selectedTaskIndex = Convert.ToInt32(selectedTask); + return new[] { tasks[selectedTaskIndex] }; + } } private static void AddMangaTaskToQueue(TaskManager taskManager, Logger logger) @@ -307,7 +307,7 @@ public static class Tranga_Cli TimeSpan reoccurrence = SelectReoccurrence(logger); logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager"); - TrangaTask newTask = taskManager.AddTask(TrangaTask.Task.DownloadNewChapters, connector.name, publication, reoccurrence, "en"); + TrangaTask? newTask = taskManager.AddTask(TrangaTask.Task.DownloadNewChapters, connector.name, publication.Value.publicationId, reoccurrence, "en"); Console.WriteLine(newTask); } @@ -319,12 +319,10 @@ public static class Tranga_Cli TrangaTask[] tasks = taskManager.GetAllTasks().Where(rTask => rTask.state is not TrangaTask.ExecutionState.Enqueued and not TrangaTask.ExecutionState.Running).ToArray(); - TrangaTask? selectedTask = SelectTask(tasks, logger); - if (selectedTask is null) - return; - - logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager"); - taskManager.AddTaskToQueue(selectedTask); + TrangaTask[] selectedTasks = SelectTasks(tasks, logger); + logger.WriteLine("Tranga_CLI", $"Sending {selectedTasks.Length} Tasks to TaskManager"); + foreach(TrangaTask task in selectedTasks) + taskManager.AddTaskToQueue(task); } private static void RemoveTaskFromQueue(TaskManager taskManager, Logger logger) @@ -334,12 +332,10 @@ public static class Tranga_Cli TrangaTask[] tasks = taskManager.GetAllTasks().Where(rTask => rTask.state is TrangaTask.ExecutionState.Enqueued).ToArray(); - TrangaTask? selectedTask = SelectTask(tasks, logger); - if (selectedTask is null) - return; - - logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager"); - taskManager.RemoveTaskFromQueue(selectedTask); + TrangaTask[] selectedTasks = SelectTasks(tasks, logger); + logger.WriteLine("Tranga_CLI", $"Sending {selectedTasks.Length} Tasks to TaskManager"); + foreach(TrangaTask task in selectedTasks) + taskManager.RemoveTaskFromQueue(task); } private static void TailLog(Logger logger) @@ -367,7 +363,7 @@ public static class Tranga_Cli if (tmpTask is null) return; TrangaTask.Task task = (TrangaTask.Task)tmpTask; - + Connector? connector = null; if (task != TrangaTask.Task.UpdateLibraries) { @@ -383,11 +379,25 @@ public static class Tranga_Cli if (publication is null) return; } - - TimeSpan reoccurrence = SelectReoccurrence(logger); - logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager"); - TrangaTask newTask = taskManager.AddTask(task, connector?.name, publication, reoccurrence, "en"); - Console.WriteLine(newTask); + + if (task is TrangaTask.Task.DownloadNewChapters) + { + TimeSpan reoccurrence = SelectReoccurrence(logger); + logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager"); + + TrangaTask newTask = new DownloadNewChaptersTask(TrangaTask.Task.DownloadNewChapters, connector!.name, (Publication)publication!, reoccurrence, "en"); + taskManager.AddTask(newTask); + Console.WriteLine(newTask); + }else if (task is TrangaTask.Task.DownloadChapter) + { + foreach (Chapter chapter in SelectChapters(connector!, (Publication)publication!, logger)) + { + TrangaTask newTask = new DownloadChapterTask(TrangaTask.Task.DownloadChapter, connector!.name, + (Publication)publication!, chapter, "en"); + taskManager.AddTask(newTask); + Console.WriteLine(newTask); + } + } } private static void ExecuteTaskNow(TaskManager taskManager, Logger logger) @@ -395,12 +405,10 @@ public static class Tranga_Cli logger.WriteLine("Tranga_CLI", "Menu: Executing Task"); TrangaTask[] tasks = taskManager.GetAllTasks().Where(nTask => nTask.state is not TrangaTask.ExecutionState.Running).ToArray(); - TrangaTask? selectedTask = SelectTask(tasks, logger); - if (selectedTask is null) - return; - - logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager"); - taskManager.ExecuteTaskNow(selectedTask); + TrangaTask[] selectedTasks = SelectTasks(tasks, logger); + logger.WriteLine("Tranga_CLI", $"Sending {selectedTasks.Length} Tasks to TaskManager"); + foreach(TrangaTask task in selectedTasks) + taskManager.ExecuteTaskNow(task); } private static void DeleteTask(TaskManager taskManager, Logger logger) @@ -408,12 +416,10 @@ public static class Tranga_Cli logger.WriteLine("Tranga_CLI", "Menu: Delete Task"); TrangaTask[] tasks = taskManager.GetAllTasks(); - TrangaTask? selectedTask = SelectTask(tasks, logger); - if (selectedTask is null) - return; - - logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager"); - taskManager.DeleteTask(selectedTask.task, selectedTask.connectorName, selectedTask.publication); + TrangaTask[] selectedTasks = SelectTasks(tasks, logger); + logger.WriteLine("Tranga_CLI", $"Sending {selectedTasks.Length} Tasks to TaskManager"); + foreach(TrangaTask task in selectedTasks) + taskManager.DeleteTask(task); } private static TrangaTask.Task? SelectTaskType(Logger logger) @@ -464,6 +470,40 @@ public static class Tranga_Cli return TimeSpan.Parse(Console.ReadLine()!, new CultureInfo("en-US")); } + private static Chapter[] SelectChapters(Connector connector, Publication publication, Logger logger) + { + logger.WriteLine("Tranga_CLI", "Menu: Select Chapters"); + Chapter[] availableChapters = connector.GetChapters(publication, "en"); + int cIndex = 0; + Console.WriteLine("Chapters:"); + foreach(Chapter chapter in availableChapters) + Console.WriteLine($"{cIndex++}: Vol.{chapter.volumeNumber} Ch.{chapter.chapterNumber} - {chapter.name}"); + + Console.WriteLine("Enter q to abort"); + Console.WriteLine($"Select Chapter(s):"); + + string? selectedChapters = Console.ReadLine(); + while(selectedChapters is null || selectedChapters.Length < 1) + selectedChapters = Console.ReadLine(); + + if (selectedChapters.Length == 1 && selectedChapters.ToLower() == "q") + { + Console.Clear(); + Console.WriteLine("aborted."); + logger.WriteLine("Tranga_CLI", "aborted."); + return Array.Empty(); + } + + if (selectedChapters.Contains('-')) + { + int start = Convert.ToInt32(selectedChapters.Split('-')[0]); + int end = Convert.ToInt32(selectedChapters.Split('-')[1]); + return availableChapters[start..end]; + } + else + return new Chapter[] { availableChapters[Convert.ToInt32(selectedChapters)] }; + } + private static Connector? SelectConnector(Connector[] connectors, Logger logger) { logger.WriteLine("Tranga_CLI", "Menu: Select Connector"); diff --git a/Tranga/Connector.cs b/Tranga/Connector.cs index 0aae075..006597c 100644 --- a/Tranga/Connector.cs +++ b/Tranga/Connector.cs @@ -4,6 +4,7 @@ using System.Runtime.InteropServices; using System.Text.RegularExpressions; using System.Xml.Linq; using Logging; +using Tranga.TrangaTasks; using static System.IO.UnixFileMode; namespace Tranga; @@ -60,7 +61,7 @@ public abstract class Connector /// Publication that contains Chapter /// Chapter with Images to retrieve /// Will be used for progress-tracking - public abstract void DownloadChapter(Publication publication, Chapter chapter, TrangaTask parentTask); + public abstract void DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask); /// /// Copies the already downloaded cover from cache to downloadLocation @@ -160,7 +161,7 @@ public abstract class Connector /// Path of the generate Chapter ComicInfo.xml, if it was generated /// RequestType for RateLimits /// Used in http request header - protected void DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, byte requestType, TrangaTask parentTask, string? comicInfoPath = null, string? referrer = null) + protected void DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, byte requestType, DownloadChapterTask parentTask, string? comicInfoPath = null, string? referrer = null) { logger?.WriteLine("Connector", $"Downloading Images for {saveArchiveFilePath}"); //Check if Publication Directory already exists @@ -180,9 +181,9 @@ public abstract class Connector { string[] split = imageUrl.Split('.'); string extension = split[^1]; - logger?.WriteLine("Connector", $"Downloading Image {chapter + 1:000}/{imageUrls.Length:000} {(parentTask.publication?.sortName)![..(int)(parentTask.publication?.sortName.Length > 25 ? 25 : parentTask.publication?.sortName.Length)!],-25} {(parentTask.publication?.internalId)![..(int)(parentTask.publication?.internalId.Length > 25 ? 25 : parentTask.publication?.internalId.Length)!],-25} Total Task Progress: {parentTask.progress:00.0}%"); + logger?.WriteLine("Connector", $"Downloading Image {chapter + 1:000}/{imageUrls.Length:000} {parentTask.publication.sortName} {parentTask.publication.internalId} Vol.{parentTask.chapter.volumeNumber} Ch.{parentTask.chapter.chapterNumber} {parentTask.progress:P2}"); DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), requestType, referrer); - parentTask.tasksFinished++; + parentTask.progress += 1f / imageUrls.Length; } if(comicInfoPath is not null) diff --git a/Tranga/Connectors/MangaDex.cs b/Tranga/Connectors/MangaDex.cs index 4bfa5ee..6e0962d 100644 --- a/Tranga/Connectors/MangaDex.cs +++ b/Tranga/Connectors/MangaDex.cs @@ -3,6 +3,7 @@ using System.Net; using System.Text.Json; using System.Text.Json.Nodes; using Logging; +using Tranga.TrangaTasks; namespace Tranga.Connectors; public class MangaDex : Connector @@ -204,7 +205,7 @@ public class MangaDex : Connector return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray(); } - public override void DownloadChapter(Publication publication, Chapter chapter, TrangaTask parentTask) + public override void DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask) { logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}"); //Request URLs for Chapter-Images diff --git a/Tranga/Connectors/Manganato.cs b/Tranga/Connectors/Manganato.cs index bef84a9..6a684be 100644 --- a/Tranga/Connectors/Manganato.cs +++ b/Tranga/Connectors/Manganato.cs @@ -2,6 +2,7 @@ using System.Text.RegularExpressions; using HtmlAgilityPack; using Logging; +using Tranga.TrangaTasks; namespace Tranga.Connectors; @@ -161,7 +162,7 @@ public class Manganato : Connector return ret.ToArray(); } - public override void DownloadChapter(Publication publication, Chapter chapter, TrangaTask parentTask) + public override void DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask) { logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}"); string requestUrl = chapter.url; diff --git a/Tranga/TaskManager.cs b/Tranga/TaskManager.cs index 39df502..076781b 100644 --- a/Tranga/TaskManager.cs +++ b/Tranga/TaskManager.cs @@ -13,24 +13,20 @@ namespace Tranga; public class TaskManager { public Dictionary> chapterCollection = new(); - private HashSet _allTasks; + private HashSet _allTasks = new HashSet(); private bool _continueRunning = true; private readonly Connector[] _connectors; - private readonly Dictionary> _taskQueue = new(); public TrangaSettings settings { get; } private Logger? logger { get; } /// Local path to save data (Manga) to /// Path to the working directory /// Path to the cover-image cache - /// The Url of the Komga-instance that you want to update - /// The Komga username - /// The Komga password + /// /// public TaskManager(string downloadFolderPath, string workingDirectory, string imageCachePath, HashSet libraryManagers, Logger? logger = null) { this.logger = logger; - _allTasks = new HashSet(); this.settings = new TrangaSettings(downloadFolderPath, workingDirectory, libraryManagers); ExportDataAndSettings(); @@ -40,8 +36,6 @@ public class TaskManager new MangaDex(downloadFolderPath, imageCachePath, logger), new Manganato(downloadFolderPath, imageCachePath, logger) }; - foreach(Connector cConnector in this._connectors) - _taskQueue.Add(cConnector, new List()); Thread taskChecker = new(TaskCheckerThread); taskChecker.Start(); @@ -72,9 +66,6 @@ public class TaskManager new MangaDex(settings.downloadLocation, settings.coverImageCache, logger), new Manganato(settings.downloadLocation, settings.coverImageCache, logger) }; - foreach(Connector cConnector in this._connectors) - _taskQueue.Add(cConnector, new List()); - _allTasks = new HashSet(); this.settings = settings; ImportData(); @@ -90,31 +81,43 @@ public class TaskManager private void TaskCheckerThread() { logger?.WriteLine(this.GetType().ToString(), "Starting TaskCheckerThread."); + int allTasksWaitingLength = _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting); while (_continueRunning) { - //Check if previous tasks have finished and execute new tasks - foreach (KeyValuePair> connectorTaskQueue in _taskQueue) - { - if(connectorTaskQueue.Value.RemoveAll(task => task.state == TrangaTask.ExecutionState.Waiting) > 0) - ExportDataAndSettings(); - - if (connectorTaskQueue.Value.Count > 0 && connectorTaskQueue.Value.All(task => task.state is TrangaTask.ExecutionState.Enqueued)) - ExecuteTaskNow(connectorTaskQueue.Value.First()); - } - - //Check if task should be executed - //Depending on type execute immediately or enqueue - foreach (TrangaTask task in _allTasks.Where(aTask => aTask.ShouldExecute())) + TrangaTask[] tmp = _allTasks.Where(taskQuery => + taskQuery.nextExecution < DateTime.Now && + taskQuery.state is TrangaTask.ExecutionState.Waiting or TrangaTask.ExecutionState.Enqueued).ToArray(); + foreach (TrangaTask task in tmp) { task.state = TrangaTask.ExecutionState.Enqueued; - if(task.task == TrangaTask.Task.UpdateLibraries) - ExecuteTaskNow(task); - else + switch (task.task) { - logger?.WriteLine(this.GetType().ToString(), $"Task due: {task}"); - _taskQueue[GetConnector(task.connectorName!)].Add(task); + case TrangaTask.Task.DownloadNewChapters: + if (!_allTasks.Any(taskQuery => taskQuery.task == TrangaTask.Task.DownloadNewChapters && + taskQuery.state is TrangaTask.ExecutionState.Running && + ((DownloadChapterTask)taskQuery).connectorName == ((DownloadNewChaptersTask)task).connectorName)) + { + ExecuteTaskNow(task); + } + break; + case TrangaTask.Task.DownloadChapter: + if (!_allTasks.Any(taskQuery => + taskQuery.task == TrangaTask.Task.DownloadChapter && + taskQuery.state is TrangaTask.ExecutionState.Running && + ((DownloadChapterTask)taskQuery).connectorName == + ((DownloadChapterTask)task).connectorName)) + { + ExecuteTaskNow(task); + } + break; + case TrangaTask.Task.UpdateLibraries: + ExecuteTaskNow(task); + break; } } + if(allTasksWaitingLength != _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting)) + ExportDataAndSettings(); + allTasksWaitingLength = _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting); Thread.Sleep(1000); } } @@ -125,9 +128,7 @@ public class TaskManager /// Task to execute public void ExecuteTaskNow(TrangaTask task) { - if (!this._allTasks.Contains(task)) - return; - + task.state = TrangaTask.ExecutionState.Running; Task t = new(() => { task.Execute(this, this.logger); @@ -135,55 +136,67 @@ public class TaskManager t.Start(); } - /// - /// Creates and adds a new Task to the task-Collection - /// - /// TrangaTask.Task to later execute - /// Name of the connector to use - /// Publication to execute Task on, can be null in case of unrelated Task - /// Time-Interval between Executions - /// language, should Task require parameter. Can be empty - /// Is thrown when connectorName is not a available Connector - public TrangaTask AddTask(TrangaTask.Task task, string? connectorName, Publication? publication, TimeSpan reoccurrence, - string language = "") + public void AddTask(TrangaTask newTask) { - logger?.WriteLine(this.GetType().ToString(), $"Adding new Task {task} {connectorName} {publication?.sortName}"); + logger?.WriteLine(this.GetType().ToString(), $"Adding new Task {newTask}"); - TrangaTask? newTask = null; - if (task == TrangaTask.Task.UpdateLibraries) + switch (newTask.task) { - newTask = new UpdateLibrariesTask(task, reoccurrence); - logger?.WriteLine(this.GetType().ToString(), $"Removing old {task}-Task."); - //Only one UpdateKomgaLibrary Task - _allTasks.RemoveWhere(trangaTask => trangaTask.task is TrangaTask.Task.UpdateLibraries); - _allTasks.Add(newTask); - logger?.WriteLine(this.GetType().ToString(), $"Added new Task {newTask}"); - }else if (task == TrangaTask.Task.DownloadNewChapters) - { - //Get appropriate Connector from available Connectors for TrangaTask - Connector? connector = _connectors.FirstOrDefault(c => c.name == connectorName); - if (connectorName is null) - throw new ArgumentException($"connectorName can not be null for task {task}"); - - if (publication is null) - throw new ArgumentException($"publication can not be null for task {task}"); - Publication pub = (Publication)publication; - newTask = new DownloadNewChaptersTask(task, connector!.name, pub, reoccurrence, language); - - if (!_allTasks.Any(trangaTask => - trangaTask.task == task && trangaTask.publication?.internalId == pub.internalId && - trangaTask.connectorName == connector.name)) - { - _allTasks.Add(newTask); - logger?.WriteLine(this.GetType().ToString(), $"Added new Task {newTask}"); - } - else - logger?.WriteLine(this.GetType().ToString(), $"Task already exists {newTask}"); + case TrangaTask.Task.UpdateLibraries: + //Only one UpdateKomgaLibrary Task + logger?.WriteLine(this.GetType().ToString(), $"Removing old {newTask.task}-Task."); + _allTasks.RemoveWhere(trangaTask => trangaTask.task is TrangaTask.Task.UpdateLibraries); + break; + case TrangaTask.Task.DownloadNewChapters: + IEnumerable matchingdnc = + _allTasks.Where(mTask => mTask.GetType() == typeof(DownloadNewChaptersTask)); + if (matchingdnc.All(mTask => + ((DownloadNewChaptersTask)mTask).publication.internalId != ((DownloadNewChaptersTask)newTask).publication.publicationId && + ((DownloadNewChaptersTask)mTask).connectorName != ((DownloadNewChaptersTask)newTask).connectorName)) + _allTasks.Add(newTask); + else + logger?.WriteLine(this.GetType().ToString(), $"Task already exists {newTask}"); + break; + case TrangaTask.Task.DownloadChapter: + IEnumerable matchingdc = + _allTasks.Where(mTask => mTask.GetType() == typeof(DownloadChapterTask)); + if (!matchingdc.Any(mTask => + ((DownloadChapterTask)mTask).publication.internalId == ((DownloadChapterTask)newTask).publication.internalId && + ((DownloadChapterTask)mTask).connectorName == ((DownloadChapterTask)newTask).connectorName && + ((DownloadChapterTask)mTask).chapter.sortNumber == ((DownloadChapterTask)newTask).chapter.sortNumber)) + _allTasks.Add(newTask); + else + logger?.WriteLine(this.GetType().ToString(), $"Task already exists {newTask}"); + break; } ExportDataAndSettings(); + } - if (newTask is null) - throw new Exception("Invalid path"); + public void DeleteTask(TrangaTask removeTask) + { + logger?.WriteLine(this.GetType().ToString(), $"Removing Task {removeTask}"); + _allTasks.Remove(removeTask); + } + + public TrangaTask? AddTask(TrangaTask.Task taskType, string? connectorName, string? publicationId, + TimeSpan reoccurrenceTime, string? language = "en") + { + TrangaTask? newTask = null; + switch (taskType) + { + case TrangaTask.Task.UpdateLibraries: + newTask = new UpdateLibrariesTask(taskType, reoccurrenceTime); + break; + case TrangaTask.Task.DownloadNewChapters: + if(connectorName is null || publicationId is null || language is null) + logger?.WriteLine(this.GetType().ToString(), $"Values connectorName, publicationName and language can not be null."); + GetConnector(connectorName); //Check if connectorName is valid + Publication publication = GetAllPublications().First(pub => pub.internalId == publicationId); + newTask = new DownloadNewChaptersTask(taskType, connectorName!, publication, reoccurrenceTime, language!); + break; + } + if(newTask is not null) + AddTask(newTask); return newTask; } @@ -196,32 +209,81 @@ public class TaskManager public void DeleteTask(TrangaTask.Task task, string? connectorName, Publication? publication) { logger?.WriteLine(this.GetType().ToString(), $"Removing Task {task} {publication?.sortName}"); - if (task == TrangaTask.Task.UpdateLibraries) + + switch (task) { - _allTasks.RemoveWhere(uTask => uTask.task == TrangaTask.Task.UpdateLibraries); - logger?.WriteLine(this.GetType().ToString(), $"Removed Task {task} from all Tasks."); - } - else if (connectorName is null) - throw new ArgumentException($"connectorName can not be null for Task {task}"); - else - { - foreach (List taskQueue in this._taskQueue.Values) - if(taskQueue.RemoveAll(trangaTask => - trangaTask.task == task && trangaTask.connectorName == connectorName && - trangaTask.publication?.internalId == publication?.internalId) > 0) - logger?.WriteLine(this.GetType().ToString(), $"Removed Task {task} {publication?.sortName} {publication?.internalId} from Queue."); - else - logger?.WriteLine(this.GetType().ToString(), $"Task {task} {publication?.sortName} {publication?.internalId} was not in Queue."); - if(_allTasks.RemoveWhere(trangaTask => - trangaTask.task == task && trangaTask.connectorName == connectorName && - trangaTask.publication?.internalId == publication?.internalId) > 0) - logger?.WriteLine(this.GetType().ToString(), $"Removed Task {task} {publication?.sortName} {publication?.internalId} from all Tasks."); - else - logger?.WriteLine(this.GetType().ToString(), $"No Task {task} {publication?.sortName} {publication?.internalId} could be found."); + case TrangaTask.Task.UpdateLibraries: + //Only one UpdateKomgaLibrary Task + logger?.WriteLine(this.GetType().ToString(), $"Removing old {task}-Task."); + _allTasks.RemoveWhere(trangaTask => trangaTask.task is TrangaTask.Task.UpdateLibraries); + break; + case TrangaTask.Task.DownloadNewChapters: + if(connectorName is null || publication is null) + logger?.WriteLine(this.GetType().ToString(), "connectorName and publication can not be null"); + _allTasks.RemoveWhere(mTask => + mTask.GetType() == typeof(DownloadNewChaptersTask) && + ((DownloadNewChaptersTask)mTask).publication.internalId != publication!.Value.publicationId && + ((DownloadNewChaptersTask)mTask).connectorName != connectorName!); + break; } ExportDataAndSettings(); } + public IEnumerable GetTasksMatching(TrangaTask.Task taskType, string? connectorName = null, string? searchString = null, string? publicationId = null) + { + switch (taskType) + { + case TrangaTask.Task.UpdateLibraries: + return _allTasks.Where(tTask => tTask.task == TrangaTask.Task.UpdateLibraries); + case TrangaTask.Task.DownloadNewChapters: + if(connectorName is null) + return _allTasks.Where(tTask => tTask.task == taskType); + GetConnector(connectorName);//Name check + IEnumerable matchingdnc = _allTasks.Where(tTask => tTask.GetType() == typeof(DownloadNewChaptersTask)); + if (searchString is not null) + { + return matchingdnc.Where(mTask => + ((DownloadNewChaptersTask)mTask).connectorName == connectorName && + ((DownloadNewChaptersTask)mTask).ToString().Contains(searchString, StringComparison.InvariantCultureIgnoreCase)); + } + else if (publicationId is not null) + { + return matchingdnc.Where(mTask => + ((DownloadNewChaptersTask)mTask).connectorName == connectorName && + ((DownloadNewChaptersTask)mTask).publication.publicationId == publicationId); + } + else + return _allTasks.Where(tTask => + tTask.GetType() == typeof(DownloadNewChaptersTask) && + ((DownloadNewChaptersTask)tTask).connectorName == connectorName); + + case TrangaTask.Task.DownloadChapter: + if(connectorName is null) + return _allTasks.Where(tTask => tTask.task == taskType); + GetConnector(connectorName);//Name check + IEnumerable matchingdc = _allTasks.Where(tTask => tTask.GetType() == typeof(DownloadChapterTask)); + if (searchString is not null) + { + return matchingdc.Where(mTask => + ((DownloadChapterTask)mTask).connectorName == connectorName && + ((DownloadChapterTask)mTask).ToString().Contains(searchString, StringComparison.InvariantCultureIgnoreCase)); + } + else if (publicationId is not null) + { + return matchingdc.Where(mTask => + ((DownloadChapterTask)mTask).connectorName == connectorName && + ((DownloadChapterTask)mTask).publication.publicationId == publicationId); + } + else + return _allTasks.Where(tTask => + tTask.GetType() == typeof(DownloadChapterTask) && + ((DownloadChapterTask)tTask).connectorName == connectorName); + + default: + return Array.Empty(); + } + } + /// /// Removes a Task from the queue /// @@ -229,8 +291,6 @@ public class TaskManager public void RemoveTaskFromQueue(TrangaTask task) { task.lastExecuted = DateTime.Now; - foreach (List taskList in this._taskQueue.Values) - taskList.Remove(task); task.state = TrangaTask.ExecutionState.Waiting; } @@ -263,7 +323,7 @@ public class TaskManager Publication[] ret = connector.GetPublications(title ?? ""); foreach (Publication publication in ret) { - if(!chapterCollection.Any(pub => pub.Key.sortName == publication.sortName)) + if(chapterCollection.All(pub => pub.Key.internalId != publication.internalId)) this.chapterCollection.TryAdd(publication, new List()); } return ret; @@ -337,14 +397,34 @@ public class TaskManager private void ExportDataAndSettings() { logger?.WriteLine(this.GetType().ToString(), $"Exporting settings to {settings.settingsFilePath}"); + while(IsFileInUse(settings.settingsFilePath)) + Thread.Sleep(50); File.WriteAllText(settings.settingsFilePath, JsonConvert.SerializeObject(settings)); logger?.WriteLine(this.GetType().ToString(), $"Exporting tasks to {settings.tasksFilePath}"); + while(IsFileInUse(settings.tasksFilePath)) + Thread.Sleep(50); File.WriteAllText(settings.tasksFilePath, JsonConvert.SerializeObject(this._allTasks)); logger?.WriteLine(this.GetType().ToString(), $"Exporting known publications to {settings.knownPublicationsPath}"); + while(IsFileInUse(settings.knownPublicationsPath)) + Thread.Sleep(50); File.WriteAllText(settings.knownPublicationsPath, JsonConvert.SerializeObject(this.chapterCollection.Keys.ToArray())); } - + private bool IsFileInUse(string path) + { + if (!File.Exists(path)) + return false; + try + { + using FileStream stream = new (path, FileMode.Open, FileAccess.Read, FileShare.None); + stream.Close(); + } + catch (IOException) + { + return true; + } + return false; + } } \ No newline at end of file diff --git a/Tranga/TrangaTask.cs b/Tranga/TrangaTask.cs index 68a4a6d..4ccd895 100644 --- a/Tranga/TrangaTask.cs +++ b/Tranga/TrangaTask.cs @@ -1,27 +1,28 @@ -using Logging; +using System.Text.Json.Serialization; +using Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Tranga.TrangaTasks; +using JsonConverter = Newtonsoft.Json.JsonConverter; namespace Tranga; /// /// Stores information on Task, when implementing new Tasks also update the serializer /// +[JsonDerivedType(typeof(DownloadNewChaptersTask), 2)] +[JsonDerivedType(typeof(UpdateLibrariesTask), 3)] +[JsonDerivedType(typeof(DownloadChapterTask), 4)] public abstract class TrangaTask { // ReSharper disable once CommentTypo ...Tell me why! // ReSharper disable once MemberCanBePrivate.Global I want it thaaat way public TimeSpan reoccurrence { get; } public DateTime lastExecuted { get; set; } - public string? connectorName { get; } public Task task { get; } - public Publication? publication { get; } - public string? language { get; } - [JsonIgnore]public ExecutionState state { get; set; } - [JsonIgnore] public float progress => (tasksFinished != 0f ? tasksFinished / tasksCount : 0f); - [JsonIgnore]public float tasksCount { get; set; } - [JsonIgnore]public float tasksFinished { get; set; } + [Newtonsoft.Json.JsonIgnore]public ExecutionState state { get; set; } + [Newtonsoft.Json.JsonIgnore]public float progress { get; set; } + [Newtonsoft.Json.JsonIgnore]public DateTime nextExecution => lastExecuted.Add(reoccurrence); public enum ExecutionState { @@ -30,16 +31,12 @@ public abstract class TrangaTask Running }; - protected TrangaTask(Task task, string? connectorName, Publication? publication, TimeSpan reoccurrence, string? language = null) + protected TrangaTask(Task task, TimeSpan reoccurrence) { - this.publication = publication; this.reoccurrence = reoccurrence; this.lastExecuted = DateTime.Now.Subtract(reoccurrence); - this.connectorName = connectorName; this.task = task; - this.language = language; - this.tasksCount = 1; - this.tasksFinished = 0; + this.progress = 0f; } /// @@ -62,24 +59,24 @@ public abstract class TrangaTask this.lastExecuted = DateTime.Now; this.state = ExecutionState.Waiting; logger?.WriteLine(this.GetType().ToString(), $"Finished Executing Task {this}"); - } /// True if elapsed time since last execution is greater than set interval public bool ShouldExecute() { - return DateTime.Now.Subtract(this.lastExecuted) > reoccurrence && state is ExecutionState.Waiting; + return nextExecution < DateTime.Now && state is ExecutionState.Waiting; } public enum Task : byte { DownloadNewChapters = 2, - UpdateLibraries = 3 + UpdateLibraries = 3, + DownloadChapter = 4 } public override string ToString() { - return $"{task}, {lastExecuted}, {reoccurrence}, {state} {(connectorName is not null ? $", {connectorName}" : "" )} {(publication is not null ? $", {progress:00.00}%" : "")} {(publication is not null ? $", {publication?.sortName}" : "")}"; + return $"{task}, {lastExecuted}, {reoccurrence}, {state}, {progress:P2}"; } public class TrangaTaskJsonConverter : JsonConverter @@ -97,6 +94,9 @@ public abstract class TrangaTask if (jo["task"]!.Value() == (Int64)Task.UpdateLibraries) return jo.ToObject(serializer)!; + + if (jo["task"]!.Value() == (Int64)Task.DownloadChapter) + return jo.ToObject(serializer)!; throw new Exception(); } diff --git a/Tranga/TrangaTasks/DownloadChapterTask.cs b/Tranga/TrangaTasks/DownloadChapterTask.cs new file mode 100644 index 0000000..38584c7 --- /dev/null +++ b/Tranga/TrangaTasks/DownloadChapterTask.cs @@ -0,0 +1,31 @@ +using Logging; + +namespace Tranga.TrangaTasks; + +public class DownloadChapterTask : TrangaTask +{ + public string connectorName { get; } + public Publication publication { get; } + public string language { get; } + public Chapter chapter { get; } + public DownloadChapterTask(Task task, string connectorName, Publication publication, Chapter chapter, string language = "en") : base(task, TimeSpan.Zero) + { + this.chapter = chapter; + this.connectorName = connectorName; + this.publication = publication; + this.language = language; + } + + protected override void ExecuteTask(TaskManager taskManager, Logger? logger) + { + Publication pub = (Publication)this.publication!; + Connector connector = taskManager.GetConnector(this.connectorName); + connector.DownloadChapter(pub, this.chapter, this); + taskManager.DeleteTask(this); + } + + public override string ToString() + { + return $"{base.ToString()}, {connectorName}, {publication.sortName} {publication.internalId}, Vol.{chapter.volumeNumber} Ch.{chapter.chapterNumber}"; + } +} \ No newline at end of file diff --git a/Tranga/TrangaTasks/DownloadNewChaptersTask.cs b/Tranga/TrangaTasks/DownloadNewChaptersTask.cs index 62bd6f7..ec1e15a 100644 --- a/Tranga/TrangaTasks/DownloadNewChaptersTask.cs +++ b/Tranga/TrangaTasks/DownloadNewChaptersTask.cs @@ -4,26 +4,37 @@ namespace Tranga.TrangaTasks; public class DownloadNewChaptersTask : TrangaTask { - public DownloadNewChaptersTask(Task task, string connectorName, Publication publication, TimeSpan reoccurrence, string language = "en") : base(task, connectorName, publication, reoccurrence, language) + public string connectorName { get; } + public Publication publication { get; } + public string language { get; } + public DownloadNewChaptersTask(Task task, string connectorName, Publication publication, TimeSpan reoccurrence, string language = "en") : base(task, reoccurrence) { + this.connectorName = connectorName; + this.publication = publication; + this.language = language; } protected override void ExecuteTask(TaskManager taskManager, Logger? logger) { - Publication pub = (Publication)this.publication!; + Publication pub = publication!; Connector connector = taskManager.GetConnector(this.connectorName); - + this.progress = 0.1f; + //Check if Publication already has a Folder pub.CreatePublicationFolder(taskManager.settings.downloadLocation); - List newChapters = UpdateChapters(connector, pub, language!, ref taskManager.chapterCollection); - this.tasksCount = newChapters.Count; + this.progress = 0.2f; + List newChapters = GetNewChaptersList(connector, pub, language!, ref taskManager.chapterCollection); + this.progress = 0.6f; connector.CopyCoverFromCacheToDownloadLocation(pub, taskManager.settings); + this.progress = 0.7f; pub.SaveSeriesInfoJson(connector.downloadLocation); + this.progress = 0.8f; - foreach(Chapter newChapter in newChapters) - connector.DownloadChapter(pub, newChapter, this); + foreach (Chapter newChapter in newChapters) + taskManager.AddTask(new DownloadChapterTask(Task.DownloadChapter, this.connectorName!, pub, newChapter, this.language)); + this.progress = 1f; } /// @@ -34,7 +45,7 @@ public class DownloadNewChaptersTask : TrangaTask /// Language to receive chapters for /// /// List of Chapters that were previously not in collection - private static List UpdateChapters(Connector connector, Publication publication, string language, ref Dictionary> chapterCollection) + private static List GetNewChaptersList(Connector connector, Publication publication, string language, ref Dictionary> chapterCollection) { List newChaptersList = new(); chapterCollection.TryAdd(publication, newChaptersList); //To ensure publication is actually in collection @@ -44,4 +55,9 @@ public class DownloadNewChaptersTask : TrangaTask return newChaptersList; } + + public override string ToString() + { + return $"{base.ToString()}, {connectorName}, {publication.sortName} {publication.internalId}"; + } } \ No newline at end of file diff --git a/Tranga/TrangaTasks/UpdateLibrariesTask.cs b/Tranga/TrangaTasks/UpdateLibrariesTask.cs index 86841d5..ef94a13 100644 --- a/Tranga/TrangaTasks/UpdateLibrariesTask.cs +++ b/Tranga/TrangaTasks/UpdateLibrariesTask.cs @@ -4,7 +4,7 @@ namespace Tranga.TrangaTasks; public class UpdateLibrariesTask : TrangaTask { - public UpdateLibrariesTask(Task task, TimeSpan reoccurrence) : base(task, null, null, reoccurrence) + public UpdateLibrariesTask(Task task, TimeSpan reoccurrence) : base(task, reoccurrence) { } @@ -12,5 +12,6 @@ public class UpdateLibrariesTask : TrangaTask { foreach(LibraryManager lm in taskManager.settings.libraryManagers) lm.UpdateLibrary(); + this.progress = 1f; } } \ No newline at end of file diff --git a/Website/interaction.js b/Website/interaction.js index 47d3e29..d424886 100644 --- a/Website/interaction.js +++ b/Website/interaction.js @@ -302,10 +302,12 @@ function ShowRunningTasks(event){ .then(json => { tagTasksPopupContent.replaceChildren(); json.forEach(task => { - console.log(task); if(task.publication != null){ var taskname = document.createElement("footer-tag-task-name"); - taskname.innerText = task.publication.sortName; + if(task.task == 2) + taskname.innerText = `${task.publication.sortName} - ${task.progress.toLocaleString(undefined,{style: 'percent', minimumFractionDigits:2})}`; + else if(task.task == 4) + taskname.innerText = `${task.publication.sortName} Vol.${task.chapter.volumeNumber} Ch.${task.chapter.chapterNumber} - ${task.progress.toLocaleString(undefined,{style: 'percent', minimumFractionDigits:2})}`; tagTasksPopupContent.appendChild(taskname); } }); @@ -322,7 +324,10 @@ function ShowQueuedTasks(event){ tagTasksPopupContent.replaceChildren(); json.forEach(task => { var taskname = document.createElement("footer-tag-task-name"); - taskname.innerText = task.publication.sortName; + if(task.task == 2) + taskname.innerText = `${task.publication.sortName}`; + else if(task.task == 4) + taskname.innerText = `${task.publication.sortName} Vol.${task.chapter.volumeNumber} Ch.${task.chapter.chapterNumber}`; tagTasksPopupContent.appendChild(taskname); }); if(json.length > 0){ diff --git a/Website/style.css b/Website/style.css index 43d6792..4fbee8a 100644 --- a/Website/style.css +++ b/Website/style.css @@ -500,7 +500,7 @@ footer-tag-content{ } footer-tag-content > * { - margin: 2px 0; + margin: 2px 5px; } footer-tag-popup::before{