using Logging;
using Newtonsoft.Json;
using Tranga.Connectors;
namespace Tranga;
///
/// Manages all TrangaTasks.
/// Provides a Threaded environment to execute Tasks, and still manage the Task-Collection
///
public class TaskManager
{
private readonly Dictionary> _chapterCollection = new();
private readonly HashSet _allTasks;
private bool _continueRunning = true;
private readonly Connector[] _connectors;
private Dictionary> tasksToExecute = new();
private string downloadLocation { get; }
private Logger? logger { get; }
public Komga? komga { get; }
/// Local path to save data (Manga) to
/// The Url of the Komga-instance that you want to update
/// The Komga username
/// The Komga password
///
public TaskManager(string folderPath, string? komgaBaseUrl = null, string? komgaUsername = null, string? komgaPassword = null, Logger? logger = null)
{
this.logger = logger;
this.downloadLocation = folderPath;
if (komgaBaseUrl != null && komgaUsername != null && komgaPassword != null)
this.komga = new Komga(komgaBaseUrl, komgaUsername, komgaPassword, logger);
this._connectors = new Connector[]{ new MangaDex(folderPath, logger) };
foreach(Connector cConnector in this._connectors)
tasksToExecute.Add(cConnector, new List());
_allTasks = new HashSet();
Thread taskChecker = new(TaskCheckerThread);
taskChecker.Start();
}
public TaskManager(SettingsData settings, Logger? logger = null)
{
this.logger = logger;
this._connectors = new Connector[]{ new MangaDex(settings.downloadLocation, logger) };
foreach(Connector cConnector in this._connectors)
tasksToExecute.Add(cConnector, new List());
this.downloadLocation = settings.downloadLocation;
this.komga = settings.komga;
_allTasks = settings.allTasks;
Thread taskChecker = new(TaskCheckerThread);
taskChecker.Start();
}
///
/// Runs continuously until shutdown.
/// Checks if tasks have to be executed (time elapsed)
///
private void TaskCheckerThread()
{
while (_continueRunning)
{
//Check if previous tasks have finished and execute new tasks
foreach (KeyValuePair> 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());
}
//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.connectorName is null)
ExecuteTaskNow(task);
else
{
logger?.WriteLine(this.GetType().ToString(), $"Task due: {task}");
tasksToExecute[GetConnector(task.connectorName!)].Add(task);
}
}
Thread.Sleep(1000);
}
}
///
/// Forces the execution of a given task
///
/// Task to execute
public void ExecuteTaskNow(TrangaTask task)
{
if (!this._allTasks.Contains(task))
return;
logger?.WriteLine(this.GetType().ToString(), $"Forcing Execution: {task}");
Task t = new Task(() =>
{
TaskExecutor.Execute(this, task, this._chapterCollection, logger);
});
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 = "")
{
logger?.WriteLine(this.GetType().ToString(), $"Adding new Task");
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());
_allTasks.Add(newTask);
}
}
logger?.WriteLine(this.GetType().ToString(), newTask.ToString());
ExportData(Directory.GetCurrentDirectory());
return newTask;
}
///
/// Removes Task from task-collection
///
/// TrangaTask.Task type
/// Name of Connector that was used
/// Publication that was used
public void RemoveTask(TrangaTask.Task task, string? connectorName, Publication? publication)
{
logger?.WriteLine(this.GetType().ToString(), $"Removing Task {task}");
if (task == TrangaTask.Task.UpdateKomgaLibrary)
_allTasks.RemoveWhere(uTask => uTask.task == TrangaTask.Task.UpdateKomgaLibrary);
else if (connectorName is null)
throw new ArgumentException($"connectorName can not be null for Task {task}");
else
_allTasks.RemoveWhere(trangaTask =>
trangaTask.task == task && trangaTask.connectorName == connectorName &&
trangaTask.publication?.downloadUrl == publication?.downloadUrl);
ExportData(Directory.GetCurrentDirectory());
}
/// 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;
}
/// 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 (Connector)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;
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);
}
///
/// Loads stored data (settings, tasks) from file
///
/// working directory, filename has to be data.json
public static SettingsData LoadData(string importFolderPath)
{
string importPath = Path.Join(importFolderPath, "data.json");
if (!File.Exists(importPath))
return new SettingsData("", null, new HashSet());
string toRead = File.ReadAllText(importPath);
SettingsData data = JsonConvert.DeserializeObject(toRead)!;
return data;
}
///
/// Exports data (settings, tasks) to file
///
/// Folder path, filename will be data.json
private void ExportData(string exportFolderPath)
{
logger?.WriteLine(this.GetType().ToString(), $"Exporting data to data.json");
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 allTasks { get; }
public SettingsData(string downloadLocation, Komga? komga, HashSet allTasks)
{
this.downloadLocation = downloadLocation;
this.komga = komga;
this.allTasks = allTasks;
}
}
}