using Newtonsoft.Json; using Tranga.Connectors; namespace Tranga; /// <summary> /// Manages all TrangaTasks. /// Provides a Threaded environment to execute Tasks, and still manage the Task-Collection /// </summary> public class TaskManager { private readonly Dictionary<Publication, List<Chapter>> _chapterCollection = new(); private readonly HashSet<TrangaTask> _allTasks; private bool _continueRunning = true; private readonly Connector[] _connectors; private Dictionary<Connector, List<TrangaTask>> tasksToExecute = new(); private string downloadLocation { get; } public Komga? komga { get; private set; } /// <param name="folderPath">Local path to save data (Manga) to</param> /// <param name="komgaBaseUrl">The Url of the Komga-instance that you want to update</param> public TaskManager(string folderPath, string? komgaBaseUrl = null, string? komgaUsername = null, string? komgaPassword = null) { this.downloadLocation = folderPath; if (komgaBaseUrl != null && komgaUsername != null && komgaPassword != null) this.komga = new Komga(komgaBaseUrl, komgaUsername, komgaPassword); this._connectors = new Connector[]{ new MangaDex(folderPath) }; foreach(Connector cConnector in this._connectors) tasksToExecute.Add(cConnector, new List<TrangaTask>()); _allTasks = new HashSet<TrangaTask>(); Thread taskChecker = new(TaskCheckerThread); taskChecker.Start(); } public TaskManager(SettingsData settings) { this._connectors = new Connector[]{ new MangaDex(settings.downloadLocation) }; foreach(Connector cConnector in this._connectors) tasksToExecute.Add(cConnector, new List<TrangaTask>()); this.downloadLocation = settings.downloadLocation; this.komga = settings.komga; _allTasks = settings.allTasks; Thread taskChecker = new(TaskCheckerThread); taskChecker.Start(); } private void TaskCheckerThread() { while (_continueRunning) { foreach (KeyValuePair<Connector, List<TrangaTask>> connectorTaskQueue in tasksToExecute) { connectorTaskQueue.Value.RemoveAll(task => task.state == TrangaTask.ExecutionState.Waiting); if (connectorTaskQueue.Value.Count > 0 && !connectorTaskQueue.Value.All(task => task.state is TrangaTask.ExecutionState.Running or TrangaTask.ExecutionState.Waiting)) ExecuteTaskNow(connectorTaskQueue.Value.First()); } foreach (TrangaTask task in _allTasks.Where(aTask => aTask.ShouldExecute())) { task.state = TrangaTask.ExecutionState.Enqueued; if(task.connectorName is null) ExecuteTaskNow(task); else { tasksToExecute[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; Task t = new Task(() => { TaskExecutor.Execute(this, task, this._chapterCollection); }); 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 = "") { if (task != TrangaTask.Task.UpdateKomgaLibrary && connectorName is null) throw new ArgumentException($"connectorName can not be null for task {task}"); TrangaTask newTask; if (task == TrangaTask.Task.UpdateKomgaLibrary) { newTask = new TrangaTask(task, null, null, reoccurrence, language); //Check if same task already exists // ReSharper disable once SimplifyLinqExpressionUseAll readabilty if (!_allTasks.Any(trangaTask => trangaTask.task == task)) { _allTasks.Add(newTask); } } else { //Get appropriate Connector from available Connectors for TrangaTask Connector? connector = _connectors.FirstOrDefault(c => c.name == connectorName); if (connector is null) throw new ArgumentException($"Connector {connectorName} is not a known connector."); newTask = new TrangaTask(task, connector.name, publication, reoccurrence, language); //Check if same task already exists if (!_allTasks.Any(trangaTask => trangaTask.task == task && trangaTask.connectorName == connector.name && trangaTask.publication?.downloadUrl == publication?.downloadUrl)) { if(task != TrangaTask.Task.UpdatePublications) _chapterCollection.Add((Publication)publication!, new List<Chapter>()); _allTasks.Add(newTask); } } ExportData(Directory.GetCurrentDirectory()); 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 RemoveTask(TrangaTask.Task task, string connectorName, Publication? publication) { _allTasks.RemoveWhere(trangaTask => trangaTask.task == task && trangaTask.connectorName == connectorName && trangaTask.publication?.downloadUrl == publication?.downloadUrl); ExportData(Directory.GetCurrentDirectory()); } /// <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; } /// <returns>All added Publications</returns> public Publication[] GetAllPublications() { return this._chapterCollection.Keys.ToArray(); } public Connector GetConnector(string connectorName) { Connector? ret = this._connectors.FirstOrDefault(connector => connector.name == connectorName); if (ret is null) throw new Exception($"Connector {connectorName} is not an available Connector."); return (Connector)ret!; } /// <summary> /// Shuts down the taskManager. /// </summary> /// <param name="force">If force is true, tasks are aborted.</param> public void Shutdown(bool force = false) { _continueRunning = false; ExportData(Directory.GetCurrentDirectory()); 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); Environment.Exit(0); } public static SettingsData ImportData(string importFolderPath) { string importPath = Path.Join(importFolderPath, "data.json"); if (!File.Exists(importPath)) return new SettingsData("", null, new HashSet<TrangaTask>()); string toRead = File.ReadAllText(importPath); SettingsData data = JsonConvert.DeserializeObject<SettingsData>(toRead)!; return data; } private void ExportData(string exportFolderPath) { SettingsData data = new SettingsData(this.downloadLocation, this.komga, this._allTasks); string exportPath = Path.Join(exportFolderPath, "data.json"); string serializedData = JsonConvert.SerializeObject(data); File.WriteAllText(exportPath, serializedData); } public class SettingsData { public string downloadLocation { get; set; } public Komga? komga { get; set; } public HashSet<TrangaTask> allTasks { get; } public SettingsData(string downloadLocation, Komga? komga, HashSet<TrangaTask> allTasks) { this.downloadLocation = downloadLocation; this.komga = komga; this.allTasks = allTasks; } } }