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
+ {
+ lock(_logMessages)
+ {
+ ret.Add(_logMessages.GetValueAtIndex(_lastLogMessageIndex + retIndex).ToString());
+ }
+ }
+ 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/README.md b/README.md
index 0ff1fd2..4b7b5cb 100644
--- a/README.md
+++ b/README.md
@@ -16,9 +16,15 @@
Automatic Manga and Metadata downloader
+
+ This is the API for Tranga-Website
+
-
+# Important for existing users:
+Tranga just had a complete rewrite. Old settings, tasks, etc. will not work.
+For the time being the docker-tag `latest` will be the old, discontinued branch. `cuttingedge` is the active branch and
+will soon be moved to the `latest` branch. There is no migration-tool. Make a backup of old files.
@@ -30,9 +36,6 @@
Built With
-
- Screenshots
-
Getting Started
@@ -60,7 +63,24 @@ Tranga can download Chapters and Metadata from "Scanlation" sites such as
- [MangaKatana](https://mangakatana.com)
- ❓ Open an [issue](https://github.com/C9Glax/tranga/issues)
-and automatically import them with [Komga](https://komga.org/) and [Kavita](https://www.kavitareader.com/). Also Notifications will be sent to your devices using [Gotify](https://gotify.net/) and [LunaSea](https://www.lunasea.app/).
+and trigger an scan with [Komga](https://komga.org/) and [Kavita](https://www.kavitareader.com/).
+Notifications will can sent to your devices using [Gotify](https://gotify.net/) and [LunaSea](https://www.lunasea.app/).
+
+### What this does and doesn't do
+
+Tranga (this git-repo) will open a port (standard 6531) and listen for requests to add Jobs to Monitor and/or download specific Manga.
+The configuration is all done through HTTP-Requests.
+The frontend in this repo is **CLI**-based.
+_**For a web-frontend use [tranga-website](https://github.com/C9Glax/tranga-website).**_
+
+This project downloads the images for a Manga from the specified Scanlation-Website and packages them with some metadata - from that same website - in a .cbz-archive (per chapter).
+It does this on an interval, and checks for any Chapters (.cbz-Archive) not already existing in your specified Download-Location. (If you rename or move files, it will download those again)
+Tranga can (if configured) trigger a scan in Komga or Kavita, however the directory in which the Manga reside has to be available to both Tranga and Komga/Kavita.
+
+The project doesn't manage metadata, doesn't curate, change or enhance any information that isn't available on the selected Scanlation-Site.
+It will blindly use whatever is scrapes (yes this is a glorified Web-scraper).
+
+
### Inspiration:
Because [Kaizoku](https://github.com/oae/kaizoku) was relying on [mangal](https://github.com/metafates/mangal) and mangal
@@ -81,17 +101,6 @@ That is why I wanted to create my own project, in a language I understand, and t
(back to top)
-
-## Screenshots
-
-| ![image](screenshots/overview.png) | ![image](screenshots/addtask.png) |
-|-----------------------------------:|:----------------------------------|
-
-| ![image](screenshots/settings.png) | ![image](screenshots/publication-description.png) | ![image](screenshots/progress.png) |
-|-----------------------------------:|:-------------------------------------------------:|:-----------------------------------|
-
-(back to top)
-
## Getting Started
@@ -102,35 +111,17 @@ There is two release types:
### CLI
-Head over to [releases](https://git.bernloehr.eu/glax/Tranga/releases) and download. The CLI will guide you through setup.
+Head over to [releases](https://git.bernloehr.eu/glax/Tranga/releases) and download.
+
+
+~~The CLI will guide you through setup.~~ Not in the current version.
+Right now it is barebones with options to view logs and make HTTP-Requests
### Docker
-Download [docker-compose.yaml](https://git.bernloehr.eu/glax/Tranga/src/branch/master/docker-compose.yaml) and configure to your needs.
-
-Wherever you are mounting `/usr/share/Tranga-API` you also need to mount that same path + `/imageCache` in the webserver container.
-
-### Docker-Website usage
-
-There is two ways to download Mangas:
-- Downloading everything and monitor for new Chapters
-- Selecting specific Volumes/Chapters
-
-On the website you add new tasks, by selecting the blue '+' field. Next select the connector/site you want to use, and enter a search term.
-After clicking 'Search' (or pressing Enter), the results will be presented below - this might, depending on the result-size, take a while because we are already preloading the cover-images.
-Next select the publication (by selecting the cover) and a new popup will open with two options:
-- "Monitor" - Download all chapters and monitor for new ones
-- "Download Chapter" - Download specific chapters only
-
-When selecting `Monitor` you will be presented with a new window and the selection of the interval you want to check for new chapters (Default: Every 3 hours).
-When selecting `Download Chapter` a list will open with all available chapters - that have not yet been downloaded - from which you can then select a range (see below).
-
-The syntax for selecting chapters is as follows:
-- To download a single Chapter enter either the index number (the number at the very start of the line) or its absolute number like so: `c(h)(apter)[number]`, spaces are allowed.
-- To download a range of chapters enter either a range of index numbers (`3-6`) or chapters (`ch 12-23`).
-- For volumes the syntax is as follows: `v(ol)[number](-[number])`, again spaces allowed.
-
-Examples: `2-12`, `c1`, `ch 2`, `chapter 3`, `v 2`, `vol3-4`, `v2c4` (note: you can only specify a single chapter with this last syntax).
+Download [docker-compose.yaml](https://git.bernloehr.eu/glax/Tranga/src/branch/master/docker-compose.yaml) and configure to your needs.
+Mount `/Manga` to wherever you want your chapters (`.cbz`-Archives) downloaded (for exampled where Komga/Kavita can access them).
+The `docker-compose` also includes [tranga-website](https://github.com/C9Glax/tranga-website) as frontend. For its configuration refer to the repo README.
### Prerequisites
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/Connectors/Mangasee.cs b/Tranga/Connectors/Mangasee.cs
deleted file mode 100644
index 76e60be..0000000
--- a/Tranga/Connectors/Mangasee.cs
+++ /dev/null
@@ -1,278 +0,0 @@
-using System.Globalization;
-using System.Net;
-using System.Text.RegularExpressions;
-using System.Xml.Linq;
-using HtmlAgilityPack;
-using Newtonsoft.Json;
-using PuppeteerSharp;
-using Tranga.TrangaTasks;
-
-namespace Tranga.Connectors;
-
-public class Mangasee : Connector
-{
- public override string name { get; }
- private IBrowser? _browser;
- private const string ChromiumVersion = "1154303";
-
- public Mangasee(TrangaSettings settings, CommonObjects commonObjects) : base(settings, commonObjects)
- {
- this.name = "Mangasee";
- this.downloadClient = new DownloadClient(new Dictionary()
- {
- { 1, 60 }
- }, commonObjects.logger);
-
- Task d = new Task(DownloadBrowser);
- d.Start();
- }
-
- private async void DownloadBrowser()
- {
- BrowserFetcher browserFetcher = new BrowserFetcher();
- foreach(string rev in browserFetcher.LocalRevisions().Where(rev => rev != ChromiumVersion))
- browserFetcher.Remove(rev);
- if (!browserFetcher.LocalRevisions().Contains(ChromiumVersion))
- {
- commonObjects.logger?.WriteLine(this.GetType().ToString(), "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)
- {
- 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}");
- last = DateTime.Now;
- }
-
- };
- if (!browserFetcher.CanDownloadAsync(ChromiumVersion).Result)
- {
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Can't download browser version {ChromiumVersion}");
- return;
- }
- await browserFetcher.DownloadAsync(ChromiumVersion);
- }
-
- commonObjects.logger?.WriteLine(this.GetType().ToString(), "Starting browser.");
- this._browser = await Puppeteer.LaunchAsync(new LaunchOptions
- {
- Headless = true,
- ExecutablePath = browserFetcher.GetExecutablePath(ChromiumVersion),
- Args = new [] {
- "--disable-gpu",
- "--disable-dev-shm-usage",
- "--disable-setuid-sandbox",
- "--no-sandbox"}
- });
- }
-
- protected override Publication[] GetPublicationsInternal(string publicationTitle = "")
- {
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Publications (title={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 ParsePublicationsFromHtml(requestResult.result, publicationTitle);
- }
-
- private Publication[] ParsePublicationsFromHtml(Stream html, string publicationTitle)
- {
- string jsonString = new StreamReader(html).ReadToEnd();
- List result = JsonConvert.DeserializeObject>(jsonString)!;
- Dictionary queryFiltered = new();
- foreach (SearchResultItem resultItem in result)
- {
- int matches = resultItem.GetMatches(publicationTitle);
- if (matches > 0)
- queryFiltered.TryAdd(resultItem, matches);
- }
-
- 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})");
-
- 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));
- }
- }
- return ret.ToArray();
- }
-
-
- private Publication ParseSinglePublicationFromHtml(Stream html, string sortName, string publicationId, string[] a)
- {
- 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"));
- 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[] authorsNodes = attributes.Descendants("li")
- .First(node => node.InnerText.Contains("author(s):", StringComparison.CurrentCultureIgnoreCase))
- .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();
- 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();
- int year = Convert.ToInt32(yearNode.InnerText);
-
- HtmlNode[] statusNodes = attributes.Descendants("li")
- .First(node => node.InnerText.Contains("status:", StringComparison.CurrentCultureIgnoreCase))
- .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();
- 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,
- year, originalLanguage, status, publicationId);
- }
-
- // ReSharper disable once ClassNeverInstantiated.Local Will be instantiated during deserialization
- private class SearchResultItem
- {
- public string i { get; init; }
- public string s { get; init; }
- public string[] a { get; init; }
-
- [JsonConstructor]
- public SearchResultItem(string i, string s, string[] a)
- {
- this.i = i;
- this.s = s;
- this.a = a;
- }
-
- public int GetMatches(string title)
- {
- int ret = 0;
- Regex cleanRex = new("[A-z0-9]*");
- string[] badWords = { "a", "an", "no", "ni", "so", "as", "and", "the", "of", "that", "in", "is", "for" };
-
- string[] titleTerms = title.Split(new[] { ' ', '-' }).Where(str => !badWords.Contains(str)).ToArray();
-
- foreach (Match matchTerm in cleanRex.Matches(this.i))
- ret += titleTerms.Count(titleTerm =>
- titleTerm.Equals(matchTerm.Value, StringComparison.OrdinalIgnoreCase));
-
- foreach (Match matchTerm in cleanRex.Matches(this.s))
- ret += titleTerms.Count(titleTerm =>
- titleTerm.Equals(matchTerm.Value, StringComparison.OrdinalIgnoreCase));
-
- foreach(string alt in this.a)
- foreach (Match matchTerm in cleanRex.Matches(alt))
- ret += titleTerms.Count(titleTerm =>
- titleTerm.Equals(matchTerm.Value, StringComparison.OrdinalIgnoreCase));
-
- return ret;
- }
- }
-
- public override Chapter[] GetChapters(Publication publication, string language = "")
- {
- XDocument doc = XDocument.Load($"https://mangasee123.com/rss/{publication.publicationId}.xml");
- XElement[] chapterItems = doc.Descendants("item").ToArray();
- List ret = new();
- foreach (XElement chapter in chapterItems)
- {
- string volumeNumber = "1";
- string chapterName = chapter.Descendants("title").First().Value;
- string chapterNumber = Regex.Matches(chapterName, "[0-9]+")[^1].ToString();
-
- 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));
- }
-
- //Return Chapters ordered by Chapter-Number
- NumberFormatInfo chapterNumberFormatInfo = new()
- {
- NumberDecimalSeparator = "."
- };
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Done getting Chapters for {publication.internalId}");
- return ret.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
- }
-
- public override HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null)
- {
- if (cancellationToken?.IsCancellationRequested ?? false)
- return HttpStatusCode.RequestTimeout;
- while (this._browser is null && !(cancellationToken?.IsCancellationRequested??false))
- {
- commonObjects.logger?.WriteLine(this.GetType().ToString(), "Waiting for headless browser to download...");
- Thread.Sleep(1000);
- }
- if (cancellationToken?.IsCancellationRequested??false)
- return HttpStatusCode.RequestTimeout;
-
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
- IPage page = _browser!.NewPageAsync().Result;
- IResponse response = page.GoToAsync(chapter.url).Result;
- if (response.Ok)
- {
- HtmlDocument document = new ();
- document.LoadHtml(page.GetContentAsync().Result);
-
- HtmlNode gallery = document.DocumentNode.Descendants("div").First(div => div.HasClass("ImageGallery"));
- HtmlNode[] images = gallery.Descendants("img").Where(img => img.HasClass("img-fluid")).ToArray();
- List urls = new();
- foreach(HtmlNode galleryImage in images)
- urls.Add(galleryImage.GetAttributeValue("src", ""));
-
- string comicInfoPath = Path.GetTempFileName();
- File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
-
- page.CloseAsync();
- return DownloadChapterImages(urls.ToArray(), chapter.GetArchiveFilePath(settings.downloadLocation), 1, parentTask, comicInfoPath, cancellationToken:cancellationToken);
- }
-
- page.CloseAsync();
- return response.Status;
- }
-}
\ No newline at end of file
diff --git a/Tranga/DownloadClient.cs b/Tranga/DownloadClient.cs
deleted file mode 100644
index 5b00a06..0000000
--- a/Tranga/DownloadClient.cs
+++ /dev/null
@@ -1,116 +0,0 @@
-using System.Net;
-using System.Net.Http.Headers;
-using Logging;
-
-namespace Tranga;
-
-internal class DownloadClient
- {
- private static readonly HttpClient Client = new()
- {
- Timeout = TimeSpan.FromSeconds(60),
- DefaultRequestHeaders =
- {
- UserAgent =
- {
- new ProductInfoHeaderValue("Tranga", "0.1")
- }
- }
- };
-
- 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)
- {
- this.logger = logger;
- _lastExecutedRateLimit = new();
- _rateLimit = new();
- foreach(KeyValuePair limit in rateLimitRequestsPerMinute)
- _rateLimit.Add(limit.Key, TimeSpan.FromMinutes(1).Divide(limit.Value));
- }
-
- ///
- /// Request Webpage
- ///
- ///
- /// For RateLimits: Same Endpoints use same type
- /// Used in http request header
- /// RequestResult with StatusCode and Stream of received data
- public RequestResult MakeRequest(string url, byte requestType, string? referrer = null)
- {
- if (_rateLimit.TryGetValue(requestType, out TimeSpan value))
- _lastExecutedRateLimit.TryAdd(requestType, DateTime.Now.Subtract(value));
- else
- {
- logger?.WriteLine(this.GetType().ToString(), "RequestType not configured for rate-limit.");
- return new RequestResult(HttpStatusCode.NotAcceptable, Stream.Null);
- }
-
- TimeSpan rateLimitTimeout = _rateLimit[requestType]
- .Subtract(DateTime.Now.Subtract(_lastExecutedRateLimit[requestType]));
-
- if(rateLimitTimeout > TimeSpan.Zero)
- Thread.Sleep(rateLimitTimeout);
-
- HttpResponseMessage? response = null;
- while (response is null)
- {
- try
- {
- HttpRequestMessage requestMessage = new(HttpMethod.Get, url);
- if(referrer is not null)
- requestMessage.Headers.Referrer = new Uri(referrer);
- _lastExecutedRateLimit[requestType] = DateTime.Now;
- response = Client.Send(requestMessage);
- }
- catch (HttpRequestException e)
- {
- logger?.WriteLine(this.GetType().ToString(), e.Message);
- logger?.WriteLine(this.GetType().ToString(), $"Waiting {_rateLimit[requestType] * 2}... Retrying.");
- Thread.Sleep(_rateLimit[requestType] * 2);
- }
- }
- if (!response.IsSuccessStatusCode)
- {
- logger?.WriteLine(this.GetType().ToString(), $"Request-Error {response.StatusCode}: {response.ReasonPhrase}");
- return new RequestResult(response.StatusCode, Stream.Null);
- }
-
- // Request has been redirected to another page. For example, it redirects directly to the results when there is only 1 result
- if(response.RequestMessage is not null && response.RequestMessage.RequestUri is not null)
- {
- return new RequestResult(response.StatusCode, response.Content.ReadAsStream(), true, response.RequestMessage.RequestUri.AbsoluteUri);
- }
-
- return new RequestResult(response.StatusCode, response.Content.ReadAsStream());
- }
-
- public struct RequestResult
- {
- public HttpStatusCode statusCode { get; }
- public Stream result { get; }
- public bool hasBeenRedirected { get; }
- public string? redirectedToUrl { get; }
-
- public RequestResult(HttpStatusCode statusCode, Stream result)
- {
- this.statusCode = statusCode;
- this.result = result;
- }
-
- public RequestResult(HttpStatusCode statusCode, Stream result, bool hasBeenRedirected, string redirectedTo)
- : this(statusCode, result)
- {
- this.hasBeenRedirected = hasBeenRedirected;
- redirectedToUrl = redirectedTo;
- }
- }
- }
\ No newline at end of file
diff --git a/Tranga/GlobalBase.cs b/Tranga/GlobalBase.cs
new file mode 100644
index 0000000..34b3cd7
--- /dev/null
+++ b/Tranga/GlobalBase.cs
@@ -0,0 +1,108 @@
+using System.Globalization;
+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 static readonly NumberFormatInfo numberFormatDecimalPoint = new (){ NumberDecimalSeparator = "." };
+
+ 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..8a27929
--- /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 $"{GetType()}-{chapter.parentManga.internalId}-{chapter.chapterNumber}";
+ }
+
+ public override string ToString()
+ {
+ return $"{id} Chapter: {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..a386b98
--- /dev/null
+++ b/Tranga/Jobs/DownloadNewChapters.cs
@@ -0,0 +1,47 @@
+using Tranga.MangaConnectors;
+
+namespace Tranga.Jobs;
+
+public class DownloadNewChapters : Job
+{
+ public Manga manga { get; init; }
+ public string translatedLanguage { get; init; }
+
+ public DownloadNewChapters(GlobalBase clone, MangaConnector connector, Manga manga, DateTime lastExecution,
+ bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null, string translatedLanguage = "en") : base(clone, connector, lastExecution, recurring,
+ recurrence, parentJobId)
+ {
+ this.manga = manga;
+ this.translatedLanguage = translatedLanguage;
+ }
+
+ public DownloadNewChapters(GlobalBase clone, MangaConnector connector, Manga manga, bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null, string translatedLanguage = "en") : base (clone, connector, recurring, recurrence, parentJobId)
+ {
+ this.manga = manga;
+ this.translatedLanguage = translatedLanguage;
+ }
+
+ protected override string GetId()
+ {
+ return $"{GetType()}-{manga.internalId}";
+ }
+
+ public override string ToString()
+ {
+ return $"{id} Manga: {manga}";
+ }
+
+ protected override IEnumerable ExecuteReturnSubTasksInternal()
+ {
+ Chapter[] chapters = mangaConnector.GetNewChapters(manga, this.translatedLanguage);
+ 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..a54a023
--- /dev/null
+++ b/Tranga/Jobs/JobBoss.cs
@@ -0,0 +1,232 @@
+using System.Text.RegularExpressions;
+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)
+ {
+ this.jobs = new();
+ LoadJobsList(connectors);
+ 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 LoadJobsList(HashSet connectors)
+ {
+ Directory.CreateDirectory(settings.jobsFolderPath);
+ Regex idRex = new (@"(.*)\.json");
+
+ foreach (FileInfo file in new DirectoryInfo(settings.jobsFolderPath).EnumerateFiles())
+ if (idRex.IsMatch(file.Name))
+ {
+ Job job = JsonConvert.DeserializeObject(File.ReadAllText(file.FullName),
+ new JobJsonConverter(this, new MangaConnectorJsonConverter(this, connectors)))!;
+ this.jobs.Add(job);
+ }
+
+ foreach (Job job in this.jobs)
+ this.jobs.FirstOrDefault(jjob => jjob.id == job.parentJobId)?.AddSubJob(job);
+
+ foreach (DownloadNewChapters ncJob in this.jobs.Where(job => job is DownloadNewChapters))
+ cachedPublications.Add(ncJob.manga);
+ }
+
+ public void ExportJobsList()
+ {
+ Log("Exporting Jobs");
+ foreach (Job job in this.jobs)
+ {
+ string jobFilePath = Path.Join(settings.jobsFolderPath, $"{job.id}.json");
+ if (!File.Exists(jobFilePath))
+ {
+ string jobStr = JsonConvert.SerializeObject(job);
+ while(IsFileInUse(jobFilePath))
+ Thread.Sleep(10);
+ Log($"Exporting Job {jobFilePath}");
+ File.WriteAllText(jobFilePath, jobStr);
+ }
+ }
+
+ //Remove files with jobs not in this.jobs-list
+ Regex idRex = new (@"(.*)\.json");
+ foreach (FileInfo file in new DirectoryInfo(settings.jobsFolderPath).EnumerateFiles())
+ {
+ if (idRex.IsMatch(file.Name))
+ {
+ string id = idRex.Match(file.Name).Groups[1].Value;
+ if (!this.jobs.Any(job => job.id == id))
+ {
+ try
+ {
+ file.Delete();
+ }
+ catch (Exception e)
+ {
+ Log(e.ToString());
+ }
+ }
+ }
+ }
+ }
+
+ 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..9bada05
--- /dev/null
+++ b/Tranga/Jobs/ProgressToken.cs
@@ -0,0 +1,66 @@
+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 DateTime executionStarted { get; private set; }
+ public TimeSpan timeRemaining => GetTimeRemaining();
+
+ 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;
+ this.executionStarted = DateTime.UnixEpoch;
+ }
+
+ private float GetProgress()
+ {
+ if(increments > 0 && incrementsCompleted > 0)
+ return (float)incrementsCompleted / (float)increments;
+ return 0;
+ }
+
+ private TimeSpan GetTimeRemaining()
+ {
+ if (increments > 0 && incrementsCompleted > 0)
+ return DateTime.Now.Subtract(this.executionStarted).Divide(incrementsCompleted).Multiply(increments - incrementsCompleted);
+ return TimeSpan.MaxValue;
+ }
+
+ public void Increment()
+ {
+ this.incrementsCompleted++;
+ if (incrementsCompleted > increments)
+ state = State.Complete;
+ }
+
+ public void Standby()
+ {
+ state = State.Standby;
+ }
+
+ public void Start()
+ {
+ state = State.Running;
+ this.executionStarted = DateTime.Now;
+ }
+
+ 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 76%
rename from Tranga/Publication.cs
rename to Tranga/Manga.cs
index 330e7c0..9bde5c6 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
@@ -28,15 +28,15 @@ public struct Publication
public string? originalLanguage { get; }
// ReSharper disable once MemberCanBePrivate.Global
public string status { get; }
- public string folderName { get; }
+ public string folderName { get; private set; }
public string publicationId { get; }
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);
@@ -68,6 +73,15 @@ public struct Publication
return publicationFolder;
}
+ public void MovePublicationFolder(string downloadDirectory, string newFolderName)
+ {
+ string oldPath = Path.Join(downloadDirectory, this.folderName);
+ this.folderName = newFolderName;
+ string newPath = CreatePublicationFolder(downloadDirectory);
+ if(Directory.Exists(oldPath))
+ Directory.Move(oldPath, newPath);
+ }
+
public void SaveSeriesInfoJson(string downloadDirectory)
{
string publicationFolder = CreatePublicationFolder(downloadDirectory);
@@ -110,14 +124,30 @@ public struct Publication
[JsonRequired]public string year { get; }
[JsonRequired]public string status { get; }
[JsonRequired]public string description_text { get; }
-
+ [JsonIgnore] public static string[] continuing = new[]
+ {
+ "ongoing",
+ "hiatus",
+ "in corso",
+ "in pausa"
+ };
+ [JsonIgnore] public static string[] ended = new[]
+ {
+ "completed",
+ "cancelled",
+ "discontinued",
+ "finito",
+ "cancellato",
+ "droppato"
+ };
+
public Metadata(string name, string year, string status, string description_text)
{
this.name = name;
this.year = year;
- if(status.ToLower() == "ongoing" || status.ToLower() == "hiatus")
+ if(continuing.Contains(status.ToLower()))
this.status = "Continuing";
- else if (status.ToLower() == "completed" || status.ToLower() == "cancelled" || status.ToLower() == "discontinued")
+ else if(ended.Contains(status.ToLower()))
this.status = "Ended";
else
this.status = status;
diff --git a/Tranga/MangaConnectors/ChromiumDownloadClient.cs b/Tranga/MangaConnectors/ChromiumDownloadClient.cs
new file mode 100644
index 0000000..bbb5949
--- /dev/null
+++ b/Tranga/MangaConnectors/ChromiumDownloadClient.cs
@@ -0,0 +1,95 @@
+using System.Net;
+using System.Text;
+using HtmlAgilityPack;
+using PuppeteerSharp;
+
+namespace Tranga.MangaConnectors;
+
+internal class ChromiumDownloadClient : DownloadClient
+{
+ private IBrowser browser { get; set; }
+ private const string ChromiumVersion = "1154303";
+
+ private async Task DownloadBrowser()
+ {
+ BrowserFetcher browserFetcher = new BrowserFetcher();
+ foreach(string rev in browserFetcher.LocalRevisions().Where(rev => rev != ChromiumVersion))
+ browserFetcher.Remove(rev);
+ if (!browserFetcher.LocalRevisions().Contains(ChromiumVersion))
+ {
+ 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))
+ {
+ Log($"Browser download progress: {currentBytes:P2}");
+ last = DateTime.Now;
+ }
+
+ };
+ if (!browserFetcher.CanDownloadAsync(ChromiumVersion).Result)
+ {
+ Log($"Can't download browser version {ChromiumVersion}");
+ throw new Exception();
+ }
+ await browserFetcher.DownloadAsync(ChromiumVersion);
+ }
+
+ Log("Starting Browser.");
+ return await Puppeteer.LaunchAsync(new LaunchOptions
+ {
+ Headless = true,
+ ExecutablePath = browserFetcher.GetExecutablePath(ChromiumVersion),
+ Args = new [] {
+ "--disable-gpu",
+ "--disable-dev-shm-usage",
+ "--disable-setuid-sandbox",
+ "--no-sandbox"}
+ });
+ }
+
+ public ChromiumDownloadClient(GlobalBase clone, Dictionary rateLimitRequestsPerMinute) : base(clone, rateLimitRequestsPerMinute)
+ {
+ this.browser = DownloadBrowser().Result;
+ }
+
+ protected override RequestResult MakeRequestInternal(string url, string? referrer = null)
+ {
+ IPage page = this.browser!.NewPageAsync().Result;
+ IResponse response = page.GoToAsync(url, WaitUntilNavigation.DOMContentLoaded).Result;
+
+ Stream stream = Stream.Null;
+ HtmlDocument? document = null;
+
+ if (response.Headers.TryGetValue("Content-Type", out string? content))
+ {
+ if (content.Contains("text/html"))
+ {
+ string htmlString = page.GetContentAsync().Result;
+ stream = new MemoryStream(Encoding.Default.GetBytes(htmlString));
+ document = new ();
+ document.LoadHtml(htmlString);
+ }else if (content.Contains("image"))
+ {
+ stream = new MemoryStream(response.BufferAsync().Result);
+ }
+ }
+ else
+ {
+ page.CloseAsync();
+ return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
+ }
+
+ page.CloseAsync();
+ return new RequestResult(response.Status, document, stream, false, "");
+ }
+
+ public override void Close()
+ {
+ this.browser.CloseAsync();
+ }
+}
\ No newline at end of file
diff --git a/Tranga/MangaConnectors/DownloadClient.cs b/Tranga/MangaConnectors/DownloadClient.cs
new file mode 100644
index 0000000..1b87413
--- /dev/null
+++ b/Tranga/MangaConnectors/DownloadClient.cs
@@ -0,0 +1,66 @@
+using System.Net;
+using HtmlAgilityPack;
+
+namespace Tranga.MangaConnectors;
+
+internal abstract class DownloadClient : GlobalBase
+{
+ private readonly Dictionary _lastExecutedRateLimit;
+ private readonly Dictionary _rateLimit;
+
+ protected DownloadClient(GlobalBase clone, Dictionary rateLimitRequestsPerMinute) : base(clone)
+ {
+ this._lastExecutedRateLimit = new();
+ _rateLimit = new();
+ foreach (KeyValuePair limit in rateLimitRequestsPerMinute)
+ _rateLimit.Add(limit.Key, TimeSpan.FromMinutes(1).Divide(limit.Value));
+ }
+
+ public RequestResult MakeRequest(string url, byte requestType, string? referrer = null)
+ {
+ if (_rateLimit.TryGetValue(requestType, out TimeSpan value))
+ _lastExecutedRateLimit.TryAdd(requestType, DateTime.Now.Subtract(value));
+ else
+ {
+ Log("RequestType not configured for rate-limit.");
+ return new RequestResult(HttpStatusCode.NotAcceptable, null, Stream.Null);
+ }
+
+ TimeSpan rateLimitTimeout = _rateLimit[requestType]
+ .Subtract(DateTime.Now.Subtract(_lastExecutedRateLimit[requestType]));
+
+ if (rateLimitTimeout > TimeSpan.Zero)
+ Thread.Sleep(rateLimitTimeout);
+
+ RequestResult result = MakeRequestInternal(url, referrer);
+ _lastExecutedRateLimit[requestType] = DateTime.Now;
+ return result;
+ }
+
+ protected abstract RequestResult MakeRequestInternal(string url, string? referrer = null);
+ public abstract void Close();
+
+ public struct RequestResult
+ {
+ public HttpStatusCode statusCode { get; }
+ public Stream result { get; }
+ public bool hasBeenRedirected { get; }
+ public string? redirectedToUrl { get; }
+ public HtmlDocument? htmlDocument { get; }
+
+ public RequestResult(HttpStatusCode statusCode, HtmlDocument? htmlDocument, Stream result)
+ {
+ this.statusCode = statusCode;
+ this.htmlDocument = htmlDocument;
+ this.result = result;
+ }
+
+ public RequestResult(HttpStatusCode statusCode, HtmlDocument? htmlDocument, Stream result, bool hasBeenRedirected, string redirectedTo)
+ : this(statusCode, htmlDocument, result)
+ {
+ this.hasBeenRedirected = hasBeenRedirected;
+ redirectedToUrl = redirectedTo;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/Tranga/MangaConnectors/HttpDownloadClient.cs b/Tranga/MangaConnectors/HttpDownloadClient.cs
new file mode 100644
index 0000000..da5b795
--- /dev/null
+++ b/Tranga/MangaConnectors/HttpDownloadClient.cs
@@ -0,0 +1,70 @@
+using System.Net.Http.Headers;
+using HtmlAgilityPack;
+
+namespace Tranga.MangaConnectors;
+
+internal class HttpDownloadClient : DownloadClient
+{
+ private static readonly HttpClient Client = new()
+ {
+ Timeout = TimeSpan.FromSeconds(60),
+ DefaultRequestHeaders =
+ {
+ UserAgent =
+ {
+ new ProductInfoHeaderValue("Tranga", "0.1")
+ }
+ }
+ };
+
+
+ public HttpDownloadClient(GlobalBase clone, Dictionary rateLimitRequestsPerMinute) : base(clone, rateLimitRequestsPerMinute)
+ {
+
+ }
+
+ protected override RequestResult MakeRequestInternal(string url, string? referrer = null)
+ {
+ HttpResponseMessage? response = null;
+ while (response is null)
+ {
+ HttpRequestMessage requestMessage = new(HttpMethod.Get, url);
+ if (referrer is not null)
+ requestMessage.Headers.Referrer = new Uri(referrer);
+ //Log($"Requesting {requestType} {url}");
+ response = Client.Send(requestMessage);
+ }
+
+ if (!response.IsSuccessStatusCode)
+ {
+ Log($"Request-Error {response.StatusCode}: {response.ReasonPhrase}");
+ return new RequestResult(response.StatusCode, null, Stream.Null);
+ }
+
+ Stream stream = response.Content.ReadAsStream();
+
+ HtmlDocument? document = null;
+
+ if (response.Content.Headers.ContentType?.MediaType == "text/html")
+ {
+ StreamReader reader = new (stream);
+ document = new ();
+ document.LoadHtml(reader.ReadToEnd());
+ stream.Position = 0;
+ }
+
+ // Request has been redirected to another page. For example, it redirects directly to the results when there is only 1 result
+ if (response.RequestMessage is not null && response.RequestMessage.RequestUri is not null)
+ {
+ return new RequestResult(response.StatusCode, document, stream, true,
+ response.RequestMessage.RequestUri.AbsoluteUri);
+ }
+
+ return new RequestResult(response.StatusCode, document, stream);
+ }
+
+ public override void Close()
+ {
+ Log("Closing.");
+ }
+}
\ No newline at end of file
diff --git a/Tranga/Connectors/Connector.cs b/Tranga/MangaConnectors/MangaConnector.cs
similarity index 63%
rename from Tranga/Connectors/Connector.cs
rename to Tranga/MangaConnectors/MangaConnector.cs
index 997aeec..74b49e9 100644
--- a/Tranga/Connectors/Connector.cs
+++ b/Tranga/MangaConnectors/MangaConnector.cs
@@ -1,82 +1,74 @@
-using System.Globalization;
-using System.IO.Compression;
+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)
- {
- this.settings = settings;
- this.commonObjects = commonObjects;
- if (!Directory.Exists(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 = "")
+ public void StopDownloadClient()
{
- Publication[] ret = GetPublicationsInternal(publicationTitle);
- foreach (Publication p in ret)
- publicationCollection.Add(p);
- return ret;
+ downloadClient.Close();
+ }
+
+ protected MangaConnector(GlobalBase clone, string name) : base(clone)
+ {
+ this.name = name;
+ Directory.CreateDirectory(settings.coverImageCache);
}
+ public string name { get; } //Name of the Connector (e.g. Website)
+
///
/// 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);
- NumberFormatInfo decimalPoint = new (){ NumberDecimalSeparator = "." };
- commonObjects.logger?.WriteLine(this.GetType().ToString(), "Checking for duplicates");
+ Log($"Getting new Chapters for {manga}");
+ Chapter[] newChapters = this.GetChapters(manga, language);
+ Log($"Checking for duplicates {manga}");
List newChaptersList = newChapters.Where(nChapter =>
- float.Parse(nChapter.chapterNumber, decimalPoint) > publication.ignoreChaptersBelow &&
+ float.Parse(nChapter.chapterNumber, numberFormatDecimalPoint) > 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 +139,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 +183,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,31 +207,38 @@ 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;
}
protected string SaveCoverImageToCache(string url, byte requestType)
{
- string[] split = url.Split('/');
- string filename = split[^1];
+ string filename = url.Split('/')[^1].Split('?')[0];
string saveImagePath = Path.Join(settings.coverImageCache, filename);
if (File.Exists(saveImagePath))
@@ -265,7 +248,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..b9f3af0
--- /dev/null
+++ b/Tranga/MangaConnectors/MangaConnectorJsonConverter.cs
@@ -0,0 +1,51 @@
+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);
+ case "Mangaworld":
+ return this.connectors.First(c => c is Mangaworld);
+ }
+
+ 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 51%
rename from Tranga/Connectors/MangaDex.cs
rename to Tranga/MangaConnectors/MangaDex.cs
index b63f7a3..f0203bd 100644
--- a/Tranga/Connectors/MangaDex.cs
+++ b/Tranga/MangaConnectors/MangaDex.cs
@@ -1,14 +1,13 @@
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; }
-
private enum RequestType : byte
{
Manga,
@@ -18,26 +17,25 @@ public class MangaDex : Connector
Author,
}
- public MangaDex(TrangaSettings settings, CommonObjects commonObjects) : base(settings, commonObjects)
+ public MangaDex(GlobalBase clone) : base(clone, "MangaDex")
{
- name = "MangaDex";
- this.downloadClient = new DownloadClient(new Dictionary()
+ this.downloadClient = new HttpDownloadClient(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 +55,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 +182,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 +213,21 @@ 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}");
- return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
+ Log($"Got {chapters.Count} chapters. {manga}");
+ return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, numberFormatDecimalPoint)).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 +249,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 +273,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 +293,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 69%
rename from Tranga/Connectors/MangaKatana.cs
rename to Tranga/MangaConnectors/MangaKatana.cs
index 6205d38..12f15cd 100644
--- a/Tranga/Connectors/MangaKatana.cs
+++ b/Tranga/MangaConnectors/MangaKatana.cs
@@ -2,32 +2,29 @@
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, "MangaKatana")
{
- this.name = "MangaKatana";
- this.downloadClient = new DownloadClient(new Dictionary()
+ this.downloadClient = new HttpDownloadClient(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 +35,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 +57,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 +65,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 +136,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);
@@ -146,16 +153,12 @@ public class MangaKatana : Connector
return Array.Empty();
//Return Chapters ordered by Chapter-Number
- NumberFormatInfo chapterNumberFormatInfo = new()
- {
- NumberDecimalSeparator = "."
- };
- List chapters = ParseChaptersFromHtml(publication, requestUrl);
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Done getting Chapters for {publication.internalId}");
- return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
+ List chapters = ParseChaptersFromHtml(manga, requestUrl);
+ Log($"Got {chapters.Count} chapters. {manga}");
+ return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, numberFormatDecimalPoint)).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 +177,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 +201,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 57%
rename from Tranga/Connectors/Manganato.cs
rename to Tranga/MangaConnectors/Manganato.cs
index cfc9791..6b3cd23 100644
--- a/Tranga/Connectors/Manganato.cs
+++ b/Tranga/MangaConnectors/Manganato.cs
@@ -2,42 +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, "Manganato")
{
- this.name = "Manganato";
- this.downloadClient = new DownloadClient(new Dictionary()
+ this.downloadClient = new HttpDownloadClient(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})");
- string sanitizedTitle = string.Join('_', Regex.Matches(publicationTitle, "[A-z]*")).ToLower();
+ Log($"Searching Publications. Term=\"{publicationTitle}\"");
+ string sanitizedTitle = string.Join('_', Regex.Matches(publicationTitle, "[A-z]*").Where(str => str.Length > 0)).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);
+ if (requestResult.htmlDocument is null)
+ return Array.Empty();
+ Manga[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
+ Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
+ return publications;
}
- private Publication[] ParsePublicationsFromHtml(Stream html)
+ private Manga[] ParsePublicationsFromHtml(HtmlDocument document)
{
- StreamReader reader = new (html);
- string htmlString = reader.ReadToEnd();
- HtmlDocument document = new ();
- document.LoadHtml(htmlString);
IEnumerable searchResults = document.DocumentNode.Descendants("div").Where(n => n.HasClass("search-story-item"));
List urls = new();
foreach (HtmlNode mangaResult in searchResults)
@@ -46,26 +43,31 @@ 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;
+
+ if (requestResult.htmlDocument is null)
+ return null;
+ return ParseSinglePublicationFromHtml(requestResult.htmlDocument, url.Split('/')[^1]);
+ }
+
+ private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId)
{
- StreamReader reader = new (html);
- string htmlString = reader.ReadToEnd();
- HtmlDocument document = new ();
- document.LoadHtml(htmlString);
string status = "";
Dictionary altTitles = new();
Dictionary? links = null;
@@ -119,79 +121,78 @@ 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)
return Array.Empty();
//Return Chapters ordered by Chapter-Number
- NumberFormatInfo chapterNumberFormatInfo = new()
- {
- NumberDecimalSeparator = "."
- };
- List chapters = ParseChaptersFromHtml(publication, requestResult.result);
- commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Done getting Chapters for {publication.internalId}");
- return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
+ if (requestResult.htmlDocument is null)
+ return Array.Empty();
+ List chapters = ParseChaptersFromHtml(manga, requestResult.htmlDocument);
+ Log($"Got {chapters.Count} chapters. {manga}");
+ return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, numberFormatDecimalPoint)).ToArray();
}
- private List ParseChaptersFromHtml(Publication publication, Stream html)
+ private List ParseChaptersFromHtml(Manga manga, HtmlDocument document)
{
- StreamReader reader = new (html);
- string htmlString = reader.ReadToEnd();
- HtmlDocument document = new ();
- document.LoadHtml(htmlString);
List ret = new();
HtmlNode chapterList = document.DocumentNode.Descendants("ul").First(l => l.HasClass("row-content-chapter"));
+ Regex volRex = new(@"Vol\.([0-9]+).*");
+ Regex chapterRex = new(@"Chapter ([0-9]+(\.[0-9]+)*){1}.*");
+ Regex nameRex = new(@"Chapter ([0-9]+(\.[0-9]+)*){1}:? (.*)");
+
foreach (HtmlNode chapterInfo in chapterList.Descendants("li"))
{
string fullString = chapterInfo.Descendants("a").First(d => d.HasClass("chapter-name")).InnerText;
- string? volumeNumber = fullString.Contains("Vol.") ? fullString.Replace("Vol.", "").Split(' ')[0] : null;
- string chapterNumber = fullString.Split(':')[0].Split("Chapter ")[1].Replace('-','.');
- string chapterName = string.Concat(fullString.Split(':')[1..]);
+ string? volumeNumber = volRex.IsMatch(fullString) ? volRex.Match(fullString).Groups[1].Value : null;
+ string chapterNumber = chapterRex.Match(fullString).Groups[1].Value;
+ string chapterName = nameRex.Match(fullString).Groups[3].Value;
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);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return requestResult.statusCode;
- string[] imageUrls = ParseImageUrlsFromHtml(requestResult.result);
+ if (requestResult.htmlDocument is null)
+ return HttpStatusCode.InternalServerError;
+ string[] imageUrls = ParseImageUrlsFromHtml(requestResult.htmlDocument);
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)
+ private string[] ParseImageUrlsFromHtml(HtmlDocument document)
{
- StreamReader reader = new (html);
- string htmlString = reader.ReadToEnd();
- HtmlDocument document = new ();
- document.LoadHtml(htmlString);
List ret = new();
HtmlNode imageContainer =
diff --git a/Tranga/MangaConnectors/Mangasee.cs b/Tranga/MangaConnectors/Mangasee.cs
new file mode 100644
index 0000000..aeae553
--- /dev/null
+++ b/Tranga/MangaConnectors/Mangasee.cs
@@ -0,0 +1,205 @@
+using System.Net;
+using System.Text.RegularExpressions;
+using System.Xml.Linq;
+using HtmlAgilityPack;
+using Newtonsoft.Json;
+using Tranga.Jobs;
+
+namespace Tranga.MangaConnectors;
+
+public class Mangasee : MangaConnector
+{
+ public Mangasee(GlobalBase clone) : base(clone, "Mangasee")
+ {
+ this.downloadClient = new ChromiumDownloadClient(clone, new Dictionary()
+ {
+ { 1, 60 }
+ });
+ }
+
+ public override Manga[] GetManga(string 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();
+
+ if (requestResult.htmlDocument is null)
+ return Array.Empty();
+ Manga[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument, publicationTitle);
+ Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
+ return publications;
+ }
+
+ public override Manga? GetMangaFromUrl(string url)
+ {
+ Regex publicationIdRex = new(@"https:\/\/mangasee123.com\/manga\/(.*)(\/.*)*");
+ string publicationId = publicationIdRex.Match(url).Groups[1].Value;
+
+ DownloadClient.RequestResult requestResult = this.downloadClient.MakeRequest(url, 1);
+ if(requestResult.htmlDocument is not null)
+ return ParseSinglePublicationFromHtml(requestResult.htmlDocument, publicationId);
+ return null;
+ }
+
+ private Manga[] ParsePublicationsFromHtml(HtmlDocument document, string publicationTitle)
+ {
+ string jsonString = document.DocumentNode.SelectSingleNode("//body").InnerText;
+ List result = JsonConvert.DeserializeObject>(jsonString)!;
+ Dictionary queryFiltered = new();
+ foreach (SearchResultItem resultItem in result)
+ {
+ int matches = resultItem.GetMatches(publicationTitle);
+ if (matches > 0)
+ queryFiltered.TryAdd(resultItem, matches);
+ }
+
+ queryFiltered = queryFiltered.Where(item => item.Value >= publicationTitle.Split(' ').Length - 1)
+ .ToDictionary(item => item.Key, item => item.Value);
+
+ Log($"Retrieved {queryFiltered.Count} publications.");
+
+ HashSet ret = new();
+ List orderedFiltered =
+ queryFiltered.OrderBy(item => item.Value).ToDictionary(item => item.Key, item => item.Value).Keys.ToList();
+
+ foreach (SearchResultItem orderedItem in orderedFiltered)
+ {
+ Manga? manga = GetMangaFromUrl($"https://mangasee123.com/manga/{orderedItem.i}");
+ if (manga is not null)
+ ret.Add((Manga)manga);
+ }
+ return ret.ToArray();
+ }
+
+
+ private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId)
+ {
+ string originalLanguage = "", status = "";
+ Dictionary altTitles = new(), links = new();
+ HashSet tags = new();
+
+ HtmlNode posterNode = document.DocumentNode.SelectSingleNode("//div[@class='BoxBody']//div[@class='row']//img");
+ string posterUrl = posterNode.GetAttributeValue("src", "");
+ string coverFileNameInCache = SaveCoverImageToCache(posterUrl, 1);
+
+ HtmlNode titleNode = document.DocumentNode.SelectSingleNode("//div[@class='BoxBody']//div[@class='row']//h1");
+ string sortName = titleNode.InnerText;
+
+ 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 = 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 = document.DocumentNode.SelectNodes("//div[@class='BoxBody']//div[@class='row']//span[text()='Released:']/..").Descendants("a").First();
+ int year = Convert.ToInt32(yearNode.InnerText);
+
+ 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 = document.DocumentNode.SelectNodes("//div[@class='BoxBody']//div[@class='row']//span[text()='Description:']/..").Descendants("div").First();
+ string description = descriptionNode.InnerText;
+
+ 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
+ private class SearchResultItem
+ {
+ public string i { get; init; }
+ public string s { get; init; }
+ public string[] a { get; init; }
+
+ [JsonConstructor]
+ public SearchResultItem(string i, string s, string[] a)
+ {
+ this.i = i;
+ this.s = s;
+ this.a = a;
+ }
+
+ public int GetMatches(string title)
+ {
+ int ret = 0;
+ Regex cleanRex = new("[A-z0-9]*");
+ string[] badWords = { "a", "an", "no", "ni", "so", "as", "and", "the", "of", "that", "in", "is", "for" };
+
+ string[] titleTerms = title.Split(new[] { ' ', '-' }).Where(str => !badWords.Contains(str)).ToArray();
+
+ foreach (Match matchTerm in cleanRex.Matches(this.i))
+ ret += titleTerms.Count(titleTerm =>
+ titleTerm.Equals(matchTerm.Value, StringComparison.OrdinalIgnoreCase));
+
+ foreach (Match matchTerm in cleanRex.Matches(this.s))
+ ret += titleTerms.Count(titleTerm =>
+ titleTerm.Equals(matchTerm.Value, StringComparison.OrdinalIgnoreCase));
+
+ foreach(string alt in this.a)
+ foreach (Match matchTerm in cleanRex.Matches(alt))
+ ret += titleTerms.Count(titleTerm =>
+ titleTerm.Equals(matchTerm.Value, StringComparison.OrdinalIgnoreCase));
+
+ return ret;
+ }
+ }
+
+ public override Chapter[] GetChapters(Manga manga, string language="en")
+ {
+ Log($"Getting chapters {manga}");
+ XDocument doc = XDocument.Load($"https://mangasee123.com/rss/{manga.publicationId}.xml");
+ XElement[] chapterItems = doc.Descendants("item").ToArray();
+ List chapters = new();
+ foreach (XElement chapter in chapterItems)
+ {
+ string volumeNumber = "1";
+ string chapterName = chapter.Descendants("title").First().Value;
+ string chapterNumber = Regex.Matches(chapterName, "[0-9]+")[^1].ToString();
+
+ string url = chapter.Descendants("link").First().Value;
+ url = url.Replace(Regex.Matches(url,"(-page-[0-9])")[0].ToString(),"");
+ chapters.Add(new Chapter(manga, "", volumeNumber, chapterNumber, url));
+ }
+
+ //Return Chapters ordered by Chapter-Number
+ Log($"Got {chapters.Count} chapters. {manga}");
+ return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, numberFormatDecimalPoint)).ToArray();
+ }
+
+ public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
+ {
+ if (progressToken?.cancellationRequested ?? false)
+ return HttpStatusCode.RequestTimeout;
+ Manga chapterParentManga = chapter.parentManga;
+ if (progressToken?.cancellationRequested??false)
+ return HttpStatusCode.RequestTimeout;
+
+ Log($"Retrieving chapter-info {chapter} {chapterParentManga}");
+
+ DownloadClient.RequestResult requestResult = this.downloadClient.MakeRequest(chapter.url, 1);
+ if(requestResult.htmlDocument is null)
+ return HttpStatusCode.RequestTimeout;
+ HtmlDocument document = requestResult.htmlDocument;
+
+ HtmlNode gallery = document.DocumentNode.Descendants("div").First(div => div.HasClass("ImageGallery"));
+ HtmlNode[] images = gallery.Descendants("img").Where(img => img.HasClass("img-fluid")).ToArray();
+ List urls = new();
+ foreach(HtmlNode galleryImage in images)
+ urls.Add(galleryImage.GetAttributeValue("src", ""));
+
+ string comicInfoPath = Path.GetTempFileName();
+ File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
+
+ return DownloadChapterImages(urls.ToArray(), chapter.GetArchiveFilePath(settings.downloadLocation), 1, comicInfoPath, progressToken:progressToken);
+ }
+}
\ No newline at end of file
diff --git a/Tranga/MangaConnectors/Mangaworld.cs b/Tranga/MangaConnectors/Mangaworld.cs
new file mode 100644
index 0000000..ae61871
--- /dev/null
+++ b/Tranga/MangaConnectors/Mangaworld.cs
@@ -0,0 +1,186 @@
+using System.Net;
+using System.Text.RegularExpressions;
+using HtmlAgilityPack;
+using Tranga.Jobs;
+
+namespace Tranga.MangaConnectors;
+
+public class Mangaworld: MangaConnector
+{
+ public Mangaworld(GlobalBase clone) : base(clone, "Mangaworld")
+ {
+ this.downloadClient = new HttpDownloadClient(clone, new Dictionary()
+ {
+ {1, 60}
+ });
+ }
+
+ public override Manga[] GetManga(string publicationTitle = "")
+ {
+ Log($"Searching Publications. Term=\"{publicationTitle}\"");
+ string sanitizedTitle = string.Join(' ', Regex.Matches(publicationTitle, "[A-z]*").Where(str => str.Length > 0)).ToLower();
+ string requestUrl = $"https://www.mangaworld.bz/archive?keyword={sanitizedTitle}";
+ DownloadClient.RequestResult requestResult =
+ downloadClient.MakeRequest(requestUrl, 1);
+ if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
+ return Array.Empty();
+
+ if (requestResult.htmlDocument is null)
+ return Array.Empty();
+ Manga[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
+ Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
+ return publications;
+ }
+
+ private Manga[] ParsePublicationsFromHtml(HtmlDocument document)
+ {
+ if (!document.DocumentNode.SelectSingleNode("//div[@class='comics-grid']").ChildNodes
+ .Any(node => node.HasClass("entry")))
+ return Array.Empty();
+
+ List urls = document.DocumentNode
+ .SelectNodes(
+ "//div[@class='comics-grid']//div[@class='entry']//a[contains(concat(' ',normalize-space(@class),' '),'thumb')]")
+ .Select(thumb => thumb.GetAttributeValue("href", "")).ToList();
+
+ HashSet ret = new();
+ foreach (string url in urls)
+ {
+ Manga? manga = GetMangaFromUrl(url);
+ if (manga is not null)
+ ret.Add((Manga)manga);
+ }
+
+ return ret.ToArray();
+ }
+
+ public override Manga? GetMangaFromUrl(string url)
+ {
+ DownloadClient.RequestResult requestResult =
+ downloadClient.MakeRequest(url, 1);
+ if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
+ return null;
+
+ if (requestResult.htmlDocument is null)
+ return null;
+
+ return ParseSinglePublicationFromHtml(requestResult.htmlDocument, url.Split('/')[^2]);
+ }
+
+ private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId)
+ {
+ string status = "";
+ Dictionary altTitles = new();
+ Dictionary? links = null;
+ HashSet tags = new();
+ string[] authors = Array.Empty();
+ string originalLanguage = "";
+
+ HtmlNode infoNode = document.DocumentNode.Descendants("div").First(d => d.HasClass("info"));
+
+ string sortName = infoNode.Descendants("h1").First().InnerText;
+
+ HtmlNode metadata = infoNode.Descendants().First(d => d.HasClass("meta-data"));
+
+ HtmlNode altTitlesNode = metadata.SelectSingleNode("//span[text()='Titoli alternativi: ']/..").ChildNodes[1];
+
+ string[] alts = altTitlesNode.InnerText.Split(", ");
+ for(int i = 0; i < alts.Length; i++)
+ altTitles.Add(i.ToString(), alts[i]);
+
+ HtmlNode genresNode =
+ metadata.SelectSingleNode("//span[text()='Generi: ']/..");
+ tags = genresNode.SelectNodes("a").Select(node => node.InnerText).ToHashSet();
+
+ HtmlNode authorsNode =
+ metadata.SelectSingleNode("//span[text()='Autore: ']/..");
+ authors = new[] { authorsNode.SelectNodes("a").First().InnerText };
+
+ status = metadata.SelectSingleNode("//span[text()='Stato: ']/..").SelectNodes("a").First().InnerText;
+
+ string posterUrl = document.DocumentNode.SelectSingleNode("//img[@class='rounded']").GetAttributeValue("src", "");
+
+ string coverFileNameInCache = SaveCoverImageToCache(posterUrl, 1);
+
+ string description = document.DocumentNode.SelectSingleNode("//div[@id='noidungm']").InnerText;
+
+ string yearString = metadata.SelectSingleNode("//span[text()='Anno di uscita: ']/..").SelectNodes("a").First().InnerText;
+ int year = Convert.ToInt32(yearString);
+
+ 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(Manga manga, string language="en")
+ {
+ Log($"Getting chapters {manga}");
+ string requestUrl = $"https://www.mangaworld.bz/manga/{manga.publicationId}";
+ DownloadClient.RequestResult requestResult =
+ downloadClient.MakeRequest(requestUrl, 1);
+ if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
+ return Array.Empty();
+
+ //Return Chapters ordered by Chapter-Number
+ if (requestResult.htmlDocument is null)
+ return Array.Empty();
+ List chapters = ParseChaptersFromHtml(manga, requestResult.htmlDocument);
+ Log($"Got {chapters.Count} chapters. {manga}");
+ return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, numberFormatDecimalPoint)).ToArray();
+ }
+
+ private List ParseChaptersFromHtml(Manga manga, HtmlDocument document)
+ {
+ List ret = new();
+
+ foreach (HtmlNode volNode in document.DocumentNode.SelectNodes(
+ "//div[contains(concat(' ',normalize-space(@class),' '),'chapters-wrapper')]//div[contains(concat(' ',normalize-space(@class),' '),'volume-element')]"))
+ {
+ string volume = volNode.SelectNodes("div").First(node => node.HasClass("volume")).SelectSingleNode("p").InnerText.Split(' ')[^1];
+ foreach (HtmlNode chNode in volNode.SelectNodes("div").First(node => node.HasClass("volume-chapters")).SelectNodes("div"))
+ {
+ string number = chNode.SelectSingleNode("a").SelectSingleNode("span").InnerText.Split(" ")[^1];
+ string url = chNode.SelectSingleNode("a").GetAttributeValue("href", "");
+ ret.Add(new Chapter(manga, null, volume, number, url));
+ }
+ }
+
+ ret.Reverse();
+ return ret;
+ }
+
+ public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
+ {
+ if (progressToken?.cancellationRequested ?? false)
+ return HttpStatusCode.RequestTimeout;
+ Manga chapterParentManga = chapter.parentManga;
+ Log($"Retrieving chapter-info {chapter} {chapterParentManga}");
+ string requestUrl = $"{chapter.url}?style=list";
+ DownloadClient.RequestResult requestResult =
+ downloadClient.MakeRequest(requestUrl, 1);
+ if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
+ return requestResult.statusCode;
+
+ if (requestResult.htmlDocument is null)
+ return HttpStatusCode.InternalServerError;
+ string[] imageUrls = ParseImageUrlsFromHtml(requestResult.htmlDocument);
+
+ string comicInfoPath = Path.GetTempFileName();
+ File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
+
+ return DownloadChapterImages(imageUrls, chapter.GetArchiveFilePath(settings.downloadLocation), 1, comicInfoPath, "https://www.mangaworld.bz/", progressToken:progressToken);
+ }
+
+ private string[] ParseImageUrlsFromHtml(HtmlDocument document)
+ {
+ List