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(); - } - - public IEnumerable GetTasksMatching(TrangaTask.Task taskType, string? connectorName = null, string? searchString = null, string? internalId = null, string? chapterNumber = null) - { - switch (taskType) - { - case TrangaTask.Task.MonitorPublication: - if(connectorName is null) - return _allTasks.Where(tTask => tTask.task == taskType); - GetConnector(connectorName);//Name check - if (searchString is not null) - { - return _allTasks.Where(mTask => - mTask is MonitorPublicationTask mpt && mpt.connectorName == connectorName && - mpt.ToString().Contains(searchString, StringComparison.InvariantCultureIgnoreCase)); - } - else if (internalId is not null) - { - return _allTasks.Where(mTask => - mTask is MonitorPublicationTask mpt && mpt.connectorName == connectorName && - mpt.publication.internalId == internalId); - } - else - return _allTasks.Where(tTask => - tTask is MonitorPublicationTask mpt && mpt.connectorName == connectorName); - - case TrangaTask.Task.DownloadChapter: - if(connectorName is null) - return _allTasks.Where(tTask => tTask.task == taskType); - GetConnector(connectorName);//Name check - if (searchString is not null) - { - return _allTasks.Where(mTask => - mTask is DownloadChapterTask dct && dct.connectorName == connectorName && - dct.ToString().Contains(searchString, StringComparison.InvariantCultureIgnoreCase)); - } - else if (internalId is not null && chapterNumber is not null) - { - return _allTasks.Where(mTask => - mTask is DownloadChapterTask dct && dct.connectorName == connectorName && - dct.publication.internalId == internalId && - dct.chapter.chapterNumber == chapterNumber); - } - else - return _allTasks.Where(mTask => - mTask is DownloadChapterTask dct && dct.connectorName == connectorName); - - default: - return Array.Empty(); - } - } - - /// - /// Removes a Task from the queue - /// - /// - public void RemoveTaskFromQueue(TrangaTask task) - { - task.lastExecuted = DateTime.Now; - task.state = TrangaTask.ExecutionState.Waiting; - } - - /// - /// Sets last execution time to start of time - /// Let taskManager handle enqueuing - /// - /// - public void AddTaskToQueue(TrangaTask task) - { - task.lastExecuted = DateTime.UnixEpoch; - } - - /// All available Connectors - public Dictionary GetAvailableConnectors() - { - return this._connectors.ToDictionary(connector => connector.name, connector => connector); - } - - /// All TrangaTasks in task-collection - public TrangaTask[] GetAllTasks() - { - TrangaTask[] ret = new TrangaTask[_allTasks.Count]; - _allTasks.CopyTo(ret); - return ret; - } - - /// All added Publications - public Publication[] GetAllPublications() - { - return this.collection.ToArray(); - } - - public List GetExistingChaptersList(Connector connector, Publication publication, string language) - { - Chapter[] newChapters = connector.GetChapters(publication, language); - return newChapters.Where(nChapter => nChapter.CheckChapterIsDownloaded(settings.downloadLocation)).ToList(); - } - - /// - /// Return Connector with given Name - /// - /// Connector-name (exact) - /// If Connector is not available - public Connector GetConnector(string? connectorName) - { - if(connectorName is null) - throw new Exception($"connectorName can not be null"); - Connector? ret = this._connectors.FirstOrDefault(connector => connector.name == connectorName); - if (ret is null) - throw new Exception($"Connector {connectorName} is not an available Connector."); - return ret; - } - - /// - /// Shuts down the taskManager. - /// - /// If force is true, tasks are aborted. - public void Shutdown(bool force = false) - { - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Shutting down (forced={force})"); - _continueRunning = false; - ExportDataAndSettings(); - - if(force) - Environment.Exit(_allTasks.Count(task => task.state is TrangaTask.ExecutionState.Enqueued or TrangaTask.ExecutionState.Running)); - - //Wait for tasks to finish - while(_allTasks.Any(task => task.state is TrangaTask.ExecutionState.Running or TrangaTask.ExecutionState.Enqueued)) - Thread.Sleep(10); - commonObjects.logger?.WriteLine(this.GetType().ToString(), "Tasks finished. Bye!"); - Environment.Exit(0); - } - - private void ImportData() - { - commonObjects.logger?.WriteLine(this.GetType().ToString(), "Importing Data"); - if (File.Exists(settings.tasksFilePath)) - { - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Importing tasks from {settings.tasksFilePath}"); - string buffer = File.ReadAllText(settings.tasksFilePath); - this._allTasks = JsonConvert.DeserializeObject>(buffer, new JsonSerializerSettings() { Converters = { new TrangaTask.TrangaTaskJsonConverter() } })!; - } - - foreach (TrangaTask task in this._allTasks.Where(tTask => tTask.parentTaskId is not null).ToArray()) - { - TrangaTask? parentTask = this._allTasks.FirstOrDefault(pTask => pTask.taskId == task.parentTaskId); - if (parentTask is not null) - { - this.DeleteTask(task); - parentTask.lastExecuted = DateTime.UnixEpoch; - } - } - } - - /// - /// Exports data (settings, tasks) to file - /// - private void ExportDataAndSettings() - { - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Exporting settings to {settings.settingsFilePath}"); - settings.ExportSettings(); - - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Exporting tasks to {settings.tasksFilePath}"); - while(IsFileInUse(settings.tasksFilePath)) - Thread.Sleep(50); - File.WriteAllText(settings.tasksFilePath, JsonConvert.SerializeObject(this._allTasks)); - } - - private bool IsFileInUse(string path) - { - if (!File.Exists(path)) - return false; - try - { - using FileStream stream = new (path, FileMode.Open, FileAccess.Read, FileShare.None); - stream.Close(); - } - catch (IOException) - { - return true; - } - return false; - } -} \ No newline at end of file diff --git a/Tranga/Tranga.cs b/Tranga/Tranga.cs index 997a787..44c697f 100644 --- a/Tranga/Tranga.cs +++ b/Tranga/Tranga.cs @@ -1,580 +1,73 @@ -using System.Globalization; -using System.Runtime.InteropServices; -using Logging; -using Tranga.API; -using Tranga.Connectors; -using Tranga.NotificationManagers; -using Tranga.TrangaTasks; +using Logging; +using Tranga.Jobs; +using Tranga.MangaConnectors; namespace Tranga; -public static class Tranga +public partial class Tranga : GlobalBase { - public static void Main(string[] args) + public bool keepRunning; + public JobBoss jobBoss; + private Server _server; + private HashSet _connectors; + + public Tranga(Logger? logger, TrangaSettings settings) : base(logger, settings) { - bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); - string applicationFolderPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Tranga-API"); - - string downloadFolderPath = isLinux ? "/Manga" : Path.Join(applicationFolderPath, "Manga"); - string logsFolderPath = isLinux ? "/var/log/Tranga" : Path.Join(applicationFolderPath, "log"); - string logFilePath = Path.Join(logsFolderPath, $"log-{DateTime.Now:dd-M-yyyy-HH-mm-ss}.txt"); - string settingsFilePath = Path.Join(applicationFolderPath, "settings.json"); - - - Directory.CreateDirectory(logsFolderPath); - Logger logger = isLinux - ? new Logger(new[] { Logger.LoggerType.FileLogger, Logger.LoggerType.ConsoleLogger }, Console.Out, Console.Out.Encoding, logFilePath) - : new Logger(new[] { Logger.LoggerType.FileLogger }, Console.Out, Console.Out.Encoding, logFilePath); - - logger.WriteLine("Tranga",value: "\n"+ - "-------------------------------------------\n"+ - " Starting Tranga-API\n"+ - "-------------------------------------------"); - logger.WriteLine("Tranga", "Migrating..."); - Migrator.Migrate(settingsFilePath, logger); - - TrangaSettings settings; - if (File.Exists(settingsFilePath)) - { - logger.WriteLine("Tranga", $"Loading settings {settingsFilePath}"); - settings = TrangaSettings.LoadSettings(settingsFilePath); - } - else - { - settings = new TrangaSettings(downloadFolderPath, applicationFolderPath); - settings.version = Migrator.CurrentVersion; - } - - Directory.CreateDirectory(settings.workingDirectory); - Directory.CreateDirectory(settings.downloadLocation); - Directory.CreateDirectory(settings.coverImageCache); - - logger.WriteLine("Tranga", $"Is Linux: {isLinux}"); - logger.WriteLine("Tranga",$"Application-Folder: {settings.workingDirectory}"); - logger.WriteLine("Tranga",$"Settings-File-Path: {settings.settingsFilePath}"); - logger.WriteLine("Tranga",$"Download-Folder-Path: {settings.downloadLocation}"); - logger.WriteLine("Tranga",$"Logfile-Path: {logFilePath}"); - logger.WriteLine("Tranga",$"Image-Cache-Path: {settings.coverImageCache}"); - - logger.WriteLine("Tranga", "Loading Taskmanager."); - TaskManager taskManager = new (settings, logger); - - Server _ = new (6531, taskManager); - foreach(NotificationManager nm in taskManager.commonObjects.notificationManagers) - nm.SendNotification("Tranga-API", "Started Tranga-API"); - - if(!isLinux) - TaskMode(taskManager, logger); - } - - private static void TaskMode(TaskManager taskManager, Logger logger) - { - ConsoleKey selection = ConsoleKey.EraseEndOfFile; - PrintMenu(taskManager, taskManager.settings.downloadLocation); - while (selection != ConsoleKey.Q) - { - int taskCount = taskManager.GetAllTasks().Length; - int taskRunningCount = taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Running); - int taskEnqueuedCount = - taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Enqueued); - Console.SetCursorPosition(0,1); - Console.WriteLine($"Tasks (Running/Queue/Total)): {taskRunningCount}/{taskEnqueuedCount}/{taskCount}"); - - if (Console.KeyAvailable) - { - selection = Console.ReadKey().Key; - switch (selection) - { - case ConsoleKey.L: - while (!Console.KeyAvailable) - { - PrintTasks(taskManager.GetAllTasks(), logger); - Console.WriteLine("Press any key."); - Thread.Sleep(500); - } - Console.ReadKey(); - break; - case ConsoleKey.C: - CreateTask(taskManager); - Console.WriteLine("Press any key."); - Console.ReadKey(); - break; - case ConsoleKey.D: - DeleteTask(taskManager); - Console.WriteLine("Press any key."); - Console.ReadKey(); - break; - case ConsoleKey.E: - ExecuteTaskNow(taskManager); - Console.WriteLine("Press any key."); - Console.ReadKey(); - break; - case ConsoleKey.S: - SearchTasks(taskManager); - Console.WriteLine("Press any key."); - Console.ReadKey(); - break; - case ConsoleKey.R: - while (!Console.KeyAvailable) - { - PrintTasks( - taskManager.GetAllTasks().Where(eTask => eTask.state == TrangaTask.ExecutionState.Running) - .ToArray(), logger); - Console.WriteLine("Press any key."); - Thread.Sleep(500); - } - Console.ReadKey(); - break; - case ConsoleKey.K: - while (!Console.KeyAvailable) - { - PrintTasks( - taskManager.GetAllTasks() - .Where(qTask => qTask.state is TrangaTask.ExecutionState.Enqueued) - .ToArray(), logger); - Console.WriteLine("Press any key."); - Thread.Sleep(500); - } - Console.ReadKey(); - break; - case ConsoleKey.F: - TailLog(logger); - Console.ReadKey(); - break; - case ConsoleKey.G: - RemoveTaskFromQueue(taskManager, logger); - Console.WriteLine("Press any key."); - Console.ReadKey(); - break; - case ConsoleKey.B: - AddTaskToQueue(taskManager, logger); - Console.WriteLine("Press any key."); - Console.ReadKey(); - break; - case ConsoleKey.M: - AddMangaTaskToQueue(taskManager, logger); - Console.WriteLine("Press any key."); - Console.ReadKey(); - break; - } - PrintMenu(taskManager, taskManager.settings.downloadLocation); - } - Thread.Sleep(200); - } - - logger.WriteLine("Tranga_CLI", "Exiting."); - Console.Clear(); - Console.WriteLine("Exiting."); - if (taskManager.GetAllTasks().Any(task => task.state == TrangaTask.ExecutionState.Running)) - { - Console.WriteLine("Force quit (Even with running tasks?) y/N"); - selection = Console.ReadKey().Key; - while(selection != ConsoleKey.Y && selection != ConsoleKey.N) - selection = Console.ReadKey().Key; - taskManager.Shutdown(selection == ConsoleKey.Y); - }else - // ReSharper disable once RedundantArgumentDefaultValue Better readability - taskManager.Shutdown(false); - } - - private static void PrintMenu(TaskManager taskManager, string folderPath) - { - int taskCount = taskManager.GetAllTasks().Length; - int taskRunningCount = taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Running); - int taskEnqueuedCount = - taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Enqueued); - Console.Clear(); - Console.WriteLine($"Download Folder: {folderPath}"); - Console.WriteLine($"Tasks (Running/Queue/Total)): {taskRunningCount}/{taskEnqueuedCount}/{taskCount}"); - Console.WriteLine(); - Console.WriteLine($"{"C: Create Task",-30}{"L: List tasks",-30}{"B: Enqueue Task", -30}"); - Console.WriteLine($"{"D: Delete Task",-30}{"S: Search Tasks", -30}{"K: List Task Queue", -30}"); - Console.WriteLine($"{"E: Execute Task now",-30}{"R: List Running Tasks", -30}{"G: Remove Task from Queue", -30}"); - Console.WriteLine($"{"M: New Download Manga Task",-30}{"", -30}{"", -30}"); - Console.WriteLine($"{"",-30}{"F: Show Log",-30}{"Q: Exit",-30}"); - } - - private static void PrintTasks(TrangaTask[] tasks, Logger? logger) - { - logger?.WriteLine("Tranga_CLI", "Printing Tasks"); - int taskCount = tasks.Length; - int taskRunningCount = tasks.Count(task => task.state == TrangaTask.ExecutionState.Running); - int taskEnqueuedCount = tasks.Count(task => task.state == TrangaTask.ExecutionState.Enqueued); - Console.Clear(); - int tIndex = 0; - Console.WriteLine($"Tasks (Running/Queue/Total): {taskRunningCount}/{taskEnqueuedCount}/{taskCount}"); - string header = - $"{"",-5}{"Task",-20} | {"Last Executed",-20} | {"Reoccurrence",-12} | {"State",-10} | {"Progress",-9} | {"Finished",-20} | {"Remaining",-12} | {"Connector",-15} | Publication/Manga "; - Console.WriteLine(header); - Console.WriteLine(new string('-', header.Length)); - foreach (TrangaTask trangaTask in tasks) - { - string[] taskSplit = trangaTask.ToString().Split(", "); - Console.WriteLine($"{tIndex++:000}: {taskSplit[0],-20} | {taskSplit[1],-20} | {taskSplit[2],-12} | {taskSplit[3],-10} | {taskSplit[4],-9} | {taskSplit[5],-20} | {taskSplit[6][..12],-12} | {(taskSplit.Length > 7 ? taskSplit[7] : ""),-15} | {(taskSplit.Length > 8 ? taskSplit[8] : "")} {(taskSplit.Length > 9 ? taskSplit[9] : "")} {(taskSplit.Length > 10 ? taskSplit[10] : "")}"); - } - + keepRunning = true; + _connectors = new HashSet() + { + new Manganato(this), + new Mangasee(this), + new MangaDex(this), + new MangaKatana(this) + }; + jobBoss = new(this, this._connectors); + StartJobBoss(); + this._server = new Server(this); } - private static TrangaTask[] SelectTasks(TrangaTask[] tasks, Logger? logger) + public MangaConnector? GetConnector(string name) { - logger?.WriteLine("Tranga_CLI", "Menu: Select task"); - if (tasks.Length < 1) - { - Console.Clear(); - Console.WriteLine("There are no available Tasks."); - logger?.WriteLine("Tranga_CLI", "No available Tasks."); - return Array.Empty(); - } - PrintTasks(tasks, logger); - - logger?.WriteLine("Tranga_CLI", "Selecting Task to Remove (from queue)"); - Console.WriteLine("Enter q to abort"); - Console.WriteLine($"Select Task(s) (0-{tasks.Length - 1}):"); - - string? selectedTask = Console.ReadLine(); - while(selectedTask is null || selectedTask.Length < 1) - selectedTask = Console.ReadLine(); - - if (selectedTask.Length == 1 && selectedTask.ToLower() == "q") - { - Console.Clear(); - Console.WriteLine("aborted."); - logger?.WriteLine("Tranga_CLI", "aborted"); - return Array.Empty(); - } - - if (selectedTask.Contains('-')) - { - int start = Convert.ToInt32(selectedTask.Split('-')[0]); - int end = Convert.ToInt32(selectedTask.Split('-')[1]); - return tasks[start..end]; - } - else - { - int selectedTaskIndex = Convert.ToInt32(selectedTask); - return new[] { tasks[selectedTaskIndex] }; - } - } - - private static void AddMangaTaskToQueue(TaskManager taskManager, Logger logger) - { - Console.Clear(); - logger.WriteLine("Tranga_CLI", "Menu: Add Manga Download to queue"); - - Connector? connector = SelectConnector(taskManager.GetAvailableConnectors().Values.ToArray(), logger); - if (connector is null) - return; - - Publication? publication = SelectPublication(taskManager, connector); - if (publication is null) - return; - - TimeSpan reoccurrence = SelectReoccurrence(logger); - logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager"); - TrangaTask nTask = new MonitorPublicationTask(connector.name, (Publication)publication, reoccurrence, "en"); - taskManager.AddTask(nTask); - Console.WriteLine(nTask); - } - - private static void AddTaskToQueue(TaskManager taskManager, Logger logger) - { - Console.Clear(); - logger.WriteLine("Tranga_CLI", "Menu: Add Task to queue"); - - TrangaTask[] tasks = taskManager.GetAllTasks().Where(rTask => - rTask.state is not TrangaTask.ExecutionState.Enqueued and not TrangaTask.ExecutionState.Running).ToArray(); - - TrangaTask[] selectedTasks = SelectTasks(tasks, logger); - logger.WriteLine("Tranga_CLI", $"Sending {selectedTasks.Length} Tasks to TaskManager"); - foreach(TrangaTask task in selectedTasks) - taskManager.AddTaskToQueue(task); - } - - private static void RemoveTaskFromQueue(TaskManager taskManager, Logger logger) - { - Console.Clear(); - logger.WriteLine("Tranga_CLI", "Menu: Remove Task from queue"); - - TrangaTask[] tasks = taskManager.GetAllTasks().Where(rTask => rTask.state is TrangaTask.ExecutionState.Enqueued).ToArray(); - - TrangaTask[] selectedTasks = SelectTasks(tasks, logger); - logger.WriteLine("Tranga_CLI", $"Sending {selectedTasks.Length} Tasks to TaskManager"); - foreach(TrangaTask task in selectedTasks) - taskManager.RemoveTaskFromQueue(task); - } - - private static void TailLog(Logger logger) - { - logger.WriteLine("Tranga_CLI", "Menu: Show Log-lines"); - Console.Clear(); - - string[] lines = logger.Tail(20); - foreach (string message in lines) - Console.Write(message); - - while (!Console.KeyAvailable) - { - string[] newLines = logger.GetNewLines(); - foreach(string message in newLines) - Console.Write(message); - Thread.Sleep(40); - } - } - - private static void CreateTask(TaskManager taskManager) - { - taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "Menu: Creating Task"); - TrangaTask.Task? tmpTask = SelectTaskType(taskManager.commonObjects.logger); - if (tmpTask is null) - return; - TrangaTask.Task task = (TrangaTask.Task)tmpTask; - - Connector? connector = null; - if (task != TrangaTask.Task.UpdateLibraries) - { - connector = SelectConnector(taskManager.GetAvailableConnectors().Values.ToArray(), taskManager.commonObjects.logger); - if (connector is null) - return; - } - - Publication? publication = null; - if (task != TrangaTask.Task.UpdateLibraries) - { - publication = SelectPublication(taskManager, connector!); - if (publication is null) - return; - } - - if (task is TrangaTask.Task.MonitorPublication) - { - TimeSpan reoccurrence = SelectReoccurrence(taskManager.commonObjects.logger); - taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "Sending Task to TaskManager"); - - TrangaTask newTask = new MonitorPublicationTask(connector!.name, (Publication)publication!, reoccurrence, "en"); - taskManager.AddTask(newTask); - Console.WriteLine(newTask); - }else if (task is TrangaTask.Task.DownloadChapter) - { - foreach (Chapter chapter in SelectChapters(connector!, (Publication)publication!, taskManager.commonObjects.logger)) - { - TrangaTask newTask = new DownloadChapterTask(connector!.name, (Publication)publication, chapter, "en"); - taskManager.AddTask(newTask); - Console.WriteLine(newTask); - } - } - } - - private static void ExecuteTaskNow(TaskManager taskManager) - { - taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "Menu: Executing Task"); - TrangaTask[] tasks = taskManager.GetAllTasks().Where(nTask => nTask.state is not TrangaTask.ExecutionState.Running).ToArray(); - - TrangaTask[] selectedTasks = SelectTasks(tasks, taskManager.commonObjects.logger); - taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", $"Sending {selectedTasks.Length} Tasks to TaskManager"); - foreach(TrangaTask task in selectedTasks) - taskManager.ExecuteTaskNow(task); - } - - private static void DeleteTask(TaskManager taskManager) - { - taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "Menu: Delete Task"); - TrangaTask[] tasks = taskManager.GetAllTasks(); - - TrangaTask[] selectedTasks = SelectTasks(tasks, taskManager.commonObjects.logger); - taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", $"Sending {selectedTasks.Length} Tasks to TaskManager"); - foreach(TrangaTask task in selectedTasks) - taskManager.DeleteTask(task); - } - - private static TrangaTask.Task? SelectTaskType(Logger? logger) - { - logger?.WriteLine("Tranga_CLI", "Menu: Select TaskType"); - Console.Clear(); - string[] taskNames = Enum.GetNames(); - - int tIndex = 0; - Console.WriteLine("Available Tasks:"); - foreach (string taskName in taskNames) - Console.WriteLine($"{tIndex++}: {taskName}"); - - Console.WriteLine("Enter q to abort"); - Console.WriteLine($"Select Task (0-{taskNames.Length - 1}):"); - - string? selectedTask = Console.ReadLine(); - while(selectedTask is null || selectedTask.Length < 1) - selectedTask = Console.ReadLine(); - - if (selectedTask.Length == 1 && selectedTask.ToLower() == "q") - { - Console.Clear(); - Console.WriteLine("aborted."); - logger?.WriteLine("Tranga_CLI", "aborted."); - return null; - } - - try - { - int selectedTaskIndex = Convert.ToInt32(selectedTask); - string selectedTaskName = taskNames[selectedTaskIndex]; - return Enum.Parse(selectedTaskName); - } - catch (Exception e) - { - Console.WriteLine($"Exception: {e.Message}"); - logger?.WriteLine("Tranga_CLI", e.Message); - } - + foreach(MangaConnector mc in _connectors) + if (mc.name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) + return mc; return null; } - private static TimeSpan SelectReoccurrence(Logger? logger) + public bool TryGetConnector(string name, out MangaConnector? connector) { - logger?.WriteLine("Tranga_CLI", "Menu: Select Reoccurrence"); - Console.WriteLine("Select reoccurrence Timer (Format hh:mm:ss):"); - return TimeSpan.Parse(Console.ReadLine()!, new CultureInfo("en-US")); + connector = GetConnector(name); + return connector is not null; } - private static Chapter[] SelectChapters(Connector connector, Publication publication, Logger? logger) + public IEnumerable GetConnectors() { - logger?.WriteLine("Tranga_CLI", "Menu: Select Chapters"); - Chapter[] availableChapters = connector.GetChapters(publication, "en"); - int cIndex = 0; - Console.WriteLine("Chapters:"); - - System.Text.StringBuilder sb = new(); - foreach(Chapter chapter in availableChapters) - { - sb.Append($"{cIndex++}: "); - - if(string.IsNullOrWhiteSpace(chapter.volumeNumber) == false) - { - sb.Append($"Vol.{chapter.volumeNumber} "); - } - - if(string.IsNullOrWhiteSpace(chapter.chapterNumber) == false) - { - sb.Append($"Ch.{chapter.chapterNumber} "); - } - - if(string.IsNullOrWhiteSpace(chapter.name) == false) - { - sb.Append($" - {chapter.name}"); - } - - Console.WriteLine(sb.ToString()); - sb.Clear(); - } - - Console.WriteLine("Enter q to abort"); - Console.WriteLine($"Select Chapter(s):"); - - string? selectedChapters = Console.ReadLine(); - while(selectedChapters is null || selectedChapters.Length < 1) - selectedChapters = Console.ReadLine(); - - return connector.SelectChapters(publication, selectedChapters); + return _connectors; } - private static Connector? SelectConnector(Connector[] connectors, Logger? logger) + public Manga? GetPublicationById(string internalId) { - logger?.WriteLine("Tranga_CLI", "Menu: Select Connector"); - Console.Clear(); - - int cIndex = 0; - Console.WriteLine("Connectors:"); - foreach (Connector connector in connectors) - Console.WriteLine($"{cIndex++}: {connector.name}"); - - Console.WriteLine("Enter q to abort"); - Console.WriteLine($"Select Connector (0-{connectors.Length - 1}):"); - - string? selectedConnector = Console.ReadLine(); - while(selectedConnector is null || selectedConnector.Length < 1) - selectedConnector = Console.ReadLine(); - - if (selectedConnector.Length == 1 && selectedConnector.ToLower() == "q") - { - Console.Clear(); - Console.WriteLine("aborted."); - logger?.WriteLine("Tranga_CLI", "aborted."); - return null; - } - - try - { - int selectedConnectorIndex = Convert.ToInt32(selectedConnector); - return connectors[selectedConnectorIndex]; - } - catch (Exception e) - { - Console.WriteLine($"Exception: {e.Message}"); - logger?.WriteLine("Tranga_CLI", e.Message); - } - + if (cachedPublications.Exists(publication => publication.internalId == internalId)) + return cachedPublications.First(publication => publication.internalId == internalId); return null; } - private static Publication? SelectPublication(TaskManager taskManager, Connector connector) + public bool TryGetPublicationById(string internalId, out Manga? manga) { - taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "Menu: Select Publication"); - - Console.Clear(); - Console.WriteLine($"Connector: {connector.name}"); - Console.WriteLine("Publication search query (leave empty for all):"); - string? query = Console.ReadLine(); - - Publication[] publications = connector.GetPublications(ref taskManager.collection, query ?? ""); - - if (publications.Length < 1) - { - taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "No publications returned"); - Console.WriteLine($"No publications for query '{query}' returned;"); - return null; - } - - int pIndex = 0; - Console.WriteLine("Publications:"); - foreach(Publication publication in publications) - Console.WriteLine($"{pIndex++}: {publication.sortName}"); - - Console.WriteLine("Enter q to abort"); - Console.WriteLine($"Select publication to Download (0-{publications.Length - 1}):"); - - string? selectedPublication = Console.ReadLine(); - while(selectedPublication is null || selectedPublication.Length < 1) - selectedPublication = Console.ReadLine(); - - if (selectedPublication.Length == 1 && selectedPublication.ToLower() == "q") - { - Console.Clear(); - Console.WriteLine("aborted."); - taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "aborted."); - return null; - } - - try - { - int selectedPublicationIndex = Convert.ToInt32(selectedPublication); - return publications[selectedPublicationIndex]; - } - catch (Exception e) - { - Console.WriteLine($"Exception: {e.Message}"); - taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", e.Message); - } - - return null; + manga = GetPublicationById(internalId); + return manga is not null; } - private static void SearchTasks(TaskManager taskManager) + private void StartJobBoss() { - taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "Menu: Search task"); - Console.Clear(); - Console.WriteLine("Enter search query:"); - string? query = Console.ReadLine(); - while (query is null || query.Length < 4) - query = Console.ReadLine(); - PrintTasks(taskManager.GetAllTasks().Where(qTask => - qTask.ToString().ToLower().Contains(query, StringComparison.OrdinalIgnoreCase)).ToArray(), taskManager.commonObjects.logger); + Thread t = new (() => + { + while (keepRunning) + { + jobBoss.CheckJobs(); + Thread.Sleep(100); + } + }); + t.Start(); } } \ No newline at end of file diff --git a/Tranga/TrangaArgs.cs b/Tranga/TrangaArgs.cs new file mode 100644 index 0000000..23be84e --- /dev/null +++ b/Tranga/TrangaArgs.cs @@ -0,0 +1,136 @@ +using Logging; + +namespace Tranga; + +public partial class Tranga : GlobalBase +{ + + public static void Main(string[] args) + { + string[]? help = GetArg(args, ArgEnum.Help); + if (help is not null) + { + PrintHelp(); + return; + } + + string[]? consoleLogger = GetArg(args, ArgEnum.ConsoleLogger); + string[]? fileLogger = GetArg(args, ArgEnum.FileLogger); + string? filePath = fileLogger?[0];//TODO validate path + + List enabledLoggers = new(); + if(consoleLogger is not null) + enabledLoggers.Add(Logger.LoggerType.ConsoleLogger); + if (fileLogger is not null) + enabledLoggers.Add(Logger.LoggerType.FileLogger); + Logger logger = new(enabledLoggers.ToArray(), Console.Out, Console.OutputEncoding, filePath); + + TrangaSettings? settings = null; + string[]? downloadLocationPath = GetArg(args, ArgEnum.DownloadLocation); + string[]? workingDirectory = GetArg(args, ArgEnum.WorkingDirectory); + + if (downloadLocationPath is not null && workingDirectory is not null) + { + settings = new TrangaSettings(downloadLocationPath[0], workingDirectory[0]); + }else if (downloadLocationPath is not null) + { + if (settings is null) + settings = new TrangaSettings(downloadLocation: downloadLocationPath[0]); + else + settings = new TrangaSettings(downloadLocation: downloadLocationPath[0], settings.workingDirectory); + }else if (workingDirectory is not null) + { + if (settings is null) + settings = new TrangaSettings(downloadLocation: workingDirectory[0]); + else + settings = new TrangaSettings(settings.downloadLocation, workingDirectory[0]); + } + else + { + settings = new TrangaSettings(); + } + + Directory.CreateDirectory(settings.downloadLocation);//TODO validate path + Directory.CreateDirectory(settings.workingDirectory);//TODO validate path + + Tranga _ = new (logger, settings); + } + + private static void PrintHelp() + { + Console.WriteLine("Tranga-Help:"); + foreach (Argument argument in arguments.Values) + { + foreach(string name in argument.names) + Console.Write("{0} ", name); + if(argument.parameterCount > 0) + Console.Write($"<{argument.parameterCount}>"); + Console.Write("\r\n {0}\r\n", argument.helpText); + } + } + + /// + /// Returns an array containing the parameters for the argument. + /// + /// List of argument-strings + /// Requested parameter + /// + /// If there are no parameters for an argument, returns an empty array. + /// If the argument is not found returns null. + /// + private static string[]? GetArg(string[] args, ArgEnum arg) + { + List argsList = args.ToList(); + List ret = new(); + foreach (string name in arguments[arg].names) + { + int argIndex = argsList.IndexOf(name); + if (argIndex != -1) + { + if (arguments[arg].parameterCount == 0) + return ret.ToArray(); + for (int parameterIndex = 1; parameterIndex <= arguments[arg].parameterCount; parameterIndex++) + { + if(argIndex + parameterIndex >= argsList.Count || args[argIndex + parameterIndex].Contains('-'))//End of arguments, or no parameter provided, when one is required + Console.WriteLine($"No parameter provided for argument {name}. -h for help."); + ret.Add(args[argIndex + parameterIndex]); + } + } + } + return ret.Any() ? ret.ToArray() : null; + } + + private static Dictionary arguments = new() + { + { ArgEnum.DownloadLocation, new(new []{"-d", "--downloadLocation"}, 1, "Directory to which downloaded Manga are saved") }, + { ArgEnum.WorkingDirectory, new(new []{"-w", "--workingDirectory"}, 1, "Directory in which application-data is saved") }, + { ArgEnum.ConsoleLogger, new(new []{"-c", "--consoleLogger"}, 0, "Enables the consoleLogger") }, + { ArgEnum.FileLogger, new(new []{"-f", "--fileLogger"}, 1, "Enables the fileLogger, Directory where logfiles are saved") }, + { ArgEnum.Help, new(new []{"-h", "--help"}, 0, "Print this") } + //{ ArgEnum., new(new []{""}, 1, "") } + }; + + internal enum ArgEnum + { + TrangaSettings, + DownloadLocation, + WorkingDirectory, + ConsoleLogger, + FileLogger, + Help + } + + private struct Argument + { + public string[] names { get; } + public byte parameterCount { get; } + public string helpText { get; } + + public Argument(string[] names, byte parameterCount, string helpText) + { + this.names = names; + this.parameterCount = parameterCount; + this.helpText = helpText; + } + } +} \ No newline at end of file diff --git a/Tranga/TrangaSettings.cs b/Tranga/TrangaSettings.cs index a37de07..96e8fed 100644 --- a/Tranga/TrangaSettings.cs +++ b/Tranga/TrangaSettings.cs @@ -1,41 +1,111 @@ -using Newtonsoft.Json; -using Tranga.LibraryManagers; -using Tranga.NotificationManagers; +using System.Runtime.InteropServices; +using Newtonsoft.Json; +using Tranga.LibraryConnectors; +using Tranga.NotificationConnectors; +using static System.IO.UnixFileMode; namespace Tranga; public class TrangaSettings { public string downloadLocation { get; private set; } - public string workingDirectory { get; init; } + public string workingDirectory { get; private set; } + public int apiPortNumber { get; init; } [JsonIgnore] public string settingsFilePath => Path.Join(workingDirectory, "settings.json"); - [JsonIgnore] public string tasksFilePath => Path.Join(workingDirectory, "tasks.json"); + [JsonIgnore] public string libraryConnectorsFilePath => Path.Join(workingDirectory, "libraryConnectors.json"); + [JsonIgnore] public string notificationConnectorsFilePath => Path.Join(workingDirectory, "notificationConnectors.json"); + [JsonIgnore] public string jobsFilePath => Path.Join(workingDirectory, "jobs.json"); [JsonIgnore] public string coverImageCache => Path.Join(workingDirectory, "imageCache"); public ushort? version { get; set; } - public TrangaSettings(string downloadLocation, string workingDirectory) + public TrangaSettings(string? downloadLocation = null, string? workingDirectory = null, int? apiPortNumber = null) { - if (downloadLocation.Length < 1 || workingDirectory.Length < 1) - throw new ArgumentException("Download-location and working-directory paths can not be empty!"); - this.workingDirectory = workingDirectory; - this.downloadLocation = downloadLocation; + string lockFilePath = $"{settingsFilePath}.lock"; + if (File.Exists(settingsFilePath) && !File.Exists(lockFilePath)) + {//Load from settings file + FileStream lockFile = File.Create(lockFilePath,0, FileOptions.DeleteOnClose); + string settingsStr = File.ReadAllText(settingsFilePath); + TrangaSettings settings = JsonConvert.DeserializeObject(settingsStr)!; + this.downloadLocation = downloadLocation ?? settings.downloadLocation; + this.workingDirectory = workingDirectory ?? settings.workingDirectory; + this.apiPortNumber = apiPortNumber ?? settings.apiPortNumber; + lockFile.Close(); + } + else if(!File.Exists(settingsFilePath)) + {//No settings file exists + if (downloadLocation?.Length < 1 || workingDirectory?.Length < 1) + throw new ArgumentException("Download-location and working-directory paths can not be empty!"); + this.apiPortNumber = apiPortNumber ?? 6531; + this.downloadLocation = downloadLocation ?? (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(Directory.GetCurrentDirectory(), "Downloads")); + this.workingDirectory = workingDirectory ?? Path.Join(RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/var/lib" : Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "tranga-api"); + ExportSettings(); + } + else + {//Settingsfile is locked + this.apiPortNumber = apiPortNumber!.Value; + this.downloadLocation = downloadLocation!; + this.workingDirectory = workingDirectory!; + } + UpdateDownloadLocation(this.downloadLocation!, false); } - public static TrangaSettings LoadSettings(string importFilePath) + public HashSet LoadLibraryConnectors(GlobalBase clone) { - if (!File.Exists(importFilePath)) - return new TrangaSettings(Path.Join(Directory.GetCurrentDirectory(), "Downloads"), Directory.GetCurrentDirectory()); + if (!File.Exists(libraryConnectorsFilePath)) + return new HashSet(); + return JsonConvert.DeserializeObject>(File.ReadAllText(libraryConnectorsFilePath), + new JsonSerializerSettings() + { + Converters = + { + new LibraryManagerJsonConverter(clone) + } + })!; + } - string toRead = File.ReadAllText(importFilePath); - SettingsJsonObject settings = JsonConvert.DeserializeObject(toRead, - new JsonSerializerSettings { Converters = { new NotificationManager.NotificationManagerJsonConverter(), new LibraryManager.LibraryManagerJsonConverter() } })!; - return settings.ts ?? new TrangaSettings(Path.Join(Directory.GetCurrentDirectory(), "Downloads"), Directory.GetCurrentDirectory()); + public HashSet LoadNotificationConnectors(GlobalBase clone) + { + if (!File.Exists(notificationConnectorsFilePath)) + return new HashSet(); + return JsonConvert.DeserializeObject>(File.ReadAllText(notificationConnectorsFilePath), + new JsonSerializerSettings() + { + Converters = + { + new NotificationManagerJsonConverter(clone) + } + })!; + } + public void UpdateDownloadLocation(string newPath, bool moveFiles = true) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + Directory.CreateDirectory(newPath, + GroupRead | GroupWrite | None | OtherRead | OtherWrite | UserRead | UserWrite); + else + Directory.CreateDirectory(newPath); + + if (moveFiles && Directory.Exists(this.downloadLocation)) + Directory.Move(this.downloadLocation, newPath); + + this.downloadLocation = newPath; + ExportSettings(); + } + + public void UpdateWorkingDirectory(string newPath) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + Directory.CreateDirectory(newPath, + GroupRead | GroupWrite | None | OtherRead | OtherWrite | UserRead | UserWrite); + else + Directory.CreateDirectory(newPath); + Directory.Move(this.workingDirectory, newPath); + this.workingDirectory = newPath; + ExportSettings(); } public void ExportSettings() { - SettingsJsonObject? settings = null; if (File.Exists(settingsFilePath)) { bool inUse = true; @@ -43,55 +113,23 @@ public class TrangaSettings { try { - using FileStream stream = new (settingsFilePath, FileMode.Open, FileAccess.Read, FileShare.None); + using FileStream stream = new(settingsFilePath, FileMode.Open, FileAccess.Read, FileShare.None); stream.Close(); inUse = false; } catch (IOException) { - inUse = true; - Thread.Sleep(50); + Thread.Sleep(100); } } - string toRead = File.ReadAllText(settingsFilePath); - settings = JsonConvert.DeserializeObject(toRead, - new JsonSerializerSettings - { - Converters = - { - new NotificationManager.NotificationManagerJsonConverter(), - new LibraryManager.LibraryManagerJsonConverter() - } - }); } - settings = new SettingsJsonObject(this, settings?.co); - File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(settings)); + else + Directory.CreateDirectory(new FileInfo(settingsFilePath).DirectoryName!); + File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(this)); } - public void UpdateSettings(UpdateField field, params string[] values) + public string GetFullCoverPath(Manga manga) { - switch (field) - { - case UpdateField.DownloadLocation: - if (values.Length != 1) - return; - this.downloadLocation = values[0]; - break; - } - ExportSettings(); - } - - public enum UpdateField { DownloadLocation, Komga, Kavita, Gotify, LunaSea} - - internal class SettingsJsonObject - { - public TrangaSettings? ts { get; } - public CommonObjects? co { get; } - - public SettingsJsonObject(TrangaSettings? ts, CommonObjects? co) - { - this.ts = ts; - this.co = co; - } + return Path.Join(this.coverImageCache, manga.coverFileNameInCache); } } \ No newline at end of file diff --git a/Tranga/TrangaTasks/DownloadChapterTask.cs b/Tranga/TrangaTasks/DownloadChapterTask.cs deleted file mode 100644 index 8134be6..0000000 --- a/Tranga/TrangaTasks/DownloadChapterTask.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Net; -using Tranga.Connectors; -using Tranga.NotificationManagers; -using Tranga.LibraryManagers; - -namespace Tranga.TrangaTasks; - -public class DownloadChapterTask : TrangaTask -{ - public string connectorName { get; } - public Publication publication { get; } - // ReSharper disable once MemberCanBePrivate.Global - public string language { get; } - public Chapter chapter { get; } - - private double _dctProgress; - - public DownloadChapterTask(string connectorName, Publication publication, Chapter chapter, string language = "en", MonitorPublicationTask? parentTask = null) : base(Task.DownloadChapter, TimeSpan.Zero, parentTask) - { - this.chapter = chapter; - this.connectorName = connectorName; - this.publication = publication; - this.language = language; - } - - protected override HttpStatusCode ExecuteTask(TaskManager taskManager, CancellationToken? cancellationToken = null) - { - if (cancellationToken?.IsCancellationRequested ?? false) - return HttpStatusCode.RequestTimeout; - Connector connector = taskManager.GetConnector(this.connectorName); - connector.CopyCoverFromCacheToDownloadLocation(this.publication); - HttpStatusCode downloadSuccess = connector.DownloadChapter(this.publication, this.chapter, this, cancellationToken); - if ((int)downloadSuccess >= 200 && (int)downloadSuccess < 300) - { - foreach(NotificationManager nm in taskManager.commonObjects.notificationManagers) - nm.SendNotification("Chapter downloaded", $"{this.publication.sortName} {this.chapter.chapterNumber} {this.chapter.name}"); - - foreach (LibraryManager lm in taskManager.commonObjects.libraryManagers) - lm.UpdateLibrary(); - } - return downloadSuccess; - } - - public override TrangaTask Clone() - { - return new DownloadChapterTask(this.connectorName, this.publication, this.chapter, - this.language, (MonitorPublicationTask?)this.parentTask); - } - - protected override double GetProgress() - { - return _dctProgress; - } - - internal void IncrementProgress(double amount) - { - this._dctProgress += amount; - this.lastChange = DateTime.Now; - if(this.parentTask is not null) - this.parentTask.lastChange = DateTime.Now; - } - - public override string ToString() - { - return $"{base.ToString()}, {connectorName}, {publication.sortName} {publication.internalId}, Vol.{chapter.volumeNumber} Ch.{chapter.chapterNumber}"; - } -} \ No newline at end of file diff --git a/Tranga/TrangaTasks/MonitorPublicationTask.cs b/Tranga/TrangaTasks/MonitorPublicationTask.cs deleted file mode 100644 index 5319759..0000000 --- a/Tranga/TrangaTasks/MonitorPublicationTask.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Net; -using Tranga.Connectors; - -namespace Tranga.TrangaTasks; - -public class MonitorPublicationTask : TrangaTask -{ - public string connectorName { get; } - public Publication publication { get; } - // ReSharper disable once MemberCanBePrivate.Global - public string language { get; } - public MonitorPublicationTask(string connectorName, Publication publication, TimeSpan reoccurrence, string language = "en") : base(Task.MonitorPublication, reoccurrence) - { - this.connectorName = connectorName; - this.publication = publication; - this.language = language; - } - - protected override HttpStatusCode ExecuteTask(TaskManager taskManager, CancellationToken? cancellationToken = null) - { - if (cancellationToken?.IsCancellationRequested ?? false) - return HttpStatusCode.RequestTimeout; - Connector connector = taskManager.GetConnector(this.connectorName); - - //Check if Publication already has a Folder - publication.CreatePublicationFolder(taskManager.settings.downloadLocation); - List newChapters = connector.GetNewChaptersList(publication, language, ref taskManager.collection); - - connector.CopyCoverFromCacheToDownloadLocation(publication); - - publication.SaveSeriesInfoJson(taskManager.settings.downloadLocation); - - foreach (Chapter newChapter in newChapters) - { - DownloadChapterTask newTask = new (this.connectorName, publication, newChapter, this.language, this); - this.childTasks.Add(newTask); - newTask.state = ExecutionState.Enqueued; - taskManager.AddTask(newTask); - } - - return HttpStatusCode.OK; - } - - public override TrangaTask Clone() - { - return new MonitorPublicationTask(this.connectorName, this.publication, this.reoccurrence, - this.language); - } - - protected override double GetProgress() - { - if (this.childTasks.Count > 0) - return this.childTasks.Sum(ct => ct.progress) / childTasks.Count; - return 1; - } - - public override string ToString() - { - return $"{base.ToString()}, {connectorName}, {publication.sortName} {publication.internalId}"; - } -} \ No newline at end of file diff --git a/Tranga/TrangaTasks/TrangaTask.cs b/Tranga/TrangaTasks/TrangaTask.cs deleted file mode 100644 index 189884c..0000000 --- a/Tranga/TrangaTasks/TrangaTask.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System.Net; -using System.Text.Json.Serialization; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using JsonConverter = Newtonsoft.Json.JsonConverter; - -namespace Tranga.TrangaTasks; - -/// -/// Stores information on Task, when implementing new Tasks also update the serializer -/// -[JsonDerivedType(typeof(MonitorPublicationTask), 2)] -[JsonDerivedType(typeof(UpdateLibrariesTask), 3)] -[JsonDerivedType(typeof(DownloadChapterTask), 4)] -public abstract class TrangaTask -{ - // ReSharper disable once MemberCanBeProtected.Global - public TimeSpan reoccurrence { get; } - public DateTime lastExecuted { get; set; } - [Newtonsoft.Json.JsonIgnore] public ExecutionState state { get; set; } - public Task task { get; } - public string taskId { get; init; } - [Newtonsoft.Json.JsonIgnore] public TrangaTask? parentTask { get; set; } - public string? parentTaskId { get; set; } - [Newtonsoft.Json.JsonIgnore] internal HashSet childTasks { get; } - public double progress => GetProgress(); - // ReSharper disable once MemberCanBePrivate.Global - [Newtonsoft.Json.JsonIgnore]public DateTime executionStarted { get; private set; } - [Newtonsoft.Json.JsonIgnore]public DateTime lastChange { get; internal set; } - // ReSharper disable once MemberCanBePrivate.Global - [Newtonsoft.Json.JsonIgnore]public DateTime executionApproximatelyFinished => lastChange.Add(GetRemainingTime()); - // ReSharper disable once MemberCanBePrivate.Global - public TimeSpan executionApproximatelyRemaining => executionApproximatelyFinished.Subtract(DateTime.Now); - [Newtonsoft.Json.JsonIgnore]public DateTime nextExecution => lastExecuted.Add(reoccurrence); - - public enum ExecutionState { Waiting, Enqueued, Running, Failed, Success } - - protected TrangaTask(Task task, TimeSpan reoccurrence, TrangaTask? parentTask = null) - { - this.reoccurrence = reoccurrence; - this.lastExecuted = DateTime.Now.Subtract(reoccurrence); - this.task = task; - this.executionStarted = DateTime.UnixEpoch; - this.lastChange = DateTime.MaxValue; - this.taskId = Convert.ToBase64String(BitConverter.GetBytes(new Random().Next())); - this.childTasks = new(); - this.parentTask = parentTask; - this.parentTaskId = parentTask?.taskId; - } - - /// - /// BL for concrete Tasks - /// - /// - /// - protected abstract HttpStatusCode ExecuteTask(TaskManager taskManager, CancellationToken? cancellationToken = null); - - public abstract TrangaTask Clone(); - - protected abstract double GetProgress(); - - /// - /// Execute the task - /// - /// Should be the parent taskManager - /// - public void Execute(TaskManager taskManager, CancellationToken? cancellationToken = null) - { - taskManager.commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Executing Task {this}"); - this.state = ExecutionState.Running; - this.executionStarted = DateTime.Now; - this.lastChange = DateTime.Now; - if(parentTask is not null && parentTask.childTasks.All(ct => ct.state is ExecutionState.Waiting or ExecutionState.Failed)) - parentTask.executionStarted = DateTime.Now; - - HttpStatusCode statusCode = ExecuteTask(taskManager, cancellationToken); - - if ((int)statusCode >= 200 && (int)statusCode < 300) - { - this.lastExecuted = DateTime.Now; - this.state = ExecutionState.Success; - } - else - { - this.state = ExecutionState.Failed; - this.lastExecuted = DateTime.MaxValue; - } - - if (this is DownloadChapterTask) - taskManager.DeleteTask(this); - - taskManager.commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Finished Executing Task {this}"); - } - - public void AddChildTask(TrangaTask childTask) - { - this.childTasks.Add(childTask); - } - - public void RemoveChildTask(TrangaTask childTask) - { - this.childTasks.Remove(childTask); - } - - private TimeSpan GetRemainingTime() - { - if(progress == 0 || state is ExecutionState.Enqueued or ExecutionState.Waiting or ExecutionState.Failed || lastChange == DateTime.MaxValue) - return DateTime.MaxValue.Subtract(lastChange).Subtract(TimeSpan.FromHours(1)); - TimeSpan elapsed = lastChange.Subtract(executionStarted); - return elapsed.Divide(progress).Multiply(1 - progress); - } - - public enum Task : byte - { - MonitorPublication = 2, - UpdateLibraries = 3, - DownloadChapter = 4, - } - - public override string ToString() - { - return $"{task}, {lastExecuted}, {reoccurrence}, {state}, {progress:P2}, {executionApproximatelyFinished}, {executionApproximatelyRemaining}"; - } - - public class TrangaTaskJsonConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - { - return objectType == typeof(TrangaTask); - } - - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - JObject jo = JObject.Load(reader); - if (jo["task"]!.Value() == (Int64)Task.MonitorPublication) - return jo.ToObject(serializer)!; - - if (jo["task"]!.Value() == (Int64)Task.UpdateLibraries) - return jo.ToObject(serializer)!; - - if (jo["task"]!.Value() == (Int64)Task.DownloadChapter) - 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/TrangaTasks/UpdateLibrariesTask.cs b/Tranga/TrangaTasks/UpdateLibrariesTask.cs deleted file mode 100644 index e7ecb02..0000000 --- a/Tranga/TrangaTasks/UpdateLibrariesTask.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Net; - -namespace Tranga.TrangaTasks; - -/// -/// LEGACY DEPRECATED -/// -public class UpdateLibrariesTask : TrangaTask -{ - public UpdateLibrariesTask(TimeSpan reoccurrence) : base(Task.UpdateLibraries, reoccurrence) - { - } - - protected override HttpStatusCode ExecuteTask(TaskManager taskManager, CancellationToken? cancellationToken = null) - { - return HttpStatusCode.BadRequest; - } - - public override TrangaTask Clone() - { - return new UpdateLibrariesTask(this.reoccurrence); - } - - protected override double GetProgress() - { - return 1; - } -} \ No newline at end of file diff --git a/Website/Dockerfile b/Website/Dockerfile deleted file mode 100644 index d178671..0000000 --- a/Website/Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM nginx:alpine3.17-slim -COPY . /usr/share/nginx/html -EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/Website/apiConnector.js b/Website/apiConnector.js deleted file mode 100644 index 560d3a5..0000000 --- a/Website/apiConnector.js +++ /dev/null @@ -1,168 +0,0 @@ -let apiUri = `http://${window.location.host.split(':')[0]}:6531` - -if(getCookie("apiUri") != ""){ - apiUri = getCookie("apiUri"); -} -function getCookie(cname) { - let name = cname + "="; - let decodedCookie = decodeURIComponent(document.cookie); - let ca = decodedCookie.split(';'); - for(let i = 0; i < ca.length; i++) { - let c = ca[i]; - while (c.charAt(0) == ' ') { - c = c.substring(1); - } - if (c.indexOf(name) == 0) { - return c.substring(name.length, c.length); - } - } - return ""; -} - -async function GetData(uri){ - let request = await fetch(uri, { - method: 'GET', - headers: { - 'Accept': 'application/json' - } - }); - let json = await request.json(); - return json; -} - -function PostData(uri){ - fetch(uri, { - method: 'POST' - }); -} - -function DeleteData(uri){ - fetch(uri, { - method: 'DELETE' - }); -} - -async function GetAvailableControllers(){ - var uri = apiUri + "/Connectors"; - let json = await GetData(uri); - return json; -} - -async function GetPublicationFromConnector(connectorName, title){ - var uri = apiUri + `/Publications/FromConnector?connectorName=${connectorName}&title=${title}`; - let json = await GetData(uri); - return json; -} - -async function GetKnownPublications(){ - var uri = apiUri + "/Publications/Known"; - let json = await GetData(uri); - return json; -} - -async function GetPublication(internalId){ - var uri = apiUri + `/Publications/Known?internalId=${internalId}`; - let json = await GetData(uri); - return json; -} - -async function GetChapters(internalId, connectorName, onlyNew, language){ - var uri = apiUri + `/Publications/Chapters?internalId=${internalId}&connectorName=${connectorName}&onlyNew=${onlyNew}&language=${language}`; - let json = await GetData(uri); - return json; -} - -async function GetTaskTypes(){ - var uri = apiUri + "/Tasks/Types"; - let json = await GetData(uri); - return json; -} -async function GetRunningTasks(){ - var uri = apiUri + "/Tasks/RunningTasks"; - let json = await GetData(uri); - return json; -} - -async function GetDownloadTasks(){ - var uri = apiUri + "/Tasks?taskType=MonitorPublication"; - let json = await GetData(uri); - return json; -} - -async function GetSettings(){ - var uri = apiUri + "/Settings"; - let json = await GetData(uri); - return json; -} - -async function GetKomgaTask(){ - var uri = apiUri + "/Tasks?taskType=UpdateLibraries"; - let json = await GetData(uri); - return json; -} - -function CreateMonitorTask(connectorName, internalId, reoccurrence, language){ - var uri = apiUri + `/Tasks/CreateMonitorTask?connectorName=${connectorName}&internalId=${internalId}&reoccurrenceTime=${reoccurrence}&language=${language}`; - PostData(uri); -} - -function CreateDownloadChaptersTask(connectorName, internalId, chapters, language){ - var uri = apiUri + `/Tasks/CreateDownloadChaptersTask?connectorName=${connectorName}&internalId=${internalId}&chapters=${chapters}&language=${language}`; - PostData(uri); -} - -function StartTask(taskType, connectorName, internalId){ - var uri = apiUri + `/Tasks/Start?taskType=${taskType}&connectorName=${connectorName}&internalId=${internalId}`; - PostData(uri); -} - -function EnqueueTask(taskType, connectorName, publicationId){ - var uri = apiUri + `/Queue/Enqueue?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}`; - PostData(uri); -} - -function UpdateDownloadLocation(downloadLocation){ - var uri = apiUri + "/Settings/Update?" - uri += "&downloadLocation="+downloadLocation; - PostData(uri); -} - -function UpdateKomga(komgaUrl, komgaAuth){ - var uri = apiUri + "/Settings/Update?" - uri += `&komgaUrl=${komgaUrl}&komgaAuth=${komgaAuth}`; - PostData(uri); -} - -function UpdateKavita(kavitaUrl, kavitaUser, kavitaPass){ - var uri = apiUri + "/Settings/Update?" - uri += `&kavitaUrl=${kavitaUrl}&kavitaUsername=${kavitaUser}&kavitaPassword=${kavitaPass}`; - PostData(uri); -} - -function UpdateGotify(gotifyUrl, gotifyAppToken){ - var uri = apiUri + "/Settings/Update?" - uri += `&gotifyUrl=${gotifyUrl}&gotifyAppToken=${gotifyAppToken}`; - PostData(uri); -} - -function UpdateLunaSea(lunaseaWebhook){ - var uri = apiUri + "/Settings/Update?" - uri += `&lunaseaWebhook=${lunaseaWebhook}`; - PostData(uri); -} - -function DeleteTask(taskType, connectorName, publicationId){ - var uri = apiUri + `/Tasks?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}`; - DeleteData(uri); -} - -function DequeueTask(taskType, connectorName, publicationId){ - var uri = apiUri + `/Queue/Dequeue?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}`; - DeleteData(uri); -} - -async function GetQueue(){ - var uri = apiUri + "/Queue/List"; - let json = await GetData(uri); - return json; -} \ No newline at end of file diff --git a/Website/favicon.ico b/Website/favicon.ico deleted file mode 100644 index ffb44eb..0000000 Binary files a/Website/favicon.ico and /dev/null differ diff --git a/Website/index.html b/Website/index.html deleted file mode 100644 index d6e9667..0000000 --- a/Website/index.html +++ /dev/null @@ -1,179 +0,0 @@ - - - - - Tranga - - - - - - - - website image is Blahaj - Tranga - - - - - - settingscog - - - -
-

+

-
- - cover - - MangaDex - Tensei Pandemic - - -
- - - - - Select Publication - -
- - -
-
- -
- -
-
-
-
- - - - - Create Task: Monitor Publication - -
- Run every - hours - minutes - -
-
-
-
- - - - - Create Task: Download Chapter(s) - -
- -
-
- -
-
-
-
- - - - - cover - - Tensei Pandemic - - Imamura Hinata - Imamura Hinata is a high school boy with a cute appearance. - Since his trauma with the first love, he wanted to be more manly than anybody else. But one day he woke up to something different… - The total opposite of his ideal male body! - Pandemic love comedy! - - - Start Task ▶️ - Delete Task ❌ - Monitor ➕ - Download Chapter ➕ - - - - - - - - - Settings - -
-

Download Location:

- -
-
-

API-URI

- -
-
- Komga -
Configured: ✅❌
- - - -
-
- Kavita -
Configured: ✅❌
- - - -
-
- Gotify -
Configured: ✅❌
- - -
-
- LunaSea -
Configured: ✅❌
- -
-
- - -
-
-
-
- - - - - Task Progress - - - - - -
-
-
- running
0
-
-
- queue
0
-
-

Made with Blåhaj 🦈

-
-
- - - - - \ No newline at end of file diff --git a/Website/interaction.js b/Website/interaction.js deleted file mode 100644 index e6695db..0000000 --- a/Website/interaction.js +++ /dev/null @@ -1,525 +0,0 @@ -let publications = []; -let tasks = []; -let toEditId; - -const searchBox = document.querySelector("#searchbox"); -const searchPublicationQuery = document.querySelector("#searchPublicationQuery"); -const selectPublication = document.querySelector("#taskSelectOutput"); -const connectorSelect = document.querySelector("#connectors"); -const settingsPopup = document.querySelector("#settingsPopup"); -const settingsCog = document.querySelector("#settingscog"); -const selectRecurrence = document.querySelector("#selectReccurrence"); -const tasksContent = document.querySelector("content"); -const selectPublicationPopup = document.querySelector("#selectPublicationPopup"); -const createMonitorTaskButton = document.querySelector("#createMonitorTaskButton"); -const createDownloadChapterTaskButton = document.querySelector("#createDownloadChapterTaskButton"); -const createMonitorTaskPopup = document.querySelector("#createMonitorTaskPopup"); -const createDownloadChaptersTask = document.querySelector("#createDownloadChaptersTask"); -const chapterOutput = document.querySelector("#chapterOutput"); -const selectedChapters = document.querySelector("#selectedChapters"); -const publicationViewerPopup = document.querySelector("#publicationViewerPopup"); -const publicationViewerWindow = document.querySelector("publication-viewer"); -const publicationViewerDescription = document.querySelector("#publicationViewerDescription"); -const publicationViewerName = document.querySelector("#publicationViewerName"); -const publicationViewerTags = document.querySelector("#publicationViewerTags"); -const publicationViewerAuthor = document.querySelector("#publicationViewerAuthor"); -const pubviewcover = document.querySelector("#pubviewcover"); -const publicationDelete = document.querySelector("publication-delete"); -const publicationTaskStart = document.querySelector("publication-starttask"); -const settingDownloadLocation = document.querySelector("#downloadLocation"); -const settingKomgaUrl = document.querySelector("#komgaUrl"); -const settingKomgaUser = document.querySelector("#komgaUsername"); -const settingKomgaPass = document.querySelector("#komgaPassword"); -const settingKavitaUrl = document.querySelector("#kavitaUrl"); -const settingKavitaUser = document.querySelector("#kavitaUsername"); -const settingKavitaPass = document.querySelector("#kavitaPassword"); -const settingGotifyUrl = document.querySelector("#gotifyUrl"); -const settingGotifyAppToken = document.querySelector("#gotifyAppToken"); -const settingLunaseaWebhook = document.querySelector("#lunaseaWebhook"); -const libraryUpdateTime = document.querySelector("#libraryUpdateTime"); -const settingKomgaConfigured = document.querySelector("#komgaConfigured"); -const settingKavitaConfigured = document.querySelector("#kavitaConfigured"); -const settingGotifyConfigured = document.querySelector("#gotifyConfigured"); -const settingLunaseaConfigured = document.querySelector("#lunaseaConfigured"); -const settingApiUri = document.querySelector("#settingApiUri"); -const tagTasksRunning = document.querySelector("#tasksRunningTag"); -const tagTasksQueued = document.querySelector("#tasksQueuedTag"); -const downloadTasksPopup = document.querySelector("#downloadTasksPopup"); -const downloadTasksOutput = downloadTasksPopup.querySelector("popup-content"); - -searchbox.addEventListener("keyup", (event) => FilterResults()); -settingsCog.addEventListener("click", () => OpenSettings()); -document.querySelector("#blurBackgroundSettingsPopup").addEventListener("click", () => settingsPopup.style.display = "none"); -document.querySelector("#blurBackgroundTaskPopup").addEventListener("click", () => selectPublicationPopup.style.display = "none"); -document.querySelector("#blurBackgroundPublicationPopup").addEventListener("click", () => HidePublicationPopup()); -document.querySelector("#blurBackgroundCreateMonitorTaskPopup").addEventListener("click", () => createMonitorTaskPopup.style.display = "none"); -document.querySelector("#blurBackgroundCreateDownloadChaptersTask").addEventListener("click", () => createDownloadChaptersTask.style.display = "none"); -document.querySelector("#blurBackgroundTasksQueuePopup").addEventListener("click", () => downloadTasksPopup.style.display = "none"); -selectedChapters.addEventListener("keypress", (event) => { - if(event.key === "Enter"){ - DownloadChapterTaskClick(); - } -}) -publicationDelete.addEventListener("click", () => DeleteTaskClick()); -createMonitorTaskButton.addEventListener("click", () => { - HidePublicationPopup(); - createMonitorTaskPopup.style.display = "block"; -}); -createDownloadChapterTaskButton.addEventListener("click", () => { - HidePublicationPopup(); - OpenDownloadChapterTaskPopup(); -}); -publicationTaskStart.addEventListener("click", () => StartTaskClick()); -searchPublicationQuery.addEventListener("keypress", (event) => { - if(event.key === "Enter"){ - NewSearch(); - } -}); - - -let availableConnectors; -GetAvailableControllers() - .then(json => availableConnectors = json) - .then(json => - json.forEach(connector => { - var option = document.createElement('option'); - option.value = connector; - option.innerText = connector; - connectorSelect.appendChild(option); - }) - ); - - -function NewSearch(){ - //Disable inputs - connectorSelect.disabled = true; - searchPublicationQuery.disabled = true; - //Waitcursor - document.body.style.cursor = "wait"; - - //Empty previous results - selectPublication.replaceChildren(); - GetPublicationFromConnector(connectorSelect.value, searchPublicationQuery.value) - .then(json => - json.forEach(publication => { - var option = CreatePublication(publication, connectorSelect.value); - option.addEventListener("click", (mouseEvent) => { - ShowPublicationViewerWindow(publication.internalId, mouseEvent, true); - }); - selectPublication.appendChild(option); - } - )) - .then(() => { - //Re-enable inputs - connectorSelect.disabled = false; - searchPublicationQuery.disabled = false; - //Cursor - document.body.style.cursor = "initial"; - }); -} - -//Returns a new "Publication" Item to display in the tasks section -function CreatePublication(publication, connector){ - var publicationElement = document.createElement('publication'); - publicationElement.setAttribute("id", publication.internalId); - var img = document.createElement('img'); - img.src = `imageCache/${publication.coverFileNameInCache}`; - publicationElement.appendChild(img); - var info = document.createElement('publication-information'); - var connectorName = document.createElement('connector-name'); - connectorName.innerText = connector; - connectorName.className = "pill"; - info.appendChild(connectorName); - var publicationName = document.createElement('publication-name'); - publicationName.innerText = publication.sortName; - info.appendChild(publicationName); - publicationElement.appendChild(info); - if(publications.filter(pub => pub.internalId === publication.internalId) < 1) - publications.push(publication); - return publicationElement; -} - -function AddMonitorTask(){ - var hours = document.querySelector("#hours").value; - var minutes = document.querySelector("#minutes").value; - CreateMonitorTask(connectorSelect.value, toEditId, `${hours}:${minutes}:00`, "en"); - HidePublicationPopup(); - createMonitorTaskPopup.style.display = "none"; - selectPublicationPopup.style.display = "none"; -} - -function OpenDownloadChapterTaskPopup(){ - selectedChapters.value = ""; - chapterOutput.replaceChildren(); - createDownloadChaptersTask.style.display = "block"; - GetChapters(toEditId, connectorSelect.value, true, "en").then((json) => { - var i = 0; - json.forEach(chapter => { - var chapterDom = document.createElement("div"); - var indexDom = document.createElement("span"); - indexDom.className = "index"; - indexDom.innerText = i++; - chapterDom.appendChild(indexDom); - - var volDom = document.createElement("span"); - volDom.className = "vol"; - volDom.innerText = chapter.volumeNumber; - chapterDom.appendChild(volDom); - - var chDom = document.createElement("span"); - chDom.className = "ch"; - chDom.innerText = chapter.chapterNumber; - chapterDom.appendChild(chDom); - - var titleDom = document.createElement("span"); - titleDom.innerText = chapter.name; - chapterDom.appendChild(titleDom); - chapterOutput.appendChild(chapterDom); - }); - }); -} - -function DownloadChapterTaskClick(){ - CreateDownloadChaptersTask(connectorSelect.value, toEditId, selectedChapters.value, "en"); - HidePublicationPopup(); - createDownloadChaptersTask.style.display = "none"; - selectPublicationPopup.style.display = "none"; -} - -function DeleteTaskClick(){ - taskToDelete = tasks.filter(tTask => tTask.publication.internalId === toEditId)[0]; - DeleteTask("MonitorPublication", taskToDelete.connectorName, toEditId); - HidePublicationPopup(); -} - -function StartTaskClick(){ - var toEditTask = tasks.filter(task => task.publication.internalId == toEditId)[0]; - StartTask("MonitorPublication", toEditTask.connectorName, toEditId); - HidePublicationPopup(); -} - -function ResetContent(){ - //Delete everything - tasksContent.replaceChildren(); - - //Add "Add new Task" Button - var add = document.createElement("div"); - add.setAttribute("id", "addPublication") - var plus = document.createElement("p"); - plus.innerText = "+"; - add.appendChild(plus); - add.addEventListener("click", () => ShowNewTaskWindow()); - tasksContent.appendChild(add); -} -function ShowPublicationViewerWindow(publicationId, event, add){ - //Show popup - publicationViewerPopup.style.display = "block"; - - //Set position to mouse-position - if(event.clientY < window.innerHeight - publicationViewerWindow.offsetHeight) - publicationViewerWindow.style.top = `${event.clientY}px`; - else - publicationViewerWindow.style.top = `${event.clientY - publicationViewerWindow.offsetHeight}px`; - - if(event.clientX < window.innerWidth - publicationViewerWindow.offsetWidth) - publicationViewerWindow.style.left = `${event.clientX}px`; - else - publicationViewerWindow.style.left = `${event.clientX - publicationViewerWindow.offsetWidth}px`; - - //Edit information inside the window - var publication = publications.filter(pub => pub.internalId === publicationId)[0]; - publicationViewerName.innerText = publication.sortName; - publicationViewerTags.innerText = publication.tags.join(", "); - publicationViewerDescription.innerText = publication.description; - publicationViewerAuthor.innerText = publication.authors.join(','); - pubviewcover.src = `imageCache/${publication.coverFileNameInCache}`; - toEditId = publicationId; - - //Check what action should be listed - if(add){ - createMonitorTaskButton.style.display = "initial"; - createDownloadChapterTaskButton.style.display = "initial"; - publicationDelete.style.display = "none"; - publicationTaskStart.style.display = "none"; - } - else{ - createMonitorTaskButton.style.display = "none"; - createDownloadChapterTaskButton.style.display = "none"; - publicationDelete.style.display = "initial"; - publicationTaskStart.style.display = "initial"; - } -} - -function HidePublicationPopup(){ - publicationViewerPopup.style.display = "none"; -} - -function ShowNewTaskWindow(){ - selectPublication.replaceChildren(); - searchPublicationQuery.value = ""; - selectPublicationPopup.style.display = "flex"; -} - - -const fadeIn = [ - { opacity: "0" }, - { opacity: "1" } -]; - -const fadeInTiming = { - duration: 50, - iterations: 1, - fill: "forwards" -} - -function OpenSettings(){ - GetSettingsClick(); - settingsPopup.style.display = "flex"; -} - -function GetSettingsClick(){ - settingApiUri.value = ""; - settingKomgaUrl.value = ""; - settingKomgaUser.value = ""; - settingKomgaPass.value = ""; - settingKomgaConfigured.innerText = "❌"; - settingKavitaUrl.value = ""; - settingKavitaUser.value = ""; - settingKavitaPass.value = ""; - settingKavitaConfigured.innerText = "❌"; - settingGotifyUrl.value = ""; - settingGotifyAppToken.value = ""; - settingGotifyConfigured.innerText = "❌"; - settingLunaseaWebhook.value = ""; - settingLunaseaConfigured.innerText = "❌"; - - settingApiUri.placeholder = apiUri; - - GetSettings().then(json => { - settingDownloadLocation.innerText = json.downloadLocation; - json.libraryManagers.forEach(lm => { - if(lm.libraryType == 0){ - settingKomgaUrl.placeholder = lm.baseUrl; - settingKomgaUser.placeholder = "User"; - settingKomgaPass.placeholder = "***"; - settingKomgaConfigured.innerText = "✅"; - } else if(lm.libraryType == 1){ - settingKavitaUrl.placeholder = lm.baseUrl; - settingKavitaUser.placeholder = "User"; - settingKavitaPass.placeholder = "***"; - settingKavitaConfigured.innerText = "✅"; - } - }); - json.notificationManagers.forEach(nm => { - if(nm.notificationManagerType == 0){ - settingGotifyConfigured.innerText = "✅"; - } else if(nm.notificationManagerType == 1){ - settingLunaseaConfigured.innerText = "✅"; - } - }); - }); - - GetKomgaTask().then(json => { - if(json.length > 0) - libraryUpdateTime.value = json[0].reoccurrence; - }); -} - -function UpdateLibrarySettings(){ - if(settingKomgaUrl.value != "" && settingKomgaUser.value != "" && settingKomgaPass.value != ""){ - var auth = utf8_to_b64(`${settingKomgaUser.value}:${settingKomgaPass.value}`); - console.log(auth); - UpdateKomga(settingKomgaUrl.value, auth); - } - - if(settingKavitaUrl.value != "" && settingKavitaUser.value != "" && settingKavitaPass.value != ""){ - UpdateKavita(settingKavitaUrl.value, settingKavitaUser.value, settingKavitaPass.value); - } - - if(settingGotifyUrl.value != "" && settingGotifyAppToken.value != ""){ - UpdateGotify(settingGotifyUrl.value, settingGotifyAppToken.value); - } - - if(settingLunaseaWebhook.value != ""){ - UpdateLunaSea(settingLunaseaWebhook.value); - } - - if(settingApiUri.value != ""){ - apiUri = settingApiUri.value; - document.cookie = `apiUri=${apiUri};`; - } - - setTimeout(() => GetSettingsClick(), 200); -} - -function utf8_to_b64( str ) { - return window.btoa(unescape(encodeURIComponent( str ))); -} - -function FilterResults(){ - if(searchBox.value.length > 0){ - tasksContent.childNodes.forEach(publication => { - publication.childNodes.forEach(item => { - if(item.nodeName.toLowerCase() == "publication-information"){ - item.childNodes.forEach(information => { - if(information.nodeName.toLowerCase() == "publication-name"){ - if(!information.textContent.toLowerCase().includes(searchBox.value.toLowerCase())){ - publication.style.display = "none"; - }else{ - publication.style.display = "initial"; - } - } - }); - } - }); - }); - }else{ - tasksContent.childNodes.forEach(publication => publication.style.display = "initial"); - } -} - -function ShowTasksQueue(){ - - downloadTasksOutput.replaceChildren(); - GetRunningTasks() - .then(json => { - tagTasksRunning.innerText = json.length; - json.forEach(task => { - if(task.task == 2 || task.task == 4) { - downloadTasksOutput.appendChild(CreateProgressChild(task)); - document.querySelector(`#progress${GetValidSelector(task.taskId)}`).value = task.progress; - var finishedHours = task.executionApproximatelyRemaining.split(':')[0]; - var finishedMinutes = task.executionApproximatelyRemaining.split(':')[1]; - var finishedSeconds = task.executionApproximatelyRemaining.split(':')[2].split('.')[0]; - document.querySelector(`#progressStr${GetValidSelector(task.taskId)}`).innerText = `${finishedHours}:${finishedMinutes}:${finishedSeconds}`; - } - }); - }); - - GetQueue() - .then(json => { - tagTasksQueued.innerText = json.length; - json.forEach(task => { - downloadTasksOutput.appendChild(CreateProgressChild(task)); - }); - }); - downloadTasksPopup.style.display = "flex"; -} - -function CreateProgressChild(task){ - var child = document.createElement("div"); - var img = document.createElement('img'); - img.src = `imageCache/${task.publication.coverFileNameInCache}`; - child.appendChild(img); - - var name = document.createElement("span"); - name.innerText = task.publication.sortName; - name.className = "pubTitle"; - child.appendChild(name); - - - var progress = document.createElement("progress"); - progress.id = `progress${GetValidSelector(task.taskId)}`; - child.appendChild(progress); - - var progressStr = document.createElement("span"); - progressStr.innerText = " \t∞"; - progressStr.className = "progressStr"; - progressStr.id = `progressStr${GetValidSelector(task.taskId)}`; - child.appendChild(progressStr); - - if(task.chapter != undefined){ - var chapterNumber = document.createElement("span"); - chapterNumber.className = "chapterNumber"; - chapterNumber.innerText = `Vol.${task.chapter.volumeNumber} Ch.${task.chapter.chapterNumber}`; - child.appendChild(chapterNumber); - - var chapterName = document.createElement("span"); - chapterName.className = "chapterName"; - chapterName.innerText = task.chapter.name; - child.appendChild(chapterName); - } - - - return child; -} - -//Resets the tasks shown -ResetContent(); -downloadTasksOutput.replaceChildren(); -//Get Tasks and show them -GetDownloadTasks() - .then(json => json.forEach(task => { - var publication = CreatePublication(task.publication, task.connectorName); - publication.addEventListener("click", (event) => ShowPublicationViewerWindow(task.publication.internalId, event, false)); - tasksContent.appendChild(publication); - tasks.push(task); - })); - -GetRunningTasks() - .then(json => { - tagTasksRunning.innerText = json.length; - json.forEach(task => { - downloadTasksOutput.appendChild(CreateProgressChild(task)); - }); - }); - -GetQueue() - .then(json => { - tagTasksQueued.innerText = json.length; - json.forEach(task => { - downloadTasksOutput.appendChild(CreateProgressChild(task)); - }); - }) - -setInterval(() => { - //Tasks from API - var cTasks = []; - GetDownloadTasks() - .then(json => json.forEach(task => cTasks.push(task))) - .then(() => { - //Only update view if tasks-amount has changed - if(tasks.length != cTasks.length) { - //Resets the tasks shown - ResetContent(); - //Add all currenttasks to view - cTasks.forEach(task => { - var publication = CreatePublication(task.publication, task.connectorName); - publication.addEventListener("click", (event) => ShowPublicationViewerWindow(task.publication.internalId, event, false)); - tasksContent.appendChild(publication); - }) - - tasks = cTasks; - } - } - ); - - GetRunningTasks() - .then(json => { - tagTasksRunning.innerText = json.length; - }); - - GetQueue() - .then(json => { - tagTasksQueued.innerText = json.length; - }); -}, 1000); - -setInterval(() => { - GetRunningTasks().then((json) => { - json.forEach(task => { - if(task.task == 2 || task.task == 4){ - document.querySelector(`#progress${GetValidSelector(task.taskId)}`).value = task.progress; - var finishedHours = task.executionApproximatelyRemaining.split(':')[0]; - var finishedMinutes = task.executionApproximatelyRemaining.split(':')[1]; - var finishedSeconds = task.executionApproximatelyRemaining.split(':')[2].split('.')[0]; - document.querySelector(`#progressStr${GetValidSelector(task.taskId)}`).innerText = `${finishedHours}:${finishedMinutes}:${finishedSeconds}`; - } - }); - }); -},500); - -function GetValidSelector(str){ - var clean = [...str.matchAll(/[a-zA-Z0-9]*-*_*/g)]; - return clean.join(''); -} \ No newline at end of file diff --git a/Website/media/blahaj.png b/Website/media/blahaj.png deleted file mode 100644 index dbbff93..0000000 Binary files a/Website/media/blahaj.png and /dev/null differ diff --git a/Website/media/close-x.svg b/Website/media/close-x.svg deleted file mode 100644 index fc8cc4d..0000000 --- a/Website/media/close-x.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/Website/media/queue.svg b/Website/media/queue.svg deleted file mode 100644 index 30b9620..0000000 --- a/Website/media/queue.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/Website/media/running.svg b/Website/media/running.svg deleted file mode 100644 index ddde0a6..0000000 --- a/Website/media/running.svg +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Website/media/settings-cogwheel.svg b/Website/media/settings-cogwheel.svg deleted file mode 100644 index 7e61388..0000000 --- a/Website/media/settings-cogwheel.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/Website/media/tasks.svg b/Website/media/tasks.svg deleted file mode 100644 index 6e64e66..0000000 --- a/Website/media/tasks.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/Website/style.css b/Website/style.css deleted file mode 100644 index f7114ed..0000000 --- a/Website/style.css +++ /dev/null @@ -1,603 +0,0 @@ -:root{ - --background-color: #030304; - --second-background-color: #fff; - --primary-color: #f5a9b8; - --secondary-color: #5bcefa; - --accent-color: #fff; - --topbar-height: 60px; - box-sizing: border-box; -} - -body{ - padding: 0; - margin: 0; - height: 100vh; - background-color: var(--background-color); - font-family: "Inter", sans-serif; - overflow-x: hidden; -} - -wrapper { - display: flex; - flex-flow: column; - flex-wrap: nowrap; - height: 100vh; -} - -background-placeholder{ - background-color: var(--second-background-color); - opacity: 1; - position: absolute; - width: 100%; - height: 100%; - border-radius: 0 0 5px 0; - z-index: -1; -} - -topbar { - display: flex; - align-items: center; - height: var(--topbar-height); - background-color: var(--secondary-color); - z-index: 100; - box-shadow: 0 0 20px black; -} - -titlebox { - position: relative; - display: flex; - margin: 0 0 0 40px; - height: 100%; - align-items:center; - justify-content:center; -} - -titlebox span{ - cursor: default; - font-size: 24pt; - font-weight: bold; - background: linear-gradient(150deg, var(--primary-color), var(--accent-color)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - margin-left: 20px; -} - -titlebox img { - height: 100%; - margin-right: 10px; - cursor: grab; -} - -spacer{ - flex-grow: 1; -} - -searchdiv{ - display: block; - margin: 0 10px 0 0; -} - -#searchbox { - padding: 3px 10px; - border: 0; - border-radius: 4px; - font-size: 14pt; - width: 250px; -} - -#settingscog { - cursor: pointer; - margin: 0px 30px; - height: 50%; - filter: invert(100%) sepia(0%) saturate(7465%) hue-rotate(115deg) brightness(116%) contrast(101%); -} - -viewport { - position: relative; - display: flex; - flex-flow: row; - flex-wrap: nowrap; - flex-grow: 1; - height: 100%; - overflow-y: scroll; -} - -footer { - display: flex; - flex-direction: row; - flex-wrap: nowrap; - width: 100%; - height: 40px; - align-items: center; - justify-content: center; - background-color: var(--primary-color); - align-content: center; -} - -footer > div { - height: 100%; - margin: 0 30px; - display: flex; - flex-direction: row; - flex-wrap: nowrap; - align-items: center; - cursor: pointer; -} - -footer > div > *{ - height: 40%; - margin: 0 5px; -} - -#madeWith { - flex-grow: 1; - text-align: right; - margin-right: 20px; - cursor: url("media/blahaj.png"), grab; -} - -content { - position: relative; - flex-grow: 1; - border-radius: 5px; - display: flex; - flex-direction: row; - flex-wrap: wrap; - justify-content: start; - align-content: start; -} - -#settingsPopup{ - z-index: 10; -} - -#settingsPopup popup-content{ - flex-direction: column; - align-items: start; - margin: 15px 10px; -} - -#settingsPopup popup-content > * { - margin: 5px 10px; -} - -#settingsPopup popup-content .title { - font-weight: bolder; -} - -#addPublication { - cursor: pointer; - background-color: var(--secondary-color); - width: 180px; - height: 300px; - border-radius: 5px; - margin: 10px 10px; - padding: 15px 20px; - position: relative; -} - -#addPublication p{ - width: 100%; - text-align: center; - font-size: 150pt; - vertical-align: middle; - line-height: 300px; - margin: 0; - color: var(--accent-color); -} - -.pill { - flex-grow: 0; - height: 14pt; - font-size: 12pt; - border-radius: 9pt; - background-color: var(--primary-color); - padding: 2pt 17px; - color: black; -} - -publication{ - cursor: pointer; - background-color: var(--secondary-color); - width: 180px; - height: 300px; - border-radius: 5px; - margin: 10px 10px; - padding: 15px 20px; - position: relative; -} - -publication::after{ - content: ''; - position: absolute; - left: 0; top: 0; - border-radius: 5px; - width: 100%; height: 100%; - background: linear-gradient(rgba(0,0,0,0.8), rgba(0, 0, 0, 0.7),rgba(0, 0, 0, 0.2)); -} - -publication-information { - display: flex; - flex-direction: column; - justify-content: start; -} - -publication-information * { - z-index: 1; - color: var(--accent-color); -} - -connector-name{ - width: fit-content; - margin: 10px 0; -} - -publication-name{ - width: fit-content; - font-size: 16pt; - font-weight: bold; -} - -publication img { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - object-fit: cover; - z-index: 0; - border-radius: 5px; -} - -popup{ - display: none; - width: 100%; - min-height: 100%; - top: 0; - left: 0; - position: fixed; - z-index: 2; - flex-direction: column; -} - - -popup popup-window { - position: absolute; - z-index: 3; - left: 25%; - top: 100px; - width: 50%; - display: flex; - flex-direction: column; - background-color: var(--second-background-color); - border-radius: 3px; - overflow: hidden; -} - -popup popup-window popup-title { - height: 30px; - font-size: 14pt; - font-weight: bolder; - padding: 5px 10px; - margin: 0; - display: flex; - align-items: center; - background-color: var(--primary-color); - color: var(--accent-color) -} - -popup popup-window popup-content{ - margin: 15px 10px; - display: flex; - align-items: center; - justify-content: space-evenly; -} - -popup popup-window popup-content div > * { - margin: 2px 3px 0 0; -} - -popup popup-window popup-content input, select { - padding: 3px 4px; - width: 130px; - border: 1px solid lightgrey; - background-color: var(--accent-color); - border-radius: 3px; -} - -#selectPublicationPopup publication { - width: 150px; - height: 250px; -} - -#createTaskPopup { - z-index: 7; -} - -#createTaskPopup input { - height: 30px; - width: 200px; -} - -#createMonitorTaskPopup, #createDownloadChaptersTask { - z-index: 9; -} - -#createMonitorTaskPopup input[type="number"] { - width: 40px; -} - -#createDownloadChaptersTask popup-content { - flex-direction: column; - align-items: start; -} - -#createDownloadChaptersTask popup-content > * { - margin: 3px 0; -} - -#createDownloadChaptersTask #chapterOutput { - max-height: 50vh; - overflow-y: scroll; -} - -#createDownloadChaptersTask #chapterOutput .index{ - display: inline-block; - width: 25px; -} - -#createDownloadChaptersTask #chapterOutput .index::after{ - content: ':'; -} - -#createDownloadChaptersTask #chapterOutput .vol::before{ - content: 'Vol.'; -} - -#createDownloadChaptersTask #chapterOutput .vol{ - display: inline-block; - width: 45px; -} - -#createDownloadChaptersTask #chapterOutput .ch::before{ - content: 'Ch.'; -} - -#createDownloadChaptersTask #chapterOutput .ch { - display: inline-block; - width: 60px; -} - -#downloadTasksPopup popup-window { - left: 0; - top: 80px; - margin: 0 0 0 10px; - height: calc(100vh - 140px); - width: 400px; - max-width: 95vw; - overflow-y: scroll; -} - -#downloadTasksPopup popup-content { - flex-direction: column; - align-items: start; - margin: 5px; -} - -#downloadTasksPopup popup-content > div { - display: block; - height: 80px; - position: relative; - margin: 5px 0; -} - -#downloadTasksPopup popup-content > div > img { - display: block; - position: absolute; - height: 100%; - width: 60px; - left: 0; - top: 0; - object-fit: cover; - border-radius: 4px; -} - -#downloadTasksPopup popup-content > div > span { - display: block; - position: absolute; - width: max-content; -} - -#downloadTasksPopup popup-content > div > .pubTitle { - left: 70px; - top: 0; -} - -#downloadTasksPopup popup-content > div > .chapterName { - left: 70px; - top: 28pt; -} - -#downloadTasksPopup popup-content > div > .chapterNumber { - left: 70px; - top: 14pt; -} - -#downloadTasksPopup popup-content > div > progress { - display: block; - position: absolute; - left: 150px; - bottom: 0; - width: 200px; -} - -#downloadTasksPopup popup-content > div > .progressStr { - display: block; - position: absolute; - left: 70px; - bottom: 0; - width: 70px; -} - -blur-background { - width: 100%; - height: 100%; - position: absolute; - left: 0; - background-color: black; - opacity: 0.5; -} - -#taskSelectOutput{ - display: flex; - flex-direction: row; - flex-wrap: wrap; - justify-content: start; - align-content: start; - max-height: 70vh; - overflow-y: scroll; -} - -#publicationViewerPopup{ - z-index: 5; -} - -publication-viewer{ - display: block; - width: 450px; - position: absolute; - top: 200px; - left: 400px; - background-color: var(--accent-color); - border-radius: 5px; - overflow: hidden; - padding: 15px; -} - -publication-viewer::after{ - content: ''; - position: absolute; - left: 0; top: 0; - border-radius: 5px; - width: 100%; - height: 100%; - background: rgba(0,0,0,0.8); - backdrop-filter: blur(3px); -} - -publication-viewer img { - position: absolute; - left: 0; - top: 0; - height: 100%; - width: 100%; - object-fit: cover; - border-radius: 5px; - z-index: 0; -} - -publication-viewer publication-information > * { - margin: 5px 0; -} - -publication-viewer publication-information publication-name { - width: initial; - overflow-x: scroll; - white-space: nowrap; - scrollbar-width: none; -} - -publication-viewer publication-information publication-tags::before { - content: "Tags"; - display: block; - font-weight: bolder; -} - -publication-viewer publication-information publication-tags { - overflow-x: scroll; - white-space: nowrap; - scrollbar-width: none; -} - -publication-viewer publication-information publication-author::before { - content: "Author: "; - font-weight: bolder; -} - -publication-viewer publication-information publication-description::before { - content: "Description"; - display: block; - font-weight: bolder; -} - -publication-viewer publication-information publication-description { - font-size: 12pt; - margin: 5px 0; - height: 145px; - overflow-x: scroll; -} - -publication-viewer publication-information publication-interactions { - display: flex; - flex-direction: row; - justify-content: end; - align-items: start; - width: 100%; -} - -publication-viewer publication-information publication-interactions > * { - margin: 0 10px; - font-size: 16pt; - cursor: pointer; -} - -publication-viewer publication-information publication-interactions publication-starttask { - color: var(--secondary-color); -} - -publication-viewer publication-information publication-interactions publication-delete { - color: red; -} - -publication-viewer publication-information publication-interactions publication-add { - color: limegreen; -} - -footer-tag-popup { - display: none; - padding: 2px 4px; - position: fixed; - bottom: 58px; - left: 20px; - background-color: var(--second-background-color); - z-index: 8; - border-radius: 5px; - max-height: 400px; -} - -footer-tag-content{ - position: relative; - max-height: 400px; - display: flex; - flex-direction: column; - flex-wrap: nowrap; - overflow-y: scroll; -} - -footer-tag-content > * { - margin: 2px 5px; -} - -footer-tag-popup::before{ - content: ""; - width: 0; - height: 0; - position: absolute; - border-right: 10px solid var(--second-background-color); - border-left: 10px solid transparent; - border-top: 10px solid var(--second-background-color); - border-bottom: 10px solid transparent; - left: 0; - bottom: -17px; - border-radius: 0 0 0 5px; -} \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 9dcf31f..0122f82 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -4,7 +4,6 @@ services: image: glax/tranga-api:latest container_name: tranga-api volumes: - - ./tranga:/usr/share/Tranga-API #1 when replacing ./tranga replace #2 with same value - ./Manga:/Manga ports: - "6531:6531" @@ -12,8 +11,6 @@ services: tranga-website: image: glax/tranga-website:latest container_name: tranga-website - volumes: - - ./tranga/imageCache:/usr/share/nginx/html/imageCache:ro #2 when replacing Point to same value as #1/imageCache ports: - "9555:80" depends_on: diff --git a/screenshots/addtask.png b/screenshots/addtask.png deleted file mode 100644 index ebaf431..0000000 Binary files a/screenshots/addtask.png and /dev/null differ diff --git a/screenshots/overview.png b/screenshots/overview.png deleted file mode 100644 index 012b0b8..0000000 Binary files a/screenshots/overview.png and /dev/null differ diff --git a/screenshots/progress.png b/screenshots/progress.png deleted file mode 100644 index 728aec7..0000000 Binary files a/screenshots/progress.png and /dev/null differ diff --git a/screenshots/publication-description.png b/screenshots/publication-description.png deleted file mode 100644 index d852b00..0000000 Binary files a/screenshots/publication-description.png and /dev/null differ diff --git a/screenshots/settings.png b/screenshots/settings.png deleted file mode 100644 index 14039e0..0000000 Binary files a/screenshots/settings.png and /dev/null differ