using Logging; using Newtonsoft.Json; using Tranga.Connectors; using Tranga.LibraryManagers; 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(); /// Local path to save data (Manga) to /// Path to the working directory /// Path to the cover-image cache /// /// public TaskManager(string downloadFolderPath, string workingDirectory, string imageCachePath, HashSet libraryManagers, Logger? logger = null) { this.logger = logger; this.settings = new TrangaSettings(downloadFolderPath, workingDirectory, libraryManagers); ExportDataAndSettings(); this._connectors = new Connector[] { new MangaDex(downloadFolderPath, imageCachePath, logger), new Manganato(downloadFolderPath, imageCachePath, logger), new Mangasee(downloadFolderPath, imageCachePath, logger) }; Thread taskChecker = new(TaskCheckerThread); taskChecker.Start(); } public void UpdateSettings(string? downloadLocation, string? komgaUrl, string? komgaAuth, string? kavitaUrl, string? kavitaUsername, string? kavitaPassword) { if (komgaUrl is not null && komgaAuth is not null && komgaUrl.Length > 0 && komgaAuth.Length > 0) { settings.libraryManagers.RemoveWhere(lm => lm.GetType() == typeof(Komga)); settings.libraryManagers.Add(new Komga(komgaUrl, komgaAuth, logger)); } if (kavitaUrl is not null && kavitaUsername is not null && kavitaPassword is not null && kavitaUrl.Length > 0 && kavitaUsername.Length > 0 && kavitaPassword.Length > 0) { settings.libraryManagers.RemoveWhere(lm => lm.GetType() == typeof(Kavita)); settings.libraryManagers.Add(new Kavita(kavitaUrl, kavitaUsername, kavitaPassword, logger)); } 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), 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 allTasksWaitingLength = _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting); while (_continueRunning) { 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; switch (task.task) { case TrangaTask.Task.DownloadNewChapters: if (!_allTasks.Any(taskQuery => taskQuery.task == TrangaTask.Task.DownloadNewChapters && taskQuery.state is TrangaTask.ExecutionState.Running && ((DownloadNewChaptersTask)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; } } HashSet toRemove = new(); foreach (KeyValuePair removeTask in _runningDownloadChapterTasks) { if (removeTask.Key.GetType() == typeof(DownloadChapterTask) && DateTime.Now.Subtract(removeTask.Key.executionStarted) > TimeSpan.FromMinutes(10))//TODO better way to check if task has failed? { logger?.WriteLine(this.GetType().ToString(), $"Removing failed task {removeTask}."); removeTask.Value.Dispose(); DeleteTask(removeTask.Key); AddTask(new DownloadChapterTask(removeTask.Key.task, removeTask.Key.connectorName, removeTask.Key.publication, removeTask.Key.chapter, removeTask.Key.language, removeTask.Key.parentTask)); toRemove.Add(removeTask.Key); } } foreach (DownloadChapterTask taskToRemove in toRemove) _runningDownloadChapterTasks.Remove(taskToRemove); 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); } } /// /// Forces the execution of a given task /// /// Task to execute public void ExecuteTaskNow(TrangaTask task) { task.state = TrangaTask.ExecutionState.Running; Task t = new(() => { task.Execute(this, this.logger); }); if(task.GetType() == typeof(DownloadChapterTask)) _runningDownloadChapterTasks.Add((DownloadChapterTask)task, t); 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(), $"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.Any(mTask => ((DownloadNewChaptersTask)mTask).publication.internalId == ((DownloadNewChaptersTask)newTask).publication.internalId && ((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(); } 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? internalId, 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) logger?.WriteLine(this.GetType().ToString(), $"Value connectorName can not be null."); if(internalId is null) logger?.WriteLine(this.GetType().ToString(), $"Value internalId can not be null."); if(language is null) logger?.WriteLine(this.GetType().ToString(), $"Value language can not be null."); if (connectorName is null || internalId is null || language is null) return null; GetConnector(connectorName); //Check if connectorName is valid Publication publication = GetAllPublications().First(pub => pub.internalId == internalId); newTask = new DownloadNewChaptersTask(taskType, connectorName!, publication, reoccurrenceTime, language!); break; } if(newTask is not null) AddTask(newTask); return newTask; } /// /// Removes Task from task-collection /// /// TrangaTask.Task type /// Name of Connector that was used /// Publication that was used public void DeleteTask(TrangaTask.Task task, string? connectorName, string? publicationId) { logger?.WriteLine(this.GetType().ToString(), $"Removing Task {task} {publicationId}"); switch (task) { 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 || publicationId is null) logger?.WriteLine(this.GetType().ToString(), "connectorName and publication can not be null"); else { _allTasks.RemoveWhere(mTask => mTask.GetType() == typeof(DownloadNewChaptersTask) && ((DownloadNewChaptersTask)mTask).publication.internalId == publicationId && ((DownloadNewChaptersTask)mTask).connectorName == connectorName!); _allTasks.RemoveWhere(mTask => mTask.GetType() == typeof(DownloadChapterTask) && ((DownloadChapterTask)mTask).publication.internalId == publicationId && ((DownloadChapterTask)mTask).connectorName == connectorName!); } break; } ExportDataAndSettings(); } public IEnumerable GetTasksMatching(TrangaTask.Task taskType, string? connectorName = null, string? searchString = null, string? internalId = 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 (internalId is not null) { return matchingdnc.Where(mTask => ((DownloadNewChaptersTask)mTask).connectorName == connectorName && ((DownloadNewChaptersTask)mTask).publication.internalId == internalId); } 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 (internalId is not null) { return matchingdc.Where(mTask => ((DownloadChapterTask)mTask).connectorName == connectorName && ((DownloadChapterTask)mTask).publication.publicationId == internalId); } else return _allTasks.Where(tTask => tTask.GetType() == typeof(DownloadChapterTask) && ((DownloadChapterTask)tTask).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(); } /// /// 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() } })!; } 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}"); 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; } }