mirror of
https://github.com/C9Glax/tranga.git
synced 2025-06-14 15:27:53 +02:00
Compare commits
122 Commits
Author | SHA1 | Date | |
---|---|---|---|
32dc58715e | |||
add0583776 | |||
6fed0e5473 | |||
a0636ac7a2 | |||
7aeb78e2f6 | |||
5cf512f2b2 | |||
7d96b0901f | |||
68e80bc066 | |||
ad971fb065 | |||
86052472bc | |||
ec30bb40fa | |||
2fa96e9793 | |||
78e44b7704 | |||
8bf9df4419 | |||
4bd54f096d | |||
877daf0a1e | |||
6d0fcc13fb | |||
f0256494fd | |||
39fa905733 | |||
c557389967 | |||
201773af50 | |||
f85e02fb0a | |||
73d98b9c0f | |||
b0ee888c82 | |||
5c4431778e | |||
ccfa213b77 | |||
22d6389d38 | |||
f53dfb0822 | |||
a966bd788d | |||
dd651adc15 | |||
ba5ae67aa7 | |||
da4a5bed09 | |||
947b521163 | |||
5674adbd5e | |||
290819de09 | |||
0d0b68a8f9 | |||
87d2357b41 | |||
e3186aebb0 | |||
1cd37e2b1b | |||
9c267f395f | |||
e2b8888130 | |||
b6ac2682f6 | |||
eddf50483f | |||
a71d65e666 | |||
9a640aed27 | |||
30b6c4680b | |||
7b6253de0f | |||
5aa3214ce5 | |||
9b70994f71 | |||
93cf341f2d | |||
01cb74c088 | |||
ec480dffad | |||
b7014cbff5 | |||
0cab921402 | |||
0e0ba1796e | |||
27d8565dc1 | |||
79dc44d707 | |||
bb6a0ad0d4 | |||
43db463ba6 | |||
9eb8ddbc40 | |||
972cba69ec | |||
962fe9529e | |||
da1b0cb1cd | |||
7f88e57e47 | |||
8865bf284f | |||
5fc2de5fcb | |||
4bae223d95 | |||
0486168b43 | |||
b64ab5c6d4 | |||
578fa5e6be | |||
4d33e78123 | |||
52ac3e4e4e | |||
8b99a98e24 | |||
cf171d5c38 | |||
6d49b4b934 | |||
b55d2a2d06 | |||
737eebf599 | |||
aef01b684c | |||
53bff61174 | |||
431a602a40 | |||
9afb81cee2 | |||
ea69b355b5 | |||
84dbc36bbf | |||
455c87b2e1 | |||
df991e3da6 | |||
13c96fd09a | |||
6f1a6a43ee | |||
e2afc09c4a | |||
e9db7cfacc | |||
755167c39a | |||
1cff93fbac | |||
6c775d6e0c | |||
876b1ab78b | |||
a321ecb1bc | |||
674c8fc37b | |||
e24652b83e | |||
5dee13c402 | |||
942a552c8e | |||
b5bd5d6126 | |||
715cf1f4f3 | |||
168bf5a358 | |||
636d17d287 | |||
294b819ff0 | |||
d763610383 | |||
2910473fec | |||
ca2d13226f | |||
95c65c981e | |||
e72efa3731 | |||
597eedb6d4 | |||
8829132046 | |||
32467191f6 | |||
fe52d2c3b5 | |||
554f6b4acc | |||
9d0fc18051 | |||
e02b00e0ef | |||
06a8e4e895 | |||
a557f8cab5 | |||
e564be08f5 | |||
b8bf7bdf30 | |||
d6af014cb7 | |||
2dcaaf4d66 | |||
e3ec5420c0 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -16,4 +16,6 @@ riderModule.iml
|
|||||||
# Datasource local storage ignored files
|
# Datasource local storage ignored files
|
||||||
/dataSources/
|
/dataSources/
|
||||||
/dataSources.local.xml
|
/dataSources.local.xml
|
||||||
/.idea
|
/.idea
|
||||||
|
cover.jpg
|
||||||
|
cover.png
|
@ -6,14 +6,27 @@ namespace Logging;
|
|||||||
public class FileLogger : LoggerBase
|
public class FileLogger : LoggerBase
|
||||||
{
|
{
|
||||||
private string logFilePath { get; }
|
private string logFilePath { get; }
|
||||||
|
private const int MaxNumberOfLogFiles = 5;
|
||||||
|
|
||||||
public FileLogger(string logFilePath, TextWriter? stdOut, Encoding? encoding = null) : base (stdOut, encoding)
|
public FileLogger(string logFilePath, TextWriter? stdOut, Encoding? encoding = null) : base (stdOut, encoding)
|
||||||
{
|
{
|
||||||
this.logFilePath = logFilePath;
|
this.logFilePath = logFilePath;
|
||||||
|
|
||||||
|
//Remove oldest logfile if more than MaxNumberOfLogFiles
|
||||||
|
string parentFolderPath = Path.GetDirectoryName(logFilePath)!;
|
||||||
|
for (int fileCount = new DirectoryInfo(parentFolderPath).EnumerateFiles().Count(); fileCount > MaxNumberOfLogFiles - 1; fileCount--) //-1 because we create own logfile later
|
||||||
|
File.Delete(new DirectoryInfo(parentFolderPath).EnumerateFiles().MinBy(file => file.LastWriteTime)!.FullName);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Write(LogMessage logMessage)
|
protected override void Write(LogMessage logMessage)
|
||||||
{
|
{
|
||||||
File.AppendAllText(logFilePath, logMessage.ToString());
|
try
|
||||||
|
{
|
||||||
|
File.AppendAllText(logFilePath, logMessage.ToString());
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
stdOut?.WriteLine(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,13 +9,12 @@ public class Logger : TextWriter
|
|||||||
public enum LoggerType
|
public enum LoggerType
|
||||||
{
|
{
|
||||||
FileLogger,
|
FileLogger,
|
||||||
ConsoleLogger,
|
ConsoleLogger
|
||||||
MemoryLogger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private FileLogger? _fileLogger;
|
private FileLogger? _fileLogger;
|
||||||
private FormattedConsoleLogger? _formattedConsoleLogger;
|
private FormattedConsoleLogger? _formattedConsoleLogger;
|
||||||
private MemoryLogger? _memoryLogger;
|
private MemoryLogger _memoryLogger;
|
||||||
private TextWriter? stdOut;
|
private TextWriter? stdOut;
|
||||||
|
|
||||||
public Logger(LoggerType[] enabledLoggers, TextWriter? stdOut, Encoding? encoding, string? logFilePath)
|
public Logger(LoggerType[] enabledLoggers, TextWriter? stdOut, Encoding? encoding, string? logFilePath)
|
||||||
@ -30,7 +29,7 @@ public class Logger : TextWriter
|
|||||||
throw new ArgumentException($"logFilePath can not be null for LoggerType {LoggerType.FileLogger}");
|
throw new ArgumentException($"logFilePath can not be null for LoggerType {LoggerType.FileLogger}");
|
||||||
}
|
}
|
||||||
_formattedConsoleLogger = enabledLoggers.Contains(LoggerType.ConsoleLogger) ? new FormattedConsoleLogger(null, encoding) : null;
|
_formattedConsoleLogger = enabledLoggers.Contains(LoggerType.ConsoleLogger) ? new FormattedConsoleLogger(null, encoding) : null;
|
||||||
_memoryLogger = enabledLoggers.Contains(LoggerType.MemoryLogger) ? new MemoryLogger(null, encoding) : null;
|
_memoryLogger = new MemoryLogger(null, encoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void WriteLine(string caller, string? value)
|
public void WriteLine(string caller, string? value)
|
||||||
@ -46,9 +45,19 @@ public class Logger : TextWriter
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
_fileLogger?.Write(caller, value);
|
_fileLogger?.Write(caller, value);
|
||||||
_memoryLogger?.Write(caller, value);
|
|
||||||
_formattedConsoleLogger?.Write(caller, value);
|
_formattedConsoleLogger?.Write(caller, value);
|
||||||
|
|
||||||
|
_memoryLogger.Write(caller, value);
|
||||||
stdOut?.Write(value);
|
stdOut?.Write(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string[] Tail(uint? lines)
|
||||||
|
{
|
||||||
|
return _memoryLogger.Tail(lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] GetNewLines()
|
||||||
|
{
|
||||||
|
return _memoryLogger.GetNewLines();
|
||||||
|
}
|
||||||
}
|
}
|
@ -5,7 +5,7 @@ namespace Logging;
|
|||||||
public abstract class LoggerBase : TextWriter
|
public abstract class LoggerBase : TextWriter
|
||||||
{
|
{
|
||||||
public override Encoding Encoding { get; }
|
public override Encoding Encoding { get; }
|
||||||
private TextWriter? stdOut { get; }
|
protected TextWriter? stdOut { get; }
|
||||||
|
|
||||||
public LoggerBase(TextWriter? stdOut, Encoding? encoding = null)
|
public LoggerBase(TextWriter? stdOut, Encoding? encoding = null)
|
||||||
{
|
{
|
||||||
@ -52,7 +52,7 @@ public abstract class LoggerBase : TextWriter
|
|||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
string dateTimeString = $"{logTime.ToShortDateString()} {logTime.ToLongTimeString()}";
|
string dateTimeString = $"{logTime.ToShortDateString()} {logTime.ToLongTimeString()}";
|
||||||
return $"[{dateTimeString}] {caller,-30} | {value}";
|
return $"[{dateTimeString}] {caller.Split(new char[]{'.','+'}).Last(),15} | {value}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,7 +4,8 @@ namespace Logging;
|
|||||||
|
|
||||||
public class MemoryLogger : LoggerBase
|
public class MemoryLogger : LoggerBase
|
||||||
{
|
{
|
||||||
private SortedList<DateTime, LogMessage> logMessages = new();
|
private readonly SortedList<DateTime, LogMessage> _logMessages = new();
|
||||||
|
private int _lastLogMessageIndex = 0;
|
||||||
|
|
||||||
public MemoryLogger(TextWriter? stdOut, Encoding? encoding = null) : base(stdOut, encoding)
|
public MemoryLogger(TextWriter? stdOut, Encoding? encoding = null) : base(stdOut, encoding)
|
||||||
{
|
{
|
||||||
@ -13,30 +14,44 @@ public class MemoryLogger : LoggerBase
|
|||||||
|
|
||||||
protected override void Write(LogMessage value)
|
protected override void Write(LogMessage value)
|
||||||
{
|
{
|
||||||
logMessages.Add(value.logTime, value);
|
_logMessages.Add(value.logTime, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string[] GetLogMessage()
|
public string[] GetLogMessage()
|
||||||
{
|
{
|
||||||
string[] ret = new string[logMessages.Count];
|
return Tail(Convert.ToUInt32(_logMessages.Count));
|
||||||
for (int logMessageIndex = 0; logMessageIndex < ret.Length; logMessageIndex++)
|
}
|
||||||
|
|
||||||
|
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++)
|
||||||
{
|
{
|
||||||
DateTime logTime = logMessages.GetValueAtIndex(logMessageIndex).logTime;
|
ret[retIndex] = _logMessages.GetValueAtIndex(_logMessages.Count - retLength + retIndex).ToString();
|
||||||
string dateTimeString = $"{logTime.ToShortDateString()} {logTime.ToShortTimeString()}";
|
|
||||||
string callerString = logMessages.GetValueAtIndex(logMessageIndex).caller.ToString();
|
|
||||||
string value = $"[{dateTimeString}] {callerString} | {logMessages.GetValueAtIndex(logMessageIndex).value}";
|
|
||||||
ret[logMessageIndex] = value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_lastLogMessageIndex = _logMessages.Count - 1;
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string[] Tail(uint length)
|
public string[] GetNewLines()
|
||||||
{
|
{
|
||||||
string[] ret = new string[length];
|
int logMessageCount = _logMessages.Count;
|
||||||
for (int logMessageIndex = logMessages.Count - 1; logMessageIndex > logMessageIndex - length; logMessageIndex--)
|
string[] ret = new string[logMessageCount - _lastLogMessageIndex];
|
||||||
ret[logMessageIndex] = logMessages.GetValueAtIndex(logMessageIndex).ToString();
|
|
||||||
|
|
||||||
return ret.Reverse().ToArray();
|
for (int retIndex = 0; retIndex < ret.Length; retIndex++)
|
||||||
|
{
|
||||||
|
ret[retIndex] = _logMessages.GetValueAtIndex(_lastLogMessageIndex + retIndex).ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastLogMessageIndex = logMessageCount;
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,82 +1,134 @@
|
|||||||
using System.Text.Json;
|
using Logging;
|
||||||
using Tranga;
|
using Tranga;
|
||||||
|
|
||||||
TaskManager taskManager = new (Directory.GetCurrentDirectory());
|
string applicationFolderPath =
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Tranga-API");
|
||||||
var app = builder.Build();
|
string downloadFolderPath = Path.Join(applicationFolderPath, "Manga");
|
||||||
|
string logsFolderPath = Path.Join(applicationFolderPath, "logs");
|
||||||
|
string logFilePath = Path.Join(logsFolderPath, $"log-{DateTime.Now:dd-M-yyyy-HH-mm-ss}.txt");
|
||||||
|
string settingsFilePath = Path.Join(applicationFolderPath, "settings.json");
|
||||||
|
|
||||||
app.MapGet("/GetConnectors", () => JsonSerializer.Serialize(taskManager.GetAvailableConnectors().Values.ToArray()));
|
Directory.CreateDirectory(applicationFolderPath);
|
||||||
|
Directory.CreateDirectory(logsFolderPath);
|
||||||
app.MapGet("/GetPublications", (string connectorName, string? publicationName) =>
|
|
||||||
{
|
|
||||||
Connector connector = taskManager.GetConnector(connectorName);
|
|
||||||
|
|
||||||
Publication[] publications;
|
|
||||||
if (publicationName is not null)
|
|
||||||
publications = connector.GetPublications(publicationName);
|
|
||||||
else
|
|
||||||
publications = connector.GetPublications();
|
|
||||||
|
|
||||||
return JsonSerializer.Serialize(publications);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.MapGet("/ListTasks", () => JsonSerializer.Serialize(taskManager.GetAllTasks()));
|
|
||||||
|
|
||||||
app.MapGet("/CreateTask",
|
|
||||||
(TrangaTask.Task task, string? connectorName, string? publicationName, TimeSpan reoccurrence, string? language) =>
|
|
||||||
{
|
|
||||||
switch (task)
|
|
||||||
{
|
|
||||||
case TrangaTask.Task.UpdateKomgaLibrary:
|
|
||||||
taskManager.AddTask(TrangaTask.Task.UpdateKomgaLibrary, null, null, reoccurrence);
|
|
||||||
break;
|
|
||||||
case TrangaTask.Task.DownloadNewChapters:
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Connector connector = taskManager.GetConnector(connectorName);
|
|
||||||
|
|
||||||
Publication[] publications;
|
|
||||||
if (publicationName is not null)
|
|
||||||
publications = connector.GetPublications(publicationName);
|
|
||||||
else
|
|
||||||
publications = connector.GetPublications();
|
|
||||||
|
|
||||||
Publication? publication = publications.FirstOrDefault(pub => pub.downloadUrl == publicationName);
|
|
||||||
if (publication is null)
|
|
||||||
JsonSerializer.Serialize($"Publication {publicationName} is unknown.");
|
|
||||||
taskManager.AddTask(TrangaTask.Task.DownloadNewChapters, connectorName, publication, reoccurrence, language ?? "");
|
|
||||||
return JsonSerializer.Serialize("Success");
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return JsonSerializer.Serialize(e.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
default: return JsonSerializer.Serialize("Not Implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
return JsonSerializer.Serialize("Not Implemented");
|
|
||||||
});
|
|
||||||
|
|
||||||
app.MapGet("/RemoveTask", (TrangaTask.Task task, string? connectorName, string? publicationName) =>
|
|
||||||
{
|
|
||||||
switch (task)
|
|
||||||
{
|
|
||||||
case TrangaTask.Task.UpdateKomgaLibrary:
|
|
||||||
taskManager.RemoveTask(TrangaTask.Task.UpdateKomgaLibrary, null, null);
|
|
||||||
return JsonSerializer.Serialize("Success");
|
|
||||||
break;
|
|
||||||
case TrangaTask.Task.DownloadNewChapters:
|
|
||||||
Publication? publication = taskManager.GetAllPublications().FirstOrDefault(pub => pub.downloadUrl == publicationName);
|
|
||||||
if (publication is null)
|
|
||||||
JsonSerializer.Serialize($"Publication {publicationName} is unknown.");
|
|
||||||
|
|
||||||
taskManager.RemoveTask(TrangaTask.Task.DownloadNewChapters, connectorName, publication);
|
|
||||||
|
|
||||||
return JsonSerializer.Serialize("Success");
|
|
||||||
|
|
||||||
default: return JsonSerializer.Serialize("Not Implemented");
|
Console.WriteLine($"Logfile-Path: {logFilePath}");
|
||||||
}
|
Console.WriteLine($"Settings-File-Path: {settingsFilePath}");
|
||||||
|
|
||||||
|
Logger logger = new(new[] { Logger.LoggerType.FileLogger }, null, null, logFilePath);
|
||||||
|
|
||||||
|
logger.WriteLine("Tranga_CLI", "Loading Taskmanager.");
|
||||||
|
TrangaSettings settings;
|
||||||
|
if (File.Exists(settingsFilePath))
|
||||||
|
settings = TrangaSettings.LoadSettings(settingsFilePath);
|
||||||
|
else
|
||||||
|
settings = new TrangaSettings(downloadFolderPath, applicationFolderPath, null);
|
||||||
|
|
||||||
|
TaskManager taskManager = new (settings, logger);
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGen();
|
||||||
|
builder.Services.AddControllers().AddNewtonsoftJson();
|
||||||
|
|
||||||
|
string corsHeader = "Tranga";
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy(name: corsHeader,
|
||||||
|
policy =>
|
||||||
|
{
|
||||||
|
policy.WithOrigins("http://localhost", "http://127.0.0.1", "http://localhost:63342");
|
||||||
|
policy.WithMethods("GET", "POST", "DELETE");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
|
||||||
|
app.UseCors(corsHeader);
|
||||||
|
|
||||||
|
app.MapGet("/Tranga/GetAvailableControllers", () => taskManager.GetAvailableConnectors().Keys.ToArray());
|
||||||
|
|
||||||
|
app.MapGet("/Tranga/GetKnownPublications", () => taskManager.GetAllPublications());
|
||||||
|
|
||||||
|
app.MapGet("/Tranga/GetPublicationsFromConnector", (string connectorName, string title) =>
|
||||||
|
{
|
||||||
|
Connector? connector = taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName).Value;
|
||||||
|
if (connector is null)
|
||||||
|
return Array.Empty<Publication>();
|
||||||
|
if(title.Length < 4)
|
||||||
|
return Array.Empty<Publication>();
|
||||||
|
return taskManager.GetPublicationsFromConnector(connector, title);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapGet("/Tasks/GetTaskTypes", () => Enum.GetNames(typeof(TrangaTask.Task)));
|
||||||
|
|
||||||
|
|
||||||
|
app.MapPost("/Tasks/Create", (string taskType, string? connectorName, string? publicationId, string reoccurrenceTime, string? language) =>
|
||||||
|
{
|
||||||
|
Publication? publication = taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == publicationId);
|
||||||
|
TrangaTask.Task task = Enum.Parse<TrangaTask.Task>(taskType);
|
||||||
|
taskManager.AddTask(task, connectorName, publication, TimeSpan.Parse(reoccurrenceTime), language??"");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapDelete("/Tasks/Delete", (string taskType, string? connectorName, string? publicationId) =>
|
||||||
|
{
|
||||||
|
Publication? publication = taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == publicationId);
|
||||||
|
TrangaTask.Task task = Enum.Parse<TrangaTask.Task>(taskType);
|
||||||
|
taskManager.DeleteTask(task, connectorName, publication);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapGet("/Tasks/Get", (string taskType, string? connectorName, string? searchString) =>
|
||||||
|
{
|
||||||
|
TrangaTask.Task task = Enum.Parse<TrangaTask.Task>(taskType);
|
||||||
|
if (searchString is null)
|
||||||
|
return taskManager.GetAllTasks().Where(tTask => tTask.task == task && tTask.connectorName == connectorName);
|
||||||
|
else
|
||||||
|
return taskManager.GetAllTasks().Where(tTask =>
|
||||||
|
tTask.task == task && tTask.connectorName == connectorName && tTask.ToString()
|
||||||
|
.Contains(searchString, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapPost("/Tasks/Start", (string taskType, string? connectorName, string? publicationId) =>
|
||||||
|
{
|
||||||
|
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType);
|
||||||
|
TrangaTask? task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
|
||||||
|
tTask.task == pTask && tTask.publication?.internalId == publicationId && tTask.connectorName == connectorName);
|
||||||
|
if (task is null)
|
||||||
|
return;
|
||||||
|
taskManager.ExecuteTaskNow(task);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapGet("/Tasks/GetRunningTasks",
|
||||||
|
() => taskManager.GetAllTasks().Where(task => task.state is TrangaTask.ExecutionState.Running));
|
||||||
|
|
||||||
|
app.MapGet("/Queue/GetList",
|
||||||
|
() => taskManager.GetAllTasks().Where(task => task.state is TrangaTask.ExecutionState.Enqueued));
|
||||||
|
|
||||||
|
app.MapPost("/Queue/Enqueue", (string taskType, string? connectorName, string? publicationId) =>
|
||||||
|
{
|
||||||
|
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType);
|
||||||
|
TrangaTask? task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
|
||||||
|
tTask.task == pTask && tTask.publication?.internalId == publicationId && tTask.connectorName == connectorName);
|
||||||
|
if (task is null)
|
||||||
|
return;
|
||||||
|
taskManager.AddTaskToQueue(task);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapDelete("/Queue/Dequeue", (string taskType, string? connectorName, string? publicationId) =>
|
||||||
|
{
|
||||||
|
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType);
|
||||||
|
TrangaTask? task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
|
||||||
|
tTask.task == pTask && tTask.publication?.internalId == publicationId && tTask.connectorName == connectorName);
|
||||||
|
if (task is null)
|
||||||
|
return;
|
||||||
|
taskManager.RemoveTaskFromQueue(task);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapGet("/Settings/Get", () => taskManager.settings);
|
||||||
|
|
||||||
|
app.MapPost("/Settings/Update", (string? downloadLocation, string? komgaUrl, string? komgaAuth) => taskManager.UpdateSettings(downloadLocation, komgaUrl, komgaAuth) );
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
@ -3,8 +3,8 @@
|
|||||||
"windowsAuthentication": false,
|
"windowsAuthentication": false,
|
||||||
"anonymousAuthentication": true,
|
"anonymousAuthentication": true,
|
||||||
"iisExpress": {
|
"iisExpress": {
|
||||||
"applicationUrl": "http://localhost:14826",
|
"applicationUrl": "http://localhost:1716",
|
||||||
"sslPort": 44333
|
"sslPort": 44391
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"profiles": {
|
"profiles": {
|
||||||
@ -12,7 +12,7 @@
|
|||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"applicationUrl": "http://localhost:5119",
|
"applicationUrl": "http://localhost:5177",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
@ -21,7 +21,7 @@
|
|||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"applicationUrl": "https://localhost:7070;http://localhost:5119",
|
"applicationUrl": "https://localhost:7036;http://localhost:5177",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,14 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Logging\Logging.csproj" />
|
||||||
<ProjectReference Include="..\Tranga\Tranga.csproj" />
|
<ProjectReference Include="..\Tranga\Tranga.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.5" />
|
||||||
|
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.6" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1 +0,0 @@
|
|||||||
[{"reoccurrence":"00:00:00","lastExecuted":"2023-05-19T17:34:40.5349215+02:00","connectorName":"MangaDex","task":0,"publication":{"sortName":null,"description":null,"tags":null,"posterUrl":null,"year":null,"originalLanguage":null,"status":null,"folderName":null,"downloadUrl":null},"language":"en"}]
|
|
@ -14,16 +14,25 @@ public static class Tranga_Cli
|
|||||||
{
|
{
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
Logger logger = new(new[] { Logger.LoggerType.FileLogger, Logger.LoggerType.MemoryLogger }, null, null,
|
string applicationFolderPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Tranga");
|
||||||
Path.Join(Directory.GetCurrentDirectory(), $"log-{DateTime.Now:dd-M-yyyy-HH-mm-ss}.txt"));
|
string logsFolderPath = Path.Join(applicationFolderPath, "logs");
|
||||||
|
string logFilePath = Path.Join(logsFolderPath, $"log-{DateTime.Now:dd-M-yyyy-HH-mm-ss}.txt");
|
||||||
|
string settingsFilePath = Path.Join(applicationFolderPath, "settings.json");
|
||||||
|
|
||||||
|
Directory.CreateDirectory(applicationFolderPath);
|
||||||
|
Directory.CreateDirectory(logsFolderPath);
|
||||||
|
|
||||||
logger.WriteLine("Tranga_CLI", "Loading Settings.");
|
Console.WriteLine($"Logfile-Path: {logFilePath}");
|
||||||
TaskManager.SettingsData settings;
|
Console.WriteLine($"Settings-File-Path: {settingsFilePath}");
|
||||||
string settingsPath = Path.Join(Directory.GetCurrentDirectory(), "data.json");
|
|
||||||
if (File.Exists(settingsPath))
|
Logger logger = new(new[] { Logger.LoggerType.FileLogger }, null, null, logFilePath);
|
||||||
settings = TaskManager.LoadData(Directory.GetCurrentDirectory());
|
|
||||||
|
logger.WriteLine("Tranga_CLI", "Loading Taskmanager.");
|
||||||
|
TrangaSettings settings;
|
||||||
|
if (File.Exists(settingsFilePath))
|
||||||
|
settings = TrangaSettings.LoadSettings(settingsFilePath);
|
||||||
else
|
else
|
||||||
settings = new TaskManager.SettingsData(Directory.GetCurrentDirectory(), null, new HashSet<TrangaTask>());
|
settings = new TrangaSettings(Directory.GetCurrentDirectory(), applicationFolderPath, null);
|
||||||
|
|
||||||
|
|
||||||
logger.WriteLine("Tranga_CLI", "User Input");
|
logger.WriteLine("Tranga_CLI", "User Input");
|
||||||
@ -31,7 +40,7 @@ public static class Tranga_Cli
|
|||||||
string? tmpPath = Console.ReadLine();
|
string? tmpPath = Console.ReadLine();
|
||||||
while(tmpPath is null)
|
while(tmpPath is null)
|
||||||
tmpPath = Console.ReadLine();
|
tmpPath = Console.ReadLine();
|
||||||
if(tmpPath.Length > 0)
|
if (tmpPath.Length > 0)
|
||||||
settings.downloadLocation = tmpPath;
|
settings.downloadLocation = tmpPath;
|
||||||
|
|
||||||
Console.WriteLine($"Komga BaseURL [{settings.komga?.baseUrl}]:");
|
Console.WriteLine($"Komga BaseURL [{settings.komga?.baseUrl}]:");
|
||||||
@ -64,7 +73,7 @@ public static class Tranga_Cli
|
|||||||
tmpPass += keyInfo.KeyChar;
|
tmpPass += keyInfo.KeyChar;
|
||||||
}
|
}
|
||||||
} while (key != ConsoleKey.Enter);
|
} while (key != ConsoleKey.Enter);
|
||||||
|
|
||||||
settings.komga = new Komga(tmpUrl, tmpUser, tmpPass, logger);
|
settings.komga = new Komga(tmpUrl, tmpUser, tmpPass, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,49 +81,92 @@ public static class Tranga_Cli
|
|||||||
TaskMode(settings, logger);
|
TaskMode(settings, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TaskMode(TaskManager.SettingsData settings, Logger logger)
|
private static void TaskMode(TrangaSettings settings, Logger logger)
|
||||||
{
|
{
|
||||||
TaskManager taskManager = new (settings, logger);
|
TaskManager taskManager = new (settings, logger);
|
||||||
ConsoleKey selection = PrintMenu(taskManager, settings.downloadLocation, logger);
|
ConsoleKey selection = ConsoleKey.EraseEndOfFile;
|
||||||
|
PrintMenu(taskManager, taskManager.settings.downloadLocation, logger);
|
||||||
while (selection != ConsoleKey.Q)
|
while (selection != ConsoleKey.Q)
|
||||||
{
|
{
|
||||||
switch (selection)
|
int taskCount = taskManager.GetAllTasks().Length;
|
||||||
|
int taskRunningCount = taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Running);
|
||||||
|
int taskEnqueuedCount =
|
||||||
|
taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Enqueued);
|
||||||
|
Console.SetCursorPosition(0,1);
|
||||||
|
Console.WriteLine($"Tasks (Running/Queue/Total)): {taskRunningCount}/{taskEnqueuedCount}/{taskCount}");
|
||||||
|
|
||||||
|
if (Console.KeyAvailable)
|
||||||
{
|
{
|
||||||
case ConsoleKey.L:
|
selection = Console.ReadKey().Key;
|
||||||
PrintTasks(taskManager.GetAllTasks(), logger);
|
switch (selection)
|
||||||
Console.WriteLine("Press any key.");
|
{
|
||||||
Console.ReadKey();
|
case ConsoleKey.L:
|
||||||
break;
|
PrintTasks(taskManager.GetAllTasks(), logger);
|
||||||
case ConsoleKey.C:
|
Console.WriteLine("Press any key.");
|
||||||
CreateTask(taskManager, settings, logger);
|
Console.ReadKey();
|
||||||
Console.WriteLine("Press any key.");
|
break;
|
||||||
Console.ReadKey();
|
case ConsoleKey.C:
|
||||||
break;
|
CreateTask(taskManager, taskManager.settings, logger);
|
||||||
case ConsoleKey.D:
|
Console.WriteLine("Press any key.");
|
||||||
RemoveTask (taskManager, logger);
|
Console.ReadKey();
|
||||||
Console.WriteLine("Press any key.");
|
break;
|
||||||
Console.ReadKey();
|
case ConsoleKey.D:
|
||||||
break;
|
DeleteTask(taskManager, logger);
|
||||||
case ConsoleKey.E:
|
Console.WriteLine("Press any key.");
|
||||||
ExecuteTaskNow(taskManager, logger);
|
Console.ReadKey();
|
||||||
Console.WriteLine("Press any key.");
|
break;
|
||||||
Console.ReadKey();
|
case ConsoleKey.E:
|
||||||
break;
|
ExecuteTaskNow(taskManager, logger);
|
||||||
case ConsoleKey.S:
|
Console.WriteLine("Press any key.");
|
||||||
SearchTasks(taskManager, logger);
|
Console.ReadKey();
|
||||||
Console.WriteLine("Press any key.");
|
break;
|
||||||
Console.ReadKey();
|
case ConsoleKey.S:
|
||||||
break;
|
SearchTasks(taskManager, logger);
|
||||||
case ConsoleKey.R:
|
Console.WriteLine("Press any key.");
|
||||||
PrintTasks(taskManager.GetAllTasks().Where(eTask => eTask.state == TrangaTask.ExecutionState.Running).ToArray(), logger);
|
Console.ReadKey();
|
||||||
Console.WriteLine("Press any key.");
|
break;
|
||||||
Console.ReadKey();
|
case ConsoleKey.R:
|
||||||
break;
|
PrintTasks(
|
||||||
|
taskManager.GetAllTasks().Where(eTask => eTask.state == TrangaTask.ExecutionState.Running)
|
||||||
|
.ToArray(), logger);
|
||||||
|
Console.WriteLine("Press any key.");
|
||||||
|
Console.ReadKey();
|
||||||
|
break;
|
||||||
|
case ConsoleKey.K:
|
||||||
|
PrintTasks(
|
||||||
|
taskManager.GetAllTasks().Where(qTask => qTask.state is TrangaTask.ExecutionState.Enqueued)
|
||||||
|
.ToArray(), logger);
|
||||||
|
Console.WriteLine("Press any key.");
|
||||||
|
Console.ReadKey();
|
||||||
|
break;
|
||||||
|
case ConsoleKey.F:
|
||||||
|
TailLog(logger);
|
||||||
|
Console.ReadKey();
|
||||||
|
break;
|
||||||
|
case ConsoleKey.G:
|
||||||
|
RemoveTaskFromQueue(taskManager, logger);
|
||||||
|
Console.WriteLine("Press any key.");
|
||||||
|
Console.ReadKey();
|
||||||
|
break;
|
||||||
|
case ConsoleKey.B:
|
||||||
|
AddTaskToQueue(taskManager, logger);
|
||||||
|
Console.WriteLine("Press any key.");
|
||||||
|
Console.ReadKey();
|
||||||
|
break;
|
||||||
|
case ConsoleKey.M:
|
||||||
|
AddMangaTaskToQueue(taskManager, logger);
|
||||||
|
Console.WriteLine("Press any key.");
|
||||||
|
Console.ReadKey();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
PrintMenu(taskManager, taskManager.settings.downloadLocation, logger);
|
||||||
}
|
}
|
||||||
selection = PrintMenu(taskManager, settings.downloadLocation, logger);
|
Thread.Sleep(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.WriteLine("Tranga_CLI", "Exiting.");
|
logger.WriteLine("Tranga_CLI", "Exiting.");
|
||||||
|
Console.Clear();
|
||||||
|
Console.WriteLine("Exiting.");
|
||||||
if (taskManager.GetAllTasks().Any(task => task.state == TrangaTask.ExecutionState.Running))
|
if (taskManager.GetAllTasks().Any(task => task.state == TrangaTask.ExecutionState.Running))
|
||||||
{
|
{
|
||||||
Console.WriteLine("Force quit (Even with running tasks?) y/N");
|
Console.WriteLine("Force quit (Even with running tasks?) y/N");
|
||||||
@ -127,7 +179,7 @@ public static class Tranga_Cli
|
|||||||
taskManager.Shutdown(false);
|
taskManager.Shutdown(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ConsoleKey PrintMenu(TaskManager taskManager, string folderPath, Logger logger)
|
private static void PrintMenu(TaskManager taskManager, string folderPath, Logger logger)
|
||||||
{
|
{
|
||||||
int taskCount = taskManager.GetAllTasks().Length;
|
int taskCount = taskManager.GetAllTasks().Length;
|
||||||
int taskRunningCount = taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Running);
|
int taskRunningCount = taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Running);
|
||||||
@ -135,16 +187,13 @@ public static class Tranga_Cli
|
|||||||
taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Enqueued);
|
taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Enqueued);
|
||||||
Console.Clear();
|
Console.Clear();
|
||||||
Console.WriteLine($"Download Folder: {folderPath}");
|
Console.WriteLine($"Download Folder: {folderPath}");
|
||||||
Console.WriteLine($"Tasks (Running/Queue/Total));: {taskRunningCount}/{taskEnqueuedCount}/{taskCount}");
|
Console.WriteLine($"Tasks (Running/Queue/Total)): {taskRunningCount}/{taskEnqueuedCount}/{taskCount}");
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine($"{"C: Create Task",-30}{"L: List tasks",-30}");
|
Console.WriteLine($"{"C: Create Task",-30}{"L: List tasks",-30}{"B: Enqueue Task", -30}");
|
||||||
Console.WriteLine($"{"D: Delete Task",-30}{"R: List Running Tasks", -30}");
|
Console.WriteLine($"{"D: Delete Task",-30}{"S: Search Tasks", -30}{"K: List Task Queue", -30}");
|
||||||
Console.WriteLine($"{"E: Execute Task now",-30}{"S: Search Tasks", -30}");
|
Console.WriteLine($"{"E: Execute Task now",-30}{"R: List Running Tasks", -30}{"G: Remove Task from Queue", -30}");
|
||||||
Console.WriteLine();
|
Console.WriteLine($"{"M: New Download Manga Task",-30}{"", -30}{"", -30}");
|
||||||
Console.WriteLine($"{"U: Update this Screen",-30}{"Q: Exit",-30}");
|
Console.WriteLine($"{"",-30}{"F: Show Log",-30}{"Q: Exit",-30}");
|
||||||
ConsoleKey selection = Console.ReadKey().Key;
|
|
||||||
logger.WriteLine("Tranga_CLI", $"Menu selection: {selection}");
|
|
||||||
return selection;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void PrintTasks(TrangaTask[] tasks, Logger logger)
|
private static void PrintTasks(TrangaTask[] tasks, Logger logger)
|
||||||
@ -160,11 +209,125 @@ public static class Tranga_Cli
|
|||||||
$"{"",-5}{"Task",-20} | {"Last Executed",-20} | {"Reoccurrence",-12} | {"State",-10} | {"Connector",-15} | Publication/Manga";
|
$"{"",-5}{"Task",-20} | {"Last Executed",-20} | {"Reoccurrence",-12} | {"State",-10} | {"Connector",-15} | Publication/Manga";
|
||||||
Console.WriteLine(header);
|
Console.WriteLine(header);
|
||||||
Console.WriteLine(new string('-', header.Length));
|
Console.WriteLine(new string('-', header.Length));
|
||||||
foreach(TrangaTask trangaTask in tasks)
|
foreach (TrangaTask trangaTask in tasks)
|
||||||
Console.WriteLine($"{tIndex++:000}: {trangaTask}");
|
{
|
||||||
|
string[] taskSplit = trangaTask.ToString().Split(", ");
|
||||||
|
Console.WriteLine($"{tIndex++:000}: {taskSplit[0],-20} | {taskSplit[1],-20} | {taskSplit[2],-12} | {taskSplit[3],-10} | {(taskSplit.Length > 4 ? taskSplit[4] : ""),-15} | {(taskSplit.Length > 5 ? taskSplit[5] : "")}");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CreateTask(TaskManager taskManager, TaskManager.SettingsData settings, Logger logger)
|
private static TrangaTask? SelectTask(TrangaTask[] tasks, Logger logger)
|
||||||
|
{
|
||||||
|
logger.WriteLine("Tranga_CLI", "Menu: Select task");
|
||||||
|
if (tasks.Length < 1)
|
||||||
|
{
|
||||||
|
Console.Clear();
|
||||||
|
Console.WriteLine("There are no available Tasks.");
|
||||||
|
logger.WriteLine("Tranga_CLI", "No available Tasks.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
PrintTasks(tasks, logger);
|
||||||
|
|
||||||
|
logger.WriteLine("Tranga_CLI", "Selecting Task to Remove (from queue)");
|
||||||
|
Console.WriteLine("Enter q to abort");
|
||||||
|
Console.WriteLine($"Select Task (0-{tasks.Length - 1}):");
|
||||||
|
|
||||||
|
string? selectedTask = Console.ReadLine();
|
||||||
|
while(selectedTask is null || selectedTask.Length < 1)
|
||||||
|
selectedTask = Console.ReadLine();
|
||||||
|
|
||||||
|
if (selectedTask.Length == 1 && selectedTask.ToLower() == "q")
|
||||||
|
{
|
||||||
|
Console.Clear();
|
||||||
|
Console.WriteLine("aborted.");
|
||||||
|
logger.WriteLine("Tranga_CLI", "aborted");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int selectedTaskIndex = Convert.ToInt32(selectedTask);
|
||||||
|
return tasks[selectedTaskIndex];
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Exception: {e.Message}");
|
||||||
|
logger.WriteLine("Tranga_CLI", e.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddMangaTaskToQueue(TaskManager taskManager, Logger logger)
|
||||||
|
{
|
||||||
|
Console.Clear();
|
||||||
|
logger.WriteLine("Tranga_CLI", "Menu: Add Manga Download to queue");
|
||||||
|
|
||||||
|
Connector? connector = SelectConnector(taskManager.settings.downloadLocation, taskManager.GetAvailableConnectors().Values.ToArray(), logger);
|
||||||
|
if (connector is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Publication? publication = SelectPublication(taskManager, connector!, logger);
|
||||||
|
if (publication is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
TimeSpan reoccurrence = SelectReoccurrence(logger);
|
||||||
|
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
|
||||||
|
TrangaTask newTask = taskManager.AddTask(TrangaTask.Task.DownloadNewChapters, connector?.name, publication, reoccurrence, "en");
|
||||||
|
Console.WriteLine(newTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddTaskToQueue(TaskManager taskManager, Logger logger)
|
||||||
|
{
|
||||||
|
Console.Clear();
|
||||||
|
logger.WriteLine("Tranga_CLI", "Menu: Add Task to queue");
|
||||||
|
|
||||||
|
TrangaTask[] tasks = taskManager.GetAllTasks().Where(rTask =>
|
||||||
|
rTask.state is not TrangaTask.ExecutionState.Enqueued and not TrangaTask.ExecutionState.Running).ToArray();
|
||||||
|
|
||||||
|
TrangaTask? selectedTask = SelectTask(tasks, logger);
|
||||||
|
if (selectedTask is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
|
||||||
|
taskManager.AddTaskToQueue(selectedTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RemoveTaskFromQueue(TaskManager taskManager, Logger logger)
|
||||||
|
{
|
||||||
|
Console.Clear();
|
||||||
|
logger.WriteLine("Tranga_CLI", "Menu: Remove Task from queue");
|
||||||
|
|
||||||
|
TrangaTask[] tasks = taskManager.GetAllTasks().Where(rTask => rTask.state is TrangaTask.ExecutionState.Enqueued).ToArray();
|
||||||
|
|
||||||
|
TrangaTask? selectedTask = SelectTask(tasks, logger);
|
||||||
|
if (selectedTask is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
|
||||||
|
taskManager.RemoveTaskFromQueue(selectedTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TailLog(Logger logger)
|
||||||
|
{
|
||||||
|
logger.WriteLine("Tranga_CLI", "Menu: Show Log-lines");
|
||||||
|
Console.Clear();
|
||||||
|
|
||||||
|
string[] lines = logger.Tail(20);
|
||||||
|
foreach (string message in lines)
|
||||||
|
Console.Write(message);
|
||||||
|
|
||||||
|
while (!Console.KeyAvailable)
|
||||||
|
{
|
||||||
|
string[] newLines = logger.GetNewLines();
|
||||||
|
foreach(string message in newLines)
|
||||||
|
Console.Write(message);
|
||||||
|
Thread.Sleep(40);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CreateTask(TaskManager taskManager, TrangaSettings settings, Logger logger)
|
||||||
{
|
{
|
||||||
logger.WriteLine("Tranga_CLI", "Menu: Creating Task");
|
logger.WriteLine("Tranga_CLI", "Menu: Creating Task");
|
||||||
TrangaTask.Task? tmpTask = SelectTaskType(logger);
|
TrangaTask.Task? tmpTask = SelectTaskType(logger);
|
||||||
@ -183,7 +346,7 @@ public static class Tranga_Cli
|
|||||||
Publication? publication = null;
|
Publication? publication = null;
|
||||||
if (task != TrangaTask.Task.UpdatePublications && task != TrangaTask.Task.UpdateKomgaLibrary)
|
if (task != TrangaTask.Task.UpdatePublications && task != TrangaTask.Task.UpdateKomgaLibrary)
|
||||||
{
|
{
|
||||||
publication = SelectPublication(connector!, logger);
|
publication = SelectPublication(taskManager, connector!, logger);
|
||||||
if (publication is null)
|
if (publication is null)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -197,86 +360,27 @@ public static class Tranga_Cli
|
|||||||
private static void ExecuteTaskNow(TaskManager taskManager, Logger logger)
|
private static void ExecuteTaskNow(TaskManager taskManager, Logger logger)
|
||||||
{
|
{
|
||||||
logger.WriteLine("Tranga_CLI", "Menu: Executing Task");
|
logger.WriteLine("Tranga_CLI", "Menu: Executing Task");
|
||||||
TrangaTask[] tasks = taskManager.GetAllTasks();
|
TrangaTask[] tasks = taskManager.GetAllTasks().Where(nTask => nTask.state is not TrangaTask.ExecutionState.Running).ToArray();
|
||||||
if (tasks.Length < 1)
|
|
||||||
{
|
TrangaTask? selectedTask = SelectTask(tasks, logger);
|
||||||
Console.Clear();
|
if (selectedTask is null)
|
||||||
Console.WriteLine("There are no available Tasks.");
|
|
||||||
logger.WriteLine("Tranga_CLI", "No available Tasks.");
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
PrintTasks(tasks, logger);
|
|
||||||
|
|
||||||
logger.WriteLine("Tranga_CLI", "Selecting Task to Execute");
|
|
||||||
Console.WriteLine("Enter q to abort");
|
|
||||||
Console.WriteLine($"Select Task (0-{tasks.Length - 1}):");
|
|
||||||
|
|
||||||
string? selectedTask = Console.ReadLine();
|
|
||||||
while(selectedTask is null || selectedTask.Length < 1)
|
|
||||||
selectedTask = Console.ReadLine();
|
|
||||||
|
|
||||||
if (selectedTask.Length == 1 && selectedTask.ToLower() == "q")
|
|
||||||
{
|
|
||||||
Console.Clear();
|
|
||||||
Console.WriteLine("aborted.");
|
|
||||||
logger.WriteLine("Tranga_CLI", "aborted");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
int selectedTaskIndex = Convert.ToInt32(selectedTask);
|
|
||||||
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
|
|
||||||
taskManager.ExecuteTaskNow(tasks[selectedTaskIndex]);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Exception: {e.Message}");
|
|
||||||
logger.WriteLine("Tranga_CLI", e.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
|
||||||
|
taskManager.ExecuteTaskNow(selectedTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RemoveTask(TaskManager taskManager, Logger logger)
|
private static void DeleteTask(TaskManager taskManager, Logger logger)
|
||||||
{
|
{
|
||||||
logger.WriteLine("Tranga_CLI", "Menu: Remove Task");
|
logger.WriteLine("Tranga_CLI", "Menu: Delete Task");
|
||||||
TrangaTask[] tasks = taskManager.GetAllTasks();
|
TrangaTask[] tasks = taskManager.GetAllTasks();
|
||||||
if (tasks.Length < 1)
|
|
||||||
{
|
|
||||||
Console.Clear();
|
|
||||||
Console.WriteLine("There are no available Tasks.");
|
|
||||||
logger.WriteLine("Tranga_CLI", "No available Tasks");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
PrintTasks(tasks, logger);
|
|
||||||
|
|
||||||
logger.WriteLine("Tranga_CLI", "Selecting Task");
|
TrangaTask? selectedTask = SelectTask(tasks, logger);
|
||||||
Console.WriteLine("Enter q to abort");
|
if (selectedTask is null)
|
||||||
Console.WriteLine($"Select Task (0-{tasks.Length - 1}):");
|
|
||||||
|
|
||||||
string? selectedTask = Console.ReadLine();
|
|
||||||
while(selectedTask is null || selectedTask.Length < 1)
|
|
||||||
selectedTask = Console.ReadLine();
|
|
||||||
|
|
||||||
if (selectedTask.Length == 1 && selectedTask.ToLower() == "q")
|
|
||||||
{
|
|
||||||
Console.Clear();
|
|
||||||
Console.WriteLine("aborted.");
|
|
||||||
logger.WriteLine("Tranga_CLI", "aborted.");
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
try
|
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
|
||||||
{
|
taskManager.DeleteTask(selectedTask.task, selectedTask.connectorName, selectedTask.publication);
|
||||||
int selectedTaskIndex = Convert.ToInt32(selectedTask);
|
|
||||||
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
|
|
||||||
taskManager.RemoveTask(tasks[selectedTaskIndex].task, tasks[selectedTaskIndex].connectorName, tasks[selectedTaskIndex].publication);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Exception: {e.Message}");
|
|
||||||
logger.WriteLine("Tranga_CLI", e.Message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TrangaTask.Task? SelectTaskType(Logger logger)
|
private static TrangaTask.Task? SelectTaskType(Logger logger)
|
||||||
@ -366,7 +470,7 @@ public static class Tranga_Cli
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Publication? SelectPublication(Connector connector, Logger logger)
|
private static Publication? SelectPublication(TaskManager taskManager, Connector connector, Logger logger)
|
||||||
{
|
{
|
||||||
logger.WriteLine("Tranga_CLI", "Menu: Select Publication");
|
logger.WriteLine("Tranga_CLI", "Menu: Select Publication");
|
||||||
|
|
||||||
@ -375,7 +479,14 @@ public static class Tranga_Cli
|
|||||||
Console.WriteLine("Publication search query (leave empty for all):");
|
Console.WriteLine("Publication search query (leave empty for all):");
|
||||||
string? query = Console.ReadLine();
|
string? query = Console.ReadLine();
|
||||||
|
|
||||||
Publication[] publications = connector.GetPublications(query ?? "");
|
Publication[] publications = taskManager.GetPublicationsFromConnector(connector, query ?? "");
|
||||||
|
|
||||||
|
if (publications.Length < 1)
|
||||||
|
{
|
||||||
|
logger.WriteLine("Tranga_CLI", "No publications returned");
|
||||||
|
Console.WriteLine($"No publications for query '{query}' returned;");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
int pIndex = 0;
|
int pIndex = 0;
|
||||||
Console.WriteLine("Publications:");
|
Console.WriteLine("Publications:");
|
||||||
@ -414,8 +525,10 @@ public static class Tranga_Cli
|
|||||||
private static void SearchTasks(TaskManager taskManager, Logger logger)
|
private static void SearchTasks(TaskManager taskManager, Logger logger)
|
||||||
{
|
{
|
||||||
logger.WriteLine("Tranga_CLI", "Menu: Search task");
|
logger.WriteLine("Tranga_CLI", "Menu: Search task");
|
||||||
|
Console.Clear();
|
||||||
|
Console.WriteLine("Enter search query:");
|
||||||
string? query = Console.ReadLine();
|
string? query = Console.ReadLine();
|
||||||
while (query is null || query.Length < 1)
|
while (query is null || query.Length < 4)
|
||||||
query = Console.ReadLine();
|
query = Console.ReadLine();
|
||||||
PrintTasks(taskManager.GetAllTasks().Where(qTask =>
|
PrintTasks(taskManager.GetAllTasks().Where(qTask =>
|
||||||
qTask.ToString().ToLower().Contains(query, StringComparison.OrdinalIgnoreCase)).ToArray(), logger);
|
qTask.ToString().ToLower().Contains(query, StringComparison.OrdinalIgnoreCase)).ToArray(), logger);
|
||||||
|
12
Tranga.sln
12
Tranga.sln
@ -4,10 +4,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tranga", ".\Tranga\Tranga.c
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tranga-CLI", "Tranga-CLI\Tranga-CLI.csproj", "{4899E3B2-B259-479A-B43E-042D043E9501}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tranga-CLI", "Tranga-CLI\Tranga-CLI.csproj", "{4899E3B2-B259-479A-B43E-042D043E9501}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tranga-API", "Tranga-API\Tranga-API.csproj", "{6284C936-4E90-486B-BC46-0AFAD85AD8EE}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logging", "Logging\Logging.csproj", "{415BE889-BB7D-426F-976F-8D977876A462}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logging", "Logging\Logging.csproj", "{415BE889-BB7D-426F-976F-8D977876A462}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tranga-API", "Tranga-API\Tranga-API.csproj", "{48F4E495-75BC-4402-8E03-DEC5B79D7E83}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -22,13 +22,13 @@ Global
|
|||||||
{4899E3B2-B259-479A-B43E-042D043E9501}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{4899E3B2-B259-479A-B43E-042D043E9501}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{4899E3B2-B259-479A-B43E-042D043E9501}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{4899E3B2-B259-479A-B43E-042D043E9501}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{4899E3B2-B259-479A-B43E-042D043E9501}.Release|Any CPU.Build.0 = Release|Any CPU
|
{4899E3B2-B259-479A-B43E-042D043E9501}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{6284C936-4E90-486B-BC46-0AFAD85AD8EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{6284C936-4E90-486B-BC46-0AFAD85AD8EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{6284C936-4E90-486B-BC46-0AFAD85AD8EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{6284C936-4E90-486B-BC46-0AFAD85AD8EE}.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.ActiveCfg = Debug|Any CPU
|
||||||
{415BE889-BB7D-426F-976F-8D977876A462}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{415BE889-BB7D-426F-976F-8D977876A462}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{415BE889-BB7D-426F-976F-8D977876A462}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{415BE889-BB7D-426F-976F-8D977876A462}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{415BE889-BB7D-426F-976F-8D977876A462}.Release|Any CPU.Build.0 = Release|Any CPU
|
{415BE889-BB7D-426F-976F-8D977876A462}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{48F4E495-75BC-4402-8E03-DEC5B79D7E83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{48F4E495-75BC-4402-8E03-DEC5B79D7E83}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{48F4E495-75BC-4402-8E03-DEC5B79D7E83}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{48F4E495-75BC-4402-8E03-DEC5B79D7E83}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
@ -12,15 +12,18 @@ namespace Tranga;
|
|||||||
public abstract class Connector
|
public abstract class Connector
|
||||||
{
|
{
|
||||||
internal string downloadLocation { get; } //Location of local files
|
internal string downloadLocation { get; } //Location of local files
|
||||||
protected DownloadClient downloadClient { get; }
|
protected DownloadClient downloadClient { get; init; }
|
||||||
|
|
||||||
protected Logger? logger;
|
protected Logger? logger;
|
||||||
|
|
||||||
protected Connector(string downloadLocation, uint downloadDelay, Logger? logger)
|
protected Connector(string downloadLocation, Logger? logger)
|
||||||
{
|
{
|
||||||
this.downloadLocation = downloadLocation;
|
this.downloadLocation = downloadLocation;
|
||||||
this.downloadClient = new DownloadClient(downloadDelay);
|
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
|
this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
|
||||||
|
{
|
||||||
|
//RequestTypes for RateLimits
|
||||||
|
}, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract string name { get; } //Name of the Connector (e.g. Website)
|
public abstract string name { get; } //Name of the Connector (e.g. Website)
|
||||||
@ -80,11 +83,12 @@ public abstract class Connector
|
|||||||
/// <returns>XML-string</returns>
|
/// <returns>XML-string</returns>
|
||||||
protected static string CreateComicInfo(Publication publication, Chapter chapter, Logger? logger)
|
protected static string CreateComicInfo(Publication publication, Chapter chapter, Logger? logger)
|
||||||
{
|
{
|
||||||
logger?.WriteLine("Connector", $"Creating ComicInfo.Xml for {publication.sortName} Chapter {chapter.sortNumber}");
|
logger?.WriteLine("Connector", $"Creating ComicInfo.Xml for {publication.sortName} Chapter {chapter.volumeNumber} {chapter.chapterNumber}");
|
||||||
XElement comicInfo = new XElement("ComicInfo",
|
XElement comicInfo = new XElement("ComicInfo",
|
||||||
new XElement("Tags", string.Join(',',publication.tags)),
|
new XElement("Tags", string.Join(',',publication.tags)),
|
||||||
new XElement("LanguageISO", publication.originalLanguage),
|
new XElement("LanguageISO", publication.originalLanguage),
|
||||||
new XElement("Title", chapter.name),
|
new XElement("Title", chapter.name),
|
||||||
|
new XElement("Writer", publication.author),
|
||||||
new XElement("Volume", chapter.volumeNumber),
|
new XElement("Volume", chapter.volumeNumber),
|
||||||
new XElement("Number", chapter.chapterNumber) //TODO check if this is correct at some point
|
new XElement("Number", chapter.chapterNumber) //TODO check if this is correct at some point
|
||||||
);
|
);
|
||||||
@ -106,23 +110,24 @@ public abstract class Connector
|
|||||||
/// <returns>Filepath</returns>
|
/// <returns>Filepath</returns>
|
||||||
protected string CreateFullFilepath(Publication publication, Chapter chapter)
|
protected string CreateFullFilepath(Publication publication, Chapter chapter)
|
||||||
{
|
{
|
||||||
return Path.Join(downloadLocation, publication.folderName, chapter.fileName);
|
return Path.Join(downloadLocation, publication.folderName, $"{chapter.fileName}.cbz");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Downloads Image from URL and saves it to the given path(incl. fileName)
|
/// Downloads Image from URL and saves it to the given path(incl. fileName)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="imageUrl"></param>
|
/// <param name="imageUrl"></param>
|
||||||
/// <param name="fullPath"></param>
|
/// <param name="fullPath"></param>
|
||||||
/// <param name="downloadClient">DownloadClient of the connector</param>
|
/// <param name="downloadClient">DownloadClient of the connector</param>
|
||||||
protected static void DownloadImage(string imageUrl, string fullPath, DownloadClient downloadClient)
|
/// <param name="requestType">Requesttype for ratelimit</param>
|
||||||
|
protected static void DownloadImage(string imageUrl, string fullPath, DownloadClient downloadClient, byte requestType)
|
||||||
{
|
{
|
||||||
DownloadClient.RequestResult requestResult = downloadClient.MakeRequest(imageUrl);
|
DownloadClient.RequestResult requestResult = downloadClient.MakeRequest(imageUrl, requestType);
|
||||||
byte[] buffer = new byte[requestResult.result.Length];
|
byte[] buffer = new byte[requestResult.result.Length];
|
||||||
requestResult.result.ReadExactly(buffer, 0, buffer.Length);
|
requestResult.result.ReadExactly(buffer, 0, buffer.Length);
|
||||||
File.WriteAllBytes(fullPath, buffer);
|
File.WriteAllBytes(fullPath, buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Downloads all Images from URLs, Compresses to zip(cbz) and saves.
|
/// Downloads all Images from URLs, Compresses to zip(cbz) and saves.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -130,17 +135,16 @@ public abstract class Connector
|
|||||||
/// <param name="saveArchiveFilePath">Full path to save archive to (without file ending .cbz)</param>
|
/// <param name="saveArchiveFilePath">Full path to save archive to (without file ending .cbz)</param>
|
||||||
/// <param name="downloadClient">DownloadClient of the connector</param>
|
/// <param name="downloadClient">DownloadClient of the connector</param>
|
||||||
/// <param name="comicInfoPath">Path of the generate Chapter ComicInfo.xml, if it was generated</param>
|
/// <param name="comicInfoPath">Path of the generate Chapter ComicInfo.xml, if it was generated</param>
|
||||||
protected static void DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, DownloadClient downloadClient, Logger? logger, string? comicInfoPath = null)
|
/// <param name="requestType">RequestType for RateLimits</param>
|
||||||
|
protected static void DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, DownloadClient downloadClient, byte requestType, Logger? logger, string? comicInfoPath = null)
|
||||||
{
|
{
|
||||||
logger?.WriteLine("Connector", "Downloading Images");
|
logger?.WriteLine("Connector", "Downloading Images");
|
||||||
//Check if Publication Directory already exists
|
//Check if Publication Directory already exists
|
||||||
string[] splitPath = saveArchiveFilePath.Split(Path.DirectorySeparatorChar);
|
string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!;
|
||||||
string directoryPath = Path.Combine(splitPath.Take(splitPath.Length - 1).ToArray());
|
|
||||||
if (!Directory.Exists(directoryPath))
|
if (!Directory.Exists(directoryPath))
|
||||||
Directory.CreateDirectory(directoryPath);
|
Directory.CreateDirectory(directoryPath);
|
||||||
|
|
||||||
string fullPath = $"{saveArchiveFilePath}.cbz";
|
if (File.Exists(saveArchiveFilePath)) //Don't download twice.
|
||||||
if (File.Exists(fullPath)) //Don't download twice.
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
//Create a temporary folder to store images
|
//Create a temporary folder to store images
|
||||||
@ -152,7 +156,7 @@ public abstract class Connector
|
|||||||
{
|
{
|
||||||
string[] split = imageUrl.Split('.');
|
string[] split = imageUrl.Split('.');
|
||||||
string extension = split[^1];
|
string extension = split[^1];
|
||||||
DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), downloadClient);
|
DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), downloadClient, requestType);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(comicInfoPath is not null)
|
if(comicInfoPath is not null)
|
||||||
@ -160,40 +164,72 @@ public abstract class Connector
|
|||||||
|
|
||||||
logger?.WriteLine("Connector", "Creating archive");
|
logger?.WriteLine("Connector", "Creating archive");
|
||||||
//ZIP-it and ship-it
|
//ZIP-it and ship-it
|
||||||
ZipFile.CreateFromDirectory(tempFolder, fullPath);
|
ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath);
|
||||||
Directory.Delete(tempFolder, true); //Cleanup
|
Directory.Delete(tempFolder, true); //Cleanup
|
||||||
}
|
}
|
||||||
|
|
||||||
protected class DownloadClient
|
protected class DownloadClient
|
||||||
{
|
{
|
||||||
private readonly TimeSpan _requestSpeed;
|
|
||||||
private DateTime _lastRequest;
|
|
||||||
private static readonly HttpClient Client = new();
|
private static readonly HttpClient Client = new();
|
||||||
|
|
||||||
|
private readonly Dictionary<byte, DateTime> _lastExecutedRateLimit;
|
||||||
|
private readonly Dictionary<byte, TimeSpan> _rateLimit;
|
||||||
|
private Logger? logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a httpClient
|
/// Creates a httpClient
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="delay">minimum delay between requests (to avoid spam)</param>
|
/// <param name="delay">minimum delay between requests (to avoid spam)</param>
|
||||||
public DownloadClient(uint delay)
|
/// <param name="rateLimitRequestsPerMinute">Rate limits for requests. byte is RequestType, int maximum requests per minute for RequestType</param>
|
||||||
|
public DownloadClient(Dictionary<byte, int> rateLimitRequestsPerMinute, Logger? logger)
|
||||||
{
|
{
|
||||||
_requestSpeed = TimeSpan.FromMilliseconds(delay);
|
this.logger = logger;
|
||||||
_lastRequest = DateTime.Now.Subtract(_requestSpeed);
|
_lastExecutedRateLimit = new();
|
||||||
|
_rateLimit = new();
|
||||||
|
foreach(KeyValuePair<byte, int> limit in rateLimitRequestsPerMinute)
|
||||||
|
_rateLimit.Add(limit.Key, TimeSpan.FromMinutes(1).Divide(limit.Value));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Request Webpage
|
/// Request Webpage
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="url"></param>
|
/// <param name="url"></param>
|
||||||
|
/// <param name="requestType">For RateLimits: Same Endpoints use same type</param>
|
||||||
/// <returns>RequestResult with StatusCode and Stream of received data</returns>
|
/// <returns>RequestResult with StatusCode and Stream of received data</returns>
|
||||||
public RequestResult MakeRequest(string url)
|
public RequestResult MakeRequest(string url, byte requestType)
|
||||||
{
|
{
|
||||||
while((DateTime.Now - _lastRequest) < _requestSpeed)
|
if (_rateLimit.TryGetValue(requestType, out TimeSpan value))
|
||||||
Thread.Sleep(10);
|
_lastExecutedRateLimit.TryAdd(requestType, DateTime.Now.Subtract(value));
|
||||||
_lastRequest = DateTime.Now;
|
else
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), "RequestType not configured for rate-limit.");
|
||||||
|
return new RequestResult(HttpStatusCode.NotAcceptable, Stream.Null);
|
||||||
|
}
|
||||||
|
|
||||||
HttpRequestMessage requestMessage = new(HttpMethod.Get, url);
|
TimeSpan rateLimitTimeout = _rateLimit[requestType]
|
||||||
HttpResponseMessage response = Client.Send(requestMessage);
|
.Subtract(DateTime.Now.Subtract(_lastExecutedRateLimit[requestType]));
|
||||||
|
|
||||||
|
if(rateLimitTimeout > TimeSpan.Zero)
|
||||||
|
Thread.Sleep(rateLimitTimeout);
|
||||||
|
|
||||||
|
HttpResponseMessage? response = null;
|
||||||
|
while (response is null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HttpRequestMessage requestMessage = new(HttpMethod.Get, url);
|
||||||
|
_lastExecutedRateLimit[requestType] = DateTime.Now;
|
||||||
|
response = Client.Send(requestMessage);
|
||||||
|
}
|
||||||
|
catch (HttpRequestException e)
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), e.Message);
|
||||||
|
Thread.Sleep(_rateLimit[requestType] * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
Stream resultString = response.IsSuccessStatusCode ? response.Content.ReadAsStream() : Stream.Null;
|
Stream resultString = response.IsSuccessStatusCode ? response.Content.ReadAsStream() : Stream.Null;
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Request-Error {response.StatusCode}: {response.ReasonPhrase}");
|
||||||
return new RequestResult(response.StatusCode, resultString);
|
return new RequestResult(response.StatusCode, resultString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,19 +9,31 @@ public class MangaDex : Connector
|
|||||||
{
|
{
|
||||||
public override string name { get; }
|
public override string name { get; }
|
||||||
|
|
||||||
public MangaDex(string downloadLocation, uint downloadDelay, Logger? logger) : base(downloadLocation, downloadDelay, logger)
|
private enum RequestType : byte
|
||||||
{
|
{
|
||||||
name = "MangaDex";
|
Manga,
|
||||||
|
Feed,
|
||||||
|
AtHomeServer,
|
||||||
|
Cover,
|
||||||
|
Author
|
||||||
}
|
}
|
||||||
|
|
||||||
public MangaDex(string downloadLocation, Logger? logger) : base(downloadLocation, 750, logger)
|
public MangaDex(string downloadLocation, Logger? logger) : base(downloadLocation, logger)
|
||||||
{
|
{
|
||||||
name = "MangaDex";
|
name = "MangaDex";
|
||||||
|
this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
|
||||||
|
{
|
||||||
|
{(byte)RequestType.Manga, 250},
|
||||||
|
{(byte)RequestType.Feed, 250},
|
||||||
|
{(byte)RequestType.AtHomeServer, 40},
|
||||||
|
{(byte)RequestType.Cover, 250},
|
||||||
|
{(byte)RequestType.Author, 250}
|
||||||
|
}, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Publication[] GetPublications(string publicationTitle = "")
|
public override Publication[] GetPublications(string publicationTitle = "")
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Getting Publications");
|
logger?.WriteLine(this.GetType().ToString(), $"Getting Publications (title={publicationTitle})");
|
||||||
const int limit = 100; //How many values we want returned at once
|
const int limit = 100; //How many values we want returned at once
|
||||||
int offset = 0; //"Page"
|
int offset = 0; //"Page"
|
||||||
int total = int.MaxValue; //How many total results are there, is updated on first request
|
int total = int.MaxValue; //How many total results are there, is updated on first request
|
||||||
@ -31,7 +43,7 @@ public class MangaDex : Connector
|
|||||||
//Request next Page
|
//Request next Page
|
||||||
DownloadClient.RequestResult requestResult =
|
DownloadClient.RequestResult requestResult =
|
||||||
downloadClient.MakeRequest(
|
downloadClient.MakeRequest(
|
||||||
$"https://api.mangadex.org/manga?limit={limit}&title={publicationTitle}&offset={offset}");
|
$"https://api.mangadex.org/manga?limit={limit}&title={publicationTitle}&offset={offset}", (byte)RequestType.Manga);
|
||||||
if (requestResult.statusCode != HttpStatusCode.OK)
|
if (requestResult.statusCode != HttpStatusCode.OK)
|
||||||
break;
|
break;
|
||||||
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
||||||
@ -49,6 +61,8 @@ public class MangaDex : Connector
|
|||||||
JsonObject manga = (JsonObject)mangeNode!;
|
JsonObject manga = (JsonObject)mangeNode!;
|
||||||
JsonObject attributes = manga["attributes"]!.AsObject();
|
JsonObject attributes = manga["attributes"]!.AsObject();
|
||||||
|
|
||||||
|
string publicationId = manga["id"]!.GetValue<string>();
|
||||||
|
|
||||||
string title = attributes["title"]!.AsObject().ContainsKey("en") && attributes["title"]!["en"] is not null
|
string title = attributes["title"]!.AsObject().ContainsKey("en") && attributes["title"]!["en"] is not null
|
||||||
? attributes["title"]!["en"]!.GetValue<string>()
|
? attributes["title"]!["en"]!.GetValue<string>()
|
||||||
: attributes["title"]![((IDictionary<string, JsonNode?>)attributes["title"]!.AsObject()).Keys.First()]!.GetValue<string>();
|
: attributes["title"]![((IDictionary<string, JsonNode?>)attributes["title"]!.AsObject()).Keys.First()]!.GetValue<string>();
|
||||||
@ -58,14 +72,12 @@ public class MangaDex : Connector
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
JsonArray altTitlesObject = attributes["altTitles"]!.AsArray();
|
JsonArray altTitlesObject = attributes["altTitles"]!.AsArray();
|
||||||
string[,] altTitles = new string[altTitlesObject.Count, 2];
|
Dictionary<string, string> altTitlesDict = new();
|
||||||
int titleIndex = 0;
|
|
||||||
foreach (JsonNode? altTitleNode in altTitlesObject)
|
foreach (JsonNode? altTitleNode in altTitlesObject)
|
||||||
{
|
{
|
||||||
JsonObject altTitleObject = (JsonObject)altTitleNode!;
|
JsonObject altTitleObject = (JsonObject)altTitleNode!;
|
||||||
string key = ((IDictionary<string, JsonNode?>)altTitleObject).Keys.ToArray()[0];
|
string key = ((IDictionary<string, JsonNode?>)altTitleObject).Keys.ToArray()[0];
|
||||||
altTitles[titleIndex, 0] = key;
|
altTitlesDict.TryAdd(key, altTitleObject[key]!.GetValue<string>());
|
||||||
altTitles[titleIndex++, 1] = altTitleObject[key]!.GetValue<string>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonArray tagsObject = attributes["tags"]!.AsArray();
|
JsonArray tagsObject = attributes["tags"]!.AsArray();
|
||||||
@ -77,23 +89,24 @@ public class MangaDex : Connector
|
|||||||
tags.Add(tagObject["attributes"]!["name"]!["en"]!.GetValue<string>());
|
tags.Add(tagObject["attributes"]!["name"]!["en"]!.GetValue<string>());
|
||||||
}
|
}
|
||||||
|
|
||||||
string? poster = null;
|
string? posterId = null;
|
||||||
|
string? authorId = null;
|
||||||
if (manga.ContainsKey("relationships") && manga["relationships"] is not null)
|
if (manga.ContainsKey("relationships") && manga["relationships"] is not null)
|
||||||
{
|
{
|
||||||
JsonArray relationships = manga["relationships"]!.AsArray();
|
JsonArray relationships = manga["relationships"]!.AsArray();
|
||||||
poster = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue<string>() == "cover_art")!["id"]!.GetValue<string>();
|
posterId = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue<string>() == "cover_art")!["id"]!.GetValue<string>();
|
||||||
|
authorId = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue<string>() == "author")!["id"]!.GetValue<string>();
|
||||||
}
|
}
|
||||||
|
string? coverUrl = GetCoverUrl(publicationId, posterId);
|
||||||
|
string? author = GetAuthor(authorId);
|
||||||
|
|
||||||
string[,]? links = null;
|
Dictionary<string, string> linksDict = new();
|
||||||
if (attributes.ContainsKey("links") && attributes["links"] is not null)
|
if (attributes.ContainsKey("links") && attributes["links"] is not null)
|
||||||
{
|
{
|
||||||
JsonObject linksObject = attributes["links"]!.AsObject();
|
JsonObject linksObject = attributes["links"]!.AsObject();
|
||||||
links = new string[linksObject.Count, 2];
|
|
||||||
int linkIndex = 0;
|
|
||||||
foreach (string key in ((IDictionary<string, JsonNode?>)linksObject).Keys)
|
foreach (string key in ((IDictionary<string, JsonNode?>)linksObject).Keys)
|
||||||
{
|
{
|
||||||
links[linkIndex, 0] = key;
|
linksDict.Add(key, linksObject[key]!.GetValue<string>());
|
||||||
links[linkIndex++, 1] = linksObject[key]!.GetValue<string>();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,17 +120,18 @@ public class MangaDex : Connector
|
|||||||
|
|
||||||
string status = attributes["status"]!.GetValue<string>();
|
string status = attributes["status"]!.GetValue<string>();
|
||||||
|
|
||||||
Publication pub = new Publication(
|
Publication pub = new (
|
||||||
title,
|
title,
|
||||||
|
author,
|
||||||
description,
|
description,
|
||||||
altTitles,
|
altTitlesDict,
|
||||||
tags.ToArray(),
|
tags.ToArray(),
|
||||||
poster,
|
coverUrl,
|
||||||
links,
|
linksDict,
|
||||||
year,
|
year,
|
||||||
originalLanguage,
|
originalLanguage,
|
||||||
status,
|
status,
|
||||||
manga["id"]!.GetValue<string>()
|
publicationId
|
||||||
);
|
);
|
||||||
publications.Add(pub); //Add Publication (Manga) to result
|
publications.Add(pub); //Add Publication (Manga) to result
|
||||||
}
|
}
|
||||||
@ -128,7 +142,7 @@ public class MangaDex : Connector
|
|||||||
|
|
||||||
public override Chapter[] GetChapters(Publication publication, string language = "")
|
public override Chapter[] GetChapters(Publication publication, string language = "")
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Getting Chapters");
|
logger?.WriteLine(this.GetType().ToString(), $"Getting Chapters {publication.sortName} (language={language})");
|
||||||
const int limit = 100; //How many values we want returned at once
|
const int limit = 100; //How many values we want returned at once
|
||||||
int offset = 0; //"Page"
|
int offset = 0; //"Page"
|
||||||
int total = int.MaxValue; //How many total results are there, is updated on first request
|
int total = int.MaxValue; //How many total results are there, is updated on first request
|
||||||
@ -139,7 +153,7 @@ public class MangaDex : Connector
|
|||||||
//Request next "Page"
|
//Request next "Page"
|
||||||
DownloadClient.RequestResult requestResult =
|
DownloadClient.RequestResult requestResult =
|
||||||
downloadClient.MakeRequest(
|
downloadClient.MakeRequest(
|
||||||
$"https://api.mangadex.org/manga/{publication.downloadUrl}/feed?limit={limit}&offset={offset}&translatedLanguage%5B%5D={language}");
|
$"https://api.mangadex.org/manga/{publication.publicationId}/feed?limit={limit}&offset={offset}&translatedLanguage%5B%5D={language}", (byte)RequestType.Feed);
|
||||||
if (requestResult.statusCode != HttpStatusCode.OK)
|
if (requestResult.statusCode != HttpStatusCode.OK)
|
||||||
break;
|
break;
|
||||||
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
||||||
@ -183,10 +197,10 @@ public class MangaDex : Connector
|
|||||||
|
|
||||||
public override void DownloadChapter(Publication publication, Chapter chapter)
|
public override void DownloadChapter(Publication publication, Chapter chapter)
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Download Chapter {publication.sortName} {chapter.sortNumber}");
|
logger?.WriteLine(this.GetType().ToString(), $"Download Chapter {publication.sortName} {chapter.volumeNumber}-{chapter.chapterNumber}");
|
||||||
//Request URLs for Chapter-Images
|
//Request URLs for Chapter-Images
|
||||||
DownloadClient.RequestResult requestResult =
|
DownloadClient.RequestResult requestResult =
|
||||||
downloadClient.MakeRequest($"https://api.mangadex.org/at-home/server/{chapter.url}?forcePort443=false'");
|
downloadClient.MakeRequest($"https://api.mangadex.org/at-home/server/{chapter.url}?forcePort443=false'", (byte)RequestType.AtHomeServer);
|
||||||
if (requestResult.statusCode != HttpStatusCode.OK)
|
if (requestResult.statusCode != HttpStatusCode.OK)
|
||||||
return;
|
return;
|
||||||
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
||||||
@ -205,7 +219,47 @@ public class MangaDex : Connector
|
|||||||
File.WriteAllText(comicInfoPath, CreateComicInfo(publication, chapter, logger));
|
File.WriteAllText(comicInfoPath, CreateComicInfo(publication, chapter, logger));
|
||||||
|
|
||||||
//Download Chapter-Images
|
//Download Chapter-Images
|
||||||
DownloadChapterImages(imageUrls.ToArray(), CreateFullFilepath(publication, chapter), downloadClient, logger, comicInfoPath);
|
DownloadChapterImages(imageUrls.ToArray(), CreateFullFilepath(publication, chapter), downloadClient, (byte)RequestType.AtHomeServer, logger, comicInfoPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? GetCoverUrl(string publicationId, string? posterId)
|
||||||
|
{
|
||||||
|
if (posterId is null)
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"No posterId");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Request information where to download Cover
|
||||||
|
DownloadClient.RequestResult requestResult =
|
||||||
|
downloadClient.MakeRequest($"https://api.mangadex.org/cover/{posterId}", (byte)RequestType.Cover);
|
||||||
|
if (requestResult.statusCode != HttpStatusCode.OK)
|
||||||
|
return null;
|
||||||
|
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
||||||
|
if (result is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
string fileName = result["data"]!["attributes"]!["fileName"]!.GetValue<string>();
|
||||||
|
|
||||||
|
string coverUrl = $"https://uploads.mangadex.org/covers/{publicationId}/{fileName}";
|
||||||
|
return coverUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? GetAuthor(string? authorId)
|
||||||
|
{
|
||||||
|
if (authorId is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
DownloadClient.RequestResult requestResult =
|
||||||
|
downloadClient.MakeRequest($"https://api.mangadex.org/author/{authorId}", (byte)RequestType.Author);
|
||||||
|
if (requestResult.statusCode != HttpStatusCode.OK)
|
||||||
|
return null;
|
||||||
|
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
||||||
|
if (result is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
string author = result["data"]!["attributes"]!["name"]!.GetValue<string>();
|
||||||
|
return author;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void DownloadCover(Publication publication)
|
public override void DownloadCover(Publication publication)
|
||||||
@ -216,31 +270,26 @@ public class MangaDex : Connector
|
|||||||
if(!Directory.Exists(publicationFolder))
|
if(!Directory.Exists(publicationFolder))
|
||||||
Directory.CreateDirectory(publicationFolder);
|
Directory.CreateDirectory(publicationFolder);
|
||||||
DirectoryInfo dirInfo = new (publicationFolder);
|
DirectoryInfo dirInfo = new (publicationFolder);
|
||||||
foreach(FileInfo fileInfo in dirInfo.EnumerateFiles())
|
if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover.")))
|
||||||
if (fileInfo.Name.Contains("cover."))
|
{
|
||||||
return;
|
logger?.WriteLine(this.GetType().ToString(), $"Cover exists {publication.sortName}");
|
||||||
|
|
||||||
//Request information where to download Cover
|
|
||||||
DownloadClient.RequestResult requestResult =
|
|
||||||
downloadClient.MakeRequest($"https://api.mangadex.org/cover/{publication.posterUrl}");
|
|
||||||
if (requestResult.statusCode != HttpStatusCode.OK)
|
|
||||||
return;
|
return;
|
||||||
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
}
|
||||||
if (result is null)
|
|
||||||
|
if (publication.posterUrl is null || publication.posterUrl!.Contains("http"))
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"No Poster-URL in publication");
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
string fileName = result["data"]!["attributes"]!["fileName"]!.GetValue<string>();
|
|
||||||
|
|
||||||
string coverUrl = $"https://uploads.mangadex.org/covers/{publication.downloadUrl}/{fileName}";
|
|
||||||
|
|
||||||
//Get file-extension (jpg, png)
|
//Get file-extension (jpg, png)
|
||||||
string[] split = coverUrl.Split('.');
|
string[] split = publication.posterUrl.Split('.');
|
||||||
string extension = split[^1];
|
string extension = split[^1];
|
||||||
|
|
||||||
string outFolderPath = Path.Join(downloadLocation, publication.folderName);
|
string outFolderPath = Path.Join(downloadLocation, publication.folderName);
|
||||||
Directory.CreateDirectory(outFolderPath);
|
Directory.CreateDirectory(outFolderPath);
|
||||||
|
|
||||||
//Download cover-Image
|
//Download cover-Image
|
||||||
DownloadImage(coverUrl, Path.Join(downloadLocation, publication.folderName, $"cover.{extension}"), this.downloadClient);
|
DownloadImage(publication.posterUrl, Path.Join(downloadLocation, publication.folderName, $"cover.{extension}"), this.downloadClient, (byte)RequestType.AtHomeServer);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using Newtonsoft.Json;
|
using System.Text;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Tranga;
|
namespace Tranga;
|
||||||
|
|
||||||
@ -8,32 +9,36 @@ namespace Tranga;
|
|||||||
public readonly struct Publication
|
public readonly struct Publication
|
||||||
{
|
{
|
||||||
public string sortName { get; }
|
public string sortName { get; }
|
||||||
// ReSharper disable UnusedAutoPropertyAccessor.Global we need it, trust
|
public string? author { get; }
|
||||||
[JsonIgnore]public string[,] altTitles { get; }
|
public Dictionary<string,string> altTitles { get; }
|
||||||
// ReSharper disable trice MemberCanBePrivate.Global, trust
|
// ReSharper disable trice MemberCanBePrivate.Global, trust
|
||||||
public string? description { get; }
|
public string? description { get; }
|
||||||
public string[] tags { get; }
|
public string[] tags { get; }
|
||||||
public string? posterUrl { get; }
|
public string? posterUrl { get; }
|
||||||
[JsonIgnore]public string[,]? links { get; }
|
public Dictionary<string,string> links { get; }
|
||||||
public int? year { get; }
|
public int? year { get; }
|
||||||
public string? originalLanguage { get; }
|
public string? originalLanguage { get; }
|
||||||
public string status { get; }
|
public string status { get; }
|
||||||
public string folderName { get; }
|
public string folderName { get; }
|
||||||
public string downloadUrl { get; }
|
public string publicationId { get; }
|
||||||
|
public string internalId { get; }
|
||||||
|
|
||||||
public Publication(string sortName, string? description, string[,] altTitles, string[] tags, string? posterUrl, string[,]? links, int? year, string? originalLanguage, string status, string downloadUrl)
|
public Publication(string sortName, string? author, string? description, Dictionary<string,string> altTitles, string[] tags, string? posterUrl, Dictionary<string,string>? links, int? year, string? originalLanguage, string status, string publicationId)
|
||||||
{
|
{
|
||||||
this.sortName = sortName;
|
this.sortName = sortName;
|
||||||
|
this.author = author;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
this.altTitles = altTitles;
|
this.altTitles = altTitles;
|
||||||
this.tags = tags;
|
this.tags = tags;
|
||||||
this.posterUrl = posterUrl;
|
this.posterUrl = posterUrl;
|
||||||
this.links = links;
|
this.links = links ?? new Dictionary<string, string>();
|
||||||
this.year = year;
|
this.year = year;
|
||||||
this.originalLanguage = originalLanguage;
|
this.originalLanguage = originalLanguage;
|
||||||
this.status = status;
|
this.status = status;
|
||||||
this.downloadUrl = downloadUrl;
|
this.publicationId = publicationId;
|
||||||
this.folderName = string.Concat(sortName.Split(Path.GetInvalidPathChars().Concat(Path.GetInvalidFileNameChars()).ToArray()));
|
this.folderName = string.Concat(sortName.Split(Path.GetInvalidPathChars().Concat(Path.GetInvalidFileNameChars()).ToArray()));
|
||||||
|
string onlyLowerLetters = string.Concat(this.sortName.ToLower().Where(Char.IsLetter));
|
||||||
|
this.internalId = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{onlyLowerLetters}{this.year}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <returns>Serialized JSON String for series.json</returns>
|
/// <returns>Serialized JSON String for series.json</returns>
|
||||||
|
@ -15,8 +15,9 @@ public static class TaskExecutor
|
|||||||
/// <param name="taskManager">Parent</param>
|
/// <param name="taskManager">Parent</param>
|
||||||
/// <param name="trangaTask">Task to execute</param>
|
/// <param name="trangaTask">Task to execute</param>
|
||||||
/// <param name="chapterCollection">Current chapterCollection to update</param>
|
/// <param name="chapterCollection">Current chapterCollection to update</param>
|
||||||
|
/// <param name="logger"></param>
|
||||||
/// <exception cref="ArgumentException">Is thrown when there is no Connector available with the name of the TrangaTask.connectorName</exception>
|
/// <exception cref="ArgumentException">Is thrown when there is no Connector available with the name of the TrangaTask.connectorName</exception>
|
||||||
public static void Execute(TaskManager taskManager, TrangaTask trangaTask, Dictionary<Publication, List<Chapter>> chapterCollection, Logger? logger)
|
public static void Execute(TaskManager taskManager, TrangaTask trangaTask, Logger? logger)
|
||||||
{
|
{
|
||||||
//Only execute task if it is not already being executed.
|
//Only execute task if it is not already being executed.
|
||||||
if (trangaTask.state == TrangaTask.ExecutionState.Running)
|
if (trangaTask.state == TrangaTask.ExecutionState.Running)
|
||||||
@ -25,7 +26,7 @@ public static class TaskExecutor
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
trangaTask.state = TrangaTask.ExecutionState.Running;
|
trangaTask.state = TrangaTask.ExecutionState.Running;
|
||||||
logger?.WriteLine("TaskExecutor", $"Executing Task {trangaTask}");
|
logger?.WriteLine("TaskExecutor", $"Starting Task {trangaTask}");
|
||||||
|
|
||||||
//Connector is not needed for all tasks
|
//Connector is not needed for all tasks
|
||||||
Connector? connector = null;
|
Connector? connector = null;
|
||||||
@ -36,22 +37,22 @@ public static class TaskExecutor
|
|||||||
switch (trangaTask.task)
|
switch (trangaTask.task)
|
||||||
{
|
{
|
||||||
case TrangaTask.Task.DownloadNewChapters:
|
case TrangaTask.Task.DownloadNewChapters:
|
||||||
DownloadNewChapters(connector!, (Publication)trangaTask.publication!, trangaTask.language, chapterCollection);
|
DownloadNewChapters(connector!, (Publication)trangaTask.publication!, trangaTask.language, ref taskManager._chapterCollection);
|
||||||
break;
|
break;
|
||||||
case TrangaTask.Task.UpdateChapters:
|
case TrangaTask.Task.UpdateChapters:
|
||||||
UpdateChapters(connector!, (Publication)trangaTask.publication!, trangaTask.language, chapterCollection);
|
UpdateChapters(connector!, (Publication)trangaTask.publication!, trangaTask.language, ref taskManager._chapterCollection);
|
||||||
break;
|
break;
|
||||||
case TrangaTask.Task.UpdatePublications:
|
case TrangaTask.Task.UpdatePublications:
|
||||||
UpdatePublications(connector!, chapterCollection);
|
UpdatePublications(connector!, ref taskManager._chapterCollection);
|
||||||
break;
|
break;
|
||||||
case TrangaTask.Task.UpdateKomgaLibrary:
|
case TrangaTask.Task.UpdateKomgaLibrary:
|
||||||
UpdateKomgaLibrary(taskManager);
|
UpdateKomgaLibrary(taskManager);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger?.WriteLine("TaskExecutor", $"Task executed! {trangaTask}");
|
logger?.WriteLine("TaskExecutor", $"Task finished! {trangaTask}");
|
||||||
trangaTask.state = TrangaTask.ExecutionState.Waiting;
|
|
||||||
trangaTask.lastExecuted = DateTime.Now;
|
trangaTask.lastExecuted = DateTime.Now;
|
||||||
|
trangaTask.state = TrangaTask.ExecutionState.Waiting;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -74,7 +75,7 @@ public static class TaskExecutor
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="connector">Connector to receive Publications from</param>
|
/// <param name="connector">Connector to receive Publications from</param>
|
||||||
/// <param name="chapterCollection"></param>
|
/// <param name="chapterCollection"></param>
|
||||||
private static void UpdatePublications(Connector connector, Dictionary<Publication, List<Chapter>> chapterCollection)
|
private static void UpdatePublications(Connector connector, ref Dictionary<Publication, List<Chapter>> chapterCollection)
|
||||||
{
|
{
|
||||||
Publication[] publications = connector.GetPublications();
|
Publication[] publications = connector.GetPublications();
|
||||||
foreach (Publication publication in publications)
|
foreach (Publication publication in publications)
|
||||||
@ -89,15 +90,15 @@ public static class TaskExecutor
|
|||||||
/// <param name="publication">Publication to check</param>
|
/// <param name="publication">Publication to check</param>
|
||||||
/// <param name="language">Language to receive chapters for</param>
|
/// <param name="language">Language to receive chapters for</param>
|
||||||
/// <param name="chapterCollection"></param>
|
/// <param name="chapterCollection"></param>
|
||||||
private static void DownloadNewChapters(Connector connector, Publication publication, string language, Dictionary<Publication, List<Chapter>> chapterCollection)
|
private static void DownloadNewChapters(Connector connector, Publication publication, string language, ref Dictionary<Publication, List<Chapter>> chapterCollection)
|
||||||
{
|
{
|
||||||
List<Chapter> newChapters = UpdateChapters(connector, publication, language, chapterCollection);
|
//Check if Publication already has a Folder
|
||||||
connector.DownloadCover(publication);
|
|
||||||
|
|
||||||
//Check if Publication already has a Folder and a series.json
|
|
||||||
string publicationFolder = Path.Join(connector.downloadLocation, publication.folderName);
|
string publicationFolder = Path.Join(connector.downloadLocation, publication.folderName);
|
||||||
if(!Directory.Exists(publicationFolder))
|
if(!Directory.Exists(publicationFolder))
|
||||||
Directory.CreateDirectory(publicationFolder);
|
Directory.CreateDirectory(publicationFolder);
|
||||||
|
List<Chapter> newChapters = UpdateChapters(connector, publication, language, ref chapterCollection);
|
||||||
|
|
||||||
|
connector.DownloadCover(publication);
|
||||||
|
|
||||||
string seriesInfoPath = Path.Join(publicationFolder, "series.json");
|
string seriesInfoPath = Path.Join(publicationFolder, "series.json");
|
||||||
if(!File.Exists(seriesInfoPath))
|
if(!File.Exists(seriesInfoPath))
|
||||||
@ -115,7 +116,7 @@ public static class TaskExecutor
|
|||||||
/// <param name="language">Language to receive chapters for</param>
|
/// <param name="language">Language to receive chapters for</param>
|
||||||
/// <param name="chapterCollection"></param>
|
/// <param name="chapterCollection"></param>
|
||||||
/// <returns>List of Chapters that were previously not in collection</returns>
|
/// <returns>List of Chapters that were previously not in collection</returns>
|
||||||
private static List<Chapter> UpdateChapters(Connector connector, Publication publication, string language, Dictionary<Publication, List<Chapter>> chapterCollection)
|
private static List<Chapter> UpdateChapters(Connector connector, Publication publication, string language, ref Dictionary<Publication, List<Chapter>> chapterCollection)
|
||||||
{
|
{
|
||||||
List<Chapter> newChaptersList = new();
|
List<Chapter> newChaptersList = new();
|
||||||
chapterCollection.TryAdd(publication, newChaptersList); //To ensure publication is actually in collection
|
chapterCollection.TryAdd(publication, newChaptersList); //To ensure publication is actually in collection
|
||||||
|
@ -10,46 +10,62 @@ namespace Tranga;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class TaskManager
|
public class TaskManager
|
||||||
{
|
{
|
||||||
private readonly Dictionary<Publication, List<Chapter>> _chapterCollection = new();
|
public Dictionary<Publication, List<Chapter>> _chapterCollection = new();
|
||||||
private readonly HashSet<TrangaTask> _allTasks;
|
private HashSet<TrangaTask> _allTasks;
|
||||||
private bool _continueRunning = true;
|
private bool _continueRunning = true;
|
||||||
private readonly Connector[] _connectors;
|
private readonly Connector[] _connectors;
|
||||||
private Dictionary<Connector, List<TrangaTask>> tasksToExecute = new();
|
private readonly Dictionary<Connector, List<TrangaTask>> _taskQueue = new();
|
||||||
private string downloadLocation { get; }
|
public TrangaSettings settings { get; }
|
||||||
private Logger? logger { get; }
|
private Logger? logger { get; }
|
||||||
|
public Komga? komga => settings.komga;
|
||||||
public Komga? komga { get; }
|
|
||||||
|
|
||||||
/// <param name="folderPath">Local path to save data (Manga) to</param>
|
/// <param name="downloadFolderPath">Local path to save data (Manga) to</param>
|
||||||
|
/// <param name="workingDirectory">Path to the working directory</param>
|
||||||
/// <param name="komgaBaseUrl">The Url of the Komga-instance that you want to update</param>
|
/// <param name="komgaBaseUrl">The Url of the Komga-instance that you want to update</param>
|
||||||
/// <param name="komgaUsername">The Komga username</param>
|
/// <param name="komgaUsername">The Komga username</param>
|
||||||
/// <param name="komgaPassword">The Komga password</param>
|
/// <param name="komgaPassword">The Komga password</param>
|
||||||
/// <param name="logger"></param>
|
/// <param name="logger"></param>
|
||||||
public TaskManager(string folderPath, string? komgaBaseUrl = null, string? komgaUsername = null, string? komgaPassword = null, Logger? logger = null)
|
public TaskManager(string downloadFolderPath, string workingDirectory, string? komgaBaseUrl = null, string? komgaUsername = null, string? komgaPassword = null, Logger? logger = null)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.downloadLocation = folderPath;
|
|
||||||
|
|
||||||
if (komgaBaseUrl != null && komgaUsername != null && komgaPassword != null)
|
|
||||||
this.komga = new Komga(komgaBaseUrl, komgaUsername, komgaPassword, logger);
|
|
||||||
this._connectors = new Connector[]{ new MangaDex(folderPath, logger) };
|
|
||||||
foreach(Connector cConnector in this._connectors)
|
|
||||||
tasksToExecute.Add(cConnector, new List<TrangaTask>());
|
|
||||||
_allTasks = new HashSet<TrangaTask>();
|
_allTasks = new HashSet<TrangaTask>();
|
||||||
|
|
||||||
|
Komga? newKomga = null;
|
||||||
|
if (komgaBaseUrl != null && komgaUsername != null && komgaPassword != null)
|
||||||
|
newKomga = new Komga(komgaBaseUrl, komgaUsername, komgaPassword, logger);
|
||||||
|
|
||||||
|
this.settings = new TrangaSettings(downloadFolderPath, workingDirectory, newKomga);
|
||||||
|
ExportData();
|
||||||
|
|
||||||
|
this._connectors = new Connector[]{ new MangaDex(downloadFolderPath, logger) };
|
||||||
|
foreach(Connector cConnector in this._connectors)
|
||||||
|
_taskQueue.Add(cConnector, new List<TrangaTask>());
|
||||||
|
|
||||||
Thread taskChecker = new(TaskCheckerThread);
|
Thread taskChecker = new(TaskCheckerThread);
|
||||||
taskChecker.Start();
|
taskChecker.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public TaskManager(SettingsData settings, Logger? logger = null)
|
public void UpdateSettings(string? downloadLocation, string? komgaUrl, string? komgaAuth)
|
||||||
|
{
|
||||||
|
Komga? komga = null;
|
||||||
|
if (komgaUrl is not null && komgaAuth is not null)
|
||||||
|
komga = new Komga(komgaUrl, komgaAuth, null);
|
||||||
|
settings.downloadLocation = downloadLocation ?? settings.downloadLocation;
|
||||||
|
settings.komga = komga ?? komga;
|
||||||
|
ExportData();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TaskManager(TrangaSettings settings, Logger? logger = null)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this._connectors = new Connector[]{ new MangaDex(settings.downloadLocation, logger) };
|
this._connectors = new Connector[]{ new MangaDex(settings.downloadLocation, logger) };
|
||||||
foreach(Connector cConnector in this._connectors)
|
foreach(Connector cConnector in this._connectors)
|
||||||
tasksToExecute.Add(cConnector, new List<TrangaTask>());
|
_taskQueue.Add(cConnector, new List<TrangaTask>());
|
||||||
this.downloadLocation = settings.downloadLocation;
|
_allTasks = new HashSet<TrangaTask>();
|
||||||
this.komga = settings.komga;
|
|
||||||
_allTasks = settings.allTasks;
|
this.settings = settings;
|
||||||
|
ImportData();
|
||||||
|
ExportData();
|
||||||
Thread taskChecker = new(TaskCheckerThread);
|
Thread taskChecker = new(TaskCheckerThread);
|
||||||
taskChecker.Start();
|
taskChecker.Start();
|
||||||
}
|
}
|
||||||
@ -60,13 +76,16 @@ public class TaskManager
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void TaskCheckerThread()
|
private void TaskCheckerThread()
|
||||||
{
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), "Starting TaskCheckerThread.");
|
||||||
while (_continueRunning)
|
while (_continueRunning)
|
||||||
{
|
{
|
||||||
//Check if previous tasks have finished and execute new tasks
|
//Check if previous tasks have finished and execute new tasks
|
||||||
foreach (KeyValuePair<Connector, List<TrangaTask>> connectorTaskQueue in tasksToExecute)
|
foreach (KeyValuePair<Connector, List<TrangaTask>> connectorTaskQueue in _taskQueue)
|
||||||
{
|
{
|
||||||
connectorTaskQueue.Value.RemoveAll(task => task.state == TrangaTask.ExecutionState.Waiting);
|
if(connectorTaskQueue.Value.RemoveAll(task => task.state == TrangaTask.ExecutionState.Waiting) > 0)
|
||||||
if (connectorTaskQueue.Value.Count > 0 && !connectorTaskQueue.Value.All(task => task.state is TrangaTask.ExecutionState.Running or TrangaTask.ExecutionState.Enqueued))
|
ExportData();
|
||||||
|
|
||||||
|
if (connectorTaskQueue.Value.Count > 0 && connectorTaskQueue.Value.All(task => task.state is TrangaTask.ExecutionState.Enqueued))
|
||||||
ExecuteTaskNow(connectorTaskQueue.Value.First());
|
ExecuteTaskNow(connectorTaskQueue.Value.First());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +99,7 @@ public class TaskManager
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Task due: {task}");
|
logger?.WriteLine(this.GetType().ToString(), $"Task due: {task}");
|
||||||
tasksToExecute[GetConnector(task.connectorName!)].Add(task);
|
_taskQueue[GetConnector(task.connectorName!)].Add(task);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Thread.Sleep(1000);
|
Thread.Sleep(1000);
|
||||||
@ -99,7 +118,7 @@ public class TaskManager
|
|||||||
logger?.WriteLine(this.GetType().ToString(), $"Forcing Execution: {task}");
|
logger?.WriteLine(this.GetType().ToString(), $"Forcing Execution: {task}");
|
||||||
Task t = new Task(() =>
|
Task t = new Task(() =>
|
||||||
{
|
{
|
||||||
TaskExecutor.Execute(this, task, this._chapterCollection, logger);
|
TaskExecutor.Execute(this, task, logger);
|
||||||
});
|
});
|
||||||
t.Start();
|
t.Start();
|
||||||
}
|
}
|
||||||
@ -116,9 +135,7 @@ public class TaskManager
|
|||||||
public TrangaTask AddTask(TrangaTask.Task task, string? connectorName, Publication? publication, TimeSpan reoccurrence,
|
public TrangaTask AddTask(TrangaTask.Task task, string? connectorName, Publication? publication, TimeSpan reoccurrence,
|
||||||
string language = "")
|
string language = "")
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Adding new Task");
|
logger?.WriteLine(this.GetType().ToString(), $"Adding new Task {task} {connectorName} {publication?.sortName}");
|
||||||
if (task != TrangaTask.Task.UpdateKomgaLibrary && connectorName is null)
|
|
||||||
throw new ArgumentException($"connectorName can not be null for task {task}");
|
|
||||||
|
|
||||||
TrangaTask newTask;
|
TrangaTask newTask;
|
||||||
if (task == TrangaTask.Task.UpdateKomgaLibrary)
|
if (task == TrangaTask.Task.UpdateKomgaLibrary)
|
||||||
@ -134,6 +151,9 @@ public class TaskManager
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
if(connectorName is null)
|
||||||
|
throw new ArgumentException($"connectorName can not be null for task {task}");
|
||||||
|
|
||||||
//Get appropriate Connector from available Connectors for TrangaTask
|
//Get appropriate Connector from available Connectors for TrangaTask
|
||||||
Connector? connector = _connectors.FirstOrDefault(c => c.name == connectorName);
|
Connector? connector = _connectors.FirstOrDefault(c => c.name == connectorName);
|
||||||
if (connector is null)
|
if (connector is null)
|
||||||
@ -143,15 +163,17 @@ public class TaskManager
|
|||||||
|
|
||||||
//Check if same task already exists
|
//Check if same task already exists
|
||||||
if (!_allTasks.Any(trangaTask => trangaTask.task == task && trangaTask.connectorName == connector.name &&
|
if (!_allTasks.Any(trangaTask => trangaTask.task == task && trangaTask.connectorName == connector.name &&
|
||||||
trangaTask.publication?.downloadUrl == publication?.downloadUrl))
|
trangaTask.publication?.internalId == publication?.internalId))
|
||||||
{
|
{
|
||||||
if(task != TrangaTask.Task.UpdatePublications)
|
if(task != TrangaTask.Task.UpdatePublications)
|
||||||
_chapterCollection.Add((Publication)publication!, new List<Chapter>());
|
_chapterCollection.TryAdd((Publication)publication!, new List<Chapter>());
|
||||||
_allTasks.Add(newTask);
|
_allTasks.Add(newTask);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Publication already exists {publication?.internalId}");
|
||||||
}
|
}
|
||||||
logger?.WriteLine(this.GetType().ToString(), newTask.ToString());
|
logger?.WriteLine(this.GetType().ToString(), $"Added new Task {newTask.ToString()}");
|
||||||
ExportData(Directory.GetCurrentDirectory());
|
ExportData();
|
||||||
|
|
||||||
return newTask;
|
return newTask;
|
||||||
}
|
}
|
||||||
@ -162,18 +184,55 @@ public class TaskManager
|
|||||||
/// <param name="task">TrangaTask.Task type</param>
|
/// <param name="task">TrangaTask.Task type</param>
|
||||||
/// <param name="connectorName">Name of Connector that was used</param>
|
/// <param name="connectorName">Name of Connector that was used</param>
|
||||||
/// <param name="publication">Publication that was used</param>
|
/// <param name="publication">Publication that was used</param>
|
||||||
public void RemoveTask(TrangaTask.Task task, string? connectorName, Publication? publication)
|
public void DeleteTask(TrangaTask.Task task, string? connectorName, Publication? publication)
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Removing Task {task}");
|
logger?.WriteLine(this.GetType().ToString(), $"Removing Task {task} {publication?.sortName}");
|
||||||
if (task == TrangaTask.Task.UpdateKomgaLibrary)
|
if (task == TrangaTask.Task.UpdateKomgaLibrary)
|
||||||
|
{
|
||||||
_allTasks.RemoveWhere(uTask => uTask.task == TrangaTask.Task.UpdateKomgaLibrary);
|
_allTasks.RemoveWhere(uTask => uTask.task == TrangaTask.Task.UpdateKomgaLibrary);
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Removed Task {task} from all Tasks.");
|
||||||
|
}
|
||||||
else if (connectorName is null)
|
else if (connectorName is null)
|
||||||
throw new ArgumentException($"connectorName can not be null for Task {task}");
|
throw new ArgumentException($"connectorName can not be null for Task {task}");
|
||||||
else
|
else
|
||||||
_allTasks.RemoveWhere(trangaTask =>
|
{
|
||||||
|
foreach (List<TrangaTask> taskQueue in this._taskQueue.Values)
|
||||||
|
if(taskQueue.RemoveAll(trangaTask =>
|
||||||
|
trangaTask.task == task && trangaTask.connectorName == connectorName &&
|
||||||
|
trangaTask.publication?.internalId == publication?.internalId) > 0)
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Removed Task {task} {publication?.sortName} {publication?.internalId} from Queue.");
|
||||||
|
else
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Task {task} {publication?.sortName} {publication?.internalId} was not in Queue.");
|
||||||
|
if(_allTasks.RemoveWhere(trangaTask =>
|
||||||
trangaTask.task == task && trangaTask.connectorName == connectorName &&
|
trangaTask.task == task && trangaTask.connectorName == connectorName &&
|
||||||
trangaTask.publication?.downloadUrl == publication?.downloadUrl);
|
trangaTask.publication?.internalId == publication?.internalId) > 0)
|
||||||
ExportData(Directory.GetCurrentDirectory());
|
logger?.WriteLine(this.GetType().ToString(), $"Removed Task {task} {publication?.sortName} {publication?.internalId} from all Tasks.");
|
||||||
|
else
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"No Task {task} {publication?.sortName} {publication?.internalId} could be found.");
|
||||||
|
}
|
||||||
|
ExportData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a Task from the queue
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="task"></param>
|
||||||
|
public void RemoveTaskFromQueue(TrangaTask task)
|
||||||
|
{
|
||||||
|
task.lastExecuted = DateTime.Now;
|
||||||
|
foreach (List<TrangaTask> taskList in this._taskQueue.Values)
|
||||||
|
taskList.Remove(task);
|
||||||
|
task.state = TrangaTask.ExecutionState.Waiting;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets last execution time to start of time
|
||||||
|
/// Let taskManager handle enqueuing
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="task"></param>
|
||||||
|
public void AddTaskToQueue(TrangaTask task)
|
||||||
|
{
|
||||||
|
task.lastExecuted = DateTime.UnixEpoch;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <returns>All available Connectors</returns>
|
/// <returns>All available Connectors</returns>
|
||||||
@ -189,6 +248,17 @@ public class TaskManager
|
|||||||
_allTasks.CopyTo(ret);
|
_allTasks.CopyTo(ret);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Publication[] GetPublicationsFromConnector(Connector connector, string? title = null)
|
||||||
|
{
|
||||||
|
Publication[] ret = connector.GetPublications(title ?? "");
|
||||||
|
foreach (Publication publication in ret)
|
||||||
|
{
|
||||||
|
if(!_chapterCollection.Any(pub => pub.Key.sortName == publication.sortName))
|
||||||
|
this._chapterCollection.TryAdd(publication, new List<Chapter>());
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
/// <returns>All added Publications</returns>
|
/// <returns>All added Publications</returns>
|
||||||
public Publication[] GetAllPublications()
|
public Publication[] GetAllPublications()
|
||||||
@ -201,7 +271,7 @@ public class TaskManager
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="connectorName">Connector-name (exact)</param>
|
/// <param name="connectorName">Connector-name (exact)</param>
|
||||||
/// <exception cref="Exception">If Connector is not available</exception>
|
/// <exception cref="Exception">If Connector is not available</exception>
|
||||||
public Connector GetConnector(string connectorName)
|
public Connector GetConnector(string? connectorName)
|
||||||
{
|
{
|
||||||
if(connectorName is null)
|
if(connectorName is null)
|
||||||
throw new Exception($"connectorName can not be null");
|
throw new Exception($"connectorName can not be null");
|
||||||
@ -219,7 +289,7 @@ public class TaskManager
|
|||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Shutting down (forced={force})");
|
logger?.WriteLine(this.GetType().ToString(), $"Shutting down (forced={force})");
|
||||||
_continueRunning = false;
|
_continueRunning = false;
|
||||||
ExportData(Directory.GetCurrentDirectory());
|
ExportData();
|
||||||
|
|
||||||
if(force)
|
if(force)
|
||||||
Environment.Exit(_allTasks.Count(task => task.state is TrangaTask.ExecutionState.Enqueued or TrangaTask.ExecutionState.Running));
|
Environment.Exit(_allTasks.Count(task => task.state is TrangaTask.ExecutionState.Enqueued or TrangaTask.ExecutionState.Running));
|
||||||
@ -227,50 +297,45 @@ public class TaskManager
|
|||||||
//Wait for tasks to finish
|
//Wait for tasks to finish
|
||||||
while(_allTasks.Any(task => task.state is TrangaTask.ExecutionState.Running or TrangaTask.ExecutionState.Enqueued))
|
while(_allTasks.Any(task => task.state is TrangaTask.ExecutionState.Running or TrangaTask.ExecutionState.Enqueued))
|
||||||
Thread.Sleep(10);
|
Thread.Sleep(10);
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), "Tasks finished. Bye!");
|
||||||
Environment.Exit(0);
|
Environment.Exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private void ImportData()
|
||||||
/// Loads stored data (settings, tasks) from file
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="importFolderPath">working directory, filename has to be data.json</param>
|
|
||||||
public static SettingsData LoadData(string importFolderPath)
|
|
||||||
{
|
{
|
||||||
string importPath = Path.Join(importFolderPath, "data.json");
|
logger?.WriteLine(this.GetType().ToString(), "Importing Data");
|
||||||
if (!File.Exists(importPath))
|
string buffer;
|
||||||
return new SettingsData("", null, new HashSet<TrangaTask>());
|
if (File.Exists(settings.tasksFilePath))
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Importing tasks from {settings.tasksFilePath}");
|
||||||
|
buffer = File.ReadAllText(settings.tasksFilePath);
|
||||||
|
this._allTasks = JsonConvert.DeserializeObject<HashSet<TrangaTask>>(buffer)!;
|
||||||
|
}
|
||||||
|
|
||||||
string toRead = File.ReadAllText(importPath);
|
if (File.Exists(settings.knownPublicationsPath))
|
||||||
SettingsData data = JsonConvert.DeserializeObject<SettingsData>(toRead)!;
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Importing known publications from {settings.knownPublicationsPath}");
|
||||||
return data;
|
buffer = File.ReadAllText(settings.knownPublicationsPath);
|
||||||
|
Publication[] publications = JsonConvert.DeserializeObject<Publication[]>(buffer)!;
|
||||||
|
foreach (Publication publication in publications)
|
||||||
|
this._chapterCollection.TryAdd(publication, new List<Chapter>());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exports data (settings, tasks) to file
|
/// Exports data (settings, tasks) to file
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="exportFolderPath">Folder path, filename will be data.json</param>
|
private void ExportData()
|
||||||
private void ExportData(string exportFolderPath)
|
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Exporting data to data.json");
|
logger?.WriteLine(this.GetType().ToString(), $"Exporting settings to {settings.settingsFilePath}");
|
||||||
SettingsData data = new SettingsData(this.downloadLocation, this.komga, this._allTasks);
|
File.WriteAllText(settings.settingsFilePath, JsonConvert.SerializeObject(settings));
|
||||||
|
|
||||||
string exportPath = Path.Join(exportFolderPath, "data.json");
|
logger?.WriteLine(this.GetType().ToString(), $"Exporting tasks to {settings.tasksFilePath}");
|
||||||
string serializedData = JsonConvert.SerializeObject(data);
|
File.WriteAllText(settings.tasksFilePath, JsonConvert.SerializeObject(this._allTasks));
|
||||||
File.WriteAllText(exportPath, serializedData);
|
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Exporting known publications to {settings.knownPublicationsPath}");
|
||||||
|
File.WriteAllText(settings.knownPublicationsPath, JsonConvert.SerializeObject(this._chapterCollection.Keys.ToArray()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SettingsData
|
|
||||||
{
|
|
||||||
public string downloadLocation { get; set; }
|
|
||||||
public Komga? komga { get; set; }
|
|
||||||
public HashSet<TrangaTask> allTasks { get; }
|
|
||||||
|
|
||||||
public SettingsData(string downloadLocation, Komga? komga, HashSet<TrangaTask> allTasks)
|
|
||||||
{
|
|
||||||
this.downloadLocation = downloadLocation;
|
|
||||||
this.komga = komga;
|
|
||||||
this.allTasks = allTasks;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
31
Tranga/TrangaSettings.cs
Normal file
31
Tranga/TrangaSettings.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Tranga;
|
||||||
|
|
||||||
|
public class TrangaSettings
|
||||||
|
{
|
||||||
|
public string downloadLocation { get; set; }
|
||||||
|
public string workingDirectory { get; set; }
|
||||||
|
[JsonIgnore]public string settingsFilePath => Path.Join(workingDirectory, "settings.json");
|
||||||
|
[JsonIgnore]public string tasksFilePath => Path.Join(workingDirectory, "tasks.json");
|
||||||
|
[JsonIgnore]public string knownPublicationsPath => Path.Join(workingDirectory, "knownPublications.json");
|
||||||
|
public Komga? komga { get; set; }
|
||||||
|
|
||||||
|
public TrangaSettings(string downloadLocation, string workingDirectory, Komga? komga)
|
||||||
|
{
|
||||||
|
this.workingDirectory = workingDirectory;
|
||||||
|
this.downloadLocation = downloadLocation;
|
||||||
|
this.komga = komga;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TrangaSettings LoadSettings(string importFilePath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(importFilePath))
|
||||||
|
return new TrangaSettings(Path.Join(Directory.GetCurrentDirectory(), "Downloads"), Directory.GetCurrentDirectory(), null);
|
||||||
|
|
||||||
|
string toRead = File.ReadAllText(importFilePath);
|
||||||
|
TrangaSettings settings = JsonConvert.DeserializeObject<TrangaSettings>(toRead)!;
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
}
|
@ -56,6 +56,6 @@ public class TrangaTask
|
|||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{task,-20} | {lastExecuted,-20} | {reoccurrence,-12} | {state,-10} | {connectorName,-15} | {publication?.sortName}";
|
return $"{task}, {lastExecuted}, {reoccurrence}, {state} {(connectorName is not null ? $", {connectorName}" : "" )} {(publication is not null ? $", {publication?.sortName}": "")}";
|
||||||
}
|
}
|
||||||
}
|
}
|
101
Website/apiConnector.js
Normal file
101
Website/apiConnector.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
const apiUri = "http://localhost:5177";
|
||||||
|
|
||||||
|
async function GetData(uri){
|
||||||
|
let request = await fetch(uri, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let json = await request.json();
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PostData(uri){
|
||||||
|
fetch(uri, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeleteData(uri){
|
||||||
|
fetch(uri, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function GetAvailableControllers(){
|
||||||
|
var uri = apiUri + "/Tranga/GetAvailableControllers";
|
||||||
|
let json = await GetData(uri);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function GetPublication(connectorName, title){
|
||||||
|
var uri = apiUri + `/Tranga/GetPublicationsFromConnector?connectorName=${connectorName}&title=${title}`;
|
||||||
|
let json = await GetData(uri);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function GetKnownPublications(){
|
||||||
|
var uri = apiUri + "/Tranga/GetKnownPublications";
|
||||||
|
let json = await GetData(uri);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function GetTaskTypes(){
|
||||||
|
var uri = apiUri + "/Tasks/GetTaskTypes";
|
||||||
|
let json = await GetData(uri);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
async function GetRunningTasks(){
|
||||||
|
var uri = apiUri + "/Tranga/GetRunningTasks";
|
||||||
|
let json = await GetData(uri);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function GetDownloadTasks(){
|
||||||
|
var uri = apiUri + "/Tasks/Get?taskType=DownloadNewChapters";
|
||||||
|
let json = await GetData(uri);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function GetSettings(){
|
||||||
|
var uri = apiUri + "/Settings/Get";
|
||||||
|
let json = await GetData(uri);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function GetKomgaTask(){
|
||||||
|
var uri = apiUri + "/Tasks/Get?taskType=UpdateKomgaLibrary";
|
||||||
|
let json = await GetData(uri);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateTask(taskType, reoccurrence, connectorName, publicationId, language){
|
||||||
|
var uri = apiUri + `/Tasks/Create?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}&reoccurrenceTime=${reoccurrence}&language=${language}`;
|
||||||
|
PostData(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StartTask(taskType, connectorName, publicationId){
|
||||||
|
var uri = apiUri + `/Tasks/Start?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}`;
|
||||||
|
PostData(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EnqueueTask(taskType, connectorName, publicationId){
|
||||||
|
var uri = apiUri + `/Queue/Enqueue?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}`;
|
||||||
|
PostData(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpdateSettings(downloadLocation, komgaUrl, komgaAuth){
|
||||||
|
var uri = apiUri + `/Settings/Update?downloadLocation=${downloadLocation}&komgaUrl=${komgaUrl}&komgaAuth=${komgaAuth}`;
|
||||||
|
PostData(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeleteTask(taskType, connectorName, publicationId){
|
||||||
|
var uri = apiUri + `/Tasks/Delete?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}`;
|
||||||
|
DeleteData(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DequeueTask(taskType, connectorName, publicationId){
|
||||||
|
var uri = apiUri + `/Queue/Dequeue?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}`;
|
||||||
|
DeleteData(uri);
|
||||||
|
}
|
99
Website/index.html
Normal file
99
Website/index.html
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Tranga</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<topbar>
|
||||||
|
<titlebox>
|
||||||
|
<img src="media/blahaj.png">
|
||||||
|
<span>Tranga</span>
|
||||||
|
</titlebox>
|
||||||
|
<spacer></spacer>
|
||||||
|
<searchdiv>
|
||||||
|
<input id="searchbox" placeholder="Filter" type="text">
|
||||||
|
</searchdiv>
|
||||||
|
<img id="settingscog" src="media/settings-cogwheel.svg" height="100%" alt="settingscog">
|
||||||
|
</topbar>
|
||||||
|
<viewport>
|
||||||
|
<sidebar>
|
||||||
|
<background-placeholder></background-placeholder>
|
||||||
|
|
||||||
|
<spacer></spacer>
|
||||||
|
<p style="text-align: center">Made with Blåhaj 🦈</p>
|
||||||
|
</sidebar>
|
||||||
|
<content>
|
||||||
|
<div id="addPublication">
|
||||||
|
<p>+</p>
|
||||||
|
</div>
|
||||||
|
<publication>
|
||||||
|
<img src="media/cover.jpg">
|
||||||
|
<publication-information>
|
||||||
|
<connector-name class="pill">MangaDex</connector-name>
|
||||||
|
<publication-name>Tensei Pandemic</publication-name>
|
||||||
|
</publication-information>
|
||||||
|
</publication>
|
||||||
|
</content>
|
||||||
|
|
||||||
|
<popup id="addTaskPopup">
|
||||||
|
<blur-background id="blurBackgroundTaskPopup"></blur-background>
|
||||||
|
<addtask-window>
|
||||||
|
<window-titlebar>
|
||||||
|
<p>Add Task</p>
|
||||||
|
<img id="closePopupImg" src="media/close-x.svg" alt="Close">
|
||||||
|
</window-titlebar>
|
||||||
|
<window-content>
|
||||||
|
<addtask-settings>
|
||||||
|
<addtask-setting><label for="selectReccurrence">Recurrence</label><input id="selectReccurrence" type="time" value="01:00" step="3600"></addtask-setting>
|
||||||
|
<addtask-setting><label for="connectors">Connector</label>
|
||||||
|
<select id="connectors">
|
||||||
|
<option value=""></option>
|
||||||
|
</select>
|
||||||
|
</addtask-setting>
|
||||||
|
<addtask-setting><label for="searchPublicationQuery">Search Title</label><input id="searchPublicationQuery" type="text"></addtask-setting>
|
||||||
|
<input type="submit" value="Search" onclick="NewSearch();">
|
||||||
|
</addtask-settings>
|
||||||
|
<div id="taskSelectOutput"></div>
|
||||||
|
</window-content>
|
||||||
|
</addtask-window>
|
||||||
|
</popup>
|
||||||
|
<popup id="publicationViewerPopup">
|
||||||
|
<blur-background id="blurBackgroundPublicationPopup"></blur-background>
|
||||||
|
<publication-viewer>
|
||||||
|
<img id="pubviewcover" src="media/cover.jpg" alt="cover">
|
||||||
|
<publication-information>
|
||||||
|
<publication-name id="publicationViewerName">Tensei Pandemic</publication-name>
|
||||||
|
<publication-author id="publicationViewerAuthor">Imamura Hinata</publication-author>
|
||||||
|
<publication-description id="publicationViewerDescription">Imamura Hinata is a high school boy with a cute appearance.
|
||||||
|
Since his trauma with the first love, he wanted to be more manly than anybody else. But one day he woke up to something different…
|
||||||
|
The total opposite of his ideal male body!
|
||||||
|
Pandemic love comedy!
|
||||||
|
</publication-description>
|
||||||
|
<publication-delete>Delete Task ❌</publication-delete>
|
||||||
|
<publication-add>Add Task ➕</publication-add>
|
||||||
|
</publication-information>
|
||||||
|
</publication-viewer>
|
||||||
|
</popup>
|
||||||
|
|
||||||
|
</viewport>
|
||||||
|
|
||||||
|
<settingstab id="settingstab">
|
||||||
|
<span class="title">Download Location:</span>
|
||||||
|
<span id="downloadLocation"></span>
|
||||||
|
<komga-settings>
|
||||||
|
<span class="title">Komga</span>
|
||||||
|
<div>Configured: <span id="komgaConfigured">✅❌</span></div>
|
||||||
|
<label for="komgaUrl"></label><input placeholder="URL" id="komgaUrl" type="text">
|
||||||
|
<label for="komgaUsername"></label><input placeholder="Username" id="komgaUsername" type="text">
|
||||||
|
<label for="komgaPassword"></label><input placeholder="Password" id="komgaPassword" type="password">
|
||||||
|
<div><label for="komgaUpdateTime" style="margin-right: 5px;">Update Time</label><input id="komgaUpdateTime" type="time" value="00:01"></div>
|
||||||
|
<input type="submit" value="Update" onclick="UpdateSettingsClick()">
|
||||||
|
</komga-settings>
|
||||||
|
</settingstab>
|
||||||
|
|
||||||
|
<script src="apiConnector.js"></script>
|
||||||
|
<script src="interaction.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
277
Website/interaction.js
Normal file
277
Website/interaction.js
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
const slideInRight = [
|
||||||
|
{ right: "-20rem" },
|
||||||
|
{ right: "0" }
|
||||||
|
];
|
||||||
|
|
||||||
|
const slideInRightTiming = {
|
||||||
|
duration: 200,
|
||||||
|
iterations: 1,
|
||||||
|
fill: "forwards",
|
||||||
|
easing: "ease-out"
|
||||||
|
}
|
||||||
|
|
||||||
|
const slideOutRightTiming = {
|
||||||
|
direction: "reverse",
|
||||||
|
duration: 200,
|
||||||
|
iterations: 1,
|
||||||
|
fill: "forwards",
|
||||||
|
easing: "ease-in"
|
||||||
|
}
|
||||||
|
|
||||||
|
let publications = [];
|
||||||
|
let tasks = [];
|
||||||
|
let toEditId;
|
||||||
|
|
||||||
|
const searchPublicationQuery = document.querySelector("#searchPublicationQuery");
|
||||||
|
const selectPublication = document.querySelector("#taskSelectOutput");
|
||||||
|
const connectorSelect = document.querySelector("#connectors");
|
||||||
|
const settingsTab = document.querySelector("#settingstab");
|
||||||
|
const settingsCog = document.querySelector("#settingscog");
|
||||||
|
const selectRecurrence = document.querySelector("#selectReccurrence");
|
||||||
|
const tasksContent = document.querySelector("content");
|
||||||
|
const addTaskPopup = document.querySelector("#addTaskPopup");
|
||||||
|
const publicationViewerPopup = document.querySelector("#publicationViewerPopup");
|
||||||
|
const publicationViewerWindow = document.querySelector("publication-viewer");
|
||||||
|
const publicationViewerDescription = document.querySelector("#publicationViewerDescription");
|
||||||
|
const publicationViewerName = document.querySelector("#publicationViewerName");
|
||||||
|
const publicationViewerAuthor = document.querySelector("#publicationViewerAuthor");
|
||||||
|
const pubviewcover = document.querySelector("#pubviewcover");
|
||||||
|
const publicationDelete = document.querySelector("publication-delete");
|
||||||
|
const publicationAdd = document.querySelector("publication-add");
|
||||||
|
const closetaskpopup = document.querySelector("#closePopupImg");
|
||||||
|
const settingDownloadLocation = document.querySelector("#downloadLocation");
|
||||||
|
const settingKomgaUrl = document.querySelector("#komgaUrl");
|
||||||
|
const settingKomgaUser = document.querySelector("#komgaUsername");
|
||||||
|
const settingKomgaPass = document.querySelector("#komgaPassword");
|
||||||
|
const settingKomgaTime = document.querySelector("#komgaUpdateTime");
|
||||||
|
const settingKomgaConfigured = document.querySelector("#komgaConfigured");
|
||||||
|
|
||||||
|
|
||||||
|
settingsCog.addEventListener("click", () => OpenSettings());
|
||||||
|
closetaskpopup.addEventListener("click", () => HideAddTaskPopup());
|
||||||
|
document.querySelector("#blurBackgroundTaskPopup").addEventListener("click", () => HideAddTaskPopup());
|
||||||
|
document.querySelector("#blurBackgroundPublicationPopup").addEventListener("click", () => HidePublicationPopup());
|
||||||
|
publicationDelete.addEventListener("click", () => DeleteTaskClick());
|
||||||
|
publicationAdd.addEventListener("click", () => AddTaskClick());
|
||||||
|
|
||||||
|
let availableConnectors;
|
||||||
|
GetAvailableControllers()
|
||||||
|
.then(json => availableConnectors = json)
|
||||||
|
.then(json =>
|
||||||
|
json.forEach(connector => {
|
||||||
|
var option = document.createElement('option');
|
||||||
|
option.value = connector;
|
||||||
|
option.innerText = connector;
|
||||||
|
connectorSelect.appendChild(option);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
searchPublicationQuery.addEventListener("keypress", (event) => {
|
||||||
|
if(event.key === "Enter"){
|
||||||
|
NewSearch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function NewSearch(){
|
||||||
|
//Disable inputs
|
||||||
|
selectRecurrence.disabled = true;
|
||||||
|
connectorSelect.disabled = true;
|
||||||
|
searchPublicationQuery.disabled = true;
|
||||||
|
|
||||||
|
//Empty previous results
|
||||||
|
selectPublication.replaceChildren();
|
||||||
|
GetPublication(connectorSelect.value, searchPublicationQuery.value)
|
||||||
|
.then(json =>
|
||||||
|
json.forEach(publication => {
|
||||||
|
var option = CreatePublication(publication, connectorSelect.value);
|
||||||
|
option.addEventListener("click", (mouseEvent) => {
|
||||||
|
ShowPublicationViewerWindow(publication.internalId, mouseEvent, true);
|
||||||
|
});
|
||||||
|
selectPublication.appendChild(option);
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.then(() => {
|
||||||
|
//Re-enable inputs
|
||||||
|
selectRecurrence.disabled = false;
|
||||||
|
connectorSelect.disabled = false;
|
||||||
|
searchPublicationQuery.disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//Returns a new "Publication" Item to display in the tasks section
|
||||||
|
function CreatePublication(publication, connector){
|
||||||
|
var publicationElement = document.createElement('publication');
|
||||||
|
publicationElement.setAttribute("id", publication.internalId);
|
||||||
|
var img = document.createElement('img');
|
||||||
|
img.src = publication.posterUrl;
|
||||||
|
publicationElement.appendChild(img);
|
||||||
|
var info = document.createElement('publication-information');
|
||||||
|
var connectorName = document.createElement('connector-name');
|
||||||
|
connectorName.innerText = connector;
|
||||||
|
connectorName.className = "pill";
|
||||||
|
info.appendChild(connectorName);
|
||||||
|
var publicationName = document.createElement('publication-name');
|
||||||
|
publicationName.innerText = publication.sortName;
|
||||||
|
info.appendChild(publicationName);
|
||||||
|
publicationElement.appendChild(info);
|
||||||
|
if(publications.filter(pub => pub.internalId === publication.internalId) < 1)
|
||||||
|
publications.push(publication);
|
||||||
|
return publicationElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeleteTaskClick(){
|
||||||
|
taskToDelete = tasks.filter(tTask => tTask.publication.internalId === toEditId)[0];
|
||||||
|
DeleteTask("DownloadNewChapters", taskToDelete.connectorName, toEditId);
|
||||||
|
HidePublicationPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
function AddTaskClick(){
|
||||||
|
CreateTask("DownloadNewChapters", selectRecurrence.value, connectorSelect.value, toEditId, "en")
|
||||||
|
HideAddTaskPopup();
|
||||||
|
HidePublicationPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
let slideIn = true;
|
||||||
|
function slide() {
|
||||||
|
if (slideIn)
|
||||||
|
settingsTab.animate(slideInRight, slideInRightTiming);
|
||||||
|
else
|
||||||
|
settingsTab.animate(slideInRight, slideOutRightTiming);
|
||||||
|
slideIn = !slideIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResetContent(){
|
||||||
|
//Delete everything
|
||||||
|
tasksContent.replaceChildren();
|
||||||
|
|
||||||
|
//Add "Add new Task" Button
|
||||||
|
var add = document.createElement("div");
|
||||||
|
add.setAttribute("id", "addPublication")
|
||||||
|
var plus = document.createElement("p");
|
||||||
|
plus.innerText = "+";
|
||||||
|
add.appendChild(plus);
|
||||||
|
add.addEventListener("click", () => ShowNewTaskWindow());
|
||||||
|
tasksContent.appendChild(add);
|
||||||
|
}
|
||||||
|
function ShowPublicationViewerWindow(publicationId, event, add){
|
||||||
|
//Set position to mouse-position
|
||||||
|
publicationViewerWindow.style.top = `${event.clientY - 60}px`;
|
||||||
|
publicationViewerWindow.style.left = `${event.clientX}px`;
|
||||||
|
|
||||||
|
//Edit information inside the window
|
||||||
|
var publication = publications.filter(pub => pub.internalId === publicationId)[0];
|
||||||
|
publicationViewerName.innerText = publication.sortName;
|
||||||
|
publicationViewerDescription.innerText = publication.description;
|
||||||
|
publicationViewerAuthor.innerText = publication.author;
|
||||||
|
pubviewcover.src = publication.posterUrl;
|
||||||
|
toEditId = publicationId;
|
||||||
|
|
||||||
|
//Check what action should be listed
|
||||||
|
if(add){
|
||||||
|
publicationAdd.style.display = "block";
|
||||||
|
publicationDelete.style.display = "none";
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
publicationAdd.style.display = "none";
|
||||||
|
publicationDelete.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
//Show popup
|
||||||
|
publicationViewerPopup.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
function HidePublicationPopup(){
|
||||||
|
publicationViewerPopup.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
function ShowNewTaskWindow(){
|
||||||
|
selectPublication.replaceChildren();
|
||||||
|
addTaskPopup.style.display = "block";
|
||||||
|
}
|
||||||
|
function HideAddTaskPopup(){
|
||||||
|
addTaskPopup.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const fadeIn = [
|
||||||
|
{ opacity: "0" },
|
||||||
|
{ opacity: "1" }
|
||||||
|
];
|
||||||
|
|
||||||
|
const fadeInTiming = {
|
||||||
|
duration: 50,
|
||||||
|
iterations: 1,
|
||||||
|
fill: "forwards"
|
||||||
|
}
|
||||||
|
|
||||||
|
function OpenSettings(){
|
||||||
|
GetSettingsClick();
|
||||||
|
slide();
|
||||||
|
}
|
||||||
|
|
||||||
|
function GetSettingsClick(){
|
||||||
|
settingKomgaUrl.value = "";
|
||||||
|
settingKomgaUser.value = "";
|
||||||
|
settingKomgaPass.value = "";
|
||||||
|
|
||||||
|
GetSettings().then(json => {
|
||||||
|
settingDownloadLocation.innerText = json.downloadLocation;
|
||||||
|
if(json.komga != null)
|
||||||
|
settingKomgaUrl.placeholder = json.komga.baseUrl;
|
||||||
|
});
|
||||||
|
|
||||||
|
GetKomgaTask().then(json => {
|
||||||
|
if(json.length > 0)
|
||||||
|
settingKomgaConfigured.innerText = "✅";
|
||||||
|
else
|
||||||
|
settingKomgaConfigured.innerText = "❌";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpdateSettingsClick(){
|
||||||
|
var auth = utf8_to_b64(`${settingKomgaUser.value}:${settingKomgaPass.value}`);
|
||||||
|
console.log(auth);
|
||||||
|
UpdateSettings("", settingKomgaUrl.value, auth);
|
||||||
|
CreateTask("UpdateKomgaLibrary", settingKomgaTime.value, "","","");
|
||||||
|
setTimeout(() => GetSettingsClick(), 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function utf8_to_b64( str ) {
|
||||||
|
return window.btoa(unescape(encodeURIComponent( str )));
|
||||||
|
}
|
||||||
|
|
||||||
|
//Resets the tasks shown
|
||||||
|
ResetContent();
|
||||||
|
//Get Tasks and show them
|
||||||
|
GetDownloadTasks()
|
||||||
|
.then(json => json.forEach(task => {
|
||||||
|
var publication = CreatePublication(task.publication, task.connectorName);
|
||||||
|
publication.addEventListener("click", (event) => ShowPublicationViewerWindow(task.publication.internalId, event, false));
|
||||||
|
tasksContent.appendChild(publication);
|
||||||
|
tasks.push(task);
|
||||||
|
}));
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
//Tasks from API
|
||||||
|
var cTasks = [];
|
||||||
|
GetDownloadTasks()
|
||||||
|
.then(json => json.forEach(task => cTasks.push(task)))
|
||||||
|
.then(() => {
|
||||||
|
//Only update view if tasks-amount has changed
|
||||||
|
if(tasks.length != cTasks.length) {
|
||||||
|
//Resets the tasks shown
|
||||||
|
ResetContent();
|
||||||
|
//Add all currenttasks to view
|
||||||
|
cTasks.forEach(task => {
|
||||||
|
var publication = CreatePublication(task.publication, task.connectorName);
|
||||||
|
publication.addEventListener("click", (event) => ShowPublicationViewerWindow(task.publication.internalId, event, false));
|
||||||
|
tasksContent.appendChild(publication);
|
||||||
|
})
|
||||||
|
|
||||||
|
tasks = cTasks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
}, 1000);
|
BIN
Website/media/blahaj.png
Normal file
BIN
Website/media/blahaj.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 124 KiB |
4
Website/media/close-x.svg
Normal file
4
Website/media/close-x.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.29289 5.29289C5.68342 4.90237 6.31658 4.90237 6.70711 5.29289L12 10.5858L17.2929 5.29289C17.6834 4.90237 18.3166 4.90237 18.7071 5.29289C19.0976 5.68342 19.0976 6.31658 18.7071 6.70711L13.4142 12L18.7071 17.2929C19.0976 17.6834 19.0976 18.3166 18.7071 18.7071C18.3166 19.0976 17.6834 19.0976 17.2929 18.7071L12 13.4142L6.70711 18.7071C6.31658 19.0976 5.68342 19.0976 5.29289 18.7071C4.90237 18.3166 4.90237 17.6834 5.29289 17.2929L10.5858 12L5.29289 6.70711C4.90237 6.31658 4.90237 5.68342 5.29289 5.29289Z" fill="#0F1729"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 804 B |
21
Website/media/settings-cogwheel.svg
Normal file
21
Website/media/settings-cogwheel.svg
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
width="800px" height="800px" viewBox="0 0 93.5 93.5" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M93.5,40.899c0-2.453-1.995-4.447-4.448-4.447H81.98c-0.74-2.545-1.756-5.001-3.035-7.331l4.998-5
|
||||||
|
c0.826-0.827,1.303-1.973,1.303-3.146c0-1.19-0.462-2.306-1.303-3.146L75.67,9.555c-1.613-1.615-4.673-1.618-6.29,0l-5,5
|
||||||
|
c-2.327-1.28-4.786-2.296-7.332-3.037v-7.07C57.048,1.995,55.053,0,52.602,0H40.899c-2.453,0-4.447,1.995-4.447,4.448v7.071
|
||||||
|
c-2.546,0.741-5.005,1.757-7.333,3.037l-5-5c-1.68-1.679-4.609-1.679-6.288,0L9.555,17.83c-1.734,1.734-1.734,4.555,0,6.289
|
||||||
|
l4.999,5c-1.279,2.33-2.295,4.788-3.036,7.333h-7.07C1.995,36.452,0,38.447,0,40.899V52.6c0,2.453,1.995,4.447,4.448,4.447h7.071
|
||||||
|
c0.74,2.545,1.757,5.003,3.036,7.332l-4.998,4.999c-0.827,0.827-1.303,1.974-1.303,3.146c0,1.189,0.462,2.307,1.302,3.146
|
||||||
|
l8.274,8.273c1.614,1.615,4.674,1.619,6.29,0l5-5c2.328,1.279,4.786,2.297,7.333,3.037v7.071c0,2.453,1.995,4.448,4.447,4.448
|
||||||
|
h11.702c2.453,0,4.446-1.995,4.446-4.448V81.98c2.546-0.74,5.005-1.756,7.332-3.037l5,5c1.681,1.68,4.608,1.68,6.288,0
|
||||||
|
l8.275-8.273c1.734-1.734,1.734-4.555,0-6.289l-4.998-5.001c1.279-2.329,2.295-4.787,3.035-7.332h7.071
|
||||||
|
c2.453,0,4.448-1.995,4.448-4.446V40.899z M62.947,46.75c0,8.932-7.266,16.197-16.197,16.197c-8.931,0-16.197-7.266-16.197-16.197
|
||||||
|
c0-8.931,7.266-16.197,16.197-16.197C55.682,30.553,62.947,37.819,62.947,46.75z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
421
Website/style.css
Normal file
421
Website/style.css
Normal file
@ -0,0 +1,421 @@
|
|||||||
|
:root{
|
||||||
|
--background-color: #eee;
|
||||||
|
--second-background-color: #fff;
|
||||||
|
--primary-color: #f5a9b8;
|
||||||
|
--secondary-color: #5bcefa;
|
||||||
|
--accent-color: #fff;
|
||||||
|
--topbar-height: 60px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body{
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
background-placeholder{
|
||||||
|
background-color: var(--second-background-color);
|
||||||
|
opacity: 1;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 0 0 5px 0;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: var(--topbar-height);
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
titlebox {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
margin: 0 0 0 40px;
|
||||||
|
height: 100%;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
titlebox span{
|
||||||
|
font-size: 24pt;
|
||||||
|
font-weight: bold;
|
||||||
|
background: linear-gradient(150deg, var(--primary-color), var(--accent-color));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
titlebox img {
|
||||||
|
height: 100%;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
spacer{
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchdiv{
|
||||||
|
display: block;
|
||||||
|
margin: 0 10px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchbox {
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 14pt;
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settingscog {
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0px 30px;
|
||||||
|
height: calc(100% - 40px);
|
||||||
|
filter: invert(100%) sepia(0%) saturate(7465%) hue-rotate(115deg) brightness(116%) contrast(101%);
|
||||||
|
}
|
||||||
|
|
||||||
|
viewport {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sidebar{
|
||||||
|
position: relative;
|
||||||
|
width: 20rem;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 0 0 5px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
content {
|
||||||
|
position: relative;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 0 10px 10px 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: start;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
settingstab{
|
||||||
|
position: absolute;
|
||||||
|
right: -20rem;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0,0,0,0.5);
|
||||||
|
width: 20rem;
|
||||||
|
height: calc(100% - var(--topbar-height) - 40px);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 5px 0 0 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
settingstab > * {
|
||||||
|
margin: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
settingstab .title {
|
||||||
|
font-size: 14pt;
|
||||||
|
font-weight: bolder;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
komga-settings {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
komga-settings > * {
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
komga-settings input {
|
||||||
|
padding: 3px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#addPublication {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
width: 180px;
|
||||||
|
height: 300px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 10px 10px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#addPublication p{
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 150pt;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 300px;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill {
|
||||||
|
flex-grow: 0;
|
||||||
|
height: 14pt;
|
||||||
|
font-size: 12pt;
|
||||||
|
border-radius: 9pt;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
padding: 2pt 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication{
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
width: 180px;
|
||||||
|
height: 300px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 10px 10px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication::after{
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0; top: 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
background: linear-gradient(rgba(0, 0, 0, 0.5),rgba(0, 0, 0, 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-information {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-information * {
|
||||||
|
z-index: 1;
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
connector-name{
|
||||||
|
width: fit-content;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-name{
|
||||||
|
width: fit-content;
|
||||||
|
font-size: 16pt;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication img {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
popup{
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
blur-background {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
background-color: black;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
addtask-window {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
position: absolute;
|
||||||
|
left: 12.5%;
|
||||||
|
top: 15%;
|
||||||
|
width: 75%;
|
||||||
|
min-height: 70%;
|
||||||
|
max-height: 80%;
|
||||||
|
padding: 0;
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
z-index: 5;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
window-titlebar {
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-radius: 5px 5px 0 0;
|
||||||
|
color: var(--accent-color);
|
||||||
|
display: flex block;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
window-titlebar p {
|
||||||
|
margin: 0 30px;
|
||||||
|
font-size: 14pt;
|
||||||
|
font-weight: bolder;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
window-titlebar #closePopupImg {
|
||||||
|
height: 70%;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 20px;
|
||||||
|
filter: invert(100%) sepia(0%) saturate(100%) hue-rotate(115deg) brightness(116%) contrast(101%);
|
||||||
|
}
|
||||||
|
|
||||||
|
window-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px 5%;
|
||||||
|
overflow-x: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
addtask-settings{
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
addtask-settings select, addtask-settings input{
|
||||||
|
padding: 5px;
|
||||||
|
font-size: 10pt;
|
||||||
|
border: 1px solid rgba(0,0,0,0.2);
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: transparent;
|
||||||
|
margin: 10px 0;
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
addtask-settings label {
|
||||||
|
font-weight: bolder;
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
addtask-settings addtask-setting{
|
||||||
|
margin: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#taskSelectOutput{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: start;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
#publicationViewerPopup{
|
||||||
|
z-index: 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer{
|
||||||
|
display: block;
|
||||||
|
width: 500px;
|
||||||
|
height: 300px;
|
||||||
|
position: absolute;
|
||||||
|
top: 200px;
|
||||||
|
left: 400px;
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer{
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer::after{
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0; top: 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
background: rgba(0,0,0,0.8);
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer img {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
min-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 5px;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer publication-information publication-name{
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer publication-information publication-author {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer publication-information publication-author::before {
|
||||||
|
content: "Author: ";
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer publication-information publication-description::before {
|
||||||
|
content: "Description";
|
||||||
|
display: block;
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer publication-information publication-description {
|
||||||
|
font-size: 12pt;
|
||||||
|
margin: 5px 0;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-x: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer publication-information publication-delete {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
right: 0px;
|
||||||
|
color: red;
|
||||||
|
margin: 20px;
|
||||||
|
font-size: 16pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer publication-information publication-add {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
right: 0px;
|
||||||
|
color: limegreen;
|
||||||
|
margin: 20px;
|
||||||
|
font-size: 16pt;
|
||||||
|
}
|
Reference in New Issue
Block a user