using Logging; using Newtonsoft.Json; using Tranga.Connectors; using Tranga.TrangaTasks; namespace Tranga; /// /// Manages all TrangaTasks. /// Provides a Threaded environment to execute Tasks, and still manage the Task-Collection /// public class TaskManager { public Dictionary> chapterCollection = new(); private HashSet _allTasks = new(); private bool _continueRunning = true; private readonly Connector[] _connectors; public TrangaSettings settings { get; } private Logger? logger { get; } private readonly Dictionary _runningDownloadChapterTasks = new(); public TaskManager(TrangaSettings settings, Logger? logger = null) { this.logger = logger; this._connectors = new Connector[] { new MangaDex(settings.downloadLocation, settings.coverImageCache, logger), new Manganato(settings.downloadLocation, settings.coverImageCache, logger), new Mangasee(settings.downloadLocation, settings.coverImageCache, logger) }; this.settings = settings; ImportData(); ExportDataAndSettings(); Thread taskChecker = new(TaskCheckerThread); taskChecker.Start(); } /// /// Runs continuously until shutdown. /// Checks if tasks have to be executed (time elapsed) /// private void TaskCheckerThread() { logger?.WriteLine(this.GetType().ToString(), "Starting TaskCheckerThread."); int waitingTasksCount = _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting); while (_continueRunning) { foreach (TrangaTask waitingButExecute in _allTasks.Where(taskQuery => taskQuery.nextExecution < DateTime.Now && taskQuery.state is TrangaTask.ExecutionState.Waiting)) { waitingButExecute.state = TrangaTask.ExecutionState.Enqueued; } foreach (TrangaTask enqueuedTask in _allTasks.Where(enqueuedTask => enqueuedTask.state is TrangaTask.ExecutionState.Enqueued)) { switch (enqueuedTask.task) { case TrangaTask.Task.DownloadChapter: case TrangaTask.Task.MonitorPublication: if (!_allTasks.Any(taskQuery => { if (taskQuery.state is not TrangaTask.ExecutionState.Running) return false; switch (taskQuery) { case DownloadChapterTask dct when enqueuedTask is DownloadChapterTask eDct && dct.connectorName == eDct.connectorName: case MonitorPublicationTask mpt when enqueuedTask is MonitorPublicationTask eMpt && mpt.connectorName == eMpt.connectorName: return true; default: return false; } })) { ExecuteTaskNow(enqueuedTask); } break; case TrangaTask.Task.UpdateLibraries: ExecuteTaskNow(enqueuedTask); break; } } foreach (TrangaTask failedDownloadChapterTask in _allTasks.Where(taskQuery => taskQuery.state is TrangaTask.ExecutionState.Failed && taskQuery is DownloadChapterTask).ToArray()) { DeleteTask(failedDownloadChapterTask); TrangaTask newTask = failedDownloadChapterTask.Clone(); failedDownloadChapterTask.parentTask?.AddChildTask(newTask); AddTask(newTask); } if(waitingTasksCount != _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting)) ExportDataAndSettings(); waitingTasksCount = _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting); Thread.Sleep(1000); } } /// /// Forces the execution of a given task /// /// Task to execute public void ExecuteTaskNow(TrangaTask task) { task.state = TrangaTask.ExecutionState.Running; CancellationTokenSource cToken = new (); Task t = new(() => { task.Execute(this, this.logger, cToken.Token); }, cToken.Token); if(task is DownloadChapterTask chapterTask) _runningDownloadChapterTasks.Add(chapterTask, cToken); t.Start(); } public void AddTask(TrangaTask newTask) { logger?.WriteLine(this.GetType().ToString(), $"Adding new Task {newTask}"); switch (newTask.task) { case TrangaTask.Task.UpdateLibraries: //Only one UpdateKomgaLibrary Task logger?.WriteLine(this.GetType().ToString(), $"Replacing old {newTask.task}-Task."); _allTasks.RemoveWhere(trangaTask => trangaTask.task is TrangaTask.Task.UpdateLibraries); _allTasks.Add(newTask); break; case TrangaTask.Task.MonitorPublication: MonitorPublicationTask mpt = (MonitorPublicationTask)newTask; if(!GetTasksMatching(mpt.task, mpt.connectorName, internalId:mpt.publication.internalId).Any()) _allTasks.Add(newTask); else logger?.WriteLine(this.GetType().ToString(), $"Task already exists {newTask}"); break; case TrangaTask.Task.DownloadChapter: DownloadChapterTask dct = (DownloadChapterTask)newTask; if(!GetTasksMatching(dct.task, dct.connectorName, internalId:dct.publication.internalId, chapterSortNumber:dct.chapter.sortNumber).Any()) _allTasks.Add(newTask); else logger?.WriteLine(this.GetType().ToString(), $"Task already exists {newTask}"); break; } ExportDataAndSettings(); } public void DeleteTask(TrangaTask removeTask) { logger?.WriteLine(this.GetType().ToString(), $"Removing Task {removeTask}"); _allTasks.Remove(removeTask); removeTask.parentTask?.RemoveChildTask(removeTask); if (removeTask is DownloadChapterTask cRemoveTask && _runningDownloadChapterTasks.ContainsKey(cRemoveTask)) { _runningDownloadChapterTasks[cRemoveTask].Cancel(); _runningDownloadChapterTasks.Remove(cRemoveTask); } foreach(TrangaTask childTask in removeTask.childTasks) DeleteTask(childTask); } public IEnumerable GetTasksMatching(TrangaTask.Task taskType, string? connectorName = null, string? searchString = null, string? internalId = null, string? chapterSortNumber = null) { switch (taskType) { case TrangaTask.Task.UpdateLibraries: return _allTasks.Where(tTask => tTask.task == TrangaTask.Task.UpdateLibraries); case TrangaTask.Task.MonitorPublication: if(connectorName is null) return _allTasks.Where(tTask => tTask.task == taskType); GetConnector(connectorName);//Name check if (searchString is not null) { return _allTasks.Where(mTask => mTask is MonitorPublicationTask mpt && mpt.connectorName == connectorName && mpt.ToString().Contains(searchString, StringComparison.InvariantCultureIgnoreCase)); } else if (internalId is not null) { return _allTasks.Where(mTask => mTask is MonitorPublicationTask mpt && mpt.connectorName == connectorName && string.Concat(settings.CleanIdRex.Matches(mpt.publication.internalId)) == string.Concat(settings.CleanIdRex.Matches(internalId))); } else return _allTasks.Where(tTask => tTask is MonitorPublicationTask mpt && mpt.connectorName == connectorName); case TrangaTask.Task.DownloadChapter: if(connectorName is null) return _allTasks.Where(tTask => tTask.task == taskType); GetConnector(connectorName);//Name check if (searchString is not null) { return _allTasks.Where(mTask => mTask is DownloadChapterTask dct && dct.connectorName == connectorName && dct.ToString().Contains(searchString, StringComparison.InvariantCultureIgnoreCase)); } else if (internalId is not null && chapterSortNumber is not null) { return _allTasks.Where(mTask => mTask is DownloadChapterTask dct && dct.connectorName == connectorName && string.Concat(settings.CleanIdRex.Matches(dct.publication.internalId)) == string.Concat(settings.CleanIdRex.Matches(internalId)) && dct.chapter.sortNumber == chapterSortNumber); } else return _allTasks.Where(mTask => mTask is DownloadChapterTask dct && dct.connectorName == connectorName); default: return Array.Empty(); } } /// /// Removes a Task from the queue /// /// public void RemoveTaskFromQueue(TrangaTask task) { task.lastExecuted = DateTime.Now; task.state = TrangaTask.ExecutionState.Waiting; } /// /// Sets last execution time to start of time /// Let taskManager handle enqueuing /// /// public void AddTaskToQueue(TrangaTask task) { task.lastExecuted = DateTime.UnixEpoch; } /// All available Connectors public Dictionary GetAvailableConnectors() { return this._connectors.ToDictionary(connector => connector.name, connector => connector); } /// All TrangaTasks in task-collection 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.All(pub => pub.Key.internalId != publication.internalId)) this.chapterCollection.TryAdd(publication, new List()); } return ret; } /// All added Publications public Publication[] GetAllPublications() { return this.chapterCollection.Keys.ToArray(); } /// /// Updates the available Chapters of a Publication /// /// Connector to use /// Publication to check /// Language to receive chapters for /// List of Chapters that were previously not in collection public List GetNewChaptersList(Connector connector, Publication publication, string language) { List newChaptersList = new(); chapterCollection.TryAdd(publication, newChaptersList); //To ensure publication is actually in collection Chapter[] newChapters = connector.GetChapters(publication, language); newChaptersList = newChapters.Where(nChapter => !connector.CheckChapterIsDownloaded(publication, nChapter)).ToList(); return newChaptersList; } public List GetExistingChaptersList(Connector connector, Publication publication, string language) { Chapter[] newChapters = connector.GetChapters(publication, language); return newChapters.Where(nChapter => connector.CheckChapterIsDownloaded(publication, nChapter)).ToList(); } /// /// Return Connector with given Name /// /// Connector-name (exact) /// If Connector is not available 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; } /// /// Shuts down the taskManager. /// /// If force is true, tasks are aborted. 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>(buffer, new JsonSerializerSettings() { Converters = { new TrangaTask.TrangaTaskJsonConverter() } })!; } foreach (TrangaTask task in this._allTasks.Where(tTask => tTask.parentTaskId is not null)) { TrangaTask? parentTask = this._allTasks.FirstOrDefault(pTask => pTask.taskId == task.parentTaskId); if (parentTask is not null) { task.parentTask = parentTask; parentTask.AddChildTask(task); } } 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(buffer)!; foreach (Publication publication in publications) this.chapterCollection.TryAdd(publication, new List()); } } /// /// Exports data (settings, tasks) to file /// private void ExportDataAndSettings() { logger?.WriteLine(this.GetType().ToString(), $"Exporting settings to {settings.settingsFilePath}"); settings.ExportSettings(); 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; } }