mirror of
https://github.com/C9Glax/tranga.git
synced 2025-02-22 23:30:13 +01:00
Remove excess
This commit is contained in:
parent
6534341fd5
commit
b9eecd3afd
@ -1,15 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<LangVersion>12</LangVersion>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Tranga\Tranga.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
157
CLI/Program.cs
157
CLI/Program.cs
@ -1,157 +0,0 @@
|
|||||||
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? logFolderPath = settings.fileLoggerPath ?? "";
|
|
||||||
Logger logger = new(enabledLoggers.ToArray(), Console.Out, Console.OutputEncoding, logFolderPath);
|
|
||||||
|
|
||||||
if(settings.workingDirectory is not null)
|
|
||||||
TrangaSettings.LoadFromWorkingDirectory(settings.workingDirectory);
|
|
||||||
else
|
|
||||||
TrangaSettings.CreateOrUpdate();
|
|
||||||
if(settings.downloadLocation is not null)
|
|
||||||
TrangaSettings.CreateOrUpdate(downloadDirectory: settings.downloadLocation);
|
|
||||||
|
|
||||||
Tranga.Tranga? api = null;
|
|
||||||
|
|
||||||
Thread trangaApi = new Thread(() =>
|
|
||||||
{
|
|
||||||
api = new(logger);
|
|
||||||
});
|
|
||||||
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,32 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Logging;
|
|
||||||
|
|
||||||
public class FileLogger : LoggerBase
|
|
||||||
{
|
|
||||||
internal string logFilePath { get; }
|
|
||||||
private const int MaxNumberOfLogFiles = 5;
|
|
||||||
|
|
||||||
public FileLogger(string logFilePath, Encoding? encoding = null) : base (encoding)
|
|
||||||
{
|
|
||||||
this.logFilePath = logFilePath;
|
|
||||||
|
|
||||||
DirectoryInfo dir = Directory.CreateDirectory(new FileInfo(logFilePath).DirectoryName!);
|
|
||||||
|
|
||||||
//Remove oldest logfile if more than MaxNumberOfLogFiles
|
|
||||||
for (int fileCount = dir.EnumerateFiles().Count(); fileCount > MaxNumberOfLogFiles - 1; fileCount--) //-1 because we create own logfile later
|
|
||||||
File.Delete(dir.EnumerateFiles().MinBy(file => file.LastWriteTime)!.FullName);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Write(LogMessage logMessage)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
File.AppendAllText(logFilePath, logMessage.formattedMessage);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Logging;
|
|
||||||
|
|
||||||
public class FormattedConsoleLogger : LoggerBase
|
|
||||||
{
|
|
||||||
private readonly TextWriter _stdOut;
|
|
||||||
public FormattedConsoleLogger(TextWriter stdOut, Encoding? encoding = null) : base(encoding)
|
|
||||||
{
|
|
||||||
this._stdOut = stdOut;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Write(LogMessage message)
|
|
||||||
{
|
|
||||||
this._stdOut.Write(message.formattedMessage);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
namespace Logging;
|
|
||||||
|
|
||||||
public readonly struct LogMessage
|
|
||||||
{
|
|
||||||
public DateTime logTime { get; }
|
|
||||||
public string caller { get; }
|
|
||||||
public string value { get; }
|
|
||||||
public string formattedMessage => ToString();
|
|
||||||
|
|
||||||
public LogMessage(DateTime messageTime, string caller, string value)
|
|
||||||
{
|
|
||||||
this.logTime = messageTime;
|
|
||||||
this.caller = caller;
|
|
||||||
this.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
string dateTimeString = $"{logTime.ToShortDateString()} {logTime.ToLongTimeString()}.{logTime.Millisecond,-3}";
|
|
||||||
string name = caller.Split(new char[] { '.', '+' }).Last();
|
|
||||||
return $"[{dateTimeString}] {name.Substring(0, name.Length >= 13 ? 13 : name.Length),13} | {value}";
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Logging;
|
|
||||||
|
|
||||||
public class Logger : TextWriter
|
|
||||||
{
|
|
||||||
private static readonly string LogDirectoryPath = RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|
|
||||||
? "/var/log/tranga-api"
|
|
||||||
: Path.Join(Directory.GetCurrentDirectory(), "logs");
|
|
||||||
public string? logFilePath => _fileLogger?.logFilePath;
|
|
||||||
public override Encoding Encoding { get; }
|
|
||||||
public enum LoggerType
|
|
||||||
{
|
|
||||||
FileLogger,
|
|
||||||
ConsoleLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly FileLogger? _fileLogger;
|
|
||||||
private readonly FormattedConsoleLogger? _formattedConsoleLogger;
|
|
||||||
private readonly MemoryLogger _memoryLogger;
|
|
||||||
|
|
||||||
public Logger(LoggerType[] enabledLoggers, TextWriter? stdOut, Encoding? encoding, string? logFolderPath)
|
|
||||||
{
|
|
||||||
this.Encoding = encoding ?? Encoding.UTF8;
|
|
||||||
DateTime now = DateTime.Now;
|
|
||||||
if(enabledLoggers.Contains(LoggerType.FileLogger) && (logFolderPath is null || logFolderPath == ""))
|
|
||||||
{
|
|
||||||
string filePath = Path.Join(LogDirectoryPath,
|
|
||||||
$"{now.ToShortDateString()}_{now.Hour}-{now.Minute}-{now.Second}.log");
|
|
||||||
_fileLogger = new FileLogger(filePath, encoding);
|
|
||||||
}else if (enabledLoggers.Contains(LoggerType.FileLogger) && logFolderPath is not null)
|
|
||||||
_fileLogger = new FileLogger(Path.Join(logFolderPath, $"{now.ToShortDateString()}_{now.Hour}-{now.Minute}-{now.Second}.log") , encoding);
|
|
||||||
|
|
||||||
|
|
||||||
if (enabledLoggers.Contains(LoggerType.ConsoleLogger) && stdOut is not null)
|
|
||||||
{
|
|
||||||
_formattedConsoleLogger = new FormattedConsoleLogger(stdOut, encoding);
|
|
||||||
}
|
|
||||||
else if (enabledLoggers.Contains(LoggerType.ConsoleLogger) && stdOut is null)
|
|
||||||
{
|
|
||||||
_formattedConsoleLogger = null;
|
|
||||||
throw new ArgumentException($"stdOut can not be null for LoggerType {LoggerType.ConsoleLogger}");
|
|
||||||
}
|
|
||||||
_memoryLogger = new MemoryLogger(encoding);
|
|
||||||
WriteLine(GetType().ToString(), $"Logfile: {logFilePath}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void WriteLine(string caller, string? value)
|
|
||||||
{
|
|
||||||
value = value is null ? Environment.NewLine : string.Concat(value, Environment.NewLine);
|
|
||||||
|
|
||||||
Write(caller, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Write(string caller, string? value)
|
|
||||||
{
|
|
||||||
if (value is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_fileLogger?.Write(caller, value);
|
|
||||||
_formattedConsoleLogger?.Write(caller, value);
|
|
||||||
_memoryLogger.Write(caller, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string[] Tail(uint? lines)
|
|
||||||
{
|
|
||||||
return _memoryLogger.Tail(lines);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string[] GetNewLines()
|
|
||||||
{
|
|
||||||
return _memoryLogger.GetNewLines();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string[] GetLog()
|
|
||||||
{
|
|
||||||
return _memoryLogger.GetLogMessages();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Logging;
|
|
||||||
|
|
||||||
public abstract class LoggerBase : TextWriter
|
|
||||||
{
|
|
||||||
public override Encoding Encoding { get; }
|
|
||||||
|
|
||||||
public LoggerBase(Encoding? encoding = null)
|
|
||||||
{
|
|
||||||
this.Encoding = encoding ?? Encoding.ASCII;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Write(string caller, string? value)
|
|
||||||
{
|
|
||||||
if (value is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
LogMessage message = new (DateTime.Now, caller, value);
|
|
||||||
|
|
||||||
Write(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract void Write(LogMessage message);
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<LangVersion>12</LangVersion>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
@ -1,74 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Logging;
|
|
||||||
|
|
||||||
public class MemoryLogger : LoggerBase
|
|
||||||
{
|
|
||||||
private readonly SortedList<DateTime, LogMessage> _logMessages = new();
|
|
||||||
private int _lastLogMessageIndex = 0;
|
|
||||||
|
|
||||||
public MemoryLogger(Encoding? encoding = null) : base(encoding)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Write(LogMessage value)
|
|
||||||
{
|
|
||||||
lock (_logMessages)
|
|
||||||
{
|
|
||||||
_logMessages.Add(DateTime.Now, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string[] GetLogMessages()
|
|
||||||
{
|
|
||||||
return Tail(Convert.ToUInt32(_logMessages.Count));
|
|
||||||
}
|
|
||||||
|
|
||||||
public string[] Tail(uint? length)
|
|
||||||
{
|
|
||||||
int retLength;
|
|
||||||
if (length is null || length > _logMessages.Count)
|
|
||||||
retLength = _logMessages.Count;
|
|
||||||
else
|
|
||||||
retLength = (int)length;
|
|
||||||
|
|
||||||
string[] ret = new string[retLength];
|
|
||||||
|
|
||||||
for (int retIndex = 0; retIndex < ret.Length; retIndex++)
|
|
||||||
{
|
|
||||||
lock (_logMessages)
|
|
||||||
{
|
|
||||||
ret[retIndex] = _logMessages.GetValueAtIndex(_logMessages.Count - retLength + retIndex).ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_lastLogMessageIndex = _logMessages.Count - 1;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string[] GetNewLines()
|
|
||||||
{
|
|
||||||
int logMessageCount = _logMessages.Count;
|
|
||||||
List<string> ret = new();
|
|
||||||
|
|
||||||
int retIndex = 0;
|
|
||||||
for (; retIndex < logMessageCount - _lastLogMessageIndex; retIndex++)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
lock(_logMessages)
|
|
||||||
{
|
|
||||||
ret.Add(_logMessages.GetValueAtIndex(_lastLogMessageIndex + retIndex).ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (NullReferenceException)//Called when LogMessage has not finished writing
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_lastLogMessageIndex = _lastLogMessageIndex + retIndex;
|
|
||||||
return ret.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
18
Tranga.sln
18
Tranga.sln
@ -1,11 +1,5 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tranga", ".\Tranga\Tranga.csproj", "{545E81B9-D96B-4C8F-A97F-2C02414DE566}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logging", "Logging\Logging.csproj", "{415BE889-BB7D-426F-976F-8D977876A462}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CLI", "CLI\CLI.csproj", "{4324C816-F9D2-468F-8ED6-397FE2F0DCB3}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API", "API\API.csproj", "{EDB07E7B-351F-4FCC-9AEF-777838E5551E}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API", "API\API.csproj", "{EDB07E7B-351F-4FCC-9AEF-777838E5551E}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
@ -14,18 +8,6 @@ Global
|
|||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{545E81B9-D96B-4C8F-A97F-2C02414DE566}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{545E81B9-D96B-4C8F-A97F-2C02414DE566}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{545E81B9-D96B-4C8F-A97F-2C02414DE566}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{545E81B9-D96B-4C8F-A97F-2C02414DE566}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{415BE889-BB7D-426F-976F-8D977876A462}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{415BE889-BB7D-426F-976F-8D977876A462}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{415BE889-BB7D-426F-976F-8D977876A462}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{415BE889-BB7D-426F-976F-8D977876A462}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{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
|
|
||||||
{EDB07E7B-351F-4FCC-9AEF-777838E5551E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{EDB07E7B-351F-4FCC-9AEF-777838E5551E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{EDB07E7B-351F-4FCC-9AEF-777838E5551E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{EDB07E7B-351F-4FCC-9AEF-777838E5551E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{EDB07E7B-351F-4FCC-9AEF-777838E5551E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{EDB07E7B-351F-4FCC-9AEF-777838E5551E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
@ -1,156 +0,0 @@
|
|||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Xml.Linq;
|
|
||||||
using static System.IO.UnixFileMode;
|
|
||||||
|
|
||||||
namespace Tranga;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Has to be Part of a publication
|
|
||||||
/// Includes the Chapter-Name, -VolumeNumber, -ChapterNumber, the location of the chapter on the internet and the saveName of the local file.
|
|
||||||
/// </summary>
|
|
||||||
public readonly struct Chapter : IComparable
|
|
||||||
{
|
|
||||||
// ReSharper disable once MemberCanBePrivate.Global
|
|
||||||
public Manga parentManga { get; }
|
|
||||||
public string? name { get; }
|
|
||||||
public float volumeNumber { get; }
|
|
||||||
public float chapterNumber { get; }
|
|
||||||
public string url { get; }
|
|
||||||
// ReSharper disable once MemberCanBePrivate.Global
|
|
||||||
public string fileName { get; }
|
|
||||||
public string? id { get; }
|
|
||||||
|
|
||||||
private static readonly Regex LegalCharacters = new (@"([A-z]*[0-9]* *\.*-*,*\]*\[*'*\'*\)*\(*~*!*)*");
|
|
||||||
private static readonly Regex IllegalStrings = new(@"(Vol(ume)?|Ch(apter)?)\.?", RegexOptions.IgnoreCase);
|
|
||||||
|
|
||||||
public Chapter(Manga parentManga, string? name, string? volumeNumber, string chapterNumber, string url, string? id = null)
|
|
||||||
: this(parentManga, name, float.Parse(volumeNumber??"0", GlobalBase.numberFormatDecimalPoint),
|
|
||||||
float.Parse(chapterNumber, GlobalBase.numberFormatDecimalPoint), url, id)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public Chapter(Manga parentManga, string? name, float? volumeNumber, float chapterNumber, string url, string? id = null)
|
|
||||||
{
|
|
||||||
this.parentManga = parentManga;
|
|
||||||
this.name = name;
|
|
||||||
this.volumeNumber = volumeNumber??0;
|
|
||||||
this.chapterNumber = chapterNumber;
|
|
||||||
this.url = url;
|
|
||||||
this.id = id;
|
|
||||||
|
|
||||||
string chapterVolNumStr = $"Vol.{this.volumeNumber} Ch.{chapterNumber}";
|
|
||||||
|
|
||||||
if (name is not null && name.Length > 0)
|
|
||||||
{
|
|
||||||
string chapterName = IllegalStrings.Replace(string.Concat(LegalCharacters.Matches(name)), "");
|
|
||||||
this.fileName = $"{chapterVolNumStr} - {chapterName}";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
this.fileName = chapterVolNumStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"Chapter {parentManga.sortName} {parentManga.internalId} {chapterNumber} {name}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object? obj)
|
|
||||||
{
|
|
||||||
if (obj is not Chapter)
|
|
||||||
return false;
|
|
||||||
return CompareTo(obj) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int CompareTo(object? obj)
|
|
||||||
{
|
|
||||||
if(obj is not Chapter otherChapter)
|
|
||||||
throw new ArgumentException($"{obj} can not be compared to {this}");
|
|
||||||
return volumeNumber.CompareTo(otherChapter.volumeNumber) switch
|
|
||||||
{
|
|
||||||
<0 => -1,
|
|
||||||
>0 => 1,
|
|
||||||
_ => chapterNumber.CompareTo(otherChapter.chapterNumber)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks if a chapter-archive is already present
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>true if chapter is present</returns>
|
|
||||||
internal bool CheckChapterIsDownloaded()
|
|
||||||
{
|
|
||||||
string mangaDirectory = Path.Join(TrangaSettings.downloadLocation, parentManga.folderName);
|
|
||||||
if (!Directory.Exists(mangaDirectory))
|
|
||||||
return false;
|
|
||||||
FileInfo? mangaArchive = null;
|
|
||||||
string markerPath = Path.Join(mangaDirectory, $".{id}");
|
|
||||||
if (this.id is not null && File.Exists(markerPath))
|
|
||||||
{
|
|
||||||
if(File.Exists(File.ReadAllText(markerPath)))
|
|
||||||
mangaArchive = new FileInfo(File.ReadAllText(markerPath));
|
|
||||||
else
|
|
||||||
File.Delete(markerPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(mangaArchive is null)
|
|
||||||
{
|
|
||||||
FileInfo[] archives = new DirectoryInfo(mangaDirectory).GetFiles("*.cbz");
|
|
||||||
Regex volChRex = new(@"(?:Vol(?:ume)?\.([0-9]+)\D*)?Ch(?:apter)?\.([0-9]+(?:\.[0-9]+)*)");
|
|
||||||
|
|
||||||
Chapter t = this;
|
|
||||||
mangaArchive = archives.FirstOrDefault(archive =>
|
|
||||||
{
|
|
||||||
Match m = volChRex.Match(archive.Name);
|
|
||||||
if (m.Groups[1].Success)
|
|
||||||
return m.Groups[1].Value == t.volumeNumber.ToString(GlobalBase.numberFormatDecimalPoint) &&
|
|
||||||
m.Groups[2].Value == t.chapterNumber.ToString(GlobalBase.numberFormatDecimalPoint);
|
|
||||||
else
|
|
||||||
return m.Groups[2].Value == t.chapterNumber.ToString(GlobalBase.numberFormatDecimalPoint);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
string correctPath = GetArchiveFilePath();
|
|
||||||
if(mangaArchive is not null && mangaArchive.FullName != correctPath)
|
|
||||||
mangaArchive.MoveTo(correctPath, true);
|
|
||||||
return (mangaArchive is not null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CreateChapterMarker()
|
|
||||||
{
|
|
||||||
if (this.id is null)
|
|
||||||
return;
|
|
||||||
string path = Path.Join(TrangaSettings.downloadLocation, parentManga.folderName, $".{id}");
|
|
||||||
File.WriteAllText(path, GetArchiveFilePath());
|
|
||||||
File.SetAttributes(path, FileAttributes.Hidden);
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
|
||||||
File.SetUnixFileMode(path, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates full file path of chapter-archive
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Filepath</returns>
|
|
||||||
internal string GetArchiveFilePath()
|
|
||||||
{
|
|
||||||
return Path.Join(TrangaSettings.downloadLocation, parentManga.folderName, $"{parentManga.folderName} - {this.fileName}.cbz");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a string containing XML of publication and chapter.
|
|
||||||
/// See ComicInfo.xml
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>XML-string</returns>
|
|
||||||
internal string GetComicInfoXmlString()
|
|
||||||
{
|
|
||||||
XElement comicInfo = new XElement("ComicInfo",
|
|
||||||
new XElement("Tags", string.Join(',', parentManga.tags)),
|
|
||||||
new XElement("LanguageISO", parentManga.originalLanguage),
|
|
||||||
new XElement("Title", this.name),
|
|
||||||
new XElement("Writer", string.Join(',', parentManga.authors)),
|
|
||||||
new XElement("Volume", this.volumeNumber),
|
|
||||||
new XElement("Number", this.chapterNumber)
|
|
||||||
);
|
|
||||||
return comicInfo.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,202 +0,0 @@
|
|||||||
using System.Globalization;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Logging;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Tranga.LibraryConnectors;
|
|
||||||
using Tranga.MangaConnectors;
|
|
||||||
using Tranga.NotificationConnectors;
|
|
||||||
|
|
||||||
namespace Tranga;
|
|
||||||
|
|
||||||
public abstract class GlobalBase
|
|
||||||
{
|
|
||||||
[JsonIgnore]
|
|
||||||
public Logger? logger { get; init; }
|
|
||||||
protected HashSet<NotificationConnector> notificationConnectors { get; init; }
|
|
||||||
protected HashSet<LibraryConnector> libraryConnectors { get; init; }
|
|
||||||
private Dictionary<string, Manga> cachedPublications { get; init; }
|
|
||||||
protected HashSet<MangaConnector> _connectors;
|
|
||||||
public static readonly NumberFormatInfo numberFormatDecimalPoint = new (){ NumberDecimalSeparator = "." };
|
|
||||||
protected static readonly Regex baseUrlRex = new(@"https?:\/\/[0-9A-z\.-]+(:[0-9]+)?");
|
|
||||||
|
|
||||||
protected GlobalBase(GlobalBase clone)
|
|
||||||
{
|
|
||||||
this.logger = clone.logger;
|
|
||||||
this.notificationConnectors = clone.notificationConnectors;
|
|
||||||
this.libraryConnectors = clone.libraryConnectors;
|
|
||||||
this.cachedPublications = clone.cachedPublications;
|
|
||||||
this._connectors = clone._connectors;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected GlobalBase(Logger? logger)
|
|
||||||
{
|
|
||||||
this.logger = logger;
|
|
||||||
this.notificationConnectors = TrangaSettings.LoadNotificationConnectors(this);
|
|
||||||
this.libraryConnectors = TrangaSettings.LoadLibraryConnectors(this);
|
|
||||||
this.cachedPublications = new();
|
|
||||||
this._connectors = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Manga? GetCachedManga(string internalId)
|
|
||||||
{
|
|
||||||
return cachedPublications.TryGetValue(internalId, out Manga manga) switch
|
|
||||||
{
|
|
||||||
true => manga,
|
|
||||||
_ => null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected IEnumerable<Manga> GetAllCachedManga() => cachedPublications.Values;
|
|
||||||
|
|
||||||
protected void AddMangaToCache(Manga manga)
|
|
||||||
{
|
|
||||||
if (!cachedPublications.TryAdd(manga.internalId, manga))
|
|
||||||
{
|
|
||||||
Log($"Overwriting Manga {manga.internalId}");
|
|
||||||
cachedPublications[manga.internalId] = manga;
|
|
||||||
}
|
|
||||||
ExportManga();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void RemoveMangaFromCache(Manga manga) => RemoveMangaFromCache(manga.internalId);
|
|
||||||
|
|
||||||
protected void RemoveMangaFromCache(string internalId)
|
|
||||||
{
|
|
||||||
cachedPublications.Remove(internalId);
|
|
||||||
ExportManga();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void ImportManga()
|
|
||||||
{
|
|
||||||
string folder = TrangaSettings.mangaCacheFolderPath;
|
|
||||||
Directory.CreateDirectory(folder);
|
|
||||||
|
|
||||||
foreach (FileInfo fileInfo in new DirectoryInfo(folder).GetFiles())
|
|
||||||
{
|
|
||||||
string content = File.ReadAllText(fileInfo.FullName);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Manga m = JsonConvert.DeserializeObject<Manga>(content, new MangaConnectorJsonConverter(this, _connectors));
|
|
||||||
this.cachedPublications.TryAdd(m.internalId, m);
|
|
||||||
}
|
|
||||||
catch (JsonException e)
|
|
||||||
{
|
|
||||||
Log($"Error parsing Manga {fileInfo.Name}:\n{e.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool ExportRunning = false;
|
|
||||||
private void ExportManga()
|
|
||||||
{
|
|
||||||
while (ExportRunning)
|
|
||||||
Thread.Sleep(1);
|
|
||||||
ExportRunning = true;
|
|
||||||
string folder = TrangaSettings.mangaCacheFolderPath;
|
|
||||||
Directory.CreateDirectory(folder);
|
|
||||||
Manga[] copy = new Manga[cachedPublications.Values.Count];
|
|
||||||
cachedPublications.Values.CopyTo(copy, 0);
|
|
||||||
foreach (Manga manga in copy)
|
|
||||||
{
|
|
||||||
string content = JsonConvert.SerializeObject(manga, Formatting.Indented);
|
|
||||||
string filePath = Path.Combine(folder, $"{manga.internalId}.json");
|
|
||||||
File.WriteAllText(filePath, content, Encoding.UTF8);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (FileInfo fileInfo in new DirectoryInfo(folder).GetFiles())
|
|
||||||
{
|
|
||||||
if(!cachedPublications.Keys.Any(key => fileInfo.Name.Substring(0, fileInfo.Name.LastIndexOf('.')).Equals(key)))
|
|
||||||
fileInfo.Delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
ExportRunning = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
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, bool buffer = false)
|
|
||||||
{
|
|
||||||
foreach (NotificationConnector nc in notificationConnectors)
|
|
||||||
nc.SendNotification(title, text, buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void AddNotificationConnector(NotificationConnector notificationConnector)
|
|
||||||
{
|
|
||||||
Log($"Adding {notificationConnector}");
|
|
||||||
notificationConnectors.RemoveWhere(nc => nc.notificationConnectorType == notificationConnector.notificationConnectorType);
|
|
||||||
notificationConnectors.Add(notificationConnector);
|
|
||||||
|
|
||||||
while(IsFileInUse(TrangaSettings.notificationConnectorsFilePath))
|
|
||||||
Thread.Sleep(100);
|
|
||||||
Log("Exporting notificationConnectors");
|
|
||||||
File.WriteAllText(TrangaSettings.notificationConnectorsFilePath, JsonConvert.SerializeObject(notificationConnectors));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void DeleteNotificationConnector(NotificationConnector.NotificationConnectorType notificationConnectorType)
|
|
||||||
{
|
|
||||||
Log($"Removing {notificationConnectorType}");
|
|
||||||
notificationConnectors.RemoveWhere(nc => nc.notificationConnectorType == notificationConnectorType);
|
|
||||||
while(IsFileInUse(TrangaSettings.notificationConnectorsFilePath))
|
|
||||||
Thread.Sleep(100);
|
|
||||||
Log("Exporting notificationConnectors");
|
|
||||||
File.WriteAllText(TrangaSettings.notificationConnectorsFilePath, JsonConvert.SerializeObject(notificationConnectors));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void UpdateLibraries()
|
|
||||||
{
|
|
||||||
foreach(LibraryConnector lc in libraryConnectors)
|
|
||||||
lc.UpdateLibrary();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void AddLibraryConnector(LibraryConnector libraryConnector)
|
|
||||||
{
|
|
||||||
Log($"Adding {libraryConnector}");
|
|
||||||
libraryConnectors.RemoveWhere(lc => lc.libraryType == libraryConnector.libraryType);
|
|
||||||
libraryConnectors.Add(libraryConnector);
|
|
||||||
|
|
||||||
while(IsFileInUse(TrangaSettings.libraryConnectorsFilePath))
|
|
||||||
Thread.Sleep(100);
|
|
||||||
Log("Exporting libraryConnectors");
|
|
||||||
File.WriteAllText(TrangaSettings.libraryConnectorsFilePath, JsonConvert.SerializeObject(libraryConnectors, Formatting.Indented));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void DeleteLibraryConnector(LibraryConnector.LibraryType libraryType)
|
|
||||||
{
|
|
||||||
Log($"Removing {libraryType}");
|
|
||||||
libraryConnectors.RemoveWhere(lc => lc.libraryType == libraryType);
|
|
||||||
while(IsFileInUse(TrangaSettings.libraryConnectorsFilePath))
|
|
||||||
Thread.Sleep(100);
|
|
||||||
Log("Exporting libraryConnectors");
|
|
||||||
File.WriteAllText(TrangaSettings.libraryConnectorsFilePath, JsonConvert.SerializeObject(libraryConnectors, Formatting.Indented));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected bool IsFileInUse(string filePath) => IsFileInUse(filePath, this.logger);
|
|
||||||
|
|
||||||
public static bool IsFileInUse(string filePath, Logger? logger)
|
|
||||||
{
|
|
||||||
if (!File.Exists(filePath))
|
|
||||||
return false;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using FileStream stream = new (filePath, FileMode.Open, FileAccess.Read, FileShare.None);
|
|
||||||
stream.Close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
catch (IOException)
|
|
||||||
{
|
|
||||||
logger?.WriteLine($"File is in use {filePath}");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using Tranga.MangaConnectors;
|
|
||||||
|
|
||||||
namespace Tranga.Jobs;
|
|
||||||
|
|
||||||
public class DownloadChapter : Job
|
|
||||||
{
|
|
||||||
public Chapter chapter { get; init; }
|
|
||||||
|
|
||||||
public DownloadChapter(GlobalBase clone, Chapter chapter, DateTime lastExecution, string? parentJobId = null) : base(clone, JobType.DownloadChapterJob, lastExecution, parentJobId: parentJobId)
|
|
||||||
{
|
|
||||||
this.chapter = chapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DownloadChapter(GlobalBase clone, Chapter chapter, string? parentJobId = null) : base(clone, JobType.DownloadChapterJob, parentJobId: parentJobId)
|
|
||||||
{
|
|
||||||
this.chapter = chapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override string GetId()
|
|
||||||
{
|
|
||||||
return $"{GetType()}-{chapter.parentManga.internalId}-{chapter.chapterNumber}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"{id} Chapter: {chapter}";
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override IEnumerable<Job> ExecuteReturnSubTasksInternal(JobBoss jobBoss)
|
|
||||||
{
|
|
||||||
Task downloadTask = new(delegate
|
|
||||||
{
|
|
||||||
mangaConnector.CopyCoverFromCacheToDownloadLocation(chapter.parentManga);
|
|
||||||
HttpStatusCode success = mangaConnector.DownloadChapter(chapter, this.progressToken);
|
|
||||||
chapter.parentManga.UpdateLatestDownloadedChapter(chapter);
|
|
||||||
if (success == HttpStatusCode.OK)
|
|
||||||
{
|
|
||||||
UpdateLibraries();
|
|
||||||
SendNotifications("Chapter downloaded", $"{chapter.parentManga.sortName} - {chapter.chapterNumber}", true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
downloadTask.Start();
|
|
||||||
return Array.Empty<Job>();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override MangaConnector GetMangaConnector()
|
|
||||||
{
|
|
||||||
return chapter.parentManga.mangaConnector;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object? obj)
|
|
||||||
{
|
|
||||||
if (obj is not DownloadChapter otherJob)
|
|
||||||
return false;
|
|
||||||
return otherJob.chapter.Equals(this.chapter);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
using Newtonsoft.Json;
|
|
||||||
using Tranga.MangaConnectors;
|
|
||||||
|
|
||||||
namespace Tranga.Jobs;
|
|
||||||
|
|
||||||
public class DownloadNewChapters : Job
|
|
||||||
{
|
|
||||||
public string mangaInternalId { get; set; }
|
|
||||||
[JsonIgnore] private Manga? manga => GetCachedManga(mangaInternalId);
|
|
||||||
public string translatedLanguage { get; init; }
|
|
||||||
|
|
||||||
public DownloadNewChapters(GlobalBase clone, string mangaInternalId, DateTime lastExecution, bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null, string translatedLanguage = "en") : base(clone, JobType.DownloadNewChaptersJob, lastExecution, recurring, recurrence, parentJobId)
|
|
||||||
{
|
|
||||||
this.mangaInternalId = mangaInternalId;
|
|
||||||
this.translatedLanguage = translatedLanguage;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DownloadNewChapters(GlobalBase clone, MangaConnector connector, string mangaInternalId, bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null, string translatedLanguage = "en") : base (clone, JobType.DownloadNewChaptersJob, recurring, recurrence, parentJobId)
|
|
||||||
{
|
|
||||||
this.mangaInternalId = mangaInternalId;
|
|
||||||
this.translatedLanguage = translatedLanguage;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override string GetId()
|
|
||||||
{
|
|
||||||
return $"{GetType()}-{mangaInternalId}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"{id} Manga: {manga}";
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override IEnumerable<Job> ExecuteReturnSubTasksInternal(JobBoss jobBoss)
|
|
||||||
{
|
|
||||||
if (manga is null)
|
|
||||||
{
|
|
||||||
Log($"Manga {mangaInternalId} is missing! Can not execute job.");
|
|
||||||
return Array.Empty<Job>();
|
|
||||||
}
|
|
||||||
manga.Value.SaveSeriesInfoJson();
|
|
||||||
Chapter[] chapters = manga.Value.mangaConnector.GetNewChapters(manga.Value, this.translatedLanguage);
|
|
||||||
this.progressToken.increments = chapters.Length;
|
|
||||||
List<Job> jobs = new();
|
|
||||||
manga.Value.mangaConnector.CopyCoverFromCacheToDownloadLocation(manga.Value);
|
|
||||||
foreach (Chapter chapter in chapters)
|
|
||||||
{
|
|
||||||
DownloadChapter downloadChapterJob = new(this, chapter, parentJobId: this.id);
|
|
||||||
jobs.Add(downloadChapterJob);
|
|
||||||
}
|
|
||||||
UpdateMetadata updateMetadataJob = new(this, mangaInternalId, parentJobId: this.id);
|
|
||||||
jobs.Add(updateMetadataJob);
|
|
||||||
progressToken.Complete();
|
|
||||||
return jobs;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override MangaConnector GetMangaConnector()
|
|
||||||
{
|
|
||||||
if (manga is null)
|
|
||||||
throw new Exception($"Missing Manga {mangaInternalId}");
|
|
||||||
return manga.Value.mangaConnector;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object? obj)
|
|
||||||
{
|
|
||||||
if (obj is not DownloadNewChapters otherJob)
|
|
||||||
return false;
|
|
||||||
return otherJob.mangaConnector == this.mangaConnector &&
|
|
||||||
otherJob.manga?.publicationId == this.manga?.publicationId;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
using Tranga.MangaConnectors;
|
|
||||||
|
|
||||||
namespace Tranga.Jobs;
|
|
||||||
|
|
||||||
public abstract class Job : GlobalBase
|
|
||||||
{
|
|
||||||
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; }
|
|
||||||
public enum JobType : byte { DownloadChapterJob = 0, DownloadNewChaptersJob = 1, UpdateMetaDataJob = 2, MonitorManga = 3 }
|
|
||||||
|
|
||||||
public MangaConnector mangaConnector => GetMangaConnector();
|
|
||||||
|
|
||||||
public JobType jobType;
|
|
||||||
|
|
||||||
internal Job(GlobalBase clone, JobType jobType, bool recurring = false, TimeSpan? recurrenceTime = null, string? parentJobId = null) : base(clone)
|
|
||||||
{
|
|
||||||
this.jobType = jobType;
|
|
||||||
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, JobType jobType, DateTime lastExecution, bool recurring = false,
|
|
||||||
TimeSpan? recurrenceTime = null, string? parentJobId = null) : base(clone)
|
|
||||||
{
|
|
||||||
this.jobType = jobType;
|
|
||||||
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 -= progressToken.incrementsCompleted;
|
|
||||||
this.lastExecution = DateTime.Now;
|
|
||||||
this.progressToken.Waiting();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ExecutionEnqueue()
|
|
||||||
{
|
|
||||||
this.progressToken.increments -= progressToken.incrementsCompleted;
|
|
||||||
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(JobBoss jobBoss)
|
|
||||||
{
|
|
||||||
progressToken.Start();
|
|
||||||
subJobs = ExecuteReturnSubTasksInternal(jobBoss);
|
|
||||||
lastExecution = DateTime.Now;
|
|
||||||
return subJobs;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract IEnumerable<Job> ExecuteReturnSubTasksInternal(JobBoss jobBoss);
|
|
||||||
|
|
||||||
protected abstract MangaConnector GetMangaConnector();
|
|
||||||
}
|
|
@ -1,303 +0,0 @@
|
|||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Tranga.MangaConnectors;
|
|
||||||
using static System.IO.UnixFileMode;
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
this.jobs = new();
|
|
||||||
LoadJobsList(connectors);
|
|
||||||
this.mangaConnectorJobQueue = new();
|
|
||||||
Log($"Next job in {jobs.MinBy(job => job.nextExecution)?.nextExecution.Subtract(DateTime.Now)} {jobs.MinBy(job => job.nextExecution)?.id}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool AddJob(Job job, string? jobFile = null)
|
|
||||||
{
|
|
||||||
if (ContainsJobLike(job))
|
|
||||||
{
|
|
||||||
Log($"Already Contains Job {job}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!this.jobs.Add(job))
|
|
||||||
return false;
|
|
||||||
Log($"Added {job}");
|
|
||||||
UpdateJobFile(job, jobFile);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddJobs(IEnumerable<Job> jobsToAdd)
|
|
||||||
{
|
|
||||||
foreach (Job job in jobsToAdd)
|
|
||||||
AddJob(job);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Compares contents of the provided job and all current jobs
|
|
||||||
/// Does not check if objects are the same
|
|
||||||
/// </summary>
|
|
||||||
public bool ContainsJobLike(Job job)
|
|
||||||
{
|
|
||||||
return this.jobs.Any(existingJob => existingJob.Equals(job));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveJob(Job job)
|
|
||||||
{
|
|
||||||
Log($"Removing {job}");
|
|
||||||
job.Cancel();
|
|
||||||
this.jobs.Remove(job);
|
|
||||||
if(job.subJobs is not null && job.subJobs.Any())
|
|
||||||
RemoveJobs(job.subJobs);
|
|
||||||
UpdateJobFile(job);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveJobs(IEnumerable<Job?> jobsToRemove)
|
|
||||||
{
|
|
||||||
List<Job?> toRemove = jobsToRemove.ToList(); //Prevent multiple enumeration
|
|
||||||
Log($"Removing {toRemove.Count()} jobs.");
|
|
||||||
foreach (Job? job in toRemove)
|
|
||||||
if(job is not null)
|
|
||||||
RemoveJob(job);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<Job> GetJobsLike(string? internalId = null, float? chapterNumber = null)
|
|
||||||
{
|
|
||||||
IEnumerable<Job> ret = this.jobs;
|
|
||||||
|
|
||||||
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.Equals(chapterNumber);
|
|
||||||
});
|
|
||||||
else if (internalId is not null)
|
|
||||||
ret = ret.Where(jjob =>
|
|
||||||
{
|
|
||||||
if (jjob is not DownloadNewChapters job)
|
|
||||||
return false;
|
|
||||||
return job.mangaInternalId == internalId;
|
|
||||||
});
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<Job> GetJobsLike(Manga? publication = null,
|
|
||||||
Chapter? chapter = null)
|
|
||||||
{
|
|
||||||
if (chapter is not null)
|
|
||||||
return GetJobsLike(chapter.Value.parentManga.internalId, chapter.Value.chapterNumber);
|
|
||||||
else
|
|
||||||
return GetJobsLike(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)
|
|
||||||
{
|
|
||||||
if (mangaConnectorJobQueue.TryAdd(job.mangaConnector, new Queue<Job>()))//If we can add the queue, there is certainly no job in it
|
|
||||||
return true;
|
|
||||||
return mangaConnectorJobQueue[job.mangaConnector].Contains(job);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddJobToQueue(Job job)
|
|
||||||
{
|
|
||||||
Log($"Adding Job to Queue. {job}");
|
|
||||||
if(!QueueContainsJob(job))
|
|
||||||
mangaConnectorJobQueue[job.mangaConnector].Enqueue(job);
|
|
||||||
job.ExecutionEnqueue();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddJobsToQueue(IEnumerable<Job> newJobs)
|
|
||||||
{
|
|
||||||
foreach(Job job in newJobs)
|
|
||||||
AddJobToQueue(job);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LoadJobsList(HashSet<MangaConnector> connectors)
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(TrangaSettings.jobsFolderPath);
|
|
||||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
|
||||||
File.SetUnixFileMode(TrangaSettings.jobsFolderPath, UserRead | UserWrite | UserExecute | GroupRead | OtherRead);
|
|
||||||
if (!Directory.Exists(TrangaSettings.jobsFolderPath)) //No jobs to load
|
|
||||||
return;
|
|
||||||
|
|
||||||
//Load Manga-Files
|
|
||||||
ImportManga();
|
|
||||||
|
|
||||||
//Load json-job-files
|
|
||||||
foreach (FileInfo file in Directory.GetFiles(TrangaSettings.jobsFolderPath, "*.json").Select(f => new FileInfo(f)))
|
|
||||||
{
|
|
||||||
Log($"Adding {file.Name}");
|
|
||||||
Job? job = JsonConvert.DeserializeObject<Job>(File.ReadAllText(file.FullName),
|
|
||||||
new JobJsonConverter(this, new MangaConnectorJsonConverter(this, connectors)));
|
|
||||||
if (job is null)
|
|
||||||
{
|
|
||||||
string newName = file.FullName + ".failed";
|
|
||||||
Log($"Failed loading file {file.Name}.\nMoving to {newName}");
|
|
||||||
File.Move(file.FullName, newName);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Log($"Adding Job {job}");
|
|
||||||
if (!AddJob(job, file.FullName)) //If we detect a duplicate, delete the file.
|
|
||||||
{
|
|
||||||
string path = string.Concat(file.FullName, ".duplicate");
|
|
||||||
file.MoveTo(path);
|
|
||||||
Log($"Duplicate detected or otherwise not able to add job to list.\nMoved job {job} to {path}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Connect jobs to parent-jobs and add Publications to cache
|
|
||||||
foreach (Job job in this.jobs)
|
|
||||||
{
|
|
||||||
Log($"Loading Job {job}");
|
|
||||||
Job? parentJob = this.jobs.FirstOrDefault(jjob => jjob.id == job.parentJobId);
|
|
||||||
if (parentJob is not null)
|
|
||||||
{
|
|
||||||
parentJob.AddSubJob(job);
|
|
||||||
Log($"Parent Job {parentJob}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
string[] jobMangaInternalIds = this.jobs.Where(job => job is DownloadNewChapters)
|
|
||||||
.Select(dnc => ((DownloadNewChapters)dnc).mangaInternalId).ToArray();
|
|
||||||
jobMangaInternalIds = jobMangaInternalIds.Concat(
|
|
||||||
this.jobs.Where(job => job is UpdateMetadata)
|
|
||||||
.Select(dnc => ((UpdateMetadata)dnc).mangaInternalId)).ToArray();
|
|
||||||
string[] internalIds = GetAllCachedManga().Select(m => m.internalId).ToArray();
|
|
||||||
|
|
||||||
string[] extraneousIds = internalIds.Except(jobMangaInternalIds).ToArray();
|
|
||||||
foreach (string internalId in extraneousIds)
|
|
||||||
RemoveMangaFromCache(internalId);
|
|
||||||
|
|
||||||
string[] coverFiles = Directory.GetFiles(TrangaSettings.coverImageCache);
|
|
||||||
foreach(string fileName in coverFiles.Where(fileName => !GetAllCachedManga().Any(manga => manga.coverFileNameInCache == fileName)))
|
|
||||||
File.Delete(fileName);
|
|
||||||
string[] mangaFiles = Directory.GetFiles(TrangaSettings.mangaCacheFolderPath);
|
|
||||||
foreach(string fileName in mangaFiles.Where(fileName => !GetAllCachedManga().Any(manga => fileName.Split('.')[0] == manga.internalId)))
|
|
||||||
File.Delete(fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void UpdateJobFile(Job job, string? oldFile = null)
|
|
||||||
{
|
|
||||||
string newJobFilePath = Path.Join(TrangaSettings.jobsFolderPath, $"{job.id}.json");
|
|
||||||
string oldFilePath = oldFile??Path.Join(TrangaSettings.jobsFolderPath, $"{job.id}.json");
|
|
||||||
|
|
||||||
//Delete old file
|
|
||||||
if (File.Exists(oldFilePath))
|
|
||||||
{
|
|
||||||
Log($"Deleting Job-file {oldFilePath}");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
while(IsFileInUse(oldFilePath))
|
|
||||||
Thread.Sleep(10);
|
|
||||||
File.Delete(oldFilePath);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Log($"Error deleting {oldFilePath} job {job.id}\n{e}");
|
|
||||||
return; //Don't export a new file when we haven't actually deleted the old one
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Export job (in new file) if it is still in our jobs list
|
|
||||||
if (GetJobById(job.id) is not null)
|
|
||||||
{
|
|
||||||
Log($"Exporting Job {newJobFilePath}");
|
|
||||||
string jobStr = JsonConvert.SerializeObject(job, Formatting.Indented);
|
|
||||||
while(IsFileInUse(newJobFilePath))
|
|
||||||
Thread.Sleep(10);
|
|
||||||
File.WriteAllText(newJobFilePath, jobStr);
|
|
||||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
|
||||||
File.SetUnixFileMode(newJobFilePath, UserRead | UserWrite | GroupRead | OtherRead);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateAllJobFiles()
|
|
||||||
{
|
|
||||||
Log("Exporting Jobs");
|
|
||||||
foreach (Job job in this.jobs)
|
|
||||||
UpdateJobFile(job);
|
|
||||||
|
|
||||||
//Remove files with jobs not in this.jobs-list
|
|
||||||
Regex idRex = new (@"(.*)\.json");
|
|
||||||
foreach (FileInfo file in new DirectoryInfo(TrangaSettings.jobsFolderPath).EnumerateFiles())
|
|
||||||
{
|
|
||||||
if (idRex.IsMatch(file.Name))
|
|
||||||
{
|
|
||||||
string id = idRex.Match(file.Name).Groups[1].Value;
|
|
||||||
if (!this.jobs.Any(job => job.id == id))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
file.Delete();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Log(e.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CheckJobs()
|
|
||||||
{
|
|
||||||
AddJobsToQueue(jobs.Where(job => job.progressToken.state == ProgressToken.State.Waiting && job.nextExecution < DateTime.Now && !QueueContainsJob(job)).OrderBy(job => job.nextExecution));
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
if(!queueHead.recurring)
|
|
||||||
RemoveJob(queueHead);
|
|
||||||
else
|
|
||||||
queueHead.ResetProgress();
|
|
||||||
jobQueue.Dequeue();
|
|
||||||
Log($"Next job in {jobs.MinBy(job => job.nextExecution)?.nextExecution.Subtract(DateTime.Now)} {jobs.MinBy(job => job.nextExecution)?.id}");
|
|
||||||
}else if (queueHead.progressToken.state is ProgressToken.State.Standby)
|
|
||||||
{
|
|
||||||
Job eJob = jobQueue.Peek();
|
|
||||||
Job[] subJobs = eJob.ExecuteReturnSubTasks(this).ToArray();
|
|
||||||
UpdateJobFile(eJob);
|
|
||||||
AddJobs(subJobs);
|
|
||||||
AddJobsToQueue(subJobs);
|
|
||||||
}else if (queueHead.progressToken.state is ProgressToken.State.Running && DateTime.Now.Subtract(queueHead.progressToken.lastUpdate) > TimeSpan.FromMinutes(5))
|
|
||||||
{
|
|
||||||
Log($"{queueHead} inactive for more than 5 minutes. Cancelling.");
|
|
||||||
queueHead.Cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
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("jobType"))
|
|
||||||
throw new Exception();
|
|
||||||
|
|
||||||
return Enum.Parse<Job.JobType>(jo["jobType"]!.Value<byte>().ToString()) switch
|
|
||||||
{
|
|
||||||
Job.JobType.UpdateMetaDataJob => new UpdateMetadata(_clone,
|
|
||||||
jo.GetValue("mangaInternalId")!.Value<string>()!,
|
|
||||||
jo.GetValue("parentJobId")!.Value<string?>()),
|
|
||||||
Job.JobType.DownloadChapterJob => new DownloadChapter(this._clone,
|
|
||||||
jo.GetValue("chapter")!.ToObject<Chapter>(JsonSerializer.Create(new JsonSerializerSettings()
|
|
||||||
{
|
|
||||||
Converters = { this._mangaConnectorJsonConverter }
|
|
||||||
})),
|
|
||||||
DateTime.UnixEpoch,
|
|
||||||
jo.GetValue("parentJobId")!.Value<string?>()),
|
|
||||||
Job.JobType.DownloadNewChaptersJob => new DownloadNewChapters(this._clone,
|
|
||||||
jo.GetValue("mangaInternalId")!.Value<string>()!,
|
|
||||||
jo.GetValue("lastExecution") is {} le
|
|
||||||
? le.ToObject<DateTime>()
|
|
||||||
: DateTime.UnixEpoch,
|
|
||||||
jo.GetValue("recurring")!.Value<bool>(),
|
|
||||||
jo.GetValue("recurrenceTime")!.ToObject<TimeSpan?>(),
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
namespace Tranga.Jobs;
|
|
||||||
|
|
||||||
public class ProgressToken
|
|
||||||
{
|
|
||||||
public bool cancellationRequested { get; set; }
|
|
||||||
public int increments { get; set; }
|
|
||||||
public int incrementsCompleted { get; set; }
|
|
||||||
public float progress => GetProgress();
|
|
||||||
public DateTime lastUpdate { get; private set; }
|
|
||||||
public DateTime executionStarted { get; private set; }
|
|
||||||
public TimeSpan timeRemaining => GetTimeRemaining();
|
|
||||||
|
|
||||||
public enum State : byte { Running = 0, Complete = 1, Standby = 2, Cancelled = 3, Waiting = 4 }
|
|
||||||
public State state { get; private set; }
|
|
||||||
|
|
||||||
public ProgressToken(int increments)
|
|
||||||
{
|
|
||||||
this.cancellationRequested = false;
|
|
||||||
this.increments = increments;
|
|
||||||
this.incrementsCompleted = 0;
|
|
||||||
this.state = State.Waiting;
|
|
||||||
this.executionStarted = DateTime.UnixEpoch;
|
|
||||||
this.lastUpdate = DateTime.UnixEpoch;
|
|
||||||
}
|
|
||||||
|
|
||||||
private float GetProgress()
|
|
||||||
{
|
|
||||||
if(increments > 0 && incrementsCompleted > 0)
|
|
||||||
return incrementsCompleted / (float)increments;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private TimeSpan GetTimeRemaining()
|
|
||||||
{
|
|
||||||
if (increments > 0 && incrementsCompleted > 0)
|
|
||||||
return DateTime.Now.Subtract(this.executionStarted).Divide(incrementsCompleted).Multiply(increments - incrementsCompleted);
|
|
||||||
return TimeSpan.MaxValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Increment()
|
|
||||||
{
|
|
||||||
this.lastUpdate = DateTime.Now;
|
|
||||||
this.incrementsCompleted++;
|
|
||||||
if (incrementsCompleted > increments)
|
|
||||||
state = State.Complete;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Standby()
|
|
||||||
{
|
|
||||||
this.lastUpdate = DateTime.Now;
|
|
||||||
state = State.Standby;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Start()
|
|
||||||
{
|
|
||||||
this.lastUpdate = DateTime.Now;
|
|
||||||
state = State.Running;
|
|
||||||
this.executionStarted = DateTime.Now;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Complete()
|
|
||||||
{
|
|
||||||
this.lastUpdate = DateTime.Now;
|
|
||||||
state = State.Complete;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Cancel()
|
|
||||||
{
|
|
||||||
this.lastUpdate = DateTime.Now;
|
|
||||||
state = State.Cancelled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Waiting()
|
|
||||||
{
|
|
||||||
this.lastUpdate = DateTime.Now;
|
|
||||||
state = State.Waiting;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,74 +0,0 @@
|
|||||||
using System.Text.Json.Serialization;
|
|
||||||
using Tranga.MangaConnectors;
|
|
||||||
|
|
||||||
namespace Tranga.Jobs;
|
|
||||||
|
|
||||||
public class UpdateMetadata : Job
|
|
||||||
{
|
|
||||||
public string mangaInternalId { get; set; }
|
|
||||||
[JsonIgnore] private Manga? manga => GetCachedManga(mangaInternalId);
|
|
||||||
|
|
||||||
public UpdateMetadata(GlobalBase clone, string mangaInternalId, string? parentJobId = null) : base(clone, JobType.UpdateMetaDataJob, parentJobId: parentJobId)
|
|
||||||
{
|
|
||||||
this.mangaInternalId = mangaInternalId;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override string GetId()
|
|
||||||
{
|
|
||||||
return $"{GetType()}-{mangaInternalId}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"{id} Manga: {manga}";
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override IEnumerable<Job> ExecuteReturnSubTasksInternal(JobBoss jobBoss)
|
|
||||||
{
|
|
||||||
if (manga is null)
|
|
||||||
{
|
|
||||||
Log($"Manga {mangaInternalId} is missing! Can not execute job.");
|
|
||||||
return Array.Empty<Job>();
|
|
||||||
}
|
|
||||||
|
|
||||||
//Retrieve new Metadata
|
|
||||||
Manga? possibleUpdatedManga = mangaConnector.GetMangaFromId(manga.Value.publicationId);
|
|
||||||
if (possibleUpdatedManga is { } updatedManga)
|
|
||||||
{
|
|
||||||
if (updatedManga.Equals(this.manga)) //Check if anything changed
|
|
||||||
{
|
|
||||||
this.progressToken.Complete();
|
|
||||||
return Array.Empty<Job>();
|
|
||||||
}
|
|
||||||
|
|
||||||
AddMangaToCache(manga.Value.WithMetadata(updatedManga));
|
|
||||||
this.manga.Value.SaveSeriesInfoJson(true);
|
|
||||||
this.mangaConnector.CopyCoverFromCacheToDownloadLocation((Manga)manga);
|
|
||||||
this.progressToken.Complete();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Log($"Could not find Manga {manga}");
|
|
||||||
this.progressToken.Cancel();
|
|
||||||
return Array.Empty<Job>();
|
|
||||||
}
|
|
||||||
this.progressToken.Cancel();
|
|
||||||
return Array.Empty<Job>();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override MangaConnector GetMangaConnector()
|
|
||||||
{
|
|
||||||
if (manga is null)
|
|
||||||
throw new Exception($"Missing Manga {mangaInternalId}");
|
|
||||||
return manga.Value.mangaConnector;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object? obj)
|
|
||||||
{
|
|
||||||
|
|
||||||
if (obj is not UpdateMetadata otherJob)
|
|
||||||
return false;
|
|
||||||
return otherJob.mangaConnector == this.mangaConnector &&
|
|
||||||
otherJob.manga?.publicationId == this.manga?.publicationId;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,126 +0,0 @@
|
|||||||
using System.Text.Json.Nodes;
|
|
||||||
using Logging;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using JsonSerializer = System.Text.Json.JsonSerializer;
|
|
||||||
|
|
||||||
namespace Tranga.LibraryConnectors;
|
|
||||||
|
|
||||||
public class Kavita : LibraryConnector
|
|
||||||
{
|
|
||||||
|
|
||||||
public Kavita(GlobalBase clone, string baseUrl, string username, string password) :
|
|
||||||
base(clone, baseUrl, GetToken(baseUrl, username, password, clone.logger), LibraryType.Kavita)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonConstructor]
|
|
||||||
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, Logger? logger = null)
|
|
||||||
{
|
|
||||||
HttpClient client = new()
|
|
||||||
{
|
|
||||||
DefaultRequestHeaders =
|
|
||||||
{
|
|
||||||
{ "Accept", "application/json" }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
HttpRequestMessage requestMessage = new ()
|
|
||||||
{
|
|
||||||
Method = HttpMethod.Post,
|
|
||||||
RequestUri = new Uri($"{baseUrl}/api/Account/login"),
|
|
||||||
Content = new StringContent($"{{\"username\":\"{username}\",\"password\":\"{password}\"}}", System.Text.Encoding.UTF8, "application/json")
|
|
||||||
};
|
|
||||||
try
|
|
||||||
{
|
|
||||||
HttpResponseMessage response = client.Send(requestMessage);
|
|
||||||
logger?.WriteLine($"Kavita | GetToken {requestMessage.RequestUri} -> {response.StatusCode}");
|
|
||||||
if (response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(response.Content.ReadAsStream());
|
|
||||||
if (result is not null)
|
|
||||||
return result["token"]!.GetValue<string>();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
logger?.WriteLine($"Kavita | {response.Content}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (HttpRequestException e)
|
|
||||||
{
|
|
||||||
logger?.WriteLine($"Kavita | Unable to retrieve token:\n\r{e}");
|
|
||||||
}
|
|
||||||
logger?.WriteLine("Kavita | Did not receive token.");
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void UpdateLibraryInternal()
|
|
||||||
{
|
|
||||||
Log("Updating libraries.");
|
|
||||||
foreach (KavitaLibrary lib in GetLibraries())
|
|
||||||
NetClient.MakePost($"{baseUrl}/api/Library/scan?libraryId={lib.id}", "Bearer", auth, logger);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal override bool Test()
|
|
||||||
{
|
|
||||||
foreach (KavitaLibrary lib in GetLibraries())
|
|
||||||
if (NetClient.MakePost($"{baseUrl}/api/Library/scan?libraryId={lib.id}", "Bearer", auth, logger))
|
|
||||||
return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fetches all libraries available to the user
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Array of KavitaLibrary</returns>
|
|
||||||
private IEnumerable<KavitaLibrary> GetLibraries()
|
|
||||||
{
|
|
||||||
Log("Getting libraries.");
|
|
||||||
Stream data = NetClient.MakeRequest($"{baseUrl}/api/Library/libraries", "Bearer", auth, logger);
|
|
||||||
if (data == Stream.Null)
|
|
||||||
{
|
|
||||||
Log("No libraries returned");
|
|
||||||
return Array.Empty<KavitaLibrary>();
|
|
||||||
}
|
|
||||||
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
|
|
||||||
if (result is null)
|
|
||||||
{
|
|
||||||
Log("No libraries returned");
|
|
||||||
return Array.Empty<KavitaLibrary>();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<KavitaLibrary> ret = new();
|
|
||||||
|
|
||||||
foreach (JsonNode? jsonNode in result)
|
|
||||||
{
|
|
||||||
JsonObject? jObject = (JsonObject?)jsonNode;
|
|
||||||
if(jObject is null)
|
|
||||||
continue;
|
|
||||||
int libraryId = jObject!["id"]!.GetValue<int>();
|
|
||||||
string libraryName = jObject["name"]!.GetValue<string>();
|
|
||||||
ret.Add(new KavitaLibrary(libraryId, libraryName));
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct KavitaLibrary
|
|
||||||
{
|
|
||||||
public int id { get; }
|
|
||||||
// ReSharper disable once UnusedAutoPropertyAccessor.Local
|
|
||||||
public string name { get; }
|
|
||||||
|
|
||||||
public KavitaLibrary(int id, string name)
|
|
||||||
{
|
|
||||||
this.id = id;
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,88 +0,0 @@
|
|||||||
using System.Text.Json.Nodes;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using JsonSerializer = System.Text.Json.JsonSerializer;
|
|
||||||
|
|
||||||
namespace Tranga.LibraryConnectors;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provides connectivity to Komga-API
|
|
||||||
/// Can fetch and update libraries
|
|
||||||
/// </summary>
|
|
||||||
public class Komga : LibraryConnector
|
|
||||||
{
|
|
||||||
public Komga(GlobalBase clone, string baseUrl, string username, string password)
|
|
||||||
: base(clone, baseUrl, Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}")), LibraryType.Komga)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonConstructor]
|
|
||||||
public Komga(GlobalBase clone, string baseUrl, string auth) : base(clone, baseUrl, auth, LibraryType.Komga)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"Komga {baseUrl}";
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void UpdateLibraryInternal()
|
|
||||||
{
|
|
||||||
Log("Updating libraries.");
|
|
||||||
foreach (KomgaLibrary lib in GetLibraries())
|
|
||||||
NetClient.MakePost($"{baseUrl}/api/v1/libraries/{lib.id}/scan", "Basic", auth, logger);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal override bool Test()
|
|
||||||
{
|
|
||||||
foreach (KomgaLibrary lib in GetLibraries())
|
|
||||||
if (NetClient.MakePost($"{baseUrl}/api/v1/libraries/{lib.id}/scan", "Basic", auth, logger))
|
|
||||||
return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fetches all libraries available to the user
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Array of KomgaLibraries</returns>
|
|
||||||
private IEnumerable<KomgaLibrary> GetLibraries()
|
|
||||||
{
|
|
||||||
Log("Getting Libraries");
|
|
||||||
Stream data = NetClient.MakeRequest($"{baseUrl}/api/v1/libraries", "Basic", auth, logger);
|
|
||||||
if (data == Stream.Null)
|
|
||||||
{
|
|
||||||
Log("No libraries returned");
|
|
||||||
return Array.Empty<KomgaLibrary>();
|
|
||||||
}
|
|
||||||
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
|
|
||||||
if (result is null)
|
|
||||||
{
|
|
||||||
Log("No libraries returned");
|
|
||||||
return Array.Empty<KomgaLibrary>();
|
|
||||||
}
|
|
||||||
|
|
||||||
HashSet<KomgaLibrary> ret = new();
|
|
||||||
|
|
||||||
foreach (JsonNode? jsonNode in result)
|
|
||||||
{
|
|
||||||
var jObject = (JsonObject?)jsonNode;
|
|
||||||
string libraryId = jObject!["id"]!.GetValue<string>();
|
|
||||||
string libraryName = jObject["name"]!.GetValue<string>();
|
|
||||||
ret.Add(new KomgaLibrary(libraryId, libraryName));
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct KomgaLibrary
|
|
||||||
{
|
|
||||||
public string id { get; }
|
|
||||||
// ReSharper disable once UnusedAutoPropertyAccessor.Local
|
|
||||||
public string name { get; }
|
|
||||||
|
|
||||||
public KomgaLibrary(string id, string name)
|
|
||||||
{
|
|
||||||
this.id = id;
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,144 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using System.Net.Http.Headers;
|
|
||||||
using Logging;
|
|
||||||
|
|
||||||
namespace Tranga.LibraryConnectors;
|
|
||||||
|
|
||||||
public abstract class LibraryConnector : GlobalBase
|
|
||||||
{
|
|
||||||
public enum LibraryType : byte
|
|
||||||
{
|
|
||||||
Komga = 0,
|
|
||||||
Kavita = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReSharper disable once UnusedAutoPropertyAccessor.Global
|
|
||||||
public LibraryType libraryType { get; }
|
|
||||||
public string baseUrl { get; }
|
|
||||||
// ReSharper disable once MemberCanBeProtected.Global
|
|
||||||
public string auth { get; } //Base64 encoded, if you use your password everywhere, you have problems
|
|
||||||
private DateTime? _updateLibraryRequested = null;
|
|
||||||
private readonly Thread? _libraryBufferThread = null;
|
|
||||||
private const int NoChangeTimeout = 2, BiggestInterval = 20;
|
|
||||||
|
|
||||||
protected LibraryConnector(GlobalBase clone, string baseUrl, string auth, LibraryType libraryType) : base(clone)
|
|
||||||
{
|
|
||||||
Log($"Creating libraryConnector {Enum.GetName(libraryType)}");
|
|
||||||
if (!baseUrlRex.IsMatch(baseUrl))
|
|
||||||
throw new ArgumentException("Base url does not match pattern");
|
|
||||||
if(auth == "")
|
|
||||||
throw new ArgumentNullException(nameof(auth), "Auth can not be empty");
|
|
||||||
this.baseUrl = baseUrlRex.Match(baseUrl).Value;
|
|
||||||
this.auth = auth;
|
|
||||||
this.libraryType = libraryType;
|
|
||||||
|
|
||||||
if (TrangaSettings.bufferLibraryUpdates)
|
|
||||||
{
|
|
||||||
_libraryBufferThread = new(CheckLibraryBuffer);
|
|
||||||
_libraryBufferThread.Start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CheckLibraryBuffer()
|
|
||||||
{
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
if (_updateLibraryRequested is not null && DateTime.Now.Subtract((DateTime)_updateLibraryRequested) > TimeSpan.FromMinutes(NoChangeTimeout)) //If no updates have been requested for NoChangeTimeout minutes, update library
|
|
||||||
{
|
|
||||||
UpdateLibraryInternal();
|
|
||||||
_updateLibraryRequested = null;
|
|
||||||
}
|
|
||||||
Thread.Sleep(100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateLibrary()
|
|
||||||
{
|
|
||||||
_updateLibraryRequested ??= DateTime.Now;
|
|
||||||
if (!TrangaSettings.bufferLibraryUpdates)
|
|
||||||
{
|
|
||||||
UpdateLibraryInternal();
|
|
||||||
return;
|
|
||||||
}else if (_updateLibraryRequested is not null &&
|
|
||||||
DateTime.Now.Subtract((DateTime)_updateLibraryRequested) > TimeSpan.FromMinutes(BiggestInterval)) //If the last update has been more than BiggestInterval minutes ago, update library
|
|
||||||
{
|
|
||||||
UpdateLibraryInternal();
|
|
||||||
_updateLibraryRequested = null;
|
|
||||||
}
|
|
||||||
else if(_updateLibraryRequested is not null)
|
|
||||||
{
|
|
||||||
Log($"Buffering Library Updates (Updates in latest {((DateTime)_updateLibraryRequested).Add(TimeSpan.FromMinutes(BiggestInterval)).Subtract(DateTime.Now)} or {((DateTime)_updateLibraryRequested).Add(TimeSpan.FromMinutes(NoChangeTimeout)).Subtract(DateTime.Now)})");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract void UpdateLibraryInternal();
|
|
||||||
internal abstract bool Test();
|
|
||||||
|
|
||||||
protected static class NetClient
|
|
||||||
{
|
|
||||||
public static Stream MakeRequest(string url, string authScheme, string auth, Logger? logger)
|
|
||||||
{
|
|
||||||
HttpClient client = new();
|
|
||||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authScheme, auth);
|
|
||||||
|
|
||||||
HttpRequestMessage requestMessage = new ()
|
|
||||||
{
|
|
||||||
Method = HttpMethod.Get,
|
|
||||||
RequestUri = new Uri(url)
|
|
||||||
};
|
|
||||||
try
|
|
||||||
{
|
|
||||||
|
|
||||||
HttpResponseMessage response = client.Send(requestMessage);
|
|
||||||
logger?.WriteLine("LibraryManager.NetClient",
|
|
||||||
$"GET {url} -> {(int)response.StatusCode}: {response.ReasonPhrase}");
|
|
||||||
|
|
||||||
if (response.StatusCode is HttpStatusCode.Unauthorized &&
|
|
||||||
response.RequestMessage!.RequestUri!.AbsoluteUri != url)
|
|
||||||
return MakeRequest(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth, logger);
|
|
||||||
else if (response.IsSuccessStatusCode)
|
|
||||||
return response.Content.ReadAsStream();
|
|
||||||
else
|
|
||||||
return Stream.Null;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
switch (e)
|
|
||||||
{
|
|
||||||
case HttpRequestException:
|
|
||||||
logger?.WriteLine("LibraryManager.NetClient", $"Failed to make Request:\n\r{e}\n\rContinuing.");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
return Stream.Null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool MakePost(string url, string authScheme, string auth, Logger? logger)
|
|
||||||
{
|
|
||||||
HttpClient client = new()
|
|
||||||
{
|
|
||||||
DefaultRequestHeaders =
|
|
||||||
{
|
|
||||||
{ "Accept", "application/json" },
|
|
||||||
{ "Authorization", new AuthenticationHeaderValue(authScheme, auth).ToString() }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
HttpRequestMessage requestMessage = new ()
|
|
||||||
{
|
|
||||||
Method = HttpMethod.Post,
|
|
||||||
RequestUri = new Uri(url)
|
|
||||||
};
|
|
||||||
HttpResponseMessage response = client.Send(requestMessage);
|
|
||||||
logger?.WriteLine("LibraryManager.NetClient", $"POST {url} -> {(int)response.StatusCode}: {response.ReasonPhrase}");
|
|
||||||
|
|
||||||
if(response.StatusCode is HttpStatusCode.Unauthorized && response.RequestMessage!.RequestUri!.AbsoluteUri != url)
|
|
||||||
return MakePost(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth, logger);
|
|
||||||
else if (response.IsSuccessStatusCode)
|
|
||||||
return true;
|
|
||||||
else
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace Tranga.LibraryConnectors;
|
|
||||||
|
|
||||||
public class LibraryManagerJsonConverter : JsonConverter
|
|
||||||
{
|
|
||||||
private readonly 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");
|
|
||||||
}
|
|
||||||
}
|
|
219
Tranga/Manga.cs
219
Tranga/Manga.cs
@ -1,219 +0,0 @@
|
|||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Web;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Tranga.MangaConnectors;
|
|
||||||
using static System.IO.UnixFileMode;
|
|
||||||
|
|
||||||
namespace Tranga;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Contains information on a Publication (Manga)
|
|
||||||
/// </summary>
|
|
||||||
public struct Manga
|
|
||||||
{
|
|
||||||
public string sortName { get; private set; }
|
|
||||||
public List<string> authors { get; private set; }
|
|
||||||
// ReSharper disable once UnusedAutoPropertyAccessor.Global
|
|
||||||
public Dictionary<string,string> altTitles { get; private set; }
|
|
||||||
// ReSharper disable once MemberCanBePrivate.Global
|
|
||||||
public string? description { get; private set; }
|
|
||||||
public string[] tags { get; private set; }
|
|
||||||
// ReSharper disable once UnusedAutoPropertyAccessor.Global
|
|
||||||
public string? coverUrl { get; private set; }
|
|
||||||
public string? coverFileNameInCache { get; private set; }
|
|
||||||
// ReSharper disable once UnusedAutoPropertyAccessor.Global
|
|
||||||
public Dictionary<string,string> links { get; }
|
|
||||||
// ReSharper disable once MemberCanBePrivate.Global
|
|
||||||
public int? year { get; private set; }
|
|
||||||
public string? originalLanguage { get; }
|
|
||||||
public ReleaseStatusByte releaseStatus { get; private set; }
|
|
||||||
public enum ReleaseStatusByte : byte
|
|
||||||
{
|
|
||||||
Continuing = 0,
|
|
||||||
Completed = 1,
|
|
||||||
OnHiatus = 2,
|
|
||||||
Cancelled = 3,
|
|
||||||
Unreleased = 4
|
|
||||||
};
|
|
||||||
public string folderName { get; private set; }
|
|
||||||
public string publicationId { get; }
|
|
||||||
public string internalId { get; }
|
|
||||||
public float ignoreChaptersBelow { get; set; }
|
|
||||||
public float latestChapterDownloaded { get; set; }
|
|
||||||
public float latestChapterAvailable { get; set; }
|
|
||||||
public string websiteUrl { get; private set; }
|
|
||||||
public MangaConnector mangaConnector { get; private set; }
|
|
||||||
|
|
||||||
private static readonly Regex LegalCharacters = new (@"[A-Za-zÀ-ÖØ-öø-ÿ0-9 \.\-,'\'\)\(~!\+]*");
|
|
||||||
|
|
||||||
[JsonConstructor]
|
|
||||||
public Manga(MangaConnector mangaConnector, 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 publicationId, ReleaseStatusByte releaseStatus, string? websiteUrl, string? folderName = null, float? ignoreChaptersBelow = 0)
|
|
||||||
{
|
|
||||||
this.mangaConnector = mangaConnector;
|
|
||||||
this.sortName = HttpUtility.HtmlDecode(sortName);
|
|
||||||
this.authors = authors.Select(HttpUtility.HtmlDecode).ToList()!;
|
|
||||||
this.description = HttpUtility.HtmlDecode(description);
|
|
||||||
this.altTitles = altTitles.ToDictionary(a => HttpUtility.HtmlDecode(a.Key), a => HttpUtility.HtmlDecode(a.Value));
|
|
||||||
this.tags = tags.Select(HttpUtility.HtmlDecode).ToArray()!;
|
|
||||||
this.coverFileNameInCache = coverFileNameInCache;
|
|
||||||
this.coverUrl = coverUrl;
|
|
||||||
this.links = links ?? new Dictionary<string, string>();
|
|
||||||
this.year = year;
|
|
||||||
this.originalLanguage = originalLanguage;
|
|
||||||
this.publicationId = publicationId;
|
|
||||||
this.folderName = folderName ?? string.Concat(LegalCharacters.Matches(HttpUtility.HtmlDecode(sortName)));
|
|
||||||
while (this.folderName.EndsWith('.'))
|
|
||||||
this.folderName = this.folderName.Substring(0, this.folderName.Length - 1);
|
|
||||||
string onlyLowerLetters = string.Concat(this.sortName.ToLower().Where(Char.IsLetter));
|
|
||||||
this.internalId = DateTime.Now.Ticks.ToString();
|
|
||||||
this.ignoreChaptersBelow = ignoreChaptersBelow ?? 0f;
|
|
||||||
this.latestChapterDownloaded = 0;
|
|
||||||
this.latestChapterAvailable = 0;
|
|
||||||
this.releaseStatus = releaseStatus;
|
|
||||||
this.websiteUrl = websiteUrl??"";
|
|
||||||
}
|
|
||||||
|
|
||||||
public Manga WithMetadata(Manga newManga)
|
|
||||||
{
|
|
||||||
return this with
|
|
||||||
{
|
|
||||||
sortName = newManga.sortName,
|
|
||||||
description = newManga.description,
|
|
||||||
coverUrl = newManga.coverUrl,
|
|
||||||
authors = authors.Union(newManga.authors).ToList(),
|
|
||||||
altTitles = altTitles.UnionBy(newManga.altTitles, kv => kv.Key).ToDictionary(x => x.Key, x => x.Value),
|
|
||||||
tags = tags.Union(newManga.tags).ToArray(),
|
|
||||||
releaseStatus = newManga.releaseStatus,
|
|
||||||
websiteUrl = newManga.websiteUrl,
|
|
||||||
year = newManga.year,
|
|
||||||
coverFileNameInCache = newManga.coverFileNameInCache
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object? obj)
|
|
||||||
{
|
|
||||||
if (obj is not Manga compareManga)
|
|
||||||
return false;
|
|
||||||
return this.description == compareManga.description &&
|
|
||||||
this.year == compareManga.year &&
|
|
||||||
this.releaseStatus == compareManga.releaseStatus &&
|
|
||||||
this.sortName == compareManga.sortName &&
|
|
||||||
this.latestChapterAvailable.Equals(compareManga.latestChapterAvailable) &&
|
|
||||||
this.authors.All(a => compareManga.authors.Contains(a)) &&
|
|
||||||
(this.coverFileNameInCache??"").Equals(compareManga.coverFileNameInCache) &&
|
|
||||||
(this.websiteUrl??"").Equals(compareManga.websiteUrl) &&
|
|
||||||
this.tags.All(t => compareManga.tags.Contains(t));
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"Publication {sortName} {internalId}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public string CreatePublicationFolder(string downloadDirectory)
|
|
||||||
{
|
|
||||||
string publicationFolder = Path.Join(downloadDirectory, this.folderName);
|
|
||||||
if(!Directory.Exists(publicationFolder))
|
|
||||||
Directory.CreateDirectory(publicationFolder);
|
|
||||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
|
||||||
File.SetUnixFileMode(publicationFolder, GroupRead | GroupWrite | GroupExecute | OtherRead | OtherWrite | OtherExecute | UserRead | UserWrite | UserExecute);
|
|
||||||
return publicationFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void MovePublicationFolder(string downloadDirectory, string newFolderName)
|
|
||||||
{
|
|
||||||
string oldPath = Path.Join(downloadDirectory, this.folderName);
|
|
||||||
this.folderName = newFolderName;//Create new Path with the new folderName
|
|
||||||
string newPath = CreatePublicationFolder(downloadDirectory);
|
|
||||||
if (Directory.Exists(oldPath))
|
|
||||||
{
|
|
||||||
if (Directory.Exists(newPath)) //Move/Overwrite old Files, Delete old Directory
|
|
||||||
{
|
|
||||||
IEnumerable<string> newPathFileNames = new DirectoryInfo(newPath).GetFiles().Select(fi => fi.Name);
|
|
||||||
foreach(FileInfo fileInfo in new DirectoryInfo(oldPath).GetFiles().Where(fi => newPathFileNames.Contains(fi.Name) == false))
|
|
||||||
File.Move(fileInfo.FullName, Path.Join(newPath, fileInfo.Name), true);
|
|
||||||
Directory.Delete(oldPath);
|
|
||||||
}else
|
|
||||||
Directory.Move(oldPath, newPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateLatestDownloadedChapter(Chapter chapter)//TODO check files if chapters are all downloaded
|
|
||||||
{
|
|
||||||
float chapterNumber = Convert.ToSingle(chapter.chapterNumber, GlobalBase.numberFormatDecimalPoint);
|
|
||||||
latestChapterDownloaded = latestChapterDownloaded < chapterNumber ? chapterNumber : latestChapterDownloaded;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SaveSeriesInfoJson(bool overwrite = false)
|
|
||||||
{
|
|
||||||
string publicationFolder = CreatePublicationFolder(TrangaSettings.downloadLocation);
|
|
||||||
string seriesInfoPath = Path.Join(publicationFolder, "series.json");
|
|
||||||
if(overwrite || (!overwrite && !File.Exists(seriesInfoPath)))
|
|
||||||
File.WriteAllText(seriesInfoPath,this.GetSeriesInfoJson());
|
|
||||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
|
||||||
File.SetUnixFileMode(seriesInfoPath, GroupRead | GroupWrite | OtherRead | OtherWrite | UserRead | UserWrite);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <returns>Serialized JSON String for series.json</returns>
|
|
||||||
private string GetSeriesInfoJson()
|
|
||||||
{
|
|
||||||
SeriesInfo si = new (new Metadata(this));
|
|
||||||
return System.Text.Json.JsonSerializer.Serialize(si);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Only for series.json
|
|
||||||
private struct SeriesInfo
|
|
||||||
{
|
|
||||||
// ReSharper disable once UnusedAutoPropertyAccessor.Local we need it, trust
|
|
||||||
[JsonRequired]public Metadata metadata { get; }
|
|
||||||
public SeriesInfo(Metadata metadata) => this.metadata = metadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Only for series.json what an abomination, why are all the fields not-null????
|
|
||||||
private struct Metadata
|
|
||||||
{
|
|
||||||
// ReSharper disable UnusedAutoPropertyAccessor.Local we need them all, trust me
|
|
||||||
[JsonRequired] public string type { get; }
|
|
||||||
[JsonRequired] public string publisher { get; }
|
|
||||||
// ReSharper disable twice IdentifierTypo
|
|
||||||
[JsonRequired] public int comicid { get; }
|
|
||||||
[JsonRequired] public string booktype { get; }
|
|
||||||
// ReSharper disable InconsistentNaming This one property is capitalized. Why?
|
|
||||||
[JsonRequired] public string ComicImage { get; }
|
|
||||||
[JsonRequired] public int total_issues { get; }
|
|
||||||
[JsonRequired] public string publication_run { get; }
|
|
||||||
[JsonRequired]public string name { get; }
|
|
||||||
[JsonRequired]public string year { get; }
|
|
||||||
[JsonRequired]public string status { get; }
|
|
||||||
[JsonRequired]public string description_text { get; }
|
|
||||||
|
|
||||||
public Metadata(Manga manga) : this(manga.sortName, manga.year.ToString() ?? string.Empty, manga.releaseStatus, manga.description ?? "")
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public Metadata(string name, string year, ReleaseStatusByte status, string description_text)
|
|
||||||
{
|
|
||||||
this.name = name;
|
|
||||||
this.year = year;
|
|
||||||
this.status = status switch
|
|
||||||
{
|
|
||||||
ReleaseStatusByte.Continuing => "Continuing",
|
|
||||||
ReleaseStatusByte.Completed => "Ended",
|
|
||||||
_ => Enum.GetName(status) ?? "Ended"
|
|
||||||
};
|
|
||||||
this.description_text = description_text;
|
|
||||||
|
|
||||||
//kill it with fire, but otherwise Komga will not parse
|
|
||||||
type = "Manga";
|
|
||||||
publisher = "";
|
|
||||||
comicid = 0;
|
|
||||||
booktype = "";
|
|
||||||
ComicImage = "";
|
|
||||||
total_issues = 0;
|
|
||||||
publication_run = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace Tranga.Server;
|
|
||||||
|
|
||||||
internal struct RequestPath
|
|
||||||
{
|
|
||||||
internal readonly string HttpMethod;
|
|
||||||
internal readonly string RegexStr;
|
|
||||||
internal readonly Func<GroupCollection, Dictionary<string, string>, ValueTuple<HttpStatusCode, object?>> Method;
|
|
||||||
|
|
||||||
public RequestPath(string httpHttpMethod, string regexStr,
|
|
||||||
Func<GroupCollection, Dictionary<string, string>, ValueTuple<HttpStatusCode, object?>> method)
|
|
||||||
{
|
|
||||||
this.HttpMethod = httpHttpMethod;
|
|
||||||
this.RegexStr = regexStr + "(?:/?)";
|
|
||||||
this.Method = method;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,269 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using SixLabors.ImageSharp;
|
|
||||||
using SixLabors.ImageSharp.Formats.Png;
|
|
||||||
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
|
|
||||||
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
|
|
||||||
using ZstdSharp;
|
|
||||||
|
|
||||||
namespace Tranga.Server;
|
|
||||||
|
|
||||||
public partial class Server : GlobalBase, IDisposable
|
|
||||||
{
|
|
||||||
private readonly HttpListener _listener = new();
|
|
||||||
private readonly Tranga _parent;
|
|
||||||
private bool _running = true;
|
|
||||||
|
|
||||||
private readonly List<RequestPath> _apiRequestPaths;
|
|
||||||
|
|
||||||
public Server(Tranga parent) : base(parent)
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
* Contains all valid Request Methods, Paths (with Regex Group Matching for specific Parameters) and Handling Methods
|
|
||||||
*/
|
|
||||||
_apiRequestPaths = new List<RequestPath>
|
|
||||||
{
|
|
||||||
new ("GET", @"/v2/Connector/Types", GetV2ConnectorTypes),
|
|
||||||
new ("GET", @"/v2/Connector/([a-zA-Z]+)/GetManga", GetV2ConnectorConnectorNameGetManga),
|
|
||||||
new ("GET", @"/v2/Mangas", GetV2Mangas),
|
|
||||||
new ("GET", @"/v2/Manga/Search", GetV2MangaSearch),
|
|
||||||
new ("GET", @"/v2/Manga", GetV2Manga),
|
|
||||||
new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Cover", GetV2MangaInternalIdCover),
|
|
||||||
new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Chapters", GetV2MangaInternalIdChapters),
|
|
||||||
new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Chapters/Latest", GetV2MangaInternalIdChaptersLatest),
|
|
||||||
new ("POST", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/ignoreChaptersBelow", PostV2MangaInternalIdIgnoreChaptersBelow),
|
|
||||||
new ("POST", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/moveFolder", PostV2MangaInternalIdMoveFolder),
|
|
||||||
new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})", GetV2MangaInternalId),
|
|
||||||
new ("DELETE", @"/v2/Manga/([-A-Za-z0-9]*={0,3})", DeleteV2MangaInternalId),
|
|
||||||
new ("GET", @"/v2/Jobs", GetV2Jobs),
|
|
||||||
new ("GET", @"/v2/Jobs/Running", GetV2JobsRunning),
|
|
||||||
new ("GET", @"/v2/Jobs/Waiting", GetV2JobsWaiting),
|
|
||||||
new ("GET", @"/v2/Jobs/Monitoring", GetV2JobsMonitoring),
|
|
||||||
new ("GET", @"/v2/Jobs/Standby", GetV2JobsStandby),
|
|
||||||
new ("GET", @"/v2/Job/Types", GetV2JobTypes),
|
|
||||||
new ("POST", @"/v2/Job/Create/([a-zA-Z]+)", PostV2JobCreateType),
|
|
||||||
new ("GET", @"/v2/Job", GetV2Job),
|
|
||||||
new ("GET", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)/Progress", GetV2JobJobIdProgress),
|
|
||||||
new ("POST", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)/StartNow", PostV2JobJobIdStartNow),
|
|
||||||
new ("POST", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)/Cancel", PostV2JobJobIdCancel),
|
|
||||||
new ("GET", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)", GetV2JobJobId),
|
|
||||||
new ("DELETE", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)", DeleteV2JobJobId),
|
|
||||||
new ("GET", @"/v2/Settings", GetV2Settings),
|
|
||||||
new ("GET", @"/v2/Settings/UserAgent", GetV2SettingsUserAgent),
|
|
||||||
new ("POST", @"/v2/Settings/UserAgent", PostV2SettingsUserAgent),
|
|
||||||
new ("GET", @"/v2/Settings/RateLimit/Types", GetV2SettingsRateLimitTypes),
|
|
||||||
new ("GET", @"/v2/Settings/RateLimit", GetV2SettingsRateLimit),
|
|
||||||
new ("POST", @"/v2/Settings/RateLimit", PostV2SettingsRateLimit),
|
|
||||||
new ("GET", @"/v2/Settings/RateLimit/([a-zA-Z]+)", GetV2SettingsRateLimitType),
|
|
||||||
new ("POST", @"/v2/Settings/RateLimit/([a-zA-Z]+)", PostV2SettingsRateLimitType),
|
|
||||||
new ("GET", @"/v2/Settings/AprilFoolsMode", GetV2SettingsAprilFoolsMode),
|
|
||||||
new ("POST", @"/v2/Settings/AprilFoolsMode", PostV2SettingsAprilFoolsMode),
|
|
||||||
new ("GET", @"/v2/Settings/CompressImages", GetV2SettingsCompressImages),
|
|
||||||
new ("POST", @"/v2/Settings/CompressImages", PostV2SettingsCompressImages),
|
|
||||||
new ("GET", @"/v2/Settings/BWImages", GetV2SettingsBwImages),
|
|
||||||
new ("POST", @"/v2/Settings/BWImages", PostV2SettingsBwImages),
|
|
||||||
new ("POST", @"/v2/Settings/DownloadLocation", PostV2SettingsDownloadLocation),
|
|
||||||
new ("GET", @"/v2/LibraryConnector", GetV2LibraryConnector),
|
|
||||||
new ("GET", @"/v2/LibraryConnector/Types", GetV2LibraryConnectorTypes),
|
|
||||||
new ("GET", @"/v2/LibraryConnector/([a-zA-Z]+)", GetV2LibraryConnectorType),
|
|
||||||
new ("POST", @"/v2/LibraryConnector/([a-zA-Z]+)", PostV2LibraryConnectorType),
|
|
||||||
new ("POST", @"/v2/LibraryConnector/([a-zA-Z]+)/Test", PostV2LibraryConnectorTypeTest),
|
|
||||||
new ("DELETE", @"/v2/LibraryConnector/([a-zA-Z]+)", DeleteV2LibraryConnectorType),
|
|
||||||
new ("GET", @"/v2/NotificationConnector", GetV2NotificationConnector),
|
|
||||||
new ("GET", @"/v2/NotificationConnector/Types", GetV2NotificationConnectorTypes),
|
|
||||||
new ("GET", @"/v2/NotificationConnector/([a-zA-Z]+)", GetV2NotificationConnectorType),
|
|
||||||
new ("POST", @"/v2/NotificationConnector/([a-zA-Z]+)", PostV2NotificationConnectorType),
|
|
||||||
new ("POST", @"/v2/NotificationConnector/([a-zA-Z]+)/Test", PostV2NotificationConnectorTypeTest),
|
|
||||||
new ("DELETE", @"/v2/NotificationConnector/([a-zA-Z]+)", DeleteV2NotificationConnectorType),
|
|
||||||
new ("GET", @"/v2/LogFile", GetV2LogFile),
|
|
||||||
new ("GET", @"/v2/Ping", GetV2Ping),
|
|
||||||
new ("POST", @"/v2/Ping", PostV2Ping)
|
|
||||||
};
|
|
||||||
|
|
||||||
this._parent = parent;
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
|
||||||
this._listener.Prefixes.Add($"http://*:{TrangaSettings.apiPortNumber}/");
|
|
||||||
else
|
|
||||||
this._listener.Prefixes.Add($"http://localhost:{TrangaSettings.apiPortNumber}/");
|
|
||||||
Thread listenThread = new(Listen);
|
|
||||||
listenThread.Start();
|
|
||||||
while(_parent.keepRunning && _running)
|
|
||||||
Thread.Sleep(100);
|
|
||||||
this.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleRequest(HttpListenerContext context)
|
|
||||||
{
|
|
||||||
HttpListenerRequest request = context.Request;
|
|
||||||
HttpListenerResponse response = context.Response;
|
|
||||||
if (request.HttpMethod == "OPTIONS")
|
|
||||||
{
|
|
||||||
SendResponse(HttpStatusCode.NoContent, response);//Response always contains all valid Request-Methods
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (request.Url!.LocalPath.Contains("favicon"))
|
|
||||||
{
|
|
||||||
SendResponse(HttpStatusCode.NoContent, response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
string path = Regex.Match(request.Url.LocalPath, @"\/[a-zA-Z0-9\.+/=-]+(\/[a-zA-Z0-9\.+/=-]+)*").Value; //Local Path
|
|
||||||
|
|
||||||
if (!Regex.IsMatch(path, "/v2(/.*)?")) //Use only v2 API
|
|
||||||
{
|
|
||||||
SendResponse(HttpStatusCode.NotFound, response, "Use Version 2 API");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Dictionary<string, string> requestVariables = GetRequestVariables(request.Url!.Query); //Variables in the URI
|
|
||||||
Dictionary<string, string> requestBody = GetRequestBody(request); //Variables in the JSON body
|
|
||||||
Dictionary<string, string> requestParams = requestVariables.UnionBy(requestBody, v => v.Key)
|
|
||||||
.ToDictionary(kv => kv.Key, kv => kv.Value); //The actual variable used for the API
|
|
||||||
|
|
||||||
ValueTuple<HttpStatusCode, object?> responseMessage; //Used to respond to the HttpRequest
|
|
||||||
if (_apiRequestPaths.Any(p => p.HttpMethod == request.HttpMethod && Regex.Match(path, p.RegexStr).Length == path.Length)) //Check if Request-Path is valid
|
|
||||||
{
|
|
||||||
RequestPath requestPath =
|
|
||||||
_apiRequestPaths.First(p => p.HttpMethod == request.HttpMethod && Regex.Match(path, p.RegexStr).Length == path.Length);
|
|
||||||
responseMessage =
|
|
||||||
requestPath.Method.Invoke(Regex.Match(path, requestPath.RegexStr).Groups, requestParams); //Get HttpResponse content
|
|
||||||
}
|
|
||||||
else
|
|
||||||
responseMessage = new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.MethodNotAllowed, "Unknown Request Path");
|
|
||||||
|
|
||||||
SendResponse(responseMessage.Item1, response, responseMessage.Item2);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 Dictionary<string, string> GetRequestBody(HttpListenerRequest request)
|
|
||||||
{
|
|
||||||
if (!request.HasEntityBody)
|
|
||||||
{
|
|
||||||
//Nospam Log("No request body");
|
|
||||||
return new Dictionary<string, string>();
|
|
||||||
}
|
|
||||||
Stream body = request.InputStream;
|
|
||||||
Encoding encoding = request.ContentEncoding;
|
|
||||||
using StreamReader streamReader = new (body, encoding);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Dictionary<string, string> requestBody =
|
|
||||||
JsonConvert.DeserializeObject<Dictionary<string, string>>(streamReader.ReadToEnd())
|
|
||||||
?? new();
|
|
||||||
return requestBody;
|
|
||||||
}
|
|
||||||
catch (JsonException e)
|
|
||||||
{
|
|
||||||
Log(e.Message);
|
|
||||||
}
|
|
||||||
return new Dictionary<string, string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
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.AddHeader("Access-Control-Allow-Origin", "*");
|
|
||||||
response.AddHeader("Content-Encoding", "zstd");
|
|
||||||
|
|
||||||
using CompressionStream compressor = new (response.OutputStream, 5);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (content is Stream stream)
|
|
||||||
{
|
|
||||||
response.ContentType = "text/plain";
|
|
||||||
response.AddHeader("Cache-Control", "private, no-store");
|
|
||||||
stream.CopyTo(compressor);
|
|
||||||
stream.Close();
|
|
||||||
}else if (content is Image image)
|
|
||||||
{
|
|
||||||
response.ContentType = image.Metadata.DecodedImageFormat?.DefaultMimeType ?? PngFormat.Instance.DefaultMimeType;
|
|
||||||
response.AddHeader("Cache-Control", "public, max-age=3600");
|
|
||||||
response.AddHeader("Expires", $"{DateTime.Now.AddHours(1):ddd\\,\\ dd\\ MMM\\ yyyy\\ HH\\:mm\\:ss} GMT");
|
|
||||||
string lastModifiedStr = "";
|
|
||||||
if (image.Metadata.IptcProfile is not null)
|
|
||||||
{
|
|
||||||
DateTime date = DateTime.ParseExact(image.Metadata.IptcProfile.GetValues(IptcTag.CreatedDate).First().Value, "yyyyMMdd",null);
|
|
||||||
DateTime time = DateTime.ParseExact(image.Metadata.IptcProfile.GetValues(IptcTag.CreatedTime).First().Value, "HHmmssK",null);
|
|
||||||
lastModifiedStr = $"{date:ddd\\,\\ dd\\ MMM\\ yyyy} {time:HH\\:mm\\:ss} GMT";
|
|
||||||
}else if (image.Metadata.ExifProfile is not null)
|
|
||||||
{
|
|
||||||
DateTime datetime = DateTime.ParseExact(image.Metadata.ExifProfile.Values.FirstOrDefault(value => value.Tag == ExifTag.DateTime)?.ToString() ?? "2000:01:01 01:01:01", "yyyy:MM:dd HH:mm:ss", null);
|
|
||||||
lastModifiedStr = $"{datetime:ddd\\,\\ dd\\ MMM\\ yyyy\\ HH\\:mm\\:ss} GMT";
|
|
||||||
}
|
|
||||||
if(lastModifiedStr.Length>0)
|
|
||||||
response.AddHeader("Last-Modified", lastModifiedStr);
|
|
||||||
image.Save(compressor, image.Metadata.DecodedImageFormat ?? PngFormat.Instance);
|
|
||||||
image.Dispose();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
response.ContentType = "application/json";
|
|
||||||
response.AddHeader("Cache-Control", "private, no-store");
|
|
||||||
if(content is not null)
|
|
||||||
new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content))).CopyTo(compressor);
|
|
||||||
else
|
|
||||||
compressor.Write(Array.Empty<byte>());
|
|
||||||
}
|
|
||||||
|
|
||||||
compressor.Flush();
|
|
||||||
response.OutputStream.Close();
|
|
||||||
}
|
|
||||||
catch (HttpListenerException e)
|
|
||||||
{
|
|
||||||
Log(e.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_running = false;
|
|
||||||
((IDisposable)_listener).Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Tranga.MangaConnectors;
|
|
||||||
|
|
||||||
namespace Tranga.Server;
|
|
||||||
|
|
||||||
public partial class Server
|
|
||||||
{
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2ConnectorTypes(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.Accepted, _parent.GetConnectors());
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2ConnectorConnectorNameGetManga(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if(groups.Count < 1 ||
|
|
||||||
!_parent.GetConnectors().Any(mangaConnector => mangaConnector.name == groups[1].Value)||
|
|
||||||
!_parent.TryGetConnector(groups[1].Value, out MangaConnector? connector) ||
|
|
||||||
connector is null)
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.BadRequest, $"Connector '{groups[1].Value}' does not exist.");
|
|
||||||
|
|
||||||
if (requestParameters.TryGetValue("title", out string? title))
|
|
||||||
{
|
|
||||||
return (HttpStatusCode.OK, connector.GetManga(title));
|
|
||||||
}else if (requestParameters.TryGetValue("url", out string? url))
|
|
||||||
{
|
|
||||||
return (HttpStatusCode.OK, connector.GetMangaFromUrl(url));
|
|
||||||
}else
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.BadRequest, "Parameter 'title' or 'url' has to be set.");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,176 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Tranga.Jobs;
|
|
||||||
using Tranga.MangaConnectors;
|
|
||||||
|
|
||||||
namespace Tranga.Server;
|
|
||||||
|
|
||||||
public partial class Server
|
|
||||||
{
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2Jobs(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, _parent.jobBoss.jobs.Select(job => job.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2JobsRunning(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, _parent.jobBoss.jobs
|
|
||||||
.Where(job => job.progressToken.state is ProgressToken.State.Running)
|
|
||||||
.Select(job => job.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2JobsWaiting(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, _parent.jobBoss.jobs
|
|
||||||
.Where(job => job.progressToken.state is ProgressToken.State.Waiting)
|
|
||||||
.Select(job => job.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2JobsStandby(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, _parent.jobBoss.jobs
|
|
||||||
.Where(job => job.progressToken.state is ProgressToken.State.Standby)
|
|
||||||
.Select(job => job.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2JobsMonitoring(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, _parent.jobBoss.jobs
|
|
||||||
.Where(job => job.jobType is Job.JobType.DownloadNewChaptersJob)
|
|
||||||
.Select(job => job.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2JobTypes(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK,
|
|
||||||
Enum.GetValues<Job.JobType>().ToDictionary(b => (byte)b, b => Enum.GetName(b)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> PostV2JobCreateType(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if (groups.Count < 1 ||
|
|
||||||
!Enum.TryParse(groups[1].Value, true, out Job.JobType jobType))
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"JobType {groups[1].Value} does not exist.");
|
|
||||||
}
|
|
||||||
|
|
||||||
string? mangaId;
|
|
||||||
Manga? manga;
|
|
||||||
switch (jobType)
|
|
||||||
{
|
|
||||||
case Job.JobType.MonitorManga:
|
|
||||||
if(!requestParameters.TryGetValue("internalId", out mangaId) ||
|
|
||||||
!_parent.TryGetPublicationById(mangaId, out manga) ||
|
|
||||||
manga is null)
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, "'internalId' Parameter missing, or is not a valid ID.");
|
|
||||||
if(!requestParameters.TryGetValue("interval", out string? intervalStr) ||
|
|
||||||
!TimeSpan.TryParse(intervalStr, out TimeSpan interval))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.InternalServerError, "'interval' Parameter missing, or is not in correct format.");
|
|
||||||
requestParameters.TryGetValue("language", out string? language);
|
|
||||||
if (requestParameters.TryGetValue("customFolder", out string? folder))
|
|
||||||
manga.Value.MovePublicationFolder(TrangaSettings.downloadLocation, folder);
|
|
||||||
if (requestParameters.TryGetValue("startChapter", out string? startChapterStr) &&
|
|
||||||
float.TryParse(startChapterStr, out float startChapter))
|
|
||||||
{
|
|
||||||
Manga manga1 = manga.Value;
|
|
||||||
manga1.ignoreChaptersBelow = startChapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _parent.jobBoss.AddJob(new DownloadNewChapters(this, ((Manga)manga).mangaConnector,
|
|
||||||
((Manga)manga).internalId, true, interval, language)) switch
|
|
||||||
{
|
|
||||||
true => new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null),
|
|
||||||
false => new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.Conflict, "Job already exists."),
|
|
||||||
};
|
|
||||||
case Job.JobType.UpdateMetaDataJob:
|
|
||||||
if(!requestParameters.TryGetValue("internalId", out mangaId) ||
|
|
||||||
!_parent.TryGetPublicationById(mangaId, out manga) ||
|
|
||||||
manga is null)
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, "InternalId Parameter missing, or is not a valid ID.");
|
|
||||||
return _parent.jobBoss.AddJob(new UpdateMetadata(this, ((Manga)manga).internalId)) switch
|
|
||||||
{
|
|
||||||
true => new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null),
|
|
||||||
false => new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.Conflict, "Job already exists."),
|
|
||||||
};
|
|
||||||
case Job.JobType.DownloadNewChaptersJob: //TODO
|
|
||||||
case Job.JobType.DownloadChapterJob: //TODO
|
|
||||||
default: return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.MethodNotAllowed, $"JobType {Enum.GetName(jobType)} is not supported.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2JobJobId(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if (groups.Count < 1 ||
|
|
||||||
!_parent.jobBoss.TryGetJobById(groups[1].Value, out Job? job) ||
|
|
||||||
job is null)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Job with ID: '{groups[1].Value}' does not exist.");
|
|
||||||
}
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, job);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> DeleteV2JobJobId(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if (groups.Count < 1 ||
|
|
||||||
!_parent.jobBoss.TryGetJobById(groups[1].Value, out Job? job) ||
|
|
||||||
job is null)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Job with ID: '{groups[1].Value}' does not exist.");
|
|
||||||
}
|
|
||||||
|
|
||||||
_parent.jobBoss.RemoveJob(job);
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2JobJobIdProgress(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
|
|
||||||
if (groups.Count < 1 ||
|
|
||||||
!_parent.jobBoss.TryGetJobById(groups[1].Value, out Job? job) ||
|
|
||||||
job is null)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.BadRequest, $"Job with ID: '{groups[1].Value}' does not exist.");
|
|
||||||
}
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, job.progressToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> PostV2JobJobIdStartNow(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if (groups.Count < 1 ||
|
|
||||||
!_parent.jobBoss.TryGetJobById(groups[1].Value, out Job? job) ||
|
|
||||||
job is null)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Job with ID: '{groups[1].Value}' does not exist.");
|
|
||||||
}
|
|
||||||
_parent.jobBoss.AddJobs(job.ExecuteReturnSubTasks(_parent.jobBoss));
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> PostV2JobJobIdCancel(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if (groups.Count < 1 ||
|
|
||||||
!_parent.jobBoss.TryGetJobById(groups[1].Value, out Job? job) ||
|
|
||||||
job is null)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Job with ID: '{groups[1].Value}' does not exist.");
|
|
||||||
}
|
|
||||||
job.Cancel();
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2Job(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if(!requestParameters.TryGetValue("jobIds", out string? jobIdListStr))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.BadRequest, "Missing parameter 'jobIds'.");
|
|
||||||
string[] jobIdList = jobIdListStr.Split(',');
|
|
||||||
List<Job> ret = new();
|
|
||||||
foreach (string jobId in jobIdList)
|
|
||||||
{
|
|
||||||
if(!_parent.jobBoss.TryGetJobById(jobId, out Job? job) || job is null)
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Job with id '{jobId}' not found.");
|
|
||||||
ret.Add(job);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, ret);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,116 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Tranga.LibraryConnectors;
|
|
||||||
|
|
||||||
namespace Tranga.Server;
|
|
||||||
|
|
||||||
public partial class Server
|
|
||||||
{
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2LibraryConnector(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, libraryConnectors);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2LibraryConnectorTypes(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK,
|
|
||||||
Enum.GetValues<LibraryConnector.LibraryType>().ToDictionary(b => (byte)b, b => Enum.GetName(b)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2LibraryConnectorType(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if (groups.Count < 1 ||
|
|
||||||
!Enum.TryParse(groups[1].Value, true, out LibraryConnector.LibraryType libraryType))
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"LibraryType {groups[1].Value} does not exist.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(libraryConnectors.All(lc => lc.libraryType != libraryType))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"LibraryType {Enum.GetName(libraryType)} not configured.");
|
|
||||||
else
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, libraryConnectors.First(lc => lc.libraryType == libraryType));
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> PostV2LibraryConnectorType(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if (groups.Count < 1 ||
|
|
||||||
!Enum.TryParse(groups[1].Value, true, out LibraryConnector.LibraryType libraryType))
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"LibraryType {groups[1].Value} does not exist.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!requestParameters.TryGetValue("url", out string? url))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'url' missing.");
|
|
||||||
if(!requestParameters.TryGetValue("username", out string? username))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'username' missing.");
|
|
||||||
if(!requestParameters.TryGetValue("password", out string? password))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'password' missing.");
|
|
||||||
|
|
||||||
switch (libraryType)
|
|
||||||
{
|
|
||||||
case LibraryConnector.LibraryType.Kavita:
|
|
||||||
Kavita kavita = new (this, url, username, password);
|
|
||||||
libraryConnectors.RemoveWhere(lc => lc.libraryType == LibraryConnector.LibraryType.Kavita);
|
|
||||||
libraryConnectors.Add(kavita);
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, kavita);
|
|
||||||
case LibraryConnector.LibraryType.Komga:
|
|
||||||
Komga komga = new (this, url, username, password);
|
|
||||||
libraryConnectors.RemoveWhere(lc => lc.libraryType == LibraryConnector.LibraryType.Komga);
|
|
||||||
libraryConnectors.Add(komga);
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, komga);
|
|
||||||
default: return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.MethodNotAllowed, $"LibraryType {Enum.GetName(libraryType)} is not supported.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> PostV2LibraryConnectorTypeTest(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if (groups.Count < 1 ||
|
|
||||||
!Enum.TryParse(groups[1].Value, true, out LibraryConnector.LibraryType libraryType))
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"LibraryType {groups[1].Value} does not exist.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!requestParameters.TryGetValue("url", out string? url))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'url' missing.");
|
|
||||||
if(!requestParameters.TryGetValue("username", out string? username))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'username' missing.");
|
|
||||||
if(!requestParameters.TryGetValue("password", out string? password))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'password' missing.");
|
|
||||||
|
|
||||||
switch (libraryType)
|
|
||||||
{
|
|
||||||
case LibraryConnector.LibraryType.Kavita:
|
|
||||||
Kavita kavita = new (this, url, username, password);
|
|
||||||
return kavita.Test() switch
|
|
||||||
{
|
|
||||||
true => new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, kavita),
|
|
||||||
_ => new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.FailedDependency, kavita)
|
|
||||||
};
|
|
||||||
case LibraryConnector.LibraryType.Komga:
|
|
||||||
Komga komga = new (this, url, username, password);
|
|
||||||
return komga.Test() switch
|
|
||||||
{
|
|
||||||
true => new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, komga),
|
|
||||||
_ => new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.FailedDependency, komga)
|
|
||||||
};
|
|
||||||
default: return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.MethodNotAllowed, $"LibraryType {Enum.GetName(libraryType)} is not supported.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> DeleteV2LibraryConnectorType(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if (groups.Count < 1 ||
|
|
||||||
!Enum.TryParse(groups[1].Value, true, out LibraryConnector.LibraryType libraryType))
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"LibraryType {groups[1].Value} does not exist.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(libraryConnectors.All(lc => lc.libraryType != libraryType))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"LibraryType {Enum.GetName(libraryType)} not configured.");
|
|
||||||
else
|
|
||||||
{
|
|
||||||
libraryConnectors.Remove(libraryConnectors.First(lc => lc.libraryType == libraryType));
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,166 +0,0 @@
|
|||||||
using SixLabors.ImageSharp;
|
|
||||||
using SixLabors.ImageSharp.Processing;
|
|
||||||
using System.Net;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using SixLabors.ImageSharp.Processing.Processors.Transforms;
|
|
||||||
using Tranga.Jobs;
|
|
||||||
using Tranga.MangaConnectors;
|
|
||||||
|
|
||||||
namespace Tranga.Server;
|
|
||||||
|
|
||||||
public partial class Server
|
|
||||||
{
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2Mangas(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, GetAllCachedManga().Select(m => m.internalId));
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2Manga(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if(!requestParameters.TryGetValue("mangaIds", out string? mangaIdListStr))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.BadRequest, "Missing parameter 'mangaIds'.");
|
|
||||||
string[] mangaIdList = mangaIdListStr.Split(',').Distinct().ToArray();
|
|
||||||
List<Manga> ret = new();
|
|
||||||
foreach (string mangaId in mangaIdList)
|
|
||||||
{
|
|
||||||
if(!_parent.TryGetPublicationById(mangaId, out Manga? manga) || manga is null)
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Manga with id '{mangaId}' not found.");
|
|
||||||
ret.Add(manga.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2MangaSearch(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if(!requestParameters.TryGetValue("title", out string? title))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.BadRequest, "Missing parameter 'title'.");
|
|
||||||
List<Manga> ret = new();
|
|
||||||
List<Thread> threads = new();
|
|
||||||
foreach (MangaConnector mangaConnector in _connectors)
|
|
||||||
{
|
|
||||||
Thread t = new (() =>
|
|
||||||
{
|
|
||||||
ret.AddRange(mangaConnector.GetManga(title));
|
|
||||||
});
|
|
||||||
t.Start();
|
|
||||||
threads.Add(t);
|
|
||||||
}
|
|
||||||
while(threads.Any(t => t.ThreadState is ThreadState.Running or ThreadState.WaitSleepJoin))
|
|
||||||
Thread.Sleep(10);
|
|
||||||
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2MangaInternalId(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if(groups.Count < 1 ||
|
|
||||||
!_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) ||
|
|
||||||
manga is null)
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'");
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, manga);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> DeleteV2MangaInternalId(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if(groups.Count < 1 ||
|
|
||||||
!_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) ||
|
|
||||||
manga is null)
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'");
|
|
||||||
Job[] jobs = _parent.jobBoss.GetJobsLike(publication: manga).ToArray();
|
|
||||||
_parent.jobBoss.RemoveJobs(jobs);
|
|
||||||
RemoveMangaFromCache(groups[1].Value);
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2MangaInternalIdCover(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if(groups.Count < 1 ||
|
|
||||||
!_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) ||
|
|
||||||
manga is null)
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'");
|
|
||||||
string filePath = manga.Value.coverFileNameInCache!;
|
|
||||||
if(!File.Exists(filePath))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, "Cover-File not found.");
|
|
||||||
|
|
||||||
Image image = Image.Load(filePath);
|
|
||||||
if (requestParameters.TryGetValue("dimensions", out string? dimensionsStr))
|
|
||||||
{
|
|
||||||
Regex dimensionsRex = new(@"([0-9]+)x([0-9]+)");
|
|
||||||
if(!dimensionsRex.IsMatch(dimensionsStr))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.BadRequest, "Requested dimensions not in required format.");
|
|
||||||
Match m = dimensionsRex.Match(dimensionsStr);
|
|
||||||
int width = int.Parse(m.Groups[1].Value);
|
|
||||||
int height = int.Parse(m.Groups[2].Value);
|
|
||||||
double aspectRequested = (double)width / (double)height;
|
|
||||||
|
|
||||||
double aspectCover = (double)image.Width / (double)image.Height;
|
|
||||||
|
|
||||||
Size newSize = aspectRequested > aspectCover
|
|
||||||
? new Size(width, (width / image.Width) * image.Height)
|
|
||||||
: new Size((height / image.Height) * image.Width, height);
|
|
||||||
|
|
||||||
image.Mutate(x => x.Resize(newSize, CubicResampler.Robidoux, true));
|
|
||||||
}
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, image);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2MangaInternalIdChapters(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if(groups.Count < 1 ||
|
|
||||||
!_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) ||
|
|
||||||
manga is null)
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'");
|
|
||||||
|
|
||||||
Chapter[] chapters = requestParameters.TryGetValue("language", out string? parameter) switch
|
|
||||||
{
|
|
||||||
true => manga.Value.mangaConnector.GetChapters((Manga)manga, parameter),
|
|
||||||
false => manga.Value.mangaConnector.GetChapters((Manga)manga)
|
|
||||||
};
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, chapters);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2MangaInternalIdChaptersLatest(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if(groups.Count < 1 ||
|
|
||||||
!_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) ||
|
|
||||||
manga is null)
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'");
|
|
||||||
|
|
||||||
float latest = requestParameters.TryGetValue("language", out string? parameter) switch
|
|
||||||
{
|
|
||||||
true => manga.Value.mangaConnector.GetChapters(manga.Value, parameter).Max().chapterNumber,
|
|
||||||
false => manga.Value.mangaConnector.GetChapters(manga.Value).Max().chapterNumber
|
|
||||||
};
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, latest);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> PostV2MangaInternalIdIgnoreChaptersBelow(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if(groups.Count < 1 ||
|
|
||||||
!_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) ||
|
|
||||||
manga is null)
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'");
|
|
||||||
if (requestParameters.TryGetValue("startChapter", out string? startChapterStr) &&
|
|
||||||
float.TryParse(startChapterStr, out float startChapter))
|
|
||||||
{
|
|
||||||
Manga manga1 = manga.Value;
|
|
||||||
manga1.ignoreChaptersBelow = startChapter;
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
|
|
||||||
}else
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.InternalServerError, "Parameter 'startChapter' missing, or failed to parse.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> PostV2MangaInternalIdMoveFolder(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
|
|
||||||
if(groups.Count < 1 ||
|
|
||||||
!_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) ||
|
|
||||||
manga is null)
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'");
|
|
||||||
if(!requestParameters.TryGetValue("location", out string? newFolder))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.BadRequest, "Parameter 'location' missing.");
|
|
||||||
manga.Value.MovePublicationFolder(TrangaSettings.downloadLocation, newFolder);
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace Tranga.Server;
|
|
||||||
|
|
||||||
public partial class Server
|
|
||||||
{
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2LogFile(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if (logger is null || !File.Exists(logger?.logFilePath))
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, "Missing Logfile");
|
|
||||||
}
|
|
||||||
|
|
||||||
FileStream logFile = new (logger.logFilePath, FileMode.Open, FileAccess.Read);
|
|
||||||
FileStream content = new(Path.GetTempFileName(), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite, 0, FileOptions.DeleteOnClose);
|
|
||||||
logFile.Position = 0;
|
|
||||||
logFile.CopyTo(content);
|
|
||||||
content.Position = 0;
|
|
||||||
logFile.Dispose();
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, content);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2Ping(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.Accepted, "Pong!");
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> PostV2Ping(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.Accepted, "Pong!");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,136 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Tranga.NotificationConnectors;
|
|
||||||
|
|
||||||
namespace Tranga.Server;
|
|
||||||
|
|
||||||
public partial class Server
|
|
||||||
{
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2NotificationConnector(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, notificationConnectors);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2NotificationConnectorTypes(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK,
|
|
||||||
Enum.GetValues<NotificationConnectors.NotificationConnector.NotificationConnectorType>().ToDictionary(b => (byte)b, b => Enum.GetName(b)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2NotificationConnectorType(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if (groups.Count < 1 ||
|
|
||||||
!Enum.TryParse(groups[1].Value, true, out NotificationConnector.NotificationConnectorType notificationConnectorType))
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"NotificationType {groups[1].Value} does not exist.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(notificationConnectors.All(nc => nc.notificationConnectorType != notificationConnectorType))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"NotificationType {Enum.GetName(notificationConnectorType)} not configured.");
|
|
||||||
else
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, notificationConnectors.First(nc => nc.notificationConnectorType != notificationConnectorType));
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> PostV2NotificationConnectorType(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if (groups.Count < 1 ||
|
|
||||||
!Enum.TryParse(groups[1].Value, true, out NotificationConnector.NotificationConnectorType notificationConnectorType))
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"NotificationType {groups[1].Value} does not exist.");
|
|
||||||
}
|
|
||||||
|
|
||||||
string? url;
|
|
||||||
switch (notificationConnectorType)
|
|
||||||
{
|
|
||||||
case NotificationConnector.NotificationConnectorType.Gotify:
|
|
||||||
if(!requestParameters.TryGetValue("url", out url))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'url' missing.");
|
|
||||||
if(!requestParameters.TryGetValue("appToken", out string? appToken))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'appToken' missing.");
|
|
||||||
Gotify gotify = new (this, url, appToken);
|
|
||||||
this.notificationConnectors.RemoveWhere(nc =>
|
|
||||||
nc.notificationConnectorType == NotificationConnector.NotificationConnectorType.Gotify);
|
|
||||||
this.notificationConnectors.Add(gotify);
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, gotify);
|
|
||||||
case NotificationConnector.NotificationConnectorType.LunaSea:
|
|
||||||
if(!requestParameters.TryGetValue("webhook", out string? webhook))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'webhook' missing.");
|
|
||||||
LunaSea lunaSea = new (this, webhook);
|
|
||||||
this.notificationConnectors.RemoveWhere(nc =>
|
|
||||||
nc.notificationConnectorType == NotificationConnector.NotificationConnectorType.LunaSea);
|
|
||||||
this.notificationConnectors.Add(lunaSea);
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, lunaSea);
|
|
||||||
case NotificationConnector.NotificationConnectorType.Ntfy:
|
|
||||||
if(!requestParameters.TryGetValue("url", out url))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'url' missing.");
|
|
||||||
if(!requestParameters.TryGetValue("username", out string? username))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'username' missing.");
|
|
||||||
if(!requestParameters.TryGetValue("password", out string? password))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'password' missing.");
|
|
||||||
Ntfy ntfy = new(this, url, username, password, null);
|
|
||||||
this.notificationConnectors.RemoveWhere(nc =>
|
|
||||||
nc.notificationConnectorType == NotificationConnector.NotificationConnectorType.Ntfy);
|
|
||||||
this.notificationConnectors.Add(ntfy);
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, ntfy);
|
|
||||||
default:
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.MethodNotAllowed, $"NotificationType {Enum.GetName(notificationConnectorType)} is not supported.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> PostV2NotificationConnectorTypeTest(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if (groups.Count < 1 ||
|
|
||||||
!Enum.TryParse(groups[1].Value, true, out NotificationConnector.NotificationConnectorType notificationConnectorType))
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"NotificationType {groups[1].Value} does not exist.");
|
|
||||||
}
|
|
||||||
|
|
||||||
string? url;
|
|
||||||
switch (notificationConnectorType)
|
|
||||||
{
|
|
||||||
case NotificationConnector.NotificationConnectorType.Gotify:
|
|
||||||
if(!requestParameters.TryGetValue("url", out url))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'url' missing.");
|
|
||||||
if(!requestParameters.TryGetValue("appToken", out string? appToken))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'appToken' missing.");
|
|
||||||
Gotify gotify = new (this, url, appToken);
|
|
||||||
gotify.SendNotification("Tranga Test", "It was successful :3");
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, gotify);
|
|
||||||
case NotificationConnector.NotificationConnectorType.LunaSea:
|
|
||||||
if(!requestParameters.TryGetValue("webhook", out string? webhook))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'webhook' missing.");
|
|
||||||
LunaSea lunaSea = new (this, webhook);
|
|
||||||
lunaSea.SendNotification("Tranga Test", "It was successful :3");
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, lunaSea);
|
|
||||||
case NotificationConnector.NotificationConnectorType.Ntfy:
|
|
||||||
if(!requestParameters.TryGetValue("url", out url))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'url' missing.");
|
|
||||||
if(!requestParameters.TryGetValue("username", out string? username))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'username' missing.");
|
|
||||||
if(!requestParameters.TryGetValue("password", out string? password))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'password' missing.");
|
|
||||||
Ntfy ntfy = new(this, url, username, password, null);
|
|
||||||
ntfy.SendNotification("Tranga Test", "It was successful :3");
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, ntfy);
|
|
||||||
default:
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.MethodNotAllowed, $"NotificationType {Enum.GetName(notificationConnectorType)} is not supported.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> DeleteV2NotificationConnectorType(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if (groups.Count < 1 ||
|
|
||||||
!Enum.TryParse(groups[1].Value, true, out NotificationConnector.NotificationConnectorType notificationConnectorType))
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"NotificationType {groups[1].Value} does not exist.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(notificationConnectors.All(nc => nc.notificationConnectorType != notificationConnectorType))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"NotificationType {Enum.GetName(notificationConnectorType)} not configured.");
|
|
||||||
else
|
|
||||||
{
|
|
||||||
notificationConnectors.Remove(notificationConnectors.First(nc => nc.notificationConnectorType != notificationConnectorType));
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,137 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Tranga.MangaConnectors;
|
|
||||||
|
|
||||||
namespace Tranga.Server;
|
|
||||||
|
|
||||||
public partial class Server
|
|
||||||
{
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2Settings(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, TrangaSettings.AsJObject());
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2SettingsUserAgent(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, TrangaSettings.userAgent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> PostV2SettingsUserAgent(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if (!requestParameters.TryGetValue("value", out string? userAgent))
|
|
||||||
{
|
|
||||||
TrangaSettings.UpdateUserAgent(null);
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.Accepted, null);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
TrangaSettings.UpdateUserAgent(userAgent);
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2SettingsRateLimitTypes(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, Enum.GetValues<RequestType>().ToDictionary(b =>(byte)b, b => Enum.GetName(b)) );
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2SettingsRateLimit(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, TrangaSettings.requestLimits);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> PostV2SettingsRateLimit(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
foreach (KeyValuePair<string, string> kv in requestParameters)
|
|
||||||
{
|
|
||||||
if(!Enum.TryParse(kv.Key, out RequestType requestType) ||
|
|
||||||
!int.TryParse(kv.Value, out int requestsPerMinute))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.InternalServerError, null);
|
|
||||||
TrangaSettings.UpdateRateLimit(requestType, requestsPerMinute);
|
|
||||||
}
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, TrangaSettings.requestLimits);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2SettingsRateLimitType(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if(groups.Count < 1 ||
|
|
||||||
!Enum.TryParse(groups[1].Value, out RequestType requestType))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"RequestType {groups[1].Value}");
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, TrangaSettings.requestLimits[requestType]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> PostV2SettingsRateLimitType(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if(groups.Count < 1 ||
|
|
||||||
!Enum.TryParse(groups[1].Value, out RequestType requestType))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"RequestType {groups[1].Value}");
|
|
||||||
if (!requestParameters.TryGetValue("value", out string? requestsPerMinuteStr) ||
|
|
||||||
!int.TryParse(requestsPerMinuteStr, out int requestsPerMinute))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.InternalServerError, "Errors parsing requestsPerMinute");
|
|
||||||
TrangaSettings.UpdateRateLimit(requestType, requestsPerMinute);
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2SettingsAprilFoolsMode(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, TrangaSettings.aprilFoolsMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> PostV2SettingsAprilFoolsMode(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if (!requestParameters.TryGetValue("value", out string? trueFalseStr) ||
|
|
||||||
!bool.TryParse(trueFalseStr, out bool trueFalse))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.InternalServerError, "Errors parsing 'value'");
|
|
||||||
TrangaSettings.UpdateAprilFoolsMode(trueFalse);
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2SettingsCompressImages(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, TrangaSettings.compression);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> PostV2SettingsCompressImages(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if (!requestParameters.TryGetValue("value", out string? valueStr) ||
|
|
||||||
!int.TryParse(valueStr, out int value)
|
|
||||||
|| value != int.Clamp(value, 1, 100))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.InternalServerError, "Errors parsing 'value'");
|
|
||||||
TrangaSettings.UpdateCompressImages(value);
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> GetV2SettingsBwImages(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, TrangaSettings.bwImages);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> PostV2SettingsBwImages(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if (!requestParameters.TryGetValue("value", out string? trueFalseStr) ||
|
|
||||||
!bool.TryParse(trueFalseStr, out bool trueFalse))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.InternalServerError, "Errors parsing 'value'");
|
|
||||||
TrangaSettings.UpdateBwImages(trueFalse);
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTuple<HttpStatusCode, object?> PostV2SettingsDownloadLocation(GroupCollection groups, Dictionary<string, string> requestParameters)
|
|
||||||
{
|
|
||||||
if (!requestParameters.TryGetValue("location", out string? folderPath))
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, "Missing Parameter 'location'");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
bool moveFiles = requestParameters.TryGetValue("moveFiles", out string? moveFilesStr) switch
|
|
||||||
{
|
|
||||||
false => true,
|
|
||||||
true => bool.Parse(moveFilesStr!)
|
|
||||||
};
|
|
||||||
TrangaSettings.UpdateDownloadLocation(folderPath, moveFiles);
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
|
|
||||||
}
|
|
||||||
catch (FormatException)
|
|
||||||
{
|
|
||||||
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.InternalServerError, "Error Parsing Parameter 'moveFiles'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
using Logging;
|
|
||||||
using Tranga.Jobs;
|
|
||||||
using Tranga.MangaConnectors;
|
|
||||||
|
|
||||||
namespace Tranga;
|
|
||||||
|
|
||||||
public partial class Tranga : GlobalBase
|
|
||||||
{
|
|
||||||
public bool keepRunning;
|
|
||||||
public JobBoss jobBoss;
|
|
||||||
private Server.Server _server;
|
|
||||||
|
|
||||||
public Tranga(Logger? logger) : base(logger)
|
|
||||||
{
|
|
||||||
Log("\n\n _______ \n|_ _|.----..---.-..-----..-----..---.-.\n | | | _|| _ || || _ || _ |\n |___| |__| |___._||__|__||___ ||___._|\n |_____| \n\n");
|
|
||||||
keepRunning = true;
|
|
||||||
foreach(DirectoryInfo dir in new DirectoryInfo(Path.GetTempPath()).GetDirectories("trangatemp"))//Cleanup old temp folders
|
|
||||||
dir.Delete();
|
|
||||||
jobBoss = new(this, this._connectors);
|
|
||||||
StartJobBoss();
|
|
||||||
this._server = new Server.Server(this);
|
|
||||||
string[] emojis = { "(•‿•)", "(づ \u25d5‿\u25d5 )づ", "( \u02d8\u25bd\u02d8)っ\u2668", "=\uff3e\u25cf \u22cf \u25cf\uff3e=", "(ΦωΦ)", "(\u272a\u3268\u272a)", "( ノ・o・ )ノ", "(〜^\u2207^ )〜", "~(\u2267ω\u2266)~","૮ \u00b4• ﻌ \u00b4• ა", "(\u02c3ᆺ\u02c2)", "(=\ud83d\udf66 \u0f1d \ud83d\udf66=)"};
|
|
||||||
SendNotifications("Tranga Started", emojis[Random.Shared.Next(0,emojis.Length-1)]);
|
|
||||||
Log(TrangaSettings.AsJObject().ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public MangaConnector? GetConnector(string name)
|
|
||||||
{
|
|
||||||
foreach(MangaConnector mc in _connectors)
|
|
||||||
if (mc.name.Equals(name, StringComparison.InvariantCultureIgnoreCase))
|
|
||||||
return mc;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryGetConnector(string name, out MangaConnector? connector)
|
|
||||||
{
|
|
||||||
connector = GetConnector(name);
|
|
||||||
return connector is not null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<MangaConnector> GetConnectors()
|
|
||||||
{
|
|
||||||
return _connectors.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Manga? GetPublicationById(string internalId) => GetCachedManga(internalId);
|
|
||||||
|
|
||||||
public bool TryGetPublicationById(string internalId, out Manga? manga)
|
|
||||||
{
|
|
||||||
manga = GetPublicationById(internalId);
|
|
||||||
return manga is not null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StartJobBoss()
|
|
||||||
{
|
|
||||||
Thread t = new (() =>
|
|
||||||
{
|
|
||||||
while (keepRunning)
|
|
||||||
{
|
|
||||||
if(!TrangaSettings.aprilFoolsMode || !IsAprilFirst())
|
|
||||||
jobBoss.CheckJobs();
|
|
||||||
else
|
|
||||||
Log("April Fools Mode in Effect");
|
|
||||||
Thread.Sleep(100);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
t.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsAprilFirst()
|
|
||||||
{
|
|
||||||
//UTC 01 Apr +-12hrs
|
|
||||||
DateTime start = new DateTime(DateTime.Now.Year, 03, 31, 12, 0, 0, DateTimeKind.Utc);
|
|
||||||
DateTime end = new DateTime(DateTime.Now.Year, 04, 02, 12, 0, 0, DateTimeKind.Utc);
|
|
||||||
if (DateTime.UtcNow > start && DateTime.UtcNow < end)
|
|
||||||
return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<LangVersion>12</LangVersion>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.71" />
|
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
|
|
||||||
<PackageReference Include="PuppeteerSharp" Version="20.0.5" />
|
|
||||||
<PackageReference Include="Soenneker.Utils.String.NeedlemanWunsch" Version="2.1.301" />
|
|
||||||
<PackageReference Include="System.Drawing.Common" Version="9.0.0-preview.7.24405.4" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Logging\Logging.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Content Include="..\.dockerignore">
|
|
||||||
<Link>.dockerignore</Link>
|
|
||||||
<DependentUpon>Dockerfile</DependentUpon>
|
|
||||||
</Content>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
@ -1,51 +0,0 @@
|
|||||||
using Logging;
|
|
||||||
using GlaxArguments;
|
|
||||||
|
|
||||||
namespace Tranga;
|
|
||||||
|
|
||||||
public partial class Tranga : GlobalBase
|
|
||||||
{
|
|
||||||
|
|
||||||
public static void Main(string[] args)
|
|
||||||
{
|
|
||||||
Argument downloadLocation = new (new[] { "-d", "--downloadLocation" }, 1, "Directory to which downloaded Manga are saved");
|
|
||||||
Argument workingDirectory = new (new[] { "-w", "--workingDirectory" }, 1, "Directory in which application-data is saved");
|
|
||||||
Argument consoleLogger = new (new []{"-c", "--consoleLogger"}, 0, "Enables the consoleLogger");
|
|
||||||
Argument fileLogger = new (new []{"-f", "--fileLogger"}, 0, "Enables the fileLogger");
|
|
||||||
Argument fPath = new (new []{"-l", "--fPath"}, 1, "Log Folder Path");
|
|
||||||
|
|
||||||
Argument[] arguments = new[]
|
|
||||||
{
|
|
||||||
downloadLocation,
|
|
||||||
workingDirectory,
|
|
||||||
consoleLogger,
|
|
||||||
fileLogger,
|
|
||||||
fPath
|
|
||||||
};
|
|
||||||
ArgumentFetcher fetcher = new (arguments);
|
|
||||||
Dictionary<Argument, string[]> fetched = fetcher.Fetch(args);
|
|
||||||
|
|
||||||
string? directoryPath = fetched.TryGetValue(fPath, out string[]? path) ? path[0] : null;
|
|
||||||
if (directoryPath is not null && !Directory.Exists(directoryPath))
|
|
||||||
Directory.CreateDirectory(directoryPath);
|
|
||||||
|
|
||||||
List<Logger.LoggerType> enabledLoggers = new();
|
|
||||||
if(fetched.ContainsKey(consoleLogger))
|
|
||||||
enabledLoggers.Add(Logger.LoggerType.ConsoleLogger);
|
|
||||||
if (fetched.ContainsKey(fileLogger))
|
|
||||||
enabledLoggers.Add(Logger.LoggerType.FileLogger);
|
|
||||||
Logger logger = new(enabledLoggers.ToArray(), Console.Out, Console.OutputEncoding, directoryPath);
|
|
||||||
|
|
||||||
bool dlp = fetched.TryGetValue(downloadLocation, out string[]? downloadLocationPath);
|
|
||||||
bool wdp = fetched.TryGetValue(workingDirectory, out string[]? workingDirectoryPath);
|
|
||||||
|
|
||||||
if (wdp)
|
|
||||||
TrangaSettings.LoadFromWorkingDirectory(workingDirectoryPath![0]);
|
|
||||||
else
|
|
||||||
TrangaSettings.CreateOrUpdate();
|
|
||||||
if(dlp)
|
|
||||||
TrangaSettings.CreateOrUpdate(downloadDirectory: downloadLocationPath![0]);
|
|
||||||
|
|
||||||
Tranga _ = new (logger);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,242 +0,0 @@
|
|||||||
using System.Runtime.InteropServices;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using Tranga.LibraryConnectors;
|
|
||||||
using Tranga.MangaConnectors;
|
|
||||||
using Tranga.NotificationConnectors;
|
|
||||||
using static System.IO.UnixFileMode;
|
|
||||||
|
|
||||||
namespace Tranga;
|
|
||||||
|
|
||||||
public static class TrangaSettings
|
|
||||||
{
|
|
||||||
[JsonIgnore] internal static readonly string DefaultUserAgent = $"Tranga ({Enum.GetName(Environment.OSVersion.Platform)}; {(Environment.Is64BitOperatingSystem ? "x64" : "")}) / 1.0";
|
|
||||||
public static string downloadLocation { get; private set; } = (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(Directory.GetCurrentDirectory(), "Downloads"));
|
|
||||||
public static string workingDirectory { get; private set; } = Path.Join(RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/usr/share" : Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "tranga-api");
|
|
||||||
public static int apiPortNumber { get; private set; } = 6531;
|
|
||||||
public static string userAgent { get; private set; } = DefaultUserAgent;
|
|
||||||
public static bool bufferLibraryUpdates { get; private set; } = false;
|
|
||||||
public static bool bufferNotifications { get; private set; } = false;
|
|
||||||
public static int compression{ get; private set; } = 40;
|
|
||||||
public static bool bwImages { get; private set; } = false;
|
|
||||||
[JsonIgnore] public static string settingsFilePath => Path.Join(workingDirectory, "settings.json");
|
|
||||||
[JsonIgnore] public static string libraryConnectorsFilePath => Path.Join(workingDirectory, "libraryConnectors.json");
|
|
||||||
[JsonIgnore] public static string notificationConnectorsFilePath => Path.Join(workingDirectory, "notificationConnectors.json");
|
|
||||||
[JsonIgnore] public static string jobsFolderPath => Path.Join(workingDirectory, "jobs");
|
|
||||||
[JsonIgnore] public static string coverImageCache => Path.Join(workingDirectory, "imageCache");
|
|
||||||
[JsonIgnore] public static string mangaCacheFolderPath => Path.Join(workingDirectory, "mangaCache");
|
|
||||||
public static ushort? version { get; } = 2;
|
|
||||||
public static bool aprilFoolsMode { get; private set; } = true;
|
|
||||||
[JsonIgnore]internal static readonly Dictionary<RequestType, int> DefaultRequestLimits = new ()
|
|
||||||
{
|
|
||||||
{RequestType.MangaInfo, 250},
|
|
||||||
{RequestType.MangaDexFeed, 250},
|
|
||||||
{RequestType.MangaDexImage, 40},
|
|
||||||
{RequestType.MangaImage, 60},
|
|
||||||
{RequestType.MangaCover, 250},
|
|
||||||
{RequestType.Default, 60}
|
|
||||||
};
|
|
||||||
|
|
||||||
public static Dictionary<RequestType, int> requestLimits { get; set; } = DefaultRequestLimits;
|
|
||||||
|
|
||||||
public static void LoadFromWorkingDirectory(string directory)
|
|
||||||
{
|
|
||||||
TrangaSettings.workingDirectory = directory;
|
|
||||||
if(File.Exists(settingsFilePath))
|
|
||||||
Deserialize(File.ReadAllText(settingsFilePath));
|
|
||||||
else return;
|
|
||||||
|
|
||||||
Directory.CreateDirectory(downloadLocation);
|
|
||||||
Directory.CreateDirectory(workingDirectory);
|
|
||||||
ExportSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void CreateOrUpdate(string? downloadDirectory = null, string? pWorkingDirectory = null,
|
|
||||||
int? pApiPortNumber = null, string? pUserAgent = null, bool? pAprilFoolsMode = null,
|
|
||||||
bool? pBufferLibraryUpdates = null, bool? pBufferNotifications = null, int? pCompression = null, bool? pbwImages = null)
|
|
||||||
{
|
|
||||||
if(pWorkingDirectory is null && File.Exists(settingsFilePath))
|
|
||||||
LoadFromWorkingDirectory(workingDirectory);
|
|
||||||
downloadLocation = downloadDirectory ?? downloadLocation;
|
|
||||||
workingDirectory = pWorkingDirectory ?? workingDirectory;
|
|
||||||
apiPortNumber = pApiPortNumber ?? apiPortNumber;
|
|
||||||
userAgent = pUserAgent ?? userAgent;
|
|
||||||
aprilFoolsMode = pAprilFoolsMode ?? aprilFoolsMode;
|
|
||||||
bufferLibraryUpdates = pBufferLibraryUpdates ?? bufferLibraryUpdates;
|
|
||||||
bufferNotifications = pBufferNotifications ?? bufferNotifications;
|
|
||||||
compression = pCompression ?? compression;
|
|
||||||
bwImages = pbwImages ?? bwImages;
|
|
||||||
Directory.CreateDirectory(downloadLocation);
|
|
||||||
Directory.CreateDirectory(workingDirectory);
|
|
||||||
ExportSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static HashSet<LibraryConnector> LoadLibraryConnectors(GlobalBase clone)
|
|
||||||
{
|
|
||||||
if (!File.Exists(libraryConnectorsFilePath))
|
|
||||||
return new HashSet<LibraryConnector>();
|
|
||||||
return JsonConvert.DeserializeObject<HashSet<LibraryConnector>>(File.ReadAllText(libraryConnectorsFilePath),
|
|
||||||
new JsonSerializerSettings()
|
|
||||||
{
|
|
||||||
Converters =
|
|
||||||
{
|
|
||||||
new LibraryManagerJsonConverter(clone)
|
|
||||||
}
|
|
||||||
})!;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static HashSet<NotificationConnector> LoadNotificationConnectors(GlobalBase clone)
|
|
||||||
{
|
|
||||||
if (!File.Exists(notificationConnectorsFilePath))
|
|
||||||
return new HashSet<NotificationConnector>();
|
|
||||||
return JsonConvert.DeserializeObject<HashSet<NotificationConnector>>(File.ReadAllText(notificationConnectorsFilePath),
|
|
||||||
new JsonSerializerSettings()
|
|
||||||
{
|
|
||||||
Converters =
|
|
||||||
{
|
|
||||||
new NotificationManagerJsonConverter(clone)
|
|
||||||
}
|
|
||||||
})!;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void UpdateAprilFoolsMode(bool enabled)
|
|
||||||
{
|
|
||||||
aprilFoolsMode = enabled;
|
|
||||||
ExportSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void UpdateCompressImages(int value)
|
|
||||||
{
|
|
||||||
compression = int.Clamp(value, 1, 100);
|
|
||||||
ExportSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void UpdateBwImages(bool enabled)
|
|
||||||
{
|
|
||||||
bwImages = enabled;
|
|
||||||
ExportSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static 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)
|
|
||||||
MoveContentsOfDirectoryTo(TrangaSettings.downloadLocation, newPath);
|
|
||||||
|
|
||||||
TrangaSettings.downloadLocation = newPath;
|
|
||||||
ExportSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void UpdateWorkingDirectory(string newPath)
|
|
||||||
{
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
|
||||||
Directory.CreateDirectory(newPath, GroupRead | GroupWrite | None | OtherRead | OtherWrite | UserRead | UserWrite);
|
|
||||||
else
|
|
||||||
Directory.CreateDirectory(newPath);
|
|
||||||
|
|
||||||
MoveContentsOfDirectoryTo(TrangaSettings.workingDirectory, newPath);
|
|
||||||
|
|
||||||
TrangaSettings.workingDirectory = newPath;
|
|
||||||
ExportSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void MoveContentsOfDirectoryTo(string oldDir, string newDir)
|
|
||||||
{
|
|
||||||
string[] directoryPaths = Directory.GetDirectories(oldDir);
|
|
||||||
string[] filePaths = Directory.GetFiles(oldDir);
|
|
||||||
foreach (string file in filePaths)
|
|
||||||
{
|
|
||||||
string newPath = Path.Join(newDir, Path.GetFileName(file));
|
|
||||||
File.Move(file, newPath, true);
|
|
||||||
}
|
|
||||||
foreach(string directory in directoryPaths)
|
|
||||||
{
|
|
||||||
string? dirName = Path.GetDirectoryName(directory);
|
|
||||||
if(dirName is null)
|
|
||||||
continue;
|
|
||||||
string newPath = Path.Join(newDir, dirName);
|
|
||||||
if(Directory.Exists(newPath))
|
|
||||||
MoveContentsOfDirectoryTo(directory, newPath);
|
|
||||||
else
|
|
||||||
Directory.Move(directory, newPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void UpdateUserAgent(string? customUserAgent)
|
|
||||||
{
|
|
||||||
userAgent = customUserAgent ?? DefaultUserAgent;
|
|
||||||
ExportSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void UpdateRateLimit(RequestType requestType, int newLimit)
|
|
||||||
{
|
|
||||||
requestLimits[requestType] = newLimit;
|
|
||||||
ExportSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void ResetRateLimits()
|
|
||||||
{
|
|
||||||
requestLimits = DefaultRequestLimits;
|
|
||||||
ExportSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void ExportSettings()
|
|
||||||
{
|
|
||||||
if (File.Exists(settingsFilePath))
|
|
||||||
{
|
|
||||||
while(GlobalBase.IsFileInUse(settingsFilePath, null))
|
|
||||||
Thread.Sleep(100);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
Directory.CreateDirectory(new FileInfo(settingsFilePath).DirectoryName!);
|
|
||||||
File.WriteAllText(settingsFilePath, Serialize());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static JObject AsJObject()
|
|
||||||
{
|
|
||||||
JObject jobj = new JObject();
|
|
||||||
jobj.Add("downloadLocation", JToken.FromObject(downloadLocation));
|
|
||||||
jobj.Add("workingDirectory", JToken.FromObject(workingDirectory));
|
|
||||||
jobj.Add("apiPortNumber", JToken.FromObject(apiPortNumber));
|
|
||||||
jobj.Add("userAgent", JToken.FromObject(userAgent));
|
|
||||||
jobj.Add("aprilFoolsMode", JToken.FromObject(aprilFoolsMode));
|
|
||||||
jobj.Add("version", JToken.FromObject(version));
|
|
||||||
jobj.Add("requestLimits", JToken.FromObject(requestLimits));
|
|
||||||
jobj.Add("bufferLibraryUpdates", JToken.FromObject(bufferLibraryUpdates));
|
|
||||||
jobj.Add("bufferNotifications", JToken.FromObject(bufferNotifications));
|
|
||||||
jobj.Add("compression", JToken.FromObject(compression));
|
|
||||||
jobj.Add("bwImages", JToken.FromObject(bwImages));
|
|
||||||
return jobj;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string Serialize() => AsJObject().ToString();
|
|
||||||
|
|
||||||
public static void Deserialize(string serialized)
|
|
||||||
{
|
|
||||||
JObject jobj = JObject.Parse(serialized);
|
|
||||||
if (jobj.TryGetValue("downloadLocation", out JToken? dl))
|
|
||||||
downloadLocation = dl.Value<string>()!;
|
|
||||||
if (jobj.TryGetValue("workingDirectory", out JToken? wd))
|
|
||||||
workingDirectory = wd.Value<string>()!;
|
|
||||||
if (jobj.TryGetValue("apiPortNumber", out JToken? apn))
|
|
||||||
apiPortNumber = apn.Value<int>();
|
|
||||||
if (jobj.TryGetValue("userAgent", out JToken? ua))
|
|
||||||
userAgent = ua.Value<string>()!;
|
|
||||||
if (jobj.TryGetValue("aprilFoolsMode", out JToken? afm))
|
|
||||||
aprilFoolsMode = afm.Value<bool>()!;
|
|
||||||
if (jobj.TryGetValue("requestLimits", out JToken? rl))
|
|
||||||
requestLimits = rl.ToObject<Dictionary<RequestType, int>>()!;
|
|
||||||
if (jobj.TryGetValue("bufferLibraryUpdates", out JToken? blu))
|
|
||||||
bufferLibraryUpdates = blu.Value<bool>()!;
|
|
||||||
if (jobj.TryGetValue("bufferNotifications", out JToken? bn))
|
|
||||||
bufferNotifications = bn.Value<bool>()!;
|
|
||||||
if (jobj.TryGetValue("compression", out JToken? ci))
|
|
||||||
compression = ci.Value<int>()!;
|
|
||||||
if (jobj.TryGetValue("bwImages", out JToken? bwi))
|
|
||||||
bwImages = bwi.Value<bool>()!;
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user