diff --git a/.github/workflows/docker-base.yml b/.github/workflows/docker-base.yml deleted file mode 100644 index 0ec505d..0000000 --- a/.github/workflows/docker-base.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Docker Image CI - -on: - workflow_dispatch: - -jobs: - - build: - - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v3 - - # https://github.com/docker/setup-qemu-action#usage - - name: Set up QEMU - uses: docker/setup-qemu-action@v2.2.0 - - # https://github.com/marketplace/actions/docker-setup-buildx - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v2.10.0 - - # https://github.com/docker/login-action#docker-hub - - name: Login to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - # https://github.com/docker/build-push-action#multi-platform-image - - name: Build and push base - uses: docker/build-push-action@v4.1.1 - with: - context: ./ - file: ./Dockerfile-base - #platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6 - platforms: linux/amd64 - pull: true - push: true - tags: | - glax/tranga-base:latest \ No newline at end of file diff --git a/.github/workflows/docker-image-cuttingedge.yml b/.github/workflows/docker-image-cuttingedge.yml index 3fe0c15..9035d11 100644 --- a/.github/workflows/docker-image-cuttingedge.yml +++ b/.github/workflows/docker-image-cuttingedge.yml @@ -31,19 +31,6 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - # https://github.com/docker/build-push-action#multi-platform-image - - name: Build and push API - uses: docker/build-push-action@v4.1.1 - with: - context: ./ - file: ./Dockerfile - #platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6 - platforms: linux/amd64 - pull: true - push: true - tags: | - glax/tranga-api:cuttingedge - # https://github.com/docker/build-push-action#multi-platform-image - name: Build and push Website uses: docker/build-push-action@v4.1.1 diff --git a/.github/workflows/docker-image-master.yml b/.github/workflows/docker-image-master.yml index 7efce24..c647b0d 100644 --- a/.github/workflows/docker-image-master.yml +++ b/.github/workflows/docker-image-master.yml @@ -33,19 +33,6 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - # https://github.com/docker/build-push-action#multi-platform-image - - name: Build and push API - uses: docker/build-push-action@v4.1.1 - with: - context: ./ - file: ./Dockerfile - #platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6 - platforms: linux/amd64 - pull: true - push: true - tags: | - glax/tranga-api:latest - # https://github.com/docker/build-push-action#multi-platform-image - name: Build and push Website uses: docker/build-push-action@v4.1.1 diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 17b6cde..0000000 --- a/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -# syntax=docker/dockerfile:1 - -FROM mcr.microsoft.com/dotnet/sdk:7.0 as build-env -WORKDIR /src -COPY Tranga /src/Tranga -COPY Logging /src/Logging -COPY Tranga.sln /src -RUN dotnet restore /src/Tranga/Tranga.csproj -RUN dotnet publish -c Release -o /publish - -FROM glax/tranga-base:latest as runtime -WORKDIR /publish -COPY --from=build-env /publish . -EXPOSE 6531 -ENTRYPOINT ["dotnet", "/publish/Tranga.dll"] diff --git a/Dockerfile-base b/Dockerfile-base deleted file mode 100644 index 6c9c7f1..0000000 --- a/Dockerfile-base +++ /dev/null @@ -1,8 +0,0 @@ -# syntax=docker/dockerfile:1 -#FROM mcr.microsoft.com/dotnet/aspnet:7.0 as runtime -FROM mcr.microsoft.com/dotnet/runtime:7.0 as runtime -WORKDIR /publish -RUN apt-get update -RUN apt-get install -y libx11-6 libx11-xcb1 libatk1.0-0 libgtk-3-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2 libxshmfence1 libnss3 -RUN apt-get autopurge -y -RUN apt-get autoclean -y \ No newline at end of file diff --git a/Logging/FileLogger.cs b/Logging/FileLogger.cs deleted file mode 100644 index 10fd0ec..0000000 --- a/Logging/FileLogger.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Text; - -namespace Logging; - -public class FileLogger : LoggerBase -{ - private string logFilePath { get; } - private const int MaxNumberOfLogFiles = 5; - - public FileLogger(string logFilePath, Encoding? encoding = null) : base (encoding) - { - this.logFilePath = logFilePath; - - //Remove oldest logfile if more than MaxNumberOfLogFiles - string parentFolderPath = Path.GetDirectoryName(logFilePath)!; - for (int fileCount = new DirectoryInfo(parentFolderPath).EnumerateFiles().Count(); fileCount > MaxNumberOfLogFiles - 1; fileCount--) //-1 because we create own logfile later - File.Delete(new DirectoryInfo(parentFolderPath).EnumerateFiles().MinBy(file => file.LastWriteTime)!.FullName); - } - - protected override void Write(LogMessage logMessage) - { - try - { - File.AppendAllText(logFilePath, logMessage.formattedMessage); - } - catch (Exception) - { - // ignored - } - } -} \ No newline at end of file diff --git a/Logging/FormattedConsoleLogger.cs b/Logging/FormattedConsoleLogger.cs deleted file mode 100644 index 2e920fe..0000000 --- a/Logging/FormattedConsoleLogger.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Text; - -namespace Logging; - -public class FormattedConsoleLogger : LoggerBase -{ - private readonly TextWriter _stdOut; - public FormattedConsoleLogger(TextWriter stdOut, Encoding? encoding = null) : base(encoding) - { - this._stdOut = stdOut; - } - - protected override void Write(LogMessage message) - { - this._stdOut.Write(message.formattedMessage); - } -} \ No newline at end of file diff --git a/Logging/LogMessage.cs b/Logging/LogMessage.cs deleted file mode 100644 index 56515c0..0000000 --- a/Logging/LogMessage.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Logging; - -public class LogMessage -{ - public DateTime logTime { get; } - public string caller { get; } - public string value { get; } - public string formattedMessage => ToString(); - - public LogMessage(DateTime messageTime, string caller, string value) - { - this.logTime = messageTime; - this.caller = caller; - this.value = value; - } - - public override string ToString() - { - string dateTimeString = $"{logTime.ToShortDateString()} {logTime.ToLongTimeString()}.{logTime.Millisecond,-3}"; - string name = caller.Split(new char[] { '.', '+' }).Last(); - return $"[{dateTimeString}] {name.Substring(0, name.Length >= 13 ? 13 : name.Length),13} | {value}"; - } -} \ No newline at end of file diff --git a/Logging/Logger.cs b/Logging/Logger.cs deleted file mode 100644 index 9ca3d28..0000000 --- a/Logging/Logger.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Text; - -namespace Logging; - -public class Logger : TextWriter -{ - public override Encoding Encoding { get; } - public enum LoggerType - { - FileLogger, - ConsoleLogger - } - - private readonly FileLogger? _fileLogger; - private readonly FormattedConsoleLogger? _formattedConsoleLogger; - private readonly MemoryLogger _memoryLogger; - - public Logger(LoggerType[] enabledLoggers, TextWriter? stdOut, Encoding? encoding, string? logFilePath) - { - this.Encoding = encoding ?? Encoding.ASCII; - if (enabledLoggers.Contains(LoggerType.FileLogger) && logFilePath is not null) - _fileLogger = new FileLogger(logFilePath, encoding); - else - { - _fileLogger = null; - throw new ArgumentException($"logFilePath can not be null for LoggerType {LoggerType.FileLogger}"); - } - - if (enabledLoggers.Contains(LoggerType.ConsoleLogger) && stdOut is not null) - { - _formattedConsoleLogger = new FormattedConsoleLogger(stdOut, encoding); - } - else if (enabledLoggers.Contains(LoggerType.ConsoleLogger) && stdOut is null) - { - _formattedConsoleLogger = null; - throw new ArgumentException($"stdOut can not be null for LoggerType {LoggerType.ConsoleLogger}"); - } - _memoryLogger = new MemoryLogger(encoding); - } - - public void WriteLine(string caller, string? value) - { - value = value is null ? Environment.NewLine : string.Concat(value, Environment.NewLine); - - Write(caller, value); - } - - public void Write(string caller, string? value) - { - if (value is null) - return; - - _fileLogger?.Write(caller, value); - _formattedConsoleLogger?.Write(caller, value); - _memoryLogger.Write(caller, value); - } - - public string[] Tail(uint? lines) - { - return _memoryLogger.Tail(lines); - } - - public string[] GetNewLines() - { - return _memoryLogger.GetNewLines(); - } -} \ No newline at end of file diff --git a/Logging/LoggerBase.cs b/Logging/LoggerBase.cs deleted file mode 100644 index 0b86074..0000000 --- a/Logging/LoggerBase.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Text; - -namespace Logging; - -public abstract class LoggerBase : TextWriter -{ - public override Encoding Encoding { get; } - - public LoggerBase(Encoding? encoding = null) - { - this.Encoding = encoding ?? Encoding.ASCII; - } - - public void Write(string caller, string? value) - { - if (value is null) - return; - - LogMessage message = new (DateTime.Now, caller, value); - - Write(message); - } - - protected abstract void Write(LogMessage message); -} \ No newline at end of file diff --git a/Logging/Logging.csproj b/Logging/Logging.csproj deleted file mode 100644 index 6836c68..0000000 --- a/Logging/Logging.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - net7.0 - enable - enable - - - diff --git a/Logging/MemoryLogger.cs b/Logging/MemoryLogger.cs deleted file mode 100644 index 47581f2..0000000 --- a/Logging/MemoryLogger.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Text; - -namespace Logging; - -public class MemoryLogger : LoggerBase -{ - private readonly SortedList _logMessages = new(); - private int _lastLogMessageIndex = 0; - - public MemoryLogger(Encoding? encoding = null) : base(encoding) - { - - } - - protected override void Write(LogMessage value) - { - while(!_logMessages.TryAdd(value.logTime, value)) - Thread.Sleep(10); - } - - public string[] GetLogMessage() - { - return Tail(Convert.ToUInt32(_logMessages.Count)); - } - - public string[] Tail(uint? length) - { - int retLength; - if (length is null || length > _logMessages.Count) - retLength = _logMessages.Count; - else - retLength = (int)length; - - string[] ret = new string[retLength]; - - for (int retIndex = 0; retIndex < ret.Length; retIndex++) - { - ret[retIndex] = _logMessages.GetValueAtIndex(_logMessages.Count - retLength + retIndex).ToString(); - } - - _lastLogMessageIndex = _logMessages.Count - 1; - return ret; - } - - public string[] GetNewLines() - { - int logMessageCount = _logMessages.Count; - string[] ret = new string[logMessageCount - _lastLogMessageIndex]; - - for (int retIndex = 0; retIndex < ret.Length; retIndex++) - { - ret[retIndex] = _logMessages.GetValueAtIndex(_lastLogMessageIndex + retIndex).ToString(); - } - - _lastLogMessageIndex = logMessageCount; - return ret; - } -} \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 0ff1fd2..0000000 --- a/README.md +++ /dev/null @@ -1,190 +0,0 @@ - - - - -
-
- -

Tranga

- -

- Automatic Manga and Metadata downloader -

-
- - - - -
- Table of Contents -
    -
  1. - About The Project - -
  2. -
  3. - Screenshots -
  4. -
  5. - Getting Started - -
  6. -
  7. Roadmap
  8. -
  9. Contributing
  10. -
  11. License
  12. -
  13. Acknowledgments
  14. -
-
- - - - -## About The Project - -Tranga can download Chapters and Metadata from "Scanlation" sites such as - -- [MangaDex.org](https://mangadex.org/) -- [Manganato.com](https://manganato.com/) -- [Mangasee](https://mangasee123.com/) -- [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/). -### Inspiration: - -Because [Kaizoku](https://github.com/oae/kaizoku) was relying on [mangal](https://github.com/metafates/mangal) and mangal -hasn't received bugfixes for it's issues with Titles not showing up, or throwing errors because of illegal characters, -there were no alternatives for automatic downloads. However [Kaizoku](https://github.com/oae/kaizoku) certainly had a great Web-UI. - -That is why I wanted to create my own project, in a language I understand, and that I am able to maintain myself. - -

(back to top)

- -### Built With - -- .NET-Core -- Newtonsoft.JSON -- [PuppeteerSharp](https://www.puppeteersharp.com/) -- [Html Agility Pack (HAP)](https://html-agility-pack.net/) -- 💙 Blåhaj 🦈 - -

(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 - -There is two release types: - -- CLI -- Docker - -### CLI - -Head over to [releases](https://git.bernloehr.eu/glax/Tranga/releases) and download. The CLI will guide you through setup. - -### 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). - -### Prerequisites - -#### To Build -[.NET-Core 7.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/7.0) -#### To Run -[.NET-Core 7.0 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/7.0) scroll down a bit, should be on the right the second item. - - -## Roadmap - -- [ ] Docker ARM support -- [ ] ❓ - -See the [open issues](https://github.com/C9Glax/tranga/issues) for a full list of proposed features (and known issues). - -

(back to top)

- - - - -## Contributing - -The following is copy & pasted: - -Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. - -If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". -Don't forget to give the project a star! Thanks again! - -1. Fork the Project -2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) -3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) -4. Push to the Branch (`git push origin feature/AmazingFeature`) -5. Open a Pull Request - -

(back to top)

- - - - -## License - -Distributed under the GNU GPLv3 License. See `LICENSE.txt` for more information. - -

(back to top)

- - - - -## Acknowledgments - -* [Choose an Open Source License](https://choosealicense.com) -* [Font Awesome](https://fontawesome.com) -* [Best-README-Template](https://github.com/othneildrew/Best-README-Template/tree/master) - -

(back to top)

diff --git a/Tranga.sln b/Tranga.sln deleted file mode 100644 index 78a7590..0000000 --- a/Tranga.sln +++ /dev/null @@ -1,22 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tranga", ".\Tranga\Tranga.csproj", "{545E81B9-D96B-4C8F-A97F-2C02414DE566}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logging", "Logging\Logging.csproj", "{415BE889-BB7D-426F-976F-8D977876A462}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {545E81B9-D96B-4C8F-A97F-2C02414DE566}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {545E81B9-D96B-4C8F-A97F-2C02414DE566}.Debug|Any CPU.Build.0 = Debug|Any CPU - {545E81B9-D96B-4C8F-A97F-2C02414DE566}.Release|Any CPU.ActiveCfg = Release|Any CPU - {545E81B9-D96B-4C8F-A97F-2C02414DE566}.Release|Any CPU.Build.0 = Release|Any CPU - {415BE889-BB7D-426F-976F-8D977876A462}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {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 - EndGlobalSection -EndGlobal diff --git a/Tranga.sln.DotSettings b/Tranga.sln.DotSettings deleted file mode 100644 index 7f9a86f..0000000 --- a/Tranga.sln.DotSettings +++ /dev/null @@ -1,10 +0,0 @@ - - True - True - True - True - True - True - True - True - True \ No newline at end of file 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 deleted file mode 100644 index 0559f40..0000000 --- a/Tranga/Chapter.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System.Text.RegularExpressions; -using System.Xml.Linq; - -namespace Tranga; - -/// -/// Has to be Part of a publication -/// Includes the Chapter-Name, -VolumeNumber, -ChapterNumber, the location of the chapter on the internet and the saveName of the local file. -/// -public readonly struct Chapter -{ - // ReSharper disable once MemberCanBePrivate.Global - public Publication parentPublication { get; } - public string? name { get; } - public string? volumeNumber { get; } - public string chapterNumber { get; } - public string url { get; } - // ReSharper disable once MemberCanBePrivate.Global - public string fileName { get; } - - 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) - { - this.parentPublication = parentPublication; - this.name = name; - this.volumeNumber = volumeNumber; - this.chapterNumber = chapterNumber; - this.url = url; - - string chapterName = string.Concat(LegalCharacters.Matches(name ?? "")); - string volStr = this.volumeNumber is not null ? $"Vol.{this.volumeNumber} " : ""; - string chNumberStr = $"Ch.{chapterNumber} "; - string chNameStr = chapterName.Length > 0 ? $"- {chapterName}" : ""; - chNameStr = IllegalStrings.Replace(chNameStr, ""); - this.fileName = $"{volStr}{chNumberStr}{chNameStr}"; - } - - - /// - /// Checks if a chapter-archive is already present - /// - /// true if chapter is present - internal bool CheckChapterIsDownloaded(string downloadLocation) - { - string newFilePath = GetArchiveFilePath(downloadLocation); - if (!Directory.Exists(Path.Join(downloadLocation, parentPublication.folderName))) - return false; - FileInfo[] archives = new DirectoryInfo(Path.Join(downloadLocation, parentPublication.folderName)).GetFiles(); - Regex chapterInfoRex = new(@"Ch\.[0-9.]+"); - Regex chapterRex = new(@"[0-9]+(\.[0-9]+)?"); - - if (File.Exists(newFilePath)) - return true; - - string cn = this.chapterNumber; - if (archives.FirstOrDefault(archive => chapterRex.Match(chapterInfoRex.Match(archive.Name).Value).Value == cn) is { } path) - { - File.Move(path.FullName, newFilePath); - return true; - } - return false; - } - /// - /// Creates full file path of chapter-archive - /// - /// Filepath - internal string GetArchiveFilePath(string downloadLocation) - { - return Path.Join(downloadLocation, parentPublication.folderName, $"{parentPublication.folderName} - {this.fileName}.cbz"); - } - - /// - /// Creates a string containing XML of publication and chapter. - /// See ComicInfo.xml - /// - /// XML-string - internal string GetComicInfoXmlString() - { - XElement comicInfo = new XElement("ComicInfo", - new XElement("Tags", string.Join(',', parentPublication.tags)), - new XElement("LanguageISO", parentPublication.originalLanguage), - new XElement("Title", this.name), - new XElement("Writer", string.Join(',', parentPublication.authors)), - new XElement("Volume", this.volumeNumber), - new XElement("Number", this.chapterNumber) - ); - return comicInfo.ToString(); - } -} \ No newline at end of file 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/Connector.cs b/Tranga/Connectors/Connector.cs deleted file mode 100644 index 997aeec..0000000 --- a/Tranga/Connectors/Connector.cs +++ /dev/null @@ -1,271 +0,0 @@ -using System.Globalization; -using System.IO.Compression; -using System.Net; -using System.Runtime.InteropServices; -using System.Text.RegularExpressions; -using Tranga.TrangaTasks; -using static System.IO.UnixFileMode; - -namespace Tranga.Connectors; - -/// -/// Base-Class for all Connectors -/// Provides some methods to be used by all Connectors, as well as a DownloadClient -/// -public abstract class Connector -{ - 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 = "") - { - Publication[] ret = GetPublicationsInternal(publicationTitle); - foreach (Publication p in ret) - publicationCollection.Add(p); - return ret; - } - - /// - /// Returns all Publications with the given string. - /// If the string is empty or null, returns all Publication of the Connector - /// - /// Search-Query - /// Publications matching the query - protected abstract Publication[] GetPublicationsInternal(string publicationTitle = ""); - - /// - /// 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 - /// Language of the Chapters - /// Array of Chapters matching Publication and Language - public abstract Chapter[] GetChapters(Publication publication, string language = ""); - - /// - /// Updates the available Chapters of a Publication - /// - /// 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) - { - Chapter[] newChapters = this.GetChapters(publication, language); - collection.Add(publication); - NumberFormatInfo decimalPoint = new (){ NumberDecimalSeparator = "." }; - commonObjects.logger?.WriteLine(this.GetType().ToString(), "Checking for duplicates"); - List newChaptersList = newChapters.Where(nChapter => - float.Parse(nChapter.chapterNumber, decimalPoint) > publication.ignoreChaptersBelow && - !nChapter.CheckChapterIsDownloaded(settings.downloadLocation)).ToList(); - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"{newChaptersList.Count} new chapters."); - - return newChaptersList; - } - - public Chapter[] SelectChapters(Publication publication, string searchTerm, string? language = null) - { - Chapter[] availableChapters = this.GetChapters(publication, 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); - Regex rangeResultRegex = new("([0-9]+(-[0-9]+))", RegexOptions.IgnoreCase); - Regex allRegex = new("a(ll)?", RegexOptions.IgnoreCase); - if (volumeRegex.IsMatch(searchTerm) && chapterRegex.IsMatch(searchTerm)) - { - string volume = singleResultRegex.Match(volumeRegex.Match(searchTerm).Value).Value; - string chapter = singleResultRegex.Match(chapterRegex.Match(searchTerm).Value).Value; - return availableChapters.Where(aCh => aCh.volumeNumber is not null && - aCh.volumeNumber.Equals(volume, StringComparison.InvariantCultureIgnoreCase) && - aCh.chapterNumber.Equals(chapter, StringComparison.InvariantCultureIgnoreCase)) - .ToArray(); - } - else if (volumeRegex.IsMatch(searchTerm)) - { - string volume = volumeRegex.Match(searchTerm).Value; - if (rangeResultRegex.IsMatch(volume)) - { - string range = rangeResultRegex.Match(volume).Value; - int start = Convert.ToInt32(range.Split('-')[0]); - int end = Convert.ToInt32(range.Split('-')[1]); - return availableChapters.Where(aCh => aCh.volumeNumber is not null && - Convert.ToInt32(aCh.volumeNumber) >= start && - Convert.ToInt32(aCh.volumeNumber) <= end).ToArray(); - } - else if (singleResultRegex.IsMatch(volume)) - { - string volumeNumber = singleResultRegex.Match(volume).Value; - return availableChapters.Where(aCh => - aCh.volumeNumber is not null && - aCh.volumeNumber.Equals(volumeNumber, StringComparison.InvariantCultureIgnoreCase)).ToArray(); - } - - } - else if (chapterRegex.IsMatch(searchTerm)) - { - string chapter = chapterRegex.Match(searchTerm).Value; - if (rangeResultRegex.IsMatch(chapter)) - { - string range = rangeResultRegex.Match(chapter).Value; - int start = Convert.ToInt32(range.Split('-')[0]); - int end = Convert.ToInt32(range.Split('-')[1]); - return availableChapters.Where(aCh => Convert.ToInt32(aCh.chapterNumber) >= start && - Convert.ToInt32(aCh.chapterNumber) <= end).ToArray(); - } - else if (singleResultRegex.IsMatch(chapter)) - { - string chapterNumber = singleResultRegex.Match(chapter).Value; - return availableChapters.Where(aCh => - aCh.chapterNumber.Equals(chapterNumber, StringComparison.InvariantCultureIgnoreCase)).ToArray(); - } - } - else - { - if (rangeResultRegex.IsMatch(searchTerm)) - { - int start = Convert.ToInt32(searchTerm.Split('-')[0]); - int end = Convert.ToInt32(searchTerm.Split('-')[1]); - return availableChapters[start..(end + 1)]; - } - else if(singleResultRegex.IsMatch(searchTerm)) - return new [] { availableChapters[Convert.ToInt32(searchTerm)] }; - else if (allRegex.IsMatch(searchTerm)) - return availableChapters; - } - - 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); - - /// - /// Copies the already downloaded cover from cache to downloadLocation - /// - /// Publication to retrieve Cover for - public void CopyCoverFromCacheToDownloadLocation(Publication publication) - { - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Cloning cover {publication.sortName} -> {publication.internalId}"); - //Check if Publication already has a Folder and cover - string publicationFolder = publication.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}"); - return; - } - - string fileInCache = Path.Join(settings.coverImageCache, publication.coverFileNameInCache); - string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(fileInCache).Split('.')[^1]}" ); - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Cloning cover {fileInCache} -> {newFilePath}"); - File.Copy(fileInCache, newFilePath, true); - if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - File.SetUnixFileMode(newFilePath, GroupRead | GroupWrite | OtherRead | OtherWrite | UserRead | UserWrite); - } - - /// - /// Downloads Image from URL and saves it to the given path(incl. fileName) - /// - /// - /// - /// RequestType for Rate-Limit - /// referrer used in html request header - private HttpStatusCode DownloadImage(string imageUrl, string fullPath, byte requestType, string? referrer = null) - { - DownloadClient.RequestResult requestResult = downloadClient.MakeRequest(imageUrl, requestType, referrer); - if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.result == Stream.Null) - return requestResult.statusCode; - byte[] buffer = new byte[requestResult.result.Length]; - requestResult.result.ReadExactly(buffer, 0, buffer.Length); - File.WriteAllBytes(fullPath, buffer); - 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) - { - if (cancellationToken?.IsCancellationRequested ?? false) - return HttpStatusCode.RequestTimeout; - commonObjects.logger?.WriteLine("Connector", $"Downloading Images for {saveArchiveFilePath}"); - //Check if Publication Directory already exists - string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!; - if (!Directory.Exists(directoryPath)) - Directory.CreateDirectory(directoryPath); - - if (File.Exists(saveArchiveFilePath)) //Don't download twice. - return HttpStatusCode.OK; - - //Create a temporary folder to store images - string tempFolder = Directory.CreateTempSubdirectory().FullName; - - int chapter = 0; - //Download all Images to temporary Folder - foreach (string imageUrl in imageUrls) - { - 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}"); - HttpStatusCode status = DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), requestType, referrer); - if ((int)status < 200 || (int)status >= 300) - return status; - parentTask.IncrementProgress(1.0 / imageUrls.Length); - if (cancellationToken?.IsCancellationRequested ?? false) - return HttpStatusCode.RequestTimeout; - } - - if(comicInfoPath is not null) - File.Copy(comicInfoPath, Path.Join(tempFolder, "ComicInfo.xml")); - - commonObjects.logger?.WriteLine("Connector", $"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 - return HttpStatusCode.OK; - } - - protected string SaveCoverImageToCache(string url, byte requestType) - { - string[] split = url.Split('/'); - string filename = split[^1]; - string saveImagePath = Path.Join(settings.coverImageCache, filename); - - if (File.Exists(saveImagePath)) - return filename; - - DownloadClient.RequestResult coverResult = downloadClient.MakeRequest(url, requestType); - using MemoryStream ms = new(); - coverResult.result.CopyTo(ms); - File.WriteAllBytes(saveImagePath, ms.ToArray()); - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Saving image to {saveImagePath}"); - return filename; - } -} \ No newline at end of file diff --git a/Tranga/Connectors/MangaDex.cs b/Tranga/Connectors/MangaDex.cs deleted file mode 100644 index b63f7a3..0000000 --- a/Tranga/Connectors/MangaDex.cs +++ /dev/null @@ -1,283 +0,0 @@ -using System.Globalization; -using System.Net; -using System.Text.Json; -using System.Text.Json.Nodes; -using Tranga.TrangaTasks; - -namespace Tranga.Connectors; -public class MangaDex : Connector -{ - public override string name { get; } - - private enum RequestType : byte - { - Manga, - Feed, - AtHomeServer, - CoverUrl, - Author, - } - - public MangaDex(TrangaSettings settings, CommonObjects commonObjects) : base(settings, commonObjects) - { - name = "MangaDex"; - this.downloadClient = new DownloadClient(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 = "") - { - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Publications (title={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(); - int loadedPublicationData = 0; - while (offset < total) //As long as we haven't requested all "Pages" - { - //Request next Page - DownloadClient.RequestResult requestResult = - downloadClient.MakeRequest( - $"https://api.mangadex.org/manga?limit={limit}&title={publicationTitle}&offset={offset}", (byte)RequestType.Manga); - if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) - break; - JsonObject? result = JsonSerializer.Deserialize(requestResult.result); - - offset += limit; - if (result is null) - break; - - total = result["total"]!.GetValue(); //Update the total number of Publications - - JsonArray mangaInResult = result["data"]!.AsArray(); //Manga-data-Array - //Loop each Manga and extract information from JSON - foreach (JsonNode? mangeNode in mangaInResult) - { - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting publication data. {++loadedPublicationData}/{total}"); - JsonObject manga = (JsonObject)mangeNode!; - JsonObject attributes = manga["attributes"]!.AsObject(); - - string publicationId = manga["id"]!.GetValue(); - - 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 - ? 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 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); - - 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 - } - } - - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Done getting publications (title={publicationTitle})"); - return publications.ToArray(); - } - - public override Chapter[] GetChapters(Publication publication, string language = "") - { - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Chapters for {publication.sortName} {publication.internalId} (language={language})"); - 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 - List chapters = new(); - //As long as we haven't requested all "Pages" - while (offset < total) - { - //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); - if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) - break; - JsonObject? result = JsonSerializer.Deserialize(requestResult.result); - - offset += limit; - if (result is null) - break; - - total = result["total"]!.GetValue(); - JsonArray chaptersInResult = result["data"]!.AsArray(); - //Loop through all Chapters in result and extract information from JSON - foreach (JsonNode? jsonNode in chaptersInResult) - { - JsonObject chapter = (JsonObject)jsonNode!; - JsonObject attributes = chapter["attributes"]!.AsObject(); - string chapterId = chapter["id"]!.GetValue(); - - string? title = attributes.ContainsKey("title") && attributes["title"] is not null - ? attributes["title"]!.GetValue() - : null; - - string? volume = attributes.ContainsKey("volume") && attributes["volume"] is not null - ? attributes["volume"]!.GetValue() - : null; - - string chapterNum = attributes.ContainsKey("chapter") && attributes["chapter"] is not null - ? attributes["chapter"]!.GetValue() - : "null"; - - if(chapterNum is not "null") - chapters.Add(new Chapter(publication, 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(); - } - - public override HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null) - { - if (cancellationToken?.IsCancellationRequested ?? false) - return HttpStatusCode.RequestTimeout; - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}"); - //Request URLs for Chapter-Images - DownloadClient.RequestResult requestResult = - downloadClient.MakeRequest($"https://api.mangadex.org/at-home/server/{chapter.url}?forcePort443=false'", (byte)RequestType.AtHomeServer); - if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) - return requestResult.statusCode; - JsonObject? result = JsonSerializer.Deserialize(requestResult.result); - if (result is null) - return HttpStatusCode.NoContent; - - string baseUrl = result["baseUrl"]!.GetValue(); - string hash = result["chapter"]!["hash"]!.GetValue(); - JsonArray imageFileNames = result["chapter"]!["data"]!.AsArray(); - //Loop through all imageNames and construct urls (imageUrl) - HashSet imageUrls = new(); - foreach (JsonNode? image in imageFileNames) - imageUrls.Add($"{baseUrl}/data/{hash}/{image!.GetValue()}"); - - string comicInfoPath = Path.GetTempFileName(); - File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString()); - - //Download Chapter-Images - return DownloadChapterImages(imageUrls.ToArray(), chapter.GetArchiveFilePath(settings.downloadLocation), (byte)RequestType.AtHomeServer, parentTask, comicInfoPath, cancellationToken:cancellationToken); - } - - private string? GetCoverUrl(string publicationId, string? posterId) - { - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting CoverUrl for {publicationId}"); - if (posterId is null) - { - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"No posterId, aborting"); - return null; - } - - //Request information where to download Cover - DownloadClient.RequestResult requestResult = - downloadClient.MakeRequest($"https://api.mangadex.org/cover/{posterId}", (byte)RequestType.CoverUrl); - if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) - return null; - JsonObject? result = JsonSerializer.Deserialize(requestResult.result); - if (result is null) - return null; - - 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}"); - return coverUrl; - } - - private List GetAuthors(IEnumerable authorIds) - { - List ret = new(); - foreach (string authorId in authorIds) - { - DownloadClient.RequestResult requestResult = - downloadClient.MakeRequest($"https://api.mangadex.org/author/{authorId}", (byte)RequestType.Author); - if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) - return ret; - JsonObject? result = JsonSerializer.Deserialize(requestResult.result); - if (result is null) - return ret; - - string authorName = result["data"]!["attributes"]!["name"]!.GetValue(); - ret.Add(authorName); - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Got author {authorId} -> {authorName}"); - } - return ret; - } -} \ No newline at end of file diff --git a/Tranga/Connectors/MangaKatana.cs b/Tranga/Connectors/MangaKatana.cs deleted file mode 100644 index 6205d38..0000000 --- a/Tranga/Connectors/MangaKatana.cs +++ /dev/null @@ -1,221 +0,0 @@ -using System.Globalization; -using System.Net; -using System.Text.RegularExpressions; -using HtmlAgilityPack; -using Tranga.TrangaTasks; - -namespace Tranga.Connectors; - -public class MangaKatana : Connector -{ - public override string name { get; } - - public MangaKatana(TrangaSettings settings, CommonObjects commonObjects) : base(settings, commonObjects) - { - this.name = "MangaKatana"; - this.downloadClient = new DownloadClient(new Dictionary() - { - {1, 60} - }, commonObjects.logger); - } - - protected override Publication[] GetPublicationsInternal(string publicationTitle = "") - { - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Publications (title={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(); - - // ReSharper disable once MergeIntoPattern - // If a single result is found, the user will be redirected to the results directly instead of a result page - if(requestResult.hasBeenRedirected - && requestResult.redirectedToUrl is not null - && requestResult.redirectedToUrl.Contains("mangakatana.com/manga")) - { - return new [] { ParseSinglePublicationFromHtml(requestResult.result, requestResult.redirectedToUrl.Split('/')[^1]) }; - } - - return ParsePublicationsFromHtml(requestResult.result); - } - - private Publication[] ParsePublicationsFromHtml(Stream html) - { - StreamReader reader = new(html); - string htmlString = reader.ReadToEnd(); - HtmlDocument document = new(); - document.LoadHtml(htmlString); - IEnumerable searchResults = document.DocumentNode.SelectNodes("//*[@id='book_list']/div"); - if (searchResults is null || !searchResults.Any()) - return Array.Empty(); - List urls = new(); - foreach (HtmlNode mangaResult in searchResults) - { - urls.Add(mangaResult.Descendants("a").First().GetAttributes() - .First(a => a.Name == "href").Value); - } - - 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])); - } - - return ret.ToArray(); - } - - private Publication ParseSinglePublicationFromHtml(Stream html, string publicationId) - { - StreamReader reader = new(html); - string htmlString = reader.ReadToEnd(); - HtmlDocument document = new(); - document.LoadHtml(htmlString); - string status = ""; - Dictionary altTitles = new(); - Dictionary? links = null; - HashSet tags = new(); - string[] authors = Array.Empty(); - string originalLanguage = ""; - - HtmlNode infoNode = document.DocumentNode.SelectSingleNode("//*[@id='single_book']"); - string sortName = infoNode.Descendants("h1").First(n => n.HasClass("heading")).InnerText; - HtmlNode infoTable = infoNode.SelectSingleNode("//*[@id='single_book']/div[2]/div/ul"); - - foreach (HtmlNode row in infoTable.Descendants("li")) - { - string key = row.SelectNodes("div").First().InnerText.ToLower(); - string value = row.SelectNodes("div").Last().InnerText; - string keySanitized = string.Concat(Regex.Matches(key, "[a-z]")); - - switch (keySanitized) - { - case "altnames": - string[] alts = value.Split(" ; "); - for (int i = 0; i < alts.Length; i++) - altTitles.Add(i.ToString(), alts[i]); - break; - case "authorsartists": - authors = value.Split(','); - break; - case "status": - status = value; - break; - case "genres": - tags = row.SelectNodes("div").Last().Descendants("a").Select(a => a.InnerText).ToHashSet(); - break; - } - } - - string posterUrl = document.DocumentNode.SelectSingleNode("//*[@id='single_book']/div[1]/div").Descendants("img").First() - .GetAttributes().First(a => a.Name == "src").Value; - - string coverFileNameInCache = SaveCoverImageToCache(posterUrl, 1); - - string description = document.DocumentNode.SelectSingleNode("//*[@id='single_book']/div[3]/p").InnerText; - while (description.StartsWith('\n')) - description = description.Substring(1); - - int year = DateTime.Now.Year; - string yearString = infoTable.Descendants("div").First(d => d.HasClass("updateAt")) - .InnerText.Split('-')[^1]; - - if(yearString.Contains("ago") == false) - { - year = Convert.ToInt32(yearString); - } - - return new Publication(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links, - year, originalLanguage, status, publicationId); - } - - public override Chapter[] GetChapters(Publication publication, string language = "") - { - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Chapters for {publication.sortName} {publication.internalId} (language={language})"); - string requestUrl = $"https://mangakatana.com/manga/{publication.publicationId}"; - // Leaving this in for verification if the page exists - 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, requestUrl); - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Done getting Chapters for {publication.internalId}"); - return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray(); - } - - private List ParseChaptersFromHtml(Publication publication, string mangaUrl) - { - // Using HtmlWeb will include the chapters since they are loaded with js - HtmlWeb web = new(); - HtmlDocument document = web.Load(mangaUrl); - - List ret = new(); - - HtmlNode chapterList = document.DocumentNode.SelectSingleNode("//div[contains(@class, 'chapters')]/table/tbody"); - - foreach (HtmlNode chapterInfo in chapterList.Descendants("tr")) - { - string fullString = chapterInfo.Descendants("a").First().InnerText; - - string? volumeNumber = fullString.Contains("Vol.") ? fullString.Replace("Vol.", "").Split(' ')[0] : null; - string chapterNumber = fullString.Split(':')[0].Split("Chapter ")[1].Split(" ")[0].Replace('-', '.'); - string chapterName = string.Concat(fullString.Split(':')[1..]); - string url = chapterInfo.Descendants("a").First() - .GetAttributeValue("href", ""); - ret.Add(new Chapter(publication, chapterName, volumeNumber, chapterNumber, url)); - } - - return ret; - } - - public override HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null) - { - if (cancellationToken?.IsCancellationRequested ?? false) - return HttpStatusCode.RequestTimeout; - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}"); - string requestUrl = chapter.url; - // Leaving this in to check if the page exists - DownloadClient.RequestResult requestResult = - downloadClient.MakeRequest(requestUrl, 1); - if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) - return requestResult.statusCode; - - string[] imageUrls = ParseImageUrlsFromHtml(requestUrl); - - string comicInfoPath = Path.GetTempFileName(); - File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString()); - - return DownloadChapterImages(imageUrls, chapter.GetArchiveFilePath(settings.downloadLocation), 1, parentTask, comicInfoPath, "https://mangakatana.com/", cancellationToken); - } - - private string[] ParseImageUrlsFromHtml(string mangaUrl) - { - HtmlWeb web = new(); - HtmlDocument document = web.Load(mangaUrl); - - // Images are loaded dynamically, but the urls are present in a piece of js code on the page - string js = document.DocumentNode.SelectSingleNode("//script[contains(., 'data-src')]").InnerText - .Replace("\r", "") - .Replace("\n", "") - .Replace("\t", ""); - - // ReSharper disable once StringLiteralTypo - string regexPat = @"(var thzq=\[')(.*)(,];function)"; - var group = Regex.Matches(js, regexPat).First().Groups[2].Value.Replace("'", ""); - var urls = group.Split(','); - - return urls; - } -} \ No newline at end of file diff --git a/Tranga/Connectors/Manganato.cs b/Tranga/Connectors/Manganato.cs deleted file mode 100644 index cfc9791..0000000 --- a/Tranga/Connectors/Manganato.cs +++ /dev/null @@ -1,204 +0,0 @@ -using System.Globalization; -using System.Net; -using System.Text.RegularExpressions; -using HtmlAgilityPack; -using Tranga.TrangaTasks; - -namespace Tranga.Connectors; - -public class Manganato : Connector -{ - public override string name { get; } - - public Manganato(TrangaSettings settings, CommonObjects commonObjects) : base(settings, commonObjects) - { - this.name = "Manganato"; - this.downloadClient = new DownloadClient(new Dictionary() - { - {1, 60} - }, commonObjects.logger); - } - - protected override Publication[] GetPublicationsInternal(string publicationTitle = "") - { - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Publications (title={publicationTitle})"); - string sanitizedTitle = string.Join('_', Regex.Matches(publicationTitle, "[A-z]*")).ToLower(); - string requestUrl = $"https://manganato.com/search/story/{sanitizedTitle}"; - DownloadClient.RequestResult requestResult = - downloadClient.MakeRequest(requestUrl, 1); - if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) - return Array.Empty(); - - return ParsePublicationsFromHtml(requestResult.result); - } - - private Publication[] ParsePublicationsFromHtml(Stream html) - { - 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) - { - urls.Add(mangaResult.Descendants("a").First(n => n.HasClass("item-title")).GetAttributes() - .First(a => a.Name == "href").Value); - } - - 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])); - } - - return ret.ToArray(); - } - - private Publication ParseSinglePublicationFromHtml(Stream html, string publicationId) - { - StreamReader reader = new (html); - string htmlString = reader.ReadToEnd(); - HtmlDocument document = new (); - document.LoadHtml(htmlString); - 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("story-info-right")); - - string sortName = infoNode.Descendants("h1").First().InnerText; - - HtmlNode infoTable = infoNode.Descendants().First(d => d.Name == "table"); - - foreach (HtmlNode row in infoTable.Descendants("tr")) - { - string key = row.SelectNodes("td").First().InnerText.ToLower(); - string value = row.SelectNodes("td").Last().InnerText; - string keySanitized = string.Concat(Regex.Matches(key, "[a-z]")); - - switch (keySanitized) - { - case "alternative": - string[] alts = value.Split(" ; "); - for(int i = 0; i < alts.Length; i++) - altTitles.Add(i.ToString(), alts[i]); - break; - case "authors": - authors = value.Split('-'); - break; - case "status": - status = value; - break; - case "genres": - string[] genres = value.Split(" - "); - tags = genres.ToHashSet(); - break; - } - } - - string posterUrl = document.DocumentNode.Descendants("span").First(s => s.HasClass("info-image")).Descendants("img").First() - .GetAttributes().First(a => a.Name == "src").Value; - - string coverFileNameInCache = SaveCoverImageToCache(posterUrl, 1); - - string description = document.DocumentNode.Descendants("div").First(d => d.HasClass("panel-story-info-description")) - .InnerText.Replace("Description :", ""); - while (description.StartsWith('\n')) - description = description.Substring(1); - - string yearString = document.DocumentNode.Descendants("li").Last(li => li.HasClass("a-h")).Descendants("span") - .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, - year, originalLanguage, status, publicationId); - } - - public override Chapter[] GetChapters(Publication publication, string language = "") - { - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Chapters for {publication.sortName} {publication.internalId} (language={language})"); - string requestUrl = $"https://chapmanganato.com/{publication.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(); - } - - private List ParseChaptersFromHtml(Publication publication, Stream html) - { - 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")); - - 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 url = chapterInfo.Descendants("a").First(d => d.HasClass("chapter-name")) - .GetAttributeValue("href", ""); - ret.Add(new Chapter(publication, chapterName, volumeNumber, chapterNumber, url)); - } - ret.Reverse(); - return ret; - } - - public override HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null) - { - if (cancellationToken?.IsCancellationRequested ?? false) - return HttpStatusCode.RequestTimeout; - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}"); - 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); - - string comicInfoPath = Path.GetTempFileName(); - File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString()); - - return DownloadChapterImages(imageUrls, chapter.GetArchiveFilePath(settings.downloadLocation), 1, parentTask, comicInfoPath, "https://chapmanganato.com/", cancellationToken); - } - - private string[] ParseImageUrlsFromHtml(Stream html) - { - StreamReader reader = new (html); - string htmlString = reader.ReadToEnd(); - HtmlDocument document = new (); - document.LoadHtml(htmlString); - List ret = new(); - - HtmlNode imageContainer = - document.DocumentNode.Descendants("div").First(i => i.HasClass("container-chapter-reader")); - foreach(HtmlNode imageNode in imageContainer.Descendants("img")) - ret.Add(imageNode.GetAttributeValue("src", "")); - - return ret.ToArray(); - } -} \ No newline at end of file diff --git a/Tranga/Connectors/Mangasee.cs b/Tranga/Connectors/Mangasee.cs deleted file mode 100644 index ea9de48..0000000 --- a/Tranga/Connectors/Mangasee.cs +++ /dev/null @@ -1,275 +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()); - - return DownloadChapterImages(urls.ToArray(), chapter.GetArchiveFilePath(settings.downloadLocation), 1, parentTask, comicInfoPath, cancellationToken:cancellationToken); - } - 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/LibraryManagers/Kavita.cs b/Tranga/LibraryManagers/Kavita.cs deleted file mode 100644 index 04cbe67..0000000 --- a/Tranga/LibraryManagers/Kavita.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Text.Json.Nodes; -using Logging; -using Newtonsoft.Json; -using JsonSerializer = System.Text.Json.JsonSerializer; - -namespace Tranga.LibraryManagers; - -public class Kavita : LibraryManager -{ - - public Kavita(string baseUrl, string username, string password, Logger? logger) : base(baseUrl, GetToken(baseUrl, username, password), logger, LibraryType.Kavita) - { - } - - [JsonConstructor] - public Kavita(string baseUrl, string auth, Logger? logger) : base(baseUrl, auth, logger, LibraryType.Kavita) - { - } - - private static string GetToken(string baseUrl, string username, string password) - { - HttpClient client = new() - { - DefaultRequestHeaders = - { - { "Accept", "application/json" } - } - }; - HttpRequestMessage requestMessage = new () - { - Method = HttpMethod.Post, - RequestUri = new Uri($"{baseUrl}/api/Account/login"), - Content = new StringContent($"{{\"username\":\"{username}\",\"password\":\"{password}\"}}", System.Text.Encoding.UTF8, "application/json") - }; - - HttpResponseMessage response = client.Send(requestMessage); - JsonObject? result = JsonSerializer.Deserialize(response.Content.ReadAsStream()); - if (result is not null) - return result["token"]!.GetValue(); - else return ""; - } - - public override void UpdateLibrary() - { - logger?.WriteLine(this.GetType().ToString(), $"Updating Libraries"); - foreach (KavitaLibrary lib in GetLibraries()) - NetClient.MakePost($"{baseUrl}/api/Library/scan?libraryId={lib.id}", "Bearer", auth, logger); - } - - /// - /// Fetches all libraries available to the user - /// - /// Array of KavitaLibrary - private IEnumerable GetLibraries() - { - logger?.WriteLine(this.GetType().ToString(), $"Getting Libraries"); - Stream data = NetClient.MakeRequest($"{baseUrl}/api/Library", "Bearer", auth, logger); - if (data == Stream.Null) - { - logger?.WriteLine(this.GetType().ToString(), $"No libraries returned"); - return Array.Empty(); - } - JsonArray? result = JsonSerializer.Deserialize(data); - if (result is null) - { - logger?.WriteLine(this.GetType().ToString(), $"No libraries returned"); - return Array.Empty(); - } - - HashSet ret = new(); - - foreach (JsonNode? jsonNode in result) - { - var jObject = (JsonObject?)jsonNode; - int libraryId = jObject!["id"]!.GetValue(); - string libraryName = jObject["name"]!.GetValue(); - ret.Add(new KavitaLibrary(libraryId, libraryName)); - } - - return ret; - } - - private struct KavitaLibrary - { - public int id { get; } - // ReSharper disable once UnusedAutoPropertyAccessor.Local - public string name { get; } - - public KavitaLibrary(int id, string name) - { - this.id = id; - this.name = name; - } - } -} \ No newline at end of file diff --git a/Tranga/LibraryManagers/Komga.cs b/Tranga/LibraryManagers/Komga.cs deleted file mode 100644 index 545ec91..0000000 --- a/Tranga/LibraryManagers/Komga.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.Text.Json.Nodes; -using Logging; -using Newtonsoft.Json; -using JsonSerializer = System.Text.Json.JsonSerializer; - -namespace Tranga.LibraryManagers; - -/// -/// Provides connectivity to Komga-API -/// Can fetch and update libraries -/// -public class Komga : LibraryManager -{ - public Komga(string baseUrl, string username, string password, Logger? logger) - : base(baseUrl, Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}")), logger, LibraryType.Komga) - { - } - - [JsonConstructor] - public Komga(string baseUrl, string auth, Logger? logger) : base(baseUrl, auth, logger, LibraryType.Komga) - { - } - - public override void UpdateLibrary() - { - logger?.WriteLine(this.GetType().ToString(), $"Updating Libraries"); - foreach (KomgaLibrary lib in GetLibraries()) - NetClient.MakePost($"{baseUrl}/api/v1/libraries/{lib.id}/scan", "Basic", auth, logger); - } - - /// - /// Fetches all libraries available to the user - /// - /// Array of KomgaLibraries - private IEnumerable GetLibraries() - { - logger?.WriteLine(this.GetType().ToString(), $"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"); - return Array.Empty(); - } - JsonArray? result = JsonSerializer.Deserialize(data); - if (result is null) - { - logger?.WriteLine(this.GetType().ToString(), $"No libraries returned"); - return Array.Empty(); - } - - HashSet ret = new(); - - foreach (JsonNode? jsonNode in result) - { - var jObject = (JsonObject?)jsonNode; - string libraryId = jObject!["id"]!.GetValue(); - string libraryName = jObject["name"]!.GetValue(); - ret.Add(new KomgaLibrary(libraryId, libraryName)); - } - - return ret; - } - - private struct KomgaLibrary - { - public string id { get; } - // ReSharper disable once UnusedAutoPropertyAccessor.Local - public string name { get; } - - public KomgaLibrary(string id, string name) - { - this.id = id; - this.name = name; - } - } -} \ No newline at end of file diff --git a/Tranga/LibraryManagers/LibraryManager.cs b/Tranga/LibraryManagers/LibraryManager.cs deleted file mode 100644 index 88e7b98..0000000 --- a/Tranga/LibraryManagers/LibraryManager.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System.Net; -using System.Net.Http.Headers; -using Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Tranga.LibraryManagers; - -public abstract class LibraryManager -{ - public enum LibraryType : byte - { - Komga = 0, - Kavita = 1 - } - - // ReSharper disable once UnusedAutoPropertyAccessor.Global - public LibraryType libraryType { get; } - 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) - { - 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) - { - HttpClient client = new(); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authScheme, auth); - - HttpRequestMessage requestMessage = new () - { - Method = HttpMethod.Get, - RequestUri = new Uri(url) - }; - HttpResponseMessage response = client.Send(requestMessage); - logger?.WriteLine("LibraryManager", $"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); - else if (response.IsSuccessStatusCode) - return response.Content.ReadAsStream(); - else - return Stream.Null; - } - - public static bool MakePost(string url, string authScheme, string auth, Logger? logger) - { - HttpClient client = new() - { - DefaultRequestHeaders = - { - { "Accept", "application/json" }, - { "Authorization", new AuthenticationHeaderValue(authScheme, auth).ToString() } - } - }; - HttpRequestMessage requestMessage = new () - { - Method = HttpMethod.Post, - RequestUri = new Uri(url) - }; - HttpResponseMessage response = client.Send(requestMessage); - logger?.WriteLine("LibraryManager", $"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); - else if (response.IsSuccessStatusCode) - return true; - else - 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/Migrator.cs b/Tranga/Migrator.cs deleted file mode 100644 index 0ac0e91..0000000 --- a/Tranga/Migrator.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System.Text.Json.Nodes; -using Logging; -using Newtonsoft.Json; -using Tranga.LibraryManagers; -using Tranga.NotificationManagers; -using Tranga.TrangaTasks; - -namespace Tranga; - -public static class Migrator -{ - internal static readonly ushort CurrentVersion = 17; - public static void Migrate(string settingsFilePath, Logger? logger) - { - if (!File.Exists(settingsFilePath)) - return; - JsonNode settingsNode = JsonNode.Parse(File.ReadAllText(settingsFilePath))!; - ushort version = settingsNode["version"] is not null - ? settingsNode["version"]!.GetValue() - : settingsNode["ts"]!["version"]!.GetValue(); - logger?.WriteLine("Migrator", $"Migrating {version} -> {CurrentVersion}"); - switch (version) - { - case 15: - MoveToCommonObjects(settingsFilePath, logger); - TrangaSettings.SettingsJsonObject sjo = JsonConvert.DeserializeObject(File.ReadAllText(settingsFilePath))!; - RemoveUpdateLibraryTask(sjo.ts!, logger); - break; - case 16: - MoveToCommonObjects(settingsFilePath, logger); - break; - } - - TrangaSettings.SettingsJsonObject sjo2 = JsonConvert.DeserializeObject( - File.ReadAllText(settingsFilePath), - new JsonSerializerSettings - { - Converters = - { - new TrangaTask.TrangaTaskJsonConverter(), - new NotificationManager.NotificationManagerJsonConverter(), - new LibraryManager.LibraryManagerJsonConverter() - } - })!; - sjo2.ts!.version = CurrentVersion; - sjo2.ts!.ExportSettings(); - } - - private static void RemoveUpdateLibraryTask(TrangaSettings settings, Logger? logger) - { - if (!File.Exists(settings.tasksFilePath)) - return; - - logger?.WriteLine("Migrator", "Removing old/deprecated UpdateLibraryTasks (v16)"); - string tasksJsonString = File.ReadAllText(settings.tasksFilePath); - HashSet tasks = JsonConvert.DeserializeObject>(tasksJsonString, - new JsonSerializerSettings { Converters = { new TrangaTask.TrangaTaskJsonConverter() } })!; - tasks.RemoveWhere(t => t.task == TrangaTask.Task.UpdateLibraries); - File.WriteAllText(settings.tasksFilePath, JsonConvert.SerializeObject(tasks)); - } - - public static void MoveToCommonObjects(string settingsFilePath, Logger? logger) - { - if (!File.Exists(settingsFilePath)) - return; - - logger?.WriteLine("Migrator", "Moving Settings to commonObjects-structure (v17)"); - JsonNode node = JsonNode.Parse(File.ReadAllText(settingsFilePath))!; - TrangaSettings ts = new( - node["downloadLocation"]!.GetValue(), - node["workingDirectory"]!.GetValue()); - JsonArray libraryManagers = node["libraryManagers"]!.AsArray(); - logger?.WriteLine("Migrator", $"\tGot {libraryManagers.Count} libraryManagers."); - JsonNode? komgaNode = libraryManagers.FirstOrDefault(lm => lm["libraryType"].GetValue() == (byte)LibraryManager.LibraryType.Komga); - JsonNode? kavitaNode = libraryManagers.FirstOrDefault(lm => lm["libraryType"].GetValue() == (byte)LibraryManager.LibraryType.Kavita); - HashSet lms = new(); - if (komgaNode is not null) - lms.Add(new Komga(komgaNode["baseUrl"]!.GetValue(), komgaNode["auth"]!.GetValue(), null)); - if (kavitaNode is not null) - lms.Add(new Kavita(kavitaNode["baseUrl"]!.GetValue(), kavitaNode["auth"]!.GetValue(), null)); - - JsonArray notificationManagers = node["notificationManagers"]!.AsArray(); - logger?.WriteLine("Migrator", $"\tGot {notificationManagers.Count} notificationManagers."); - JsonNode? gotifyNode = notificationManagers.FirstOrDefault(nm => - nm["notificationManagerType"].GetValue() == (byte)NotificationManager.NotificationManagerType.Gotify); - JsonNode? lunaSeaNode = notificationManagers.FirstOrDefault(nm => - nm["notificationManagerType"].GetValue() == (byte)NotificationManager.NotificationManagerType.LunaSea); - HashSet nms = new(); - if (gotifyNode is not null) - nms.Add(new Gotify(gotifyNode["endpoint"]!.GetValue(), gotifyNode["appToken"]!.GetValue())); - if (lunaSeaNode is not null) - nms.Add(new LunaSea(lunaSeaNode["id"]!.GetValue())); - - CommonObjects co = new (lms, nms, logger, settingsFilePath); - - TrangaSettings.SettingsJsonObject sjo = new(ts, co); - File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(sjo)); - } -} \ No newline at end of file diff --git a/Tranga/NotificationManagers/Gotify.cs b/Tranga/NotificationManagers/Gotify.cs deleted file mode 100644 index a823665..0000000 --- a/Tranga/NotificationManagers/Gotify.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Text; -using Logging; -using Newtonsoft.Json; - -namespace Tranga.NotificationManagers; - -public class Gotify : NotificationManager -{ - public string endpoint { get; } - // ReSharper disable once MemberCanBePrivate.Global - public string appToken { get; } - private readonly HttpClient _client = new(); - - [JsonConstructor] - public Gotify(string endpoint, string appToken, Logger? logger = null) : base(NotificationManagerType.Gotify, logger) - { - this.endpoint = endpoint; - this.appToken = appToken; - } - - public override void SendNotification(string title, string notificationText) - { - logger?.WriteLine(this.GetType().ToString(), $"Sending notification: {title} - {notificationText}"); - MessageData message = new(title, notificationText); - HttpRequestMessage request = new(HttpMethod.Post, $"{endpoint}/message"); - request.Headers.Add("X-Gotify-Key", this.appToken); - request.Content = new StringContent(JsonConvert.SerializeObject(message, Formatting.None), Encoding.UTF8, "application/json"); - HttpResponseMessage response = _client.Send(request); - if (!response.IsSuccessStatusCode) - { - StreamReader sr = new (response.Content.ReadAsStream()); - logger?.WriteLine(this.GetType().ToString(), $"{response.StatusCode}: {sr.ReadToEnd()}"); - } - } - - private class MessageData - { - // ReSharper disable four times UnusedAutoPropertyAccessor.Local - public string message { get; } - public long priority { get; } - public string title { get; } - public Dictionary extras { get; } - - public MessageData(string title, string message) - { - this.title = title; - this.message = message; - this.extras = new(); - this.priority = 4; - } - } -} \ No newline at end of file diff --git a/Tranga/NotificationManagers/LunaSea.cs b/Tranga/NotificationManagers/LunaSea.cs deleted file mode 100644 index aef14c5..0000000 --- a/Tranga/NotificationManagers/LunaSea.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Text; -using Logging; -using Newtonsoft.Json; - -namespace Tranga.NotificationManagers; - -public class LunaSea : NotificationManager -{ - // ReSharper disable once MemberCanBePrivate.Global - public string id { get; init; } - private readonly HttpClient _client = new(); - - [JsonConstructor] - public LunaSea(string id, Logger? logger = null) : base(NotificationManagerType.LunaSea, logger) - { - this.id = id; - } - - public override void SendNotification(string title, string notificationText) - { - logger?.WriteLine(this.GetType().ToString(), $"Sending notification: {title} - {notificationText}"); - MessageData message = new(title, notificationText); - HttpRequestMessage request = new(HttpMethod.Post, $"https://notify.lunasea.app/v1/custom/{id}"); - request.Content = new StringContent(JsonConvert.SerializeObject(message, Formatting.None), Encoding.UTF8, "application/json"); - HttpResponseMessage response = _client.Send(request); - if (!response.IsSuccessStatusCode) - { - StreamReader sr = new (response.Content.ReadAsStream()); - logger?.WriteLine(this.GetType().ToString(), $"{response.StatusCode}: {sr.ReadToEnd()}"); - } - } - - private class MessageData - { - // ReSharper disable twice UnusedAutoPropertyAccessor.Local - public string title { get; } - public string body { get; } - - public MessageData(string title, string body) - { - this.title = title; - this.body = body; - } - } -} \ No newline at end of file diff --git a/Tranga/NotificationManagers/NotificationManager.cs b/Tranga/NotificationManagers/NotificationManager.cs deleted file mode 100644 index 7c65063..0000000 --- a/Tranga/NotificationManagers/NotificationManager.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Tranga.NotificationManagers; - -public abstract class NotificationManager -{ - protected Logger? logger; - public NotificationManagerType notificationManagerType; - - protected NotificationManager(NotificationManagerType notificationManagerType, Logger? logger = null) - { - this.notificationManagerType = notificationManagerType; - this.logger = logger; - } - - public enum NotificationManagerType : byte { Gotify = 0, LunaSea = 1 } - - public abstract void SendNotification(string title, string notificationText); - - public void AddLogger(Logger pLogger) - { - this.logger = pLogger; - } - - public class NotificationManagerJsonConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - { - return (objectType == typeof(NotificationManager)); - } - - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, - JsonSerializer serializer) - { - JObject jo = JObject.Load(reader); - if (jo["notificationManagerType"]!.Value() == (byte)NotificationManagerType.Gotify) - return jo.ToObject(serializer)!; - else if (jo["notificationManagerType"]!.Value() == (byte)NotificationManagerType.LunaSea) - return jo.ToObject(serializer)!; - - throw new Exception(); - } - - public override bool CanWrite => false; - - /// - /// Don't call this - /// - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - throw new Exception("Dont call this"); - } - } -} \ No newline at end of file diff --git a/Tranga/Publication.cs b/Tranga/Publication.cs deleted file mode 100644 index 330e7c0..0000000 --- a/Tranga/Publication.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System.Runtime.InteropServices; -using System.Text; -using System.Text.RegularExpressions; -using Newtonsoft.Json; -using static System.IO.UnixFileMode; - -namespace Tranga; - -/// -/// Contains information on a Publication (Manga) -/// -public struct Publication -{ - public string sortName { get; } - public List authors { get; } - // ReSharper disable once UnusedAutoPropertyAccessor.Global - public Dictionary altTitles { get; } - // ReSharper disable once MemberCanBePrivate.Global - public string? description { get; } - public string[] tags { get; } - // ReSharper disable once UnusedAutoPropertyAccessor.Global - public string? posterUrl { get; } - public string? coverFileNameInCache { get; } - // ReSharper disable once UnusedAutoPropertyAccessor.Global - public Dictionary links { get; } - // ReSharper disable once MemberCanBePrivate.Global - public int? year { get; } - public string? originalLanguage { get; } - // ReSharper disable once MemberCanBePrivate.Global - public string status { get; } - public string folderName { get; } - 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]* *\.*-*,*'*\'*\)*\(*~*!*"); - - [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) - { - this.sortName = sortName; - this.authors = authors; - this.description = description; - this.altTitles = altTitles; - this.tags = tags; - this.coverFileNameInCache = coverFileNameInCache; - this.posterUrl = posterUrl; - this.links = links ?? new Dictionary(); - this.year = year; - this.originalLanguage = originalLanguage; - this.status = status; - this.publicationId = publicationId; - this.folderName = folderName ?? string.Concat(LegalCharacters.Matches(sortName)); - while (this.folderName.EndsWith('.')) - this.folderName = this.folderName.Substring(0, this.folderName.Length - 1); - string onlyLowerLetters = string.Concat(this.sortName.ToLower().Where(Char.IsLetter)); - this.internalId = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{onlyLowerLetters}{this.year}")); - this.ignoreChaptersBelow = ignoreChaptersBelow ?? 0f; - } - - public string CreatePublicationFolder(string downloadDirectory) - { - string publicationFolder = Path.Join(downloadDirectory, this.folderName); - if(!Directory.Exists(publicationFolder)) - Directory.CreateDirectory(publicationFolder); - if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - File.SetUnixFileMode(publicationFolder, GroupRead | GroupWrite | GroupExecute | OtherRead | OtherWrite | OtherExecute | UserRead | UserWrite | UserExecute); - return publicationFolder; - } - - public void SaveSeriesInfoJson(string downloadDirectory) - { - string publicationFolder = CreatePublicationFolder(downloadDirectory); - string seriesInfoPath = Path.Join(publicationFolder, "series.json"); - if(!File.Exists(seriesInfoPath)) - File.WriteAllText(seriesInfoPath,this.GetSeriesInfoJson()); - if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - File.SetUnixFileMode(seriesInfoPath, GroupRead | GroupWrite | OtherRead | OtherWrite | UserRead | UserWrite); - } - - /// Serialized JSON String for series.json - private string GetSeriesInfoJson() - { - SeriesInfo si = new (new Metadata(this.sortName, this.year.ToString() ?? string.Empty, this.status, this.description ?? "")); - return System.Text.Json.JsonSerializer.Serialize(si); - } - - //Only for series.json - private struct SeriesInfo - { - // ReSharper disable once UnusedAutoPropertyAccessor.Local we need it, trust - [JsonRequired]public Metadata metadata { get; } - public SeriesInfo(Metadata metadata) => this.metadata = metadata; - } - - //Only for series.json what an abomination, why are all the fields not-null???? - private struct Metadata - { - // ReSharper disable UnusedAutoPropertyAccessor.Local we need them all, trust me - [JsonRequired] public string type { get; } - [JsonRequired] public string publisher { get; } - // ReSharper disable twice IdentifierTypo - [JsonRequired] public int comicid { get; } - [JsonRequired] public string booktype { get; } - // ReSharper disable InconsistentNaming This one property is capitalized. Why? - [JsonRequired] public string ComicImage { get; } - [JsonRequired] public int total_issues { get; } - [JsonRequired] public string publication_run { get; } - [JsonRequired]public string name { get; } - [JsonRequired]public string year { get; } - [JsonRequired]public string status { get; } - [JsonRequired]public string description_text { get; } - - public Metadata(string name, string year, string status, string description_text) - { - this.name = name; - this.year = year; - if(status.ToLower() == "ongoing" || status.ToLower() == "hiatus") - this.status = "Continuing"; - else if (status.ToLower() == "completed" || status.ToLower() == "cancelled" || status.ToLower() == "discontinued") - this.status = "Ended"; - else - this.status = status; - this.description_text = description_text; - - //kill it with fire, but otherwise Komga will not parse - type = "Manga"; - publisher = ""; - comicid = 0; - booktype = ""; - ComicImage = ""; - total_issues = 0; - publication_run = ""; - } - } -} \ No newline at end of file diff --git a/Tranga/TaskManager.cs b/Tranga/TaskManager.cs deleted file mode 100644 index 518c5ad..0000000 --- a/Tranga/TaskManager.cs +++ /dev/null @@ -1,385 +0,0 @@ -using Newtonsoft.Json; -using Tranga.Connectors; -using Tranga.TrangaTasks; - -namespace Tranga; - -/// -/// Manages all TrangaTasks. -/// Provides a Threaded environment to execute Tasks, and still manage the Task-Collection -/// -public class TaskManager -{ - public HashSet collection = new(); - private HashSet _allTasks = new(); - private readonly Dictionary _runningTasks = new (); - public bool _continueRunning = true; - private readonly Connector[] _connectors; - public TrangaSettings settings { get; } - public CommonObjects commonObjects { get; init; } - - public TaskManager(TrangaSettings settings, Logging.Logger? logger) - { - commonObjects = CommonObjects.LoadSettings(settings.settingsFilePath, logger); - commonObjects.logger?.WriteLine(this.GetType().ToString(), value: "\n"+ - @"-----------------------------------------------------------------"+"\n"+ - @" |¯¯¯¯¯¯|°|¯¯¯¯¯¯\ /¯¯¯¯¯¯| |¯¯¯\|¯¯¯| /¯¯¯¯¯¯\' /¯¯¯¯¯¯| "+"\n"+ - @" | | | x <|' / ! | | '| | (/¯¯¯\° / ! | "+ "\n"+ - @" ¯|__|¯ |__|\\__\\ /___/¯|_'| |___|\\__| \\_____/' /___/¯|_'| "+ "\n"+ - @"-----------------------------------------------------------------"); - this._connectors = new Connector[] - { - new MangaDex(settings, commonObjects), - new Manganato(settings, commonObjects), - new Mangasee(settings, commonObjects), - new MangaKatana(settings, commonObjects) - }; - - this.settings = settings; - ImportData(); - ExportDataAndSettings(); - Thread taskChecker = new(TaskCheckerThread); - taskChecker.Start(); - } - - /// - /// Runs continuously until shutdown. - /// Checks if tasks have to be executed (time elapsed) - /// - private void TaskCheckerThread() - { - commonObjects.logger?.WriteLine(this.GetType().ToString(), "Starting TaskCheckerThread."); - int waitingTasksCount = _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting); - while (_continueRunning) - { - foreach (TrangaTask waitingButExecute in _allTasks.Where(taskQuery => - taskQuery.nextExecution < DateTime.Now && - taskQuery.state is TrangaTask.ExecutionState.Waiting)) - { - waitingButExecute.state = TrangaTask.ExecutionState.Enqueued; - } - - foreach (TrangaTask enqueuedTask in _allTasks.Where(enqueuedTask => enqueuedTask.state is TrangaTask.ExecutionState.Enqueued).OrderBy(enqueuedTask => enqueuedTask.nextExecution)) - { - switch (enqueuedTask.task) - { - case TrangaTask.Task.DownloadChapter: - case TrangaTask.Task.MonitorPublication: - if (!_allTasks.Any(taskQuery => - { - if (taskQuery.state is not TrangaTask.ExecutionState.Running) return false; - switch (taskQuery) - { - case DownloadChapterTask dct when enqueuedTask is DownloadChapterTask eDct && dct.connectorName == eDct.connectorName: - case MonitorPublicationTask mpt when enqueuedTask is MonitorPublicationTask eMpt && mpt.connectorName == eMpt.connectorName: - return true; - default: - return false; - } - })) - { - ExecuteTaskNow(enqueuedTask); - } - break; - case TrangaTask.Task.UpdateLibraries: - ExecuteTaskNow(enqueuedTask); - break; - } - } - - foreach (TrangaTask timedOutTask in _runningTasks.Keys - .Where(taskQuery => taskQuery.lastChange < DateTime.Now.Subtract(TimeSpan.FromMinutes(3)))) - { - _runningTasks[timedOutTask].Cancel(); - timedOutTask.state = TrangaTask.ExecutionState.Failed; - } - - foreach (TrangaTask finishedTask in _allTasks - .Where(taskQuery => taskQuery.state is TrangaTask.ExecutionState.Success).ToArray()) - { - if(finishedTask is DownloadChapterTask) - { - DeleteTask(finishedTask); - finishedTask.state = TrangaTask.ExecutionState.Success; - } - else - { - finishedTask.state = TrangaTask.ExecutionState.Waiting; - this._runningTasks.Remove(finishedTask); - } - } - - foreach (TrangaTask failedTask in _allTasks.Where(taskQuery => - taskQuery.state is TrangaTask.ExecutionState.Failed).ToArray()) - { - DeleteTask(failedTask); - TrangaTask newTask = failedTask.Clone(); - failedTask.parentTask?.AddChildTask(newTask); - AddTask(newTask); - } - - if(waitingTasksCount != _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting)) - ExportDataAndSettings(); - waitingTasksCount = _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting); - Thread.Sleep(1000); - } - } - - /// - /// Forces the execution of a given task - /// - /// Task to execute - public void ExecuteTaskNow(TrangaTask task) - { - task.state = TrangaTask.ExecutionState.Running; - CancellationTokenSource cToken = new (); - Task t = new(() => - { - task.Execute(this, cToken.Token); - }, cToken.Token); - _runningTasks.Add(task, cToken); - t.Start(); - } - - public void AddTask(TrangaTask newTask) - { - switch (newTask.task) - { - case TrangaTask.Task.UpdateLibraries: - //Only one UpdateKomgaLibrary Task - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Replacing old {newTask.task}-Task."); - if (GetTasksMatching(newTask).FirstOrDefault() is { } exists) - _allTasks.Remove(exists); - _allTasks.Add(newTask); - ExportDataAndSettings(); - break; - default: - if (!GetTasksMatching(newTask).Any()) - { - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Adding new Task {newTask}"); - _allTasks.Add(newTask); - ExportDataAndSettings(); - } - else - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Task already exists {newTask}"); - break; - } - } - - public void DeleteTask(TrangaTask removeTask) - { - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Removing Task {removeTask}"); - if(_allTasks.Contains(removeTask)) - _allTasks.Remove(removeTask); - removeTask.parentTask?.RemoveChildTask(removeTask); - if (_runningTasks.ContainsKey(removeTask)) - { - _runningTasks[removeTask].Cancel(); - _runningTasks.Remove(removeTask); - } - foreach(TrangaTask childTask in removeTask.childTasks) - DeleteTask(childTask); - ExportDataAndSettings(); - } - - // ReSharper disable once MemberCanBePrivate.Global - public IEnumerable GetTasksMatching(TrangaTask mTask) - { - switch (mTask.task) - { - case TrangaTask.Task.UpdateLibraries: - return GetTasksMatching(TrangaTask.Task.UpdateLibraries); - case TrangaTask.Task.DownloadChapter: - DownloadChapterTask dct = (DownloadChapterTask)mTask; - return GetTasksMatching(TrangaTask.Task.DownloadChapter, connectorName: dct.connectorName, - internalId: dct.publication.internalId, chapterNumber: dct.chapter.chapterNumber); - case TrangaTask.Task.MonitorPublication: - MonitorPublicationTask mpt = (MonitorPublicationTask)mTask; - return GetTasksMatching(TrangaTask.Task.MonitorPublication, connectorName: mpt.connectorName, - internalId: mpt.publication.internalId); - } - return Array.Empty(); - } - - public IEnumerable GetTasksMatching(TrangaTask.Task taskType, string? connectorName = null, string? searchString = null, string? internalId = null, string? chapterNumber = null) - { - switch (taskType) - { - case TrangaTask.Task.MonitorPublication: - if(connectorName is null) - return _allTasks.Where(tTask => tTask.task == taskType); - GetConnector(connectorName);//Name check - if (searchString is not null) - { - return _allTasks.Where(mTask => - mTask is MonitorPublicationTask mpt && mpt.connectorName == connectorName && - mpt.ToString().Contains(searchString, StringComparison.InvariantCultureIgnoreCase)); - } - else if (internalId is not null) - { - return _allTasks.Where(mTask => - mTask is MonitorPublicationTask mpt && mpt.connectorName == connectorName && - mpt.publication.internalId == internalId); - } - else - return _allTasks.Where(tTask => - tTask is MonitorPublicationTask mpt && mpt.connectorName == connectorName); - - case TrangaTask.Task.DownloadChapter: - if(connectorName is null) - return _allTasks.Where(tTask => tTask.task == taskType); - GetConnector(connectorName);//Name check - if (searchString is not null) - { - return _allTasks.Where(mTask => - mTask is DownloadChapterTask dct && dct.connectorName == connectorName && - dct.ToString().Contains(searchString, StringComparison.InvariantCultureIgnoreCase)); - } - else if (internalId is not null && chapterNumber is not null) - { - return _allTasks.Where(mTask => - mTask is DownloadChapterTask dct && dct.connectorName == connectorName && - dct.publication.internalId == internalId && - dct.chapter.chapterNumber == chapterNumber); - } - else - return _allTasks.Where(mTask => - mTask is DownloadChapterTask dct && dct.connectorName == connectorName); - - default: - return Array.Empty(); - } - } - - /// - /// Removes a Task from the queue - /// - /// - public void RemoveTaskFromQueue(TrangaTask task) - { - task.lastExecuted = DateTime.Now; - task.state = TrangaTask.ExecutionState.Waiting; - } - - /// - /// Sets last execution time to start of time - /// Let taskManager handle enqueuing - /// - /// - public void AddTaskToQueue(TrangaTask task) - { - task.lastExecuted = DateTime.UnixEpoch; - } - - /// All available Connectors - public Dictionary GetAvailableConnectors() - { - return this._connectors.ToDictionary(connector => connector.name, connector => connector); - } - - /// All TrangaTasks in task-collection - public TrangaTask[] GetAllTasks() - { - TrangaTask[] ret = new TrangaTask[_allTasks.Count]; - _allTasks.CopyTo(ret); - return ret; - } - - /// All added Publications - public Publication[] GetAllPublications() - { - return this.collection.ToArray(); - } - - public List GetExistingChaptersList(Connector connector, Publication publication, string language) - { - Chapter[] newChapters = connector.GetChapters(publication, language); - return newChapters.Where(nChapter => nChapter.CheckChapterIsDownloaded(settings.downloadLocation)).ToList(); - } - - /// - /// Return Connector with given Name - /// - /// Connector-name (exact) - /// If Connector is not available - public Connector GetConnector(string? connectorName) - { - if(connectorName is null) - throw new Exception($"connectorName can not be null"); - Connector? ret = this._connectors.FirstOrDefault(connector => connector.name == connectorName); - if (ret is null) - throw new Exception($"Connector {connectorName} is not an available Connector."); - return ret; - } - - /// - /// Shuts down the taskManager. - /// - /// If force is true, tasks are aborted. - public void Shutdown(bool force = false) - { - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Shutting down (forced={force})"); - _continueRunning = false; - ExportDataAndSettings(); - - if(force) - Environment.Exit(_allTasks.Count(task => task.state is TrangaTask.ExecutionState.Enqueued or TrangaTask.ExecutionState.Running)); - - //Wait for tasks to finish - while(_allTasks.Any(task => task.state is TrangaTask.ExecutionState.Running or TrangaTask.ExecutionState.Enqueued)) - Thread.Sleep(10); - commonObjects.logger?.WriteLine(this.GetType().ToString(), "Tasks finished. Bye!"); - Environment.Exit(0); - } - - private void ImportData() - { - commonObjects.logger?.WriteLine(this.GetType().ToString(), "Importing Data"); - if (File.Exists(settings.tasksFilePath)) - { - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Importing tasks from {settings.tasksFilePath}"); - string buffer = File.ReadAllText(settings.tasksFilePath); - this._allTasks = JsonConvert.DeserializeObject>(buffer, new JsonSerializerSettings() { Converters = { new TrangaTask.TrangaTaskJsonConverter() } })!; - } - - foreach (TrangaTask task in this._allTasks.Where(tTask => tTask.parentTaskId is not null).ToArray()) - { - TrangaTask? parentTask = this._allTasks.FirstOrDefault(pTask => pTask.taskId == task.parentTaskId); - if (parentTask is not null) - { - this.DeleteTask(task); - parentTask.lastExecuted = DateTime.UnixEpoch; - } - } - } - - /// - /// Exports data (settings, tasks) to file - /// - private void ExportDataAndSettings() - { - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Exporting settings to {settings.settingsFilePath}"); - settings.ExportSettings(); - - commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Exporting tasks to {settings.tasksFilePath}"); - while(IsFileInUse(settings.tasksFilePath)) - Thread.Sleep(50); - File.WriteAllText(settings.tasksFilePath, JsonConvert.SerializeObject(this._allTasks)); - } - - private bool IsFileInUse(string path) - { - if (!File.Exists(path)) - return false; - try - { - using FileStream stream = new (path, FileMode.Open, FileAccess.Read, FileShare.None); - stream.Close(); - } - catch (IOException) - { - return true; - } - return false; - } -} \ No newline at end of file diff --git a/Tranga/Tranga.cs b/Tranga/Tranga.cs deleted file mode 100644 index 997a787..0000000 --- a/Tranga/Tranga.cs +++ /dev/null @@ -1,580 +0,0 @@ -using System.Globalization; -using System.Runtime.InteropServices; -using Logging; -using Tranga.API; -using Tranga.Connectors; -using Tranga.NotificationManagers; -using Tranga.TrangaTasks; - -namespace Tranga; - -public static class Tranga -{ - public static void Main(string[] args) - { - bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); - string applicationFolderPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Tranga-API"); - - string downloadFolderPath = isLinux ? "/Manga" : Path.Join(applicationFolderPath, "Manga"); - string logsFolderPath = isLinux ? "/var/log/Tranga" : Path.Join(applicationFolderPath, "log"); - string logFilePath = Path.Join(logsFolderPath, $"log-{DateTime.Now:dd-M-yyyy-HH-mm-ss}.txt"); - string settingsFilePath = Path.Join(applicationFolderPath, "settings.json"); - - - Directory.CreateDirectory(logsFolderPath); - Logger logger = isLinux - ? new Logger(new[] { Logger.LoggerType.FileLogger, Logger.LoggerType.ConsoleLogger }, Console.Out, Console.Out.Encoding, logFilePath) - : new Logger(new[] { Logger.LoggerType.FileLogger }, Console.Out, Console.Out.Encoding, logFilePath); - - logger.WriteLine("Tranga",value: "\n"+ - "-------------------------------------------\n"+ - " Starting Tranga-API\n"+ - "-------------------------------------------"); - logger.WriteLine("Tranga", "Migrating..."); - Migrator.Migrate(settingsFilePath, logger); - - TrangaSettings settings; - if (File.Exists(settingsFilePath)) - { - logger.WriteLine("Tranga", $"Loading settings {settingsFilePath}"); - settings = TrangaSettings.LoadSettings(settingsFilePath); - } - else - { - settings = new TrangaSettings(downloadFolderPath, applicationFolderPath); - settings.version = Migrator.CurrentVersion; - } - - Directory.CreateDirectory(settings.workingDirectory); - Directory.CreateDirectory(settings.downloadLocation); - Directory.CreateDirectory(settings.coverImageCache); - - logger.WriteLine("Tranga", $"Is Linux: {isLinux}"); - logger.WriteLine("Tranga",$"Application-Folder: {settings.workingDirectory}"); - logger.WriteLine("Tranga",$"Settings-File-Path: {settings.settingsFilePath}"); - logger.WriteLine("Tranga",$"Download-Folder-Path: {settings.downloadLocation}"); - logger.WriteLine("Tranga",$"Logfile-Path: {logFilePath}"); - logger.WriteLine("Tranga",$"Image-Cache-Path: {settings.coverImageCache}"); - - logger.WriteLine("Tranga", "Loading Taskmanager."); - TaskManager taskManager = new (settings, logger); - - Server _ = new (6531, taskManager); - foreach(NotificationManager nm in taskManager.commonObjects.notificationManagers) - nm.SendNotification("Tranga-API", "Started Tranga-API"); - - if(!isLinux) - TaskMode(taskManager, logger); - } - - private static void TaskMode(TaskManager taskManager, Logger logger) - { - ConsoleKey selection = ConsoleKey.EraseEndOfFile; - PrintMenu(taskManager, taskManager.settings.downloadLocation); - while (selection != ConsoleKey.Q) - { - int taskCount = taskManager.GetAllTasks().Length; - int taskRunningCount = taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Running); - int taskEnqueuedCount = - taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Enqueued); - Console.SetCursorPosition(0,1); - Console.WriteLine($"Tasks (Running/Queue/Total)): {taskRunningCount}/{taskEnqueuedCount}/{taskCount}"); - - if (Console.KeyAvailable) - { - selection = Console.ReadKey().Key; - switch (selection) - { - case ConsoleKey.L: - while (!Console.KeyAvailable) - { - PrintTasks(taskManager.GetAllTasks(), logger); - Console.WriteLine("Press any key."); - Thread.Sleep(500); - } - Console.ReadKey(); - break; - case ConsoleKey.C: - CreateTask(taskManager); - Console.WriteLine("Press any key."); - Console.ReadKey(); - break; - case ConsoleKey.D: - DeleteTask(taskManager); - Console.WriteLine("Press any key."); - Console.ReadKey(); - break; - case ConsoleKey.E: - ExecuteTaskNow(taskManager); - Console.WriteLine("Press any key."); - Console.ReadKey(); - break; - case ConsoleKey.S: - SearchTasks(taskManager); - Console.WriteLine("Press any key."); - Console.ReadKey(); - break; - case ConsoleKey.R: - while (!Console.KeyAvailable) - { - PrintTasks( - taskManager.GetAllTasks().Where(eTask => eTask.state == TrangaTask.ExecutionState.Running) - .ToArray(), logger); - Console.WriteLine("Press any key."); - Thread.Sleep(500); - } - Console.ReadKey(); - break; - case ConsoleKey.K: - while (!Console.KeyAvailable) - { - PrintTasks( - taskManager.GetAllTasks() - .Where(qTask => qTask.state is TrangaTask.ExecutionState.Enqueued) - .ToArray(), logger); - Console.WriteLine("Press any key."); - Thread.Sleep(500); - } - Console.ReadKey(); - break; - case ConsoleKey.F: - TailLog(logger); - Console.ReadKey(); - break; - case ConsoleKey.G: - RemoveTaskFromQueue(taskManager, logger); - Console.WriteLine("Press any key."); - Console.ReadKey(); - break; - case ConsoleKey.B: - AddTaskToQueue(taskManager, logger); - Console.WriteLine("Press any key."); - Console.ReadKey(); - break; - case ConsoleKey.M: - AddMangaTaskToQueue(taskManager, logger); - Console.WriteLine("Press any key."); - Console.ReadKey(); - break; - } - PrintMenu(taskManager, taskManager.settings.downloadLocation); - } - Thread.Sleep(200); - } - - logger.WriteLine("Tranga_CLI", "Exiting."); - Console.Clear(); - Console.WriteLine("Exiting."); - if (taskManager.GetAllTasks().Any(task => task.state == TrangaTask.ExecutionState.Running)) - { - Console.WriteLine("Force quit (Even with running tasks?) y/N"); - selection = Console.ReadKey().Key; - while(selection != ConsoleKey.Y && selection != ConsoleKey.N) - selection = Console.ReadKey().Key; - taskManager.Shutdown(selection == ConsoleKey.Y); - }else - // ReSharper disable once RedundantArgumentDefaultValue Better readability - taskManager.Shutdown(false); - } - - private static void PrintMenu(TaskManager taskManager, string folderPath) - { - int taskCount = taskManager.GetAllTasks().Length; - int taskRunningCount = taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Running); - int taskEnqueuedCount = - taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Enqueued); - Console.Clear(); - Console.WriteLine($"Download Folder: {folderPath}"); - Console.WriteLine($"Tasks (Running/Queue/Total)): {taskRunningCount}/{taskEnqueuedCount}/{taskCount}"); - Console.WriteLine(); - Console.WriteLine($"{"C: Create Task",-30}{"L: List tasks",-30}{"B: Enqueue Task", -30}"); - Console.WriteLine($"{"D: Delete Task",-30}{"S: Search Tasks", -30}{"K: List Task Queue", -30}"); - Console.WriteLine($"{"E: Execute Task now",-30}{"R: List Running Tasks", -30}{"G: Remove Task from Queue", -30}"); - Console.WriteLine($"{"M: New Download Manga Task",-30}{"", -30}{"", -30}"); - Console.WriteLine($"{"",-30}{"F: Show Log",-30}{"Q: Exit",-30}"); - } - - private static void PrintTasks(TrangaTask[] tasks, Logger? logger) - { - logger?.WriteLine("Tranga_CLI", "Printing Tasks"); - int taskCount = tasks.Length; - int taskRunningCount = tasks.Count(task => task.state == TrangaTask.ExecutionState.Running); - int taskEnqueuedCount = tasks.Count(task => task.state == TrangaTask.ExecutionState.Enqueued); - Console.Clear(); - int tIndex = 0; - Console.WriteLine($"Tasks (Running/Queue/Total): {taskRunningCount}/{taskEnqueuedCount}/{taskCount}"); - string header = - $"{"",-5}{"Task",-20} | {"Last Executed",-20} | {"Reoccurrence",-12} | {"State",-10} | {"Progress",-9} | {"Finished",-20} | {"Remaining",-12} | {"Connector",-15} | Publication/Manga "; - Console.WriteLine(header); - Console.WriteLine(new string('-', header.Length)); - foreach (TrangaTask trangaTask in tasks) - { - string[] taskSplit = trangaTask.ToString().Split(", "); - Console.WriteLine($"{tIndex++:000}: {taskSplit[0],-20} | {taskSplit[1],-20} | {taskSplit[2],-12} | {taskSplit[3],-10} | {taskSplit[4],-9} | {taskSplit[5],-20} | {taskSplit[6][..12],-12} | {(taskSplit.Length > 7 ? taskSplit[7] : ""),-15} | {(taskSplit.Length > 8 ? taskSplit[8] : "")} {(taskSplit.Length > 9 ? taskSplit[9] : "")} {(taskSplit.Length > 10 ? taskSplit[10] : "")}"); - } - - } - - private static TrangaTask[] SelectTasks(TrangaTask[] tasks, Logger? logger) - { - logger?.WriteLine("Tranga_CLI", "Menu: Select task"); - if (tasks.Length < 1) - { - Console.Clear(); - Console.WriteLine("There are no available Tasks."); - logger?.WriteLine("Tranga_CLI", "No available Tasks."); - return Array.Empty(); - } - PrintTasks(tasks, logger); - - logger?.WriteLine("Tranga_CLI", "Selecting Task to Remove (from queue)"); - Console.WriteLine("Enter q to abort"); - Console.WriteLine($"Select Task(s) (0-{tasks.Length - 1}):"); - - string? selectedTask = Console.ReadLine(); - while(selectedTask is null || selectedTask.Length < 1) - selectedTask = Console.ReadLine(); - - if (selectedTask.Length == 1 && selectedTask.ToLower() == "q") - { - Console.Clear(); - Console.WriteLine("aborted."); - logger?.WriteLine("Tranga_CLI", "aborted"); - return Array.Empty(); - } - - if (selectedTask.Contains('-')) - { - int start = Convert.ToInt32(selectedTask.Split('-')[0]); - int end = Convert.ToInt32(selectedTask.Split('-')[1]); - return tasks[start..end]; - } - else - { - int selectedTaskIndex = Convert.ToInt32(selectedTask); - return new[] { tasks[selectedTaskIndex] }; - } - } - - private static void AddMangaTaskToQueue(TaskManager taskManager, Logger logger) - { - Console.Clear(); - logger.WriteLine("Tranga_CLI", "Menu: Add Manga Download to queue"); - - Connector? connector = SelectConnector(taskManager.GetAvailableConnectors().Values.ToArray(), logger); - if (connector is null) - return; - - Publication? publication = SelectPublication(taskManager, connector); - if (publication is null) - return; - - TimeSpan reoccurrence = SelectReoccurrence(logger); - logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager"); - TrangaTask nTask = new MonitorPublicationTask(connector.name, (Publication)publication, reoccurrence, "en"); - taskManager.AddTask(nTask); - Console.WriteLine(nTask); - } - - private static void AddTaskToQueue(TaskManager taskManager, Logger logger) - { - Console.Clear(); - logger.WriteLine("Tranga_CLI", "Menu: Add Task to queue"); - - TrangaTask[] tasks = taskManager.GetAllTasks().Where(rTask => - rTask.state is not TrangaTask.ExecutionState.Enqueued and not TrangaTask.ExecutionState.Running).ToArray(); - - TrangaTask[] selectedTasks = SelectTasks(tasks, logger); - logger.WriteLine("Tranga_CLI", $"Sending {selectedTasks.Length} Tasks to TaskManager"); - foreach(TrangaTask task in selectedTasks) - taskManager.AddTaskToQueue(task); - } - - private static void RemoveTaskFromQueue(TaskManager taskManager, Logger logger) - { - Console.Clear(); - logger.WriteLine("Tranga_CLI", "Menu: Remove Task from queue"); - - TrangaTask[] tasks = taskManager.GetAllTasks().Where(rTask => rTask.state is TrangaTask.ExecutionState.Enqueued).ToArray(); - - TrangaTask[] selectedTasks = SelectTasks(tasks, logger); - logger.WriteLine("Tranga_CLI", $"Sending {selectedTasks.Length} Tasks to TaskManager"); - foreach(TrangaTask task in selectedTasks) - taskManager.RemoveTaskFromQueue(task); - } - - private static void TailLog(Logger logger) - { - logger.WriteLine("Tranga_CLI", "Menu: Show Log-lines"); - Console.Clear(); - - string[] lines = logger.Tail(20); - foreach (string message in lines) - Console.Write(message); - - while (!Console.KeyAvailable) - { - string[] newLines = logger.GetNewLines(); - foreach(string message in newLines) - Console.Write(message); - Thread.Sleep(40); - } - } - - private static void CreateTask(TaskManager taskManager) - { - taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "Menu: Creating Task"); - TrangaTask.Task? tmpTask = SelectTaskType(taskManager.commonObjects.logger); - if (tmpTask is null) - return; - TrangaTask.Task task = (TrangaTask.Task)tmpTask; - - Connector? connector = null; - if (task != TrangaTask.Task.UpdateLibraries) - { - connector = SelectConnector(taskManager.GetAvailableConnectors().Values.ToArray(), taskManager.commonObjects.logger); - if (connector is null) - return; - } - - Publication? publication = null; - if (task != TrangaTask.Task.UpdateLibraries) - { - publication = SelectPublication(taskManager, connector!); - if (publication is null) - return; - } - - if (task is TrangaTask.Task.MonitorPublication) - { - TimeSpan reoccurrence = SelectReoccurrence(taskManager.commonObjects.logger); - taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "Sending Task to TaskManager"); - - TrangaTask newTask = new MonitorPublicationTask(connector!.name, (Publication)publication!, reoccurrence, "en"); - taskManager.AddTask(newTask); - Console.WriteLine(newTask); - }else if (task is TrangaTask.Task.DownloadChapter) - { - foreach (Chapter chapter in SelectChapters(connector!, (Publication)publication!, taskManager.commonObjects.logger)) - { - TrangaTask newTask = new DownloadChapterTask(connector!.name, (Publication)publication, chapter, "en"); - taskManager.AddTask(newTask); - Console.WriteLine(newTask); - } - } - } - - private static void ExecuteTaskNow(TaskManager taskManager) - { - taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "Menu: Executing Task"); - TrangaTask[] tasks = taskManager.GetAllTasks().Where(nTask => nTask.state is not TrangaTask.ExecutionState.Running).ToArray(); - - TrangaTask[] selectedTasks = SelectTasks(tasks, taskManager.commonObjects.logger); - taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", $"Sending {selectedTasks.Length} Tasks to TaskManager"); - foreach(TrangaTask task in selectedTasks) - taskManager.ExecuteTaskNow(task); - } - - private static void DeleteTask(TaskManager taskManager) - { - taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "Menu: Delete Task"); - TrangaTask[] tasks = taskManager.GetAllTasks(); - - TrangaTask[] selectedTasks = SelectTasks(tasks, taskManager.commonObjects.logger); - taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", $"Sending {selectedTasks.Length} Tasks to TaskManager"); - foreach(TrangaTask task in selectedTasks) - taskManager.DeleteTask(task); - } - - private static TrangaTask.Task? SelectTaskType(Logger? logger) - { - logger?.WriteLine("Tranga_CLI", "Menu: Select TaskType"); - Console.Clear(); - string[] taskNames = Enum.GetNames(); - - int tIndex = 0; - Console.WriteLine("Available Tasks:"); - foreach (string taskName in taskNames) - Console.WriteLine($"{tIndex++}: {taskName}"); - - Console.WriteLine("Enter q to abort"); - Console.WriteLine($"Select Task (0-{taskNames.Length - 1}):"); - - string? selectedTask = Console.ReadLine(); - while(selectedTask is null || selectedTask.Length < 1) - selectedTask = Console.ReadLine(); - - if (selectedTask.Length == 1 && selectedTask.ToLower() == "q") - { - Console.Clear(); - Console.WriteLine("aborted."); - logger?.WriteLine("Tranga_CLI", "aborted."); - return null; - } - - try - { - int selectedTaskIndex = Convert.ToInt32(selectedTask); - string selectedTaskName = taskNames[selectedTaskIndex]; - return Enum.Parse(selectedTaskName); - } - catch (Exception e) - { - Console.WriteLine($"Exception: {e.Message}"); - logger?.WriteLine("Tranga_CLI", e.Message); - } - - return null; - } - - private static TimeSpan SelectReoccurrence(Logger? logger) - { - logger?.WriteLine("Tranga_CLI", "Menu: Select Reoccurrence"); - Console.WriteLine("Select reoccurrence Timer (Format hh:mm:ss):"); - return TimeSpan.Parse(Console.ReadLine()!, new CultureInfo("en-US")); - } - - private static Chapter[] SelectChapters(Connector connector, Publication publication, Logger? logger) - { - logger?.WriteLine("Tranga_CLI", "Menu: Select Chapters"); - Chapter[] availableChapters = connector.GetChapters(publication, "en"); - int cIndex = 0; - Console.WriteLine("Chapters:"); - - System.Text.StringBuilder sb = new(); - foreach(Chapter chapter in availableChapters) - { - sb.Append($"{cIndex++}: "); - - if(string.IsNullOrWhiteSpace(chapter.volumeNumber) == false) - { - sb.Append($"Vol.{chapter.volumeNumber} "); - } - - if(string.IsNullOrWhiteSpace(chapter.chapterNumber) == false) - { - sb.Append($"Ch.{chapter.chapterNumber} "); - } - - if(string.IsNullOrWhiteSpace(chapter.name) == false) - { - sb.Append($" - {chapter.name}"); - } - - Console.WriteLine(sb.ToString()); - sb.Clear(); - } - - Console.WriteLine("Enter q to abort"); - Console.WriteLine($"Select Chapter(s):"); - - string? selectedChapters = Console.ReadLine(); - while(selectedChapters is null || selectedChapters.Length < 1) - selectedChapters = Console.ReadLine(); - - return connector.SelectChapters(publication, selectedChapters); - } - - private static Connector? SelectConnector(Connector[] connectors, Logger? logger) - { - logger?.WriteLine("Tranga_CLI", "Menu: Select Connector"); - Console.Clear(); - - int cIndex = 0; - Console.WriteLine("Connectors:"); - foreach (Connector connector in connectors) - Console.WriteLine($"{cIndex++}: {connector.name}"); - - Console.WriteLine("Enter q to abort"); - Console.WriteLine($"Select Connector (0-{connectors.Length - 1}):"); - - string? selectedConnector = Console.ReadLine(); - while(selectedConnector is null || selectedConnector.Length < 1) - selectedConnector = Console.ReadLine(); - - if (selectedConnector.Length == 1 && selectedConnector.ToLower() == "q") - { - Console.Clear(); - Console.WriteLine("aborted."); - logger?.WriteLine("Tranga_CLI", "aborted."); - return null; - } - - try - { - int selectedConnectorIndex = Convert.ToInt32(selectedConnector); - return connectors[selectedConnectorIndex]; - } - catch (Exception e) - { - Console.WriteLine($"Exception: {e.Message}"); - logger?.WriteLine("Tranga_CLI", e.Message); - } - - return null; - } - - private static Publication? SelectPublication(TaskManager taskManager, Connector connector) - { - taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "Menu: Select Publication"); - - Console.Clear(); - Console.WriteLine($"Connector: {connector.name}"); - Console.WriteLine("Publication search query (leave empty for all):"); - string? query = Console.ReadLine(); - - Publication[] publications = connector.GetPublications(ref taskManager.collection, query ?? ""); - - if (publications.Length < 1) - { - taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "No publications returned"); - Console.WriteLine($"No publications for query '{query}' returned;"); - return null; - } - - int pIndex = 0; - Console.WriteLine("Publications:"); - foreach(Publication publication in publications) - Console.WriteLine($"{pIndex++}: {publication.sortName}"); - - Console.WriteLine("Enter q to abort"); - Console.WriteLine($"Select publication to Download (0-{publications.Length - 1}):"); - - string? selectedPublication = Console.ReadLine(); - while(selectedPublication is null || selectedPublication.Length < 1) - selectedPublication = Console.ReadLine(); - - if (selectedPublication.Length == 1 && selectedPublication.ToLower() == "q") - { - Console.Clear(); - Console.WriteLine("aborted."); - taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "aborted."); - return null; - } - - try - { - int selectedPublicationIndex = Convert.ToInt32(selectedPublication); - return publications[selectedPublicationIndex]; - } - catch (Exception e) - { - Console.WriteLine($"Exception: {e.Message}"); - taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", e.Message); - } - - return null; - } - - private static void SearchTasks(TaskManager taskManager) - { - taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "Menu: Search task"); - Console.Clear(); - Console.WriteLine("Enter search query:"); - string? query = Console.ReadLine(); - while (query is null || query.Length < 4) - query = Console.ReadLine(); - PrintTasks(taskManager.GetAllTasks().Where(qTask => - qTask.ToString().ToLower().Contains(query, StringComparison.OrdinalIgnoreCase)).ToArray(), taskManager.commonObjects.logger); - } -} \ No newline at end of file diff --git a/Tranga/Tranga.csproj b/Tranga/Tranga.csproj deleted file mode 100644 index 2e4242b..0000000 --- a/Tranga/Tranga.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - net7.0 - enable - enable - Exe - - - - - - - - - - - - - - - .dockerignore - Dockerfile - - - - diff --git a/Tranga/TrangaSettings.cs b/Tranga/TrangaSettings.cs deleted file mode 100644 index a37de07..0000000 --- a/Tranga/TrangaSettings.cs +++ /dev/null @@ -1,97 +0,0 @@ -using Newtonsoft.Json; -using Tranga.LibraryManagers; -using Tranga.NotificationManagers; - -namespace Tranga; - -public class TrangaSettings -{ - public string downloadLocation { get; private set; } - public string workingDirectory { get; init; } - [JsonIgnore] public string settingsFilePath => Path.Join(workingDirectory, "settings.json"); - [JsonIgnore] public string tasksFilePath => Path.Join(workingDirectory, "tasks.json"); - [JsonIgnore] public string coverImageCache => Path.Join(workingDirectory, "imageCache"); - public ushort? version { get; set; } - - public TrangaSettings(string downloadLocation, string workingDirectory) - { - if (downloadLocation.Length < 1 || workingDirectory.Length < 1) - throw new ArgumentException("Download-location and working-directory paths can not be empty!"); - this.workingDirectory = workingDirectory; - this.downloadLocation = downloadLocation; - } - - public static TrangaSettings LoadSettings(string importFilePath) - { - if (!File.Exists(importFilePath)) - return new TrangaSettings(Path.Join(Directory.GetCurrentDirectory(), "Downloads"), Directory.GetCurrentDirectory()); - - string toRead = File.ReadAllText(importFilePath); - SettingsJsonObject settings = JsonConvert.DeserializeObject(toRead, - new JsonSerializerSettings { Converters = { new NotificationManager.NotificationManagerJsonConverter(), new LibraryManager.LibraryManagerJsonConverter() } })!; - return settings.ts ?? new TrangaSettings(Path.Join(Directory.GetCurrentDirectory(), "Downloads"), Directory.GetCurrentDirectory()); - - } - - public void ExportSettings() - { - 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 SettingsJsonObject(this, settings?.co); - File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(settings)); - } - - public void UpdateSettings(UpdateField field, params string[] values) - { - switch (field) - { - case UpdateField.DownloadLocation: - if (values.Length != 1) - return; - this.downloadLocation = values[0]; - break; - } - ExportSettings(); - } - - public enum UpdateField { DownloadLocation, Komga, Kavita, Gotify, LunaSea} - - internal class SettingsJsonObject - { - public TrangaSettings? ts { get; } - public CommonObjects? co { get; } - - public SettingsJsonObject(TrangaSettings? ts, CommonObjects? co) - { - this.ts = ts; - this.co = co; - } - } -} \ No newline at end of file diff --git a/Tranga/TrangaTasks/DownloadChapterTask.cs b/Tranga/TrangaTasks/DownloadChapterTask.cs deleted file mode 100644 index 8134be6..0000000 --- a/Tranga/TrangaTasks/DownloadChapterTask.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Net; -using Tranga.Connectors; -using Tranga.NotificationManagers; -using Tranga.LibraryManagers; - -namespace Tranga.TrangaTasks; - -public class DownloadChapterTask : TrangaTask -{ - public string connectorName { get; } - public Publication publication { get; } - // ReSharper disable once MemberCanBePrivate.Global - public string language { get; } - public Chapter chapter { get; } - - private double _dctProgress; - - public DownloadChapterTask(string connectorName, Publication publication, Chapter chapter, string language = "en", MonitorPublicationTask? parentTask = null) : base(Task.DownloadChapter, TimeSpan.Zero, parentTask) - { - this.chapter = chapter; - this.connectorName = connectorName; - this.publication = publication; - this.language = language; - } - - protected override HttpStatusCode ExecuteTask(TaskManager taskManager, CancellationToken? cancellationToken = null) - { - if (cancellationToken?.IsCancellationRequested ?? false) - return HttpStatusCode.RequestTimeout; - Connector connector = taskManager.GetConnector(this.connectorName); - connector.CopyCoverFromCacheToDownloadLocation(this.publication); - HttpStatusCode downloadSuccess = connector.DownloadChapter(this.publication, this.chapter, this, cancellationToken); - if ((int)downloadSuccess >= 200 && (int)downloadSuccess < 300) - { - foreach(NotificationManager nm in taskManager.commonObjects.notificationManagers) - nm.SendNotification("Chapter downloaded", $"{this.publication.sortName} {this.chapter.chapterNumber} {this.chapter.name}"); - - foreach (LibraryManager lm in taskManager.commonObjects.libraryManagers) - lm.UpdateLibrary(); - } - return downloadSuccess; - } - - public override TrangaTask Clone() - { - return new DownloadChapterTask(this.connectorName, this.publication, this.chapter, - this.language, (MonitorPublicationTask?)this.parentTask); - } - - protected override double GetProgress() - { - return _dctProgress; - } - - internal void IncrementProgress(double amount) - { - this._dctProgress += amount; - this.lastChange = DateTime.Now; - if(this.parentTask is not null) - this.parentTask.lastChange = DateTime.Now; - } - - public override string ToString() - { - return $"{base.ToString()}, {connectorName}, {publication.sortName} {publication.internalId}, Vol.{chapter.volumeNumber} Ch.{chapter.chapterNumber}"; - } -} \ No newline at end of file diff --git a/Tranga/TrangaTasks/MonitorPublicationTask.cs b/Tranga/TrangaTasks/MonitorPublicationTask.cs deleted file mode 100644 index 5319759..0000000 --- a/Tranga/TrangaTasks/MonitorPublicationTask.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Net; -using Tranga.Connectors; - -namespace Tranga.TrangaTasks; - -public class MonitorPublicationTask : TrangaTask -{ - public string connectorName { get; } - public Publication publication { get; } - // ReSharper disable once MemberCanBePrivate.Global - public string language { get; } - public MonitorPublicationTask(string connectorName, Publication publication, TimeSpan reoccurrence, string language = "en") : base(Task.MonitorPublication, reoccurrence) - { - this.connectorName = connectorName; - this.publication = publication; - this.language = language; - } - - protected override HttpStatusCode ExecuteTask(TaskManager taskManager, CancellationToken? cancellationToken = null) - { - if (cancellationToken?.IsCancellationRequested ?? false) - return HttpStatusCode.RequestTimeout; - Connector connector = taskManager.GetConnector(this.connectorName); - - //Check if Publication already has a Folder - publication.CreatePublicationFolder(taskManager.settings.downloadLocation); - List newChapters = connector.GetNewChaptersList(publication, language, ref taskManager.collection); - - connector.CopyCoverFromCacheToDownloadLocation(publication); - - publication.SaveSeriesInfoJson(taskManager.settings.downloadLocation); - - foreach (Chapter newChapter in newChapters) - { - DownloadChapterTask newTask = new (this.connectorName, publication, newChapter, this.language, this); - this.childTasks.Add(newTask); - newTask.state = ExecutionState.Enqueued; - taskManager.AddTask(newTask); - } - - return HttpStatusCode.OK; - } - - public override TrangaTask Clone() - { - return new MonitorPublicationTask(this.connectorName, this.publication, this.reoccurrence, - this.language); - } - - protected override double GetProgress() - { - if (this.childTasks.Count > 0) - return this.childTasks.Sum(ct => ct.progress) / childTasks.Count; - return 1; - } - - public override string ToString() - { - return $"{base.ToString()}, {connectorName}, {publication.sortName} {publication.internalId}"; - } -} \ No newline at end of file diff --git a/Tranga/TrangaTasks/TrangaTask.cs b/Tranga/TrangaTasks/TrangaTask.cs deleted file mode 100644 index 189884c..0000000 --- a/Tranga/TrangaTasks/TrangaTask.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System.Net; -using System.Text.Json.Serialization; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using JsonConverter = Newtonsoft.Json.JsonConverter; - -namespace Tranga.TrangaTasks; - -/// -/// Stores information on Task, when implementing new Tasks also update the serializer -/// -[JsonDerivedType(typeof(MonitorPublicationTask), 2)] -[JsonDerivedType(typeof(UpdateLibrariesTask), 3)] -[JsonDerivedType(typeof(DownloadChapterTask), 4)] -public abstract class TrangaTask -{ - // ReSharper disable once MemberCanBeProtected.Global - public TimeSpan reoccurrence { get; } - public DateTime lastExecuted { get; set; } - [Newtonsoft.Json.JsonIgnore] public ExecutionState state { get; set; } - public Task task { get; } - public string taskId { get; init; } - [Newtonsoft.Json.JsonIgnore] public TrangaTask? parentTask { get; set; } - public string? parentTaskId { get; set; } - [Newtonsoft.Json.JsonIgnore] internal HashSet childTasks { get; } - public double progress => GetProgress(); - // ReSharper disable once MemberCanBePrivate.Global - [Newtonsoft.Json.JsonIgnore]public DateTime executionStarted { get; private set; } - [Newtonsoft.Json.JsonIgnore]public DateTime lastChange { get; internal set; } - // ReSharper disable once MemberCanBePrivate.Global - [Newtonsoft.Json.JsonIgnore]public DateTime executionApproximatelyFinished => lastChange.Add(GetRemainingTime()); - // ReSharper disable once MemberCanBePrivate.Global - public TimeSpan executionApproximatelyRemaining => executionApproximatelyFinished.Subtract(DateTime.Now); - [Newtonsoft.Json.JsonIgnore]public DateTime nextExecution => lastExecuted.Add(reoccurrence); - - public enum ExecutionState { Waiting, Enqueued, Running, Failed, Success } - - protected TrangaTask(Task task, TimeSpan reoccurrence, TrangaTask? parentTask = null) - { - this.reoccurrence = reoccurrence; - this.lastExecuted = DateTime.Now.Subtract(reoccurrence); - this.task = task; - this.executionStarted = DateTime.UnixEpoch; - this.lastChange = DateTime.MaxValue; - this.taskId = Convert.ToBase64String(BitConverter.GetBytes(new Random().Next())); - this.childTasks = new(); - this.parentTask = parentTask; - this.parentTaskId = parentTask?.taskId; - } - - /// - /// BL for concrete Tasks - /// - /// - /// - protected abstract HttpStatusCode ExecuteTask(TaskManager taskManager, CancellationToken? cancellationToken = null); - - public abstract TrangaTask Clone(); - - protected abstract double GetProgress(); - - /// - /// Execute the task - /// - /// Should be the parent taskManager - /// - public void Execute(TaskManager taskManager, CancellationToken? cancellationToken = null) - { - taskManager.commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Executing Task {this}"); - this.state = ExecutionState.Running; - this.executionStarted = DateTime.Now; - this.lastChange = DateTime.Now; - if(parentTask is not null && parentTask.childTasks.All(ct => ct.state is ExecutionState.Waiting or ExecutionState.Failed)) - parentTask.executionStarted = DateTime.Now; - - HttpStatusCode statusCode = ExecuteTask(taskManager, cancellationToken); - - if ((int)statusCode >= 200 && (int)statusCode < 300) - { - this.lastExecuted = DateTime.Now; - this.state = ExecutionState.Success; - } - else - { - this.state = ExecutionState.Failed; - this.lastExecuted = DateTime.MaxValue; - } - - if (this is DownloadChapterTask) - taskManager.DeleteTask(this); - - taskManager.commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Finished Executing Task {this}"); - } - - public void AddChildTask(TrangaTask childTask) - { - this.childTasks.Add(childTask); - } - - public void RemoveChildTask(TrangaTask childTask) - { - this.childTasks.Remove(childTask); - } - - private TimeSpan GetRemainingTime() - { - if(progress == 0 || state is ExecutionState.Enqueued or ExecutionState.Waiting or ExecutionState.Failed || lastChange == DateTime.MaxValue) - return DateTime.MaxValue.Subtract(lastChange).Subtract(TimeSpan.FromHours(1)); - TimeSpan elapsed = lastChange.Subtract(executionStarted); - return elapsed.Divide(progress).Multiply(1 - progress); - } - - public enum Task : byte - { - MonitorPublication = 2, - UpdateLibraries = 3, - DownloadChapter = 4, - } - - public override string ToString() - { - return $"{task}, {lastExecuted}, {reoccurrence}, {state}, {progress:P2}, {executionApproximatelyFinished}, {executionApproximatelyRemaining}"; - } - - public class TrangaTaskJsonConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - { - return objectType == typeof(TrangaTask); - } - - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - JObject jo = JObject.Load(reader); - if (jo["task"]!.Value() == (Int64)Task.MonitorPublication) - return jo.ToObject(serializer)!; - - if (jo["task"]!.Value() == (Int64)Task.UpdateLibraries) - return jo.ToObject(serializer)!; - - if (jo["task"]!.Value() == (Int64)Task.DownloadChapter) - return jo.ToObject(serializer)!; - - throw new Exception(); - } - - public override bool CanWrite => false; - - /// - /// Don't call this - /// - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - throw new Exception("Dont call this"); - } - } -} \ No newline at end of file diff --git a/Tranga/TrangaTasks/UpdateLibrariesTask.cs b/Tranga/TrangaTasks/UpdateLibrariesTask.cs deleted file mode 100644 index e7ecb02..0000000 --- a/Tranga/TrangaTasks/UpdateLibrariesTask.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Net; - -namespace Tranga.TrangaTasks; - -/// -/// LEGACY DEPRECATED -/// -public class UpdateLibrariesTask : TrangaTask -{ - public UpdateLibrariesTask(TimeSpan reoccurrence) : base(Task.UpdateLibraries, reoccurrence) - { - } - - protected override HttpStatusCode ExecuteTask(TaskManager taskManager, CancellationToken? cancellationToken = null) - { - return HttpStatusCode.BadRequest; - } - - public override TrangaTask Clone() - { - return new UpdateLibrariesTask(this.reoccurrence); - } - - protected override double GetProgress() - { - return 1; - } -} \ No newline at end of file diff --git a/screenshots/addtask.png b/screenshots/addtask.png deleted file mode 100644 index ebaf431..0000000 Binary files a/screenshots/addtask.png and /dev/null differ diff --git a/screenshots/overview.png b/screenshots/overview.png deleted file mode 100644 index 012b0b8..0000000 Binary files a/screenshots/overview.png and /dev/null differ diff --git a/screenshots/progress.png b/screenshots/progress.png deleted file mode 100644 index 728aec7..0000000 Binary files a/screenshots/progress.png and /dev/null differ diff --git a/screenshots/publication-description.png b/screenshots/publication-description.png deleted file mode 100644 index d852b00..0000000 Binary files a/screenshots/publication-description.png and /dev/null differ diff --git a/screenshots/settings.png b/screenshots/settings.png deleted file mode 100644 index 14039e0..0000000 Binary files a/screenshots/settings.png and /dev/null differ