Merge branch '41_-_trash_everything' into cuttingedge
# Conflicts: # Tranga/MangaConnectors/DownloadClient.cs
15
.github/workflows/docker-image-cuttingedge.yml
vendored
@ -42,17 +42,4 @@ jobs:
|
|||||||
pull: true
|
pull: true
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
glax/tranga-api:cuttingedge
|
glax/tranga-api:cuttingedge
|
||||||
|
|
||||||
# https://github.com/docker/build-push-action#multi-platform-image
|
|
||||||
- name: Build and push Website
|
|
||||||
uses: docker/build-push-action@v4.1.1
|
|
||||||
with:
|
|
||||||
context: ./Website
|
|
||||||
file: ./Website/Dockerfile
|
|
||||||
#platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
|
|
||||||
platforms: linux/amd64
|
|
||||||
pull: true
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
glax/tranga-website:cuttingedge
|
|
15
.github/workflows/docker-image-master.yml
vendored
@ -44,17 +44,4 @@ jobs:
|
|||||||
pull: true
|
pull: true
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
glax/tranga-api:latest
|
glax/tranga-api:latest
|
||||||
|
|
||||||
# https://github.com/docker/build-push-action#multi-platform-image
|
|
||||||
- name: Build and push Website
|
|
||||||
uses: docker/build-push-action@v4.1.1
|
|
||||||
with:
|
|
||||||
context: ./Website
|
|
||||||
file: ./Website/Dockerfile
|
|
||||||
#platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
|
|
||||||
platforms: linux/amd64
|
|
||||||
pull: true
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
glax/tranga-website:latest
|
|
18
CLI/CLI.csproj
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Spectre.Console.Cli" Version="0.47.1-preview.0.11" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Tranga\Tranga.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
155
CLI/Program.cs
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Logging;
|
||||||
|
using Spectre.Console;
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
using Tranga;
|
||||||
|
|
||||||
|
var app = new CommandApp<TrangaCli>();
|
||||||
|
return app.Run(args);
|
||||||
|
|
||||||
|
internal sealed class TrangaCli : Command<TrangaCli.Settings>
|
||||||
|
{
|
||||||
|
public sealed class Settings : CommandSettings
|
||||||
|
{
|
||||||
|
[Description("Directory to which downloaded Manga are saved")]
|
||||||
|
[CommandOption("-d|--downloadLocation")]
|
||||||
|
[DefaultValue(null)]
|
||||||
|
public string? downloadLocation { get; init; }
|
||||||
|
|
||||||
|
[Description("Directory in which application-data is saved")]
|
||||||
|
[CommandOption("-w|--workingDirectory")]
|
||||||
|
[DefaultValue(null)]
|
||||||
|
public string? workingDirectory { get; init; }
|
||||||
|
|
||||||
|
[Description("Enables the file-logger")]
|
||||||
|
[CommandOption("-f")]
|
||||||
|
[DefaultValue(null)]
|
||||||
|
public bool? fileLogger { get; init; }
|
||||||
|
|
||||||
|
[Description("Path to save logfile to")]
|
||||||
|
[CommandOption("-l|--fPath")]
|
||||||
|
[DefaultValue(null)]
|
||||||
|
public string? fileLoggerPath { get; init; }
|
||||||
|
|
||||||
|
[Description("Port on which to run API on")]
|
||||||
|
[CommandOption("-p|--port")]
|
||||||
|
[DefaultValue(null)]
|
||||||
|
public int? apiPort { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings)
|
||||||
|
{
|
||||||
|
List<Logger.LoggerType> enabledLoggers = new();
|
||||||
|
if(settings.fileLogger is true)
|
||||||
|
enabledLoggers.Add(Logger.LoggerType.FileLogger);
|
||||||
|
|
||||||
|
string? logFilePath = settings.fileLoggerPath ?? "";
|
||||||
|
Logger logger = new(enabledLoggers.ToArray(), Console.Out, Console.OutputEncoding, logFilePath);
|
||||||
|
|
||||||
|
TrangaSettings trangaSettings = new (settings.downloadLocation, settings.workingDirectory, settings.apiPort);
|
||||||
|
|
||||||
|
Directory.CreateDirectory(trangaSettings.downloadLocation);
|
||||||
|
Directory.CreateDirectory(trangaSettings.workingDirectory);
|
||||||
|
|
||||||
|
Tranga.Tranga? api = null;
|
||||||
|
|
||||||
|
Thread trangaApi = new Thread(() =>
|
||||||
|
{
|
||||||
|
api = new(logger, trangaSettings);
|
||||||
|
});
|
||||||
|
trangaApi.Start();
|
||||||
|
|
||||||
|
HttpClient client = new();
|
||||||
|
|
||||||
|
bool exit = false;
|
||||||
|
while (!exit)
|
||||||
|
{
|
||||||
|
string menuSelect = AnsiConsole.Prompt(
|
||||||
|
new SelectionPrompt<string>()
|
||||||
|
.Title("Menu")
|
||||||
|
.PageSize(10)
|
||||||
|
.MoreChoicesText("Up/Down")
|
||||||
|
.AddChoices(new[]
|
||||||
|
{
|
||||||
|
"CustomRequest",
|
||||||
|
"Log",
|
||||||
|
"Exit"
|
||||||
|
}));
|
||||||
|
|
||||||
|
switch (menuSelect)
|
||||||
|
{
|
||||||
|
case "CustomRequest":
|
||||||
|
HttpMethod requestMethod = AnsiConsole.Prompt(
|
||||||
|
new SelectionPrompt<HttpMethod>()
|
||||||
|
.Title("Request Type")
|
||||||
|
.AddChoices(new[]
|
||||||
|
{
|
||||||
|
HttpMethod.Get,
|
||||||
|
HttpMethod.Delete,
|
||||||
|
HttpMethod.Post
|
||||||
|
}));
|
||||||
|
string requestPath = AnsiConsole.Prompt(
|
||||||
|
new TextPrompt<string>("Request Path:"));
|
||||||
|
List<ValueTuple<string, string>> parameters = new();
|
||||||
|
while (AnsiConsole.Confirm("Add Parameter?"))
|
||||||
|
{
|
||||||
|
string name = AnsiConsole.Ask<string>("Parameter Name:");
|
||||||
|
string value = AnsiConsole.Ask<string>("Parameter Value:");
|
||||||
|
parameters.Add(new ValueTuple<string, string>(name, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
string requestString = $"http://localhost:{trangaSettings.apiPortNumber}/{requestPath}";
|
||||||
|
if (parameters.Any())
|
||||||
|
{
|
||||||
|
requestString += "?";
|
||||||
|
foreach (ValueTuple<string, string> parameter in parameters)
|
||||||
|
requestString += $"{parameter.Item1}={parameter.Item2}&";
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpRequestMessage request = new (requestMethod, requestString);
|
||||||
|
AnsiConsole.WriteLine($"Request: {request.Method} {request.RequestUri}");
|
||||||
|
HttpResponseMessage response;
|
||||||
|
if (AnsiConsole.Confirm("Send Request?"))
|
||||||
|
response = client.Send(request);
|
||||||
|
else break;
|
||||||
|
AnsiConsole.WriteLine($"Response: {(int)response.StatusCode} {response.StatusCode}");
|
||||||
|
AnsiConsole.WriteLine(response.Content.ReadAsStringAsync().Result);
|
||||||
|
break;
|
||||||
|
case "Log":
|
||||||
|
List<string> lines = logger.Tail(10).ToList();
|
||||||
|
Rows rows = new Rows(lines.Select(line => new Text(line)));
|
||||||
|
|
||||||
|
AnsiConsole.Live(rows).Start(context =>
|
||||||
|
{
|
||||||
|
bool running = true;
|
||||||
|
while (running)
|
||||||
|
{
|
||||||
|
string[] newLines = logger.GetNewLines();
|
||||||
|
if (newLines.Length > 0)
|
||||||
|
{
|
||||||
|
lines.AddRange(newLines);
|
||||||
|
rows = new Rows(lines.Select(line => new Text(line)));
|
||||||
|
context.UpdateTarget(rows);
|
||||||
|
}
|
||||||
|
Thread.Sleep(100);
|
||||||
|
if (AnsiConsole.Console.Input.IsKeyAvailable())
|
||||||
|
{
|
||||||
|
AnsiConsole.Console.Input.ReadKey(true); //Do not process input
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "Exit":
|
||||||
|
exit = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (api is not null)
|
||||||
|
api.keepRunning = false;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,13 @@
|
|||||||
using System.Text;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace Logging;
|
namespace Logging;
|
||||||
|
|
||||||
public class Logger : TextWriter
|
public class Logger : TextWriter
|
||||||
{
|
{
|
||||||
|
private static readonly string LogDirectoryPath = RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|
||||||
|
? "/var/log/tranga-api"
|
||||||
|
: Path.Join(Directory.GetCurrentDirectory(), "logs");
|
||||||
public override Encoding Encoding { get; }
|
public override Encoding Encoding { get; }
|
||||||
public enum LoggerType
|
public enum LoggerType
|
||||||
{
|
{
|
||||||
@ -17,13 +21,14 @@ public class Logger : TextWriter
|
|||||||
|
|
||||||
public Logger(LoggerType[] enabledLoggers, TextWriter? stdOut, Encoding? encoding, string? logFilePath)
|
public Logger(LoggerType[] enabledLoggers, TextWriter? stdOut, Encoding? encoding, string? logFilePath)
|
||||||
{
|
{
|
||||||
this.Encoding = encoding ?? Encoding.ASCII;
|
this.Encoding = encoding ?? Encoding.UTF8;
|
||||||
if (enabledLoggers.Contains(LoggerType.FileLogger) && logFilePath is not null)
|
if (enabledLoggers.Contains(LoggerType.FileLogger) && logFilePath is not null)
|
||||||
_fileLogger = new FileLogger(logFilePath, encoding);
|
_fileLogger = new FileLogger(logFilePath, encoding);
|
||||||
else
|
else if(enabledLoggers.Contains(LoggerType.FileLogger) && logFilePath is null)
|
||||||
{
|
{
|
||||||
_fileLogger = null;
|
logFilePath = Path.Join(LogDirectoryPath,
|
||||||
throw new ArgumentException($"logFilePath can not be null for LoggerType {LoggerType.FileLogger}");
|
$"{DateTime.Now.ToShortDateString()} {DateTime.Now.ToShortTimeString()}.log");
|
||||||
|
_fileLogger = new FileLogger(logFilePath, encoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enabledLoggers.Contains(LoggerType.ConsoleLogger) && stdOut is not null)
|
if (enabledLoggers.Contains(LoggerType.ConsoleLogger) && stdOut is not null)
|
||||||
|
@ -6,6 +6,7 @@ public class MemoryLogger : LoggerBase
|
|||||||
{
|
{
|
||||||
private readonly SortedList<DateTime, LogMessage> _logMessages = new();
|
private readonly SortedList<DateTime, LogMessage> _logMessages = new();
|
||||||
private int _lastLogMessageIndex = 0;
|
private int _lastLogMessageIndex = 0;
|
||||||
|
private bool _lockLogMessages = false;
|
||||||
|
|
||||||
public MemoryLogger(Encoding? encoding = null) : base(encoding)
|
public MemoryLogger(Encoding? encoding = null) : base(encoding)
|
||||||
{
|
{
|
||||||
@ -14,8 +15,13 @@ public class MemoryLogger : LoggerBase
|
|||||||
|
|
||||||
protected override void Write(LogMessage value)
|
protected override void Write(LogMessage value)
|
||||||
{
|
{
|
||||||
while(!_logMessages.TryAdd(value.logTime, value))
|
if (!_lockLogMessages)
|
||||||
Thread.Sleep(10);
|
{
|
||||||
|
_lockLogMessages = true;
|
||||||
|
while(!_logMessages.TryAdd(DateTime.Now, value))
|
||||||
|
Thread.Sleep(10);
|
||||||
|
_lockLogMessages = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string[] GetLogMessage()
|
public string[] GetLogMessage()
|
||||||
@ -35,7 +41,12 @@ public class MemoryLogger : LoggerBase
|
|||||||
|
|
||||||
for (int retIndex = 0; retIndex < ret.Length; retIndex++)
|
for (int retIndex = 0; retIndex < ret.Length; retIndex++)
|
||||||
{
|
{
|
||||||
ret[retIndex] = _logMessages.GetValueAtIndex(_logMessages.Count - retLength + retIndex).ToString();
|
if (!_lockLogMessages)
|
||||||
|
{
|
||||||
|
_lockLogMessages = true;
|
||||||
|
ret[retIndex] = _logMessages.GetValueAtIndex(_logMessages.Count - retLength + retIndex).ToString();
|
||||||
|
_lockLogMessages = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_lastLogMessageIndex = _logMessages.Count - 1;
|
_lastLogMessageIndex = _logMessages.Count - 1;
|
||||||
@ -45,14 +56,27 @@ public class MemoryLogger : LoggerBase
|
|||||||
public string[] GetNewLines()
|
public string[] GetNewLines()
|
||||||
{
|
{
|
||||||
int logMessageCount = _logMessages.Count;
|
int logMessageCount = _logMessages.Count;
|
||||||
string[] ret = new string[logMessageCount - _lastLogMessageIndex];
|
List<string> ret = new();
|
||||||
|
|
||||||
for (int retIndex = 0; retIndex < ret.Length; retIndex++)
|
int retIndex = 0;
|
||||||
|
for (; retIndex < logMessageCount - _lastLogMessageIndex; retIndex++)
|
||||||
{
|
{
|
||||||
ret[retIndex] = _logMessages.GetValueAtIndex(_lastLogMessageIndex + retIndex).ToString();
|
try
|
||||||
|
{
|
||||||
|
if (!_lockLogMessages)
|
||||||
|
{
|
||||||
|
_lockLogMessages = true;
|
||||||
|
ret.Add(_logMessages.GetValueAtIndex(_lastLogMessageIndex + retIndex).ToString());
|
||||||
|
_lockLogMessages = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (NullReferenceException e)//Called when LogMessage has not finished writing
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_lastLogMessageIndex = logMessageCount;
|
_lastLogMessageIndex = _lastLogMessageIndex + retIndex;
|
||||||
return ret;
|
return ret.ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tranga", ".\Tranga\Tranga.c
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logging", "Logging\Logging.csproj", "{415BE889-BB7D-426F-976F-8D977876A462}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logging", "Logging\Logging.csproj", "{415BE889-BB7D-426F-976F-8D977876A462}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CLI", "CLI\CLI.csproj", "{4324C816-F9D2-468F-8ED6-397FE2F0DCB3}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -18,5 +20,9 @@ Global
|
|||||||
{415BE889-BB7D-426F-976F-8D977876A462}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{415BE889-BB7D-426F-976F-8D977876A462}.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.ActiveCfg = Release|Any CPU
|
||||||
{415BE889-BB7D-426F-976F-8D977876A462}.Release|Any CPU.Build.0 = Release|Any CPU
|
{415BE889-BB7D-426F-976F-8D977876A462}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{4324C816-F9D2-468F-8ED6-397FE2F0DCB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{4324C816-F9D2-468F-8ED6-397FE2F0DCB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{4324C816-F9D2-468F-8ED6-397FE2F0DCB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{4324C816-F9D2-468F-8ED6-397FE2F0DCB3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
@ -2,7 +2,9 @@
|
|||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=altnames/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=altnames/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=authorsartists/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=authorsartists/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Gotify/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Gotify/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=jjob/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Komga/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Komga/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=lunasea/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=mangakatana/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=mangakatana/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Manganato/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Manganato/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mangasee/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mangasee/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
@ -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<ValueTuple<HttpMethod, string, string[]>> _validRequestPaths = new()
|
|
||||||
{
|
|
||||||
new(HttpMethod.Get, "/", Array.Empty<string>()),
|
|
||||||
new(HttpMethod.Get, "/Connectors", Array.Empty<string>()),
|
|
||||||
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<string>()),
|
|
||||||
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<string>()),
|
|
||||||
new(HttpMethod.Get, "/Queue/List", Array.Empty<string>()),
|
|
||||||
new(HttpMethod.Post, "/Queue/Enqueue", new[] { "taskType", "connectorName?", "publicationId?" }),
|
|
||||||
new(HttpMethod.Delete, "/Queue/Dequeue", new[] { "taskType", "connectorName?", "publicationId?" }),
|
|
||||||
new(HttpMethod.Get, "/Settings", Array.Empty<string>()),
|
|
||||||
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<string, string> 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<string, string> GetRequestVariables(string query)
|
|
||||||
{
|
|
||||||
Dictionary<string, string> 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<string, string> 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<TrangaTask.Task>(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<TrangaTask.Task>(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<string, string> 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<Chapter> 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<TrangaTask.Task>(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<TrangaTask.Task>(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<string, string> 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<TrangaTask.Task>(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<TrangaTask.Task>(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<byte>());
|
|
||||||
response.OutputStream.Close();
|
|
||||||
}
|
|
||||||
catch (HttpListenerException)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -10,7 +10,7 @@ namespace Tranga;
|
|||||||
public readonly struct Chapter
|
public readonly struct Chapter
|
||||||
{
|
{
|
||||||
// ReSharper disable once MemberCanBePrivate.Global
|
// ReSharper disable once MemberCanBePrivate.Global
|
||||||
public Publication parentPublication { get; }
|
public Manga parentManga { get; }
|
||||||
public string? name { get; }
|
public string? name { get; }
|
||||||
public string? volumeNumber { get; }
|
public string? volumeNumber { get; }
|
||||||
public string chapterNumber { get; }
|
public string chapterNumber { get; }
|
||||||
@ -20,9 +20,9 @@ public readonly struct Chapter
|
|||||||
|
|
||||||
private static readonly Regex LegalCharacters = new (@"([A-z]*[0-9]* *\.*-*,*\]*\[*'*\'*\)*\(*~*!*)*");
|
private static readonly Regex LegalCharacters = new (@"([A-z]*[0-9]* *\.*-*,*\]*\[*'*\'*\)*\(*~*!*)*");
|
||||||
private static readonly Regex IllegalStrings = new(@"Vol(ume)?.?", RegexOptions.IgnoreCase);
|
private static readonly Regex IllegalStrings = new(@"Vol(ume)?.?", RegexOptions.IgnoreCase);
|
||||||
public Chapter(Publication parentPublication, string? name, string? volumeNumber, string chapterNumber, string url)
|
public Chapter(Manga parentManga, string? name, string? volumeNumber, string chapterNumber, string url)
|
||||||
{
|
{
|
||||||
this.parentPublication = parentPublication;
|
this.parentManga = parentManga;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.volumeNumber = volumeNumber;
|
this.volumeNumber = volumeNumber;
|
||||||
this.chapterNumber = chapterNumber;
|
this.chapterNumber = chapterNumber;
|
||||||
@ -35,8 +35,12 @@ public readonly struct Chapter
|
|||||||
chNameStr = IllegalStrings.Replace(chNameStr, "");
|
chNameStr = IllegalStrings.Replace(chNameStr, "");
|
||||||
this.fileName = $"{volStr}{chNumberStr}{chNameStr}";
|
this.fileName = $"{volStr}{chNumberStr}{chNameStr}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"Chapter {parentManga.sortName} {parentManga.internalId} {chapterNumber} {name}";
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if a chapter-archive is already present
|
/// Checks if a chapter-archive is already present
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -44,9 +48,9 @@ public readonly struct Chapter
|
|||||||
internal bool CheckChapterIsDownloaded(string downloadLocation)
|
internal bool CheckChapterIsDownloaded(string downloadLocation)
|
||||||
{
|
{
|
||||||
string newFilePath = GetArchiveFilePath(downloadLocation);
|
string newFilePath = GetArchiveFilePath(downloadLocation);
|
||||||
if (!Directory.Exists(Path.Join(downloadLocation, parentPublication.folderName)))
|
if (!Directory.Exists(Path.Join(downloadLocation, parentManga.folderName)))
|
||||||
return false;
|
return false;
|
||||||
FileInfo[] archives = new DirectoryInfo(Path.Join(downloadLocation, parentPublication.folderName)).GetFiles();
|
FileInfo[] archives = new DirectoryInfo(Path.Join(downloadLocation, parentManga.folderName)).GetFiles();
|
||||||
Regex chapterInfoRex = new(@"Ch\.[0-9.]+");
|
Regex chapterInfoRex = new(@"Ch\.[0-9.]+");
|
||||||
Regex chapterRex = new(@"[0-9]+(\.[0-9]+)?");
|
Regex chapterRex = new(@"[0-9]+(\.[0-9]+)?");
|
||||||
|
|
||||||
@ -67,7 +71,7 @@ public readonly struct Chapter
|
|||||||
/// <returns>Filepath</returns>
|
/// <returns>Filepath</returns>
|
||||||
internal string GetArchiveFilePath(string downloadLocation)
|
internal string GetArchiveFilePath(string downloadLocation)
|
||||||
{
|
{
|
||||||
return Path.Join(downloadLocation, parentPublication.folderName, $"{parentPublication.folderName} - {this.fileName}.cbz");
|
return Path.Join(downloadLocation, parentManga.folderName, $"{parentManga.folderName} - {this.fileName}.cbz");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -78,10 +82,10 @@ public readonly struct Chapter
|
|||||||
internal string GetComicInfoXmlString()
|
internal string GetComicInfoXmlString()
|
||||||
{
|
{
|
||||||
XElement comicInfo = new XElement("ComicInfo",
|
XElement comicInfo = new XElement("ComicInfo",
|
||||||
new XElement("Tags", string.Join(',', parentPublication.tags)),
|
new XElement("Tags", string.Join(',', parentManga.tags)),
|
||||||
new XElement("LanguageISO", parentPublication.originalLanguage),
|
new XElement("LanguageISO", parentManga.originalLanguage),
|
||||||
new XElement("Title", this.name),
|
new XElement("Title", this.name),
|
||||||
new XElement("Writer", string.Join(',', parentPublication.authors)),
|
new XElement("Writer", string.Join(',', parentManga.authors)),
|
||||||
new XElement("Volume", this.volumeNumber),
|
new XElement("Volume", this.volumeNumber),
|
||||||
new XElement("Number", this.chapterNumber)
|
new XElement("Number", this.chapterNumber)
|
||||||
);
|
);
|
||||||
|
@ -1,125 +0,0 @@
|
|||||||
using Logging;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Tranga.LibraryManagers;
|
|
||||||
using Tranga.NotificationManagers;
|
|
||||||
|
|
||||||
namespace Tranga;
|
|
||||||
|
|
||||||
public class CommonObjects
|
|
||||||
{
|
|
||||||
public HashSet<LibraryManager> libraryManagers { get; init; }
|
|
||||||
public HashSet<NotificationManager> notificationManagers { get; init; }
|
|
||||||
[JsonIgnore]public Logger? logger { get; set; }
|
|
||||||
[JsonIgnore]private string settingsFilePath { get; init; }
|
|
||||||
|
|
||||||
public CommonObjects(HashSet<LibraryManager>? libraryManagers, HashSet<NotificationManager>? 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<TrangaSettings.SettingsJsonObject>(
|
|
||||||
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<TrangaSettings.SettingsJsonObject>(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();
|
|
||||||
}
|
|
||||||
}
|
|
106
Tranga/GlobalBase.cs
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
using Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Tranga.LibraryConnectors;
|
||||||
|
using Tranga.NotificationConnectors;
|
||||||
|
|
||||||
|
namespace Tranga;
|
||||||
|
|
||||||
|
public abstract class GlobalBase
|
||||||
|
{
|
||||||
|
protected Logger? logger { get; init; }
|
||||||
|
protected TrangaSettings settings { get; init; }
|
||||||
|
protected HashSet<NotificationConnector> notificationConnectors { get; init; }
|
||||||
|
protected HashSet<LibraryConnector> libraryConnectors { get; init; }
|
||||||
|
protected List<Manga> cachedPublications { get; init; }
|
||||||
|
|
||||||
|
protected GlobalBase(GlobalBase clone)
|
||||||
|
{
|
||||||
|
this.logger = clone.logger;
|
||||||
|
this.settings = clone.settings;
|
||||||
|
this.notificationConnectors = clone.notificationConnectors;
|
||||||
|
this.libraryConnectors = clone.libraryConnectors;
|
||||||
|
this.cachedPublications = clone.cachedPublications;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected GlobalBase(Logger? logger, TrangaSettings settings)
|
||||||
|
{
|
||||||
|
this.logger = logger;
|
||||||
|
this.settings = settings;
|
||||||
|
this.notificationConnectors = settings.LoadNotificationConnectors(this);
|
||||||
|
this.libraryConnectors = settings.LoadLibraryConnectors(this);
|
||||||
|
this.cachedPublications = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void Log(string message)
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().Name, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void Log(string fStr, params object?[] replace)
|
||||||
|
{
|
||||||
|
Log(string.Format(fStr, replace));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void SendNotifications(string title, string text)
|
||||||
|
{
|
||||||
|
foreach (NotificationConnector nc in notificationConnectors)
|
||||||
|
nc.SendNotification(title, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void AddNotificationConnector(NotificationConnector notificationConnector)
|
||||||
|
{
|
||||||
|
Log($"Adding {notificationConnector}");
|
||||||
|
notificationConnectors.RemoveWhere(nc => nc.GetType() == notificationConnector.GetType());
|
||||||
|
notificationConnectors.Add(notificationConnector);
|
||||||
|
|
||||||
|
while(IsFileInUse(settings.notificationConnectorsFilePath))
|
||||||
|
Thread.Sleep(100);
|
||||||
|
File.WriteAllText(settings.notificationConnectorsFilePath, JsonConvert.SerializeObject(notificationConnectors));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void DeleteNotificationConnector(NotificationConnector.NotificationConnectorType notificationConnectorType)
|
||||||
|
{
|
||||||
|
Log($"Removing {notificationConnectorType}");
|
||||||
|
notificationConnectors.RemoveWhere(nc => nc.notificationConnectorType == notificationConnectorType);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void UpdateLibraries()
|
||||||
|
{
|
||||||
|
foreach(LibraryConnector lc in libraryConnectors)
|
||||||
|
lc.UpdateLibrary();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void AddLibraryConnector(LibraryConnector libraryConnector)
|
||||||
|
{
|
||||||
|
Log($"Adding {libraryConnector}");
|
||||||
|
libraryConnectors.RemoveWhere(lc => lc.GetType() == libraryConnector.GetType());
|
||||||
|
libraryConnectors.Add(libraryConnector);
|
||||||
|
|
||||||
|
while(IsFileInUse(settings.libraryConnectorsFilePath))
|
||||||
|
Thread.Sleep(100);
|
||||||
|
File.WriteAllText(settings.libraryConnectorsFilePath, JsonConvert.SerializeObject(libraryConnectors));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void DeleteLibraryConnector(LibraryConnector.LibraryType libraryType)
|
||||||
|
{
|
||||||
|
Log($"Removing {libraryType}");
|
||||||
|
libraryConnectors.RemoveWhere(lc => lc.libraryType == libraryType);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected bool IsFileInUse(string filePath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using FileStream stream = new (filePath, FileMode.Open, FileAccess.Read, FileShare.None);
|
||||||
|
stream.Close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
Log($"File is in use {filePath}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
Tranga/Jobs/DownloadChapter.cs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Tranga.MangaConnectors;
|
||||||
|
|
||||||
|
namespace Tranga.Jobs;
|
||||||
|
|
||||||
|
public class DownloadChapter : Job
|
||||||
|
{
|
||||||
|
public Chapter chapter { get; init; }
|
||||||
|
|
||||||
|
public DownloadChapter(GlobalBase clone, MangaConnector connector, Chapter chapter, DateTime lastExecution, string? parentJobId = null) : base(clone, connector, lastExecution, parentJobId: parentJobId)
|
||||||
|
{
|
||||||
|
this.chapter = chapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DownloadChapter(GlobalBase clone, MangaConnector connector, Chapter chapter, string? parentJobId = null) : base(clone, connector, parentJobId: parentJobId)
|
||||||
|
{
|
||||||
|
this.chapter = chapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string GetId()
|
||||||
|
{
|
||||||
|
return Convert.ToBase64String(Encoding.ASCII.GetBytes(string.Concat(this.GetType().ToString(), chapter.parentManga.internalId, chapter.chapterNumber)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"DownloadChapter {id} {chapter}";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IEnumerable<Job> ExecuteReturnSubTasksInternal()
|
||||||
|
{
|
||||||
|
Task downloadTask = new(delegate
|
||||||
|
{
|
||||||
|
mangaConnector.DownloadChapter(chapter, this.progressToken);
|
||||||
|
UpdateLibraries();
|
||||||
|
SendNotifications("Chapter downloaded", $"{chapter.parentManga.sortName} - {chapter.chapterNumber}");
|
||||||
|
});
|
||||||
|
downloadTask.Start();
|
||||||
|
return Array.Empty<Job>();
|
||||||
|
}
|
||||||
|
}
|
45
Tranga/Jobs/DownloadNewChapters.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Tranga.MangaConnectors;
|
||||||
|
|
||||||
|
namespace Tranga.Jobs;
|
||||||
|
|
||||||
|
public class DownloadNewChapters : Job
|
||||||
|
{
|
||||||
|
public Manga manga { get; init; }
|
||||||
|
|
||||||
|
public DownloadNewChapters(GlobalBase clone, MangaConnector connector, Manga manga, DateTime lastExecution,
|
||||||
|
bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null) : base(clone, connector, lastExecution, recurring,
|
||||||
|
recurrence, parentJobId)
|
||||||
|
{
|
||||||
|
this.manga = manga;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DownloadNewChapters(GlobalBase clone, MangaConnector connector, Manga manga, bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null) : base (clone, connector, recurring, recurrence, parentJobId)
|
||||||
|
{
|
||||||
|
this.manga = manga;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string GetId()
|
||||||
|
{
|
||||||
|
return Convert.ToBase64String(Encoding.ASCII.GetBytes(string.Concat(this.GetType().ToString(), manga.internalId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"DownloadChapter {id} {manga}";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IEnumerable<Job> ExecuteReturnSubTasksInternal()
|
||||||
|
{
|
||||||
|
Chapter[] chapters = mangaConnector.GetNewChapters(manga);
|
||||||
|
this.progressToken.increments = chapters.Length;
|
||||||
|
List<Job> jobs = new();
|
||||||
|
foreach (Chapter chapter in chapters)
|
||||||
|
{
|
||||||
|
DownloadChapter downloadChapterJob = new(this, this.mangaConnector, chapter, parentJobId: this.id);
|
||||||
|
jobs.Add(downloadChapterJob);
|
||||||
|
}
|
||||||
|
progressToken.Complete();
|
||||||
|
return jobs;
|
||||||
|
}
|
||||||
|
}
|
93
Tranga/Jobs/Job.cs
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
using Tranga.MangaConnectors;
|
||||||
|
|
||||||
|
namespace Tranga.Jobs;
|
||||||
|
|
||||||
|
public abstract class Job : GlobalBase
|
||||||
|
{
|
||||||
|
public MangaConnector mangaConnector { get; init; }
|
||||||
|
public ProgressToken progressToken { get; private set; }
|
||||||
|
public bool recurring { get; init; }
|
||||||
|
public TimeSpan? recurrenceTime { get; set; }
|
||||||
|
public DateTime? lastExecution { get; private set; }
|
||||||
|
public DateTime nextExecution => NextExecution();
|
||||||
|
public string id => GetId();
|
||||||
|
internal IEnumerable<Job>? subJobs { get; private set; }
|
||||||
|
public string? parentJobId { get; init; }
|
||||||
|
|
||||||
|
internal Job(GlobalBase clone, MangaConnector connector, bool recurring = false, TimeSpan? recurrenceTime = null, string? parentJobId = null) : base(clone)
|
||||||
|
{
|
||||||
|
this.mangaConnector = connector;
|
||||||
|
this.progressToken = new ProgressToken(0);
|
||||||
|
this.recurring = recurring;
|
||||||
|
if (recurring && recurrenceTime is null)
|
||||||
|
throw new ArgumentException("If recurrence is set to true, a recurrence time has to be provided.");
|
||||||
|
else if(recurring && recurrenceTime is not null)
|
||||||
|
this.lastExecution = DateTime.Now.Subtract((TimeSpan)recurrenceTime);
|
||||||
|
this.recurrenceTime = recurrenceTime ?? TimeSpan.Zero;
|
||||||
|
this.parentJobId = parentJobId;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Job(GlobalBase clone, MangaConnector connector, DateTime lastExecution, bool recurring = false,
|
||||||
|
TimeSpan? recurrenceTime = null, string? parentJobId = null) : base(clone)
|
||||||
|
{
|
||||||
|
this.mangaConnector = connector;
|
||||||
|
this.progressToken = new ProgressToken(0);
|
||||||
|
this.recurring = recurring;
|
||||||
|
if (recurring && recurrenceTime is null)
|
||||||
|
throw new ArgumentException("If recurrence is set to true, a recurrence time has to be provided.");
|
||||||
|
this.lastExecution = lastExecution;
|
||||||
|
this.recurrenceTime = recurrenceTime ?? TimeSpan.Zero;
|
||||||
|
this.parentJobId = parentJobId;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract string GetId();
|
||||||
|
|
||||||
|
public void AddSubJob(Job job)
|
||||||
|
{
|
||||||
|
subJobs ??= new List<Job>();
|
||||||
|
subJobs = subJobs.Append(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DateTime NextExecution()
|
||||||
|
{
|
||||||
|
if(recurrenceTime.HasValue && lastExecution.HasValue)
|
||||||
|
return lastExecution.Value.Add(recurrenceTime.Value);
|
||||||
|
if(recurrenceTime.HasValue && !lastExecution.HasValue)
|
||||||
|
return DateTime.Now;
|
||||||
|
return DateTime.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetProgress()
|
||||||
|
{
|
||||||
|
this.progressToken.increments = this.progressToken.increments - this.progressToken.incrementsCompleted;
|
||||||
|
this.lastExecution = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ExecutionEnqueue()
|
||||||
|
{
|
||||||
|
this.progressToken.increments = this.progressToken.increments - this.progressToken.incrementsCompleted;
|
||||||
|
this.lastExecution = recurrenceTime is not null ? DateTime.Now.Subtract((TimeSpan)recurrenceTime) : DateTime.UnixEpoch;
|
||||||
|
this.progressToken.Standby();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cancel()
|
||||||
|
{
|
||||||
|
Log($"Cancelling {this}");
|
||||||
|
this.progressToken.cancellationRequested = true;
|
||||||
|
this.progressToken.Cancel();
|
||||||
|
this.lastExecution = DateTime.Now;
|
||||||
|
if(subJobs is not null)
|
||||||
|
foreach(Job subJob in subJobs)
|
||||||
|
subJob.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<Job> ExecuteReturnSubTasks()
|
||||||
|
{
|
||||||
|
progressToken.Start();
|
||||||
|
subJobs = ExecuteReturnSubTasksInternal();
|
||||||
|
lastExecution = DateTime.Now;
|
||||||
|
return subJobs;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract IEnumerable<Job> ExecuteReturnSubTasksInternal();
|
||||||
|
}
|
189
Tranga/Jobs/JobBoss.cs
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using Tranga.MangaConnectors;
|
||||||
|
|
||||||
|
namespace Tranga.Jobs;
|
||||||
|
|
||||||
|
public class JobBoss : GlobalBase
|
||||||
|
{
|
||||||
|
public HashSet<Job> jobs { get; init; }
|
||||||
|
private Dictionary<MangaConnector, Queue<Job>> mangaConnectorJobQueue { get; init; }
|
||||||
|
|
||||||
|
public JobBoss(GlobalBase clone, HashSet<MangaConnector> connectors) : base(clone)
|
||||||
|
{
|
||||||
|
if (File.Exists(settings.jobsFilePath))
|
||||||
|
{
|
||||||
|
this.jobs = JsonConvert.DeserializeObject<HashSet<Job>>(File.ReadAllText(settings.jobsFilePath), new JobJsonConverter(this, new MangaConnectorJsonConverter(this, connectors)))!;
|
||||||
|
foreach (Job job in this.jobs)
|
||||||
|
this.jobs.FirstOrDefault(jjob => jjob.id == job.parentJobId)?.AddSubJob(job);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
this.jobs = new();
|
||||||
|
foreach (DownloadNewChapters ncJob in this.jobs.Where(job => job is DownloadNewChapters))
|
||||||
|
cachedPublications.Add(ncJob.manga);
|
||||||
|
this.mangaConnectorJobQueue = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddJob(Job job)
|
||||||
|
{
|
||||||
|
if (ContainsJobLike(job))
|
||||||
|
{
|
||||||
|
Log($"Already Contains Job {job}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log($"Added {job}");
|
||||||
|
this.jobs.Add(job);
|
||||||
|
ExportJobsList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddJobs(IEnumerable<Job> jobsToAdd)
|
||||||
|
{
|
||||||
|
foreach (Job job in jobsToAdd)
|
||||||
|
AddJob(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ContainsJobLike(Job job)
|
||||||
|
{
|
||||||
|
if (job is DownloadChapter dcJob)
|
||||||
|
{
|
||||||
|
return this.GetJobsLike(dcJob.mangaConnector, chapter: dcJob.chapter).Any();
|
||||||
|
}else if (job is DownloadNewChapters ncJob)
|
||||||
|
{
|
||||||
|
return this.GetJobsLike(ncJob.mangaConnector, ncJob.manga).Any();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveJob(Job job)
|
||||||
|
{
|
||||||
|
Log($"Removing {job}");
|
||||||
|
job.Cancel();
|
||||||
|
this.jobs.Remove(job);
|
||||||
|
if(job.subJobs is not null)
|
||||||
|
RemoveJobs(job.subJobs);
|
||||||
|
ExportJobsList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveJobs(IEnumerable<Job?> jobsToRemove)
|
||||||
|
{
|
||||||
|
Log($"Removing {jobsToRemove.Count()} jobs.");
|
||||||
|
foreach (Job? job in jobsToRemove)
|
||||||
|
if(job is not null)
|
||||||
|
RemoveJob(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<Job> GetJobsLike(string? connectorName = null, string? internalId = null, string? chapterNumber = null)
|
||||||
|
{
|
||||||
|
IEnumerable<Job> ret = this.jobs;
|
||||||
|
if (connectorName is not null)
|
||||||
|
ret = ret.Where(job => job.mangaConnector.name == connectorName);
|
||||||
|
|
||||||
|
if (internalId is not null && chapterNumber is not null)
|
||||||
|
ret = ret.Where(jjob =>
|
||||||
|
{
|
||||||
|
if (jjob is not DownloadChapter job)
|
||||||
|
return false;
|
||||||
|
return job.chapter.parentManga.internalId == internalId &&
|
||||||
|
job.chapter.chapterNumber == chapterNumber;
|
||||||
|
});
|
||||||
|
else if (internalId is not null)
|
||||||
|
ret = ret.Where(jjob =>
|
||||||
|
{
|
||||||
|
if (jjob is not DownloadNewChapters job)
|
||||||
|
return false;
|
||||||
|
return job.manga.internalId == internalId;
|
||||||
|
});
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<Job> GetJobsLike(MangaConnector? mangaConnector = null, Manga? publication = null,
|
||||||
|
Chapter? chapter = null)
|
||||||
|
{
|
||||||
|
if (chapter is not null)
|
||||||
|
return GetJobsLike(mangaConnector?.name, chapter.Value.parentManga.internalId, chapter?.chapterNumber);
|
||||||
|
else
|
||||||
|
return GetJobsLike(mangaConnector?.name, publication?.internalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Job? GetJobById(string jobId)
|
||||||
|
{
|
||||||
|
if (this.jobs.FirstOrDefault(jjob => jjob.id == jobId) is { } job)
|
||||||
|
return job;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetJobById(string jobId, out Job? job)
|
||||||
|
{
|
||||||
|
if (this.jobs.FirstOrDefault(jjob => jjob.id == jobId) is { } ret)
|
||||||
|
{
|
||||||
|
job = ret;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
job = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool QueueContainsJob(Job job)
|
||||||
|
{
|
||||||
|
mangaConnectorJobQueue.TryAdd(job.mangaConnector, new Queue<Job>());
|
||||||
|
return mangaConnectorJobQueue[job.mangaConnector].Contains(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddJobToQueue(Job job)
|
||||||
|
{
|
||||||
|
Log($"Adding Job to Queue. {job}");
|
||||||
|
mangaConnectorJobQueue.TryAdd(job.mangaConnector, new Queue<Job>());
|
||||||
|
Queue<Job> connectorJobQueue = mangaConnectorJobQueue[job.mangaConnector];
|
||||||
|
if(!connectorJobQueue.Contains(job))
|
||||||
|
connectorJobQueue.Enqueue(job);
|
||||||
|
job.ExecutionEnqueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddJobsToQueue(IEnumerable<Job> jobs)
|
||||||
|
{
|
||||||
|
foreach(Job job in jobs)
|
||||||
|
AddJobToQueue(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ExportJobsList()
|
||||||
|
{
|
||||||
|
Log($"Exporting {settings.jobsFilePath}");
|
||||||
|
while(IsFileInUse(settings.jobsFilePath))
|
||||||
|
Thread.Sleep(10);
|
||||||
|
File.WriteAllText(settings.jobsFilePath, JsonConvert.SerializeObject(this.jobs));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CheckJobs()
|
||||||
|
{
|
||||||
|
foreach (Job job in jobs.Where(job => job.nextExecution < DateTime.Now && !QueueContainsJob(job)).OrderBy(job => job.nextExecution))
|
||||||
|
AddJobToQueue(job);
|
||||||
|
foreach (Queue<Job> jobQueue in mangaConnectorJobQueue.Values)
|
||||||
|
{
|
||||||
|
if(jobQueue.Count < 1)
|
||||||
|
continue;
|
||||||
|
Job queueHead = jobQueue.Peek();
|
||||||
|
if (queueHead.progressToken.state is ProgressToken.State.Complete or ProgressToken.State.Cancelled)
|
||||||
|
{
|
||||||
|
switch (queueHead)
|
||||||
|
{
|
||||||
|
case DownloadChapter:
|
||||||
|
RemoveJob(queueHead);
|
||||||
|
break;
|
||||||
|
case DownloadNewChapters:
|
||||||
|
if(queueHead.recurring)
|
||||||
|
queueHead.progressToken.Complete();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
jobQueue.Dequeue();
|
||||||
|
}else if (queueHead.progressToken.state is ProgressToken.State.Standby)
|
||||||
|
{
|
||||||
|
Job[] subJobs = jobQueue.Peek().ExecuteReturnSubTasks().ToArray();
|
||||||
|
AddJobs(subJobs);
|
||||||
|
AddJobsToQueue(subJobs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
70
Tranga/Jobs/JobJsonConverter.cs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Tranga.MangaConnectors;
|
||||||
|
|
||||||
|
namespace Tranga.Jobs;
|
||||||
|
|
||||||
|
public class JobJsonConverter : JsonConverter
|
||||||
|
{
|
||||||
|
private GlobalBase _clone;
|
||||||
|
private MangaConnectorJsonConverter _mangaConnectorJsonConverter;
|
||||||
|
|
||||||
|
internal JobJsonConverter(GlobalBase clone, MangaConnectorJsonConverter mangaConnectorJsonConverter)
|
||||||
|
{
|
||||||
|
this._clone = clone;
|
||||||
|
this._mangaConnectorJsonConverter = mangaConnectorJsonConverter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanConvert(Type objectType)
|
||||||
|
{
|
||||||
|
return (objectType == typeof(Job));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
JObject jo = JObject.Load(reader);
|
||||||
|
if (jo.ContainsKey("manga"))//DownloadNewChapters
|
||||||
|
{
|
||||||
|
return new DownloadNewChapters(this._clone,
|
||||||
|
jo.GetValue("mangaConnector")!.ToObject<MangaConnector>(JsonSerializer.Create(new JsonSerializerSettings()
|
||||||
|
{
|
||||||
|
Converters =
|
||||||
|
{
|
||||||
|
this._mangaConnectorJsonConverter
|
||||||
|
}
|
||||||
|
}))!,
|
||||||
|
jo.GetValue("manga")!.ToObject<Manga>(),
|
||||||
|
jo.GetValue("lastExecution")!.ToObject<DateTime>(),
|
||||||
|
jo.GetValue("recurring")!.Value<bool>(),
|
||||||
|
jo.GetValue("recurrenceTime")!.ToObject<TimeSpan?>(),
|
||||||
|
jo.GetValue("parentJobId")!.Value<string?>());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jo.ContainsKey("chapter"))//DownloadChapter
|
||||||
|
{
|
||||||
|
return new DownloadChapter(this._clone,
|
||||||
|
jo.GetValue("mangaConnector")!.ToObject<MangaConnector>(JsonSerializer.Create(new JsonSerializerSettings()
|
||||||
|
{
|
||||||
|
Converters =
|
||||||
|
{
|
||||||
|
this._mangaConnectorJsonConverter
|
||||||
|
}
|
||||||
|
}))!,
|
||||||
|
jo.GetValue("chapter")!.ToObject<Chapter>(),
|
||||||
|
DateTime.UnixEpoch,
|
||||||
|
jo.GetValue("parentJobId")!.Value<string?>());
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanWrite => false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Don't call this
|
||||||
|
/// </summary>
|
||||||
|
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
throw new Exception("Dont call this");
|
||||||
|
}
|
||||||
|
}
|
54
Tranga/Jobs/ProgressToken.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
namespace Tranga.Jobs;
|
||||||
|
|
||||||
|
public class ProgressToken
|
||||||
|
{
|
||||||
|
public bool cancellationRequested { get; set; }
|
||||||
|
public int increments { get; set; }
|
||||||
|
public int incrementsCompleted { get; set; }
|
||||||
|
public float progress => GetProgress();
|
||||||
|
|
||||||
|
public enum State { Running, Complete, Standby, Cancelled }
|
||||||
|
public State state { get; private set; }
|
||||||
|
|
||||||
|
public ProgressToken(int increments)
|
||||||
|
{
|
||||||
|
this.cancellationRequested = false;
|
||||||
|
this.increments = increments;
|
||||||
|
this.incrementsCompleted = 0;
|
||||||
|
this.state = State.Complete;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float GetProgress()
|
||||||
|
{
|
||||||
|
if(increments > 0 && incrementsCompleted > 0)
|
||||||
|
return (float)incrementsCompleted / (float)increments;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Increment()
|
||||||
|
{
|
||||||
|
this.incrementsCompleted++;
|
||||||
|
if (incrementsCompleted > increments)
|
||||||
|
state = State.Complete;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Standby()
|
||||||
|
{
|
||||||
|
state = State.Standby;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
state = State.Running;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Complete()
|
||||||
|
{
|
||||||
|
state = State.Complete;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cancel()
|
||||||
|
{
|
||||||
|
state = State.Cancelled;
|
||||||
|
}
|
||||||
|
}
|
@ -1,22 +1,27 @@
|
|||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using Logging;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using JsonSerializer = System.Text.Json.JsonSerializer;
|
using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||||
|
|
||||||
namespace Tranga.LibraryManagers;
|
namespace Tranga.LibraryConnectors;
|
||||||
|
|
||||||
public class Kavita : LibraryManager
|
public class Kavita : LibraryConnector
|
||||||
{
|
{
|
||||||
|
|
||||||
public Kavita(string baseUrl, string username, string password, Logger? logger) : base(baseUrl, GetToken(baseUrl, username, password), logger, LibraryType.Kavita)
|
public Kavita(GlobalBase clone, string baseUrl, string username, string password) :
|
||||||
|
base(clone, baseUrl, GetToken(baseUrl, username, password), LibraryType.Kavita)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonConstructor]
|
[JsonConstructor]
|
||||||
public Kavita(string baseUrl, string auth, Logger? logger) : base(baseUrl, auth, logger, LibraryType.Kavita)
|
public Kavita(GlobalBase clone, string baseUrl, string auth) : base(clone, baseUrl, auth, LibraryType.Kavita)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"Kavita {baseUrl}";
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetToken(string baseUrl, string username, string password)
|
private static string GetToken(string baseUrl, string username, string password)
|
||||||
{
|
{
|
||||||
HttpClient client = new()
|
HttpClient client = new()
|
||||||
@ -37,12 +42,12 @@ public class Kavita : LibraryManager
|
|||||||
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(response.Content.ReadAsStream());
|
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(response.Content.ReadAsStream());
|
||||||
if (result is not null)
|
if (result is not null)
|
||||||
return result["token"]!.GetValue<string>();
|
return result["token"]!.GetValue<string>();
|
||||||
else return "";
|
else throw new Exception("Did not receive token.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void UpdateLibrary()
|
public override void UpdateLibrary()
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Updating Libraries");
|
Log("Updating libraries.");
|
||||||
foreach (KavitaLibrary lib in GetLibraries())
|
foreach (KavitaLibrary lib in GetLibraries())
|
||||||
NetClient.MakePost($"{baseUrl}/api/Library/scan?libraryId={lib.id}", "Bearer", auth, logger);
|
NetClient.MakePost($"{baseUrl}/api/Library/scan?libraryId={lib.id}", "Bearer", auth, logger);
|
||||||
}
|
}
|
||||||
@ -53,17 +58,17 @@ public class Kavita : LibraryManager
|
|||||||
/// <returns>Array of KavitaLibrary</returns>
|
/// <returns>Array of KavitaLibrary</returns>
|
||||||
private IEnumerable<KavitaLibrary> GetLibraries()
|
private IEnumerable<KavitaLibrary> GetLibraries()
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Getting Libraries");
|
Log("Getting libraries.");
|
||||||
Stream data = NetClient.MakeRequest($"{baseUrl}/api/Library", "Bearer", auth, logger);
|
Stream data = NetClient.MakeRequest($"{baseUrl}/api/Library", "Bearer", auth, logger);
|
||||||
if (data == Stream.Null)
|
if (data == Stream.Null)
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"No libraries returned");
|
Log("No libraries returned");
|
||||||
return Array.Empty<KavitaLibrary>();
|
return Array.Empty<KavitaLibrary>();
|
||||||
}
|
}
|
||||||
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
|
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
|
||||||
if (result is null)
|
if (result is null)
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"No libraries returned");
|
Log("No libraries returned");
|
||||||
return Array.Empty<KavitaLibrary>();
|
return Array.Empty<KavitaLibrary>();
|
||||||
}
|
}
|
||||||
|
|
@ -1,29 +1,33 @@
|
|||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using Logging;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using JsonSerializer = System.Text.Json.JsonSerializer;
|
using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||||
|
|
||||||
namespace Tranga.LibraryManagers;
|
namespace Tranga.LibraryConnectors;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides connectivity to Komga-API
|
/// Provides connectivity to Komga-API
|
||||||
/// Can fetch and update libraries
|
/// Can fetch and update libraries
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Komga : LibraryManager
|
public class Komga : LibraryConnector
|
||||||
{
|
{
|
||||||
public Komga(string baseUrl, string username, string password, Logger? logger)
|
public Komga(GlobalBase clone, string baseUrl, string username, string password)
|
||||||
: base(baseUrl, Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}")), logger, LibraryType.Komga)
|
: base(clone, baseUrl, Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}")), LibraryType.Komga)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonConstructor]
|
[JsonConstructor]
|
||||||
public Komga(string baseUrl, string auth, Logger? logger) : base(baseUrl, auth, logger, LibraryType.Komga)
|
public Komga(GlobalBase clone, string baseUrl, string auth) : base(clone, baseUrl, auth, LibraryType.Komga)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"Komga {baseUrl}";
|
||||||
|
}
|
||||||
|
|
||||||
public override void UpdateLibrary()
|
public override void UpdateLibrary()
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Updating Libraries");
|
Log("Updating libraries.");
|
||||||
foreach (KomgaLibrary lib in GetLibraries())
|
foreach (KomgaLibrary lib in GetLibraries())
|
||||||
NetClient.MakePost($"{baseUrl}/api/v1/libraries/{lib.id}/scan", "Basic", auth, logger);
|
NetClient.MakePost($"{baseUrl}/api/v1/libraries/{lib.id}/scan", "Basic", auth, logger);
|
||||||
}
|
}
|
||||||
@ -34,17 +38,17 @@ public class Komga : LibraryManager
|
|||||||
/// <returns>Array of KomgaLibraries</returns>
|
/// <returns>Array of KomgaLibraries</returns>
|
||||||
private IEnumerable<KomgaLibrary> GetLibraries()
|
private IEnumerable<KomgaLibrary> GetLibraries()
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Getting Libraries");
|
Log("Getting Libraries");
|
||||||
Stream data = NetClient.MakeRequest($"{baseUrl}/api/v1/libraries", "Basic", auth, logger);
|
Stream data = NetClient.MakeRequest($"{baseUrl}/api/v1/libraries", "Basic", auth, logger);
|
||||||
if (data == Stream.Null)
|
if (data == Stream.Null)
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"No libraries returned");
|
Log("No libraries returned");
|
||||||
return Array.Empty<KomgaLibrary>();
|
return Array.Empty<KomgaLibrary>();
|
||||||
}
|
}
|
||||||
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
|
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
|
||||||
if (result is null)
|
if (result is null)
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"No libraries returned");
|
Log("No libraries returned");
|
||||||
return Array.Empty<KomgaLibrary>();
|
return Array.Empty<KomgaLibrary>();
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,10 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using Logging;
|
using Logging;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace Tranga.LibraryManagers;
|
namespace Tranga.LibraryConnectors;
|
||||||
|
|
||||||
public abstract class LibraryManager
|
public abstract class LibraryConnector : GlobalBase
|
||||||
{
|
{
|
||||||
public enum LibraryType : byte
|
public enum LibraryType : byte
|
||||||
{
|
{
|
||||||
@ -19,26 +17,15 @@ public abstract class LibraryManager
|
|||||||
public string baseUrl { get; }
|
public string baseUrl { get; }
|
||||||
// ReSharper disable once MemberCanBeProtected.Global
|
// ReSharper disable once MemberCanBeProtected.Global
|
||||||
public string auth { get; } //Base64 encoded, if you use your password everywhere, you have problems
|
public string auth { get; } //Base64 encoded, if you use your password everywhere, you have problems
|
||||||
protected Logger? logger;
|
|
||||||
|
|
||||||
/// <param name="baseUrl">Base-URL of Komga instance, no trailing slashes(/)</param>
|
protected LibraryConnector(GlobalBase clone, string baseUrl, string auth, LibraryType libraryType) : base(clone)
|
||||||
/// <param name="auth">Base64 string of username and password (username):(password)</param>
|
|
||||||
/// <param name="logger"></param>
|
|
||||||
/// <param name="libraryType"></param>
|
|
||||||
protected LibraryManager(string baseUrl, string auth, Logger? logger, LibraryType libraryType)
|
|
||||||
{
|
{
|
||||||
this.baseUrl = baseUrl;
|
this.baseUrl = baseUrl;
|
||||||
this.auth = auth;
|
this.auth = auth;
|
||||||
this.logger = logger;
|
|
||||||
this.libraryType = libraryType;
|
this.libraryType = libraryType;
|
||||||
}
|
}
|
||||||
public abstract void UpdateLibrary();
|
public abstract void UpdateLibrary();
|
||||||
|
|
||||||
public void AddLogger(Logger newLogger)
|
|
||||||
{
|
|
||||||
this.logger = newLogger;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static class NetClient
|
protected static class NetClient
|
||||||
{
|
{
|
||||||
public static Stream MakeRequest(string url, string authScheme, string auth, Logger? logger)
|
public static Stream MakeRequest(string url, string authScheme, string auth, Logger? logger)
|
||||||
@ -52,7 +39,7 @@ public abstract class LibraryManager
|
|||||||
RequestUri = new Uri(url)
|
RequestUri = new Uri(url)
|
||||||
};
|
};
|
||||||
HttpResponseMessage response = client.Send(requestMessage);
|
HttpResponseMessage response = client.Send(requestMessage);
|
||||||
logger?.WriteLine("LibraryManager", $"GET {url} -> {(int)response.StatusCode}: {response.ReasonPhrase}");
|
logger?.WriteLine("LibraryManager.NetClient", $"GET {url} -> {(int)response.StatusCode}: {response.ReasonPhrase}");
|
||||||
|
|
||||||
if(response.StatusCode is HttpStatusCode.Unauthorized && response.RequestMessage!.RequestUri!.AbsoluteUri != url)
|
if(response.StatusCode is HttpStatusCode.Unauthorized && response.RequestMessage!.RequestUri!.AbsoluteUri != url)
|
||||||
return MakeRequest(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth, logger);
|
return MakeRequest(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth, logger);
|
||||||
@ -78,7 +65,7 @@ public abstract class LibraryManager
|
|||||||
RequestUri = new Uri(url)
|
RequestUri = new Uri(url)
|
||||||
};
|
};
|
||||||
HttpResponseMessage response = client.Send(requestMessage);
|
HttpResponseMessage response = client.Send(requestMessage);
|
||||||
logger?.WriteLine("LibraryManager", $"POST {url} -> {(int)response.StatusCode}: {response.ReasonPhrase}");
|
logger?.WriteLine("LibraryManager.NetClient", $"POST {url} -> {(int)response.StatusCode}: {response.ReasonPhrase}");
|
||||||
|
|
||||||
if(response.StatusCode is HttpStatusCode.Unauthorized && response.RequestMessage!.RequestUri!.AbsoluteUri != url)
|
if(response.StatusCode is HttpStatusCode.Unauthorized && response.RequestMessage!.RequestUri!.AbsoluteUri != url)
|
||||||
return MakePost(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth, logger);
|
return MakePost(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth, logger);
|
||||||
@ -88,34 +75,4 @@ public abstract class LibraryManager
|
|||||||
return false;
|
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>() == (Int64)LibraryType.Komga)
|
|
||||||
return jo.ToObject<Komga>(serializer)!;
|
|
||||||
|
|
||||||
if (jo["libraryType"]!.Value<Int64>() == (Int64)LibraryType.Kavita)
|
|
||||||
return jo.ToObject<Kavita>(serializer)!;
|
|
||||||
|
|
||||||
throw new Exception();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool CanWrite => false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Don't call this
|
|
||||||
/// </summary>
|
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
throw new Exception("Dont call this");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
45
Tranga/LibraryConnectors/LibraryManagerJsonConverter.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace Tranga.LibraryConnectors;
|
||||||
|
|
||||||
|
public class LibraryManagerJsonConverter : JsonConverter
|
||||||
|
{
|
||||||
|
private GlobalBase _clone;
|
||||||
|
|
||||||
|
internal LibraryManagerJsonConverter(GlobalBase clone)
|
||||||
|
{
|
||||||
|
this._clone = clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanConvert(Type objectType)
|
||||||
|
{
|
||||||
|
return (objectType == typeof(LibraryConnector));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
JObject jo = JObject.Load(reader);
|
||||||
|
if (jo["libraryType"]!.Value<byte>() == (byte)LibraryConnector.LibraryType.Komga)
|
||||||
|
return new Komga(this._clone,
|
||||||
|
jo.GetValue("baseUrl")!.Value<string>()!,
|
||||||
|
jo.GetValue("auth")!.Value<string>()!);
|
||||||
|
|
||||||
|
if (jo["libraryType"]!.Value<byte>() == (byte)LibraryConnector.LibraryType.Kavita)
|
||||||
|
return new Kavita(this._clone,
|
||||||
|
jo.GetValue("baseUrl")!.Value<string>()!,
|
||||||
|
jo.GetValue("auth")!.Value<string>()!);
|
||||||
|
|
||||||
|
throw new Exception();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanWrite => false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Don't call this
|
||||||
|
/// </summary>
|
||||||
|
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
throw new Exception("Dont call this");
|
||||||
|
}
|
||||||
|
}
|
@ -9,7 +9,7 @@ namespace Tranga;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Contains information on a Publication (Manga)
|
/// Contains information on a Publication (Manga)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public struct Publication
|
public struct Manga
|
||||||
{
|
{
|
||||||
public string sortName { get; }
|
public string sortName { get; }
|
||||||
public List<string> authors { get; }
|
public List<string> authors { get; }
|
||||||
@ -19,8 +19,8 @@ public struct Publication
|
|||||||
public string? description { get; }
|
public string? description { get; }
|
||||||
public string[] tags { get; }
|
public string[] tags { get; }
|
||||||
// ReSharper disable once UnusedAutoPropertyAccessor.Global
|
// ReSharper disable once UnusedAutoPropertyAccessor.Global
|
||||||
public string? posterUrl { get; }
|
public string? coverUrl { get; }
|
||||||
public string? coverFileNameInCache { get; }
|
public string? coverFileNameInCache { get; set; }
|
||||||
// ReSharper disable once UnusedAutoPropertyAccessor.Global
|
// ReSharper disable once UnusedAutoPropertyAccessor.Global
|
||||||
public Dictionary<string,string> links { get; }
|
public Dictionary<string,string> links { get; }
|
||||||
// ReSharper disable once MemberCanBePrivate.Global
|
// ReSharper disable once MemberCanBePrivate.Global
|
||||||
@ -33,10 +33,10 @@ public struct Publication
|
|||||||
public string internalId { get; }
|
public string internalId { get; }
|
||||||
public float ignoreChaptersBelow { get; set; }
|
public float ignoreChaptersBelow { get; set; }
|
||||||
|
|
||||||
private static readonly Regex LegalCharacters = new Regex(@"[A-Z]*[a-z]*[0-9]* *\.*-*,*'*\'*\)*\(*~*!*");
|
private static readonly Regex LegalCharacters = new (@"[A-Z]*[a-z]*[0-9]* *\.*-*,*'*\'*\)*\(*~*!*");
|
||||||
|
|
||||||
[JsonConstructor]
|
[JsonConstructor]
|
||||||
public Publication(string sortName, List<string> authors, string? description, Dictionary<string,string> altTitles, string[] tags, string? posterUrl, string? coverFileNameInCache, Dictionary<string,string>? links, int? year, string? originalLanguage, string status, string publicationId, string? folderName = null, float? ignoreChaptersBelow = 0)
|
public Manga(string sortName, List<string> authors, string? description, Dictionary<string,string> altTitles, string[] tags, string? coverUrl, string? coverFileNameInCache, Dictionary<string,string>? links, int? year, string? originalLanguage, string status, string publicationId, string? folderName = null, float? ignoreChaptersBelow = 0)
|
||||||
{
|
{
|
||||||
this.sortName = sortName;
|
this.sortName = sortName;
|
||||||
this.authors = authors;
|
this.authors = authors;
|
||||||
@ -44,7 +44,7 @@ public struct Publication
|
|||||||
this.altTitles = altTitles;
|
this.altTitles = altTitles;
|
||||||
this.tags = tags;
|
this.tags = tags;
|
||||||
this.coverFileNameInCache = coverFileNameInCache;
|
this.coverFileNameInCache = coverFileNameInCache;
|
||||||
this.posterUrl = posterUrl;
|
this.coverUrl = coverUrl;
|
||||||
this.links = links ?? new Dictionary<string, string>();
|
this.links = links ?? new Dictionary<string, string>();
|
||||||
this.year = year;
|
this.year = year;
|
||||||
this.originalLanguage = originalLanguage;
|
this.originalLanguage = originalLanguage;
|
||||||
@ -58,6 +58,11 @@ public struct Publication
|
|||||||
this.ignoreChaptersBelow = ignoreChaptersBelow ?? 0f;
|
this.ignoreChaptersBelow = ignoreChaptersBelow ?? 0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"Publication {sortName} {internalId}";
|
||||||
|
}
|
||||||
|
|
||||||
public string CreatePublicationFolder(string downloadDirectory)
|
public string CreatePublicationFolder(string downloadDirectory)
|
||||||
{
|
{
|
||||||
string publicationFolder = Path.Join(downloadDirectory, this.folderName);
|
string publicationFolder = Path.Join(downloadDirectory, this.folderName);
|
@ -1,10 +1,9 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using Logging;
|
|
||||||
|
|
||||||
namespace Tranga;
|
namespace Tranga.MangaConnectors;
|
||||||
|
|
||||||
internal class DownloadClient
|
internal class DownloadClient : GlobalBase
|
||||||
{
|
{
|
||||||
private static readonly HttpClient Client = new()
|
private static readonly HttpClient Client = new()
|
||||||
{
|
{
|
||||||
@ -20,17 +19,9 @@ internal class DownloadClient
|
|||||||
|
|
||||||
private readonly Dictionary<byte, DateTime> _lastExecutedRateLimit;
|
private readonly Dictionary<byte, DateTime> _lastExecutedRateLimit;
|
||||||
private readonly Dictionary<byte, TimeSpan> _rateLimit;
|
private readonly Dictionary<byte, TimeSpan> _rateLimit;
|
||||||
// ReSharper disable once InconsistentNaming
|
|
||||||
private readonly Logger? logger;
|
|
||||||
|
|
||||||
/// <summary>
|
public DownloadClient(GlobalBase clone, Dictionary<byte, int> rateLimitRequestsPerMinute) : base(clone)
|
||||||
/// Creates a httpClient
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="rateLimitRequestsPerMinute">Rate limits for requests. byte is RequestType, int maximum requests per minute for RequestType</param>
|
|
||||||
/// <param name="logger"></param>
|
|
||||||
public DownloadClient(Dictionary<byte, int> rateLimitRequestsPerMinute, Logger? logger)
|
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
|
||||||
_lastExecutedRateLimit = new();
|
_lastExecutedRateLimit = new();
|
||||||
_rateLimit = new();
|
_rateLimit = new();
|
||||||
foreach(KeyValuePair<byte, int> limit in rateLimitRequestsPerMinute)
|
foreach(KeyValuePair<byte, int> limit in rateLimitRequestsPerMinute)
|
||||||
@ -50,7 +41,7 @@ internal class DownloadClient
|
|||||||
_lastExecutedRateLimit.TryAdd(requestType, DateTime.Now.Subtract(value));
|
_lastExecutedRateLimit.TryAdd(requestType, DateTime.Now.Subtract(value));
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), "RequestType not configured for rate-limit.");
|
Log("RequestType not configured for rate-limit.");
|
||||||
return new RequestResult(HttpStatusCode.NotAcceptable, Stream.Null);
|
return new RequestResult(HttpStatusCode.NotAcceptable, Stream.Null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,18 +60,18 @@ internal class DownloadClient
|
|||||||
if(referrer is not null)
|
if(referrer is not null)
|
||||||
requestMessage.Headers.Referrer = new Uri(referrer);
|
requestMessage.Headers.Referrer = new Uri(referrer);
|
||||||
_lastExecutedRateLimit[requestType] = DateTime.Now;
|
_lastExecutedRateLimit[requestType] = DateTime.Now;
|
||||||
|
//Log($"Requesting {requestType} {url}");
|
||||||
response = Client.Send(requestMessage);
|
response = Client.Send(requestMessage);
|
||||||
}
|
}
|
||||||
catch (HttpRequestException e)
|
catch (HttpRequestException e)
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), e.Message);
|
Log("Exception:\n\t{0}\n\tWaiting {1} before retrying.", e.Message, _rateLimit[requestType] * 2);
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Waiting {_rateLimit[requestType] * 2}... Retrying.");
|
|
||||||
Thread.Sleep(_rateLimit[requestType] * 2);
|
Thread.Sleep(_rateLimit[requestType] * 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Request-Error {response.StatusCode}: {response.ReasonPhrase}");
|
Log($"Request-Error {response.StatusCode}: {response.ReasonPhrase}");
|
||||||
return new RequestResult(response.StatusCode, Stream.Null);
|
return new RequestResult(response.StatusCode, Stream.Null);
|
||||||
}
|
}
|
||||||
|
|
@ -3,80 +3,68 @@ using System.IO.Compression;
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Tranga.TrangaTasks;
|
using Tranga.Jobs;
|
||||||
using static System.IO.UnixFileMode;
|
using static System.IO.UnixFileMode;
|
||||||
|
|
||||||
namespace Tranga.Connectors;
|
namespace Tranga.MangaConnectors;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base-Class for all Connectors
|
/// Base-Class for all Connectors
|
||||||
/// Provides some methods to be used by all Connectors, as well as a DownloadClient
|
/// Provides some methods to be used by all Connectors, as well as a DownloadClient
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class Connector
|
public abstract class MangaConnector : GlobalBase
|
||||||
{
|
{
|
||||||
protected CommonObjects commonObjects;
|
|
||||||
protected TrangaSettings settings { get; }
|
|
||||||
internal DownloadClient downloadClient { get; init; } = null!;
|
internal DownloadClient downloadClient { get; init; } = null!;
|
||||||
|
|
||||||
protected Connector(TrangaSettings settings, CommonObjects commonObjects)
|
protected MangaConnector(GlobalBase clone) : base(clone)
|
||||||
{
|
{
|
||||||
this.settings = settings;
|
Directory.CreateDirectory(settings.coverImageCache);
|
||||||
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 abstract string name { get; } //Name of the Connector (e.g. Website)
|
||||||
|
|
||||||
public Publication[] GetPublications(ref HashSet<Publication> publicationCollection, string publicationTitle = "")
|
|
||||||
{
|
|
||||||
Publication[] ret = GetPublicationsInternal(publicationTitle);
|
|
||||||
foreach (Publication p in ret)
|
|
||||||
publicationCollection.Add(p);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns all Publications with the given string.
|
/// Returns all Publications with the given string.
|
||||||
/// If the string is empty or null, returns all Publication of the Connector
|
/// If the string is empty or null, returns all Publication of the Connector
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="publicationTitle">Search-Query</param>
|
/// <param name="publicationTitle">Search-Query</param>
|
||||||
/// <returns>Publications matching the query</returns>
|
/// <returns>Publications matching the query</returns>
|
||||||
protected abstract Publication[] GetPublicationsInternal(string publicationTitle = "");
|
public abstract Manga[] GetManga(string publicationTitle = "");
|
||||||
|
|
||||||
|
public abstract Manga? GetMangaFromUrl(string url);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns all Chapters of the publication in the provided language.
|
/// Returns all Chapters of the publication in the provided language.
|
||||||
/// If the language is empty or null, returns all Chapters in all Languages.
|
/// If the language is empty or null, returns all Chapters in all Languages.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="publication">Publication to get Chapters for</param>
|
/// <param name="manga">Publication to get Chapters for</param>
|
||||||
/// <param name="language">Language of the Chapters</param>
|
/// <param name="language">Language of the Chapters</param>
|
||||||
/// <returns>Array of Chapters matching Publication and Language</returns>
|
/// <returns>Array of Chapters matching Publication and Language</returns>
|
||||||
public abstract Chapter[] GetChapters(Publication publication, string language = "");
|
public abstract Chapter[] GetChapters(Manga manga, string language="en");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the available Chapters of a Publication
|
/// Updates the available Chapters of a Publication
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="publication">Publication to check</param>
|
/// <param name="manga">Publication to check</param>
|
||||||
/// <param name="language">Language to receive chapters for</param>
|
/// <param name="language">Language to receive chapters for</param>
|
||||||
/// <param name="collection"></param>
|
|
||||||
/// <returns>List of Chapters that were previously not in collection</returns>
|
/// <returns>List of Chapters that were previously not in collection</returns>
|
||||||
public List<Chapter> GetNewChaptersList(Publication publication, string language, ref HashSet<Publication> collection)
|
public Chapter[] GetNewChapters(Manga manga, string language = "en")
|
||||||
{
|
{
|
||||||
Chapter[] newChapters = this.GetChapters(publication, language);
|
Log($"Getting new Chapters for {manga}");
|
||||||
collection.Add(publication);
|
Chapter[] newChapters = this.GetChapters(manga, language);
|
||||||
NumberFormatInfo decimalPoint = new (){ NumberDecimalSeparator = "." };
|
NumberFormatInfo decimalPoint = new (){ NumberDecimalSeparator = "." };
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), "Checking for duplicates");
|
Log($"Checking for duplicates {manga}");
|
||||||
List<Chapter> newChaptersList = newChapters.Where(nChapter =>
|
List<Chapter> newChaptersList = newChapters.Where(nChapter =>
|
||||||
float.Parse(nChapter.chapterNumber, decimalPoint) > publication.ignoreChaptersBelow &&
|
float.Parse(nChapter.chapterNumber, decimalPoint) > manga.ignoreChaptersBelow &&
|
||||||
!nChapter.CheckChapterIsDownloaded(settings.downloadLocation)).ToList();
|
!nChapter.CheckChapterIsDownloaded(settings.downloadLocation)).ToList();
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"{newChaptersList.Count} new chapters.");
|
Log($"{newChaptersList.Count} new chapters. {manga}");
|
||||||
|
|
||||||
return newChaptersList;
|
return newChaptersList.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Chapter[] SelectChapters(Publication publication, string searchTerm, string? language = null)
|
public Chapter[] SelectChapters(Manga manga, string searchTerm, string? language = null)
|
||||||
{
|
{
|
||||||
Chapter[] availableChapters = this.GetChapters(publication, language??"en");
|
Chapter[] availableChapters = this.GetChapters(manga, language??"en");
|
||||||
Regex volumeRegex = new ("((v(ol)*(olume)*){1} *([0-9]+(-[0-9]+)?){1})", RegexOptions.IgnoreCase);
|
Regex 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 chapterRegex = new ("((c(h)*(hapter)*){1} *([0-9]+(-[0-9]+)?){1})", RegexOptions.IgnoreCase);
|
||||||
Regex singleResultRegex = new("([0-9]+)", RegexOptions.IgnoreCase);
|
Regex singleResultRegex = new("([0-9]+)", RegexOptions.IgnoreCase);
|
||||||
@ -147,35 +135,27 @@ public abstract class Connector
|
|||||||
return Array.Empty<Chapter>();
|
return Array.Empty<Chapter>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public abstract HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null);
|
||||||
/// Retrieves the Chapter (+Images) from the website.
|
|
||||||
/// Should later call DownloadChapterImages to retrieve the individual Images of the Chapter and create .cbz archive.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="publication">Publication that contains Chapter</param>
|
|
||||||
/// <param name="chapter">Chapter with Images to retrieve</param>
|
|
||||||
/// <param name="parentTask">Will be used for progress-tracking</param>
|
|
||||||
/// <param name="cancellationToken"></param>
|
|
||||||
public abstract HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copies the already downloaded cover from cache to downloadLocation
|
/// Copies the already downloaded cover from cache to downloadLocation
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="publication">Publication to retrieve Cover for</param>
|
/// <param name="manga">Publication to retrieve Cover for</param>
|
||||||
public void CopyCoverFromCacheToDownloadLocation(Publication publication)
|
public void CopyCoverFromCacheToDownloadLocation(Manga manga)
|
||||||
{
|
{
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Cloning cover {publication.sortName} -> {publication.internalId}");
|
Log($"Copy cover {manga}");
|
||||||
//Check if Publication already has a Folder and cover
|
//Check if Publication already has a Folder and cover
|
||||||
string publicationFolder = publication.CreatePublicationFolder(settings.downloadLocation);
|
string publicationFolder = manga.CreatePublicationFolder(settings.downloadLocation);
|
||||||
DirectoryInfo dirInfo = new (publicationFolder);
|
DirectoryInfo dirInfo = new (publicationFolder);
|
||||||
if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover", StringComparison.InvariantCultureIgnoreCase)))
|
if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover", StringComparison.InvariantCultureIgnoreCase)))
|
||||||
{
|
{
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Cover exists {publication.sortName}");
|
Log($"Cover exists {manga}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string fileInCache = Path.Join(settings.coverImageCache, publication.coverFileNameInCache);
|
string fileInCache = Path.Join(settings.coverImageCache, manga.coverFileNameInCache);
|
||||||
string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(fileInCache).Split('.')[^1]}" );
|
string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(fileInCache).Split('.')[^1]}" );
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Cloning cover {fileInCache} -> {newFilePath}");
|
Log($"Cloning cover {fileInCache} -> {newFilePath}");
|
||||||
File.Copy(fileInCache, newFilePath, true);
|
File.Copy(fileInCache, newFilePath, true);
|
||||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
File.SetUnixFileMode(newFilePath, GroupRead | GroupWrite | OtherRead | OtherWrite | UserRead | UserWrite);
|
File.SetUnixFileMode(newFilePath, GroupRead | GroupWrite | OtherRead | OtherWrite | UserRead | UserWrite);
|
||||||
@ -199,21 +179,13 @@ public abstract class Connector
|
|||||||
return requestResult.statusCode;
|
return requestResult.statusCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
protected HttpStatusCode DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, byte requestType, string? comicInfoPath = null, string? referrer = null, ProgressToken? progressToken = null)
|
||||||
/// Downloads all Images from URLs, Compresses to zip(cbz) and saves.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="imageUrls">List of URLs to download Images from</param>
|
|
||||||
/// <param name="saveArchiveFilePath">Full path to save archive to (without file ending .cbz)</param>
|
|
||||||
/// <param name="parentTask">Used for progress tracking</param>
|
|
||||||
/// <param name="comicInfoPath">Path of the generate Chapter ComicInfo.xml, if it was generated</param>
|
|
||||||
/// <param name="requestType">RequestType for RateLimits</param>
|
|
||||||
/// <param name="referrer">Used in http request header</param>
|
|
||||||
/// <param name="cancellationToken"></param>
|
|
||||||
protected HttpStatusCode DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, byte requestType, DownloadChapterTask parentTask, string? comicInfoPath = null, string? referrer = null, CancellationToken? cancellationToken = null)
|
|
||||||
{
|
{
|
||||||
if (cancellationToken?.IsCancellationRequested ?? false)
|
if (progressToken?.cancellationRequested ?? false)
|
||||||
return HttpStatusCode.RequestTimeout;
|
return HttpStatusCode.RequestTimeout;
|
||||||
commonObjects.logger?.WriteLine("Connector", $"Downloading Images for {saveArchiveFilePath}");
|
Log($"Downloading Images for {saveArchiveFilePath}");
|
||||||
|
if(progressToken is not null)
|
||||||
|
progressToken.increments = imageUrls.Length;
|
||||||
//Check if Publication Directory already exists
|
//Check if Publication Directory already exists
|
||||||
string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!;
|
string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!;
|
||||||
if (!Directory.Exists(directoryPath))
|
if (!Directory.Exists(directoryPath))
|
||||||
@ -231,24 +203,32 @@ public abstract class Connector
|
|||||||
{
|
{
|
||||||
string[] split = imageUrl.Split('.');
|
string[] split = imageUrl.Split('.');
|
||||||
string extension = split[^1];
|
string extension = split[^1];
|
||||||
commonObjects.logger?.WriteLine("Connector", $"Downloading Image {chapter + 1:000}/{imageUrls.Length:000} {parentTask.publication.sortName} {parentTask.publication.internalId} Vol.{parentTask.chapter.volumeNumber} Ch.{parentTask.chapter.chapterNumber} {parentTask.progress:P2}");
|
Log($"Downloading image {chapter + 1:000}/{imageUrls.Length:000}"); //TODO
|
||||||
HttpStatusCode status = DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), requestType, referrer);
|
HttpStatusCode status = DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), requestType, referrer);
|
||||||
if ((int)status < 200 || (int)status >= 300)
|
if ((int)status < 200 || (int)status >= 300)
|
||||||
|
{
|
||||||
|
progressToken?.Complete();
|
||||||
return status;
|
return status;
|
||||||
parentTask.IncrementProgress(1.0 / imageUrls.Length);
|
}
|
||||||
if (cancellationToken?.IsCancellationRequested ?? false)
|
if (progressToken?.cancellationRequested ?? false)
|
||||||
|
{
|
||||||
|
progressToken?.Complete();
|
||||||
return HttpStatusCode.RequestTimeout;
|
return HttpStatusCode.RequestTimeout;
|
||||||
|
}
|
||||||
|
progressToken?.Increment();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(comicInfoPath is not null)
|
if(comicInfoPath is not null)
|
||||||
File.Copy(comicInfoPath, Path.Join(tempFolder, "ComicInfo.xml"));
|
File.Copy(comicInfoPath, Path.Join(tempFolder, "ComicInfo.xml"));
|
||||||
|
|
||||||
commonObjects.logger?.WriteLine("Connector", $"Creating archive {saveArchiveFilePath}");
|
Log($"Creating archive {saveArchiveFilePath}");
|
||||||
//ZIP-it and ship-it
|
//ZIP-it and ship-it
|
||||||
ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath);
|
ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath);
|
||||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
File.SetUnixFileMode(saveArchiveFilePath, GroupRead | GroupWrite | OtherRead | OtherWrite | UserRead | UserWrite);
|
File.SetUnixFileMode(saveArchiveFilePath, GroupRead | GroupWrite | OtherRead | OtherWrite | UserRead | UserWrite);
|
||||||
Directory.Delete(tempFolder, true); //Cleanup
|
Directory.Delete(tempFolder, true); //Cleanup
|
||||||
|
|
||||||
|
progressToken?.Complete();
|
||||||
return HttpStatusCode.OK;
|
return HttpStatusCode.OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -265,7 +245,7 @@ public abstract class Connector
|
|||||||
using MemoryStream ms = new();
|
using MemoryStream ms = new();
|
||||||
coverResult.result.CopyTo(ms);
|
coverResult.result.CopyTo(ms);
|
||||||
File.WriteAllBytes(saveImagePath, ms.ToArray());
|
File.WriteAllBytes(saveImagePath, ms.ToArray());
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Saving image to {saveImagePath}");
|
Log($"Saving cover to {saveImagePath}");
|
||||||
return filename;
|
return filename;
|
||||||
}
|
}
|
||||||
}
|
}
|
49
Tranga/MangaConnectors/MangaConnectorJsonConverter.cs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace Tranga.MangaConnectors;
|
||||||
|
|
||||||
|
public class MangaConnectorJsonConverter : JsonConverter
|
||||||
|
{
|
||||||
|
private GlobalBase _clone;
|
||||||
|
private HashSet<MangaConnector> connectors;
|
||||||
|
|
||||||
|
internal MangaConnectorJsonConverter(GlobalBase clone, HashSet<MangaConnector> connectors)
|
||||||
|
{
|
||||||
|
this._clone = clone;
|
||||||
|
this.connectors = connectors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanConvert(Type objectType)
|
||||||
|
{
|
||||||
|
return (objectType == typeof(MangaConnector));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
JObject jo = JObject.Load(reader);
|
||||||
|
switch (jo.GetValue("name")!.Value<string>()!)
|
||||||
|
{
|
||||||
|
case "MangaDex":
|
||||||
|
return this.connectors.First(c => c is MangaDex);
|
||||||
|
case "Manganato":
|
||||||
|
return this.connectors.First(c => c is Manganato);
|
||||||
|
case "MangaKatana":
|
||||||
|
return this.connectors.First(c => c is MangaKatana);
|
||||||
|
case "Mangasee":
|
||||||
|
return this.connectors.First(c => c is Mangasee);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanWrite => false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Don't call this
|
||||||
|
/// </summary>
|
||||||
|
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
throw new Exception("Dont call this");
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,12 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using Tranga.TrangaTasks;
|
using System.Text.RegularExpressions;
|
||||||
|
using Tranga.Jobs;
|
||||||
|
using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||||
|
|
||||||
namespace Tranga.Connectors;
|
namespace Tranga.MangaConnectors;
|
||||||
public class MangaDex : Connector
|
public class MangaDex : MangaConnector
|
||||||
{
|
{
|
||||||
public override string name { get; }
|
public override string name { get; }
|
||||||
|
|
||||||
@ -18,26 +19,26 @@ public class MangaDex : Connector
|
|||||||
Author,
|
Author,
|
||||||
}
|
}
|
||||||
|
|
||||||
public MangaDex(TrangaSettings settings, CommonObjects commonObjects) : base(settings, commonObjects)
|
public MangaDex(GlobalBase clone) : base(clone)
|
||||||
{
|
{
|
||||||
name = "MangaDex";
|
name = "MangaDex";
|
||||||
this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
|
this.downloadClient = new DownloadClient(clone, new Dictionary<byte, int>()
|
||||||
{
|
{
|
||||||
{(byte)RequestType.Manga, 250},
|
{(byte)RequestType.Manga, 250},
|
||||||
{(byte)RequestType.Feed, 250},
|
{(byte)RequestType.Feed, 250},
|
||||||
{(byte)RequestType.AtHomeServer, 40},
|
{(byte)RequestType.AtHomeServer, 40},
|
||||||
{(byte)RequestType.CoverUrl, 250},
|
{(byte)RequestType.CoverUrl, 250},
|
||||||
{(byte)RequestType.Author, 250}
|
{(byte)RequestType.Author, 250}
|
||||||
}, commonObjects.logger);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Publication[] GetPublicationsInternal(string publicationTitle = "")
|
public override Manga[] GetManga(string publicationTitle = "")
|
||||||
{
|
{
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Publications (title={publicationTitle})");
|
Log($"Searching Publications. Term=\"{publicationTitle}\"");
|
||||||
const int limit = 100; //How many values we want returned at once
|
const int limit = 100; //How many values we want returned at once
|
||||||
int offset = 0; //"Page"
|
int offset = 0; //"Page"
|
||||||
int total = int.MaxValue; //How many total results are there, is updated on first request
|
int total = int.MaxValue; //How many total results are there, is updated on first request
|
||||||
HashSet<Publication> publications = new();
|
HashSet<Manga> retManga = new();
|
||||||
int loadedPublicationData = 0;
|
int loadedPublicationData = 0;
|
||||||
while (offset < total) //As long as we haven't requested all "Pages"
|
while (offset < total) //As long as we haven't requested all "Pages"
|
||||||
{
|
{
|
||||||
@ -57,102 +58,123 @@ public class MangaDex : Connector
|
|||||||
|
|
||||||
JsonArray mangaInResult = result["data"]!.AsArray(); //Manga-data-Array
|
JsonArray mangaInResult = result["data"]!.AsArray(); //Manga-data-Array
|
||||||
//Loop each Manga and extract information from JSON
|
//Loop each Manga and extract information from JSON
|
||||||
foreach (JsonNode? mangeNode in mangaInResult)
|
foreach (JsonNode? mangaNode in mangaInResult)
|
||||||
{
|
{
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting publication data. {++loadedPublicationData}/{total}");
|
Log($"Getting publication data. {++loadedPublicationData}/{total}");
|
||||||
JsonObject manga = (JsonObject)mangeNode!;
|
Manga manga = MangaFromJsonObject((JsonObject)mangaNode);
|
||||||
JsonObject attributes = manga["attributes"]!.AsObject();
|
retManga.Add(manga); //Add Publication (Manga) to result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log($"Retrieved {retManga.Count} publications. Term=\"{publicationTitle}\"");
|
||||||
|
return retManga.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Manga? GetMangaFromUrl(string url)
|
||||||
|
{
|
||||||
|
Regex idRex = new (@"https:\/\/mangadex.org\/title\/([A-z0-9-]*)\/.*");
|
||||||
|
string id = idRex.Match(url).Groups[1].Value;
|
||||||
|
Log($"Got id {id} from {url}");
|
||||||
|
DownloadClient.RequestResult requestResult =
|
||||||
|
downloadClient.MakeRequest($"https://api.mangadex.org/manga/{id}", (byte)RequestType.Manga);
|
||||||
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
|
return null;
|
||||||
|
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
||||||
|
if(result is not null)
|
||||||
|
return MangaFromJsonObject(result["data"]!.AsObject());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Manga MangaFromJsonObject(JsonObject manga)
|
||||||
|
{
|
||||||
|
JsonObject attributes = manga["attributes"]!.AsObject();
|
||||||
|
|
||||||
string publicationId = manga["id"]!.GetValue<string>();
|
string publicationId = manga["id"]!.GetValue<string>();
|
||||||
|
|
||||||
string title = attributes["title"]!.AsObject().ContainsKey("en") && attributes["title"]!["en"] is not null
|
string title = attributes["title"]!.AsObject().ContainsKey("en") && attributes["title"]!["en"] is not null
|
||||||
? attributes["title"]!["en"]!.GetValue<string>()
|
? attributes["title"]!["en"]!.GetValue<string>()
|
||||||
: attributes["title"]![((IDictionary<string, JsonNode?>)attributes["title"]!.AsObject()).Keys.First()]!.GetValue<string>();
|
: attributes["title"]![((IDictionary<string, JsonNode?>)attributes["title"]!.AsObject()).Keys.First()]!.GetValue<string>();
|
||||||
|
|
||||||
string? description = attributes["description"]!.AsObject().ContainsKey("en") && attributes["description"]!["en"] is not null
|
string? description = attributes["description"]!.AsObject().ContainsKey("en") && attributes["description"]!["en"] is not null
|
||||||
? attributes["description"]!["en"]!.GetValue<string?>()
|
? attributes["description"]!["en"]!.GetValue<string?>()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
JsonArray altTitlesObject = attributes["altTitles"]!.AsArray();
|
JsonArray altTitlesObject = attributes["altTitles"]!.AsArray();
|
||||||
Dictionary<string, string> altTitlesDict = new();
|
Dictionary<string, string> altTitlesDict = new();
|
||||||
foreach (JsonNode? altTitleNode in altTitlesObject)
|
foreach (JsonNode? altTitleNode in altTitlesObject)
|
||||||
{
|
{
|
||||||
JsonObject altTitleObject = (JsonObject)altTitleNode!;
|
JsonObject altTitleObject = (JsonObject)altTitleNode!;
|
||||||
string key = ((IDictionary<string, JsonNode?>)altTitleObject).Keys.ToArray()[0];
|
string key = ((IDictionary<string, JsonNode?>)altTitleObject).Keys.ToArray()[0];
|
||||||
altTitlesDict.TryAdd(key, altTitleObject[key]!.GetValue<string>());
|
altTitlesDict.TryAdd(key, altTitleObject[key]!.GetValue<string>());
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonArray tagsObject = attributes["tags"]!.AsArray();
|
JsonArray tagsObject = attributes["tags"]!.AsArray();
|
||||||
HashSet<string> tags = new();
|
HashSet<string> tags = new();
|
||||||
foreach (JsonNode? tagNode in tagsObject)
|
foreach (JsonNode? tagNode in tagsObject)
|
||||||
{
|
{
|
||||||
JsonObject tagObject = (JsonObject)tagNode!;
|
JsonObject tagObject = (JsonObject)tagNode!;
|
||||||
if(tagObject["attributes"]!["name"]!.AsObject().ContainsKey("en"))
|
if(tagObject["attributes"]!["name"]!.AsObject().ContainsKey("en"))
|
||||||
tags.Add(tagObject["attributes"]!["name"]!["en"]!.GetValue<string>());
|
tags.Add(tagObject["attributes"]!["name"]!["en"]!.GetValue<string>());
|
||||||
}
|
}
|
||||||
|
|
||||||
string? posterId = null;
|
string? posterId = null;
|
||||||
HashSet<string> authorIds = new();
|
HashSet<string> authorIds = new();
|
||||||
if (manga.ContainsKey("relationships") && manga["relationships"] is not null)
|
if (manga.ContainsKey("relationships") && manga["relationships"] is not null)
|
||||||
{
|
{
|
||||||
JsonArray relationships = manga["relationships"]!.AsArray();
|
JsonArray relationships = manga["relationships"]!.AsArray();
|
||||||
posterId = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue<string>() == "cover_art")!["id"]!.GetValue<string>();
|
posterId = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue<string>() == "cover_art")!["id"]!.GetValue<string>();
|
||||||
foreach (JsonNode? node in relationships.Where(relationship =>
|
foreach (JsonNode? node in relationships.Where(relationship =>
|
||||||
relationship!["type"]!.GetValue<string>() == "author"))
|
relationship!["type"]!.GetValue<string>() == "author"))
|
||||||
authorIds.Add(node!["id"]!.GetValue<string>());
|
authorIds.Add(node!["id"]!.GetValue<string>());
|
||||||
}
|
}
|
||||||
string? coverUrl = GetCoverUrl(publicationId, posterId);
|
string? coverUrl = GetCoverUrl(publicationId, posterId);
|
||||||
string? coverCacheName = null;
|
string? coverCacheName = null;
|
||||||
if (coverUrl is not null)
|
if (coverUrl is not null)
|
||||||
coverCacheName = SaveCoverImageToCache(coverUrl, (byte)RequestType.AtHomeServer);
|
coverCacheName = SaveCoverImageToCache(coverUrl, (byte)RequestType.AtHomeServer);
|
||||||
|
|
||||||
List<string> authors = GetAuthors(authorIds);
|
List<string> authors = GetAuthors(authorIds);
|
||||||
|
|
||||||
Dictionary<string, string> linksDict = new();
|
Dictionary<string, string> linksDict = new();
|
||||||
if (attributes.ContainsKey("links") && attributes["links"] is not null)
|
if (attributes.ContainsKey("links") && attributes["links"] is not null)
|
||||||
{
|
{
|
||||||
JsonObject linksObject = attributes["links"]!.AsObject();
|
JsonObject linksObject = attributes["links"]!.AsObject();
|
||||||
foreach (string key in ((IDictionary<string, JsonNode?>)linksObject).Keys)
|
foreach (string key in ((IDictionary<string, JsonNode?>)linksObject).Keys)
|
||||||
{
|
{
|
||||||
linksDict.Add(key, linksObject[key]!.GetValue<string>());
|
linksDict.Add(key, linksObject[key]!.GetValue<string>());
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int? year = attributes.ContainsKey("year") && attributes["year"] is not null
|
|
||||||
? attributes["year"]!.GetValue<int?>()
|
|
||||||
: null;
|
|
||||||
|
|
||||||
string? originalLanguage = attributes.ContainsKey("originalLanguage") && attributes["originalLanguage"] is not null
|
|
||||||
? attributes["originalLanguage"]!.GetValue<string?>()
|
|
||||||
: null;
|
|
||||||
|
|
||||||
string status = attributes["status"]!.GetValue<string>();
|
|
||||||
|
|
||||||
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})");
|
int? year = attributes.ContainsKey("year") && attributes["year"] is not null
|
||||||
return publications.ToArray();
|
? attributes["year"]!.GetValue<int?>()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
string? originalLanguage =
|
||||||
|
attributes.ContainsKey("originalLanguage") && attributes["originalLanguage"] is not null
|
||||||
|
? attributes["originalLanguage"]!.GetValue<string?>()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
string status = attributes["status"]!.GetValue<string>();
|
||||||
|
|
||||||
|
Manga pub = new(
|
||||||
|
title,
|
||||||
|
authors,
|
||||||
|
description,
|
||||||
|
altTitlesDict,
|
||||||
|
tags.ToArray(),
|
||||||
|
coverUrl,
|
||||||
|
coverCacheName,
|
||||||
|
linksDict,
|
||||||
|
year,
|
||||||
|
originalLanguage,
|
||||||
|
status,
|
||||||
|
publicationId
|
||||||
|
);
|
||||||
|
cachedPublications.Add(pub);
|
||||||
|
return pub;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Chapter[] GetChapters(Publication publication, string language = "")
|
public override Chapter[] GetChapters(Manga manga, string language="en")
|
||||||
{
|
{
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Chapters for {publication.sortName} {publication.internalId} (language={language})");
|
Log($"Getting chapters {manga}");
|
||||||
const int limit = 100; //How many values we want returned at once
|
const int limit = 100; //How many values we want returned at once
|
||||||
int offset = 0; //"Page"
|
int offset = 0; //"Page"
|
||||||
int total = int.MaxValue; //How many total results are there, is updated on first request
|
int total = int.MaxValue; //How many total results are there, is updated on first request
|
||||||
@ -163,7 +185,7 @@ public class MangaDex : Connector
|
|||||||
//Request next "Page"
|
//Request next "Page"
|
||||||
DownloadClient.RequestResult requestResult =
|
DownloadClient.RequestResult requestResult =
|
||||||
downloadClient.MakeRequest(
|
downloadClient.MakeRequest(
|
||||||
$"https://api.mangadex.org/manga/{publication.publicationId}/feed?limit={limit}&offset={offset}&translatedLanguage%5B%5D={language}", (byte)RequestType.Feed);
|
$"https://api.mangadex.org/manga/{manga.publicationId}/feed?limit={limit}&offset={offset}&translatedLanguage%5B%5D={language}", (byte)RequestType.Feed);
|
||||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
break;
|
break;
|
||||||
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
||||||
@ -194,24 +216,22 @@ public class MangaDex : Connector
|
|||||||
: "null";
|
: "null";
|
||||||
|
|
||||||
if(chapterNum is not "null")
|
if(chapterNum is not "null")
|
||||||
chapters.Add(new Chapter(publication, title, volume, chapterNum, chapterId));
|
chapters.Add(new Chapter(manga, title, volume, chapterNum, chapterId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Return Chapters ordered by Chapter-Number
|
//Return Chapters ordered by Chapter-Number
|
||||||
NumberFormatInfo chapterNumberFormatInfo = new()
|
NumberFormatInfo chapterNumberFormatInfo = new() { NumberDecimalSeparator = "." };
|
||||||
{
|
Log($"Got {chapters.Count} chapters. {manga}");
|
||||||
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();
|
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null)
|
public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
|
||||||
{
|
{
|
||||||
if (cancellationToken?.IsCancellationRequested ?? false)
|
if (progressToken?.cancellationRequested ?? false)
|
||||||
return HttpStatusCode.RequestTimeout;
|
return HttpStatusCode.RequestTimeout;
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
|
Manga chapterParentManga = chapter.parentManga;
|
||||||
|
Log($"Retrieving chapter-info {chapter} {chapterParentManga}");
|
||||||
//Request URLs for Chapter-Images
|
//Request URLs for Chapter-Images
|
||||||
DownloadClient.RequestResult requestResult =
|
DownloadClient.RequestResult requestResult =
|
||||||
downloadClient.MakeRequest($"https://api.mangadex.org/at-home/server/{chapter.url}?forcePort443=false'", (byte)RequestType.AtHomeServer);
|
downloadClient.MakeRequest($"https://api.mangadex.org/at-home/server/{chapter.url}?forcePort443=false'", (byte)RequestType.AtHomeServer);
|
||||||
@ -233,15 +253,15 @@ public class MangaDex : Connector
|
|||||||
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
|
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
|
||||||
|
|
||||||
//Download Chapter-Images
|
//Download Chapter-Images
|
||||||
return DownloadChapterImages(imageUrls.ToArray(), chapter.GetArchiveFilePath(settings.downloadLocation), (byte)RequestType.AtHomeServer, parentTask, comicInfoPath, cancellationToken:cancellationToken);
|
return DownloadChapterImages(imageUrls.ToArray(), chapter.GetArchiveFilePath(settings.downloadLocation), (byte)RequestType.AtHomeServer, comicInfoPath, progressToken:progressToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string? GetCoverUrl(string publicationId, string? posterId)
|
private string? GetCoverUrl(string publicationId, string? posterId)
|
||||||
{
|
{
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting CoverUrl for {publicationId}");
|
Log($"Getting CoverUrl for Publication {publicationId}");
|
||||||
if (posterId is null)
|
if (posterId is null)
|
||||||
{
|
{
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"No posterId, aborting");
|
Log("No cover.");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,12 +277,13 @@ public class MangaDex : Connector
|
|||||||
string fileName = result["data"]!["attributes"]!["fileName"]!.GetValue<string>();
|
string fileName = result["data"]!["attributes"]!["fileName"]!.GetValue<string>();
|
||||||
|
|
||||||
string coverUrl = $"https://uploads.mangadex.org/covers/{publicationId}/{fileName}";
|
string coverUrl = $"https://uploads.mangadex.org/covers/{publicationId}/{fileName}";
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Got Cover-Url for {publicationId} -> {coverUrl}");
|
Log($"Cover-Url {publicationId} -> {coverUrl}");
|
||||||
return coverUrl;
|
return coverUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<string> GetAuthors(IEnumerable<string> authorIds)
|
private List<string> GetAuthors(IEnumerable<string> authorIds)
|
||||||
{
|
{
|
||||||
|
Log("Retrieving authors.");
|
||||||
List<string> ret = new();
|
List<string> ret = new();
|
||||||
foreach (string authorId in authorIds)
|
foreach (string authorId in authorIds)
|
||||||
{
|
{
|
||||||
@ -276,7 +297,7 @@ public class MangaDex : Connector
|
|||||||
|
|
||||||
string authorName = result["data"]!["attributes"]!["name"]!.GetValue<string>();
|
string authorName = result["data"]!["attributes"]!["name"]!.GetValue<string>();
|
||||||
ret.Add(authorName);
|
ret.Add(authorName);
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Got author {authorId} -> {authorName}");
|
Log($"Got author {authorId} -> {authorName}");
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
@ -2,32 +2,32 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
using Tranga.TrangaTasks;
|
using Tranga.Jobs;
|
||||||
|
|
||||||
namespace Tranga.Connectors;
|
namespace Tranga.MangaConnectors;
|
||||||
|
|
||||||
public class MangaKatana : Connector
|
public class MangaKatana : MangaConnector
|
||||||
{
|
{
|
||||||
public override string name { get; }
|
public override string name { get; }
|
||||||
|
|
||||||
public MangaKatana(TrangaSettings settings, CommonObjects commonObjects) : base(settings, commonObjects)
|
public MangaKatana(GlobalBase clone) : base(clone)
|
||||||
{
|
{
|
||||||
this.name = "MangaKatana";
|
this.name = "MangaKatana";
|
||||||
this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
|
this.downloadClient = new DownloadClient(clone, new Dictionary<byte, int>()
|
||||||
{
|
{
|
||||||
{1, 60}
|
{1, 60}
|
||||||
}, commonObjects.logger);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Publication[] GetPublicationsInternal(string publicationTitle = "")
|
public override Manga[] GetManga(string publicationTitle = "")
|
||||||
{
|
{
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Publications (title={publicationTitle})");
|
Log($"Searching Publications. Term=\"{publicationTitle}\"");
|
||||||
string sanitizedTitle = string.Join('_', Regex.Matches(publicationTitle, "[A-z]*").Where(m => m.Value.Length > 0)).ToLower();
|
string 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";
|
string requestUrl = $"https://mangakatana.com/?search={sanitizedTitle}&search_by=book_name";
|
||||||
DownloadClient.RequestResult requestResult =
|
DownloadClient.RequestResult requestResult =
|
||||||
downloadClient.MakeRequest(requestUrl, 1);
|
downloadClient.MakeRequest(requestUrl, 1);
|
||||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
return Array.Empty<Publication>();
|
return Array.Empty<Manga>();
|
||||||
|
|
||||||
// ReSharper disable once MergeIntoPattern
|
// 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 a single result is found, the user will be redirected to the results directly instead of a result page
|
||||||
@ -38,10 +38,21 @@ public class MangaKatana : Connector
|
|||||||
return new [] { ParseSinglePublicationFromHtml(requestResult.result, requestResult.redirectedToUrl.Split('/')[^1]) };
|
return new [] { ParseSinglePublicationFromHtml(requestResult.result, requestResult.redirectedToUrl.Split('/')[^1]) };
|
||||||
}
|
}
|
||||||
|
|
||||||
return ParsePublicationsFromHtml(requestResult.result);
|
Manga[] publications = ParsePublicationsFromHtml(requestResult.result);
|
||||||
|
Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
|
||||||
|
return publications;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Publication[] ParsePublicationsFromHtml(Stream html)
|
public override Manga? GetMangaFromUrl(string url)
|
||||||
|
{
|
||||||
|
DownloadClient.RequestResult requestResult =
|
||||||
|
downloadClient.MakeRequest(url, 1);
|
||||||
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
|
return null;
|
||||||
|
return ParseSinglePublicationFromHtml(requestResult.result, url.Split('/')[^1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Manga[] ParsePublicationsFromHtml(Stream html)
|
||||||
{
|
{
|
||||||
StreamReader reader = new(html);
|
StreamReader reader = new(html);
|
||||||
string htmlString = reader.ReadToEnd();
|
string htmlString = reader.ReadToEnd();
|
||||||
@ -49,7 +60,7 @@ public class MangaKatana : Connector
|
|||||||
document.LoadHtml(htmlString);
|
document.LoadHtml(htmlString);
|
||||||
IEnumerable<HtmlNode> searchResults = document.DocumentNode.SelectNodes("//*[@id='book_list']/div");
|
IEnumerable<HtmlNode> searchResults = document.DocumentNode.SelectNodes("//*[@id='book_list']/div");
|
||||||
if (searchResults is null || !searchResults.Any())
|
if (searchResults is null || !searchResults.Any())
|
||||||
return Array.Empty<Publication>();
|
return Array.Empty<Manga>();
|
||||||
List<string> urls = new();
|
List<string> urls = new();
|
||||||
foreach (HtmlNode mangaResult in searchResults)
|
foreach (HtmlNode mangaResult in searchResults)
|
||||||
{
|
{
|
||||||
@ -57,21 +68,18 @@ public class MangaKatana : Connector
|
|||||||
.First(a => a.Name == "href").Value);
|
.First(a => a.Name == "href").Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
HashSet<Publication> ret = new();
|
HashSet<Manga> ret = new();
|
||||||
foreach (string url in urls)
|
foreach (string url in urls)
|
||||||
{
|
{
|
||||||
DownloadClient.RequestResult requestResult =
|
Manga? manga = GetMangaFromUrl(url);
|
||||||
downloadClient.MakeRequest(url, 1);
|
if (manga is not null)
|
||||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
ret.Add((Manga)manga);
|
||||||
return Array.Empty<Publication>();
|
|
||||||
|
|
||||||
ret.Add(ParseSinglePublicationFromHtml(requestResult.result, url.Split('/')[^1]));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret.ToArray();
|
return ret.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Publication ParseSinglePublicationFromHtml(Stream html, string publicationId)
|
private Manga ParseSinglePublicationFromHtml(Stream html, string publicationId)
|
||||||
{
|
{
|
||||||
StreamReader reader = new(html);
|
StreamReader reader = new(html);
|
||||||
string htmlString = reader.ReadToEnd();
|
string htmlString = reader.ReadToEnd();
|
||||||
@ -131,14 +139,16 @@ public class MangaKatana : Connector
|
|||||||
year = Convert.ToInt32(yearString);
|
year = Convert.ToInt32(yearString);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Publication(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
|
Manga manga = new (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
|
||||||
year, originalLanguage, status, publicationId);
|
year, originalLanguage, status, publicationId);
|
||||||
|
cachedPublications.Add(manga);
|
||||||
|
return manga;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Chapter[] GetChapters(Publication publication, string language = "")
|
public override Chapter[] GetChapters(Manga manga, string language="en")
|
||||||
{
|
{
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Chapters for {publication.sortName} {publication.internalId} (language={language})");
|
Log($"Getting chapters {manga}");
|
||||||
string requestUrl = $"https://mangakatana.com/manga/{publication.publicationId}";
|
string requestUrl = $"https://mangakatana.com/manga/{manga.publicationId}";
|
||||||
// Leaving this in for verification if the page exists
|
// Leaving this in for verification if the page exists
|
||||||
DownloadClient.RequestResult requestResult =
|
DownloadClient.RequestResult requestResult =
|
||||||
downloadClient.MakeRequest(requestUrl, 1);
|
downloadClient.MakeRequest(requestUrl, 1);
|
||||||
@ -150,12 +160,12 @@ public class MangaKatana : Connector
|
|||||||
{
|
{
|
||||||
NumberDecimalSeparator = "."
|
NumberDecimalSeparator = "."
|
||||||
};
|
};
|
||||||
List<Chapter> chapters = ParseChaptersFromHtml(publication, requestUrl);
|
List<Chapter> chapters = ParseChaptersFromHtml(manga, requestUrl);
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Done getting Chapters for {publication.internalId}");
|
Log($"Got {chapters.Count} chapters. {manga}");
|
||||||
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
|
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Chapter> ParseChaptersFromHtml(Publication publication, string mangaUrl)
|
private List<Chapter> ParseChaptersFromHtml(Manga manga, string mangaUrl)
|
||||||
{
|
{
|
||||||
// Using HtmlWeb will include the chapters since they are loaded with js
|
// Using HtmlWeb will include the chapters since they are loaded with js
|
||||||
HtmlWeb web = new();
|
HtmlWeb web = new();
|
||||||
@ -174,17 +184,18 @@ public class MangaKatana : Connector
|
|||||||
string chapterName = string.Concat(fullString.Split(':')[1..]);
|
string chapterName = string.Concat(fullString.Split(':')[1..]);
|
||||||
string url = chapterInfo.Descendants("a").First()
|
string url = chapterInfo.Descendants("a").First()
|
||||||
.GetAttributeValue("href", "");
|
.GetAttributeValue("href", "");
|
||||||
ret.Add(new Chapter(publication, chapterName, volumeNumber, chapterNumber, url));
|
ret.Add(new Chapter(manga, chapterName, volumeNumber, chapterNumber, url));
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null)
|
public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
|
||||||
{
|
{
|
||||||
if (cancellationToken?.IsCancellationRequested ?? false)
|
if (progressToken?.cancellationRequested ?? false)
|
||||||
return HttpStatusCode.RequestTimeout;
|
return HttpStatusCode.RequestTimeout;
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
|
Manga chapterParentManga = chapter.parentManga;
|
||||||
|
Log($"Retrieving chapter-info {chapter} {chapterParentManga}");
|
||||||
string requestUrl = chapter.url;
|
string requestUrl = chapter.url;
|
||||||
// Leaving this in to check if the page exists
|
// Leaving this in to check if the page exists
|
||||||
DownloadClient.RequestResult requestResult =
|
DownloadClient.RequestResult requestResult =
|
||||||
@ -197,7 +208,7 @@ public class MangaKatana : Connector
|
|||||||
string comicInfoPath = Path.GetTempFileName();
|
string comicInfoPath = Path.GetTempFileName();
|
||||||
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
|
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
|
||||||
|
|
||||||
return DownloadChapterImages(imageUrls, chapter.GetArchiveFilePath(settings.downloadLocation), 1, parentTask, comicInfoPath, "https://mangakatana.com/", cancellationToken);
|
return DownloadChapterImages(imageUrls, chapter.GetArchiveFilePath(settings.downloadLocation), 1, comicInfoPath, "https://mangakatana.com/", progressToken:progressToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string[] ParseImageUrlsFromHtml(string mangaUrl)
|
private string[] ParseImageUrlsFromHtml(string mangaUrl)
|
@ -2,37 +2,39 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
using Tranga.TrangaTasks;
|
using Tranga.Jobs;
|
||||||
|
|
||||||
namespace Tranga.Connectors;
|
namespace Tranga.MangaConnectors;
|
||||||
|
|
||||||
public class Manganato : Connector
|
public class Manganato : MangaConnector
|
||||||
{
|
{
|
||||||
public override string name { get; }
|
public override string name { get; }
|
||||||
|
|
||||||
public Manganato(TrangaSettings settings, CommonObjects commonObjects) : base(settings, commonObjects)
|
public Manganato(GlobalBase clone) : base(clone)
|
||||||
{
|
{
|
||||||
this.name = "Manganato";
|
this.name = "Manganato";
|
||||||
this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
|
this.downloadClient = new DownloadClient(clone, new Dictionary<byte, int>()
|
||||||
{
|
{
|
||||||
{1, 60}
|
{1, 60}
|
||||||
}, commonObjects.logger);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Publication[] GetPublicationsInternal(string publicationTitle = "")
|
public override Manga[] GetManga(string publicationTitle = "")
|
||||||
{
|
{
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Publications (title={publicationTitle})");
|
Log($"Searching Publications. Term=\"{publicationTitle}\"");
|
||||||
string sanitizedTitle = string.Join('_', Regex.Matches(publicationTitle, "[A-z]*")).ToLower();
|
string sanitizedTitle = string.Join('_', Regex.Matches(publicationTitle, "[A-z]*")).ToLower();
|
||||||
string requestUrl = $"https://manganato.com/search/story/{sanitizedTitle}";
|
string requestUrl = $"https://manganato.com/search/story/{sanitizedTitle}";
|
||||||
DownloadClient.RequestResult requestResult =
|
DownloadClient.RequestResult requestResult =
|
||||||
downloadClient.MakeRequest(requestUrl, 1);
|
downloadClient.MakeRequest(requestUrl, 1);
|
||||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
return Array.Empty<Publication>();
|
return Array.Empty<Manga>();
|
||||||
|
|
||||||
return ParsePublicationsFromHtml(requestResult.result);
|
Manga[] publications = ParsePublicationsFromHtml(requestResult.result);
|
||||||
|
Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
|
||||||
|
return publications;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Publication[] ParsePublicationsFromHtml(Stream html)
|
private Manga[] ParsePublicationsFromHtml(Stream html)
|
||||||
{
|
{
|
||||||
StreamReader reader = new (html);
|
StreamReader reader = new (html);
|
||||||
string htmlString = reader.ReadToEnd();
|
string htmlString = reader.ReadToEnd();
|
||||||
@ -46,21 +48,28 @@ public class Manganato : Connector
|
|||||||
.First(a => a.Name == "href").Value);
|
.First(a => a.Name == "href").Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
HashSet<Publication> ret = new();
|
HashSet<Manga> ret = new();
|
||||||
foreach (string url in urls)
|
foreach (string url in urls)
|
||||||
{
|
{
|
||||||
DownloadClient.RequestResult requestResult =
|
Manga? manga = GetMangaFromUrl(url);
|
||||||
downloadClient.MakeRequest(url, 1);
|
if (manga is not null)
|
||||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
ret.Add((Manga)manga);
|
||||||
return Array.Empty<Publication>();
|
|
||||||
|
|
||||||
ret.Add(ParseSinglePublicationFromHtml(requestResult.result, url.Split('/')[^1]));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret.ToArray();
|
return ret.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Publication ParseSinglePublicationFromHtml(Stream html, string publicationId)
|
public override Manga? GetMangaFromUrl(string url)
|
||||||
|
{
|
||||||
|
DownloadClient.RequestResult requestResult =
|
||||||
|
downloadClient.MakeRequest(url, 1);
|
||||||
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return ParseSinglePublicationFromHtml(requestResult.result, url.Split('/')[^1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Manga ParseSinglePublicationFromHtml(Stream html, string publicationId)
|
||||||
{
|
{
|
||||||
StreamReader reader = new (html);
|
StreamReader reader = new (html);
|
||||||
string htmlString = reader.ReadToEnd();
|
string htmlString = reader.ReadToEnd();
|
||||||
@ -119,14 +128,16 @@ public class Manganato : Connector
|
|||||||
.First(s => s.HasClass("chapter-time")).InnerText;
|
.First(s => s.HasClass("chapter-time")).InnerText;
|
||||||
int year = Convert.ToInt32(yearString.Split(',')[^1]) + 2000;
|
int year = Convert.ToInt32(yearString.Split(',')[^1]) + 2000;
|
||||||
|
|
||||||
return new Publication(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
|
Manga manga = new (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
|
||||||
year, originalLanguage, status, publicationId);
|
year, originalLanguage, status, publicationId);
|
||||||
|
cachedPublications.Add(manga);
|
||||||
|
return manga;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Chapter[] GetChapters(Publication publication, string language = "")
|
public override Chapter[] GetChapters(Manga manga, string language="en")
|
||||||
{
|
{
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Chapters for {publication.sortName} {publication.internalId} (language={language})");
|
Log($"Getting chapters {manga}");
|
||||||
string requestUrl = $"https://chapmanganato.com/{publication.publicationId}";
|
string requestUrl = $"https://chapmanganato.com/{manga.publicationId}";
|
||||||
DownloadClient.RequestResult requestResult =
|
DownloadClient.RequestResult requestResult =
|
||||||
downloadClient.MakeRequest(requestUrl, 1);
|
downloadClient.MakeRequest(requestUrl, 1);
|
||||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
@ -137,12 +148,12 @@ public class Manganato : Connector
|
|||||||
{
|
{
|
||||||
NumberDecimalSeparator = "."
|
NumberDecimalSeparator = "."
|
||||||
};
|
};
|
||||||
List<Chapter> chapters = ParseChaptersFromHtml(publication, requestResult.result);
|
List<Chapter> chapters = ParseChaptersFromHtml(manga, requestResult.result);
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Done getting Chapters for {publication.internalId}");
|
Log($"Got {chapters.Count} chapters. {manga}");
|
||||||
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
|
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Chapter> ParseChaptersFromHtml(Publication publication, Stream html)
|
private List<Chapter> ParseChaptersFromHtml(Manga manga, Stream html)
|
||||||
{
|
{
|
||||||
StreamReader reader = new (html);
|
StreamReader reader = new (html);
|
||||||
string htmlString = reader.ReadToEnd();
|
string htmlString = reader.ReadToEnd();
|
||||||
@ -161,17 +172,18 @@ public class Manganato : Connector
|
|||||||
string chapterName = string.Concat(fullString.Split(':')[1..]);
|
string chapterName = string.Concat(fullString.Split(':')[1..]);
|
||||||
string url = chapterInfo.Descendants("a").First(d => d.HasClass("chapter-name"))
|
string url = chapterInfo.Descendants("a").First(d => d.HasClass("chapter-name"))
|
||||||
.GetAttributeValue("href", "");
|
.GetAttributeValue("href", "");
|
||||||
ret.Add(new Chapter(publication, chapterName, volumeNumber, chapterNumber, url));
|
ret.Add(new Chapter(manga, chapterName, volumeNumber, chapterNumber, url));
|
||||||
}
|
}
|
||||||
ret.Reverse();
|
ret.Reverse();
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null)
|
public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
|
||||||
{
|
{
|
||||||
if (cancellationToken?.IsCancellationRequested ?? false)
|
if (progressToken?.cancellationRequested ?? false)
|
||||||
return HttpStatusCode.RequestTimeout;
|
return HttpStatusCode.RequestTimeout;
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
|
Manga chapterParentManga = chapter.parentManga;
|
||||||
|
Log($"Retrieving chapter-info {chapter} {chapterParentManga}");
|
||||||
string requestUrl = chapter.url;
|
string requestUrl = chapter.url;
|
||||||
DownloadClient.RequestResult requestResult =
|
DownloadClient.RequestResult requestResult =
|
||||||
downloadClient.MakeRequest(requestUrl, 1);
|
downloadClient.MakeRequest(requestUrl, 1);
|
||||||
@ -183,7 +195,7 @@ public class Manganato : Connector
|
|||||||
string comicInfoPath = Path.GetTempFileName();
|
string comicInfoPath = Path.GetTempFileName();
|
||||||
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
|
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
|
||||||
|
|
||||||
return DownloadChapterImages(imageUrls, chapter.GetArchiveFilePath(settings.downloadLocation), 1, parentTask, comicInfoPath, "https://chapmanganato.com/", cancellationToken);
|
return DownloadChapterImages(imageUrls, chapter.GetArchiveFilePath(settings.downloadLocation), 1, comicInfoPath, "https://chapmanganato.com/", progressToken:progressToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string[] ParseImageUrlsFromHtml(Stream html)
|
private string[] ParseImageUrlsFromHtml(Stream html)
|
@ -5,23 +5,23 @@ using System.Xml.Linq;
|
|||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using PuppeteerSharp;
|
using PuppeteerSharp;
|
||||||
using Tranga.TrangaTasks;
|
using Tranga.Jobs;
|
||||||
|
|
||||||
namespace Tranga.Connectors;
|
namespace Tranga.MangaConnectors;
|
||||||
|
|
||||||
public class Mangasee : Connector
|
public class Mangasee : MangaConnector
|
||||||
{
|
{
|
||||||
public override string name { get; }
|
public override string name { get; }
|
||||||
private IBrowser? _browser;
|
private IBrowser? _browser;
|
||||||
private const string ChromiumVersion = "1154303";
|
private const string ChromiumVersion = "1154303";
|
||||||
|
|
||||||
public Mangasee(TrangaSettings settings, CommonObjects commonObjects) : base(settings, commonObjects)
|
public Mangasee(GlobalBase clone) : base(clone)
|
||||||
{
|
{
|
||||||
this.name = "Mangasee";
|
this.name = "Mangasee";
|
||||||
this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
|
this.downloadClient = new DownloadClient(clone, new Dictionary<byte, int>()
|
||||||
{
|
{
|
||||||
{ 1, 60 }
|
{ 1, 60 }
|
||||||
}, commonObjects.logger);
|
});
|
||||||
|
|
||||||
Task d = new Task(DownloadBrowser);
|
Task d = new Task(DownloadBrowser);
|
||||||
d.Start();
|
d.Start();
|
||||||
@ -34,31 +34,29 @@ public class Mangasee : Connector
|
|||||||
browserFetcher.Remove(rev);
|
browserFetcher.Remove(rev);
|
||||||
if (!browserFetcher.LocalRevisions().Contains(ChromiumVersion))
|
if (!browserFetcher.LocalRevisions().Contains(ChromiumVersion))
|
||||||
{
|
{
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), "Downloading headless browser");
|
Log("Downloading headless browser");
|
||||||
DateTime last = DateTime.Now.Subtract(TimeSpan.FromSeconds(5));
|
DateTime last = DateTime.Now.Subtract(TimeSpan.FromSeconds(5));
|
||||||
browserFetcher.DownloadProgressChanged += (_, args) =>
|
browserFetcher.DownloadProgressChanged += (_, args) =>
|
||||||
{
|
{
|
||||||
double currentBytes = Convert.ToDouble(args.BytesReceived) / Convert.ToDouble(args.TotalBytesToReceive);
|
double currentBytes = Convert.ToDouble(args.BytesReceived) / Convert.ToDouble(args.TotalBytesToReceive);
|
||||||
if (args.TotalBytesToReceive == args.BytesReceived)
|
if (args.TotalBytesToReceive == args.BytesReceived)
|
||||||
|
Log("Browser downloaded.");
|
||||||
|
else if (DateTime.Now > last.AddSeconds(1))
|
||||||
{
|
{
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), "Browser downloaded.");
|
Log($"Browser download progress: {currentBytes:P2}");
|
||||||
}
|
|
||||||
else if (DateTime.Now > last.AddSeconds(5))
|
|
||||||
{
|
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Browser download progress: {currentBytes:P2}");
|
|
||||||
last = DateTime.Now;
|
last = DateTime.Now;
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
if (!browserFetcher.CanDownloadAsync(ChromiumVersion).Result)
|
if (!browserFetcher.CanDownloadAsync(ChromiumVersion).Result)
|
||||||
{
|
{
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Can't download browser version {ChromiumVersion}");
|
Log($"Can't download browser version {ChromiumVersion}");
|
||||||
return;
|
throw new Exception();
|
||||||
}
|
}
|
||||||
await browserFetcher.DownloadAsync(ChromiumVersion);
|
await browserFetcher.DownloadAsync(ChromiumVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), "Starting browser.");
|
Log("Starting Browser.");
|
||||||
this._browser = await Puppeteer.LaunchAsync(new LaunchOptions
|
this._browser = await Puppeteer.LaunchAsync(new LaunchOptions
|
||||||
{
|
{
|
||||||
Headless = true,
|
Headless = true,
|
||||||
@ -71,19 +69,43 @@ public class Mangasee : Connector
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Publication[] GetPublicationsInternal(string publicationTitle = "")
|
public override Manga[] GetManga(string publicationTitle = "")
|
||||||
{
|
{
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Publications (title={publicationTitle})");
|
Log($"Searching Publications. Term=\"{publicationTitle}\"");
|
||||||
string requestUrl = $"https://mangasee123.com/_search.php";
|
string requestUrl = $"https://mangasee123.com/_search.php";
|
||||||
DownloadClient.RequestResult requestResult =
|
DownloadClient.RequestResult requestResult =
|
||||||
downloadClient.MakeRequest(requestUrl, 1);
|
downloadClient.MakeRequest(requestUrl, 1);
|
||||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
return Array.Empty<Publication>();
|
return Array.Empty<Manga>();
|
||||||
|
|
||||||
return ParsePublicationsFromHtml(requestResult.result, publicationTitle);
|
Manga[] publications = ParsePublicationsFromHtml(requestResult.result, publicationTitle);
|
||||||
|
Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
|
||||||
|
return publications;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Publication[] ParsePublicationsFromHtml(Stream html, string publicationTitle)
|
public override Manga? GetMangaFromUrl(string url)
|
||||||
|
{
|
||||||
|
while (this._browser is null)
|
||||||
|
{
|
||||||
|
Log("Waiting for headless browser to download...");
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
IPage page = _browser!.NewPageAsync().Result;
|
||||||
|
IResponse response = page.GoToAsync(url, WaitUntilNavigation.DOMContentLoaded).Result;
|
||||||
|
if (response.Ok)
|
||||||
|
{
|
||||||
|
HtmlDocument document = new();
|
||||||
|
document.LoadHtml(page.GetContentAsync().Result);
|
||||||
|
page.CloseAsync();
|
||||||
|
return ParseSinglePublicationFromHtml(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Manga[] ParsePublicationsFromHtml(Stream html, string publicationTitle)
|
||||||
{
|
{
|
||||||
string jsonString = new StreamReader(html).ReadToEnd();
|
string jsonString = new StreamReader(html).ReadToEnd();
|
||||||
List<SearchResultItem> result = JsonConvert.DeserializeObject<List<SearchResultItem>>(jsonString)!;
|
List<SearchResultItem> result = JsonConvert.DeserializeObject<List<SearchResultItem>>(jsonString)!;
|
||||||
@ -98,80 +120,60 @@ public class Mangasee : Connector
|
|||||||
queryFiltered = queryFiltered.Where(item => item.Value >= publicationTitle.Split(' ').Length - 1)
|
queryFiltered = queryFiltered.Where(item => item.Value >= publicationTitle.Split(' ').Length - 1)
|
||||||
.ToDictionary(item => item.Key, item => item.Value);
|
.ToDictionary(item => item.Key, item => item.Value);
|
||||||
|
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Got {queryFiltered.Count} Publications (title={publicationTitle})");
|
Log($"Retrieved {queryFiltered.Count} publications.");
|
||||||
|
|
||||||
HashSet<Publication> ret = new();
|
HashSet<Manga> ret = new();
|
||||||
List<SearchResultItem> orderedFiltered =
|
List<SearchResultItem> orderedFiltered =
|
||||||
queryFiltered.OrderBy(item => item.Value).ToDictionary(item => item.Key, item => item.Value).Keys.ToList();
|
queryFiltered.OrderBy(item => item.Value).ToDictionary(item => item.Key, item => item.Value).Keys.ToList();
|
||||||
|
|
||||||
uint index = 1;
|
|
||||||
foreach (SearchResultItem orderedItem in orderedFiltered)
|
foreach (SearchResultItem orderedItem in orderedFiltered)
|
||||||
{
|
{
|
||||||
DownloadClient.RequestResult requestResult =
|
Manga? manga = GetMangaFromUrl($"https://mangasee123.com/manga/{orderedItem.i}");
|
||||||
downloadClient.MakeRequest($"https://mangasee123.com/manga/{orderedItem.i}", 1);
|
if (manga is not null)
|
||||||
if ((int)requestResult.statusCode >= 200 || (int)requestResult.statusCode < 300)
|
ret.Add((Manga)manga);
|
||||||
{
|
|
||||||
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();
|
return ret.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private Publication ParseSinglePublicationFromHtml(Stream html, string sortName, string publicationId, string[] a)
|
private Manga ParseSinglePublicationFromHtml(HtmlDocument document)
|
||||||
{
|
{
|
||||||
StreamReader reader = new (html);
|
|
||||||
HtmlDocument document = new ();
|
|
||||||
document.LoadHtml(reader.ReadToEnd());
|
|
||||||
|
|
||||||
string originalLanguage = "", status = "";
|
string originalLanguage = "", status = "";
|
||||||
Dictionary<string, string> altTitles = new(), links = new();
|
Dictionary<string, string> altTitles = new(), links = new();
|
||||||
HashSet<string> tags = new();
|
HashSet<string> tags = new();
|
||||||
|
|
||||||
HtmlNode posterNode =
|
HtmlNode posterNode = document.DocumentNode.SelectSingleNode("//div[@class='BoxBody']//div[@class='row']//img");
|
||||||
document.DocumentNode.Descendants("img").First(img => img.HasClass("img-fluid") && img.HasClass("bottom-5"));
|
|
||||||
string posterUrl = posterNode.GetAttributeValue("src", "");
|
string posterUrl = posterNode.GetAttributeValue("src", "");
|
||||||
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, 1);
|
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, 1);
|
||||||
|
|
||||||
HtmlNode attributes = document.DocumentNode.Descendants("div")
|
HtmlNode titleNode = document.DocumentNode.SelectSingleNode("//div[@class='BoxBody']//div[@class='row']//h1");
|
||||||
.First(div => div.HasClass("col-md-9") && div.HasClass("col-sm-8") && div.HasClass("top-5"))
|
string sortName = titleNode.InnerText;
|
||||||
.Descendants("ul").First();
|
string publicationId = sortName;
|
||||||
|
|
||||||
HtmlNode[] authorsNodes = attributes.Descendants("li")
|
HtmlNode[] authorsNodes = document.DocumentNode.SelectNodes("//div[@class='BoxBody']//div[@class='row']//span[text()='Author(s):']/..").Descendants("a").ToArray();
|
||||||
.First(node => node.InnerText.Contains("author(s):", StringComparison.CurrentCultureIgnoreCase))
|
|
||||||
.Descendants("a").ToArray();
|
|
||||||
List<string> authors = new();
|
List<string> authors = new();
|
||||||
foreach(HtmlNode authorNode in authorsNodes)
|
foreach(HtmlNode authorNode in authorsNodes)
|
||||||
authors.Add(authorNode.InnerText);
|
authors.Add(authorNode.InnerText);
|
||||||
|
|
||||||
HtmlNode[] genreNodes = attributes.Descendants("li")
|
HtmlNode[] genreNodes = document.DocumentNode.SelectNodes("//div[@class='BoxBody']//div[@class='row']//span[text()='Genre(s):']/..").Descendants("a").ToArray();
|
||||||
.First(node => node.InnerText.Contains("genre(s):", StringComparison.CurrentCultureIgnoreCase))
|
|
||||||
.Descendants("a").ToArray();
|
|
||||||
foreach (HtmlNode genreNode in genreNodes)
|
foreach (HtmlNode genreNode in genreNodes)
|
||||||
tags.Add(genreNode.InnerText);
|
tags.Add(genreNode.InnerText);
|
||||||
|
|
||||||
HtmlNode yearNode = attributes.Descendants("li")
|
HtmlNode yearNode = document.DocumentNode.SelectNodes("//div[@class='BoxBody']//div[@class='row']//span[text()='Released:']/..").Descendants("a").First();
|
||||||
.First(node => node.InnerText.Contains("released:", StringComparison.CurrentCultureIgnoreCase))
|
|
||||||
.Descendants("a").First();
|
|
||||||
int year = Convert.ToInt32(yearNode.InnerText);
|
int year = Convert.ToInt32(yearNode.InnerText);
|
||||||
|
|
||||||
HtmlNode[] statusNodes = attributes.Descendants("li")
|
HtmlNode[] statusNodes = document.DocumentNode.SelectNodes("//div[@class='BoxBody']//div[@class='row']//span[text()='Status:']/..").Descendants("a").ToArray();
|
||||||
.First(node => node.InnerText.Contains("status:", StringComparison.CurrentCultureIgnoreCase))
|
|
||||||
.Descendants("a").ToArray();
|
|
||||||
foreach(HtmlNode statusNode in statusNodes)
|
foreach(HtmlNode statusNode in statusNodes)
|
||||||
if (statusNode.InnerText.Contains("publish", StringComparison.CurrentCultureIgnoreCase))
|
if (statusNode.InnerText.Contains("publish", StringComparison.CurrentCultureIgnoreCase))
|
||||||
status = statusNode.InnerText.Split(' ')[0];
|
status = statusNode.InnerText.Split(' ')[0];
|
||||||
|
|
||||||
HtmlNode descriptionNode = attributes.Descendants("li").First(node => node.InnerText.Contains("description:", StringComparison.CurrentCultureIgnoreCase)).Descendants("div").First();
|
HtmlNode descriptionNode = document.DocumentNode.SelectNodes("//div[@class='BoxBody']//div[@class='row']//span[text()='Description:']/..").Descendants("div").First();
|
||||||
string description = descriptionNode.InnerText;
|
string description = descriptionNode.InnerText;
|
||||||
|
|
||||||
int i = 0;
|
Manga manga = new (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
|
||||||
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);
|
year, originalLanguage, status, publicationId);
|
||||||
|
cachedPublications.Add(manga);
|
||||||
|
return manga;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReSharper disable once ClassNeverInstantiated.Local Will be instantiated during deserialization
|
// ReSharper disable once ClassNeverInstantiated.Local Will be instantiated during deserialization
|
||||||
@ -214,11 +216,12 @@ public class Mangasee : Connector
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Chapter[] GetChapters(Publication publication, string language = "")
|
public override Chapter[] GetChapters(Manga manga, string language="en")
|
||||||
{
|
{
|
||||||
XDocument doc = XDocument.Load($"https://mangasee123.com/rss/{publication.publicationId}.xml");
|
Log($"Getting chapters {manga}");
|
||||||
|
XDocument doc = XDocument.Load($"https://mangasee123.com/rss/{manga.publicationId}.xml");
|
||||||
XElement[] chapterItems = doc.Descendants("item").ToArray();
|
XElement[] chapterItems = doc.Descendants("item").ToArray();
|
||||||
List<Chapter> ret = new();
|
List<Chapter> chapters = new();
|
||||||
foreach (XElement chapter in chapterItems)
|
foreach (XElement chapter in chapterItems)
|
||||||
{
|
{
|
||||||
string volumeNumber = "1";
|
string volumeNumber = "1";
|
||||||
@ -227,7 +230,7 @@ public class Mangasee : Connector
|
|||||||
|
|
||||||
string url = chapter.Descendants("link").First().Value;
|
string url = chapter.Descendants("link").First().Value;
|
||||||
url = url.Replace(Regex.Matches(url,"(-page-[0-9])")[0].ToString(),"");
|
url = url.Replace(Regex.Matches(url,"(-page-[0-9])")[0].ToString(),"");
|
||||||
ret.Add(new Chapter(publication, "", volumeNumber, chapterNumber, url));
|
chapters.Add(new Chapter(manga, "", volumeNumber, chapterNumber, url));
|
||||||
}
|
}
|
||||||
|
|
||||||
//Return Chapters ordered by Chapter-Number
|
//Return Chapters ordered by Chapter-Number
|
||||||
@ -235,23 +238,24 @@ public class Mangasee : Connector
|
|||||||
{
|
{
|
||||||
NumberDecimalSeparator = "."
|
NumberDecimalSeparator = "."
|
||||||
};
|
};
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Done getting Chapters for {publication.internalId}");
|
Log($"Got {chapters.Count} chapters. {manga}");
|
||||||
return ret.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
|
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null)
|
public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
|
||||||
{
|
{
|
||||||
if (cancellationToken?.IsCancellationRequested ?? false)
|
if (progressToken?.cancellationRequested ?? false)
|
||||||
return HttpStatusCode.RequestTimeout;
|
return HttpStatusCode.RequestTimeout;
|
||||||
while (this._browser is null && !(cancellationToken?.IsCancellationRequested??false))
|
Manga chapterParentManga = chapter.parentManga;
|
||||||
|
while (this._browser is null && !(progressToken?.cancellationRequested??false))
|
||||||
{
|
{
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), "Waiting for headless browser to download...");
|
Log("Waiting for headless browser to download...");
|
||||||
Thread.Sleep(1000);
|
Thread.Sleep(1000);
|
||||||
}
|
}
|
||||||
if (cancellationToken?.IsCancellationRequested??false)
|
if (progressToken?.cancellationRequested??false)
|
||||||
return HttpStatusCode.RequestTimeout;
|
return HttpStatusCode.RequestTimeout;
|
||||||
|
|
||||||
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
|
Log($"Retrieving chapter-info {chapter} {chapterParentManga}");
|
||||||
IPage page = _browser!.NewPageAsync().Result;
|
IPage page = _browser!.NewPageAsync().Result;
|
||||||
IResponse response = page.GoToAsync(chapter.url).Result;
|
IResponse response = page.GoToAsync(chapter.url).Result;
|
||||||
if (response.Ok)
|
if (response.Ok)
|
||||||
@ -268,7 +272,7 @@ public class Mangasee : Connector
|
|||||||
string comicInfoPath = Path.GetTempFileName();
|
string comicInfoPath = Path.GetTempFileName();
|
||||||
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
|
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
|
||||||
|
|
||||||
return DownloadChapterImages(urls.ToArray(), chapter.GetArchiveFilePath(settings.downloadLocation), 1, parentTask, comicInfoPath, cancellationToken:cancellationToken);
|
return DownloadChapterImages(urls.ToArray(), chapter.GetArchiveFilePath(settings.downloadLocation), 1, comicInfoPath, progressToken:progressToken);
|
||||||
}
|
}
|
||||||
return response.Status;
|
return response.Status;
|
||||||
}
|
}
|
@ -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<ushort>()
|
|
||||||
: settingsNode["ts"]!["version"]!.GetValue<ushort>();
|
|
||||||
logger?.WriteLine("Migrator", $"Migrating {version} -> {CurrentVersion}");
|
|
||||||
switch (version)
|
|
||||||
{
|
|
||||||
case 15:
|
|
||||||
MoveToCommonObjects(settingsFilePath, logger);
|
|
||||||
TrangaSettings.SettingsJsonObject sjo = JsonConvert.DeserializeObject<TrangaSettings.SettingsJsonObject>(File.ReadAllText(settingsFilePath))!;
|
|
||||||
RemoveUpdateLibraryTask(sjo.ts!, logger);
|
|
||||||
break;
|
|
||||||
case 16:
|
|
||||||
MoveToCommonObjects(settingsFilePath, logger);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
TrangaSettings.SettingsJsonObject sjo2 = JsonConvert.DeserializeObject<TrangaSettings.SettingsJsonObject>(
|
|
||||||
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<TrangaTask> tasks = JsonConvert.DeserializeObject<HashSet<TrangaTask>>(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<string>(),
|
|
||||||
node["workingDirectory"]!.GetValue<string>());
|
|
||||||
JsonArray libraryManagers = node["libraryManagers"]!.AsArray();
|
|
||||||
logger?.WriteLine("Migrator", $"\tGot {libraryManagers.Count} libraryManagers.");
|
|
||||||
JsonNode? komgaNode = libraryManagers.FirstOrDefault(lm => lm["libraryType"].GetValue<byte>() == (byte)LibraryManager.LibraryType.Komga);
|
|
||||||
JsonNode? kavitaNode = libraryManagers.FirstOrDefault(lm => lm["libraryType"].GetValue<byte>() == (byte)LibraryManager.LibraryType.Kavita);
|
|
||||||
HashSet<LibraryManager> lms = new();
|
|
||||||
if (komgaNode is not null)
|
|
||||||
lms.Add(new Komga(komgaNode["baseUrl"]!.GetValue<string>(), komgaNode["auth"]!.GetValue<string>(), null));
|
|
||||||
if (kavitaNode is not null)
|
|
||||||
lms.Add(new Kavita(kavitaNode["baseUrl"]!.GetValue<string>(), kavitaNode["auth"]!.GetValue<string>(), null));
|
|
||||||
|
|
||||||
JsonArray notificationManagers = node["notificationManagers"]!.AsArray();
|
|
||||||
logger?.WriteLine("Migrator", $"\tGot {notificationManagers.Count} notificationManagers.");
|
|
||||||
JsonNode? gotifyNode = notificationManagers.FirstOrDefault(nm =>
|
|
||||||
nm["notificationManagerType"].GetValue<byte>() == (byte)NotificationManager.NotificationManagerType.Gotify);
|
|
||||||
JsonNode? lunaSeaNode = notificationManagers.FirstOrDefault(nm =>
|
|
||||||
nm["notificationManagerType"].GetValue<byte>() == (byte)NotificationManager.NotificationManagerType.LunaSea);
|
|
||||||
HashSet<NotificationManager> nms = new();
|
|
||||||
if (gotifyNode is not null)
|
|
||||||
nms.Add(new Gotify(gotifyNode["endpoint"]!.GetValue<string>(), gotifyNode["appToken"]!.GetValue<string>()));
|
|
||||||
if (lunaSeaNode is not null)
|
|
||||||
nms.Add(new LunaSea(lunaSeaNode["id"]!.GetValue<string>()));
|
|
||||||
|
|
||||||
CommonObjects co = new (lms, nms, logger, settingsFilePath);
|
|
||||||
|
|
||||||
TrangaSettings.SettingsJsonObject sjo = new(ts, co);
|
|
||||||
File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(sjo));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +1,9 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Logging;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Tranga.NotificationManagers;
|
namespace Tranga.NotificationConnectors;
|
||||||
|
|
||||||
public class Gotify : NotificationManager
|
public class Gotify : NotificationConnector
|
||||||
{
|
{
|
||||||
public string endpoint { get; }
|
public string endpoint { get; }
|
||||||
// ReSharper disable once MemberCanBePrivate.Global
|
// ReSharper disable once MemberCanBePrivate.Global
|
||||||
@ -12,15 +11,20 @@ public class Gotify : NotificationManager
|
|||||||
private readonly HttpClient _client = new();
|
private readonly HttpClient _client = new();
|
||||||
|
|
||||||
[JsonConstructor]
|
[JsonConstructor]
|
||||||
public Gotify(string endpoint, string appToken, Logger? logger = null) : base(NotificationManagerType.Gotify, logger)
|
public Gotify(GlobalBase clone, string endpoint, string appToken) : base(clone, NotificationConnectorType.Gotify)
|
||||||
{
|
{
|
||||||
this.endpoint = endpoint;
|
this.endpoint = endpoint;
|
||||||
this.appToken = appToken;
|
this.appToken = appToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"Gotify {endpoint}";
|
||||||
|
}
|
||||||
|
|
||||||
public override void SendNotification(string title, string notificationText)
|
public override void SendNotification(string title, string notificationText)
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Sending notification: {title} - {notificationText}");
|
Log($"Sending notification: {title} - {notificationText}");
|
||||||
MessageData message = new(title, notificationText);
|
MessageData message = new(title, notificationText);
|
||||||
HttpRequestMessage request = new(HttpMethod.Post, $"{endpoint}/message");
|
HttpRequestMessage request = new(HttpMethod.Post, $"{endpoint}/message");
|
||||||
request.Headers.Add("X-Gotify-Key", this.appToken);
|
request.Headers.Add("X-Gotify-Key", this.appToken);
|
||||||
@ -29,7 +33,7 @@ public class Gotify : NotificationManager
|
|||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
StreamReader sr = new (response.Content.ReadAsStream());
|
StreamReader sr = new (response.Content.ReadAsStream());
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"{response.StatusCode}: {sr.ReadToEnd()}");
|
Log($"{response.StatusCode}: {sr.ReadToEnd()}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,24 +1,28 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Logging;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Tranga.NotificationManagers;
|
namespace Tranga.NotificationConnectors;
|
||||||
|
|
||||||
public class LunaSea : NotificationManager
|
public class LunaSea : NotificationConnector
|
||||||
{
|
{
|
||||||
// ReSharper disable once MemberCanBePrivate.Global
|
// ReSharper disable once MemberCanBePrivate.Global
|
||||||
public string id { get; init; }
|
public string id { get; init; }
|
||||||
private readonly HttpClient _client = new();
|
private readonly HttpClient _client = new();
|
||||||
|
|
||||||
[JsonConstructor]
|
[JsonConstructor]
|
||||||
public LunaSea(string id, Logger? logger = null) : base(NotificationManagerType.LunaSea, logger)
|
public LunaSea(GlobalBase clone, string id) : base(clone, NotificationConnectorType.LunaSea)
|
||||||
{
|
{
|
||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"LunaSea {id}";
|
||||||
|
}
|
||||||
|
|
||||||
public override void SendNotification(string title, string notificationText)
|
public override void SendNotification(string title, string notificationText)
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Sending notification: {title} - {notificationText}");
|
Log($"Sending notification: {title} - {notificationText}");
|
||||||
MessageData message = new(title, notificationText);
|
MessageData message = new(title, notificationText);
|
||||||
HttpRequestMessage request = new(HttpMethod.Post, $"https://notify.lunasea.app/v1/custom/{id}");
|
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");
|
request.Content = new StringContent(JsonConvert.SerializeObject(message, Formatting.None), Encoding.UTF8, "application/json");
|
||||||
@ -26,7 +30,7 @@ public class LunaSea : NotificationManager
|
|||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
StreamReader sr = new (response.Content.ReadAsStream());
|
StreamReader sr = new (response.Content.ReadAsStream());
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"{response.StatusCode}: {sr.ReadToEnd()}");
|
Log($"{response.StatusCode}: {sr.ReadToEnd()}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
15
Tranga/NotificationConnectors/NotificationConnector.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
namespace Tranga.NotificationConnectors;
|
||||||
|
|
||||||
|
public abstract class NotificationConnector : GlobalBase
|
||||||
|
{
|
||||||
|
public readonly NotificationConnectorType notificationConnectorType;
|
||||||
|
|
||||||
|
protected NotificationConnector(GlobalBase clone, NotificationConnectorType notificationConnectorType) : base(clone)
|
||||||
|
{
|
||||||
|
this.notificationConnectorType = notificationConnectorType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum NotificationConnectorType : byte { Gotify = 0, LunaSea = 1 }
|
||||||
|
|
||||||
|
public abstract void SendNotification(string title, string notificationText);
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace Tranga.NotificationConnectors;
|
||||||
|
|
||||||
|
public class NotificationManagerJsonConverter : JsonConverter
|
||||||
|
{
|
||||||
|
private GlobalBase _clone;
|
||||||
|
|
||||||
|
public NotificationManagerJsonConverter(GlobalBase clone)
|
||||||
|
{
|
||||||
|
this._clone = clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanConvert(Type objectType)
|
||||||
|
{
|
||||||
|
return (objectType == typeof(NotificationConnector));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue,
|
||||||
|
JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
JObject jo = JObject.Load(reader);
|
||||||
|
if (jo["notificationConnectorType"]!.Value<byte>() == (byte)NotificationConnector.NotificationConnectorType.Gotify)
|
||||||
|
return new Gotify(this._clone, jo.GetValue("endpoint")!.Value<string>()!, jo.GetValue("appToken")!.Value<string>()!);
|
||||||
|
else if (jo["notificationConnectorType"]!.Value<byte>() ==
|
||||||
|
(byte)NotificationConnector.NotificationConnectorType.LunaSea)
|
||||||
|
return new LunaSea(this._clone, jo.GetValue("id")!.Value<string>()!);
|
||||||
|
|
||||||
|
throw new Exception();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanWrite => false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Don't call this
|
||||||
|
/// </summary>
|
||||||
|
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
throw new Exception("Dont call this");
|
||||||
|
}
|
||||||
|
}
|
@ -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>() == (byte)NotificationManagerType.Gotify)
|
|
||||||
return jo.ToObject<Gotify>(serializer)!;
|
|
||||||
else if (jo["notificationManagerType"]!.Value<byte>() == (byte)NotificationManagerType.LunaSea)
|
|
||||||
return jo.ToObject<LunaSea>(serializer)!;
|
|
||||||
|
|
||||||
throw new Exception();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool CanWrite => false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Don't call this
|
|
||||||
/// </summary>
|
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
throw new Exception("Dont call this");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
468
Tranga/Server.cs
Normal file
@ -0,0 +1,468 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Tranga.Jobs;
|
||||||
|
using Tranga.LibraryConnectors;
|
||||||
|
using Tranga.MangaConnectors;
|
||||||
|
using Tranga.NotificationConnectors;
|
||||||
|
|
||||||
|
namespace Tranga;
|
||||||
|
|
||||||
|
public class Server : GlobalBase
|
||||||
|
{
|
||||||
|
private readonly HttpListener _listener = new ();
|
||||||
|
private readonly Tranga _parent;
|
||||||
|
|
||||||
|
public Server(Tranga parent) : base(parent)
|
||||||
|
{
|
||||||
|
this._parent = parent;
|
||||||
|
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
this._listener.Prefixes.Add($"http://*:{settings.apiPortNumber}/");
|
||||||
|
else
|
||||||
|
this._listener.Prefixes.Add($"http://localhost:{settings.apiPortNumber}/");
|
||||||
|
Thread listenThread = new (Listen);
|
||||||
|
listenThread.Start();
|
||||||
|
Thread watchThread = new(WatchRunning);
|
||||||
|
watchThread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WatchRunning()
|
||||||
|
{
|
||||||
|
while(_parent.keepRunning)
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
this._listener.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Listen()
|
||||||
|
{
|
||||||
|
this._listener.Start();
|
||||||
|
foreach(string prefix in this._listener.Prefixes)
|
||||||
|
Log($"Listening on {prefix}");
|
||||||
|
while (this._listener.IsListening && _parent.keepRunning)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HttpListenerContext context = this._listener.GetContext();
|
||||||
|
//Log($"{context.Request.HttpMethod} {context.Request.Url} {context.Request.UserAgent}");
|
||||||
|
Task t = new(() =>
|
||||||
|
{
|
||||||
|
HandleRequest(context);
|
||||||
|
});
|
||||||
|
t.Start();
|
||||||
|
}
|
||||||
|
catch (HttpListenerException e)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleRequest(HttpListenerContext context)
|
||||||
|
{
|
||||||
|
HttpListenerRequest request = context.Request;
|
||||||
|
HttpListenerResponse response = context.Response;
|
||||||
|
if(request.HttpMethod == "OPTIONS")
|
||||||
|
SendResponse(HttpStatusCode.OK, context.Response);
|
||||||
|
if(request.Url!.LocalPath.Contains("favicon"))
|
||||||
|
SendResponse(HttpStatusCode.NoContent, response);
|
||||||
|
|
||||||
|
switch (request.HttpMethod)
|
||||||
|
{
|
||||||
|
case "GET":
|
||||||
|
HandleGet(request, response);
|
||||||
|
break;
|
||||||
|
case "POST":
|
||||||
|
HandlePost(request, response);
|
||||||
|
break;
|
||||||
|
case "DELETE":
|
||||||
|
HandleDelete(request, response);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
SendResponse(HttpStatusCode.BadRequest, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<string, string> GetRequestVariables(string query)
|
||||||
|
{
|
||||||
|
Dictionary<string, string> ret = new();
|
||||||
|
Regex queryRex = new (@"\?{1}&?([A-z0-9-=]+=[A-z0-9-=]+)+(&[A-z0-9-=]+=[A-z0-9-=]+)*");
|
||||||
|
if (!queryRex.IsMatch(query))
|
||||||
|
return ret;
|
||||||
|
query = query.Substring(1);
|
||||||
|
foreach (string keyValuePair in query.Split('&').Where(str => str.Length >= 3))
|
||||||
|
{
|
||||||
|
string var = keyValuePair.Split('=')[0];
|
||||||
|
string val = Regex.Replace(keyValuePair.Substring(var.Length + 1), "%20", " ");
|
||||||
|
val = Regex.Replace(val, "%[0-9]{2}", "");
|
||||||
|
ret.Add(var, val);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleGet(HttpListenerRequest request, HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
Dictionary<string, string> requestVariables = GetRequestVariables(request.Url!.Query);
|
||||||
|
string? connectorName, jobId, internalId;
|
||||||
|
MangaConnector? connector;
|
||||||
|
Manga? manga;
|
||||||
|
string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value;
|
||||||
|
switch (path)
|
||||||
|
{
|
||||||
|
case "Connectors":
|
||||||
|
SendResponse(HttpStatusCode.OK, response, _parent.GetConnectors().Select(con => con.name).ToArray());
|
||||||
|
break;
|
||||||
|
case "Manga/Cover":
|
||||||
|
if (!requestVariables.TryGetValue("internalId", out internalId) ||
|
||||||
|
!_parent.TryGetPublicationById(internalId, out manga))
|
||||||
|
{
|
||||||
|
SendResponse(HttpStatusCode.BadRequest, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
string filePath = settings.GetFullCoverPath((Manga)manga!);
|
||||||
|
if (File.Exists(filePath))
|
||||||
|
{
|
||||||
|
FileStream coverStream = new(filePath, FileMode.Open);
|
||||||
|
SendResponse(HttpStatusCode.OK, response, coverStream);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SendResponse(HttpStatusCode.NotFound, response);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "Manga/FromConnector":
|
||||||
|
requestVariables.TryGetValue("title", out string? title);
|
||||||
|
requestVariables.TryGetValue("url", out string? url);
|
||||||
|
if (!requestVariables.TryGetValue("connector", out connectorName) ||
|
||||||
|
!_parent.TryGetConnector(connectorName, out connector) ||
|
||||||
|
(title is null && url is null))
|
||||||
|
{
|
||||||
|
SendResponse(HttpStatusCode.BadRequest, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url is not null)
|
||||||
|
{
|
||||||
|
HashSet<Manga> ret = new();
|
||||||
|
manga = connector!.GetMangaFromUrl(url);
|
||||||
|
if (manga is not null)
|
||||||
|
ret.Add((Manga)manga);
|
||||||
|
SendResponse(HttpStatusCode.OK, response, ret);
|
||||||
|
}else
|
||||||
|
SendResponse(HttpStatusCode.OK, response, connector!.GetManga(title!));
|
||||||
|
break;
|
||||||
|
case "Manga/Chapters":
|
||||||
|
if(!requestVariables.TryGetValue("connector", out connectorName) ||
|
||||||
|
!requestVariables.TryGetValue("internalId", out internalId) ||
|
||||||
|
!_parent.TryGetConnector(connectorName, out connector) ||
|
||||||
|
!_parent.TryGetPublicationById(internalId, out manga))
|
||||||
|
{
|
||||||
|
SendResponse(HttpStatusCode.BadRequest, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
SendResponse(HttpStatusCode.OK, response, connector!.GetChapters((Manga)manga!));
|
||||||
|
break;
|
||||||
|
case "Jobs":
|
||||||
|
if (!requestVariables.TryGetValue("jobId", out jobId))
|
||||||
|
{
|
||||||
|
if(!_parent.jobBoss.jobs.Any(jjob => jjob.id == jobId))
|
||||||
|
SendResponse(HttpStatusCode.BadRequest, response);
|
||||||
|
else
|
||||||
|
SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.First(jjob => jjob.id == jobId));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs);
|
||||||
|
break;
|
||||||
|
case "Jobs/Progress":
|
||||||
|
if (!requestVariables.TryGetValue("jobId", out jobId))
|
||||||
|
{
|
||||||
|
if(!_parent.jobBoss.jobs.Any(jjob => jjob.id == jobId))
|
||||||
|
SendResponse(HttpStatusCode.BadRequest, response);
|
||||||
|
else
|
||||||
|
SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.First(jjob => jjob.id == jobId).progressToken);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Select(jjob => jjob.progressToken));
|
||||||
|
break;
|
||||||
|
case "Jobs/Running":
|
||||||
|
SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Where(jjob => jjob.progressToken.state is ProgressToken.State.Running));
|
||||||
|
break;
|
||||||
|
case "Jobs/Waiting":
|
||||||
|
SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Where(jjob => jjob.progressToken.state is ProgressToken.State.Standby));
|
||||||
|
break;
|
||||||
|
case "Jobs/MonitorJobs":
|
||||||
|
SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Where(jjob => jjob is DownloadNewChapters));
|
||||||
|
break;
|
||||||
|
case "Settings":
|
||||||
|
SendResponse(HttpStatusCode.OK, response, settings);
|
||||||
|
break;
|
||||||
|
case "NotificationConnectors":
|
||||||
|
SendResponse(HttpStatusCode.OK, response, notificationConnectors);
|
||||||
|
break;
|
||||||
|
case "NotificationConnectors/Types":
|
||||||
|
SendResponse(HttpStatusCode.OK, response,
|
||||||
|
Enum.GetValues<NotificationConnector.NotificationConnectorType>().Select(nc => new KeyValuePair<byte, string?>((byte)nc, Enum.GetName(nc))));
|
||||||
|
break;
|
||||||
|
case "LibraryConnectors":
|
||||||
|
SendResponse(HttpStatusCode.OK, response, libraryConnectors);
|
||||||
|
break;
|
||||||
|
case "LibraryConnectors/Types":
|
||||||
|
SendResponse(HttpStatusCode.OK, response,
|
||||||
|
Enum.GetValues<LibraryConnector.LibraryType>().Select(lc => new KeyValuePair<byte, string?>((byte)lc, Enum.GetName(lc))));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
SendResponse(HttpStatusCode.BadRequest, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandlePost(HttpListenerRequest request, HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
Dictionary<string, string> requestVariables = GetRequestVariables(request.Url!.Query);
|
||||||
|
string? connectorName, internalId, jobId;
|
||||||
|
MangaConnector connector;
|
||||||
|
Manga manga;
|
||||||
|
Job? job;
|
||||||
|
string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value;
|
||||||
|
switch (path)
|
||||||
|
{
|
||||||
|
case "Jobs/MonitorManga":
|
||||||
|
if(!requestVariables.TryGetValue("connector", out connectorName) ||
|
||||||
|
!requestVariables.TryGetValue("internalId", out internalId) ||
|
||||||
|
!requestVariables.TryGetValue("interval", out string? intervalStr) ||
|
||||||
|
_parent.GetConnector(connectorName) is null ||
|
||||||
|
_parent.GetPublicationById(internalId) is null ||
|
||||||
|
!TimeSpan.TryParse(intervalStr, out TimeSpan interval))
|
||||||
|
{
|
||||||
|
SendResponse(HttpStatusCode.BadRequest, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
connector = _parent.GetConnector(connectorName)!;
|
||||||
|
manga = (Manga)_parent.GetPublicationById(internalId)!;
|
||||||
|
_parent.jobBoss.AddJob(new DownloadNewChapters(this, connector, manga, true, interval));
|
||||||
|
SendResponse(HttpStatusCode.Accepted, response);
|
||||||
|
break;
|
||||||
|
case "Jobs/DownloadNewChapters":
|
||||||
|
if(!requestVariables.TryGetValue("connector", out connectorName) ||
|
||||||
|
!requestVariables.TryGetValue("internalId", out internalId) ||
|
||||||
|
_parent.GetConnector(connectorName) is null ||
|
||||||
|
_parent.GetPublicationById(internalId) is null)
|
||||||
|
{
|
||||||
|
SendResponse(HttpStatusCode.BadRequest, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
connector = _parent.GetConnector(connectorName)!;
|
||||||
|
manga = (Manga)_parent.GetPublicationById(internalId)!;
|
||||||
|
_parent.jobBoss.AddJob(new DownloadNewChapters(this, connector, manga, false));
|
||||||
|
SendResponse(HttpStatusCode.Accepted, response);
|
||||||
|
break;
|
||||||
|
case "Jobs/StartNow":
|
||||||
|
if (!requestVariables.TryGetValue("jobId", out jobId) ||
|
||||||
|
!_parent.jobBoss.TryGetJobById(jobId, out job))
|
||||||
|
{
|
||||||
|
SendResponse(HttpStatusCode.BadRequest, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_parent.jobBoss.AddJobToQueue(job!);
|
||||||
|
SendResponse(HttpStatusCode.Accepted, response);
|
||||||
|
break;
|
||||||
|
case "Jobs/Cancel":
|
||||||
|
if (!requestVariables.TryGetValue("jobId", out jobId) ||
|
||||||
|
!_parent.jobBoss.TryGetJobById(jobId, out job))
|
||||||
|
{
|
||||||
|
SendResponse(HttpStatusCode.BadRequest, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
job!.Cancel();
|
||||||
|
SendResponse(HttpStatusCode.Accepted, response);
|
||||||
|
break;
|
||||||
|
case "Settings/UpdateDownloadLocation":
|
||||||
|
if (!requestVariables.TryGetValue("downloadLocation", out string? downloadLocation) ||
|
||||||
|
!requestVariables.TryGetValue("moveFiles", out string? moveFilesStr) ||
|
||||||
|
!Boolean.TryParse(moveFilesStr, out bool moveFiles))
|
||||||
|
{
|
||||||
|
SendResponse(HttpStatusCode.BadRequest, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
settings.UpdateDownloadLocation(downloadLocation, moveFiles);
|
||||||
|
SendResponse(HttpStatusCode.Accepted, response);
|
||||||
|
break;
|
||||||
|
/*case "Settings/UpdateWorkingDirectory":
|
||||||
|
if (!requestVariables.TryGetValue("workingDirectory", out string? workingDirectory))
|
||||||
|
{
|
||||||
|
SendResponse(HttpStatusCode.BadRequest, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
settings.UpdateWorkingDirectory(workingDirectory);
|
||||||
|
SendResponse(HttpStatusCode.Accepted, response);
|
||||||
|
break;*/
|
||||||
|
case "NotificationConnectors/Update":
|
||||||
|
if (!requestVariables.TryGetValue("notificationConnector", out string? notificationConnectorStr) ||
|
||||||
|
!Enum.TryParse(notificationConnectorStr, out NotificationConnector.NotificationConnectorType notificationConnectorType))
|
||||||
|
{
|
||||||
|
SendResponse(HttpStatusCode.BadRequest, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Gotify)
|
||||||
|
{
|
||||||
|
if (!requestVariables.TryGetValue("gotifyUrl", out string? gotifyUrl) ||
|
||||||
|
!requestVariables.TryGetValue("gotifyAppToken", out string? gotifyAppToken))
|
||||||
|
{
|
||||||
|
SendResponse(HttpStatusCode.BadRequest, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
AddNotificationConnector(new Gotify(this, gotifyUrl, gotifyAppToken));
|
||||||
|
SendResponse(HttpStatusCode.Accepted, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notificationConnectorType is NotificationConnector.NotificationConnectorType.LunaSea)
|
||||||
|
{
|
||||||
|
if (!requestVariables.TryGetValue("lunaseaWebhook", out string? lunaseaWebhook))
|
||||||
|
{
|
||||||
|
SendResponse(HttpStatusCode.BadRequest, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
AddNotificationConnector(new LunaSea(this, lunaseaWebhook));
|
||||||
|
SendResponse(HttpStatusCode.Accepted, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "LibraryConnectors/Update":
|
||||||
|
if (!requestVariables.TryGetValue("libraryConnector", out string? libraryConnectorStr) ||
|
||||||
|
!Enum.TryParse(libraryConnectorStr,
|
||||||
|
out LibraryConnector.LibraryType libraryConnectorType))
|
||||||
|
{
|
||||||
|
SendResponse(HttpStatusCode.BadRequest, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (libraryConnectorType is LibraryConnector.LibraryType.Kavita)
|
||||||
|
{
|
||||||
|
if (!requestVariables.TryGetValue("kavitaUrl", out string? kavitaUrl) ||
|
||||||
|
!requestVariables.TryGetValue("kavitaUsername", out string? kavitaUsername) ||
|
||||||
|
!requestVariables.TryGetValue("kavitaPassword", out string? kavitaPassword))
|
||||||
|
{
|
||||||
|
SendResponse(HttpStatusCode.BadRequest, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
AddLibraryConnector(new Kavita(this, kavitaUrl, kavitaUsername, kavitaPassword));
|
||||||
|
SendResponse(HttpStatusCode.Accepted, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (libraryConnectorType is LibraryConnector.LibraryType.Komga)
|
||||||
|
{
|
||||||
|
if (!requestVariables.TryGetValue("komgaUrl", out string? komgaUrl) ||
|
||||||
|
!requestVariables.TryGetValue("komgaAuth", out string? komgaAuth))
|
||||||
|
{
|
||||||
|
SendResponse(HttpStatusCode.BadRequest, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
AddLibraryConnector(new Komga(this, komgaUrl, komgaAuth));
|
||||||
|
SendResponse(HttpStatusCode.Accepted, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
SendResponse(HttpStatusCode.BadRequest, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleDelete(HttpListenerRequest request, HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
Dictionary<string, string> requestVariables = GetRequestVariables(request.Url!.Query);
|
||||||
|
string? connectorName, internalId;
|
||||||
|
MangaConnector connector;
|
||||||
|
Manga manga;
|
||||||
|
string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value;
|
||||||
|
switch (path)
|
||||||
|
{
|
||||||
|
case "Jobs":
|
||||||
|
if (!requestVariables.TryGetValue("jobId", out string? jobId) ||
|
||||||
|
!_parent.jobBoss.TryGetJobById(jobId, out Job? job))
|
||||||
|
{
|
||||||
|
SendResponse(HttpStatusCode.BadRequest, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_parent.jobBoss.RemoveJob(job!);
|
||||||
|
SendResponse(HttpStatusCode.Accepted, response);
|
||||||
|
break;
|
||||||
|
case "Jobs/DownloadNewChapters":
|
||||||
|
if(!requestVariables.TryGetValue("connector", out connectorName) ||
|
||||||
|
!requestVariables.TryGetValue("internalId", out internalId) ||
|
||||||
|
_parent.GetConnector(connectorName) is null ||
|
||||||
|
_parent.GetPublicationById(internalId) is null)
|
||||||
|
{
|
||||||
|
SendResponse(HttpStatusCode.BadRequest, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
connector = _parent.GetConnector(connectorName)!;
|
||||||
|
manga = (Manga)_parent.GetPublicationById(internalId)!;
|
||||||
|
_parent.jobBoss.RemoveJobs(_parent.jobBoss.GetJobsLike(connector, manga));
|
||||||
|
SendResponse(HttpStatusCode.Accepted, response);
|
||||||
|
break;
|
||||||
|
case "NotificationConnectors":
|
||||||
|
if (!requestVariables.TryGetValue("notificationConnector", out string? notificationConnectorStr) ||
|
||||||
|
!Enum.TryParse(notificationConnectorStr, out NotificationConnector.NotificationConnectorType notificationConnectorType))
|
||||||
|
{
|
||||||
|
SendResponse(HttpStatusCode.BadRequest, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
DeleteNotificationConnector(notificationConnectorType);
|
||||||
|
SendResponse(HttpStatusCode.Accepted, response);
|
||||||
|
break;
|
||||||
|
case "LibraryConnectors":
|
||||||
|
if (!requestVariables.TryGetValue("libraryConnectors", out string? libraryConnectorStr) ||
|
||||||
|
!Enum.TryParse(libraryConnectorStr,
|
||||||
|
out LibraryConnector.LibraryType libraryConnectoryType))
|
||||||
|
{
|
||||||
|
SendResponse(HttpStatusCode.BadRequest, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
DeleteLibraryConnector(libraryConnectoryType);
|
||||||
|
SendResponse(HttpStatusCode.Accepted, response);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
SendResponse(HttpStatusCode.BadRequest, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendResponse(HttpStatusCode statusCode, HttpListenerResponse response, object? content = null)
|
||||||
|
{
|
||||||
|
//Log($"Response: {statusCode} {content}");
|
||||||
|
response.StatusCode = (int)statusCode;
|
||||||
|
response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With");
|
||||||
|
response.AddHeader("Access-Control-Allow-Methods", "GET, POST, DELETE");
|
||||||
|
response.AddHeader("Access-Control-Max-Age", "1728000");
|
||||||
|
response.AppendHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
|
||||||
|
if (content is not FileStream stream)
|
||||||
|
{
|
||||||
|
response.ContentType = "application/json";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
response.OutputStream.Write(content is not null
|
||||||
|
? Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content))
|
||||||
|
: Array.Empty<byte>());
|
||||||
|
response.OutputStream.Close();
|
||||||
|
}
|
||||||
|
catch (HttpListenerException e)
|
||||||
|
{
|
||||||
|
Log(e.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
stream.CopyTo(response.OutputStream);
|
||||||
|
response.OutputStream.Close();
|
||||||
|
stream.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,385 +0,0 @@
|
|||||||
using Newtonsoft.Json;
|
|
||||||
using Tranga.Connectors;
|
|
||||||
using Tranga.TrangaTasks;
|
|
||||||
|
|
||||||
namespace Tranga;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Manages all TrangaTasks.
|
|
||||||
/// Provides a Threaded environment to execute Tasks, and still manage the Task-Collection
|
|
||||||
/// </summary>
|
|
||||||
public class TaskManager
|
|
||||||
{
|
|
||||||
public HashSet<Publication> collection = new();
|
|
||||||
private HashSet<TrangaTask> _allTasks = new();
|
|
||||||
private readonly Dictionary<TrangaTask, CancellationTokenSource> _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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Runs continuously until shutdown.
|
|
||||||
/// Checks if tasks have to be executed (time elapsed)
|
|
||||||
/// </summary>
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Forces the execution of a given task
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="task">Task to execute</param>
|
|
||||||
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<TrangaTask> 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<TrangaTask>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<TrangaTask> 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<TrangaTask>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes a Task from the queue
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="task"></param>
|
|
||||||
public void RemoveTaskFromQueue(TrangaTask task)
|
|
||||||
{
|
|
||||||
task.lastExecuted = DateTime.Now;
|
|
||||||
task.state = TrangaTask.ExecutionState.Waiting;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets last execution time to start of time
|
|
||||||
/// Let taskManager handle enqueuing
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="task"></param>
|
|
||||||
public void AddTaskToQueue(TrangaTask task)
|
|
||||||
{
|
|
||||||
task.lastExecuted = DateTime.UnixEpoch;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <returns>All available Connectors</returns>
|
|
||||||
public Dictionary<string, Connector> GetAvailableConnectors()
|
|
||||||
{
|
|
||||||
return this._connectors.ToDictionary(connector => connector.name, connector => connector);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <returns>All TrangaTasks in task-collection</returns>
|
|
||||||
public TrangaTask[] GetAllTasks()
|
|
||||||
{
|
|
||||||
TrangaTask[] ret = new TrangaTask[_allTasks.Count];
|
|
||||||
_allTasks.CopyTo(ret);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <returns>All added Publications</returns>
|
|
||||||
public Publication[] GetAllPublications()
|
|
||||||
{
|
|
||||||
return this.collection.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Chapter> GetExistingChaptersList(Connector connector, Publication publication, string language)
|
|
||||||
{
|
|
||||||
Chapter[] newChapters = connector.GetChapters(publication, language);
|
|
||||||
return newChapters.Where(nChapter => nChapter.CheckChapterIsDownloaded(settings.downloadLocation)).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Return Connector with given Name
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="connectorName">Connector-name (exact)</param>
|
|
||||||
/// <exception cref="Exception">If Connector is not available</exception>
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Shuts down the taskManager.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="force">If force is true, tasks are aborted.</param>
|
|
||||||
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<HashSet<TrangaTask>>(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Exports data (settings, tasks) to file
|
|
||||||
/// </summary>
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
599
Tranga/Tranga.cs
@ -1,580 +1,73 @@
|
|||||||
using System.Globalization;
|
using Logging;
|
||||||
using System.Runtime.InteropServices;
|
using Tranga.Jobs;
|
||||||
using Logging;
|
using Tranga.MangaConnectors;
|
||||||
using Tranga.API;
|
|
||||||
using Tranga.Connectors;
|
|
||||||
using Tranga.NotificationManagers;
|
|
||||||
using Tranga.TrangaTasks;
|
|
||||||
|
|
||||||
namespace Tranga;
|
namespace Tranga;
|
||||||
|
|
||||||
public static class Tranga
|
public partial class Tranga : GlobalBase
|
||||||
{
|
{
|
||||||
public static void Main(string[] args)
|
public bool keepRunning;
|
||||||
|
public JobBoss jobBoss;
|
||||||
|
private Server _server;
|
||||||
|
private HashSet<MangaConnector> _connectors;
|
||||||
|
|
||||||
|
public Tranga(Logger? logger, TrangaSettings settings) : base(logger, settings)
|
||||||
{
|
{
|
||||||
bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
|
keepRunning = true;
|
||||||
string applicationFolderPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Tranga-API");
|
_connectors = new HashSet<MangaConnector>()
|
||||||
|
{
|
||||||
string downloadFolderPath = isLinux ? "/Manga" : Path.Join(applicationFolderPath, "Manga");
|
new Manganato(this),
|
||||||
string logsFolderPath = isLinux ? "/var/log/Tranga" : Path.Join(applicationFolderPath, "log");
|
new Mangasee(this),
|
||||||
string logFilePath = Path.Join(logsFolderPath, $"log-{DateTime.Now:dd-M-yyyy-HH-mm-ss}.txt");
|
new MangaDex(this),
|
||||||
string settingsFilePath = Path.Join(applicationFolderPath, "settings.json");
|
new MangaKatana(this)
|
||||||
|
};
|
||||||
|
jobBoss = new(this, this._connectors);
|
||||||
Directory.CreateDirectory(logsFolderPath);
|
StartJobBoss();
|
||||||
Logger logger = isLinux
|
this._server = new Server(this);
|
||||||
? 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)
|
public MangaConnector? GetConnector(string name)
|
||||||
{
|
{
|
||||||
logger?.WriteLine("Tranga_CLI", "Menu: Select task");
|
foreach(MangaConnector mc in _connectors)
|
||||||
if (tasks.Length < 1)
|
if (mc.name.Equals(name, StringComparison.InvariantCultureIgnoreCase))
|
||||||
{
|
return mc;
|
||||||
Console.Clear();
|
|
||||||
Console.WriteLine("There are no available Tasks.");
|
|
||||||
logger?.WriteLine("Tranga_CLI", "No available Tasks.");
|
|
||||||
return Array.Empty<TrangaTask>();
|
|
||||||
}
|
|
||||||
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<TrangaTask>();
|
|
||||||
}
|
|
||||||
|
|
||||||
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<TrangaTask.Task>();
|
|
||||||
|
|
||||||
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<TrangaTask.Task>(selectedTaskName);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Exception: {e.Message}");
|
|
||||||
logger?.WriteLine("Tranga_CLI", e.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TimeSpan SelectReoccurrence(Logger? logger)
|
public bool TryGetConnector(string name, out MangaConnector? connector)
|
||||||
{
|
{
|
||||||
logger?.WriteLine("Tranga_CLI", "Menu: Select Reoccurrence");
|
connector = GetConnector(name);
|
||||||
Console.WriteLine("Select reoccurrence Timer (Format hh:mm:ss):");
|
return connector is not null;
|
||||||
return TimeSpan.Parse(Console.ReadLine()!, new CultureInfo("en-US"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Chapter[] SelectChapters(Connector connector, Publication publication, Logger? logger)
|
public IEnumerable<MangaConnector> GetConnectors()
|
||||||
{
|
{
|
||||||
logger?.WriteLine("Tranga_CLI", "Menu: Select Chapters");
|
return _connectors;
|
||||||
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)
|
public Manga? GetPublicationById(string internalId)
|
||||||
{
|
{
|
||||||
logger?.WriteLine("Tranga_CLI", "Menu: Select Connector");
|
if (cachedPublications.Exists(publication => publication.internalId == internalId))
|
||||||
Console.Clear();
|
return cachedPublications.First(publication => publication.internalId == internalId);
|
||||||
|
|
||||||
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Publication? SelectPublication(TaskManager taskManager, Connector connector)
|
public bool TryGetPublicationById(string internalId, out Manga? manga)
|
||||||
{
|
{
|
||||||
taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "Menu: Select Publication");
|
manga = GetPublicationById(internalId);
|
||||||
|
return manga is not null;
|
||||||
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)
|
private void StartJobBoss()
|
||||||
{
|
{
|
||||||
taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "Menu: Search task");
|
Thread t = new (() =>
|
||||||
Console.Clear();
|
{
|
||||||
Console.WriteLine("Enter search query:");
|
while (keepRunning)
|
||||||
string? query = Console.ReadLine();
|
{
|
||||||
while (query is null || query.Length < 4)
|
jobBoss.CheckJobs();
|
||||||
query = Console.ReadLine();
|
Thread.Sleep(100);
|
||||||
PrintTasks(taskManager.GetAllTasks().Where(qTask =>
|
}
|
||||||
qTask.ToString().ToLower().Contains(query, StringComparison.OrdinalIgnoreCase)).ToArray(), taskManager.commonObjects.logger);
|
});
|
||||||
|
t.Start();
|
||||||
}
|
}
|
||||||
}
|
}
|
136
Tranga/TrangaArgs.cs
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
using Logging;
|
||||||
|
|
||||||
|
namespace Tranga;
|
||||||
|
|
||||||
|
public partial class Tranga : GlobalBase
|
||||||
|
{
|
||||||
|
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
string[]? help = GetArg(args, ArgEnum.Help);
|
||||||
|
if (help is not null)
|
||||||
|
{
|
||||||
|
PrintHelp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string[]? consoleLogger = GetArg(args, ArgEnum.ConsoleLogger);
|
||||||
|
string[]? fileLogger = GetArg(args, ArgEnum.FileLogger);
|
||||||
|
string? filePath = fileLogger?[0];//TODO validate path
|
||||||
|
|
||||||
|
List<Logger.LoggerType> enabledLoggers = new();
|
||||||
|
if(consoleLogger is not null)
|
||||||
|
enabledLoggers.Add(Logger.LoggerType.ConsoleLogger);
|
||||||
|
if (fileLogger is not null)
|
||||||
|
enabledLoggers.Add(Logger.LoggerType.FileLogger);
|
||||||
|
Logger logger = new(enabledLoggers.ToArray(), Console.Out, Console.OutputEncoding, filePath);
|
||||||
|
|
||||||
|
TrangaSettings? settings = null;
|
||||||
|
string[]? downloadLocationPath = GetArg(args, ArgEnum.DownloadLocation);
|
||||||
|
string[]? workingDirectory = GetArg(args, ArgEnum.WorkingDirectory);
|
||||||
|
|
||||||
|
if (downloadLocationPath is not null && workingDirectory is not null)
|
||||||
|
{
|
||||||
|
settings = new TrangaSettings(downloadLocationPath[0], workingDirectory[0]);
|
||||||
|
}else if (downloadLocationPath is not null)
|
||||||
|
{
|
||||||
|
if (settings is null)
|
||||||
|
settings = new TrangaSettings(downloadLocation: downloadLocationPath[0]);
|
||||||
|
else
|
||||||
|
settings = new TrangaSettings(downloadLocation: downloadLocationPath[0], settings.workingDirectory);
|
||||||
|
}else if (workingDirectory is not null)
|
||||||
|
{
|
||||||
|
if (settings is null)
|
||||||
|
settings = new TrangaSettings(downloadLocation: workingDirectory[0]);
|
||||||
|
else
|
||||||
|
settings = new TrangaSettings(settings.downloadLocation, workingDirectory[0]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
settings = new TrangaSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.CreateDirectory(settings.downloadLocation);//TODO validate path
|
||||||
|
Directory.CreateDirectory(settings.workingDirectory);//TODO validate path
|
||||||
|
|
||||||
|
Tranga _ = new (logger, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PrintHelp()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Tranga-Help:");
|
||||||
|
foreach (Argument argument in arguments.Values)
|
||||||
|
{
|
||||||
|
foreach(string name in argument.names)
|
||||||
|
Console.Write("{0} ", name);
|
||||||
|
if(argument.parameterCount > 0)
|
||||||
|
Console.Write($"<{argument.parameterCount}>");
|
||||||
|
Console.Write("\r\n {0}\r\n", argument.helpText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns an array containing the parameters for the argument.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">List of argument-strings</param>
|
||||||
|
/// <param name="arg">Requested parameter</param>
|
||||||
|
/// <returns>
|
||||||
|
/// If there are no parameters for an argument, returns an empty array.
|
||||||
|
/// If the argument is not found returns null.
|
||||||
|
/// </returns>
|
||||||
|
private static string[]? GetArg(string[] args, ArgEnum arg)
|
||||||
|
{
|
||||||
|
List<string> argsList = args.ToList();
|
||||||
|
List<string> ret = new();
|
||||||
|
foreach (string name in arguments[arg].names)
|
||||||
|
{
|
||||||
|
int argIndex = argsList.IndexOf(name);
|
||||||
|
if (argIndex != -1)
|
||||||
|
{
|
||||||
|
if (arguments[arg].parameterCount == 0)
|
||||||
|
return ret.ToArray();
|
||||||
|
for (int parameterIndex = 1; parameterIndex <= arguments[arg].parameterCount; parameterIndex++)
|
||||||
|
{
|
||||||
|
if(argIndex + parameterIndex >= argsList.Count || args[argIndex + parameterIndex].Contains('-'))//End of arguments, or no parameter provided, when one is required
|
||||||
|
Console.WriteLine($"No parameter provided for argument {name}. -h for help.");
|
||||||
|
ret.Add(args[argIndex + parameterIndex]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret.Any() ? ret.ToArray() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<ArgEnum, Argument> arguments = new()
|
||||||
|
{
|
||||||
|
{ ArgEnum.DownloadLocation, new(new []{"-d", "--downloadLocation"}, 1, "Directory to which downloaded Manga are saved") },
|
||||||
|
{ ArgEnum.WorkingDirectory, new(new []{"-w", "--workingDirectory"}, 1, "Directory in which application-data is saved") },
|
||||||
|
{ ArgEnum.ConsoleLogger, new(new []{"-c", "--consoleLogger"}, 0, "Enables the consoleLogger") },
|
||||||
|
{ ArgEnum.FileLogger, new(new []{"-f", "--fileLogger"}, 1, "Enables the fileLogger, Directory where logfiles are saved") },
|
||||||
|
{ ArgEnum.Help, new(new []{"-h", "--help"}, 0, "Print this") }
|
||||||
|
//{ ArgEnum., new(new []{""}, 1, "") }
|
||||||
|
};
|
||||||
|
|
||||||
|
internal enum ArgEnum
|
||||||
|
{
|
||||||
|
TrangaSettings,
|
||||||
|
DownloadLocation,
|
||||||
|
WorkingDirectory,
|
||||||
|
ConsoleLogger,
|
||||||
|
FileLogger,
|
||||||
|
Help
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Argument
|
||||||
|
{
|
||||||
|
public string[] names { get; }
|
||||||
|
public byte parameterCount { get; }
|
||||||
|
public string helpText { get; }
|
||||||
|
|
||||||
|
public Argument(string[] names, byte parameterCount, string helpText)
|
||||||
|
{
|
||||||
|
this.names = names;
|
||||||
|
this.parameterCount = parameterCount;
|
||||||
|
this.helpText = helpText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,41 +1,111 @@
|
|||||||
using Newtonsoft.Json;
|
using System.Runtime.InteropServices;
|
||||||
using Tranga.LibraryManagers;
|
using Newtonsoft.Json;
|
||||||
using Tranga.NotificationManagers;
|
using Tranga.LibraryConnectors;
|
||||||
|
using Tranga.NotificationConnectors;
|
||||||
|
using static System.IO.UnixFileMode;
|
||||||
|
|
||||||
namespace Tranga;
|
namespace Tranga;
|
||||||
|
|
||||||
public class TrangaSettings
|
public class TrangaSettings
|
||||||
{
|
{
|
||||||
public string downloadLocation { get; private set; }
|
public string downloadLocation { get; private set; }
|
||||||
public string workingDirectory { get; init; }
|
public string workingDirectory { get; private set; }
|
||||||
|
public int apiPortNumber { get; init; }
|
||||||
[JsonIgnore] public string settingsFilePath => Path.Join(workingDirectory, "settings.json");
|
[JsonIgnore] public string settingsFilePath => Path.Join(workingDirectory, "settings.json");
|
||||||
[JsonIgnore] public string tasksFilePath => Path.Join(workingDirectory, "tasks.json");
|
[JsonIgnore] public string libraryConnectorsFilePath => Path.Join(workingDirectory, "libraryConnectors.json");
|
||||||
|
[JsonIgnore] public string notificationConnectorsFilePath => Path.Join(workingDirectory, "notificationConnectors.json");
|
||||||
|
[JsonIgnore] public string jobsFilePath => Path.Join(workingDirectory, "jobs.json");
|
||||||
[JsonIgnore] public string coverImageCache => Path.Join(workingDirectory, "imageCache");
|
[JsonIgnore] public string coverImageCache => Path.Join(workingDirectory, "imageCache");
|
||||||
public ushort? version { get; set; }
|
public ushort? version { get; set; }
|
||||||
|
|
||||||
public TrangaSettings(string downloadLocation, string workingDirectory)
|
public TrangaSettings(string? downloadLocation = null, string? workingDirectory = null, int? apiPortNumber = null)
|
||||||
{
|
{
|
||||||
if (downloadLocation.Length < 1 || workingDirectory.Length < 1)
|
string lockFilePath = $"{settingsFilePath}.lock";
|
||||||
throw new ArgumentException("Download-location and working-directory paths can not be empty!");
|
if (File.Exists(settingsFilePath) && !File.Exists(lockFilePath))
|
||||||
this.workingDirectory = workingDirectory;
|
{//Load from settings file
|
||||||
this.downloadLocation = downloadLocation;
|
FileStream lockFile = File.Create(lockFilePath,0, FileOptions.DeleteOnClose);
|
||||||
|
string settingsStr = File.ReadAllText(settingsFilePath);
|
||||||
|
TrangaSettings settings = JsonConvert.DeserializeObject<TrangaSettings>(settingsStr)!;
|
||||||
|
this.downloadLocation = downloadLocation ?? settings.downloadLocation;
|
||||||
|
this.workingDirectory = workingDirectory ?? settings.workingDirectory;
|
||||||
|
this.apiPortNumber = apiPortNumber ?? settings.apiPortNumber;
|
||||||
|
lockFile.Close();
|
||||||
|
}
|
||||||
|
else if(!File.Exists(settingsFilePath))
|
||||||
|
{//No settings file exists
|
||||||
|
if (downloadLocation?.Length < 1 || workingDirectory?.Length < 1)
|
||||||
|
throw new ArgumentException("Download-location and working-directory paths can not be empty!");
|
||||||
|
this.apiPortNumber = apiPortNumber ?? 6531;
|
||||||
|
this.downloadLocation = downloadLocation ?? (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(Directory.GetCurrentDirectory(), "Downloads"));
|
||||||
|
this.workingDirectory = workingDirectory ?? Path.Join(RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/var/lib" : Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "tranga-api");
|
||||||
|
ExportSettings();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{//Settingsfile is locked
|
||||||
|
this.apiPortNumber = apiPortNumber!.Value;
|
||||||
|
this.downloadLocation = downloadLocation!;
|
||||||
|
this.workingDirectory = workingDirectory!;
|
||||||
|
}
|
||||||
|
UpdateDownloadLocation(this.downloadLocation!, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TrangaSettings LoadSettings(string importFilePath)
|
public HashSet<LibraryConnector> LoadLibraryConnectors(GlobalBase clone)
|
||||||
{
|
{
|
||||||
if (!File.Exists(importFilePath))
|
if (!File.Exists(libraryConnectorsFilePath))
|
||||||
return new TrangaSettings(Path.Join(Directory.GetCurrentDirectory(), "Downloads"), Directory.GetCurrentDirectory());
|
return new HashSet<LibraryConnector>();
|
||||||
|
return JsonConvert.DeserializeObject<HashSet<LibraryConnector>>(File.ReadAllText(libraryConnectorsFilePath),
|
||||||
|
new JsonSerializerSettings()
|
||||||
|
{
|
||||||
|
Converters =
|
||||||
|
{
|
||||||
|
new LibraryManagerJsonConverter(clone)
|
||||||
|
}
|
||||||
|
})!;
|
||||||
|
}
|
||||||
|
|
||||||
string toRead = File.ReadAllText(importFilePath);
|
public HashSet<NotificationConnector> LoadNotificationConnectors(GlobalBase clone)
|
||||||
SettingsJsonObject settings = JsonConvert.DeserializeObject<SettingsJsonObject>(toRead,
|
{
|
||||||
new JsonSerializerSettings { Converters = { new NotificationManager.NotificationManagerJsonConverter(), new LibraryManager.LibraryManagerJsonConverter() } })!;
|
if (!File.Exists(notificationConnectorsFilePath))
|
||||||
return settings.ts ?? new TrangaSettings(Path.Join(Directory.GetCurrentDirectory(), "Downloads"), Directory.GetCurrentDirectory());
|
return new HashSet<NotificationConnector>();
|
||||||
|
return JsonConvert.DeserializeObject<HashSet<NotificationConnector>>(File.ReadAllText(notificationConnectorsFilePath),
|
||||||
|
new JsonSerializerSettings()
|
||||||
|
{
|
||||||
|
Converters =
|
||||||
|
{
|
||||||
|
new NotificationManagerJsonConverter(clone)
|
||||||
|
}
|
||||||
|
})!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateDownloadLocation(string newPath, bool moveFiles = true)
|
||||||
|
{
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
Directory.CreateDirectory(newPath,
|
||||||
|
GroupRead | GroupWrite | None | OtherRead | OtherWrite | UserRead | UserWrite);
|
||||||
|
else
|
||||||
|
Directory.CreateDirectory(newPath);
|
||||||
|
|
||||||
|
if (moveFiles && Directory.Exists(this.downloadLocation))
|
||||||
|
Directory.Move(this.downloadLocation, newPath);
|
||||||
|
|
||||||
|
this.downloadLocation = newPath;
|
||||||
|
ExportSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateWorkingDirectory(string newPath)
|
||||||
|
{
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
Directory.CreateDirectory(newPath,
|
||||||
|
GroupRead | GroupWrite | None | OtherRead | OtherWrite | UserRead | UserWrite);
|
||||||
|
else
|
||||||
|
Directory.CreateDirectory(newPath);
|
||||||
|
Directory.Move(this.workingDirectory, newPath);
|
||||||
|
this.workingDirectory = newPath;
|
||||||
|
ExportSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ExportSettings()
|
public void ExportSettings()
|
||||||
{
|
{
|
||||||
SettingsJsonObject? settings = null;
|
|
||||||
if (File.Exists(settingsFilePath))
|
if (File.Exists(settingsFilePath))
|
||||||
{
|
{
|
||||||
bool inUse = true;
|
bool inUse = true;
|
||||||
@ -43,55 +113,23 @@ public class TrangaSettings
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using FileStream stream = new (settingsFilePath, FileMode.Open, FileAccess.Read, FileShare.None);
|
using FileStream stream = new(settingsFilePath, FileMode.Open, FileAccess.Read, FileShare.None);
|
||||||
stream.Close();
|
stream.Close();
|
||||||
inUse = false;
|
inUse = false;
|
||||||
}
|
}
|
||||||
catch (IOException)
|
catch (IOException)
|
||||||
{
|
{
|
||||||
inUse = true;
|
Thread.Sleep(100);
|
||||||
Thread.Sleep(50);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
string toRead = File.ReadAllText(settingsFilePath);
|
|
||||||
settings = JsonConvert.DeserializeObject<SettingsJsonObject>(toRead,
|
|
||||||
new JsonSerializerSettings
|
|
||||||
{
|
|
||||||
Converters =
|
|
||||||
{
|
|
||||||
new NotificationManager.NotificationManagerJsonConverter(),
|
|
||||||
new LibraryManager.LibraryManagerJsonConverter()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
settings = new SettingsJsonObject(this, settings?.co);
|
else
|
||||||
File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(settings));
|
Directory.CreateDirectory(new FileInfo(settingsFilePath).DirectoryName!);
|
||||||
|
File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateSettings(UpdateField field, params string[] values)
|
public string GetFullCoverPath(Manga manga)
|
||||||
{
|
{
|
||||||
switch (field)
|
return Path.Join(this.coverImageCache, manga.coverFileNameInCache);
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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}";
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<Chapter> 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}";
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stores information on Task, when implementing new Tasks also update the serializer
|
|
||||||
/// </summary>
|
|
||||||
[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<TrangaTask> 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// BL for concrete Tasks
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="taskManager"></param>
|
|
||||||
/// <param name="cancellationToken"></param>
|
|
||||||
protected abstract HttpStatusCode ExecuteTask(TaskManager taskManager, CancellationToken? cancellationToken = null);
|
|
||||||
|
|
||||||
public abstract TrangaTask Clone();
|
|
||||||
|
|
||||||
protected abstract double GetProgress();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Execute the task
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="taskManager">Should be the parent taskManager</param>
|
|
||||||
/// <param name="cancellationToken"></param>
|
|
||||||
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>() == (Int64)Task.MonitorPublication)
|
|
||||||
return jo.ToObject<MonitorPublicationTask>(serializer)!;
|
|
||||||
|
|
||||||
if (jo["task"]!.Value<Int64>() == (Int64)Task.UpdateLibraries)
|
|
||||||
return jo.ToObject<UpdateLibrariesTask>(serializer)!;
|
|
||||||
|
|
||||||
if (jo["task"]!.Value<Int64>() == (Int64)Task.DownloadChapter)
|
|
||||||
return jo.ToObject<DownloadChapterTask>(serializer)!;
|
|
||||||
|
|
||||||
throw new Exception();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool CanWrite => false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Don't call this
|
|
||||||
/// </summary>
|
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
throw new Exception("Dont call this");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace Tranga.TrangaTasks;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// LEGACY DEPRECATED
|
|
||||||
/// </summary>
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
FROM nginx:alpine3.17-slim
|
|
||||||
COPY . /usr/share/nginx/html
|
|
||||||
EXPOSE 80
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
|
@ -1,168 +0,0 @@
|
|||||||
let apiUri = `http://${window.location.host.split(':')[0]}:6531`
|
|
||||||
|
|
||||||
if(getCookie("apiUri") != ""){
|
|
||||||
apiUri = getCookie("apiUri");
|
|
||||||
}
|
|
||||||
function getCookie(cname) {
|
|
||||||
let name = cname + "=";
|
|
||||||
let decodedCookie = decodeURIComponent(document.cookie);
|
|
||||||
let ca = decodedCookie.split(';');
|
|
||||||
for(let i = 0; i < ca.length; i++) {
|
|
||||||
let c = ca[i];
|
|
||||||
while (c.charAt(0) == ' ') {
|
|
||||||
c = c.substring(1);
|
|
||||||
}
|
|
||||||
if (c.indexOf(name) == 0) {
|
|
||||||
return c.substring(name.length, c.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetData(uri){
|
|
||||||
let request = await fetch(uri, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let json = await request.json();
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
function PostData(uri){
|
|
||||||
fetch(uri, {
|
|
||||||
method: 'POST'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function DeleteData(uri){
|
|
||||||
fetch(uri, {
|
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetAvailableControllers(){
|
|
||||||
var uri = apiUri + "/Connectors";
|
|
||||||
let json = await GetData(uri);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetPublicationFromConnector(connectorName, title){
|
|
||||||
var uri = apiUri + `/Publications/FromConnector?connectorName=${connectorName}&title=${title}`;
|
|
||||||
let json = await GetData(uri);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetKnownPublications(){
|
|
||||||
var uri = apiUri + "/Publications/Known";
|
|
||||||
let json = await GetData(uri);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetPublication(internalId){
|
|
||||||
var uri = apiUri + `/Publications/Known?internalId=${internalId}`;
|
|
||||||
let json = await GetData(uri);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetChapters(internalId, connectorName, onlyNew, language){
|
|
||||||
var uri = apiUri + `/Publications/Chapters?internalId=${internalId}&connectorName=${connectorName}&onlyNew=${onlyNew}&language=${language}`;
|
|
||||||
let json = await GetData(uri);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetTaskTypes(){
|
|
||||||
var uri = apiUri + "/Tasks/Types";
|
|
||||||
let json = await GetData(uri);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
async function GetRunningTasks(){
|
|
||||||
var uri = apiUri + "/Tasks/RunningTasks";
|
|
||||||
let json = await GetData(uri);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetDownloadTasks(){
|
|
||||||
var uri = apiUri + "/Tasks?taskType=MonitorPublication";
|
|
||||||
let json = await GetData(uri);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetSettings(){
|
|
||||||
var uri = apiUri + "/Settings";
|
|
||||||
let json = await GetData(uri);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetKomgaTask(){
|
|
||||||
var uri = apiUri + "/Tasks?taskType=UpdateLibraries";
|
|
||||||
let json = await GetData(uri);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
function CreateMonitorTask(connectorName, internalId, reoccurrence, language){
|
|
||||||
var uri = apiUri + `/Tasks/CreateMonitorTask?connectorName=${connectorName}&internalId=${internalId}&reoccurrenceTime=${reoccurrence}&language=${language}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CreateDownloadChaptersTask(connectorName, internalId, chapters, language){
|
|
||||||
var uri = apiUri + `/Tasks/CreateDownloadChaptersTask?connectorName=${connectorName}&internalId=${internalId}&chapters=${chapters}&language=${language}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function StartTask(taskType, connectorName, internalId){
|
|
||||||
var uri = apiUri + `/Tasks/Start?taskType=${taskType}&connectorName=${connectorName}&internalId=${internalId}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EnqueueTask(taskType, connectorName, publicationId){
|
|
||||||
var uri = apiUri + `/Queue/Enqueue?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateDownloadLocation(downloadLocation){
|
|
||||||
var uri = apiUri + "/Settings/Update?"
|
|
||||||
uri += "&downloadLocation="+downloadLocation;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateKomga(komgaUrl, komgaAuth){
|
|
||||||
var uri = apiUri + "/Settings/Update?"
|
|
||||||
uri += `&komgaUrl=${komgaUrl}&komgaAuth=${komgaAuth}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateKavita(kavitaUrl, kavitaUser, kavitaPass){
|
|
||||||
var uri = apiUri + "/Settings/Update?"
|
|
||||||
uri += `&kavitaUrl=${kavitaUrl}&kavitaUsername=${kavitaUser}&kavitaPassword=${kavitaPass}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateGotify(gotifyUrl, gotifyAppToken){
|
|
||||||
var uri = apiUri + "/Settings/Update?"
|
|
||||||
uri += `&gotifyUrl=${gotifyUrl}&gotifyAppToken=${gotifyAppToken}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateLunaSea(lunaseaWebhook){
|
|
||||||
var uri = apiUri + "/Settings/Update?"
|
|
||||||
uri += `&lunaseaWebhook=${lunaseaWebhook}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DeleteTask(taskType, connectorName, publicationId){
|
|
||||||
var uri = apiUri + `/Tasks?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}`;
|
|
||||||
DeleteData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DequeueTask(taskType, connectorName, publicationId){
|
|
||||||
var uri = apiUri + `/Queue/Dequeue?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}`;
|
|
||||||
DeleteData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetQueue(){
|
|
||||||
var uri = apiUri + "/Queue/List";
|
|
||||||
let json = await GetData(uri);
|
|
||||||
return json;
|
|
||||||
}
|
|
Before Width: | Height: | Size: 66 KiB |
@ -1,179 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Tranga</title>
|
|
||||||
<link rel="stylesheet" href="style.css">
|
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<wrapper>
|
|
||||||
<topbar>
|
|
||||||
<titlebox>
|
|
||||||
<img alt="website image is Blahaj" src="media/blahaj.png">
|
|
||||||
<span>Tranga</span>
|
|
||||||
</titlebox>
|
|
||||||
<spacer></spacer>
|
|
||||||
<searchdiv>
|
|
||||||
<label for="searchbox"></label><input id="searchbox" placeholder="Filter" type="text">
|
|
||||||
</searchdiv>
|
|
||||||
<img id="settingscog" src="media/settings-cogwheel.svg" height="100%" alt="settingscog">
|
|
||||||
</topbar>
|
|
||||||
<viewport>
|
|
||||||
<content>
|
|
||||||
<div id="addPublication">
|
|
||||||
<p>+</p>
|
|
||||||
</div>
|
|
||||||
<publication>
|
|
||||||
<img alt="cover" src="media/cover.jpg">
|
|
||||||
<publication-information>
|
|
||||||
<connector-name class="pill">MangaDex</connector-name>
|
|
||||||
<publication-name>Tensei Pandemic</publication-name>
|
|
||||||
</publication-information>
|
|
||||||
</publication>
|
|
||||||
</content>
|
|
||||||
|
|
||||||
<popup id="selectPublicationPopup">
|
|
||||||
<blur-background id="blurBackgroundTaskPopup"></blur-background>
|
|
||||||
<popup-window>
|
|
||||||
<popup-title>Select Publication</popup-title>
|
|
||||||
<popup-content>
|
|
||||||
<div>
|
|
||||||
<label for="connectors">Connector</label>
|
|
||||||
<select id="connectors">
|
|
||||||
<option value=""></option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="searchPublicationQuery">Search Title</label><input id="searchPublicationQuery" type="text"></addtask-setting>
|
|
||||||
</div>
|
|
||||||
<input type="submit" value="Search" style="font-weight: bolder" onclick="NewSearch();">
|
|
||||||
</popup-content>
|
|
||||||
<div id="taskSelectOutput"></div>
|
|
||||||
</popup-window>
|
|
||||||
</popup>
|
|
||||||
|
|
||||||
<popup id="createMonitorTaskPopup">
|
|
||||||
<blur-background id="blurBackgroundCreateMonitorTaskPopup"></blur-background>
|
|
||||||
<popup-window>
|
|
||||||
<popup-title>Create Task: Monitor Publication</popup-title>
|
|
||||||
<popup-content>
|
|
||||||
<div>
|
|
||||||
<span>Run every</span>
|
|
||||||
<label for="hours"></label><input id="hours" type="number" value="3" min="0" max="23"><span>hours</span>
|
|
||||||
<label for="minutes"></label><input id="minutes" type="number" value="0" min="0" max="59"><span>minutes</span>
|
|
||||||
<input type="submit" value="Create" onclick="AddMonitorTask()">
|
|
||||||
</div>
|
|
||||||
</popup-content>
|
|
||||||
</popup-window>
|
|
||||||
</popup>
|
|
||||||
|
|
||||||
<popup id="createDownloadChaptersTask">
|
|
||||||
<blur-background id="blurBackgroundCreateDownloadChaptersTask"></blur-background>
|
|
||||||
<popup-window>
|
|
||||||
<popup-title>Create Task: Download Chapter(s)</popup-title>
|
|
||||||
<popup-content>
|
|
||||||
<div>
|
|
||||||
<label for="selectedChapters">Chapters:</label><input id="selectedChapters" placeholder="Select"><input type="submit" value="Select" onclick="DownloadChapterTaskClick()">
|
|
||||||
</div>
|
|
||||||
<div id="chapterOutput">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</popup-content>
|
|
||||||
</popup-window>
|
|
||||||
</popup>
|
|
||||||
|
|
||||||
<popup id="publicationViewerPopup">
|
|
||||||
<blur-background id="blurBackgroundPublicationPopup"></blur-background>
|
|
||||||
<publication-viewer>
|
|
||||||
<img id="pubviewcover" src="media/cover.jpg" alt="cover">
|
|
||||||
<publication-information>
|
|
||||||
<publication-name id="publicationViewerName">Tensei Pandemic</publication-name>
|
|
||||||
<publication-tags id="publicationViewerTags"></publication-tags>
|
|
||||||
<publication-author id="publicationViewerAuthor">Imamura Hinata</publication-author>
|
|
||||||
<publication-description id="publicationViewerDescription">Imamura Hinata is a high school boy with a cute appearance.
|
|
||||||
Since his trauma with the first love, he wanted to be more manly than anybody else. But one day he woke up to something different…
|
|
||||||
The total opposite of his ideal male body!
|
|
||||||
Pandemic love comedy!
|
|
||||||
</publication-description>
|
|
||||||
<publication-interactions>
|
|
||||||
<publication-starttask>Start Task ▶️</publication-starttask>
|
|
||||||
<publication-delete>Delete Task ❌</publication-delete>
|
|
||||||
<publication-add id="createMonitorTaskButton">Monitor ➕</publication-add>
|
|
||||||
<publication-add id="createDownloadChapterTaskButton">Download Chapter ➕</publication-add>
|
|
||||||
</publication-interactions>
|
|
||||||
</publication-information>
|
|
||||||
</publication-viewer>
|
|
||||||
</popup>
|
|
||||||
|
|
||||||
<popup id="settingsPopup">
|
|
||||||
<blur-background id="blurBackgroundSettingsPopup"></blur-background>
|
|
||||||
<popup-window>
|
|
||||||
<popup-title>Settings</popup-title>
|
|
||||||
<popup-content>
|
|
||||||
<div>
|
|
||||||
<p class="title">Download Location:</p>
|
|
||||||
<span id="downloadLocation"></span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="title">API-URI</p>
|
|
||||||
<label for="settingApiUri"></label><input placeholder="https://" type="text" id="settingApiUri">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="title">Komga</span>
|
|
||||||
<div>Configured: <span id="komgaConfigured">✅❌</span></div>
|
|
||||||
<label for="komgaUrl"></label><input placeholder="URL" id="komgaUrl" type="text">
|
|
||||||
<label for="komgaUsername"></label><input placeholder="Username" id="komgaUsername" type="text">
|
|
||||||
<label for="komgaPassword"></label><input placeholder="Password" id="komgaPassword" type="password">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="title">Kavita</span>
|
|
||||||
<div>Configured: <span id="kavitaConfigured">✅❌</span></div>
|
|
||||||
<label for="kavitaUrl"></label><input placeholder="URL" id="kavitaUrl" type="text">
|
|
||||||
<label for="kavitaUsername"></label><input placeholder="Username" id="kavitaUsername" type="text">
|
|
||||||
<label for="kavitaPassword"></label><input placeholder="Password" id="kavitaPassword" type="password">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="title">Gotify</span>
|
|
||||||
<div>Configured: <span id="gotifyConfigured">✅❌</span></div>
|
|
||||||
<label for="gotifyUrl"></label><input placeholder="URL" id="gotifyUrl" type="text">
|
|
||||||
<label for="gotifyAppToken"></label><input placeholder="App-Token" id="gotifyAppToken" type="text">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="title">LunaSea</span>
|
|
||||||
<div>Configured: <span id="lunaseaConfigured">✅❌</span></div>
|
|
||||||
<label for="lunaseaWebhook"></label><input placeholder="device/:id or user/:id" id="lunaseaWebhook" type="text">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="libraryUpdateTime" style="margin-right: 5px;">Update Time</label><input id="libraryUpdateTime" type="time" value="00:01:00" step="10">
|
|
||||||
<input type="submit" value="Update" onclick="UpdateLibrarySettings()">
|
|
||||||
</div>
|
|
||||||
</popup-content>
|
|
||||||
</popup-window>
|
|
||||||
</popup>
|
|
||||||
|
|
||||||
<popup id="downloadTasksPopup">
|
|
||||||
<blur-background id="blurBackgroundTasksQueuePopup"></blur-background>
|
|
||||||
<popup-window>
|
|
||||||
<popup-title>Task Progress</popup-title>
|
|
||||||
<popup-content>
|
|
||||||
|
|
||||||
</popup-content>
|
|
||||||
</popup-window>
|
|
||||||
</popup>
|
|
||||||
</viewport>
|
|
||||||
<footer>
|
|
||||||
<div onclick="ShowTasksQueue();">
|
|
||||||
<img src="media/running.svg" alt="running"><div id="tasksRunningTag">0</div>
|
|
||||||
</div>
|
|
||||||
<div onclick="ShowTasksQueue();">
|
|
||||||
<img src="media/queue.svg" alt="queue"><div id="tasksQueuedTag">0</div>
|
|
||||||
</div>
|
|
||||||
<p id="madeWith">Made with Blåhaj 🦈</p>
|
|
||||||
</footer>
|
|
||||||
</wrapper>
|
|
||||||
|
|
||||||
<script src="apiConnector.js"></script>
|
|
||||||
<script src="interaction.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,525 +0,0 @@
|
|||||||
let publications = [];
|
|
||||||
let tasks = [];
|
|
||||||
let toEditId;
|
|
||||||
|
|
||||||
const searchBox = document.querySelector("#searchbox");
|
|
||||||
const searchPublicationQuery = document.querySelector("#searchPublicationQuery");
|
|
||||||
const selectPublication = document.querySelector("#taskSelectOutput");
|
|
||||||
const connectorSelect = document.querySelector("#connectors");
|
|
||||||
const settingsPopup = document.querySelector("#settingsPopup");
|
|
||||||
const settingsCog = document.querySelector("#settingscog");
|
|
||||||
const selectRecurrence = document.querySelector("#selectReccurrence");
|
|
||||||
const tasksContent = document.querySelector("content");
|
|
||||||
const selectPublicationPopup = document.querySelector("#selectPublicationPopup");
|
|
||||||
const createMonitorTaskButton = document.querySelector("#createMonitorTaskButton");
|
|
||||||
const createDownloadChapterTaskButton = document.querySelector("#createDownloadChapterTaskButton");
|
|
||||||
const createMonitorTaskPopup = document.querySelector("#createMonitorTaskPopup");
|
|
||||||
const createDownloadChaptersTask = document.querySelector("#createDownloadChaptersTask");
|
|
||||||
const chapterOutput = document.querySelector("#chapterOutput");
|
|
||||||
const selectedChapters = document.querySelector("#selectedChapters");
|
|
||||||
const publicationViewerPopup = document.querySelector("#publicationViewerPopup");
|
|
||||||
const publicationViewerWindow = document.querySelector("publication-viewer");
|
|
||||||
const publicationViewerDescription = document.querySelector("#publicationViewerDescription");
|
|
||||||
const publicationViewerName = document.querySelector("#publicationViewerName");
|
|
||||||
const publicationViewerTags = document.querySelector("#publicationViewerTags");
|
|
||||||
const publicationViewerAuthor = document.querySelector("#publicationViewerAuthor");
|
|
||||||
const pubviewcover = document.querySelector("#pubviewcover");
|
|
||||||
const publicationDelete = document.querySelector("publication-delete");
|
|
||||||
const publicationTaskStart = document.querySelector("publication-starttask");
|
|
||||||
const settingDownloadLocation = document.querySelector("#downloadLocation");
|
|
||||||
const settingKomgaUrl = document.querySelector("#komgaUrl");
|
|
||||||
const settingKomgaUser = document.querySelector("#komgaUsername");
|
|
||||||
const settingKomgaPass = document.querySelector("#komgaPassword");
|
|
||||||
const settingKavitaUrl = document.querySelector("#kavitaUrl");
|
|
||||||
const settingKavitaUser = document.querySelector("#kavitaUsername");
|
|
||||||
const settingKavitaPass = document.querySelector("#kavitaPassword");
|
|
||||||
const settingGotifyUrl = document.querySelector("#gotifyUrl");
|
|
||||||
const settingGotifyAppToken = document.querySelector("#gotifyAppToken");
|
|
||||||
const settingLunaseaWebhook = document.querySelector("#lunaseaWebhook");
|
|
||||||
const libraryUpdateTime = document.querySelector("#libraryUpdateTime");
|
|
||||||
const settingKomgaConfigured = document.querySelector("#komgaConfigured");
|
|
||||||
const settingKavitaConfigured = document.querySelector("#kavitaConfigured");
|
|
||||||
const settingGotifyConfigured = document.querySelector("#gotifyConfigured");
|
|
||||||
const settingLunaseaConfigured = document.querySelector("#lunaseaConfigured");
|
|
||||||
const settingApiUri = document.querySelector("#settingApiUri");
|
|
||||||
const tagTasksRunning = document.querySelector("#tasksRunningTag");
|
|
||||||
const tagTasksQueued = document.querySelector("#tasksQueuedTag");
|
|
||||||
const downloadTasksPopup = document.querySelector("#downloadTasksPopup");
|
|
||||||
const downloadTasksOutput = downloadTasksPopup.querySelector("popup-content");
|
|
||||||
|
|
||||||
searchbox.addEventListener("keyup", (event) => FilterResults());
|
|
||||||
settingsCog.addEventListener("click", () => OpenSettings());
|
|
||||||
document.querySelector("#blurBackgroundSettingsPopup").addEventListener("click", () => settingsPopup.style.display = "none");
|
|
||||||
document.querySelector("#blurBackgroundTaskPopup").addEventListener("click", () => selectPublicationPopup.style.display = "none");
|
|
||||||
document.querySelector("#blurBackgroundPublicationPopup").addEventListener("click", () => HidePublicationPopup());
|
|
||||||
document.querySelector("#blurBackgroundCreateMonitorTaskPopup").addEventListener("click", () => createMonitorTaskPopup.style.display = "none");
|
|
||||||
document.querySelector("#blurBackgroundCreateDownloadChaptersTask").addEventListener("click", () => createDownloadChaptersTask.style.display = "none");
|
|
||||||
document.querySelector("#blurBackgroundTasksQueuePopup").addEventListener("click", () => downloadTasksPopup.style.display = "none");
|
|
||||||
selectedChapters.addEventListener("keypress", (event) => {
|
|
||||||
if(event.key === "Enter"){
|
|
||||||
DownloadChapterTaskClick();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
publicationDelete.addEventListener("click", () => DeleteTaskClick());
|
|
||||||
createMonitorTaskButton.addEventListener("click", () => {
|
|
||||||
HidePublicationPopup();
|
|
||||||
createMonitorTaskPopup.style.display = "block";
|
|
||||||
});
|
|
||||||
createDownloadChapterTaskButton.addEventListener("click", () => {
|
|
||||||
HidePublicationPopup();
|
|
||||||
OpenDownloadChapterTaskPopup();
|
|
||||||
});
|
|
||||||
publicationTaskStart.addEventListener("click", () => StartTaskClick());
|
|
||||||
searchPublicationQuery.addEventListener("keypress", (event) => {
|
|
||||||
if(event.key === "Enter"){
|
|
||||||
NewSearch();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
let availableConnectors;
|
|
||||||
GetAvailableControllers()
|
|
||||||
.then(json => availableConnectors = json)
|
|
||||||
.then(json =>
|
|
||||||
json.forEach(connector => {
|
|
||||||
var option = document.createElement('option');
|
|
||||||
option.value = connector;
|
|
||||||
option.innerText = connector;
|
|
||||||
connectorSelect.appendChild(option);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
function NewSearch(){
|
|
||||||
//Disable inputs
|
|
||||||
connectorSelect.disabled = true;
|
|
||||||
searchPublicationQuery.disabled = true;
|
|
||||||
//Waitcursor
|
|
||||||
document.body.style.cursor = "wait";
|
|
||||||
|
|
||||||
//Empty previous results
|
|
||||||
selectPublication.replaceChildren();
|
|
||||||
GetPublicationFromConnector(connectorSelect.value, searchPublicationQuery.value)
|
|
||||||
.then(json =>
|
|
||||||
json.forEach(publication => {
|
|
||||||
var option = CreatePublication(publication, connectorSelect.value);
|
|
||||||
option.addEventListener("click", (mouseEvent) => {
|
|
||||||
ShowPublicationViewerWindow(publication.internalId, mouseEvent, true);
|
|
||||||
});
|
|
||||||
selectPublication.appendChild(option);
|
|
||||||
}
|
|
||||||
))
|
|
||||||
.then(() => {
|
|
||||||
//Re-enable inputs
|
|
||||||
connectorSelect.disabled = false;
|
|
||||||
searchPublicationQuery.disabled = false;
|
|
||||||
//Cursor
|
|
||||||
document.body.style.cursor = "initial";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//Returns a new "Publication" Item to display in the tasks section
|
|
||||||
function CreatePublication(publication, connector){
|
|
||||||
var publicationElement = document.createElement('publication');
|
|
||||||
publicationElement.setAttribute("id", publication.internalId);
|
|
||||||
var img = document.createElement('img');
|
|
||||||
img.src = `imageCache/${publication.coverFileNameInCache}`;
|
|
||||||
publicationElement.appendChild(img);
|
|
||||||
var info = document.createElement('publication-information');
|
|
||||||
var connectorName = document.createElement('connector-name');
|
|
||||||
connectorName.innerText = connector;
|
|
||||||
connectorName.className = "pill";
|
|
||||||
info.appendChild(connectorName);
|
|
||||||
var publicationName = document.createElement('publication-name');
|
|
||||||
publicationName.innerText = publication.sortName;
|
|
||||||
info.appendChild(publicationName);
|
|
||||||
publicationElement.appendChild(info);
|
|
||||||
if(publications.filter(pub => pub.internalId === publication.internalId) < 1)
|
|
||||||
publications.push(publication);
|
|
||||||
return publicationElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
function AddMonitorTask(){
|
|
||||||
var hours = document.querySelector("#hours").value;
|
|
||||||
var minutes = document.querySelector("#minutes").value;
|
|
||||||
CreateMonitorTask(connectorSelect.value, toEditId, `${hours}:${minutes}:00`, "en");
|
|
||||||
HidePublicationPopup();
|
|
||||||
createMonitorTaskPopup.style.display = "none";
|
|
||||||
selectPublicationPopup.style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
function OpenDownloadChapterTaskPopup(){
|
|
||||||
selectedChapters.value = "";
|
|
||||||
chapterOutput.replaceChildren();
|
|
||||||
createDownloadChaptersTask.style.display = "block";
|
|
||||||
GetChapters(toEditId, connectorSelect.value, true, "en").then((json) => {
|
|
||||||
var i = 0;
|
|
||||||
json.forEach(chapter => {
|
|
||||||
var chapterDom = document.createElement("div");
|
|
||||||
var indexDom = document.createElement("span");
|
|
||||||
indexDom.className = "index";
|
|
||||||
indexDom.innerText = i++;
|
|
||||||
chapterDom.appendChild(indexDom);
|
|
||||||
|
|
||||||
var volDom = document.createElement("span");
|
|
||||||
volDom.className = "vol";
|
|
||||||
volDom.innerText = chapter.volumeNumber;
|
|
||||||
chapterDom.appendChild(volDom);
|
|
||||||
|
|
||||||
var chDom = document.createElement("span");
|
|
||||||
chDom.className = "ch";
|
|
||||||
chDom.innerText = chapter.chapterNumber;
|
|
||||||
chapterDom.appendChild(chDom);
|
|
||||||
|
|
||||||
var titleDom = document.createElement("span");
|
|
||||||
titleDom.innerText = chapter.name;
|
|
||||||
chapterDom.appendChild(titleDom);
|
|
||||||
chapterOutput.appendChild(chapterDom);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function DownloadChapterTaskClick(){
|
|
||||||
CreateDownloadChaptersTask(connectorSelect.value, toEditId, selectedChapters.value, "en");
|
|
||||||
HidePublicationPopup();
|
|
||||||
createDownloadChaptersTask.style.display = "none";
|
|
||||||
selectPublicationPopup.style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
function DeleteTaskClick(){
|
|
||||||
taskToDelete = tasks.filter(tTask => tTask.publication.internalId === toEditId)[0];
|
|
||||||
DeleteTask("MonitorPublication", taskToDelete.connectorName, toEditId);
|
|
||||||
HidePublicationPopup();
|
|
||||||
}
|
|
||||||
|
|
||||||
function StartTaskClick(){
|
|
||||||
var toEditTask = tasks.filter(task => task.publication.internalId == toEditId)[0];
|
|
||||||
StartTask("MonitorPublication", toEditTask.connectorName, toEditId);
|
|
||||||
HidePublicationPopup();
|
|
||||||
}
|
|
||||||
|
|
||||||
function ResetContent(){
|
|
||||||
//Delete everything
|
|
||||||
tasksContent.replaceChildren();
|
|
||||||
|
|
||||||
//Add "Add new Task" Button
|
|
||||||
var add = document.createElement("div");
|
|
||||||
add.setAttribute("id", "addPublication")
|
|
||||||
var plus = document.createElement("p");
|
|
||||||
plus.innerText = "+";
|
|
||||||
add.appendChild(plus);
|
|
||||||
add.addEventListener("click", () => ShowNewTaskWindow());
|
|
||||||
tasksContent.appendChild(add);
|
|
||||||
}
|
|
||||||
function ShowPublicationViewerWindow(publicationId, event, add){
|
|
||||||
//Show popup
|
|
||||||
publicationViewerPopup.style.display = "block";
|
|
||||||
|
|
||||||
//Set position to mouse-position
|
|
||||||
if(event.clientY < window.innerHeight - publicationViewerWindow.offsetHeight)
|
|
||||||
publicationViewerWindow.style.top = `${event.clientY}px`;
|
|
||||||
else
|
|
||||||
publicationViewerWindow.style.top = `${event.clientY - publicationViewerWindow.offsetHeight}px`;
|
|
||||||
|
|
||||||
if(event.clientX < window.innerWidth - publicationViewerWindow.offsetWidth)
|
|
||||||
publicationViewerWindow.style.left = `${event.clientX}px`;
|
|
||||||
else
|
|
||||||
publicationViewerWindow.style.left = `${event.clientX - publicationViewerWindow.offsetWidth}px`;
|
|
||||||
|
|
||||||
//Edit information inside the window
|
|
||||||
var publication = publications.filter(pub => pub.internalId === publicationId)[0];
|
|
||||||
publicationViewerName.innerText = publication.sortName;
|
|
||||||
publicationViewerTags.innerText = publication.tags.join(", ");
|
|
||||||
publicationViewerDescription.innerText = publication.description;
|
|
||||||
publicationViewerAuthor.innerText = publication.authors.join(',');
|
|
||||||
pubviewcover.src = `imageCache/${publication.coverFileNameInCache}`;
|
|
||||||
toEditId = publicationId;
|
|
||||||
|
|
||||||
//Check what action should be listed
|
|
||||||
if(add){
|
|
||||||
createMonitorTaskButton.style.display = "initial";
|
|
||||||
createDownloadChapterTaskButton.style.display = "initial";
|
|
||||||
publicationDelete.style.display = "none";
|
|
||||||
publicationTaskStart.style.display = "none";
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
createMonitorTaskButton.style.display = "none";
|
|
||||||
createDownloadChapterTaskButton.style.display = "none";
|
|
||||||
publicationDelete.style.display = "initial";
|
|
||||||
publicationTaskStart.style.display = "initial";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function HidePublicationPopup(){
|
|
||||||
publicationViewerPopup.style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
function ShowNewTaskWindow(){
|
|
||||||
selectPublication.replaceChildren();
|
|
||||||
searchPublicationQuery.value = "";
|
|
||||||
selectPublicationPopup.style.display = "flex";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const fadeIn = [
|
|
||||||
{ opacity: "0" },
|
|
||||||
{ opacity: "1" }
|
|
||||||
];
|
|
||||||
|
|
||||||
const fadeInTiming = {
|
|
||||||
duration: 50,
|
|
||||||
iterations: 1,
|
|
||||||
fill: "forwards"
|
|
||||||
}
|
|
||||||
|
|
||||||
function OpenSettings(){
|
|
||||||
GetSettingsClick();
|
|
||||||
settingsPopup.style.display = "flex";
|
|
||||||
}
|
|
||||||
|
|
||||||
function GetSettingsClick(){
|
|
||||||
settingApiUri.value = "";
|
|
||||||
settingKomgaUrl.value = "";
|
|
||||||
settingKomgaUser.value = "";
|
|
||||||
settingKomgaPass.value = "";
|
|
||||||
settingKomgaConfigured.innerText = "❌";
|
|
||||||
settingKavitaUrl.value = "";
|
|
||||||
settingKavitaUser.value = "";
|
|
||||||
settingKavitaPass.value = "";
|
|
||||||
settingKavitaConfigured.innerText = "❌";
|
|
||||||
settingGotifyUrl.value = "";
|
|
||||||
settingGotifyAppToken.value = "";
|
|
||||||
settingGotifyConfigured.innerText = "❌";
|
|
||||||
settingLunaseaWebhook.value = "";
|
|
||||||
settingLunaseaConfigured.innerText = "❌";
|
|
||||||
|
|
||||||
settingApiUri.placeholder = apiUri;
|
|
||||||
|
|
||||||
GetSettings().then(json => {
|
|
||||||
settingDownloadLocation.innerText = json.downloadLocation;
|
|
||||||
json.libraryManagers.forEach(lm => {
|
|
||||||
if(lm.libraryType == 0){
|
|
||||||
settingKomgaUrl.placeholder = lm.baseUrl;
|
|
||||||
settingKomgaUser.placeholder = "User";
|
|
||||||
settingKomgaPass.placeholder = "***";
|
|
||||||
settingKomgaConfigured.innerText = "✅";
|
|
||||||
} else if(lm.libraryType == 1){
|
|
||||||
settingKavitaUrl.placeholder = lm.baseUrl;
|
|
||||||
settingKavitaUser.placeholder = "User";
|
|
||||||
settingKavitaPass.placeholder = "***";
|
|
||||||
settingKavitaConfigured.innerText = "✅";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
json.notificationManagers.forEach(nm => {
|
|
||||||
if(nm.notificationManagerType == 0){
|
|
||||||
settingGotifyConfigured.innerText = "✅";
|
|
||||||
} else if(nm.notificationManagerType == 1){
|
|
||||||
settingLunaseaConfigured.innerText = "✅";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
GetKomgaTask().then(json => {
|
|
||||||
if(json.length > 0)
|
|
||||||
libraryUpdateTime.value = json[0].reoccurrence;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateLibrarySettings(){
|
|
||||||
if(settingKomgaUrl.value != "" && settingKomgaUser.value != "" && settingKomgaPass.value != ""){
|
|
||||||
var auth = utf8_to_b64(`${settingKomgaUser.value}:${settingKomgaPass.value}`);
|
|
||||||
console.log(auth);
|
|
||||||
UpdateKomga(settingKomgaUrl.value, auth);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(settingKavitaUrl.value != "" && settingKavitaUser.value != "" && settingKavitaPass.value != ""){
|
|
||||||
UpdateKavita(settingKavitaUrl.value, settingKavitaUser.value, settingKavitaPass.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(settingGotifyUrl.value != "" && settingGotifyAppToken.value != ""){
|
|
||||||
UpdateGotify(settingGotifyUrl.value, settingGotifyAppToken.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(settingLunaseaWebhook.value != ""){
|
|
||||||
UpdateLunaSea(settingLunaseaWebhook.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(settingApiUri.value != ""){
|
|
||||||
apiUri = settingApiUri.value;
|
|
||||||
document.cookie = `apiUri=${apiUri};`;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => GetSettingsClick(), 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
function utf8_to_b64( str ) {
|
|
||||||
return window.btoa(unescape(encodeURIComponent( str )));
|
|
||||||
}
|
|
||||||
|
|
||||||
function FilterResults(){
|
|
||||||
if(searchBox.value.length > 0){
|
|
||||||
tasksContent.childNodes.forEach(publication => {
|
|
||||||
publication.childNodes.forEach(item => {
|
|
||||||
if(item.nodeName.toLowerCase() == "publication-information"){
|
|
||||||
item.childNodes.forEach(information => {
|
|
||||||
if(information.nodeName.toLowerCase() == "publication-name"){
|
|
||||||
if(!information.textContent.toLowerCase().includes(searchBox.value.toLowerCase())){
|
|
||||||
publication.style.display = "none";
|
|
||||||
}else{
|
|
||||||
publication.style.display = "initial";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}else{
|
|
||||||
tasksContent.childNodes.forEach(publication => publication.style.display = "initial");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ShowTasksQueue(){
|
|
||||||
|
|
||||||
downloadTasksOutput.replaceChildren();
|
|
||||||
GetRunningTasks()
|
|
||||||
.then(json => {
|
|
||||||
tagTasksRunning.innerText = json.length;
|
|
||||||
json.forEach(task => {
|
|
||||||
if(task.task == 2 || task.task == 4) {
|
|
||||||
downloadTasksOutput.appendChild(CreateProgressChild(task));
|
|
||||||
document.querySelector(`#progress${GetValidSelector(task.taskId)}`).value = task.progress;
|
|
||||||
var finishedHours = task.executionApproximatelyRemaining.split(':')[0];
|
|
||||||
var finishedMinutes = task.executionApproximatelyRemaining.split(':')[1];
|
|
||||||
var finishedSeconds = task.executionApproximatelyRemaining.split(':')[2].split('.')[0];
|
|
||||||
document.querySelector(`#progressStr${GetValidSelector(task.taskId)}`).innerText = `${finishedHours}:${finishedMinutes}:${finishedSeconds}`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
GetQueue()
|
|
||||||
.then(json => {
|
|
||||||
tagTasksQueued.innerText = json.length;
|
|
||||||
json.forEach(task => {
|
|
||||||
downloadTasksOutput.appendChild(CreateProgressChild(task));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
downloadTasksPopup.style.display = "flex";
|
|
||||||
}
|
|
||||||
|
|
||||||
function CreateProgressChild(task){
|
|
||||||
var child = document.createElement("div");
|
|
||||||
var img = document.createElement('img');
|
|
||||||
img.src = `imageCache/${task.publication.coverFileNameInCache}`;
|
|
||||||
child.appendChild(img);
|
|
||||||
|
|
||||||
var name = document.createElement("span");
|
|
||||||
name.innerText = task.publication.sortName;
|
|
||||||
name.className = "pubTitle";
|
|
||||||
child.appendChild(name);
|
|
||||||
|
|
||||||
|
|
||||||
var progress = document.createElement("progress");
|
|
||||||
progress.id = `progress${GetValidSelector(task.taskId)}`;
|
|
||||||
child.appendChild(progress);
|
|
||||||
|
|
||||||
var progressStr = document.createElement("span");
|
|
||||||
progressStr.innerText = " \t∞";
|
|
||||||
progressStr.className = "progressStr";
|
|
||||||
progressStr.id = `progressStr${GetValidSelector(task.taskId)}`;
|
|
||||||
child.appendChild(progressStr);
|
|
||||||
|
|
||||||
if(task.chapter != undefined){
|
|
||||||
var chapterNumber = document.createElement("span");
|
|
||||||
chapterNumber.className = "chapterNumber";
|
|
||||||
chapterNumber.innerText = `Vol.${task.chapter.volumeNumber} Ch.${task.chapter.chapterNumber}`;
|
|
||||||
child.appendChild(chapterNumber);
|
|
||||||
|
|
||||||
var chapterName = document.createElement("span");
|
|
||||||
chapterName.className = "chapterName";
|
|
||||||
chapterName.innerText = task.chapter.name;
|
|
||||||
child.appendChild(chapterName);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return child;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Resets the tasks shown
|
|
||||||
ResetContent();
|
|
||||||
downloadTasksOutput.replaceChildren();
|
|
||||||
//Get Tasks and show them
|
|
||||||
GetDownloadTasks()
|
|
||||||
.then(json => json.forEach(task => {
|
|
||||||
var publication = CreatePublication(task.publication, task.connectorName);
|
|
||||||
publication.addEventListener("click", (event) => ShowPublicationViewerWindow(task.publication.internalId, event, false));
|
|
||||||
tasksContent.appendChild(publication);
|
|
||||||
tasks.push(task);
|
|
||||||
}));
|
|
||||||
|
|
||||||
GetRunningTasks()
|
|
||||||
.then(json => {
|
|
||||||
tagTasksRunning.innerText = json.length;
|
|
||||||
json.forEach(task => {
|
|
||||||
downloadTasksOutput.appendChild(CreateProgressChild(task));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
GetQueue()
|
|
||||||
.then(json => {
|
|
||||||
tagTasksQueued.innerText = json.length;
|
|
||||||
json.forEach(task => {
|
|
||||||
downloadTasksOutput.appendChild(CreateProgressChild(task));
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
//Tasks from API
|
|
||||||
var cTasks = [];
|
|
||||||
GetDownloadTasks()
|
|
||||||
.then(json => json.forEach(task => cTasks.push(task)))
|
|
||||||
.then(() => {
|
|
||||||
//Only update view if tasks-amount has changed
|
|
||||||
if(tasks.length != cTasks.length) {
|
|
||||||
//Resets the tasks shown
|
|
||||||
ResetContent();
|
|
||||||
//Add all currenttasks to view
|
|
||||||
cTasks.forEach(task => {
|
|
||||||
var publication = CreatePublication(task.publication, task.connectorName);
|
|
||||||
publication.addEventListener("click", (event) => ShowPublicationViewerWindow(task.publication.internalId, event, false));
|
|
||||||
tasksContent.appendChild(publication);
|
|
||||||
})
|
|
||||||
|
|
||||||
tasks = cTasks;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
GetRunningTasks()
|
|
||||||
.then(json => {
|
|
||||||
tagTasksRunning.innerText = json.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
GetQueue()
|
|
||||||
.then(json => {
|
|
||||||
tagTasksQueued.innerText = json.length;
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
GetRunningTasks().then((json) => {
|
|
||||||
json.forEach(task => {
|
|
||||||
if(task.task == 2 || task.task == 4){
|
|
||||||
document.querySelector(`#progress${GetValidSelector(task.taskId)}`).value = task.progress;
|
|
||||||
var finishedHours = task.executionApproximatelyRemaining.split(':')[0];
|
|
||||||
var finishedMinutes = task.executionApproximatelyRemaining.split(':')[1];
|
|
||||||
var finishedSeconds = task.executionApproximatelyRemaining.split(':')[2].split('.')[0];
|
|
||||||
document.querySelector(`#progressStr${GetValidSelector(task.taskId)}`).innerText = `${finishedHours}:${finishedMinutes}:${finishedSeconds}`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},500);
|
|
||||||
|
|
||||||
function GetValidSelector(str){
|
|
||||||
var clean = [...str.matchAll(/[a-zA-Z0-9]*-*_*/g)];
|
|
||||||
return clean.join('');
|
|
||||||
}
|
|
Before Width: | Height: | Size: 124 KiB |
@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.29289 5.29289C5.68342 4.90237 6.31658 4.90237 6.70711 5.29289L12 10.5858L17.2929 5.29289C17.6834 4.90237 18.3166 4.90237 18.7071 5.29289C19.0976 5.68342 19.0976 6.31658 18.7071 6.70711L13.4142 12L18.7071 17.2929C19.0976 17.6834 19.0976 18.3166 18.7071 18.7071C18.3166 19.0976 17.6834 19.0976 17.2929 18.7071L12 13.4142L6.70711 18.7071C6.31658 19.0976 5.68342 19.0976 5.29289 18.7071C4.90237 18.3166 4.90237 17.6834 5.29289 17.2929L10.5858 12L5.29289 6.70711C4.90237 6.31658 4.90237 5.68342 5.29289 5.29289Z" fill="#0F1729"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 804 B |
@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
|
|
||||||
<g fill="#000000">
|
|
||||||
<path d="M2.23 2.674a.75.75 0 00-.96 1.152L3.578 5.75 1.27 7.674a.75.75 0 00.96 1.152l3-2.5a.75.75 0 000-1.152l-3-2.5zM8.25 5a.75.75 0 000 1.5h6a.75.75 0 000-1.5h-6zM5.5 9.25a.75.75 0 01.75-.75h8a.75.75 0 010 1.5h-8a.75.75 0 01-.75-.75zM6.25 12a.75.75 0 000 1.5h8a.75.75 0 000-1.5h-8z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 545 B |
@ -1,53 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
width="800px" height="800px" viewBox="0 0 235.504 235.504"
|
|
||||||
xml:space="preserve">
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<path d="M195.209,81.456l-49.227-0.15c0.737-0.886,1.351-1.868,2.284-2.583c3.282-2.497,3.911-7.166,1.427-10.438
|
|
||||||
c-2.501-3.266-7.161-3.919-10.443-1.423c-4.873,3.715-8.388,8.704-10.255,14.389l-22.191-0.064
|
|
||||||
c-9.508,0-19.588,7.398-22.938,16.851l-16.877,47.479c-1.775,5.013-1.338,9.966,1.207,13.568
|
|
||||||
c2.412,3.427,6.384,5.318,11.187,5.358l45.126,0.136c-1.509,5.186-4.701,9.622-9.352,12.424
|
|
||||||
c-4.891,2.957-10.636,3.814-16.172,2.444c-3.994-0.998-8.031,1.442-9.027,5.418c-0.99,4.012,1.445,8.035,5.432,9.032
|
|
||||||
c2.927,0.738,5.879,1.091,8.808,1.091c6.516,0,12.93-1.788,18.645-5.23c8.312-5.013,14.172-12.979,16.484-22.409
|
|
||||||
c0.232-0.905,0.232-1.823,0.124-2.713l28.296,0.092h0.049c2.925,0,5.854-0.89,8.684-2.147c0.2,0.493,0.32,1.014,0.661,1.471
|
|
||||||
c3.335,4.677,4.629,10.343,3.688,15.993c-0.95,5.627-4.028,10.536-8.688,13.862c-3.351,2.376-4.14,7.037-1.755,10.379
|
|
||||||
c1.466,2.04,3.751,3.122,6.062,3.122c1.491,0,3.006-0.429,4.312-1.367c7.919-5.61,13.16-13.966,14.771-23.52
|
|
||||||
c1.603-9.565-0.613-19.203-6.28-27.122c-0.48-0.693-1.134-1.19-1.779-1.659c1.318-1.831,2.501-3.763,3.238-5.854l16.863-47.464
|
|
||||||
c1.795-5.018,1.351-9.969-1.194-13.58C203.954,83.387,200.015,81.47,195.209,81.456z M201.979,98.405l-16.868,47.464
|
|
||||||
c-0.981,2.757-2.941,5.214-5.213,7.329c-0.337,0.16-0.706,0.229-1.026,0.465c-0.673,0.485-1.182,1.122-1.639,1.747
|
|
||||||
c-2.962,1.996-6.288,3.339-9.434,3.339v2.989l-0.044-2.989l-33.194-0.101c-0.232-0.076-0.424-0.261-0.661-0.324
|
|
||||||
c-1.435-0.353-2.805-0.145-4.095,0.309l-29.768-0.101l1.192-3.358c0.549-1.547-0.269-3.25-1.813-3.795
|
|
||||||
c-1.521-0.553-3.25,0.24-3.799,1.804l-1.899,5.334l-14.318-0.044c-2.805,0-5.063-0.998-6.336-2.813
|
|
||||||
c-1.437-2.032-1.603-4.921-0.463-8.144l16.877-47.478c2.48-6.979,10.417-12.868,17.356-12.868l12.217,0.038l-1.963,5.536
|
|
||||||
c-0.555,1.549,0.262,3.25,1.805,3.797c0.331,0.12,0.661,0.174,0.998,0.174c1.227,0,2.372-0.768,2.793-1.986l2.497-7.019
|
|
||||||
c0.064-0.164-0.048-0.322-0.016-0.487h2.512c-0.905,7.758,1.163,15.42,5.947,21.638c5.903,7.687,14.852,11.726,23.873,11.726
|
|
||||||
c6.371,0,12.771-2.001,18.186-6.129c3.266-2.488,3.911-7.167,1.426-10.441c-2.508-3.267-7.161-3.901-10.455-1.415
|
|
||||||
c-6.612,5.056-16.146,3.775-21.223-2.809c-2.445-3.194-3.487-7.133-2.958-11.117c0.061-0.503,0.353-0.916,0.481-1.402
|
|
||||||
l52.216,0.156c2.806,0,5.054,1.004,6.324,2.811C202.928,92.241,203.105,95.223,201.979,98.405z"/>
|
|
||||||
<path d="M107.997,127.194c-1.531-0.553-3.248,0.244-3.799,1.791l-4.302,12.099c-0.551,1.543,0.265,3.242,1.813,3.795
|
|
||||||
c0.331,0.116,0.659,0.16,0.998,0.16c1.214,0,2.372-0.765,2.801-1.976l4.294-12.099
|
|
||||||
C110.369,129.446,109.551,127.728,107.997,127.194z"/>
|
|
||||||
<path d="M116.6,103.014c-1.529-0.541-3.25,0.252-3.805,1.805l-4.298,12.088c-0.547,1.547,0.261,3.252,1.799,3.799
|
|
||||||
c0.329,0.12,0.659,0.172,1,0.172c1.222,0,2.368-0.769,2.809-1.983l4.294-12.09C118.955,105.268,118.139,103.555,116.6,103.014z"/>
|
|
||||||
<path d="M232.527,90.428l-14.896-0.038l0,0c-1.639,0-2.974,1.327-2.997,2.976c0,1.639,1.342,2.981,2.981,2.989l14.896,0.042l0,0
|
|
||||||
c1.643,0,2.978-1.331,2.993-2.979C235.504,91.763,234.17,90.436,232.527,90.428z"/>
|
|
||||||
<path d="M220.333,80.436c0.629,0,1.242-0.188,1.771-0.583l11.994-8.83c1.326-0.974,1.611-2.842,0.645-4.168
|
|
||||||
c-0.965-1.327-2.845-1.611-4.163-0.637l-11.998,8.833c-1.323,0.974-1.607,2.841-0.642,4.167
|
|
||||||
C218.513,80.003,219.418,80.436,220.333,80.436z"/>
|
|
||||||
<path d="M209.152,56.279c-1.547-0.549-3.25,0.269-3.787,1.805l-4.997,14.036c-0.537,1.547,0.26,3.252,1.803,3.807
|
|
||||||
c0.337,0.12,0.674,0.172,0.994,0.172c1.242,0,2.385-0.757,2.821-1.986l4.985-14.036C211.516,58.541,210.695,56.846,209.152,56.279
|
|
||||||
z"/>
|
|
||||||
<path d="M17.587,100.894h55.208c1.641,0,2.976-1.343,2.976-2.981c0-1.641-1.334-2.988-2.976-2.988H17.587
|
|
||||||
c-1.641,0-2.988,1.338-2.988,2.988C14.599,99.559,15.946,100.894,17.587,100.894z"/>
|
|
||||||
<path d="M68.471,119.328c0-1.641-1.345-2.987-2.986-2.987H10.283c-1.639,0-2.981,1.338-2.981,2.987
|
|
||||||
c0,1.639,1.342,2.974,2.981,2.974h55.202C67.119,122.301,68.471,120.967,68.471,119.328z"/>
|
|
||||||
<path d="M58.188,137.758H2.974c-1.641,0-2.974,1.335-2.974,2.989c0,1.64,1.333,2.974,2.974,2.974h55.214
|
|
||||||
c1.639,0,2.981-1.334,2.981-2.974C61.162,139.093,59.827,137.758,58.188,137.758z"/>
|
|
||||||
<path d="M169.611,28.097c11.821,0,21.403,9.584,21.403,21.41c0,11.82-9.582,21.408-21.403,21.408
|
|
||||||
c-11.822,0-21.412-9.588-21.412-21.408C148.199,37.681,157.789,28.097,169.611,28.097z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 4.6 KiB |
@ -1,21 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
width="800px" height="800px" viewBox="0 0 93.5 93.5" xml:space="preserve">
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<path d="M93.5,40.899c0-2.453-1.995-4.447-4.448-4.447H81.98c-0.74-2.545-1.756-5.001-3.035-7.331l4.998-5
|
|
||||||
c0.826-0.827,1.303-1.973,1.303-3.146c0-1.19-0.462-2.306-1.303-3.146L75.67,9.555c-1.613-1.615-4.673-1.618-6.29,0l-5,5
|
|
||||||
c-2.327-1.28-4.786-2.296-7.332-3.037v-7.07C57.048,1.995,55.053,0,52.602,0H40.899c-2.453,0-4.447,1.995-4.447,4.448v7.071
|
|
||||||
c-2.546,0.741-5.005,1.757-7.333,3.037l-5-5c-1.68-1.679-4.609-1.679-6.288,0L9.555,17.83c-1.734,1.734-1.734,4.555,0,6.289
|
|
||||||
l4.999,5c-1.279,2.33-2.295,4.788-3.036,7.333h-7.07C1.995,36.452,0,38.447,0,40.899V52.6c0,2.453,1.995,4.447,4.448,4.447h7.071
|
|
||||||
c0.74,2.545,1.757,5.003,3.036,7.332l-4.998,4.999c-0.827,0.827-1.303,1.974-1.303,3.146c0,1.189,0.462,2.307,1.302,3.146
|
|
||||||
l8.274,8.273c1.614,1.615,4.674,1.619,6.29,0l5-5c2.328,1.279,4.786,2.297,7.333,3.037v7.071c0,2.453,1.995,4.448,4.447,4.448
|
|
||||||
h11.702c2.453,0,4.446-1.995,4.446-4.448V81.98c2.546-0.74,5.005-1.756,7.332-3.037l5,5c1.681,1.68,4.608,1.68,6.288,0
|
|
||||||
l8.275-8.273c1.734-1.734,1.734-4.555,0-6.289l-4.998-5.001c1.279-2.329,2.295-4.787,3.035-7.332h7.071
|
|
||||||
c2.453,0,4.448-1.995,4.448-4.446V40.899z M62.947,46.75c0,8.932-7.266,16.197-16.197,16.197c-8.931,0-16.197-7.266-16.197-16.197
|
|
||||||
c0-8.931,7.266-16.197,16.197-16.197C55.682,30.553,62.947,37.819,62.947,46.75z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.7 KiB |
@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
|
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<svg fill="#000000" height="800px" width="800px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
|
|
||||||
<g id="task">
|
|
||||||
<path d="M4,23.4l-3.7-3.7l1.4-1.4L4,20.6l4.3-4.3l1.4,1.4L4,23.4z M24,21H12v-2h12V21z M4,15.4l-3.7-3.7l1.4-1.4L4,12.6l4.3-4.3
|
|
||||||
l1.4,1.4L4,15.4z M24,13H12v-2h12V13z M4,7.4L0.3,3.7l1.4-1.4L4,4.6l4.3-4.3l1.4,1.4L4,7.4z M24,5H12V3h12V5z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 603 B |
@ -1,603 +0,0 @@
|
|||||||
:root{
|
|
||||||
--background-color: #030304;
|
|
||||||
--second-background-color: #fff;
|
|
||||||
--primary-color: #f5a9b8;
|
|
||||||
--secondary-color: #5bcefa;
|
|
||||||
--accent-color: #fff;
|
|
||||||
--topbar-height: 60px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body{
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
height: 100vh;
|
|
||||||
background-color: var(--background-color);
|
|
||||||
font-family: "Inter", sans-serif;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
background-placeholder{
|
|
||||||
background-color: var(--second-background-color);
|
|
||||||
opacity: 1;
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 0 0 5px 0;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
topbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: var(--topbar-height);
|
|
||||||
background-color: var(--secondary-color);
|
|
||||||
z-index: 100;
|
|
||||||
box-shadow: 0 0 20px black;
|
|
||||||
}
|
|
||||||
|
|
||||||
titlebox {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
margin: 0 0 0 40px;
|
|
||||||
height: 100%;
|
|
||||||
align-items:center;
|
|
||||||
justify-content:center;
|
|
||||||
}
|
|
||||||
|
|
||||||
titlebox span{
|
|
||||||
cursor: default;
|
|
||||||
font-size: 24pt;
|
|
||||||
font-weight: bold;
|
|
||||||
background: linear-gradient(150deg, var(--primary-color), var(--accent-color));
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
titlebox img {
|
|
||||||
height: 100%;
|
|
||||||
margin-right: 10px;
|
|
||||||
cursor: grab;
|
|
||||||
}
|
|
||||||
|
|
||||||
spacer{
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
searchdiv{
|
|
||||||
display: block;
|
|
||||||
margin: 0 10px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#searchbox {
|
|
||||||
padding: 3px 10px;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14pt;
|
|
||||||
width: 250px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#settingscog {
|
|
||||||
cursor: pointer;
|
|
||||||
margin: 0px 30px;
|
|
||||||
height: 50%;
|
|
||||||
filter: invert(100%) sepia(0%) saturate(7465%) hue-rotate(115deg) brightness(116%) contrast(101%);
|
|
||||||
}
|
|
||||||
|
|
||||||
viewport {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
flex-grow: 1;
|
|
||||||
height: 100%;
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
width: 100%;
|
|
||||||
height: 40px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
align-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer > div {
|
|
||||||
height: 100%;
|
|
||||||
margin: 0 30px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer > div > *{
|
|
||||||
height: 40%;
|
|
||||||
margin: 0 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#madeWith {
|
|
||||||
flex-grow: 1;
|
|
||||||
text-align: right;
|
|
||||||
margin-right: 20px;
|
|
||||||
cursor: url("media/blahaj.png"), grab;
|
|
||||||
}
|
|
||||||
|
|
||||||
content {
|
|
||||||
position: relative;
|
|
||||||
flex-grow: 1;
|
|
||||||
border-radius: 5px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: start;
|
|
||||||
align-content: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
#settingsPopup{
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
#settingsPopup popup-content{
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: start;
|
|
||||||
margin: 15px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#settingsPopup popup-content > * {
|
|
||||||
margin: 5px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#settingsPopup popup-content .title {
|
|
||||||
font-weight: bolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
#addPublication {
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: var(--secondary-color);
|
|
||||||
width: 180px;
|
|
||||||
height: 300px;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin: 10px 10px;
|
|
||||||
padding: 15px 20px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#addPublication p{
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 150pt;
|
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 300px;
|
|
||||||
margin: 0;
|
|
||||||
color: var(--accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill {
|
|
||||||
flex-grow: 0;
|
|
||||||
height: 14pt;
|
|
||||||
font-size: 12pt;
|
|
||||||
border-radius: 9pt;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
padding: 2pt 17px;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication{
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: var(--secondary-color);
|
|
||||||
width: 180px;
|
|
||||||
height: 300px;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin: 10px 10px;
|
|
||||||
padding: 15px 20px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication::after{
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0; top: 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
width: 100%; height: 100%;
|
|
||||||
background: linear-gradient(rgba(0,0,0,0.8), rgba(0, 0, 0, 0.7),rgba(0, 0, 0, 0.2));
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-information {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-information * {
|
|
||||||
z-index: 1;
|
|
||||||
color: var(--accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
connector-name{
|
|
||||||
width: fit-content;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-name{
|
|
||||||
width: fit-content;
|
|
||||||
font-size: 16pt;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication img {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
z-index: 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
popup{
|
|
||||||
display: none;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
position: fixed;
|
|
||||||
z-index: 2;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
popup popup-window {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 3;
|
|
||||||
left: 25%;
|
|
||||||
top: 100px;
|
|
||||||
width: 50%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background-color: var(--second-background-color);
|
|
||||||
border-radius: 3px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
popup popup-window popup-title {
|
|
||||||
height: 30px;
|
|
||||||
font-size: 14pt;
|
|
||||||
font-weight: bolder;
|
|
||||||
padding: 5px 10px;
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: var(--accent-color)
|
|
||||||
}
|
|
||||||
|
|
||||||
popup popup-window popup-content{
|
|
||||||
margin: 15px 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
}
|
|
||||||
|
|
||||||
popup popup-window popup-content div > * {
|
|
||||||
margin: 2px 3px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
popup popup-window popup-content input, select {
|
|
||||||
padding: 3px 4px;
|
|
||||||
width: 130px;
|
|
||||||
border: 1px solid lightgrey;
|
|
||||||
background-color: var(--accent-color);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#selectPublicationPopup publication {
|
|
||||||
width: 150px;
|
|
||||||
height: 250px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#createTaskPopup {
|
|
||||||
z-index: 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
#createTaskPopup input {
|
|
||||||
height: 30px;
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#createMonitorTaskPopup, #createDownloadChaptersTask {
|
|
||||||
z-index: 9;
|
|
||||||
}
|
|
||||||
|
|
||||||
#createMonitorTaskPopup input[type="number"] {
|
|
||||||
width: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#createDownloadChaptersTask popup-content {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
#createDownloadChaptersTask popup-content > * {
|
|
||||||
margin: 3px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#createDownloadChaptersTask #chapterOutput {
|
|
||||||
max-height: 50vh;
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
#createDownloadChaptersTask #chapterOutput .index{
|
|
||||||
display: inline-block;
|
|
||||||
width: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#createDownloadChaptersTask #chapterOutput .index::after{
|
|
||||||
content: ':';
|
|
||||||
}
|
|
||||||
|
|
||||||
#createDownloadChaptersTask #chapterOutput .vol::before{
|
|
||||||
content: 'Vol.';
|
|
||||||
}
|
|
||||||
|
|
||||||
#createDownloadChaptersTask #chapterOutput .vol{
|
|
||||||
display: inline-block;
|
|
||||||
width: 45px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#createDownloadChaptersTask #chapterOutput .ch::before{
|
|
||||||
content: 'Ch.';
|
|
||||||
}
|
|
||||||
|
|
||||||
#createDownloadChaptersTask #chapterOutput .ch {
|
|
||||||
display: inline-block;
|
|
||||||
width: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#downloadTasksPopup popup-window {
|
|
||||||
left: 0;
|
|
||||||
top: 80px;
|
|
||||||
margin: 0 0 0 10px;
|
|
||||||
height: calc(100vh - 140px);
|
|
||||||
width: 400px;
|
|
||||||
max-width: 95vw;
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
#downloadTasksPopup popup-content {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: start;
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#downloadTasksPopup popup-content > div {
|
|
||||||
display: block;
|
|
||||||
height: 80px;
|
|
||||||
position: relative;
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#downloadTasksPopup popup-content > div > img {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
height: 100%;
|
|
||||||
width: 60px;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#downloadTasksPopup popup-content > div > span {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
width: max-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
#downloadTasksPopup popup-content > div > .pubTitle {
|
|
||||||
left: 70px;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#downloadTasksPopup popup-content > div > .chapterName {
|
|
||||||
left: 70px;
|
|
||||||
top: 28pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
#downloadTasksPopup popup-content > div > .chapterNumber {
|
|
||||||
left: 70px;
|
|
||||||
top: 14pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
#downloadTasksPopup popup-content > div > progress {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
left: 150px;
|
|
||||||
bottom: 0;
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#downloadTasksPopup popup-content > div > .progressStr {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
left: 70px;
|
|
||||||
bottom: 0;
|
|
||||||
width: 70px;
|
|
||||||
}
|
|
||||||
|
|
||||||
blur-background {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
background-color: black;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
#taskSelectOutput{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: start;
|
|
||||||
align-content: start;
|
|
||||||
max-height: 70vh;
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
#publicationViewerPopup{
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer{
|
|
||||||
display: block;
|
|
||||||
width: 450px;
|
|
||||||
position: absolute;
|
|
||||||
top: 200px;
|
|
||||||
left: 400px;
|
|
||||||
background-color: var(--accent-color);
|
|
||||||
border-radius: 5px;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer::after{
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0; top: 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(0,0,0,0.8);
|
|
||||||
backdrop-filter: blur(3px);
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer img {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 5px;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer publication-information > * {
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer publication-information publication-name {
|
|
||||||
width: initial;
|
|
||||||
overflow-x: scroll;
|
|
||||||
white-space: nowrap;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer publication-information publication-tags::before {
|
|
||||||
content: "Tags";
|
|
||||||
display: block;
|
|
||||||
font-weight: bolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer publication-information publication-tags {
|
|
||||||
overflow-x: scroll;
|
|
||||||
white-space: nowrap;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer publication-information publication-author::before {
|
|
||||||
content: "Author: ";
|
|
||||||
font-weight: bolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer publication-information publication-description::before {
|
|
||||||
content: "Description";
|
|
||||||
display: block;
|
|
||||||
font-weight: bolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer publication-information publication-description {
|
|
||||||
font-size: 12pt;
|
|
||||||
margin: 5px 0;
|
|
||||||
height: 145px;
|
|
||||||
overflow-x: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer publication-information publication-interactions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: end;
|
|
||||||
align-items: start;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer publication-information publication-interactions > * {
|
|
||||||
margin: 0 10px;
|
|
||||||
font-size: 16pt;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer publication-information publication-interactions publication-starttask {
|
|
||||||
color: var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer publication-information publication-interactions publication-delete {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer publication-information publication-interactions publication-add {
|
|
||||||
color: limegreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer-tag-popup {
|
|
||||||
display: none;
|
|
||||||
padding: 2px 4px;
|
|
||||||
position: fixed;
|
|
||||||
bottom: 58px;
|
|
||||||
left: 20px;
|
|
||||||
background-color: var(--second-background-color);
|
|
||||||
z-index: 8;
|
|
||||||
border-radius: 5px;
|
|
||||||
max-height: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer-tag-content{
|
|
||||||
position: relative;
|
|
||||||
max-height: 400px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer-tag-content > * {
|
|
||||||
margin: 2px 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer-tag-popup::before{
|
|
||||||
content: "";
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
position: absolute;
|
|
||||||
border-right: 10px solid var(--second-background-color);
|
|
||||||
border-left: 10px solid transparent;
|
|
||||||
border-top: 10px solid var(--second-background-color);
|
|
||||||
border-bottom: 10px solid transparent;
|
|
||||||
left: 0;
|
|
||||||
bottom: -17px;
|
|
||||||
border-radius: 0 0 0 5px;
|
|
||||||
}
|
|
@ -4,7 +4,6 @@ services:
|
|||||||
image: glax/tranga-api:latest
|
image: glax/tranga-api:latest
|
||||||
container_name: tranga-api
|
container_name: tranga-api
|
||||||
volumes:
|
volumes:
|
||||||
- ./tranga:/usr/share/Tranga-API #1 when replacing ./tranga replace #2 with same value
|
|
||||||
- ./Manga:/Manga
|
- ./Manga:/Manga
|
||||||
ports:
|
ports:
|
||||||
- "6531:6531"
|
- "6531:6531"
|
||||||
@ -12,8 +11,6 @@ services:
|
|||||||
tranga-website:
|
tranga-website:
|
||||||
image: glax/tranga-website:latest
|
image: glax/tranga-website:latest
|
||||||
container_name: tranga-website
|
container_name: tranga-website
|
||||||
volumes:
|
|
||||||
- ./tranga/imageCache:/usr/share/nginx/html/imageCache:ro #2 when replacing Point to same value as #1/imageCache
|
|
||||||
ports:
|
ports:
|
||||||
- "9555:80"
|
- "9555:80"
|
||||||
depends_on:
|
depends_on:
|
||||||
|
Before Width: | Height: | Size: 2.1 MiB |
Before Width: | Height: | Size: 2.5 MiB |
Before Width: | Height: | Size: 354 KiB |
Before Width: | Height: | Size: 1.8 MiB |
Before Width: | Height: | Size: 1.5 MiB |