diff --git a/.github/workflows/docker-image-cuttingedge.yml b/.github/workflows/docker-image-cuttingedge.yml
index b36acd2..42c6f76 100644
--- a/.github/workflows/docker-image-cuttingedge.yml
+++ b/.github/workflows/docker-image-cuttingedge.yml
@@ -42,17 +42,4 @@ jobs:
pull: true
push: true
tags: |
- glax/tranga-api:cuttingedge
-
- # https://github.com/docker/build-push-action#multi-platform-image
- - name: Build and push Website
- uses: docker/build-push-action@v4.1.1
- with:
- context: ./Website
- file: ./Website/Dockerfile
- #platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
- platforms: linux/amd64
- pull: true
- push: true
- tags: |
- glax/tranga-website:cuttingedge
+ glax/tranga-api:cuttingedge
\ No newline at end of file
diff --git a/.github/workflows/docker-image-master.yml b/.github/workflows/docker-image-master.yml
index 995c8a2..710587b 100644
--- a/.github/workflows/docker-image-master.yml
+++ b/.github/workflows/docker-image-master.yml
@@ -44,17 +44,4 @@ jobs:
pull: true
push: true
tags: |
- glax/tranga-api:latest
-
- # https://github.com/docker/build-push-action#multi-platform-image
- - name: Build and push Website
- uses: docker/build-push-action@v4.1.1
- with:
- context: ./Website
- file: ./Website/Dockerfile
- #platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
- platforms: linux/amd64
- pull: true
- push: true
- tags: |
- glax/tranga-website:latest
+ glax/tranga-api:latest
\ No newline at end of file
diff --git a/CLI/CLI.csproj b/CLI/CLI.csproj
new file mode 100644
index 0000000..abf151f
--- /dev/null
+++ b/CLI/CLI.csproj
@@ -0,0 +1,18 @@
+
+
+
+ Exe
+ net7.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CLI/Program.cs b/CLI/Program.cs
new file mode 100644
index 0000000..9543773
--- /dev/null
+++ b/CLI/Program.cs
@@ -0,0 +1,155 @@
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using Logging;
+using Spectre.Console;
+using Spectre.Console.Cli;
+using Tranga;
+
+var app = new CommandApp();
+return app.Run(args);
+
+internal sealed class TrangaCli : Command
+{
+ public sealed class Settings : CommandSettings
+ {
+ [Description("Directory to which downloaded Manga are saved")]
+ [CommandOption("-d|--downloadLocation")]
+ [DefaultValue(null)]
+ public string? downloadLocation { get; init; }
+
+ [Description("Directory in which application-data is saved")]
+ [CommandOption("-w|--workingDirectory")]
+ [DefaultValue(null)]
+ public string? workingDirectory { get; init; }
+
+ [Description("Enables the file-logger")]
+ [CommandOption("-f")]
+ [DefaultValue(null)]
+ public bool? fileLogger { get; init; }
+
+ [Description("Path to save logfile to")]
+ [CommandOption("-l|--fPath")]
+ [DefaultValue(null)]
+ public string? fileLoggerPath { get; init; }
+
+ [Description("Port on which to run API on")]
+ [CommandOption("-p|--port")]
+ [DefaultValue(null)]
+ public int? apiPort { get; init; }
+ }
+
+ public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings)
+ {
+ List enabledLoggers = new();
+ if(settings.fileLogger is true)
+ enabledLoggers.Add(Logger.LoggerType.FileLogger);
+
+ string? logFilePath = settings.fileLoggerPath ?? "";
+ Logger logger = new(enabledLoggers.ToArray(), Console.Out, Console.OutputEncoding, logFilePath);
+
+ TrangaSettings trangaSettings = new (settings.downloadLocation, settings.workingDirectory, settings.apiPort);
+
+ Directory.CreateDirectory(trangaSettings.downloadLocation);
+ Directory.CreateDirectory(trangaSettings.workingDirectory);
+
+ Tranga.Tranga? api = null;
+
+ Thread trangaApi = new Thread(() =>
+ {
+ api = new(logger, trangaSettings);
+ });
+ trangaApi.Start();
+
+ HttpClient client = new();
+
+ bool exit = false;
+ while (!exit)
+ {
+ string menuSelect = AnsiConsole.Prompt(
+ new SelectionPrompt()
+ .Title("Menu")
+ .PageSize(10)
+ .MoreChoicesText("Up/Down")
+ .AddChoices(new[]
+ {
+ "CustomRequest",
+ "Log",
+ "Exit"
+ }));
+
+ switch (menuSelect)
+ {
+ case "CustomRequest":
+ HttpMethod requestMethod = AnsiConsole.Prompt(
+ new SelectionPrompt()
+ .Title("Request Type")
+ .AddChoices(new[]
+ {
+ HttpMethod.Get,
+ HttpMethod.Delete,
+ HttpMethod.Post
+ }));
+ string requestPath = AnsiConsole.Prompt(
+ new TextPrompt("Request Path:"));
+ List> parameters = new();
+ while (AnsiConsole.Confirm("Add Parameter?"))
+ {
+ string name = AnsiConsole.Ask("Parameter Name:");
+ string value = AnsiConsole.Ask("Parameter Value:");
+ parameters.Add(new ValueTuple(name, value));
+ }
+
+ string requestString = $"http://localhost:{trangaSettings.apiPortNumber}/{requestPath}";
+ if (parameters.Any())
+ {
+ requestString += "?";
+ foreach (ValueTuple parameter in parameters)
+ requestString += $"{parameter.Item1}={parameter.Item2}&";
+ }
+
+ HttpRequestMessage request = new (requestMethod, requestString);
+ AnsiConsole.WriteLine($"Request: {request.Method} {request.RequestUri}");
+ HttpResponseMessage response;
+ if (AnsiConsole.Confirm("Send Request?"))
+ response = client.Send(request);
+ else break;
+ AnsiConsole.WriteLine($"Response: {(int)response.StatusCode} {response.StatusCode}");
+ AnsiConsole.WriteLine(response.Content.ReadAsStringAsync().Result);
+ break;
+ case "Log":
+ List lines = logger.Tail(10).ToList();
+ Rows rows = new Rows(lines.Select(line => new Text(line)));
+
+ AnsiConsole.Live(rows).Start(context =>
+ {
+ bool running = true;
+ while (running)
+ {
+ string[] newLines = logger.GetNewLines();
+ if (newLines.Length > 0)
+ {
+ lines.AddRange(newLines);
+ rows = new Rows(lines.Select(line => new Text(line)));
+ context.UpdateTarget(rows);
+ }
+ Thread.Sleep(100);
+ if (AnsiConsole.Console.Input.IsKeyAvailable())
+ {
+ AnsiConsole.Console.Input.ReadKey(true); //Do not process input
+ running = false;
+ }
+ }
+ });
+ break;
+ case "Exit":
+ exit = true;
+ break;
+ }
+ }
+
+ if (api is not null)
+ api.keepRunning = false;
+
+ return 0;
+ }
+}
\ No newline at end of file
diff --git a/Logging/Logger.cs b/Logging/Logger.cs
index 9ca3d28..a4ed3d5 100644
--- a/Logging/Logger.cs
+++ b/Logging/Logger.cs
@@ -1,9 +1,13 @@
-using System.Text;
+using System.Runtime.InteropServices;
+using System.Text;
namespace Logging;
public class Logger : TextWriter
{
+ private static readonly string LogDirectoryPath = RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
+ ? "/var/log/tranga-api"
+ : Path.Join(Directory.GetCurrentDirectory(), "logs");
public override Encoding Encoding { get; }
public enum LoggerType
{
@@ -17,13 +21,14 @@ public class Logger : TextWriter
public Logger(LoggerType[] enabledLoggers, TextWriter? stdOut, Encoding? encoding, string? logFilePath)
{
- this.Encoding = encoding ?? Encoding.ASCII;
+ this.Encoding = encoding ?? Encoding.UTF8;
if (enabledLoggers.Contains(LoggerType.FileLogger) && logFilePath is not null)
_fileLogger = new FileLogger(logFilePath, encoding);
- else
+ else if(enabledLoggers.Contains(LoggerType.FileLogger) && logFilePath is null)
{
- _fileLogger = null;
- throw new ArgumentException($"logFilePath can not be null for LoggerType {LoggerType.FileLogger}");
+ logFilePath = Path.Join(LogDirectoryPath,
+ $"{DateTime.Now.ToShortDateString()} {DateTime.Now.ToShortTimeString()}.log");
+ _fileLogger = new FileLogger(logFilePath, encoding);
}
if (enabledLoggers.Contains(LoggerType.ConsoleLogger) && stdOut is not null)
diff --git a/Logging/MemoryLogger.cs b/Logging/MemoryLogger.cs
index 47581f2..2921cbf 100644
--- a/Logging/MemoryLogger.cs
+++ b/Logging/MemoryLogger.cs
@@ -6,6 +6,7 @@ public class MemoryLogger : LoggerBase
{
private readonly SortedList _logMessages = new();
private int _lastLogMessageIndex = 0;
+ private bool _lockLogMessages = false;
public MemoryLogger(Encoding? encoding = null) : base(encoding)
{
@@ -14,8 +15,13 @@ public class MemoryLogger : LoggerBase
protected override void Write(LogMessage value)
{
- while(!_logMessages.TryAdd(value.logTime, value))
- Thread.Sleep(10);
+ if (!_lockLogMessages)
+ {
+ _lockLogMessages = true;
+ while(!_logMessages.TryAdd(DateTime.Now, value))
+ Thread.Sleep(10);
+ _lockLogMessages = false;
+ }
}
public string[] GetLogMessage()
@@ -35,7 +41,12 @@ public class MemoryLogger : LoggerBase
for (int retIndex = 0; retIndex < ret.Length; retIndex++)
{
- ret[retIndex] = _logMessages.GetValueAtIndex(_logMessages.Count - retLength + retIndex).ToString();
+ if (!_lockLogMessages)
+ {
+ _lockLogMessages = true;
+ ret[retIndex] = _logMessages.GetValueAtIndex(_logMessages.Count - retLength + retIndex).ToString();
+ _lockLogMessages = false;
+ }
}
_lastLogMessageIndex = _logMessages.Count - 1;
@@ -45,14 +56,27 @@ public class MemoryLogger : LoggerBase
public string[] GetNewLines()
{
int logMessageCount = _logMessages.Count;
- string[] ret = new string[logMessageCount - _lastLogMessageIndex];
+ List ret = new();
- for (int retIndex = 0; retIndex < ret.Length; retIndex++)
+ int retIndex = 0;
+ for (; retIndex < logMessageCount - _lastLogMessageIndex; retIndex++)
{
- ret[retIndex] = _logMessages.GetValueAtIndex(_lastLogMessageIndex + retIndex).ToString();
+ try
+ {
+ if (!_lockLogMessages)
+ {
+ _lockLogMessages = true;
+ ret.Add(_logMessages.GetValueAtIndex(_lastLogMessageIndex + retIndex).ToString());
+ _lockLogMessages = false;
+ }
+ }
+ catch (NullReferenceException e)//Called when LogMessage has not finished writing
+ {
+ break;
+ }
}
- _lastLogMessageIndex = logMessageCount;
- return ret;
+ _lastLogMessageIndex = _lastLogMessageIndex + retIndex;
+ return ret.ToArray();
}
}
\ No newline at end of file
diff --git a/Tranga.sln b/Tranga.sln
index 78a7590..501c3c3 100644
--- a/Tranga.sln
+++ b/Tranga.sln
@@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tranga", ".\Tranga\Tranga.c
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logging", "Logging\Logging.csproj", "{415BE889-BB7D-426F-976F-8D977876A462}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CLI", "CLI\CLI.csproj", "{4324C816-F9D2-468F-8ED6-397FE2F0DCB3}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -18,5 +20,9 @@ Global
{415BE889-BB7D-426F-976F-8D977876A462}.Debug|Any CPU.Build.0 = Debug|Any CPU
{415BE889-BB7D-426F-976F-8D977876A462}.Release|Any CPU.ActiveCfg = Release|Any CPU
{415BE889-BB7D-426F-976F-8D977876A462}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4324C816-F9D2-468F-8ED6-397FE2F0DCB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4324C816-F9D2-468F-8ED6-397FE2F0DCB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4324C816-F9D2-468F-8ED6-397FE2F0DCB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4324C816-F9D2-468F-8ED6-397FE2F0DCB3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
diff --git a/Tranga.sln.DotSettings b/Tranga.sln.DotSettings
index 7f9a86f..1162c8f 100644
--- a/Tranga.sln.DotSettings
+++ b/Tranga.sln.DotSettings
@@ -2,7 +2,9 @@
True
True
True
+ True
True
+ True
True
True
True
diff --git a/Tranga/API/RequestHandler.cs b/Tranga/API/RequestHandler.cs
deleted file mode 100644
index 62f7f5c..0000000
--- a/Tranga/API/RequestHandler.cs
+++ /dev/null
@@ -1,365 +0,0 @@
-using System.Globalization;
-using System.Net;
-using System.Text.RegularExpressions;
-using Tranga;
-using Tranga.Connectors;
-using Tranga.TrangaTasks;
-
-namespace Tranga.API;
-
-public class RequestHandler
-{
- private TaskManager _taskManager;
- private Server _parent;
-
- private List> _validRequestPaths = new()
- {
- new(HttpMethod.Get, "/", Array.Empty()),
- new(HttpMethod.Get, "/Connectors", Array.Empty()),
- new(HttpMethod.Get, "/Publications/Known", new[] { "internalId?" }),
- new(HttpMethod.Get, "/Publications/FromConnector", new[] { "connectorName", "title" }),
- new(HttpMethod.Get, "/Publications/Chapters",
- new[] { "connectorName", "internalId", "onlyNew?", "onlyExisting?", "language?" }),
- new(HttpMethod.Get, "/Tasks/Types", Array.Empty()),
- new(HttpMethod.Post, "/Tasks/CreateMonitorTask",
- new[] { "connectorName", "internalId", "reoccurrenceTime", "language?", "ignoreChaptersBelow?" }),
- new(HttpMethod.Post, "/Tasks/CreateDownloadChaptersTask",
- new[] { "connectorName", "internalId", "chapters", "language?" }),
- new(HttpMethod.Get, "/Tasks", new[] { "taskType", "connectorName?", "publicationId?" }),
- new(HttpMethod.Delete, "/Tasks", new[] { "taskType", "connectorName?", "searchString?" }),
- new(HttpMethod.Get, "/Tasks/Progress",
- new[] { "taskType", "connectorName", "publicationId", "chapterSortNumber?" }),
- new(HttpMethod.Post, "/Tasks/Start", new[] { "taskType", "connectorName?", "internalId?" }),
- new(HttpMethod.Get, "/Tasks/RunningTasks", Array.Empty()),
- new(HttpMethod.Get, "/Queue/List", Array.Empty()),
- new(HttpMethod.Post, "/Queue/Enqueue", new[] { "taskType", "connectorName?", "publicationId?" }),
- new(HttpMethod.Delete, "/Queue/Dequeue", new[] { "taskType", "connectorName?", "publicationId?" }),
- new(HttpMethod.Get, "/Settings", Array.Empty()),
- new(HttpMethod.Post, "/Settings/Update", new[]
- {
- "downloadLocation?", "komgaUrl?", "komgaAuth?", "kavitaUrl?", "kavitaUsername?",
- "kavitaPassword?", "gotifyUrl?", "gotifyAppToken?", "lunaseaWebhook?"
- })
- };
-
- public RequestHandler(TaskManager taskManager, Server parent)
- {
- this._taskManager = taskManager;
- this._parent = parent;
- }
-
- internal void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
- {
- string requestPath = request.Url!.LocalPath;
- if (requestPath.Contains("favicon"))
- {
- _parent.SendResponse(HttpStatusCode.NoContent, response);
- return;
- }
- if (!this._validRequestPaths.Any(path => path.Item1.Method == request.HttpMethod && path.Item2 == requestPath))
- {
- _parent.SendResponse(HttpStatusCode.BadRequest, response);
- return;
- }
- Dictionary variables = GetRequestVariables(request.Url!.Query);
- object? responseObject = null;
- switch (request.HttpMethod)
- {
- case "GET":
- responseObject = this.HandleGet(requestPath, variables);
- break;
- case "POST":
- this.HandlePost(requestPath, variables);
- break;
- case "DELETE":
- this.HandleDelete(requestPath, variables);
- break;
- }
- _parent.SendResponse(HttpStatusCode.OK, response, responseObject);
- }
-
- private Dictionary GetRequestVariables(string query)
- {
- Dictionary ret = new();
- Regex queryRex = new (@"\?{1}&?([A-z0-9-=]+=[A-z0-9-=]+)+(&[A-z0-9-=]+=[A-z0-9-=]+)*");
- if (!queryRex.IsMatch(query))
- return ret;
- query = query.Substring(1);
- foreach (string kvpair in query.Split('&').Where(str => str.Length >= 3))
- {
- string var = kvpair.Split('=')[0];
- string val = Regex.Replace(kvpair.Substring(var.Length + 1), "%20", " ");
- val = Regex.Replace(val, "%[0-9]{2}", "");
- ret.Add(var, val);
- }
- return ret;
- }
-
- private void HandleDelete(string requestPath, Dictionary variables)
- {
- switch (requestPath)
- {
- case "/Tasks":
- variables.TryGetValue("taskType", out string? taskType1);
- variables.TryGetValue("connectorName", out string? connectorName1);
- variables.TryGetValue("publicationId", out string? publicationId1);
- if(taskType1 is null)
- return;
-
- try
- {
- TrangaTask.Task task = Enum.Parse(taskType1);
- foreach(TrangaTask tTask in _taskManager.GetTasksMatching(task, connectorName1, internalId: publicationId1))
- _taskManager.DeleteTask(tTask);
- }
- catch (ArgumentException)
- {
- return;
- }
- break;
- case "/Queue/Dequeue":
- variables.TryGetValue("taskType", out string? taskType2);
- variables.TryGetValue("connectorName", out string? connectorName2);
- variables.TryGetValue("publicationId", out string? publicationId2);
- if(taskType2 is null)
- return;
-
- try
- {
- TrangaTask.Task pTask = Enum.Parse(taskType2);
- TrangaTask? task = _taskManager
- .GetTasksMatching(pTask, connectorName: connectorName2, internalId: publicationId2).FirstOrDefault();
-
- if (task is null)
- return;
- _taskManager.RemoveTaskFromQueue(task);
- }
- catch (ArgumentException)
- {
- return;
- }
- break;
- }
- }
-
- private void HandlePost(string requestPath, Dictionary variables)
- {
- switch (requestPath)
- {
-
- case "/Tasks/CreateMonitorTask":
- variables.TryGetValue("connectorName", out string? connectorName1);
- variables.TryGetValue("internalId", out string? internalId1);
- variables.TryGetValue("reoccurrenceTime", out string? reoccurrenceTime1);
- variables.TryGetValue("language", out string? language1);
- variables.TryGetValue("ignoreChaptersBelow", out string? minChapter);
- if (connectorName1 is null || internalId1 is null || reoccurrenceTime1 is null)
- return;
- Connector? connector1 =
- _taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName1).Value;
- if (connector1 is null)
- return;
- Publication? publication1 = _taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == internalId1);
- if (!publication1.HasValue)
- return;
- Publication pPublication1 = (Publication)publication1;
- if (minChapter is not null)
- pPublication1.ignoreChaptersBelow = float.Parse(minChapter,new NumberFormatInfo() { NumberDecimalSeparator = "." });
- _taskManager.AddTask(new MonitorPublicationTask(connectorName1, pPublication1, TimeSpan.Parse(reoccurrenceTime1), language1 ?? "en"));
- break;
- case "/Tasks/CreateDownloadChaptersTask":
- variables.TryGetValue("connectorName", out string? connectorName2);
- variables.TryGetValue("internalId", out string? internalId2);
- variables.TryGetValue("chapters", out string? chapters);
- variables.TryGetValue("language", out string? language2);
- if (connectorName2 is null || internalId2 is null || chapters is null)
- return;
- Connector? connector2 =
- _taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName2).Value;
- if (connector2 is null)
- return;
- Publication? publication2 = _taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == internalId2);
- if (publication2 is null)
- return;
-
- IEnumerable toDownload = connector2.SelectChapters((Publication)publication2, chapters, language2 ?? "en");
- foreach(Chapter chapter in toDownload)
- _taskManager.AddTask(new DownloadChapterTask(connectorName2, (Publication)publication2, chapter, "en"));
- break;
- case "/Tasks/Start":
- variables.TryGetValue("taskType", out string? taskType1);
- variables.TryGetValue("connectorName", out string? connectorName3);
- variables.TryGetValue("internalId", out string? internalId3);
- if (taskType1 is null)
- return;
- try
- {
- TrangaTask.Task pTask = Enum.Parse(taskType1);
- TrangaTask? task = _taskManager
- .GetTasksMatching(pTask, connectorName: connectorName3, internalId: internalId3).FirstOrDefault();
-
- if (task is null)
- return;
- _taskManager.ExecuteTaskNow(task);
- }
- catch (ArgumentException)
- {
- return;
- }
- break;
- case "/Queue/Enqueue":
- variables.TryGetValue("taskType", out string? taskType2);
- variables.TryGetValue("connectorName", out string? connectorName4);
- variables.TryGetValue("publicationId", out string? publicationId);
- if (taskType2 is null)
- return;
- try
- {
- TrangaTask.Task pTask = Enum.Parse(taskType2);
- TrangaTask? task = _taskManager
- .GetTasksMatching(pTask, connectorName: connectorName4, internalId: publicationId).FirstOrDefault();
-
- if (task is null)
- return;
- _taskManager.AddTaskToQueue(task);
- }
- catch (ArgumentException)
- {
- return;
- }
- break;
- case "/Settings/Update":
- variables.TryGetValue("downloadLocation", out string? downloadLocation);
- variables.TryGetValue("komgaUrl", out string? komgaUrl);
- variables.TryGetValue("komgaAuth", out string? komgaAuth);
- variables.TryGetValue("kavitaUrl", out string? kavitaUrl);
- variables.TryGetValue("kavitaUsername", out string? kavitaUsername);
- variables.TryGetValue("kavitaPassword", out string? kavitaPassword);
- variables.TryGetValue("gotifyUrl", out string? gotifyUrl);
- variables.TryGetValue("gotifyAppToken", out string? gotifyAppToken);
- variables.TryGetValue("lunaseaWebhook", out string? lunaseaWebhook);
-
- if (downloadLocation is not null && downloadLocation.Length > 0)
- _taskManager.settings.UpdateSettings(TrangaSettings.UpdateField.DownloadLocation, downloadLocation);
- if (komgaUrl is not null && komgaAuth is not null && komgaUrl.Length > 5 && komgaAuth.Length > 0)
- _taskManager.settings.UpdateSettings(TrangaSettings.UpdateField.Komga, komgaUrl, komgaAuth);
- if (kavitaUrl is not null && kavitaPassword is not null && kavitaUsername is not null && kavitaUrl.Length > 5 &&
- kavitaUsername.Length > 0 && kavitaPassword.Length > 0)
- _taskManager.settings.UpdateSettings(TrangaSettings.UpdateField.Kavita, kavitaUrl, kavitaUsername,
- kavitaPassword);
- if (gotifyUrl is not null && gotifyAppToken is not null && gotifyUrl.Length > 5 && gotifyAppToken.Length > 0)
- _taskManager.settings.UpdateSettings(TrangaSettings.UpdateField.Gotify, gotifyUrl, gotifyAppToken);
- if(lunaseaWebhook is not null && lunaseaWebhook.Length > 5)
- _taskManager.settings.UpdateSettings(TrangaSettings.UpdateField.LunaSea, lunaseaWebhook);
- break;
- }
- }
-
- private object? HandleGet(string requestPath, Dictionary variables)
- {
- switch (requestPath)
- {
- case "/Connectors":
- return this._taskManager.GetAvailableConnectors().Keys.ToArray();
- case "/Publications/Known":
- variables.TryGetValue("internalId", out string? internalId1);
- if(internalId1 is null)
- return _taskManager.GetAllPublications();
- return new [] { _taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == internalId1) };
- case "/Publications/FromConnector":
- variables.TryGetValue("connectorName", out string? connectorName1);
- variables.TryGetValue("title", out string? title);
- if (connectorName1 is null || title is null)
- return null;
- Connector? connector1 = _taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName1).Value;
- if (connector1 is null)
- return null;
- if(title.Length < 4)
- return null;
- return connector1.GetPublications(ref _taskManager.collection, title);
- case "/Publications/Chapters":
- string[] yes = { "true", "yes", "1", "y" };
- variables.TryGetValue("connectorName", out string? connectorName2);
- variables.TryGetValue("internalId", out string? internalId2);
- variables.TryGetValue("onlyNew", out string? onlyNew);
- variables.TryGetValue("onlyExisting", out string? onlyExisting);
- variables.TryGetValue("language", out string? language);
- if (connectorName2 is null || internalId2 is null)
- return null;
- bool newOnly = onlyNew is not null && yes.Contains(onlyNew);
- bool existingOnly = onlyExisting is not null && yes.Contains(onlyExisting);
-
- Connector? connector2 = _taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName2).Value;
- if (connector2 is null)
- return null;
- Publication? publication = _taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == internalId2);
- if (publication is null)
- return null;
-
- if(newOnly)
- return connector2.GetNewChaptersList((Publication)publication, language??"en", ref _taskManager.collection).ToArray();
- else if (existingOnly)
- return _taskManager.GetExistingChaptersList(connector2, (Publication)publication, language ?? "en").ToArray();
- else
- return connector2.GetChapters((Publication)publication, language??"en");
- case "/Tasks/Types":
- return Enum.GetNames(typeof(TrangaTask.Task));
- case "/Tasks":
- variables.TryGetValue("taskType", out string? taskType1);
- variables.TryGetValue("connectorName", out string? connectorName3);
- variables.TryGetValue("searchString", out string? searchString);
- if (taskType1 is null)
- return null;
- try
- {
- TrangaTask.Task task = Enum.Parse(taskType1);
- return _taskManager.GetTasksMatching(task, connectorName:connectorName3, searchString:searchString);
- }
- catch (ArgumentException)
- {
- return null;
- }
- case "/Tasks/Progress":
- variables.TryGetValue("taskType", out string? taskType2);
- variables.TryGetValue("connectorName", out string? connectorName4);
- variables.TryGetValue("publicationId", out string? publicationId);
- variables.TryGetValue("chapterNumber", out string? chapterNumber);
- if (taskType2 is null || connectorName4 is null || publicationId is null)
- return null;
- Connector? connector =
- _taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName4).Value;
- if (connector is null)
- return null;
- try
- {
- TrangaTask? task = null;
- TrangaTask.Task pTask = Enum.Parse(taskType2);
- if (pTask is TrangaTask.Task.MonitorPublication)
- {
- task = _taskManager.GetTasksMatching(pTask, connectorName: connectorName4, internalId: publicationId).FirstOrDefault();
- }else if (pTask is TrangaTask.Task.DownloadChapter && chapterNumber is not null)
- {
- task = _taskManager.GetTasksMatching(pTask, connectorName: connectorName4, internalId: publicationId,
- chapterNumber: chapterNumber).FirstOrDefault();
- }
- if (task is null)
- return null;
-
- return task.progress;
- }
- catch (ArgumentException)
- {
- return null;
- }
- case "/Tasks/RunningTasks":
- return _taskManager.GetAllTasks().Where(task => task.state is TrangaTask.ExecutionState.Running);
- case "/Queue/List":
- return _taskManager.GetAllTasks().Where(task => task.state is TrangaTask.ExecutionState.Enqueued).OrderBy(task => task.nextExecution);
- case "/Settings":
- return _taskManager.settings;
- case "/":
- default:
- return this._validRequestPaths;
- }
- }
-}
\ No newline at end of file
diff --git a/Tranga/API/Server.cs b/Tranga/API/Server.cs
deleted file mode 100644
index 8d19bc4..0000000
--- a/Tranga/API/Server.cs
+++ /dev/null
@@ -1,92 +0,0 @@
-using System.Net;
-using System.Runtime.InteropServices;
-using System.Text;
-using System.Text.RegularExpressions;
-using Logging;
-using Newtonsoft.Json;
-using Tranga;
-
-namespace Tranga.API;
-
-public class Server
-{
- private readonly HttpListener _listener = new ();
- private readonly RequestHandler _requestHandler;
- private readonly TaskManager _taskManager;
- internal readonly Logger? logger;
-
- private readonly Regex _validUrl =
- new (@"https?:\/\/(www\.)?[-A-z0-9]{1,256}(\.[-a-zA-Z0-9]{1,6})?(:[0-9]{1,5})?(\/{1}[A-z0-9()@:%_\+.~#?&=]+)*\/?");
- public Server(int port, TaskManager taskManager, Logger? logger = null)
- {
- this.logger = logger;
- this._taskManager = taskManager;
- if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
- this._listener.Prefixes.Add($"http://*:{port}/");
- else
- this._listener.Prefixes.Add($"http://localhost:{port}/");
- this._requestHandler = new RequestHandler(taskManager, this);
- Thread listenThread = new Thread(Listen);
- listenThread.Start();
- }
-
- private void Listen()
- {
- this._listener.Start();
- foreach (string prefix in this._listener.Prefixes)
- this.logger?.WriteLine(this.GetType().ToString(), $"Listening on {prefix}");
- while (this._listener.IsListening && _taskManager._continueRunning)
- {
- HttpListenerContext context = this._listener.GetContextAsync().Result;
- Task t = new (() =>
- {
- HandleContext(context);
- });
- t.Start();
- }
- }
-
- private void HandleContext(HttpListenerContext context)
- {
- HttpListenerRequest request = context.Request;
- HttpListenerResponse response = context.Response;
- //logger?.WriteLine(this.GetType().ToString(), $"New request: {request.HttpMethod} {request.Url}");
-
- if (!_validUrl.IsMatch(request.Url!.ToString()))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- return;
- }
-
- if (request.HttpMethod == "OPTIONS")
- {
- SendResponse(HttpStatusCode.OK, response);
- }
- else
- {
- _requestHandler.HandleRequest(request, response);
- }
- }
-
- internal void SendResponse(HttpStatusCode statusCode, HttpListenerResponse response, object? content = null)
- {
- //logger?.WriteLine(this.GetType().ToString(), $"Sending response: {statusCode}");
- response.StatusCode = (int)statusCode;
- response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With");
- response.AddHeader("Access-Control-Allow-Methods", "GET, POST, DELETE");
- response.AddHeader("Access-Control-Max-Age", "1728000");
- response.AppendHeader("Access-Control-Allow-Origin", "*");
- response.ContentType = "application/json";
- try
- {
- response.OutputStream.Write(content is not null
- ? Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content))
- : Array.Empty());
- response.OutputStream.Close();
- }
- catch (HttpListenerException)
- {
-
- }
- }
-}
\ No newline at end of file
diff --git a/Tranga/Chapter.cs b/Tranga/Chapter.cs
index 0559f40..4af7722 100644
--- a/Tranga/Chapter.cs
+++ b/Tranga/Chapter.cs
@@ -10,7 +10,7 @@ namespace Tranga;
public readonly struct Chapter
{
// ReSharper disable once MemberCanBePrivate.Global
- public Publication parentPublication { get; }
+ public Manga parentManga { get; }
public string? name { get; }
public string? volumeNumber { get; }
public string chapterNumber { get; }
@@ -20,9 +20,9 @@ public readonly struct Chapter
private static readonly Regex LegalCharacters = new (@"([A-z]*[0-9]* *\.*-*,*\]*\[*'*\'*\)*\(*~*!*)*");
private static readonly Regex IllegalStrings = new(@"Vol(ume)?.?", RegexOptions.IgnoreCase);
- public Chapter(Publication parentPublication, string? name, string? volumeNumber, string chapterNumber, string url)
+ public Chapter(Manga parentManga, string? name, string? volumeNumber, string chapterNumber, string url)
{
- this.parentPublication = parentPublication;
+ this.parentManga = parentManga;
this.name = name;
this.volumeNumber = volumeNumber;
this.chapterNumber = chapterNumber;
@@ -35,8 +35,12 @@ public readonly struct Chapter
chNameStr = IllegalStrings.Replace(chNameStr, "");
this.fileName = $"{volStr}{chNumberStr}{chNameStr}";
}
-
-
+
+ public override string ToString()
+ {
+ return $"Chapter {parentManga.sortName} {parentManga.internalId} {chapterNumber} {name}";
+ }
+
///
/// Checks if a chapter-archive is already present
///
@@ -44,9 +48,9 @@ public readonly struct Chapter
internal bool CheckChapterIsDownloaded(string downloadLocation)
{
string newFilePath = GetArchiveFilePath(downloadLocation);
- if (!Directory.Exists(Path.Join(downloadLocation, parentPublication.folderName)))
+ if (!Directory.Exists(Path.Join(downloadLocation, parentManga.folderName)))
return false;
- FileInfo[] archives = new DirectoryInfo(Path.Join(downloadLocation, parentPublication.folderName)).GetFiles();
+ FileInfo[] archives = new DirectoryInfo(Path.Join(downloadLocation, parentManga.folderName)).GetFiles();
Regex chapterInfoRex = new(@"Ch\.[0-9.]+");
Regex chapterRex = new(@"[0-9]+(\.[0-9]+)?");
@@ -67,7 +71,7 @@ public readonly struct Chapter
/// Filepath
internal string GetArchiveFilePath(string downloadLocation)
{
- return Path.Join(downloadLocation, parentPublication.folderName, $"{parentPublication.folderName} - {this.fileName}.cbz");
+ return Path.Join(downloadLocation, parentManga.folderName, $"{parentManga.folderName} - {this.fileName}.cbz");
}
///
@@ -78,10 +82,10 @@ public readonly struct Chapter
internal string GetComicInfoXmlString()
{
XElement comicInfo = new XElement("ComicInfo",
- new XElement("Tags", string.Join(',', parentPublication.tags)),
- new XElement("LanguageISO", parentPublication.originalLanguage),
+ new XElement("Tags", string.Join(',', parentManga.tags)),
+ new XElement("LanguageISO", parentManga.originalLanguage),
new XElement("Title", this.name),
- new XElement("Writer", string.Join(',', parentPublication.authors)),
+ new XElement("Writer", string.Join(',', parentManga.authors)),
new XElement("Volume", this.volumeNumber),
new XElement("Number", this.chapterNumber)
);
diff --git a/Tranga/CommonObjects.cs b/Tranga/CommonObjects.cs
deleted file mode 100644
index 0d9c5f4..0000000
--- a/Tranga/CommonObjects.cs
+++ /dev/null
@@ -1,125 +0,0 @@
-using Logging;
-using Newtonsoft.Json;
-using Tranga.LibraryManagers;
-using Tranga.NotificationManagers;
-
-namespace Tranga;
-
-public class CommonObjects
-{
- public HashSet libraryManagers { get; init; }
- public HashSet notificationManagers { get; init; }
- [JsonIgnore]public Logger? logger { get; set; }
- [JsonIgnore]private string settingsFilePath { get; init; }
-
- public CommonObjects(HashSet? libraryManagers, HashSet? notificationManagers, Logger? logger, string settingsFilePath)
- {
- this.libraryManagers = libraryManagers??new();
- this.notificationManagers = notificationManagers??new();
- this.logger = logger;
- this.settingsFilePath = settingsFilePath;
- }
-
- public static CommonObjects LoadSettings(string settingsFilePath, Logger? logger)
- {
- if (!File.Exists(settingsFilePath))
- return new CommonObjects(null, null, logger, settingsFilePath);
-
- string toRead = File.ReadAllText(settingsFilePath);
- TrangaSettings.SettingsJsonObject settings = JsonConvert.DeserializeObject(
- toRead,
- new JsonSerializerSettings
- {
- Converters =
- {
- new NotificationManager.NotificationManagerJsonConverter(),
- new LibraryManager.LibraryManagerJsonConverter()
- }
- })!;
-
- if(settings.co is null)
- return new CommonObjects(null, null, logger, settingsFilePath);
-
- if (logger is not null)
- {
- settings.co.logger = logger;
- foreach (LibraryManager lm in settings.co.libraryManagers)
- lm.AddLogger(logger);
- foreach(NotificationManager nm in settings.co.notificationManagers)
- nm.AddLogger(logger);
- }
-
- return settings.co;
- }
-
- public void ExportSettings()
- {
- TrangaSettings.SettingsJsonObject? settings = null;
- if (File.Exists(settingsFilePath))
- {
- bool inUse = true;
- while (inUse)
- {
- try
- {
- using FileStream stream = new (settingsFilePath, FileMode.Open, FileAccess.Read, FileShare.None);
- stream.Close();
- inUse = false;
- }
- catch (IOException)
- {
- inUse = true;
- Thread.Sleep(50);
- }
- }
- string toRead = File.ReadAllText(settingsFilePath);
- settings = JsonConvert.DeserializeObject(toRead,
- new JsonSerializerSettings
- {
- Converters =
- {
- new NotificationManager.NotificationManagerJsonConverter(),
- new LibraryManager.LibraryManagerJsonConverter()
- }
- });
- }
- settings = new TrangaSettings.SettingsJsonObject(settings?.ts, this);
- File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(settings));
- }
-
- public void UpdateSettings(TrangaSettings.UpdateField field, params string[] values)
- {
- switch (field)
- {
- case TrangaSettings.UpdateField.Komga:
- if (values.Length != 2)
- return;
- libraryManagers.RemoveWhere(lm => lm.GetType() == typeof(Komga));
- libraryManagers.Add(new Komga(values[0], values[1], this.logger));
- break;
- case TrangaSettings.UpdateField.Kavita:
- if (values.Length != 3)
- return;
- libraryManagers.RemoveWhere(lm => lm.GetType() == typeof(Kavita));
- libraryManagers.Add(new Kavita(values[0], values[1], values[2], this.logger));
- break;
- case TrangaSettings.UpdateField.Gotify:
- if (values.Length != 2)
- return;
- notificationManagers.RemoveWhere(nm => nm.GetType() == typeof(Gotify));
- Gotify newGotify = new(values[0], values[1], this.logger);
- notificationManagers.Add(newGotify);
- newGotify.SendNotification("Success!", "Gotify was added to Tranga!");
- break;
- case TrangaSettings.UpdateField.LunaSea:
- if(values.Length != 1)
- return;
- notificationManagers.RemoveWhere(nm => nm.GetType() == typeof(LunaSea));
- LunaSea newLunaSea = new(values[0], this.logger);
- notificationManagers.Add(newLunaSea);
- newLunaSea.SendNotification("Success!", "LunaSea was added to Tranga!");
- break;
- }
- ExportSettings();
- }
-}
\ No newline at end of file
diff --git a/Tranga/GlobalBase.cs b/Tranga/GlobalBase.cs
new file mode 100644
index 0000000..36125fc
--- /dev/null
+++ b/Tranga/GlobalBase.cs
@@ -0,0 +1,106 @@
+using Logging;
+using Newtonsoft.Json;
+using Tranga.LibraryConnectors;
+using Tranga.NotificationConnectors;
+
+namespace Tranga;
+
+public abstract class GlobalBase
+{
+ protected Logger? logger { get; init; }
+ protected TrangaSettings settings { get; init; }
+ protected HashSet notificationConnectors { get; init; }
+ protected HashSet libraryConnectors { get; init; }
+ protected List cachedPublications { get; init; }
+
+ protected GlobalBase(GlobalBase clone)
+ {
+ this.logger = clone.logger;
+ this.settings = clone.settings;
+ this.notificationConnectors = clone.notificationConnectors;
+ this.libraryConnectors = clone.libraryConnectors;
+ this.cachedPublications = clone.cachedPublications;
+ }
+
+ protected GlobalBase(Logger? logger, TrangaSettings settings)
+ {
+ this.logger = logger;
+ this.settings = settings;
+ this.notificationConnectors = settings.LoadNotificationConnectors(this);
+ this.libraryConnectors = settings.LoadLibraryConnectors(this);
+ this.cachedPublications = new();
+ }
+
+ protected void Log(string message)
+ {
+ logger?.WriteLine(this.GetType().Name, message);
+ }
+
+ protected void Log(string fStr, params object?[] replace)
+ {
+ Log(string.Format(fStr, replace));
+ }
+
+ protected void SendNotifications(string title, string text)
+ {
+ foreach (NotificationConnector nc in notificationConnectors)
+ nc.SendNotification(title, text);
+ }
+
+ protected void AddNotificationConnector(NotificationConnector notificationConnector)
+ {
+ Log($"Adding {notificationConnector}");
+ notificationConnectors.RemoveWhere(nc => nc.GetType() == notificationConnector.GetType());
+ notificationConnectors.Add(notificationConnector);
+
+ while(IsFileInUse(settings.notificationConnectorsFilePath))
+ Thread.Sleep(100);
+ File.WriteAllText(settings.notificationConnectorsFilePath, JsonConvert.SerializeObject(notificationConnectors));
+ }
+
+ protected void DeleteNotificationConnector(NotificationConnector.NotificationConnectorType notificationConnectorType)
+ {
+ Log($"Removing {notificationConnectorType}");
+ notificationConnectors.RemoveWhere(nc => nc.notificationConnectorType == notificationConnectorType);
+ }
+
+ protected void UpdateLibraries()
+ {
+ foreach(LibraryConnector lc in libraryConnectors)
+ lc.UpdateLibrary();
+ }
+
+ protected void AddLibraryConnector(LibraryConnector libraryConnector)
+ {
+ Log($"Adding {libraryConnector}");
+ libraryConnectors.RemoveWhere(lc => lc.GetType() == libraryConnector.GetType());
+ libraryConnectors.Add(libraryConnector);
+
+ while(IsFileInUse(settings.libraryConnectorsFilePath))
+ Thread.Sleep(100);
+ File.WriteAllText(settings.libraryConnectorsFilePath, JsonConvert.SerializeObject(libraryConnectors));
+ }
+
+ protected void DeleteLibraryConnector(LibraryConnector.LibraryType libraryType)
+ {
+ Log($"Removing {libraryType}");
+ libraryConnectors.RemoveWhere(lc => lc.libraryType == libraryType);
+ }
+
+ protected bool IsFileInUse(string filePath)
+ {
+ if (!File.Exists(filePath))
+ return false;
+ try
+ {
+ using FileStream stream = new (filePath, FileMode.Open, FileAccess.Read, FileShare.None);
+ stream.Close();
+ return false;
+ }
+ catch (IOException)
+ {
+ Log($"File is in use {filePath}");
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tranga/Jobs/DownloadChapter.cs b/Tranga/Jobs/DownloadChapter.cs
new file mode 100644
index 0000000..357e7ba
--- /dev/null
+++ b/Tranga/Jobs/DownloadChapter.cs
@@ -0,0 +1,41 @@
+using System.Text;
+using Tranga.MangaConnectors;
+
+namespace Tranga.Jobs;
+
+public class DownloadChapter : Job
+{
+ public Chapter chapter { get; init; }
+
+ public DownloadChapter(GlobalBase clone, MangaConnector connector, Chapter chapter, DateTime lastExecution, string? parentJobId = null) : base(clone, connector, lastExecution, parentJobId: parentJobId)
+ {
+ this.chapter = chapter;
+ }
+
+ public DownloadChapter(GlobalBase clone, MangaConnector connector, Chapter chapter, string? parentJobId = null) : base(clone, connector, parentJobId: parentJobId)
+ {
+ this.chapter = chapter;
+ }
+
+ protected override string GetId()
+ {
+ return Convert.ToBase64String(Encoding.ASCII.GetBytes(string.Concat(this.GetType().ToString(), chapter.parentManga.internalId, chapter.chapterNumber)));
+ }
+
+ public override string ToString()
+ {
+ return $"DownloadChapter {id} {chapter}";
+ }
+
+ protected override IEnumerable ExecuteReturnSubTasksInternal()
+ {
+ Task downloadTask = new(delegate
+ {
+ mangaConnector.DownloadChapter(chapter, this.progressToken);
+ UpdateLibraries();
+ SendNotifications("Chapter downloaded", $"{chapter.parentManga.sortName} - {chapter.chapterNumber}");
+ });
+ downloadTask.Start();
+ return Array.Empty();
+ }
+}
\ No newline at end of file
diff --git a/Tranga/Jobs/DownloadNewChapters.cs b/Tranga/Jobs/DownloadNewChapters.cs
new file mode 100644
index 0000000..b41746d
--- /dev/null
+++ b/Tranga/Jobs/DownloadNewChapters.cs
@@ -0,0 +1,45 @@
+using System.Text;
+using Tranga.MangaConnectors;
+
+namespace Tranga.Jobs;
+
+public class DownloadNewChapters : Job
+{
+ public Manga manga { get; init; }
+
+ public DownloadNewChapters(GlobalBase clone, MangaConnector connector, Manga manga, DateTime lastExecution,
+ bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null) : base(clone, connector, lastExecution, recurring,
+ recurrence, parentJobId)
+ {
+ this.manga = manga;
+ }
+
+ public DownloadNewChapters(GlobalBase clone, MangaConnector connector, Manga manga, bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null) : base (clone, connector, recurring, recurrence, parentJobId)
+ {
+ this.manga = manga;
+ }
+
+ protected override string GetId()
+ {
+ return Convert.ToBase64String(Encoding.ASCII.GetBytes(string.Concat(this.GetType().ToString(), manga.internalId)));
+ }
+
+ public override string ToString()
+ {
+ return $"DownloadChapter {id} {manga}";
+ }
+
+ protected override IEnumerable ExecuteReturnSubTasksInternal()
+ {
+ Chapter[] chapters = mangaConnector.GetNewChapters(manga);
+ this.progressToken.increments = chapters.Length;
+ List jobs = new();
+ foreach (Chapter chapter in chapters)
+ {
+ DownloadChapter downloadChapterJob = new(this, this.mangaConnector, chapter, parentJobId: this.id);
+ jobs.Add(downloadChapterJob);
+ }
+ progressToken.Complete();
+ return jobs;
+ }
+}
\ No newline at end of file
diff --git a/Tranga/Jobs/Job.cs b/Tranga/Jobs/Job.cs
new file mode 100644
index 0000000..86d5b19
--- /dev/null
+++ b/Tranga/Jobs/Job.cs
@@ -0,0 +1,93 @@
+using Tranga.MangaConnectors;
+
+namespace Tranga.Jobs;
+
+public abstract class Job : GlobalBase
+{
+ public MangaConnector mangaConnector { get; init; }
+ public ProgressToken progressToken { get; private set; }
+ public bool recurring { get; init; }
+ public TimeSpan? recurrenceTime { get; set; }
+ public DateTime? lastExecution { get; private set; }
+ public DateTime nextExecution => NextExecution();
+ public string id => GetId();
+ internal IEnumerable? subJobs { get; private set; }
+ public string? parentJobId { get; init; }
+
+ internal Job(GlobalBase clone, MangaConnector connector, bool recurring = false, TimeSpan? recurrenceTime = null, string? parentJobId = null) : base(clone)
+ {
+ this.mangaConnector = connector;
+ this.progressToken = new ProgressToken(0);
+ this.recurring = recurring;
+ if (recurring && recurrenceTime is null)
+ throw new ArgumentException("If recurrence is set to true, a recurrence time has to be provided.");
+ else if(recurring && recurrenceTime is not null)
+ this.lastExecution = DateTime.Now.Subtract((TimeSpan)recurrenceTime);
+ this.recurrenceTime = recurrenceTime ?? TimeSpan.Zero;
+ this.parentJobId = parentJobId;
+ }
+
+ internal Job(GlobalBase clone, MangaConnector connector, DateTime lastExecution, bool recurring = false,
+ TimeSpan? recurrenceTime = null, string? parentJobId = null) : base(clone)
+ {
+ this.mangaConnector = connector;
+ this.progressToken = new ProgressToken(0);
+ this.recurring = recurring;
+ if (recurring && recurrenceTime is null)
+ throw new ArgumentException("If recurrence is set to true, a recurrence time has to be provided.");
+ this.lastExecution = lastExecution;
+ this.recurrenceTime = recurrenceTime ?? TimeSpan.Zero;
+ this.parentJobId = parentJobId;
+ }
+
+ protected abstract string GetId();
+
+ public void AddSubJob(Job job)
+ {
+ subJobs ??= new List();
+ subJobs = subJobs.Append(job);
+ }
+
+ private DateTime NextExecution()
+ {
+ if(recurrenceTime.HasValue && lastExecution.HasValue)
+ return lastExecution.Value.Add(recurrenceTime.Value);
+ if(recurrenceTime.HasValue && !lastExecution.HasValue)
+ return DateTime.Now;
+ return DateTime.MaxValue;
+ }
+
+ public void ResetProgress()
+ {
+ this.progressToken.increments = this.progressToken.increments - this.progressToken.incrementsCompleted;
+ this.lastExecution = DateTime.Now;
+ }
+
+ public void ExecutionEnqueue()
+ {
+ this.progressToken.increments = this.progressToken.increments - this.progressToken.incrementsCompleted;
+ this.lastExecution = recurrenceTime is not null ? DateTime.Now.Subtract((TimeSpan)recurrenceTime) : DateTime.UnixEpoch;
+ this.progressToken.Standby();
+ }
+
+ public void Cancel()
+ {
+ Log($"Cancelling {this}");
+ this.progressToken.cancellationRequested = true;
+ this.progressToken.Cancel();
+ this.lastExecution = DateTime.Now;
+ if(subJobs is not null)
+ foreach(Job subJob in subJobs)
+ subJob.Cancel();
+ }
+
+ public IEnumerable ExecuteReturnSubTasks()
+ {
+ progressToken.Start();
+ subJobs = ExecuteReturnSubTasksInternal();
+ lastExecution = DateTime.Now;
+ return subJobs;
+ }
+
+ protected abstract IEnumerable ExecuteReturnSubTasksInternal();
+}
\ No newline at end of file
diff --git a/Tranga/Jobs/JobBoss.cs b/Tranga/Jobs/JobBoss.cs
new file mode 100644
index 0000000..3be3204
--- /dev/null
+++ b/Tranga/Jobs/JobBoss.cs
@@ -0,0 +1,189 @@
+using Newtonsoft.Json;
+using Tranga.MangaConnectors;
+
+namespace Tranga.Jobs;
+
+public class JobBoss : GlobalBase
+{
+ public HashSet jobs { get; init; }
+ private Dictionary> mangaConnectorJobQueue { get; init; }
+
+ public JobBoss(GlobalBase clone, HashSet connectors) : base(clone)
+ {
+ if (File.Exists(settings.jobsFilePath))
+ {
+ this.jobs = JsonConvert.DeserializeObject>(File.ReadAllText(settings.jobsFilePath), new JobJsonConverter(this, new MangaConnectorJsonConverter(this, connectors)))!;
+ foreach (Job job in this.jobs)
+ this.jobs.FirstOrDefault(jjob => jjob.id == job.parentJobId)?.AddSubJob(job);
+ }
+ else
+ this.jobs = new();
+ foreach (DownloadNewChapters ncJob in this.jobs.Where(job => job is DownloadNewChapters))
+ cachedPublications.Add(ncJob.manga);
+ this.mangaConnectorJobQueue = new();
+ }
+
+ public void AddJob(Job job)
+ {
+ if (ContainsJobLike(job))
+ {
+ Log($"Already Contains Job {job}");
+ }
+ else
+ {
+ Log($"Added {job}");
+ this.jobs.Add(job);
+ ExportJobsList();
+ }
+ }
+
+ public void AddJobs(IEnumerable jobsToAdd)
+ {
+ foreach (Job job in jobsToAdd)
+ AddJob(job);
+ }
+
+ public bool ContainsJobLike(Job job)
+ {
+ if (job is DownloadChapter dcJob)
+ {
+ return this.GetJobsLike(dcJob.mangaConnector, chapter: dcJob.chapter).Any();
+ }else if (job is DownloadNewChapters ncJob)
+ {
+ return this.GetJobsLike(ncJob.mangaConnector, ncJob.manga).Any();
+ }
+
+ return false;
+ }
+
+ public void RemoveJob(Job job)
+ {
+ Log($"Removing {job}");
+ job.Cancel();
+ this.jobs.Remove(job);
+ if(job.subJobs is not null)
+ RemoveJobs(job.subJobs);
+ ExportJobsList();
+ }
+
+ public void RemoveJobs(IEnumerable jobsToRemove)
+ {
+ Log($"Removing {jobsToRemove.Count()} jobs.");
+ foreach (Job? job in jobsToRemove)
+ if(job is not null)
+ RemoveJob(job);
+ }
+
+ public IEnumerable GetJobsLike(string? connectorName = null, string? internalId = null, string? chapterNumber = null)
+ {
+ IEnumerable ret = this.jobs;
+ if (connectorName is not null)
+ ret = ret.Where(job => job.mangaConnector.name == connectorName);
+
+ if (internalId is not null && chapterNumber is not null)
+ ret = ret.Where(jjob =>
+ {
+ if (jjob is not DownloadChapter job)
+ return false;
+ return job.chapter.parentManga.internalId == internalId &&
+ job.chapter.chapterNumber == chapterNumber;
+ });
+ else if (internalId is not null)
+ ret = ret.Where(jjob =>
+ {
+ if (jjob is not DownloadNewChapters job)
+ return false;
+ return job.manga.internalId == internalId;
+ });
+ return ret;
+ }
+
+ public IEnumerable GetJobsLike(MangaConnector? mangaConnector = null, Manga? publication = null,
+ Chapter? chapter = null)
+ {
+ if (chapter is not null)
+ return GetJobsLike(mangaConnector?.name, chapter.Value.parentManga.internalId, chapter?.chapterNumber);
+ else
+ return GetJobsLike(mangaConnector?.name, publication?.internalId);
+ }
+
+ public Job? GetJobById(string jobId)
+ {
+ if (this.jobs.FirstOrDefault(jjob => jjob.id == jobId) is { } job)
+ return job;
+ return null;
+ }
+
+ public bool TryGetJobById(string jobId, out Job? job)
+ {
+ if (this.jobs.FirstOrDefault(jjob => jjob.id == jobId) is { } ret)
+ {
+ job = ret;
+ return true;
+ }
+
+ job = null;
+ return false;
+ }
+
+ private bool QueueContainsJob(Job job)
+ {
+ mangaConnectorJobQueue.TryAdd(job.mangaConnector, new Queue());
+ return mangaConnectorJobQueue[job.mangaConnector].Contains(job);
+ }
+
+ public void AddJobToQueue(Job job)
+ {
+ Log($"Adding Job to Queue. {job}");
+ mangaConnectorJobQueue.TryAdd(job.mangaConnector, new Queue());
+ Queue connectorJobQueue = mangaConnectorJobQueue[job.mangaConnector];
+ if(!connectorJobQueue.Contains(job))
+ connectorJobQueue.Enqueue(job);
+ job.ExecutionEnqueue();
+ }
+
+ public void AddJobsToQueue(IEnumerable jobs)
+ {
+ foreach(Job job in jobs)
+ AddJobToQueue(job);
+ }
+
+ public void ExportJobsList()
+ {
+ Log($"Exporting {settings.jobsFilePath}");
+ while(IsFileInUse(settings.jobsFilePath))
+ Thread.Sleep(10);
+ File.WriteAllText(settings.jobsFilePath, JsonConvert.SerializeObject(this.jobs));
+ }
+
+ public void CheckJobs()
+ {
+ foreach (Job job in jobs.Where(job => job.nextExecution < DateTime.Now && !QueueContainsJob(job)).OrderBy(job => job.nextExecution))
+ AddJobToQueue(job);
+ foreach (Queue jobQueue in mangaConnectorJobQueue.Values)
+ {
+ if(jobQueue.Count < 1)
+ continue;
+ Job queueHead = jobQueue.Peek();
+ if (queueHead.progressToken.state is ProgressToken.State.Complete or ProgressToken.State.Cancelled)
+ {
+ switch (queueHead)
+ {
+ case DownloadChapter:
+ RemoveJob(queueHead);
+ break;
+ case DownloadNewChapters:
+ if(queueHead.recurring)
+ queueHead.progressToken.Complete();
+ break;
+ }
+ jobQueue.Dequeue();
+ }else if (queueHead.progressToken.state is ProgressToken.State.Standby)
+ {
+ Job[] subJobs = jobQueue.Peek().ExecuteReturnSubTasks().ToArray();
+ AddJobs(subJobs);
+ AddJobsToQueue(subJobs);
+ }
+ }
+ }
+ }
\ No newline at end of file
diff --git a/Tranga/Jobs/JobJsonConverter.cs b/Tranga/Jobs/JobJsonConverter.cs
new file mode 100644
index 0000000..794075e
--- /dev/null
+++ b/Tranga/Jobs/JobJsonConverter.cs
@@ -0,0 +1,70 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Tranga.MangaConnectors;
+
+namespace Tranga.Jobs;
+
+public class JobJsonConverter : JsonConverter
+{
+ private GlobalBase _clone;
+ private MangaConnectorJsonConverter _mangaConnectorJsonConverter;
+
+ internal JobJsonConverter(GlobalBase clone, MangaConnectorJsonConverter mangaConnectorJsonConverter)
+ {
+ this._clone = clone;
+ this._mangaConnectorJsonConverter = mangaConnectorJsonConverter;
+ }
+
+ public override bool CanConvert(Type objectType)
+ {
+ return (objectType == typeof(Job));
+ }
+
+ public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
+ {
+ JObject jo = JObject.Load(reader);
+ if (jo.ContainsKey("manga"))//DownloadNewChapters
+ {
+ return new DownloadNewChapters(this._clone,
+ jo.GetValue("mangaConnector")!.ToObject(JsonSerializer.Create(new JsonSerializerSettings()
+ {
+ Converters =
+ {
+ this._mangaConnectorJsonConverter
+ }
+ }))!,
+ jo.GetValue("manga")!.ToObject(),
+ jo.GetValue("lastExecution")!.ToObject(),
+ jo.GetValue("recurring")!.Value(),
+ jo.GetValue("recurrenceTime")!.ToObject(),
+ jo.GetValue("parentJobId")!.Value());
+ }
+
+ if (jo.ContainsKey("chapter"))//DownloadChapter
+ {
+ return new DownloadChapter(this._clone,
+ jo.GetValue("mangaConnector")!.ToObject(JsonSerializer.Create(new JsonSerializerSettings()
+ {
+ Converters =
+ {
+ this._mangaConnectorJsonConverter
+ }
+ }))!,
+ jo.GetValue("chapter")!.ToObject(),
+ DateTime.UnixEpoch,
+ jo.GetValue("parentJobId")!.Value());
+ }
+
+ throw new Exception();
+ }
+
+ public override bool CanWrite => false;
+
+ ///
+ /// Don't call this
+ ///
+ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
+ {
+ throw new Exception("Dont call this");
+ }
+}
\ No newline at end of file
diff --git a/Tranga/Jobs/ProgressToken.cs b/Tranga/Jobs/ProgressToken.cs
new file mode 100644
index 0000000..6624bc6
--- /dev/null
+++ b/Tranga/Jobs/ProgressToken.cs
@@ -0,0 +1,54 @@
+namespace Tranga.Jobs;
+
+public class ProgressToken
+{
+ public bool cancellationRequested { get; set; }
+ public int increments { get; set; }
+ public int incrementsCompleted { get; set; }
+ public float progress => GetProgress();
+
+ public enum State { Running, Complete, Standby, Cancelled }
+ public State state { get; private set; }
+
+ public ProgressToken(int increments)
+ {
+ this.cancellationRequested = false;
+ this.increments = increments;
+ this.incrementsCompleted = 0;
+ this.state = State.Complete;
+ }
+
+ private float GetProgress()
+ {
+ if(increments > 0 && incrementsCompleted > 0)
+ return (float)incrementsCompleted / (float)increments;
+ return 0;
+ }
+
+ public void Increment()
+ {
+ this.incrementsCompleted++;
+ if (incrementsCompleted > increments)
+ state = State.Complete;
+ }
+
+ public void Standby()
+ {
+ state = State.Standby;
+ }
+
+ public void Start()
+ {
+ state = State.Running;
+ }
+
+ public void Complete()
+ {
+ state = State.Complete;
+ }
+
+ public void Cancel()
+ {
+ state = State.Cancelled;
+ }
+}
\ No newline at end of file
diff --git a/Tranga/LibraryManagers/Kavita.cs b/Tranga/LibraryConnectors/Kavita.cs
similarity index 77%
rename from Tranga/LibraryManagers/Kavita.cs
rename to Tranga/LibraryConnectors/Kavita.cs
index 04cbe67..a6442f2 100644
--- a/Tranga/LibraryManagers/Kavita.cs
+++ b/Tranga/LibraryConnectors/Kavita.cs
@@ -1,22 +1,27 @@
using System.Text.Json.Nodes;
-using Logging;
using Newtonsoft.Json;
using JsonSerializer = System.Text.Json.JsonSerializer;
-namespace Tranga.LibraryManagers;
+namespace Tranga.LibraryConnectors;
-public class Kavita : LibraryManager
+public class Kavita : LibraryConnector
{
- public Kavita(string baseUrl, string username, string password, Logger? logger) : base(baseUrl, GetToken(baseUrl, username, password), logger, LibraryType.Kavita)
+ public Kavita(GlobalBase clone, string baseUrl, string username, string password) :
+ base(clone, baseUrl, GetToken(baseUrl, username, password), LibraryType.Kavita)
{
}
[JsonConstructor]
- public Kavita(string baseUrl, string auth, Logger? logger) : base(baseUrl, auth, logger, LibraryType.Kavita)
+ public Kavita(GlobalBase clone, string baseUrl, string auth) : base(clone, baseUrl, auth, LibraryType.Kavita)
{
}
+ public override string ToString()
+ {
+ return $"Kavita {baseUrl}";
+ }
+
private static string GetToken(string baseUrl, string username, string password)
{
HttpClient client = new()
@@ -37,12 +42,12 @@ public class Kavita : LibraryManager
JsonObject? result = JsonSerializer.Deserialize(response.Content.ReadAsStream());
if (result is not null)
return result["token"]!.GetValue();
- else return "";
+ else throw new Exception("Did not receive token.");
}
public override void UpdateLibrary()
{
- logger?.WriteLine(this.GetType().ToString(), $"Updating Libraries");
+ Log("Updating libraries.");
foreach (KavitaLibrary lib in GetLibraries())
NetClient.MakePost($"{baseUrl}/api/Library/scan?libraryId={lib.id}", "Bearer", auth, logger);
}
@@ -53,17 +58,17 @@ public class Kavita : LibraryManager
/// Array of KavitaLibrary
private IEnumerable GetLibraries()
{
- logger?.WriteLine(this.GetType().ToString(), $"Getting Libraries");
+ Log("Getting libraries.");
Stream data = NetClient.MakeRequest($"{baseUrl}/api/Library", "Bearer", auth, logger);
if (data == Stream.Null)
{
- logger?.WriteLine(this.GetType().ToString(), $"No libraries returned");
+ Log("No libraries returned");
return Array.Empty();
}
JsonArray? result = JsonSerializer.Deserialize(data);
if (result is null)
{
- logger?.WriteLine(this.GetType().ToString(), $"No libraries returned");
+ Log("No libraries returned");
return Array.Empty();
}
diff --git a/Tranga/LibraryManagers/Komga.cs b/Tranga/LibraryConnectors/Komga.cs
similarity index 70%
rename from Tranga/LibraryManagers/Komga.cs
rename to Tranga/LibraryConnectors/Komga.cs
index 545ec91..7a0c485 100644
--- a/Tranga/LibraryManagers/Komga.cs
+++ b/Tranga/LibraryConnectors/Komga.cs
@@ -1,29 +1,33 @@
using System.Text.Json.Nodes;
-using Logging;
using Newtonsoft.Json;
using JsonSerializer = System.Text.Json.JsonSerializer;
-namespace Tranga.LibraryManagers;
+namespace Tranga.LibraryConnectors;
///
/// Provides connectivity to Komga-API
/// Can fetch and update libraries
///
-public class Komga : LibraryManager
+public class Komga : LibraryConnector
{
- public Komga(string baseUrl, string username, string password, Logger? logger)
- : base(baseUrl, Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}")), logger, LibraryType.Komga)
+ public Komga(GlobalBase clone, string baseUrl, string username, string password)
+ : base(clone, baseUrl, Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}")), LibraryType.Komga)
{
}
[JsonConstructor]
- public Komga(string baseUrl, string auth, Logger? logger) : base(baseUrl, auth, logger, LibraryType.Komga)
+ public Komga(GlobalBase clone, string baseUrl, string auth) : base(clone, baseUrl, auth, LibraryType.Komga)
{
}
+ public override string ToString()
+ {
+ return $"Komga {baseUrl}";
+ }
+
public override void UpdateLibrary()
{
- logger?.WriteLine(this.GetType().ToString(), $"Updating Libraries");
+ Log("Updating libraries.");
foreach (KomgaLibrary lib in GetLibraries())
NetClient.MakePost($"{baseUrl}/api/v1/libraries/{lib.id}/scan", "Basic", auth, logger);
}
@@ -34,17 +38,17 @@ public class Komga : LibraryManager
/// Array of KomgaLibraries
private IEnumerable GetLibraries()
{
- logger?.WriteLine(this.GetType().ToString(), $"Getting Libraries");
+ Log("Getting Libraries");
Stream data = NetClient.MakeRequest($"{baseUrl}/api/v1/libraries", "Basic", auth, logger);
if (data == Stream.Null)
{
- logger?.WriteLine(this.GetType().ToString(), $"No libraries returned");
+ Log("No libraries returned");
return Array.Empty();
}
JsonArray? result = JsonSerializer.Deserialize(data);
if (result is null)
{
- logger?.WriteLine(this.GetType().ToString(), $"No libraries returned");
+ Log("No libraries returned");
return Array.Empty();
}
diff --git a/Tranga/LibraryManagers/LibraryManager.cs b/Tranga/LibraryConnectors/LibraryConnector.cs
similarity index 58%
rename from Tranga/LibraryManagers/LibraryManager.cs
rename to Tranga/LibraryConnectors/LibraryConnector.cs
index 88e7b98..e3e3caa 100644
--- a/Tranga/LibraryManagers/LibraryManager.cs
+++ b/Tranga/LibraryConnectors/LibraryConnector.cs
@@ -1,12 +1,10 @@
using System.Net;
using System.Net.Http.Headers;
using Logging;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
-namespace Tranga.LibraryManagers;
+namespace Tranga.LibraryConnectors;
-public abstract class LibraryManager
+public abstract class LibraryConnector : GlobalBase
{
public enum LibraryType : byte
{
@@ -19,26 +17,15 @@ public abstract class LibraryManager
public string baseUrl { get; }
// ReSharper disable once MemberCanBeProtected.Global
public string auth { get; } //Base64 encoded, if you use your password everywhere, you have problems
- protected Logger? logger;
- /// Base-URL of Komga instance, no trailing slashes(/)
- /// Base64 string of username and password (username):(password)
- ///
- ///
- protected LibraryManager(string baseUrl, string auth, Logger? logger, LibraryType libraryType)
+ protected LibraryConnector(GlobalBase clone, string baseUrl, string auth, LibraryType libraryType) : base(clone)
{
this.baseUrl = baseUrl;
this.auth = auth;
- this.logger = logger;
this.libraryType = libraryType;
}
public abstract void UpdateLibrary();
- public void AddLogger(Logger newLogger)
- {
- this.logger = newLogger;
- }
-
protected static class NetClient
{
public static Stream MakeRequest(string url, string authScheme, string auth, Logger? logger)
@@ -52,7 +39,7 @@ public abstract class LibraryManager
RequestUri = new Uri(url)
};
HttpResponseMessage response = client.Send(requestMessage);
- logger?.WriteLine("LibraryManager", $"GET {url} -> {(int)response.StatusCode}: {response.ReasonPhrase}");
+ logger?.WriteLine("LibraryManager.NetClient", $"GET {url} -> {(int)response.StatusCode}: {response.ReasonPhrase}");
if(response.StatusCode is HttpStatusCode.Unauthorized && response.RequestMessage!.RequestUri!.AbsoluteUri != url)
return MakeRequest(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth, logger);
@@ -78,7 +65,7 @@ public abstract class LibraryManager
RequestUri = new Uri(url)
};
HttpResponseMessage response = client.Send(requestMessage);
- logger?.WriteLine("LibraryManager", $"POST {url} -> {(int)response.StatusCode}: {response.ReasonPhrase}");
+ logger?.WriteLine("LibraryManager.NetClient", $"POST {url} -> {(int)response.StatusCode}: {response.ReasonPhrase}");
if(response.StatusCode is HttpStatusCode.Unauthorized && response.RequestMessage!.RequestUri!.AbsoluteUri != url)
return MakePost(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth, logger);
@@ -88,34 +75,4 @@ public abstract class LibraryManager
return false;
}
}
-
- public class LibraryManagerJsonConverter : JsonConverter
- {
- public override bool CanConvert(Type objectType)
- {
- return (objectType == typeof(LibraryManager));
- }
-
- public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
- {
- JObject jo = JObject.Load(reader);
- if (jo["libraryType"]!.Value() == (Int64)LibraryType.Komga)
- return jo.ToObject(serializer)!;
-
- if (jo["libraryType"]!.Value() == (Int64)LibraryType.Kavita)
- return jo.ToObject(serializer)!;
-
- throw new Exception();
- }
-
- public override bool CanWrite => false;
-
- ///
- /// Don't call this
- ///
- public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
- {
- throw new Exception("Dont call this");
- }
- }
}
\ No newline at end of file
diff --git a/Tranga/LibraryConnectors/LibraryManagerJsonConverter.cs b/Tranga/LibraryConnectors/LibraryManagerJsonConverter.cs
new file mode 100644
index 0000000..be8983f
--- /dev/null
+++ b/Tranga/LibraryConnectors/LibraryManagerJsonConverter.cs
@@ -0,0 +1,45 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Tranga.LibraryConnectors;
+
+public class LibraryManagerJsonConverter : JsonConverter
+{
+ private GlobalBase _clone;
+
+ internal LibraryManagerJsonConverter(GlobalBase clone)
+ {
+ this._clone = clone;
+ }
+
+ public override bool CanConvert(Type objectType)
+ {
+ return (objectType == typeof(LibraryConnector));
+ }
+
+ public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
+ {
+ JObject jo = JObject.Load(reader);
+ if (jo["libraryType"]!.Value() == (byte)LibraryConnector.LibraryType.Komga)
+ return new Komga(this._clone,
+ jo.GetValue("baseUrl")!.Value()!,
+ jo.GetValue("auth")!.Value()!);
+
+ if (jo["libraryType"]!.Value() == (byte)LibraryConnector.LibraryType.Kavita)
+ return new Kavita(this._clone,
+ jo.GetValue("baseUrl")!.Value()!,
+ jo.GetValue("auth")!.Value()!);
+
+ throw new Exception();
+ }
+
+ public override bool CanWrite => false;
+
+ ///
+ /// Don't call this
+ ///
+ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
+ {
+ throw new Exception("Dont call this");
+ }
+}
\ No newline at end of file
diff --git a/Tranga/Publication.cs b/Tranga/Manga.cs
similarity index 88%
rename from Tranga/Publication.cs
rename to Tranga/Manga.cs
index 330e7c0..247be53 100644
--- a/Tranga/Publication.cs
+++ b/Tranga/Manga.cs
@@ -9,7 +9,7 @@ namespace Tranga;
///
/// Contains information on a Publication (Manga)
///
-public struct Publication
+public struct Manga
{
public string sortName { get; }
public List authors { get; }
@@ -19,8 +19,8 @@ public struct Publication
public string? description { get; }
public string[] tags { get; }
// ReSharper disable once UnusedAutoPropertyAccessor.Global
- public string? posterUrl { get; }
- public string? coverFileNameInCache { get; }
+ public string? coverUrl { get; }
+ public string? coverFileNameInCache { get; set; }
// ReSharper disable once UnusedAutoPropertyAccessor.Global
public Dictionary links { get; }
// ReSharper disable once MemberCanBePrivate.Global
@@ -33,10 +33,10 @@ public struct Publication
public string internalId { get; }
public float ignoreChaptersBelow { get; set; }
- private static readonly Regex LegalCharacters = new Regex(@"[A-Z]*[a-z]*[0-9]* *\.*-*,*'*\'*\)*\(*~*!*");
+ private static readonly Regex LegalCharacters = new (@"[A-Z]*[a-z]*[0-9]* *\.*-*,*'*\'*\)*\(*~*!*");
[JsonConstructor]
- public Publication(string sortName, List authors, string? description, Dictionary altTitles, string[] tags, string? posterUrl, string? coverFileNameInCache, Dictionary? links, int? year, string? originalLanguage, string status, string publicationId, string? folderName = null, float? ignoreChaptersBelow = 0)
+ public Manga(string sortName, List authors, string? description, Dictionary altTitles, string[] tags, string? coverUrl, string? coverFileNameInCache, Dictionary? links, int? year, string? originalLanguage, string status, string publicationId, string? folderName = null, float? ignoreChaptersBelow = 0)
{
this.sortName = sortName;
this.authors = authors;
@@ -44,7 +44,7 @@ public struct Publication
this.altTitles = altTitles;
this.tags = tags;
this.coverFileNameInCache = coverFileNameInCache;
- this.posterUrl = posterUrl;
+ this.coverUrl = coverUrl;
this.links = links ?? new Dictionary();
this.year = year;
this.originalLanguage = originalLanguage;
@@ -58,6 +58,11 @@ public struct Publication
this.ignoreChaptersBelow = ignoreChaptersBelow ?? 0f;
}
+ public override string ToString()
+ {
+ return $"Publication {sortName} {internalId}";
+ }
+
public string CreatePublicationFolder(string downloadDirectory)
{
string publicationFolder = Path.Join(downloadDirectory, this.folderName);
diff --git a/Tranga/DownloadClient.cs b/Tranga/MangaConnectors/DownloadClient.cs
similarity index 79%
rename from Tranga/DownloadClient.cs
rename to Tranga/MangaConnectors/DownloadClient.cs
index 5b00a06..efdc9ac 100644
--- a/Tranga/DownloadClient.cs
+++ b/Tranga/MangaConnectors/DownloadClient.cs
@@ -1,10 +1,9 @@
using System.Net;
using System.Net.Http.Headers;
-using Logging;
-namespace Tranga;
+namespace Tranga.MangaConnectors;
-internal class DownloadClient
+internal class DownloadClient : GlobalBase
{
private static readonly HttpClient Client = new()
{
@@ -20,17 +19,9 @@ internal class DownloadClient
private readonly Dictionary _lastExecutedRateLimit;
private readonly Dictionary _rateLimit;
- // ReSharper disable once InconsistentNaming
- private readonly Logger? logger;
- ///
- /// Creates a httpClient
- ///
- /// Rate limits for requests. byte is RequestType, int maximum requests per minute for RequestType
- ///
- public DownloadClient(Dictionary rateLimitRequestsPerMinute, Logger? logger)
+ public DownloadClient(GlobalBase clone, Dictionary rateLimitRequestsPerMinute) : base(clone)
{
- this.logger = logger;
_lastExecutedRateLimit = new();
_rateLimit = new();
foreach(KeyValuePair limit in rateLimitRequestsPerMinute)
@@ -50,7 +41,7 @@ internal class DownloadClient
_lastExecutedRateLimit.TryAdd(requestType, DateTime.Now.Subtract(value));
else
{
- logger?.WriteLine(this.GetType().ToString(), "RequestType not configured for rate-limit.");
+ Log("RequestType not configured for rate-limit.");
return new RequestResult(HttpStatusCode.NotAcceptable, Stream.Null);
}
@@ -69,18 +60,18 @@ internal class DownloadClient
if(referrer is not null)
requestMessage.Headers.Referrer = new Uri(referrer);
_lastExecutedRateLimit[requestType] = DateTime.Now;
+ //Log($"Requesting {requestType} {url}");
response = Client.Send(requestMessage);
}
catch (HttpRequestException e)
{
- logger?.WriteLine(this.GetType().ToString(), e.Message);
- logger?.WriteLine(this.GetType().ToString(), $"Waiting {_rateLimit[requestType] * 2}... Retrying.");
+ Log("Exception:\n\t{0}\n\tWaiting {1} before retrying.", e.Message, _rateLimit[requestType] * 2);
Thread.Sleep(_rateLimit[requestType] * 2);
}
}
if (!response.IsSuccessStatusCode)
{
- logger?.WriteLine(this.GetType().ToString(), $"Request-Error {response.StatusCode}: {response.ReasonPhrase}");
+ Log($"Request-Error {response.StatusCode}: {response.ReasonPhrase}");
return new RequestResult(response.StatusCode, Stream.Null);
}
diff --git a/Tranga/Connectors/Connector.cs b/Tranga/MangaConnectors/MangaConnector.cs
similarity index 66%
rename from Tranga/Connectors/Connector.cs
rename to Tranga/MangaConnectors/MangaConnector.cs
index 997aeec..0fe756d 100644
--- a/Tranga/Connectors/Connector.cs
+++ b/Tranga/MangaConnectors/MangaConnector.cs
@@ -3,80 +3,68 @@ using System.IO.Compression;
using System.Net;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
-using Tranga.TrangaTasks;
+using Tranga.Jobs;
using static System.IO.UnixFileMode;
-namespace Tranga.Connectors;
+namespace Tranga.MangaConnectors;
///
/// Base-Class for all Connectors
/// Provides some methods to be used by all Connectors, as well as a DownloadClient
///
-public abstract class Connector
+public abstract class MangaConnector : GlobalBase
{
- protected CommonObjects commonObjects;
- protected TrangaSettings settings { get; }
internal DownloadClient downloadClient { get; init; } = null!;
-
- protected Connector(TrangaSettings settings, CommonObjects commonObjects)
+
+ protected MangaConnector(GlobalBase clone) : base(clone)
{
- this.settings = settings;
- this.commonObjects = commonObjects;
- if (!Directory.Exists(settings.coverImageCache))
- Directory.CreateDirectory(settings.coverImageCache);
+ Directory.CreateDirectory(settings.coverImageCache);
}
public abstract string name { get; } //Name of the Connector (e.g. Website)
- public Publication[] GetPublications(ref HashSet publicationCollection, string publicationTitle = "")
- {
- Publication[] ret = GetPublicationsInternal(publicationTitle);
- foreach (Publication p in ret)
- publicationCollection.Add(p);
- return ret;
- }
-
///
/// Returns all Publications with the given string.
/// If the string is empty or null, returns all Publication of the Connector
///
/// Search-Query
/// Publications matching the query
- protected abstract Publication[] GetPublicationsInternal(string publicationTitle = "");
+ public abstract Manga[] GetManga(string publicationTitle = "");
+
+ public abstract Manga? GetMangaFromUrl(string url);
///
/// Returns all Chapters of the publication in the provided language.
/// If the language is empty or null, returns all Chapters in all Languages.
///
- /// Publication to get Chapters for
+ /// Publication to get Chapters for
/// Language of the Chapters
/// Array of Chapters matching Publication and Language
- public abstract Chapter[] GetChapters(Publication publication, string language = "");
+ public abstract Chapter[] GetChapters(Manga manga, string language="en");
///
/// Updates the available Chapters of a Publication
///
- /// Publication to check
+ /// Publication to check
/// Language to receive chapters for
- ///
/// List of Chapters that were previously not in collection
- public List GetNewChaptersList(Publication publication, string language, ref HashSet collection)
+ public Chapter[] GetNewChapters(Manga manga, string language = "en")
{
- Chapter[] newChapters = this.GetChapters(publication, language);
- collection.Add(publication);
+ Log($"Getting new Chapters for {manga}");
+ Chapter[] newChapters = this.GetChapters(manga, language);
NumberFormatInfo decimalPoint = new (){ NumberDecimalSeparator = "." };
- commonObjects.logger?.WriteLine(this.GetType().ToString(), "Checking for duplicates");
+ Log($"Checking for duplicates {manga}");
List newChaptersList = newChapters.Where(nChapter =>
- float.Parse(nChapter.chapterNumber, decimalPoint) > publication.ignoreChaptersBelow &&
+ float.Parse(nChapter.chapterNumber, decimalPoint) > manga.ignoreChaptersBelow &&
!nChapter.CheckChapterIsDownloaded(settings.downloadLocation)).ToList();
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"{newChaptersList.Count} new chapters.");
+ Log($"{newChaptersList.Count} new chapters. {manga}");
- return newChaptersList;
+ return newChaptersList.ToArray();
}
- public Chapter[] SelectChapters(Publication publication, string searchTerm, string? language = null)
+ public Chapter[] SelectChapters(Manga manga, string searchTerm, string? language = null)
{
- Chapter[] availableChapters = this.GetChapters(publication, language??"en");
+ Chapter[] availableChapters = this.GetChapters(manga, language??"en");
Regex volumeRegex = new ("((v(ol)*(olume)*){1} *([0-9]+(-[0-9]+)?){1})", RegexOptions.IgnoreCase);
Regex chapterRegex = new ("((c(h)*(hapter)*){1} *([0-9]+(-[0-9]+)?){1})", RegexOptions.IgnoreCase);
Regex singleResultRegex = new("([0-9]+)", RegexOptions.IgnoreCase);
@@ -147,35 +135,27 @@ public abstract class Connector
return Array.Empty();
}
- ///
- /// Retrieves the Chapter (+Images) from the website.
- /// Should later call DownloadChapterImages to retrieve the individual Images of the Chapter and create .cbz archive.
- ///
- /// Publication that contains Chapter
- /// Chapter with Images to retrieve
- /// Will be used for progress-tracking
- ///
- public abstract HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null);
+ public abstract HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null);
///
/// Copies the already downloaded cover from cache to downloadLocation
///
- /// Publication to retrieve Cover for
- public void CopyCoverFromCacheToDownloadLocation(Publication publication)
+ /// Publication to retrieve Cover for
+ public void CopyCoverFromCacheToDownloadLocation(Manga manga)
{
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Cloning cover {publication.sortName} -> {publication.internalId}");
+ Log($"Copy cover {manga}");
//Check if Publication already has a Folder and cover
- string publicationFolder = publication.CreatePublicationFolder(settings.downloadLocation);
+ string publicationFolder = manga.CreatePublicationFolder(settings.downloadLocation);
DirectoryInfo dirInfo = new (publicationFolder);
if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover", StringComparison.InvariantCultureIgnoreCase)))
{
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Cover exists {publication.sortName}");
+ Log($"Cover exists {manga}");
return;
}
- string fileInCache = Path.Join(settings.coverImageCache, publication.coverFileNameInCache);
+ string fileInCache = Path.Join(settings.coverImageCache, manga.coverFileNameInCache);
string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(fileInCache).Split('.')[^1]}" );
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Cloning cover {fileInCache} -> {newFilePath}");
+ Log($"Cloning cover {fileInCache} -> {newFilePath}");
File.Copy(fileInCache, newFilePath, true);
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
File.SetUnixFileMode(newFilePath, GroupRead | GroupWrite | OtherRead | OtherWrite | UserRead | UserWrite);
@@ -199,21 +179,13 @@ public abstract class Connector
return requestResult.statusCode;
}
- ///
- /// Downloads all Images from URLs, Compresses to zip(cbz) and saves.
- ///
- /// List of URLs to download Images from
- /// Full path to save archive to (without file ending .cbz)
- /// Used for progress tracking
- /// Path of the generate Chapter ComicInfo.xml, if it was generated
- /// RequestType for RateLimits
- /// Used in http request header
- ///
- protected HttpStatusCode DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, byte requestType, DownloadChapterTask parentTask, string? comicInfoPath = null, string? referrer = null, CancellationToken? cancellationToken = null)
+ protected HttpStatusCode DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, byte requestType, string? comicInfoPath = null, string? referrer = null, ProgressToken? progressToken = null)
{
- if (cancellationToken?.IsCancellationRequested ?? false)
+ if (progressToken?.cancellationRequested ?? false)
return HttpStatusCode.RequestTimeout;
- commonObjects.logger?.WriteLine("Connector", $"Downloading Images for {saveArchiveFilePath}");
+ Log($"Downloading Images for {saveArchiveFilePath}");
+ if(progressToken is not null)
+ progressToken.increments = imageUrls.Length;
//Check if Publication Directory already exists
string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!;
if (!Directory.Exists(directoryPath))
@@ -231,24 +203,32 @@ public abstract class Connector
{
string[] split = imageUrl.Split('.');
string extension = split[^1];
- commonObjects.logger?.WriteLine("Connector", $"Downloading Image {chapter + 1:000}/{imageUrls.Length:000} {parentTask.publication.sortName} {parentTask.publication.internalId} Vol.{parentTask.chapter.volumeNumber} Ch.{parentTask.chapter.chapterNumber} {parentTask.progress:P2}");
+ Log($"Downloading image {chapter + 1:000}/{imageUrls.Length:000}"); //TODO
HttpStatusCode status = DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), requestType, referrer);
if ((int)status < 200 || (int)status >= 300)
+ {
+ progressToken?.Complete();
return status;
- parentTask.IncrementProgress(1.0 / imageUrls.Length);
- if (cancellationToken?.IsCancellationRequested ?? false)
+ }
+ if (progressToken?.cancellationRequested ?? false)
+ {
+ progressToken?.Complete();
return HttpStatusCode.RequestTimeout;
+ }
+ progressToken?.Increment();
}
if(comicInfoPath is not null)
File.Copy(comicInfoPath, Path.Join(tempFolder, "ComicInfo.xml"));
- commonObjects.logger?.WriteLine("Connector", $"Creating archive {saveArchiveFilePath}");
+ Log($"Creating archive {saveArchiveFilePath}");
//ZIP-it and ship-it
ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath);
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
File.SetUnixFileMode(saveArchiveFilePath, GroupRead | GroupWrite | OtherRead | OtherWrite | UserRead | UserWrite);
Directory.Delete(tempFolder, true); //Cleanup
+
+ progressToken?.Complete();
return HttpStatusCode.OK;
}
@@ -265,7 +245,7 @@ public abstract class Connector
using MemoryStream ms = new();
coverResult.result.CopyTo(ms);
File.WriteAllBytes(saveImagePath, ms.ToArray());
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Saving image to {saveImagePath}");
+ Log($"Saving cover to {saveImagePath}");
return filename;
}
}
\ No newline at end of file
diff --git a/Tranga/MangaConnectors/MangaConnectorJsonConverter.cs b/Tranga/MangaConnectors/MangaConnectorJsonConverter.cs
new file mode 100644
index 0000000..5b98845
--- /dev/null
+++ b/Tranga/MangaConnectors/MangaConnectorJsonConverter.cs
@@ -0,0 +1,49 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Tranga.MangaConnectors;
+
+public class MangaConnectorJsonConverter : JsonConverter
+{
+ private GlobalBase _clone;
+ private HashSet connectors;
+
+ internal MangaConnectorJsonConverter(GlobalBase clone, HashSet connectors)
+ {
+ this._clone = clone;
+ this.connectors = connectors;
+ }
+
+ public override bool CanConvert(Type objectType)
+ {
+ return (objectType == typeof(MangaConnector));
+ }
+
+ public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
+ {
+ JObject jo = JObject.Load(reader);
+ switch (jo.GetValue("name")!.Value()!)
+ {
+ case "MangaDex":
+ return this.connectors.First(c => c is MangaDex);
+ case "Manganato":
+ return this.connectors.First(c => c is Manganato);
+ case "MangaKatana":
+ return this.connectors.First(c => c is MangaKatana);
+ case "Mangasee":
+ return this.connectors.First(c => c is Mangasee);
+ }
+
+ throw new Exception();
+ }
+
+ public override bool CanWrite => false;
+
+ ///
+ /// Don't call this
+ ///
+ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
+ {
+ throw new Exception("Dont call this");
+ }
+}
\ No newline at end of file
diff --git a/Tranga/Connectors/MangaDex.cs b/Tranga/MangaConnectors/MangaDex.cs
similarity index 52%
rename from Tranga/Connectors/MangaDex.cs
rename to Tranga/MangaConnectors/MangaDex.cs
index b63f7a3..a3073b5 100644
--- a/Tranga/Connectors/MangaDex.cs
+++ b/Tranga/MangaConnectors/MangaDex.cs
@@ -1,11 +1,12 @@
using System.Globalization;
using System.Net;
-using System.Text.Json;
using System.Text.Json.Nodes;
-using Tranga.TrangaTasks;
+using System.Text.RegularExpressions;
+using Tranga.Jobs;
+using JsonSerializer = System.Text.Json.JsonSerializer;
-namespace Tranga.Connectors;
-public class MangaDex : Connector
+namespace Tranga.MangaConnectors;
+public class MangaDex : MangaConnector
{
public override string name { get; }
@@ -18,26 +19,26 @@ public class MangaDex : Connector
Author,
}
- public MangaDex(TrangaSettings settings, CommonObjects commonObjects) : base(settings, commonObjects)
+ public MangaDex(GlobalBase clone) : base(clone)
{
name = "MangaDex";
- this.downloadClient = new DownloadClient(new Dictionary()
+ this.downloadClient = new DownloadClient(clone, new Dictionary()
{
{(byte)RequestType.Manga, 250},
{(byte)RequestType.Feed, 250},
{(byte)RequestType.AtHomeServer, 40},
{(byte)RequestType.CoverUrl, 250},
{(byte)RequestType.Author, 250}
- }, commonObjects.logger);
+ });
}
- protected override Publication[] GetPublicationsInternal(string publicationTitle = "")
+ public override Manga[] GetManga(string publicationTitle = "")
{
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Publications (title={publicationTitle})");
+ Log($"Searching Publications. Term=\"{publicationTitle}\"");
const int limit = 100; //How many values we want returned at once
int offset = 0; //"Page"
int total = int.MaxValue; //How many total results are there, is updated on first request
- HashSet publications = new();
+ HashSet retManga = new();
int loadedPublicationData = 0;
while (offset < total) //As long as we haven't requested all "Pages"
{
@@ -57,102 +58,123 @@ public class MangaDex : Connector
JsonArray mangaInResult = result["data"]!.AsArray(); //Manga-data-Array
//Loop each Manga and extract information from JSON
- foreach (JsonNode? mangeNode in mangaInResult)
+ foreach (JsonNode? mangaNode in mangaInResult)
{
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting publication data. {++loadedPublicationData}/{total}");
- JsonObject manga = (JsonObject)mangeNode!;
- JsonObject attributes = manga["attributes"]!.AsObject();
+ Log($"Getting publication data. {++loadedPublicationData}/{total}");
+ Manga manga = MangaFromJsonObject((JsonObject)mangaNode);
+ retManga.Add(manga); //Add Publication (Manga) to result
+ }
+ }
+ Log($"Retrieved {retManga.Count} publications. Term=\"{publicationTitle}\"");
+ return retManga.ToArray();
+ }
+
+ public override Manga? GetMangaFromUrl(string url)
+ {
+ Regex idRex = new (@"https:\/\/mangadex.org\/title\/([A-z0-9-]*)\/.*");
+ string id = idRex.Match(url).Groups[1].Value;
+ Log($"Got id {id} from {url}");
+ DownloadClient.RequestResult requestResult =
+ downloadClient.MakeRequest($"https://api.mangadex.org/manga/{id}", (byte)RequestType.Manga);
+ if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
+ return null;
+ JsonObject? result = JsonSerializer.Deserialize(requestResult.result);
+ if(result is not null)
+ return MangaFromJsonObject(result["data"]!.AsObject());
+ return null;
+ }
+
+ private Manga MangaFromJsonObject(JsonObject manga)
+ {
+ JsonObject attributes = manga["attributes"]!.AsObject();
- string publicationId = manga["id"]!.GetValue();
+ string publicationId = manga["id"]!.GetValue();
- string title = attributes["title"]!.AsObject().ContainsKey("en") && attributes["title"]!["en"] is not null
+ string title = attributes["title"]!.AsObject().ContainsKey("en") && attributes["title"]!["en"] is not null
? attributes["title"]!["en"]!.GetValue()
: attributes["title"]![((IDictionary)attributes["title"]!.AsObject()).Keys.First()]!.GetValue();
- string? description = attributes["description"]!.AsObject().ContainsKey("en") && attributes["description"]!["en"] is not null
+ string? description = attributes["description"]!.AsObject().ContainsKey("en") && attributes["description"]!["en"] is not null
? attributes["description"]!["en"]!.GetValue()
: null;
- JsonArray altTitlesObject = attributes["altTitles"]!.AsArray();
- Dictionary altTitlesDict = new();
- foreach (JsonNode? altTitleNode in altTitlesObject)
- {
- JsonObject altTitleObject = (JsonObject)altTitleNode!;
- string key = ((IDictionary)altTitleObject).Keys.ToArray()[0];
- altTitlesDict.TryAdd(key, altTitleObject[key]!.GetValue());
- }
+ JsonArray altTitlesObject = attributes["altTitles"]!.AsArray();
+ Dictionary altTitlesDict = new();
+ foreach (JsonNode? altTitleNode in altTitlesObject)
+ {
+ JsonObject altTitleObject = (JsonObject)altTitleNode!;
+ string key = ((IDictionary)altTitleObject).Keys.ToArray()[0];
+ altTitlesDict.TryAdd(key, altTitleObject[key]!.GetValue());
+ }
- JsonArray tagsObject = attributes["tags"]!.AsArray();
- HashSet tags = new();
- foreach (JsonNode? tagNode in tagsObject)
- {
- JsonObject tagObject = (JsonObject)tagNode!;
- if(tagObject["attributes"]!["name"]!.AsObject().ContainsKey("en"))
- tags.Add(tagObject["attributes"]!["name"]!["en"]!.GetValue());
- }
+ JsonArray tagsObject = attributes["tags"]!.AsArray();
+ HashSet tags = new();
+ foreach (JsonNode? tagNode in tagsObject)
+ {
+ JsonObject tagObject = (JsonObject)tagNode!;
+ if(tagObject["attributes"]!["name"]!.AsObject().ContainsKey("en"))
+ tags.Add(tagObject["attributes"]!["name"]!["en"]!.GetValue());
+ }
- string? posterId = null;
- HashSet authorIds = new();
- if (manga.ContainsKey("relationships") && manga["relationships"] is not null)
- {
- JsonArray relationships = manga["relationships"]!.AsArray();
- posterId = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue() == "cover_art")!["id"]!.GetValue();
- foreach (JsonNode? node in relationships.Where(relationship =>
- relationship!["type"]!.GetValue() == "author"))
- authorIds.Add(node!["id"]!.GetValue());
- }
- string? coverUrl = GetCoverUrl(publicationId, posterId);
- string? coverCacheName = null;
- if (coverUrl is not null)
- coverCacheName = SaveCoverImageToCache(coverUrl, (byte)RequestType.AtHomeServer);
+ string? posterId = null;
+ HashSet authorIds = new();
+ if (manga.ContainsKey("relationships") && manga["relationships"] is not null)
+ {
+ JsonArray relationships = manga["relationships"]!.AsArray();
+ posterId = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue() == "cover_art")!["id"]!.GetValue();
+ foreach (JsonNode? node in relationships.Where(relationship =>
+ relationship!["type"]!.GetValue() == "author"))
+ authorIds.Add(node!["id"]!.GetValue());
+ }
+ string? coverUrl = GetCoverUrl(publicationId, posterId);
+ string? coverCacheName = null;
+ if (coverUrl is not null)
+ coverCacheName = SaveCoverImageToCache(coverUrl, (byte)RequestType.AtHomeServer);
- List authors = GetAuthors(authorIds);
+ List authors = GetAuthors(authorIds);
- Dictionary linksDict = new();
- if (attributes.ContainsKey("links") && attributes["links"] is not null)
- {
- JsonObject linksObject = attributes["links"]!.AsObject();
- foreach (string key in ((IDictionary)linksObject).Keys)
- {
- linksDict.Add(key, linksObject[key]!.GetValue());
- }
- }
-
- int? year = attributes.ContainsKey("year") && attributes["year"] is not null
- ? attributes["year"]!.GetValue()
- : null;
-
- string? originalLanguage = attributes.ContainsKey("originalLanguage") && attributes["originalLanguage"] is not null
- ? attributes["originalLanguage"]!.GetValue()
- : null;
-
- string status = attributes["status"]!.GetValue();
-
- Publication pub = new (
- title,
- authors,
- description,
- altTitlesDict,
- tags.ToArray(),
- coverUrl,
- coverCacheName,
- linksDict,
- year,
- originalLanguage,
- status,
- publicationId
- );
- publications.Add(pub); //Add Publication (Manga) to result
+ Dictionary linksDict = new();
+ if (attributes.ContainsKey("links") && attributes["links"] is not null)
+ {
+ JsonObject linksObject = attributes["links"]!.AsObject();
+ foreach (string key in ((IDictionary)linksObject).Keys)
+ {
+ linksDict.Add(key, linksObject[key]!.GetValue());
}
}
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Done getting publications (title={publicationTitle})");
- return publications.ToArray();
+ int? year = attributes.ContainsKey("year") && attributes["year"] is not null
+ ? attributes["year"]!.GetValue()
+ : null;
+
+ string? originalLanguage =
+ attributes.ContainsKey("originalLanguage") && attributes["originalLanguage"] is not null
+ ? attributes["originalLanguage"]!.GetValue()
+ : null;
+
+ string status = attributes["status"]!.GetValue();
+
+ Manga pub = new(
+ title,
+ authors,
+ description,
+ altTitlesDict,
+ tags.ToArray(),
+ coverUrl,
+ coverCacheName,
+ linksDict,
+ year,
+ originalLanguage,
+ status,
+ publicationId
+ );
+ cachedPublications.Add(pub);
+ return pub;
}
- public override Chapter[] GetChapters(Publication publication, string language = "")
+ public override Chapter[] GetChapters(Manga manga, string language="en")
{
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Chapters for {publication.sortName} {publication.internalId} (language={language})");
+ Log($"Getting chapters {manga}");
const int limit = 100; //How many values we want returned at once
int offset = 0; //"Page"
int total = int.MaxValue; //How many total results are there, is updated on first request
@@ -163,7 +185,7 @@ public class MangaDex : Connector
//Request next "Page"
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest(
- $"https://api.mangadex.org/manga/{publication.publicationId}/feed?limit={limit}&offset={offset}&translatedLanguage%5B%5D={language}", (byte)RequestType.Feed);
+ $"https://api.mangadex.org/manga/{manga.publicationId}/feed?limit={limit}&offset={offset}&translatedLanguage%5B%5D={language}", (byte)RequestType.Feed);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
break;
JsonObject? result = JsonSerializer.Deserialize(requestResult.result);
@@ -194,24 +216,22 @@ public class MangaDex : Connector
: "null";
if(chapterNum is not "null")
- chapters.Add(new Chapter(publication, title, volume, chapterNum, chapterId));
+ chapters.Add(new Chapter(manga, title, volume, chapterNum, chapterId));
}
}
//Return Chapters ordered by Chapter-Number
- NumberFormatInfo chapterNumberFormatInfo = new()
- {
- NumberDecimalSeparator = "."
- };
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Done getting {chapters.Count} Chapters for {publication.internalId}");
+ NumberFormatInfo chapterNumberFormatInfo = new() { NumberDecimalSeparator = "." };
+ Log($"Got {chapters.Count} chapters. {manga}");
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
}
- public override HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null)
+ public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
{
- if (cancellationToken?.IsCancellationRequested ?? false)
+ if (progressToken?.cancellationRequested ?? false)
return HttpStatusCode.RequestTimeout;
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
+ Manga chapterParentManga = chapter.parentManga;
+ Log($"Retrieving chapter-info {chapter} {chapterParentManga}");
//Request URLs for Chapter-Images
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest($"https://api.mangadex.org/at-home/server/{chapter.url}?forcePort443=false'", (byte)RequestType.AtHomeServer);
@@ -233,15 +253,15 @@ public class MangaDex : Connector
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
//Download Chapter-Images
- return DownloadChapterImages(imageUrls.ToArray(), chapter.GetArchiveFilePath(settings.downloadLocation), (byte)RequestType.AtHomeServer, parentTask, comicInfoPath, cancellationToken:cancellationToken);
+ return DownloadChapterImages(imageUrls.ToArray(), chapter.GetArchiveFilePath(settings.downloadLocation), (byte)RequestType.AtHomeServer, comicInfoPath, progressToken:progressToken);
}
private string? GetCoverUrl(string publicationId, string? posterId)
{
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting CoverUrl for {publicationId}");
+ Log($"Getting CoverUrl for Publication {publicationId}");
if (posterId is null)
{
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"No posterId, aborting");
+ Log("No cover.");
return null;
}
@@ -257,12 +277,13 @@ public class MangaDex : Connector
string fileName = result["data"]!["attributes"]!["fileName"]!.GetValue();
string coverUrl = $"https://uploads.mangadex.org/covers/{publicationId}/{fileName}";
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Got Cover-Url for {publicationId} -> {coverUrl}");
+ Log($"Cover-Url {publicationId} -> {coverUrl}");
return coverUrl;
}
private List GetAuthors(IEnumerable authorIds)
{
+ Log("Retrieving authors.");
List ret = new();
foreach (string authorId in authorIds)
{
@@ -276,7 +297,7 @@ public class MangaDex : Connector
string authorName = result["data"]!["attributes"]!["name"]!.GetValue();
ret.Add(authorName);
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Got author {authorId} -> {authorName}");
+ Log($"Got author {authorId} -> {authorName}");
}
return ret;
}
diff --git a/Tranga/Connectors/MangaKatana.cs b/Tranga/MangaConnectors/MangaKatana.cs
similarity index 71%
rename from Tranga/Connectors/MangaKatana.cs
rename to Tranga/MangaConnectors/MangaKatana.cs
index 6205d38..3000ea8 100644
--- a/Tranga/Connectors/MangaKatana.cs
+++ b/Tranga/MangaConnectors/MangaKatana.cs
@@ -2,32 +2,32 @@
using System.Net;
using System.Text.RegularExpressions;
using HtmlAgilityPack;
-using Tranga.TrangaTasks;
+using Tranga.Jobs;
-namespace Tranga.Connectors;
+namespace Tranga.MangaConnectors;
-public class MangaKatana : Connector
+public class MangaKatana : MangaConnector
{
public override string name { get; }
- public MangaKatana(TrangaSettings settings, CommonObjects commonObjects) : base(settings, commonObjects)
+ public MangaKatana(GlobalBase clone) : base(clone)
{
this.name = "MangaKatana";
- this.downloadClient = new DownloadClient(new Dictionary()
+ this.downloadClient = new DownloadClient(clone, new Dictionary()
{
{1, 60}
- }, commonObjects.logger);
+ });
}
- protected override Publication[] GetPublicationsInternal(string publicationTitle = "")
+ public override Manga[] GetManga(string publicationTitle = "")
{
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Publications (title={publicationTitle})");
+ Log($"Searching Publications. Term=\"{publicationTitle}\"");
string sanitizedTitle = string.Join('_', Regex.Matches(publicationTitle, "[A-z]*").Where(m => m.Value.Length > 0)).ToLower();
string requestUrl = $"https://mangakatana.com/?search={sanitizedTitle}&search_by=book_name";
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, 1);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
- return Array.Empty();
+ return Array.Empty();
// ReSharper disable once MergeIntoPattern
// If a single result is found, the user will be redirected to the results directly instead of a result page
@@ -38,10 +38,21 @@ public class MangaKatana : Connector
return new [] { ParseSinglePublicationFromHtml(requestResult.result, requestResult.redirectedToUrl.Split('/')[^1]) };
}
- return ParsePublicationsFromHtml(requestResult.result);
+ Manga[] publications = ParsePublicationsFromHtml(requestResult.result);
+ Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
+ return publications;
}
- private Publication[] ParsePublicationsFromHtml(Stream html)
+ public override Manga? GetMangaFromUrl(string url)
+ {
+ DownloadClient.RequestResult requestResult =
+ downloadClient.MakeRequest(url, 1);
+ if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
+ return null;
+ return ParseSinglePublicationFromHtml(requestResult.result, url.Split('/')[^1]);
+ }
+
+ private Manga[] ParsePublicationsFromHtml(Stream html)
{
StreamReader reader = new(html);
string htmlString = reader.ReadToEnd();
@@ -49,7 +60,7 @@ public class MangaKatana : Connector
document.LoadHtml(htmlString);
IEnumerable searchResults = document.DocumentNode.SelectNodes("//*[@id='book_list']/div");
if (searchResults is null || !searchResults.Any())
- return Array.Empty();
+ return Array.Empty();
List urls = new();
foreach (HtmlNode mangaResult in searchResults)
{
@@ -57,21 +68,18 @@ public class MangaKatana : Connector
.First(a => a.Name == "href").Value);
}
- HashSet ret = new();
+ HashSet ret = new();
foreach (string url in urls)
{
- DownloadClient.RequestResult requestResult =
- downloadClient.MakeRequest(url, 1);
- if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
- return Array.Empty();
-
- ret.Add(ParseSinglePublicationFromHtml(requestResult.result, url.Split('/')[^1]));
+ Manga? manga = GetMangaFromUrl(url);
+ if (manga is not null)
+ ret.Add((Manga)manga);
}
return ret.ToArray();
}
- private Publication ParseSinglePublicationFromHtml(Stream html, string publicationId)
+ private Manga ParseSinglePublicationFromHtml(Stream html, string publicationId)
{
StreamReader reader = new(html);
string htmlString = reader.ReadToEnd();
@@ -131,14 +139,16 @@ public class MangaKatana : Connector
year = Convert.ToInt32(yearString);
}
- return new Publication(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
+ Manga manga = new (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
year, originalLanguage, status, publicationId);
+ cachedPublications.Add(manga);
+ return manga;
}
- public override Chapter[] GetChapters(Publication publication, string language = "")
+ public override Chapter[] GetChapters(Manga manga, string language="en")
{
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Chapters for {publication.sortName} {publication.internalId} (language={language})");
- string requestUrl = $"https://mangakatana.com/manga/{publication.publicationId}";
+ Log($"Getting chapters {manga}");
+ string requestUrl = $"https://mangakatana.com/manga/{manga.publicationId}";
// Leaving this in for verification if the page exists
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, 1);
@@ -150,12 +160,12 @@ public class MangaKatana : Connector
{
NumberDecimalSeparator = "."
};
- List chapters = ParseChaptersFromHtml(publication, requestUrl);
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Done getting Chapters for {publication.internalId}");
+ List chapters = ParseChaptersFromHtml(manga, requestUrl);
+ Log($"Got {chapters.Count} chapters. {manga}");
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
}
- private List ParseChaptersFromHtml(Publication publication, string mangaUrl)
+ private List ParseChaptersFromHtml(Manga manga, string mangaUrl)
{
// Using HtmlWeb will include the chapters since they are loaded with js
HtmlWeb web = new();
@@ -174,17 +184,18 @@ public class MangaKatana : Connector
string chapterName = string.Concat(fullString.Split(':')[1..]);
string url = chapterInfo.Descendants("a").First()
.GetAttributeValue("href", "");
- ret.Add(new Chapter(publication, chapterName, volumeNumber, chapterNumber, url));
+ ret.Add(new Chapter(manga, chapterName, volumeNumber, chapterNumber, url));
}
return ret;
}
- public override HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null)
+ public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
{
- if (cancellationToken?.IsCancellationRequested ?? false)
+ if (progressToken?.cancellationRequested ?? false)
return HttpStatusCode.RequestTimeout;
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
+ Manga chapterParentManga = chapter.parentManga;
+ Log($"Retrieving chapter-info {chapter} {chapterParentManga}");
string requestUrl = chapter.url;
// Leaving this in to check if the page exists
DownloadClient.RequestResult requestResult =
@@ -197,7 +208,7 @@ public class MangaKatana : Connector
string comicInfoPath = Path.GetTempFileName();
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
- return DownloadChapterImages(imageUrls, chapter.GetArchiveFilePath(settings.downloadLocation), 1, parentTask, comicInfoPath, "https://mangakatana.com/", cancellationToken);
+ return DownloadChapterImages(imageUrls, chapter.GetArchiveFilePath(settings.downloadLocation), 1, comicInfoPath, "https://mangakatana.com/", progressToken:progressToken);
}
private string[] ParseImageUrlsFromHtml(string mangaUrl)
diff --git a/Tranga/Connectors/Manganato.cs b/Tranga/MangaConnectors/Manganato.cs
similarity index 71%
rename from Tranga/Connectors/Manganato.cs
rename to Tranga/MangaConnectors/Manganato.cs
index cfc9791..fedd909 100644
--- a/Tranga/Connectors/Manganato.cs
+++ b/Tranga/MangaConnectors/Manganato.cs
@@ -2,37 +2,39 @@
using System.Net;
using System.Text.RegularExpressions;
using HtmlAgilityPack;
-using Tranga.TrangaTasks;
+using Tranga.Jobs;
-namespace Tranga.Connectors;
+namespace Tranga.MangaConnectors;
-public class Manganato : Connector
+public class Manganato : MangaConnector
{
public override string name { get; }
-
- public Manganato(TrangaSettings settings, CommonObjects commonObjects) : base(settings, commonObjects)
+
+ public Manganato(GlobalBase clone) : base(clone)
{
this.name = "Manganato";
- this.downloadClient = new DownloadClient(new Dictionary()
+ this.downloadClient = new DownloadClient(clone, new Dictionary()
{
{1, 60}
- }, commonObjects.logger);
+ });
}
- protected override Publication[] GetPublicationsInternal(string publicationTitle = "")
+ public override Manga[] GetManga(string publicationTitle = "")
{
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Publications (title={publicationTitle})");
+ Log($"Searching Publications. Term=\"{publicationTitle}\"");
string sanitizedTitle = string.Join('_', Regex.Matches(publicationTitle, "[A-z]*")).ToLower();
string requestUrl = $"https://manganato.com/search/story/{sanitizedTitle}";
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, 1);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
- return Array.Empty();
+ return Array.Empty();
- return ParsePublicationsFromHtml(requestResult.result);
+ Manga[] publications = ParsePublicationsFromHtml(requestResult.result);
+ Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
+ return publications;
}
- private Publication[] ParsePublicationsFromHtml(Stream html)
+ private Manga[] ParsePublicationsFromHtml(Stream html)
{
StreamReader reader = new (html);
string htmlString = reader.ReadToEnd();
@@ -46,21 +48,28 @@ public class Manganato : Connector
.First(a => a.Name == "href").Value);
}
- HashSet ret = new();
+ HashSet ret = new();
foreach (string url in urls)
{
- DownloadClient.RequestResult requestResult =
- downloadClient.MakeRequest(url, 1);
- if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
- return Array.Empty();
-
- ret.Add(ParseSinglePublicationFromHtml(requestResult.result, url.Split('/')[^1]));
+ Manga? manga = GetMangaFromUrl(url);
+ if (manga is not null)
+ ret.Add((Manga)manga);
}
return ret.ToArray();
}
- private Publication ParseSinglePublicationFromHtml(Stream html, string publicationId)
+ public override Manga? GetMangaFromUrl(string url)
+ {
+ DownloadClient.RequestResult requestResult =
+ downloadClient.MakeRequest(url, 1);
+ if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
+ return null;
+
+ return ParseSinglePublicationFromHtml(requestResult.result, url.Split('/')[^1]);
+ }
+
+ private Manga ParseSinglePublicationFromHtml(Stream html, string publicationId)
{
StreamReader reader = new (html);
string htmlString = reader.ReadToEnd();
@@ -119,14 +128,16 @@ public class Manganato : Connector
.First(s => s.HasClass("chapter-time")).InnerText;
int year = Convert.ToInt32(yearString.Split(',')[^1]) + 2000;
- return new Publication(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
+ Manga manga = new (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
year, originalLanguage, status, publicationId);
+ cachedPublications.Add(manga);
+ return manga;
}
- public override Chapter[] GetChapters(Publication publication, string language = "")
+ public override Chapter[] GetChapters(Manga manga, string language="en")
{
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Chapters for {publication.sortName} {publication.internalId} (language={language})");
- string requestUrl = $"https://chapmanganato.com/{publication.publicationId}";
+ Log($"Getting chapters {manga}");
+ string requestUrl = $"https://chapmanganato.com/{manga.publicationId}";
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, 1);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
@@ -137,12 +148,12 @@ public class Manganato : Connector
{
NumberDecimalSeparator = "."
};
- List chapters = ParseChaptersFromHtml(publication, requestResult.result);
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Done getting Chapters for {publication.internalId}");
+ List chapters = ParseChaptersFromHtml(manga, requestResult.result);
+ Log($"Got {chapters.Count} chapters. {manga}");
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
}
- private List ParseChaptersFromHtml(Publication publication, Stream html)
+ private List ParseChaptersFromHtml(Manga manga, Stream html)
{
StreamReader reader = new (html);
string htmlString = reader.ReadToEnd();
@@ -161,17 +172,18 @@ public class Manganato : Connector
string chapterName = string.Concat(fullString.Split(':')[1..]);
string url = chapterInfo.Descendants("a").First(d => d.HasClass("chapter-name"))
.GetAttributeValue("href", "");
- ret.Add(new Chapter(publication, chapterName, volumeNumber, chapterNumber, url));
+ ret.Add(new Chapter(manga, chapterName, volumeNumber, chapterNumber, url));
}
ret.Reverse();
return ret;
}
- public override HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null)
+ public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
{
- if (cancellationToken?.IsCancellationRequested ?? false)
+ if (progressToken?.cancellationRequested ?? false)
return HttpStatusCode.RequestTimeout;
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
+ Manga chapterParentManga = chapter.parentManga;
+ Log($"Retrieving chapter-info {chapter} {chapterParentManga}");
string requestUrl = chapter.url;
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, 1);
@@ -183,7 +195,7 @@ public class Manganato : Connector
string comicInfoPath = Path.GetTempFileName();
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
- return DownloadChapterImages(imageUrls, chapter.GetArchiveFilePath(settings.downloadLocation), 1, parentTask, comicInfoPath, "https://chapmanganato.com/", cancellationToken);
+ return DownloadChapterImages(imageUrls, chapter.GetArchiveFilePath(settings.downloadLocation), 1, comicInfoPath, "https://chapmanganato.com/", progressToken:progressToken);
}
private string[] ParseImageUrlsFromHtml(Stream html)
diff --git a/Tranga/Connectors/Mangasee.cs b/Tranga/MangaConnectors/Mangasee.cs
similarity index 58%
rename from Tranga/Connectors/Mangasee.cs
rename to Tranga/MangaConnectors/Mangasee.cs
index ea9de48..ddf6153 100644
--- a/Tranga/Connectors/Mangasee.cs
+++ b/Tranga/MangaConnectors/Mangasee.cs
@@ -5,23 +5,23 @@ using System.Xml.Linq;
using HtmlAgilityPack;
using Newtonsoft.Json;
using PuppeteerSharp;
-using Tranga.TrangaTasks;
+using Tranga.Jobs;
-namespace Tranga.Connectors;
+namespace Tranga.MangaConnectors;
-public class Mangasee : Connector
+public class Mangasee : MangaConnector
{
public override string name { get; }
private IBrowser? _browser;
private const string ChromiumVersion = "1154303";
- public Mangasee(TrangaSettings settings, CommonObjects commonObjects) : base(settings, commonObjects)
+ public Mangasee(GlobalBase clone) : base(clone)
{
this.name = "Mangasee";
- this.downloadClient = new DownloadClient(new Dictionary()
+ this.downloadClient = new DownloadClient(clone, new Dictionary()
{
{ 1, 60 }
- }, commonObjects.logger);
+ });
Task d = new Task(DownloadBrowser);
d.Start();
@@ -34,31 +34,29 @@ public class Mangasee : Connector
browserFetcher.Remove(rev);
if (!browserFetcher.LocalRevisions().Contains(ChromiumVersion))
{
- commonObjects.logger?.WriteLine(this.GetType().ToString(), "Downloading headless browser");
+ Log("Downloading headless browser");
DateTime last = DateTime.Now.Subtract(TimeSpan.FromSeconds(5));
browserFetcher.DownloadProgressChanged += (_, args) =>
{
double currentBytes = Convert.ToDouble(args.BytesReceived) / Convert.ToDouble(args.TotalBytesToReceive);
if (args.TotalBytesToReceive == args.BytesReceived)
+ Log("Browser downloaded.");
+ else if (DateTime.Now > last.AddSeconds(1))
{
- commonObjects.logger?.WriteLine(this.GetType().ToString(), "Browser downloaded.");
- }
- else if (DateTime.Now > last.AddSeconds(5))
- {
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Browser download progress: {currentBytes:P2}");
+ Log($"Browser download progress: {currentBytes:P2}");
last = DateTime.Now;
}
};
if (!browserFetcher.CanDownloadAsync(ChromiumVersion).Result)
{
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Can't download browser version {ChromiumVersion}");
- return;
+ Log($"Can't download browser version {ChromiumVersion}");
+ throw new Exception();
}
await browserFetcher.DownloadAsync(ChromiumVersion);
}
- commonObjects.logger?.WriteLine(this.GetType().ToString(), "Starting browser.");
+ Log("Starting Browser.");
this._browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
Headless = true,
@@ -71,19 +69,43 @@ public class Mangasee : Connector
});
}
- protected override Publication[] GetPublicationsInternal(string publicationTitle = "")
+ public override Manga[] GetManga(string publicationTitle = "")
{
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Publications (title={publicationTitle})");
+ Log($"Searching Publications. Term=\"{publicationTitle}\"");
string requestUrl = $"https://mangasee123.com/_search.php";
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, 1);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
- return Array.Empty();
+ return Array.Empty();
- return ParsePublicationsFromHtml(requestResult.result, publicationTitle);
+ Manga[] publications = ParsePublicationsFromHtml(requestResult.result, publicationTitle);
+ Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
+ return publications;
}
- private Publication[] ParsePublicationsFromHtml(Stream html, string publicationTitle)
+ public override Manga? GetMangaFromUrl(string url)
+ {
+ while (this._browser is null)
+ {
+ Log("Waiting for headless browser to download...");
+ Thread.Sleep(1000);
+ }
+
+
+ IPage page = _browser!.NewPageAsync().Result;
+ IResponse response = page.GoToAsync(url, WaitUntilNavigation.DOMContentLoaded).Result;
+ if (response.Ok)
+ {
+ HtmlDocument document = new();
+ document.LoadHtml(page.GetContentAsync().Result);
+ page.CloseAsync();
+ return ParseSinglePublicationFromHtml(document);
+ }
+
+ return null;
+ }
+
+ private Manga[] ParsePublicationsFromHtml(Stream html, string publicationTitle)
{
string jsonString = new StreamReader(html).ReadToEnd();
List result = JsonConvert.DeserializeObject>(jsonString)!;
@@ -98,80 +120,60 @@ public class Mangasee : Connector
queryFiltered = queryFiltered.Where(item => item.Value >= publicationTitle.Split(' ').Length - 1)
.ToDictionary(item => item.Key, item => item.Value);
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Got {queryFiltered.Count} Publications (title={publicationTitle})");
+ Log($"Retrieved {queryFiltered.Count} publications.");
- HashSet ret = new();
+ HashSet ret = new();
List orderedFiltered =
queryFiltered.OrderBy(item => item.Value).ToDictionary(item => item.Key, item => item.Value).Keys.ToList();
- uint index = 1;
foreach (SearchResultItem orderedItem in orderedFiltered)
{
- DownloadClient.RequestResult requestResult =
- downloadClient.MakeRequest($"https://mangasee123.com/manga/{orderedItem.i}", 1);
- if ((int)requestResult.statusCode >= 200 || (int)requestResult.statusCode < 300)
- {
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Retrieving Publication info: {orderedItem.s} {index++}/{orderedFiltered.Count}");
- ret.Add(ParseSinglePublicationFromHtml(requestResult.result, orderedItem.s, orderedItem.i, orderedItem.a));
- }
+ Manga? manga = GetMangaFromUrl($"https://mangasee123.com/manga/{orderedItem.i}");
+ if (manga is not null)
+ ret.Add((Manga)manga);
}
return ret.ToArray();
}
- private Publication ParseSinglePublicationFromHtml(Stream html, string sortName, string publicationId, string[] a)
+ private Manga ParseSinglePublicationFromHtml(HtmlDocument document)
{
- StreamReader reader = new (html);
- HtmlDocument document = new ();
- document.LoadHtml(reader.ReadToEnd());
-
string originalLanguage = "", status = "";
Dictionary altTitles = new(), links = new();
HashSet tags = new();
- HtmlNode posterNode =
- document.DocumentNode.Descendants("img").First(img => img.HasClass("img-fluid") && img.HasClass("bottom-5"));
+ HtmlNode posterNode = document.DocumentNode.SelectSingleNode("//div[@class='BoxBody']//div[@class='row']//img");
string posterUrl = posterNode.GetAttributeValue("src", "");
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, 1);
- HtmlNode attributes = document.DocumentNode.Descendants("div")
- .First(div => div.HasClass("col-md-9") && div.HasClass("col-sm-8") && div.HasClass("top-5"))
- .Descendants("ul").First();
+ HtmlNode titleNode = document.DocumentNode.SelectSingleNode("//div[@class='BoxBody']//div[@class='row']//h1");
+ string sortName = titleNode.InnerText;
+ string publicationId = sortName;
- HtmlNode[] authorsNodes = attributes.Descendants("li")
- .First(node => node.InnerText.Contains("author(s):", StringComparison.CurrentCultureIgnoreCase))
- .Descendants("a").ToArray();
+ HtmlNode[] authorsNodes = document.DocumentNode.SelectNodes("//div[@class='BoxBody']//div[@class='row']//span[text()='Author(s):']/..").Descendants("a").ToArray();
List authors = new();
foreach(HtmlNode authorNode in authorsNodes)
authors.Add(authorNode.InnerText);
-
- HtmlNode[] genreNodes = attributes.Descendants("li")
- .First(node => node.InnerText.Contains("genre(s):", StringComparison.CurrentCultureIgnoreCase))
- .Descendants("a").ToArray();
+
+ HtmlNode[] genreNodes = document.DocumentNode.SelectNodes("//div[@class='BoxBody']//div[@class='row']//span[text()='Genre(s):']/..").Descendants("a").ToArray();
foreach (HtmlNode genreNode in genreNodes)
tags.Add(genreNode.InnerText);
-
- HtmlNode yearNode = attributes.Descendants("li")
- .First(node => node.InnerText.Contains("released:", StringComparison.CurrentCultureIgnoreCase))
- .Descendants("a").First();
+
+ HtmlNode yearNode = document.DocumentNode.SelectNodes("//div[@class='BoxBody']//div[@class='row']//span[text()='Released:']/..").Descendants("a").First();
int year = Convert.ToInt32(yearNode.InnerText);
-
- HtmlNode[] statusNodes = attributes.Descendants("li")
- .First(node => node.InnerText.Contains("status:", StringComparison.CurrentCultureIgnoreCase))
- .Descendants("a").ToArray();
+
+ HtmlNode[] statusNodes = document.DocumentNode.SelectNodes("//div[@class='BoxBody']//div[@class='row']//span[text()='Status:']/..").Descendants("a").ToArray();
foreach(HtmlNode statusNode in statusNodes)
if (statusNode.InnerText.Contains("publish", StringComparison.CurrentCultureIgnoreCase))
status = statusNode.InnerText.Split(' ')[0];
- HtmlNode descriptionNode = attributes.Descendants("li").First(node => node.InnerText.Contains("description:", StringComparison.CurrentCultureIgnoreCase)).Descendants("div").First();
+ HtmlNode descriptionNode = document.DocumentNode.SelectNodes("//div[@class='BoxBody']//div[@class='row']//span[text()='Description:']/..").Descendants("div").First();
string description = descriptionNode.InnerText;
- int i = 0;
- foreach(string at in a)
- altTitles.Add((i++).ToString(), at);
-
- return new Publication(sortName, authors, description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
+ Manga manga = new (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
year, originalLanguage, status, publicationId);
+ cachedPublications.Add(manga);
+ return manga;
}
// ReSharper disable once ClassNeverInstantiated.Local Will be instantiated during deserialization
@@ -214,11 +216,12 @@ public class Mangasee : Connector
}
}
- public override Chapter[] GetChapters(Publication publication, string language = "")
+ public override Chapter[] GetChapters(Manga manga, string language="en")
{
- XDocument doc = XDocument.Load($"https://mangasee123.com/rss/{publication.publicationId}.xml");
+ Log($"Getting chapters {manga}");
+ XDocument doc = XDocument.Load($"https://mangasee123.com/rss/{manga.publicationId}.xml");
XElement[] chapterItems = doc.Descendants("item").ToArray();
- List ret = new();
+ List chapters = new();
foreach (XElement chapter in chapterItems)
{
string volumeNumber = "1";
@@ -227,7 +230,7 @@ public class Mangasee : Connector
string url = chapter.Descendants("link").First().Value;
url = url.Replace(Regex.Matches(url,"(-page-[0-9])")[0].ToString(),"");
- ret.Add(new Chapter(publication, "", volumeNumber, chapterNumber, url));
+ chapters.Add(new Chapter(manga, "", volumeNumber, chapterNumber, url));
}
//Return Chapters ordered by Chapter-Number
@@ -235,23 +238,24 @@ public class Mangasee : Connector
{
NumberDecimalSeparator = "."
};
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Done getting Chapters for {publication.internalId}");
- return ret.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
+ Log($"Got {chapters.Count} chapters. {manga}");
+ return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
}
- public override HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null)
+ public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
{
- if (cancellationToken?.IsCancellationRequested ?? false)
+ if (progressToken?.cancellationRequested ?? false)
return HttpStatusCode.RequestTimeout;
- while (this._browser is null && !(cancellationToken?.IsCancellationRequested??false))
+ Manga chapterParentManga = chapter.parentManga;
+ while (this._browser is null && !(progressToken?.cancellationRequested??false))
{
- commonObjects.logger?.WriteLine(this.GetType().ToString(), "Waiting for headless browser to download...");
+ Log("Waiting for headless browser to download...");
Thread.Sleep(1000);
}
- if (cancellationToken?.IsCancellationRequested??false)
+ if (progressToken?.cancellationRequested??false)
return HttpStatusCode.RequestTimeout;
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
+ Log($"Retrieving chapter-info {chapter} {chapterParentManga}");
IPage page = _browser!.NewPageAsync().Result;
IResponse response = page.GoToAsync(chapter.url).Result;
if (response.Ok)
@@ -268,7 +272,7 @@ public class Mangasee : Connector
string comicInfoPath = Path.GetTempFileName();
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
- return DownloadChapterImages(urls.ToArray(), chapter.GetArchiveFilePath(settings.downloadLocation), 1, parentTask, comicInfoPath, cancellationToken:cancellationToken);
+ return DownloadChapterImages(urls.ToArray(), chapter.GetArchiveFilePath(settings.downloadLocation), 1, comicInfoPath, progressToken:progressToken);
}
return response.Status;
}
diff --git a/Tranga/Migrator.cs b/Tranga/Migrator.cs
deleted file mode 100644
index 0ac0e91..0000000
--- a/Tranga/Migrator.cs
+++ /dev/null
@@ -1,99 +0,0 @@
-using System.Text.Json.Nodes;
-using Logging;
-using Newtonsoft.Json;
-using Tranga.LibraryManagers;
-using Tranga.NotificationManagers;
-using Tranga.TrangaTasks;
-
-namespace Tranga;
-
-public static class Migrator
-{
- internal static readonly ushort CurrentVersion = 17;
- public static void Migrate(string settingsFilePath, Logger? logger)
- {
- if (!File.Exists(settingsFilePath))
- return;
- JsonNode settingsNode = JsonNode.Parse(File.ReadAllText(settingsFilePath))!;
- ushort version = settingsNode["version"] is not null
- ? settingsNode["version"]!.GetValue()
- : settingsNode["ts"]!["version"]!.GetValue();
- logger?.WriteLine("Migrator", $"Migrating {version} -> {CurrentVersion}");
- switch (version)
- {
- case 15:
- MoveToCommonObjects(settingsFilePath, logger);
- TrangaSettings.SettingsJsonObject sjo = JsonConvert.DeserializeObject(File.ReadAllText(settingsFilePath))!;
- RemoveUpdateLibraryTask(sjo.ts!, logger);
- break;
- case 16:
- MoveToCommonObjects(settingsFilePath, logger);
- break;
- }
-
- TrangaSettings.SettingsJsonObject sjo2 = JsonConvert.DeserializeObject(
- File.ReadAllText(settingsFilePath),
- new JsonSerializerSettings
- {
- Converters =
- {
- new TrangaTask.TrangaTaskJsonConverter(),
- new NotificationManager.NotificationManagerJsonConverter(),
- new LibraryManager.LibraryManagerJsonConverter()
- }
- })!;
- sjo2.ts!.version = CurrentVersion;
- sjo2.ts!.ExportSettings();
- }
-
- private static void RemoveUpdateLibraryTask(TrangaSettings settings, Logger? logger)
- {
- if (!File.Exists(settings.tasksFilePath))
- return;
-
- logger?.WriteLine("Migrator", "Removing old/deprecated UpdateLibraryTasks (v16)");
- string tasksJsonString = File.ReadAllText(settings.tasksFilePath);
- HashSet tasks = JsonConvert.DeserializeObject>(tasksJsonString,
- new JsonSerializerSettings { Converters = { new TrangaTask.TrangaTaskJsonConverter() } })!;
- tasks.RemoveWhere(t => t.task == TrangaTask.Task.UpdateLibraries);
- File.WriteAllText(settings.tasksFilePath, JsonConvert.SerializeObject(tasks));
- }
-
- public static void MoveToCommonObjects(string settingsFilePath, Logger? logger)
- {
- if (!File.Exists(settingsFilePath))
- return;
-
- logger?.WriteLine("Migrator", "Moving Settings to commonObjects-structure (v17)");
- JsonNode node = JsonNode.Parse(File.ReadAllText(settingsFilePath))!;
- TrangaSettings ts = new(
- node["downloadLocation"]!.GetValue(),
- node["workingDirectory"]!.GetValue());
- JsonArray libraryManagers = node["libraryManagers"]!.AsArray();
- logger?.WriteLine("Migrator", $"\tGot {libraryManagers.Count} libraryManagers.");
- JsonNode? komgaNode = libraryManagers.FirstOrDefault(lm => lm["libraryType"].GetValue() == (byte)LibraryManager.LibraryType.Komga);
- JsonNode? kavitaNode = libraryManagers.FirstOrDefault(lm => lm["libraryType"].GetValue() == (byte)LibraryManager.LibraryType.Kavita);
- HashSet lms = new();
- if (komgaNode is not null)
- lms.Add(new Komga(komgaNode["baseUrl"]!.GetValue(), komgaNode["auth"]!.GetValue(), null));
- if (kavitaNode is not null)
- lms.Add(new Kavita(kavitaNode["baseUrl"]!.GetValue(), kavitaNode["auth"]!.GetValue(), null));
-
- JsonArray notificationManagers = node["notificationManagers"]!.AsArray();
- logger?.WriteLine("Migrator", $"\tGot {notificationManagers.Count} notificationManagers.");
- JsonNode? gotifyNode = notificationManagers.FirstOrDefault(nm =>
- nm["notificationManagerType"].GetValue() == (byte)NotificationManager.NotificationManagerType.Gotify);
- JsonNode? lunaSeaNode = notificationManagers.FirstOrDefault(nm =>
- nm["notificationManagerType"].GetValue() == (byte)NotificationManager.NotificationManagerType.LunaSea);
- HashSet nms = new();
- if (gotifyNode is not null)
- nms.Add(new Gotify(gotifyNode["endpoint"]!.GetValue(), gotifyNode["appToken"]!.GetValue()));
- if (lunaSeaNode is not null)
- nms.Add(new LunaSea(lunaSeaNode["id"]!.GetValue()));
-
- CommonObjects co = new (lms, nms, logger, settingsFilePath);
-
- TrangaSettings.SettingsJsonObject sjo = new(ts, co);
- File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(sjo));
- }
-}
\ No newline at end of file
diff --git a/Tranga/NotificationManagers/Gotify.cs b/Tranga/NotificationConnectors/Gotify.cs
similarity index 77%
rename from Tranga/NotificationManagers/Gotify.cs
rename to Tranga/NotificationConnectors/Gotify.cs
index a823665..deb8b57 100644
--- a/Tranga/NotificationManagers/Gotify.cs
+++ b/Tranga/NotificationConnectors/Gotify.cs
@@ -1,10 +1,9 @@
using System.Text;
-using Logging;
using Newtonsoft.Json;
-namespace Tranga.NotificationManagers;
+namespace Tranga.NotificationConnectors;
-public class Gotify : NotificationManager
+public class Gotify : NotificationConnector
{
public string endpoint { get; }
// ReSharper disable once MemberCanBePrivate.Global
@@ -12,15 +11,20 @@ public class Gotify : NotificationManager
private readonly HttpClient _client = new();
[JsonConstructor]
- public Gotify(string endpoint, string appToken, Logger? logger = null) : base(NotificationManagerType.Gotify, logger)
+ public Gotify(GlobalBase clone, string endpoint, string appToken) : base(clone, NotificationConnectorType.Gotify)
{
this.endpoint = endpoint;
this.appToken = appToken;
}
-
+
+ public override string ToString()
+ {
+ return $"Gotify {endpoint}";
+ }
+
public override void SendNotification(string title, string notificationText)
{
- logger?.WriteLine(this.GetType().ToString(), $"Sending notification: {title} - {notificationText}");
+ Log($"Sending notification: {title} - {notificationText}");
MessageData message = new(title, notificationText);
HttpRequestMessage request = new(HttpMethod.Post, $"{endpoint}/message");
request.Headers.Add("X-Gotify-Key", this.appToken);
@@ -29,7 +33,7 @@ public class Gotify : NotificationManager
if (!response.IsSuccessStatusCode)
{
StreamReader sr = new (response.Content.ReadAsStream());
- logger?.WriteLine(this.GetType().ToString(), $"{response.StatusCode}: {sr.ReadToEnd()}");
+ Log($"{response.StatusCode}: {sr.ReadToEnd()}");
}
}
diff --git a/Tranga/NotificationManagers/LunaSea.cs b/Tranga/NotificationConnectors/LunaSea.cs
similarity index 73%
rename from Tranga/NotificationManagers/LunaSea.cs
rename to Tranga/NotificationConnectors/LunaSea.cs
index aef14c5..41c220a 100644
--- a/Tranga/NotificationManagers/LunaSea.cs
+++ b/Tranga/NotificationConnectors/LunaSea.cs
@@ -1,24 +1,28 @@
using System.Text;
-using Logging;
using Newtonsoft.Json;
-namespace Tranga.NotificationManagers;
+namespace Tranga.NotificationConnectors;
-public class LunaSea : NotificationManager
+public class LunaSea : NotificationConnector
{
// ReSharper disable once MemberCanBePrivate.Global
public string id { get; init; }
private readonly HttpClient _client = new();
[JsonConstructor]
- public LunaSea(string id, Logger? logger = null) : base(NotificationManagerType.LunaSea, logger)
+ public LunaSea(GlobalBase clone, string id) : base(clone, NotificationConnectorType.LunaSea)
{
this.id = id;
}
+ public override string ToString()
+ {
+ return $"LunaSea {id}";
+ }
+
public override void SendNotification(string title, string notificationText)
{
- logger?.WriteLine(this.GetType().ToString(), $"Sending notification: {title} - {notificationText}");
+ Log($"Sending notification: {title} - {notificationText}");
MessageData message = new(title, notificationText);
HttpRequestMessage request = new(HttpMethod.Post, $"https://notify.lunasea.app/v1/custom/{id}");
request.Content = new StringContent(JsonConvert.SerializeObject(message, Formatting.None), Encoding.UTF8, "application/json");
@@ -26,7 +30,7 @@ public class LunaSea : NotificationManager
if (!response.IsSuccessStatusCode)
{
StreamReader sr = new (response.Content.ReadAsStream());
- logger?.WriteLine(this.GetType().ToString(), $"{response.StatusCode}: {sr.ReadToEnd()}");
+ Log($"{response.StatusCode}: {sr.ReadToEnd()}");
}
}
diff --git a/Tranga/NotificationConnectors/NotificationConnector.cs b/Tranga/NotificationConnectors/NotificationConnector.cs
new file mode 100644
index 0000000..7734371
--- /dev/null
+++ b/Tranga/NotificationConnectors/NotificationConnector.cs
@@ -0,0 +1,15 @@
+namespace Tranga.NotificationConnectors;
+
+public abstract class NotificationConnector : GlobalBase
+{
+ public readonly NotificationConnectorType notificationConnectorType;
+
+ protected NotificationConnector(GlobalBase clone, NotificationConnectorType notificationConnectorType) : base(clone)
+ {
+ this.notificationConnectorType = notificationConnectorType;
+ }
+
+ public enum NotificationConnectorType : byte { Gotify = 0, LunaSea = 1 }
+
+ public abstract void SendNotification(string title, string notificationText);
+}
\ No newline at end of file
diff --git a/Tranga/NotificationConnectors/NotificationManagerJsonConverter.cs b/Tranga/NotificationConnectors/NotificationManagerJsonConverter.cs
new file mode 100644
index 0000000..5214183
--- /dev/null
+++ b/Tranga/NotificationConnectors/NotificationManagerJsonConverter.cs
@@ -0,0 +1,42 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Tranga.NotificationConnectors;
+
+public class NotificationManagerJsonConverter : JsonConverter
+{
+ private GlobalBase _clone;
+
+ public NotificationManagerJsonConverter(GlobalBase clone)
+ {
+ this._clone = clone;
+ }
+
+ public override bool CanConvert(Type objectType)
+ {
+ return (objectType == typeof(NotificationConnector));
+ }
+
+ public override object ReadJson(JsonReader reader, Type objectType, object? existingValue,
+ JsonSerializer serializer)
+ {
+ JObject jo = JObject.Load(reader);
+ if (jo["notificationConnectorType"]!.Value() == (byte)NotificationConnector.NotificationConnectorType.Gotify)
+ return new Gotify(this._clone, jo.GetValue("endpoint")!.Value()!, jo.GetValue("appToken")!.Value()!);
+ else if (jo["notificationConnectorType"]!.Value() ==
+ (byte)NotificationConnector.NotificationConnectorType.LunaSea)
+ return new LunaSea(this._clone, jo.GetValue("id")!.Value()!);
+
+ throw new Exception();
+ }
+
+ public override bool CanWrite => false;
+
+ ///
+ /// Don't call this
+ ///
+ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
+ {
+ throw new Exception("Dont call this");
+ }
+}
\ No newline at end of file
diff --git a/Tranga/NotificationManagers/NotificationManager.cs b/Tranga/NotificationManagers/NotificationManager.cs
deleted file mode 100644
index 7c65063..0000000
--- a/Tranga/NotificationManagers/NotificationManager.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-using Logging;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
-
-namespace Tranga.NotificationManagers;
-
-public abstract class NotificationManager
-{
- protected Logger? logger;
- public NotificationManagerType notificationManagerType;
-
- protected NotificationManager(NotificationManagerType notificationManagerType, Logger? logger = null)
- {
- this.notificationManagerType = notificationManagerType;
- this.logger = logger;
- }
-
- public enum NotificationManagerType : byte { Gotify = 0, LunaSea = 1 }
-
- public abstract void SendNotification(string title, string notificationText);
-
- public void AddLogger(Logger pLogger)
- {
- this.logger = pLogger;
- }
-
- public class NotificationManagerJsonConverter : JsonConverter
- {
- public override bool CanConvert(Type objectType)
- {
- return (objectType == typeof(NotificationManager));
- }
-
- public override object ReadJson(JsonReader reader, Type objectType, object? existingValue,
- JsonSerializer serializer)
- {
- JObject jo = JObject.Load(reader);
- if (jo["notificationManagerType"]!.Value() == (byte)NotificationManagerType.Gotify)
- return jo.ToObject(serializer)!;
- else if (jo["notificationManagerType"]!.Value() == (byte)NotificationManagerType.LunaSea)
- return jo.ToObject(serializer)!;
-
- throw new Exception();
- }
-
- public override bool CanWrite => false;
-
- ///
- /// Don't call this
- ///
- public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
- {
- throw new Exception("Dont call this");
- }
- }
-}
\ No newline at end of file
diff --git a/Tranga/Server.cs b/Tranga/Server.cs
new file mode 100644
index 0000000..40fdf1b
--- /dev/null
+++ b/Tranga/Server.cs
@@ -0,0 +1,468 @@
+using System.Net;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Text.RegularExpressions;
+using Newtonsoft.Json;
+using Tranga.Jobs;
+using Tranga.LibraryConnectors;
+using Tranga.MangaConnectors;
+using Tranga.NotificationConnectors;
+
+namespace Tranga;
+
+public class Server : GlobalBase
+{
+ private readonly HttpListener _listener = new ();
+ private readonly Tranga _parent;
+
+ public Server(Tranga parent) : base(parent)
+ {
+ this._parent = parent;
+ if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ this._listener.Prefixes.Add($"http://*:{settings.apiPortNumber}/");
+ else
+ this._listener.Prefixes.Add($"http://localhost:{settings.apiPortNumber}/");
+ Thread listenThread = new (Listen);
+ listenThread.Start();
+ Thread watchThread = new(WatchRunning);
+ watchThread.Start();
+ }
+
+ private void WatchRunning()
+ {
+ while(_parent.keepRunning)
+ Thread.Sleep(1000);
+ this._listener.Close();
+ }
+
+ private void Listen()
+ {
+ this._listener.Start();
+ foreach(string prefix in this._listener.Prefixes)
+ Log($"Listening on {prefix}");
+ while (this._listener.IsListening && _parent.keepRunning)
+ {
+ try
+ {
+ HttpListenerContext context = this._listener.GetContext();
+ //Log($"{context.Request.HttpMethod} {context.Request.Url} {context.Request.UserAgent}");
+ Task t = new(() =>
+ {
+ HandleRequest(context);
+ });
+ t.Start();
+ }
+ catch (HttpListenerException e)
+ {
+
+ }
+ }
+ }
+
+ private void HandleRequest(HttpListenerContext context)
+ {
+ HttpListenerRequest request = context.Request;
+ HttpListenerResponse response = context.Response;
+ if(request.HttpMethod == "OPTIONS")
+ SendResponse(HttpStatusCode.OK, context.Response);
+ if(request.Url!.LocalPath.Contains("favicon"))
+ SendResponse(HttpStatusCode.NoContent, response);
+
+ switch (request.HttpMethod)
+ {
+ case "GET":
+ HandleGet(request, response);
+ break;
+ case "POST":
+ HandlePost(request, response);
+ break;
+ case "DELETE":
+ HandleDelete(request, response);
+ break;
+ default:
+ SendResponse(HttpStatusCode.BadRequest, response);
+ break;
+ }
+ }
+
+ private Dictionary GetRequestVariables(string query)
+ {
+ Dictionary ret = new();
+ Regex queryRex = new (@"\?{1}&?([A-z0-9-=]+=[A-z0-9-=]+)+(&[A-z0-9-=]+=[A-z0-9-=]+)*");
+ if (!queryRex.IsMatch(query))
+ return ret;
+ query = query.Substring(1);
+ foreach (string keyValuePair in query.Split('&').Where(str => str.Length >= 3))
+ {
+ string var = keyValuePair.Split('=')[0];
+ string val = Regex.Replace(keyValuePair.Substring(var.Length + 1), "%20", " ");
+ val = Regex.Replace(val, "%[0-9]{2}", "");
+ ret.Add(var, val);
+ }
+ return ret;
+ }
+
+ private void HandleGet(HttpListenerRequest request, HttpListenerResponse response)
+ {
+ Dictionary requestVariables = GetRequestVariables(request.Url!.Query);
+ string? connectorName, jobId, internalId;
+ MangaConnector? connector;
+ Manga? manga;
+ string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value;
+ switch (path)
+ {
+ case "Connectors":
+ SendResponse(HttpStatusCode.OK, response, _parent.GetConnectors().Select(con => con.name).ToArray());
+ break;
+ case "Manga/Cover":
+ if (!requestVariables.TryGetValue("internalId", out internalId) ||
+ !_parent.TryGetPublicationById(internalId, out manga))
+ {
+ SendResponse(HttpStatusCode.BadRequest, response);
+ break;
+ }
+
+ string filePath = settings.GetFullCoverPath((Manga)manga!);
+ if (File.Exists(filePath))
+ {
+ FileStream coverStream = new(filePath, FileMode.Open);
+ SendResponse(HttpStatusCode.OK, response, coverStream);
+ }
+ else
+ {
+ SendResponse(HttpStatusCode.NotFound, response);
+ }
+ break;
+ case "Manga/FromConnector":
+ requestVariables.TryGetValue("title", out string? title);
+ requestVariables.TryGetValue("url", out string? url);
+ if (!requestVariables.TryGetValue("connector", out connectorName) ||
+ !_parent.TryGetConnector(connectorName, out connector) ||
+ (title is null && url is null))
+ {
+ SendResponse(HttpStatusCode.BadRequest, response);
+ break;
+ }
+
+ if (url is not null)
+ {
+ HashSet ret = new();
+ manga = connector!.GetMangaFromUrl(url);
+ if (manga is not null)
+ ret.Add((Manga)manga);
+ SendResponse(HttpStatusCode.OK, response, ret);
+ }else
+ SendResponse(HttpStatusCode.OK, response, connector!.GetManga(title!));
+ break;
+ case "Manga/Chapters":
+ if(!requestVariables.TryGetValue("connector", out connectorName) ||
+ !requestVariables.TryGetValue("internalId", out internalId) ||
+ !_parent.TryGetConnector(connectorName, out connector) ||
+ !_parent.TryGetPublicationById(internalId, out manga))
+ {
+ SendResponse(HttpStatusCode.BadRequest, response);
+ break;
+ }
+ SendResponse(HttpStatusCode.OK, response, connector!.GetChapters((Manga)manga!));
+ break;
+ case "Jobs":
+ if (!requestVariables.TryGetValue("jobId", out jobId))
+ {
+ if(!_parent.jobBoss.jobs.Any(jjob => jjob.id == jobId))
+ SendResponse(HttpStatusCode.BadRequest, response);
+ else
+ SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.First(jjob => jjob.id == jobId));
+ break;
+ }
+ SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs);
+ break;
+ case "Jobs/Progress":
+ if (!requestVariables.TryGetValue("jobId", out jobId))
+ {
+ if(!_parent.jobBoss.jobs.Any(jjob => jjob.id == jobId))
+ SendResponse(HttpStatusCode.BadRequest, response);
+ else
+ SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.First(jjob => jjob.id == jobId).progressToken);
+ break;
+ }
+ SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Select(jjob => jjob.progressToken));
+ break;
+ case "Jobs/Running":
+ SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Where(jjob => jjob.progressToken.state is ProgressToken.State.Running));
+ break;
+ case "Jobs/Waiting":
+ SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Where(jjob => jjob.progressToken.state is ProgressToken.State.Standby));
+ break;
+ case "Jobs/MonitorJobs":
+ SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Where(jjob => jjob is DownloadNewChapters));
+ break;
+ case "Settings":
+ SendResponse(HttpStatusCode.OK, response, settings);
+ break;
+ case "NotificationConnectors":
+ SendResponse(HttpStatusCode.OK, response, notificationConnectors);
+ break;
+ case "NotificationConnectors/Types":
+ SendResponse(HttpStatusCode.OK, response,
+ Enum.GetValues().Select(nc => new KeyValuePair((byte)nc, Enum.GetName(nc))));
+ break;
+ case "LibraryConnectors":
+ SendResponse(HttpStatusCode.OK, response, libraryConnectors);
+ break;
+ case "LibraryConnectors/Types":
+ SendResponse(HttpStatusCode.OK, response,
+ Enum.GetValues().Select(lc => new KeyValuePair((byte)lc, Enum.GetName(lc))));
+ break;
+ default:
+ SendResponse(HttpStatusCode.BadRequest, response);
+ break;
+ }
+ }
+
+ private void HandlePost(HttpListenerRequest request, HttpListenerResponse response)
+ {
+ Dictionary requestVariables = GetRequestVariables(request.Url!.Query);
+ string? connectorName, internalId, jobId;
+ MangaConnector connector;
+ Manga manga;
+ Job? job;
+ string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value;
+ switch (path)
+ {
+ case "Jobs/MonitorManga":
+ if(!requestVariables.TryGetValue("connector", out connectorName) ||
+ !requestVariables.TryGetValue("internalId", out internalId) ||
+ !requestVariables.TryGetValue("interval", out string? intervalStr) ||
+ _parent.GetConnector(connectorName) is null ||
+ _parent.GetPublicationById(internalId) is null ||
+ !TimeSpan.TryParse(intervalStr, out TimeSpan interval))
+ {
+ SendResponse(HttpStatusCode.BadRequest, response);
+ break;
+ }
+ connector = _parent.GetConnector(connectorName)!;
+ manga = (Manga)_parent.GetPublicationById(internalId)!;
+ _parent.jobBoss.AddJob(new DownloadNewChapters(this, connector, manga, true, interval));
+ SendResponse(HttpStatusCode.Accepted, response);
+ break;
+ case "Jobs/DownloadNewChapters":
+ if(!requestVariables.TryGetValue("connector", out connectorName) ||
+ !requestVariables.TryGetValue("internalId", out internalId) ||
+ _parent.GetConnector(connectorName) is null ||
+ _parent.GetPublicationById(internalId) is null)
+ {
+ SendResponse(HttpStatusCode.BadRequest, response);
+ break;
+ }
+ connector = _parent.GetConnector(connectorName)!;
+ manga = (Manga)_parent.GetPublicationById(internalId)!;
+ _parent.jobBoss.AddJob(new DownloadNewChapters(this, connector, manga, false));
+ SendResponse(HttpStatusCode.Accepted, response);
+ break;
+ case "Jobs/StartNow":
+ if (!requestVariables.TryGetValue("jobId", out jobId) ||
+ !_parent.jobBoss.TryGetJobById(jobId, out job))
+ {
+ SendResponse(HttpStatusCode.BadRequest, response);
+ break;
+ }
+ _parent.jobBoss.AddJobToQueue(job!);
+ SendResponse(HttpStatusCode.Accepted, response);
+ break;
+ case "Jobs/Cancel":
+ if (!requestVariables.TryGetValue("jobId", out jobId) ||
+ !_parent.jobBoss.TryGetJobById(jobId, out job))
+ {
+ SendResponse(HttpStatusCode.BadRequest, response);
+ break;
+ }
+ job!.Cancel();
+ SendResponse(HttpStatusCode.Accepted, response);
+ break;
+ case "Settings/UpdateDownloadLocation":
+ if (!requestVariables.TryGetValue("downloadLocation", out string? downloadLocation) ||
+ !requestVariables.TryGetValue("moveFiles", out string? moveFilesStr) ||
+ !Boolean.TryParse(moveFilesStr, out bool moveFiles))
+ {
+ SendResponse(HttpStatusCode.BadRequest, response);
+ break;
+ }
+ settings.UpdateDownloadLocation(downloadLocation, moveFiles);
+ SendResponse(HttpStatusCode.Accepted, response);
+ break;
+ /*case "Settings/UpdateWorkingDirectory":
+ if (!requestVariables.TryGetValue("workingDirectory", out string? workingDirectory))
+ {
+ SendResponse(HttpStatusCode.BadRequest, response);
+ break;
+ }
+ settings.UpdateWorkingDirectory(workingDirectory);
+ SendResponse(HttpStatusCode.Accepted, response);
+ break;*/
+ case "NotificationConnectors/Update":
+ if (!requestVariables.TryGetValue("notificationConnector", out string? notificationConnectorStr) ||
+ !Enum.TryParse(notificationConnectorStr, out NotificationConnector.NotificationConnectorType notificationConnectorType))
+ {
+ SendResponse(HttpStatusCode.BadRequest, response);
+ break;
+ }
+
+ if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Gotify)
+ {
+ if (!requestVariables.TryGetValue("gotifyUrl", out string? gotifyUrl) ||
+ !requestVariables.TryGetValue("gotifyAppToken", out string? gotifyAppToken))
+ {
+ SendResponse(HttpStatusCode.BadRequest, response);
+ break;
+ }
+ AddNotificationConnector(new Gotify(this, gotifyUrl, gotifyAppToken));
+ SendResponse(HttpStatusCode.Accepted, response);
+ break;
+ }
+
+ if (notificationConnectorType is NotificationConnector.NotificationConnectorType.LunaSea)
+ {
+ if (!requestVariables.TryGetValue("lunaseaWebhook", out string? lunaseaWebhook))
+ {
+ SendResponse(HttpStatusCode.BadRequest, response);
+ break;
+ }
+ AddNotificationConnector(new LunaSea(this, lunaseaWebhook));
+ SendResponse(HttpStatusCode.Accepted, response);
+ break;
+ }
+ break;
+ case "LibraryConnectors/Update":
+ if (!requestVariables.TryGetValue("libraryConnector", out string? libraryConnectorStr) ||
+ !Enum.TryParse(libraryConnectorStr,
+ out LibraryConnector.LibraryType libraryConnectorType))
+ {
+ SendResponse(HttpStatusCode.BadRequest, response);
+ break;
+ }
+
+ if (libraryConnectorType is LibraryConnector.LibraryType.Kavita)
+ {
+ if (!requestVariables.TryGetValue("kavitaUrl", out string? kavitaUrl) ||
+ !requestVariables.TryGetValue("kavitaUsername", out string? kavitaUsername) ||
+ !requestVariables.TryGetValue("kavitaPassword", out string? kavitaPassword))
+ {
+ SendResponse(HttpStatusCode.BadRequest, response);
+ break;
+ }
+ AddLibraryConnector(new Kavita(this, kavitaUrl, kavitaUsername, kavitaPassword));
+ SendResponse(HttpStatusCode.Accepted, response);
+ break;
+ }
+
+ if (libraryConnectorType is LibraryConnector.LibraryType.Komga)
+ {
+ if (!requestVariables.TryGetValue("komgaUrl", out string? komgaUrl) ||
+ !requestVariables.TryGetValue("komgaAuth", out string? komgaAuth))
+ {
+ SendResponse(HttpStatusCode.BadRequest, response);
+ break;
+ }
+ AddLibraryConnector(new Komga(this, komgaUrl, komgaAuth));
+ SendResponse(HttpStatusCode.Accepted, response);
+ break;
+ }
+ break;
+ default:
+ SendResponse(HttpStatusCode.BadRequest, response);
+ break;
+ }
+ }
+
+ private void HandleDelete(HttpListenerRequest request, HttpListenerResponse response)
+ {
+ Dictionary requestVariables = GetRequestVariables(request.Url!.Query);
+ string? connectorName, internalId;
+ MangaConnector connector;
+ Manga manga;
+ string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value;
+ switch (path)
+ {
+ case "Jobs":
+ if (!requestVariables.TryGetValue("jobId", out string? jobId) ||
+ !_parent.jobBoss.TryGetJobById(jobId, out Job? job))
+ {
+ SendResponse(HttpStatusCode.BadRequest, response);
+ break;
+ }
+ _parent.jobBoss.RemoveJob(job!);
+ SendResponse(HttpStatusCode.Accepted, response);
+ break;
+ case "Jobs/DownloadNewChapters":
+ if(!requestVariables.TryGetValue("connector", out connectorName) ||
+ !requestVariables.TryGetValue("internalId", out internalId) ||
+ _parent.GetConnector(connectorName) is null ||
+ _parent.GetPublicationById(internalId) is null)
+ {
+ SendResponse(HttpStatusCode.BadRequest, response);
+ break;
+ }
+ connector = _parent.GetConnector(connectorName)!;
+ manga = (Manga)_parent.GetPublicationById(internalId)!;
+ _parent.jobBoss.RemoveJobs(_parent.jobBoss.GetJobsLike(connector, manga));
+ SendResponse(HttpStatusCode.Accepted, response);
+ break;
+ case "NotificationConnectors":
+ if (!requestVariables.TryGetValue("notificationConnector", out string? notificationConnectorStr) ||
+ !Enum.TryParse(notificationConnectorStr, out NotificationConnector.NotificationConnectorType notificationConnectorType))
+ {
+ SendResponse(HttpStatusCode.BadRequest, response);
+ break;
+ }
+ DeleteNotificationConnector(notificationConnectorType);
+ SendResponse(HttpStatusCode.Accepted, response);
+ break;
+ case "LibraryConnectors":
+ if (!requestVariables.TryGetValue("libraryConnectors", out string? libraryConnectorStr) ||
+ !Enum.TryParse(libraryConnectorStr,
+ out LibraryConnector.LibraryType libraryConnectoryType))
+ {
+ SendResponse(HttpStatusCode.BadRequest, response);
+ break;
+ }
+ DeleteLibraryConnector(libraryConnectoryType);
+ SendResponse(HttpStatusCode.Accepted, response);
+ break;
+ default:
+ SendResponse(HttpStatusCode.BadRequest, response);
+ break;
+ }
+ }
+
+ private void SendResponse(HttpStatusCode statusCode, HttpListenerResponse response, object? content = null)
+ {
+ //Log($"Response: {statusCode} {content}");
+ response.StatusCode = (int)statusCode;
+ response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With");
+ response.AddHeader("Access-Control-Allow-Methods", "GET, POST, DELETE");
+ response.AddHeader("Access-Control-Max-Age", "1728000");
+ response.AppendHeader("Access-Control-Allow-Origin", "*");
+
+ if (content is not FileStream stream)
+ {
+ response.ContentType = "application/json";
+ try
+ {
+ response.OutputStream.Write(content is not null
+ ? Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content))
+ : Array.Empty());
+ response.OutputStream.Close();
+ }
+ catch (HttpListenerException e)
+ {
+ Log(e.ToString());
+ }
+ }
+ else
+ {
+ stream.CopyTo(response.OutputStream);
+ response.OutputStream.Close();
+ stream.Close();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tranga/TaskManager.cs b/Tranga/TaskManager.cs
deleted file mode 100644
index 518c5ad..0000000
--- a/Tranga/TaskManager.cs
+++ /dev/null
@@ -1,385 +0,0 @@
-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 HashSet collection = new();
- private HashSet _allTasks = new();
- private readonly Dictionary _runningTasks = new ();
- public bool _continueRunning = true;
- private readonly Connector[] _connectors;
- public TrangaSettings settings { get; }
- public CommonObjects commonObjects { get; init; }
-
- public TaskManager(TrangaSettings settings, Logging.Logger? logger)
- {
- commonObjects = CommonObjects.LoadSettings(settings.settingsFilePath, logger);
- commonObjects.logger?.WriteLine(this.GetType().ToString(), value: "\n"+
- @"-----------------------------------------------------------------"+"\n"+
- @" |¯¯¯¯¯¯|°|¯¯¯¯¯¯\ /¯¯¯¯¯¯| |¯¯¯\|¯¯¯| /¯¯¯¯¯¯\' /¯¯¯¯¯¯| "+"\n"+
- @" | | | x <|' / ! | | '| | (/¯¯¯\° / ! | "+ "\n"+
- @" ¯|__|¯ |__|\\__\\ /___/¯|_'| |___|\\__| \\_____/' /___/¯|_'| "+ "\n"+
- @"-----------------------------------------------------------------");
- this._connectors = new Connector[]
- {
- new MangaDex(settings, commonObjects),
- new Manganato(settings, commonObjects),
- new Mangasee(settings, commonObjects),
- new MangaKatana(settings, commonObjects)
- };
-
- 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()
- {
- commonObjects.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).OrderBy(enqueuedTask => enqueuedTask.nextExecution))
- {
- 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 timedOutTask in _runningTasks.Keys
- .Where(taskQuery => taskQuery.lastChange < DateTime.Now.Subtract(TimeSpan.FromMinutes(3))))
- {
- _runningTasks[timedOutTask].Cancel();
- timedOutTask.state = TrangaTask.ExecutionState.Failed;
- }
-
- foreach (TrangaTask finishedTask in _allTasks
- .Where(taskQuery => taskQuery.state is TrangaTask.ExecutionState.Success).ToArray())
- {
- if(finishedTask is DownloadChapterTask)
- {
- DeleteTask(finishedTask);
- finishedTask.state = TrangaTask.ExecutionState.Success;
- }
- else
- {
- finishedTask.state = TrangaTask.ExecutionState.Waiting;
- this._runningTasks.Remove(finishedTask);
- }
- }
-
- foreach (TrangaTask failedTask in _allTasks.Where(taskQuery =>
- taskQuery.state is TrangaTask.ExecutionState.Failed).ToArray())
- {
- DeleteTask(failedTask);
- TrangaTask newTask = failedTask.Clone();
- failedTask.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, cToken.Token);
- }, cToken.Token);
- _runningTasks.Add(task, cToken);
- t.Start();
- }
-
- public void AddTask(TrangaTask newTask)
- {
- switch (newTask.task)
- {
- case TrangaTask.Task.UpdateLibraries:
- //Only one UpdateKomgaLibrary Task
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Replacing old {newTask.task}-Task.");
- if (GetTasksMatching(newTask).FirstOrDefault() is { } exists)
- _allTasks.Remove(exists);
- _allTasks.Add(newTask);
- ExportDataAndSettings();
- break;
- default:
- if (!GetTasksMatching(newTask).Any())
- {
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Adding new Task {newTask}");
- _allTasks.Add(newTask);
- ExportDataAndSettings();
- }
- else
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Task already exists {newTask}");
- break;
- }
- }
-
- public void DeleteTask(TrangaTask removeTask)
- {
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Removing Task {removeTask}");
- if(_allTasks.Contains(removeTask))
- _allTasks.Remove(removeTask);
- removeTask.parentTask?.RemoveChildTask(removeTask);
- if (_runningTasks.ContainsKey(removeTask))
- {
- _runningTasks[removeTask].Cancel();
- _runningTasks.Remove(removeTask);
- }
- foreach(TrangaTask childTask in removeTask.childTasks)
- DeleteTask(childTask);
- ExportDataAndSettings();
- }
-
- // ReSharper disable once MemberCanBePrivate.Global
- public IEnumerable GetTasksMatching(TrangaTask mTask)
- {
- switch (mTask.task)
- {
- case TrangaTask.Task.UpdateLibraries:
- return GetTasksMatching(TrangaTask.Task.UpdateLibraries);
- case TrangaTask.Task.DownloadChapter:
- DownloadChapterTask dct = (DownloadChapterTask)mTask;
- return GetTasksMatching(TrangaTask.Task.DownloadChapter, connectorName: dct.connectorName,
- internalId: dct.publication.internalId, chapterNumber: dct.chapter.chapterNumber);
- case TrangaTask.Task.MonitorPublication:
- MonitorPublicationTask mpt = (MonitorPublicationTask)mTask;
- return GetTasksMatching(TrangaTask.Task.MonitorPublication, connectorName: mpt.connectorName,
- internalId: mpt.publication.internalId);
- }
- return Array.Empty