using Logging; using Newtonsoft.Json; using Tranga.Connectors; using Tranga.TrangaTasks; namespace Tranga; /// <summary> /// Manages all TrangaTasks. /// Provides a Threaded environment to execute Tasks, and still manage the Task-Collection /// </summary> public class TaskManager { public Dictionary<Publication, List<Chapter>> chapterCollection = new(); private HashSet<TrangaTask> _allTasks; private bool _continueRunning = true; private readonly Connector[] _connectors; private readonly Dictionary<Connector, List<TrangaTask>> _taskQueue = new(); public TrangaSettings settings { get; } private Logger? logger { get; } public Komga? komga => settings.komga; /// <param name="downloadFolderPath">Local path to save data (Manga) to</param> /// <param name="workingDirectory">Path to the working directory</param> /// <param name="imageCachePath">Path to the cover-image cache</param> /// <param name="komgaBaseUrl">The Url of the Komga-instance that you want to update</param> /// <param name="komgaUsername">The Komga username</param> /// <param name="komgaPassword">The Komga password</param> /// <param name="logger"></param> public TaskManager(string downloadFolderPath, string workingDirectory, string imageCachePath, string? komgaBaseUrl = null, string? komgaUsername = null, string? komgaPassword = null, Logger? logger = null) { this.logger = logger; _allTasks = new HashSet<TrangaTask>(); Komga? newKomga = null; if (komgaBaseUrl != null && komgaUsername != null && komgaPassword != null) newKomga = new Komga(komgaBaseUrl, komgaUsername, komgaPassword, logger); this.settings = new TrangaSettings(downloadFolderPath, workingDirectory, newKomga); ExportDataAndSettings(); this._connectors = new Connector[]{ new MangaDex(downloadFolderPath, imageCachePath, logger) }; foreach(Connector cConnector in this._connectors) _taskQueue.Add(cConnector, new List<TrangaTask>()); Thread taskChecker = new(TaskCheckerThread); taskChecker.Start(); } public void UpdateSettings(string? downloadLocation, string? komgaUrl, string? komgaAuth) { if (komgaUrl is not null && komgaAuth is not null && komgaUrl.Length > 0 && komgaAuth.Length > 0) settings.komga = new Komga(komgaUrl, komgaAuth, null); if (downloadLocation is not null && downloadLocation.Length > 0) settings.downloadLocation = downloadLocation; ExportDataAndSettings(); } public TaskManager(TrangaSettings settings, Logger? logger = null) { this.logger = logger; this._connectors = new Connector[]{ new MangaDex(settings.downloadLocation, settings.coverImageCache, logger) }; foreach(Connector cConnector in this._connectors) _taskQueue.Add(cConnector, new List<TrangaTask>()); _allTasks = new HashSet<TrangaTask>(); this.settings = settings; ImportData(); ExportDataAndSettings(); Thread taskChecker = new(TaskCheckerThread); taskChecker.Start(); } /// <summary> /// Runs continuously until shutdown. /// Checks if tasks have to be executed (time elapsed) /// </summary> private void TaskCheckerThread() { logger?.WriteLine(this.GetType().ToString(), "Starting TaskCheckerThread."); while (_continueRunning) { //Check if previous tasks have finished and execute new tasks foreach (KeyValuePair<Connector, List<TrangaTask>> 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())) { task.state = TrangaTask.ExecutionState.Enqueued; if(task.task == TrangaTask.Task.UpdateKomgaLibrary) ExecuteTaskNow(task); else { logger?.WriteLine(this.GetType().ToString(), $"Task due: {task}"); _taskQueue[GetConnector(task.connectorName!)].Add(task); } } Thread.Sleep(1000); } } /// <summary> /// Forces the execution of a given task /// </summary> /// <param name="task">Task to execute</param> public void ExecuteTaskNow(TrangaTask task) { if (!this._allTasks.Contains(task)) return; logger?.WriteLine(this.GetType().ToString(), $"Forcing Execution: {task}"); Task t = new(() => { task.Execute(this); }); t.Start(); } /// <summary> /// Creates and adds a new Task to the task-Collection /// </summary> /// <param name="task">TrangaTask.Task to later execute</param> /// <param name="connectorName">Name of the connector to use</param> /// <param name="publication">Publication to execute Task on, can be null in case of unrelated Task</param> /// <param name="reoccurrence">Time-Interval between Executions</param> /// <param name="language">language, should Task require parameter. Can be empty</param> /// <exception cref="ArgumentException">Is thrown when connectorName is not a available Connector</exception> public TrangaTask AddTask(TrangaTask.Task task, string? connectorName, Publication? publication, TimeSpan reoccurrence, string language = "") { logger?.WriteLine(this.GetType().ToString(), $"Adding new Task {task} {connectorName} {publication?.sortName}"); TrangaTask? newTask = null; if (task == TrangaTask.Task.UpdateKomgaLibrary) { newTask = new UpdateKomgaLibraryTask(task, reoccurrence); logger?.WriteLine(this.GetType().ToString(), $"Removing old {task}-Task."); //Only one UpdateKomgaLibrary Task _allTasks.RemoveWhere(trangaTask => trangaTask.task is TrangaTask.Task.UpdateKomgaLibrary); _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}"); } ExportDataAndSettings(); if (newTask is null) throw new Exception("Invalid path"); return newTask; } /// <summary> /// Removes Task from task-collection /// </summary> /// <param name="task">TrangaTask.Task type</param> /// <param name="connectorName">Name of Connector that was used</param> /// <param name="publication">Publication that was used</param> public void DeleteTask(TrangaTask.Task task, string? connectorName, Publication? publication) { logger?.WriteLine(this.GetType().ToString(), $"Removing Task {task} {publication?.sortName}"); if (task == TrangaTask.Task.UpdateKomgaLibrary) { _allTasks.RemoveWhere(uTask => uTask.task == TrangaTask.Task.UpdateKomgaLibrary); 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<TrangaTask> 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."); } ExportDataAndSettings(); } /// <summary> /// Removes a Task from the queue /// </summary> /// <param name="task"></param> public void RemoveTaskFromQueue(TrangaTask task) { task.lastExecuted = DateTime.Now; foreach (List<TrangaTask> taskList in this._taskQueue.Values) taskList.Remove(task); task.state = TrangaTask.ExecutionState.Waiting; } /// <summary> /// Sets last execution time to start of time /// Let taskManager handle enqueuing /// </summary> /// <param name="task"></param> public void AddTaskToQueue(TrangaTask task) { task.lastExecuted = DateTime.UnixEpoch; } /// <returns>All available Connectors</returns> public Dictionary<string, Connector> GetAvailableConnectors() { return this._connectors.ToDictionary(connector => connector.name, connector => connector); } /// <returns>All TrangaTasks in task-collection</returns> public TrangaTask[] GetAllTasks() { TrangaTask[] ret = new TrangaTask[_allTasks.Count]; _allTasks.CopyTo(ret); return ret; } public Publication[] GetPublicationsFromConnector(Connector connector, string? title = null) { Publication[] ret = connector.GetPublications(title ?? ""); foreach (Publication publication in ret) { if(!chapterCollection.Any(pub => pub.Key.sortName == publication.sortName)) this.chapterCollection.TryAdd(publication, new List<Chapter>()); } return ret; } /// <returns>All added Publications</returns> public Publication[] GetAllPublications() { return this.chapterCollection.Keys.ToArray(); } /// <summary> /// Return Connector with given Name /// </summary> /// <param name="connectorName">Connector-name (exact)</param> /// <exception cref="Exception">If Connector is not available</exception> public Connector GetConnector(string? connectorName) { if(connectorName is null) throw new Exception($"connectorName can not be null"); Connector? ret = this._connectors.FirstOrDefault(connector => connector.name == connectorName); if (ret is null) throw new Exception($"Connector {connectorName} is not an available Connector."); return ret; } /// <summary> /// Shuts down the taskManager. /// </summary> /// <param name="force">If force is true, tasks are aborted.</param> public void Shutdown(bool force = false) { logger?.WriteLine(this.GetType().ToString(), $"Shutting down (forced={force})"); _continueRunning = false; ExportDataAndSettings(); if(force) Environment.Exit(_allTasks.Count(task => task.state is TrangaTask.ExecutionState.Enqueued or TrangaTask.ExecutionState.Running)); //Wait for tasks to finish while(_allTasks.Any(task => task.state is TrangaTask.ExecutionState.Running or TrangaTask.ExecutionState.Enqueued)) Thread.Sleep(10); logger?.WriteLine(this.GetType().ToString(), "Tasks finished. Bye!"); Environment.Exit(0); } private void ImportData() { logger?.WriteLine(this.GetType().ToString(), "Importing Data"); string buffer; if (File.Exists(settings.tasksFilePath)) { logger?.WriteLine(this.GetType().ToString(), $"Importing tasks from {settings.tasksFilePath}"); buffer = File.ReadAllText(settings.tasksFilePath); this._allTasks = JsonConvert.DeserializeObject<HashSet<TrangaTask>>(buffer, new JsonSerializerSettings() { Converters = { new TrangaTask.TrangaTaskJsonConverter() } })!; } if (File.Exists(settings.knownPublicationsPath)) { logger?.WriteLine(this.GetType().ToString(), $"Importing known publications from {settings.knownPublicationsPath}"); buffer = File.ReadAllText(settings.knownPublicationsPath); Publication[] publications = JsonConvert.DeserializeObject<Publication[]>(buffer)!; foreach (Publication publication in publications) this.chapterCollection.TryAdd(publication, new List<Chapter>()); } } /// <summary> /// Exports data (settings, tasks) to file /// </summary> private void ExportDataAndSettings() { logger?.WriteLine(this.GetType().ToString(), $"Exporting settings to {settings.settingsFilePath}"); File.WriteAllText(settings.settingsFilePath, JsonConvert.SerializeObject(settings)); logger?.WriteLine(this.GetType().ToString(), $"Exporting tasks to {settings.tasksFilePath}"); File.WriteAllText(settings.tasksFilePath, JsonConvert.SerializeObject(this._allTasks)); logger?.WriteLine(this.GetType().ToString(), $"Exporting known publications to {settings.knownPublicationsPath}"); File.WriteAllText(settings.knownPublicationsPath, JsonConvert.SerializeObject(this.chapterCollection.Keys.ToArray())); } }