Trash everything and writing everything from scratch

This commit is contained in:
glax 2023-08-01 18:21:29 +02:00
parent a4f67c9ab4
commit 675effd317
47 changed files with 294 additions and 3862 deletions

View File

@ -42,17 +42,4 @@ jobs:
pull: true
push: true
tags: |
glax/tranga-api:cuttingedge
# https://github.com/docker/build-push-action#multi-platform-image
- name: Build and push Website
uses: docker/build-push-action@v4.1.1
with:
context: ./Website
file: ./Website/Dockerfile
#platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
platforms: linux/amd64
pull: true
push: true
tags: |
glax/tranga-website:cuttingedge
glax/tranga-api:cuttingedge

View File

@ -44,17 +44,4 @@ jobs:
pull: true
push: true
tags: |
glax/tranga-api:latest
# https://github.com/docker/build-push-action#multi-platform-image
- name: Build and push Website
uses: docker/build-push-action@v4.1.1
with:
context: ./Website
file: ./Website/Dockerfile
#platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
platforms: linux/amd64
pull: true
push: true
tags: |
glax/tranga-website:latest
glax/tranga-api:latest

View File

@ -1,365 +0,0 @@
using System.Globalization;
using System.Net;
using System.Text.RegularExpressions;
using Tranga;
using Tranga.Connectors;
using Tranga.TrangaTasks;
namespace Tranga.API;
public class RequestHandler
{
private TaskManager _taskManager;
private Server _parent;
private List<ValueTuple<HttpMethod, string, string[]>> _validRequestPaths = new()
{
new(HttpMethod.Get, "/", Array.Empty<string>()),
new(HttpMethod.Get, "/Connectors", Array.Empty<string>()),
new(HttpMethod.Get, "/Publications/Known", new[] { "internalId?" }),
new(HttpMethod.Get, "/Publications/FromConnector", new[] { "connectorName", "title" }),
new(HttpMethod.Get, "/Publications/Chapters",
new[] { "connectorName", "internalId", "onlyNew?", "onlyExisting?", "language?" }),
new(HttpMethod.Get, "/Tasks/Types", Array.Empty<string>()),
new(HttpMethod.Post, "/Tasks/CreateMonitorTask",
new[] { "connectorName", "internalId", "reoccurrenceTime", "language?", "ignoreChaptersBelow?" }),
new(HttpMethod.Post, "/Tasks/CreateDownloadChaptersTask",
new[] { "connectorName", "internalId", "chapters", "language?" }),
new(HttpMethod.Get, "/Tasks", new[] { "taskType", "connectorName?", "publicationId?" }),
new(HttpMethod.Delete, "/Tasks", new[] { "taskType", "connectorName?", "searchString?" }),
new(HttpMethod.Get, "/Tasks/Progress",
new[] { "taskType", "connectorName", "publicationId", "chapterSortNumber?" }),
new(HttpMethod.Post, "/Tasks/Start", new[] { "taskType", "connectorName?", "internalId?" }),
new(HttpMethod.Get, "/Tasks/RunningTasks", Array.Empty<string>()),
new(HttpMethod.Get, "/Queue/List", Array.Empty<string>()),
new(HttpMethod.Post, "/Queue/Enqueue", new[] { "taskType", "connectorName?", "publicationId?" }),
new(HttpMethod.Delete, "/Queue/Dequeue", new[] { "taskType", "connectorName?", "publicationId?" }),
new(HttpMethod.Get, "/Settings", Array.Empty<string>()),
new(HttpMethod.Post, "/Settings/Update", new[]
{
"downloadLocation?", "komgaUrl?", "komgaAuth?", "kavitaUrl?", "kavitaUsername?",
"kavitaPassword?", "gotifyUrl?", "gotifyAppToken?", "lunaseaWebhook?"
})
};
public RequestHandler(TaskManager taskManager, Server parent)
{
this._taskManager = taskManager;
this._parent = parent;
}
internal void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
{
string requestPath = request.Url!.LocalPath;
if (requestPath.Contains("favicon"))
{
_parent.SendResponse(HttpStatusCode.NoContent, response);
return;
}
if (!this._validRequestPaths.Any(path => path.Item1.Method == request.HttpMethod && path.Item2 == requestPath))
{
_parent.SendResponse(HttpStatusCode.BadRequest, response);
return;
}
Dictionary<string, string> variables = GetRequestVariables(request.Url!.Query);
object? responseObject = null;
switch (request.HttpMethod)
{
case "GET":
responseObject = this.HandleGet(requestPath, variables);
break;
case "POST":
this.HandlePost(requestPath, variables);
break;
case "DELETE":
this.HandleDelete(requestPath, variables);
break;
}
_parent.SendResponse(HttpStatusCode.OK, response, responseObject);
}
private Dictionary<string, string> GetRequestVariables(string query)
{
Dictionary<string, string> ret = new();
Regex queryRex = new (@"\?{1}&?([A-z0-9-=]+=[A-z0-9-=]+)+(&[A-z0-9-=]+=[A-z0-9-=]+)*");
if (!queryRex.IsMatch(query))
return ret;
query = query.Substring(1);
foreach (string kvpair in query.Split('&').Where(str => str.Length >= 3))
{
string var = kvpair.Split('=')[0];
string val = Regex.Replace(kvpair.Substring(var.Length + 1), "%20", " ");
val = Regex.Replace(val, "%[0-9]{2}", "");
ret.Add(var, val);
}
return ret;
}
private void HandleDelete(string requestPath, Dictionary<string, string> variables)
{
switch (requestPath)
{
case "/Tasks":
variables.TryGetValue("taskType", out string? taskType1);
variables.TryGetValue("connectorName", out string? connectorName1);
variables.TryGetValue("publicationId", out string? publicationId1);
if(taskType1 is null)
return;
try
{
TrangaTask.Task task = Enum.Parse<TrangaTask.Task>(taskType1);
foreach(TrangaTask tTask in _taskManager.GetTasksMatching(task, connectorName1, internalId: publicationId1))
_taskManager.DeleteTask(tTask);
}
catch (ArgumentException)
{
return;
}
break;
case "/Queue/Dequeue":
variables.TryGetValue("taskType", out string? taskType2);
variables.TryGetValue("connectorName", out string? connectorName2);
variables.TryGetValue("publicationId", out string? publicationId2);
if(taskType2 is null)
return;
try
{
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType2);
TrangaTask? task = _taskManager
.GetTasksMatching(pTask, connectorName: connectorName2, internalId: publicationId2).FirstOrDefault();
if (task is null)
return;
_taskManager.RemoveTaskFromQueue(task);
}
catch (ArgumentException)
{
return;
}
break;
}
}
private void HandlePost(string requestPath, Dictionary<string, string> variables)
{
switch (requestPath)
{
case "/Tasks/CreateMonitorTask":
variables.TryGetValue("connectorName", out string? connectorName1);
variables.TryGetValue("internalId", out string? internalId1);
variables.TryGetValue("reoccurrenceTime", out string? reoccurrenceTime1);
variables.TryGetValue("language", out string? language1);
variables.TryGetValue("ignoreChaptersBelow", out string? minChapter);
if (connectorName1 is null || internalId1 is null || reoccurrenceTime1 is null)
return;
Connector? connector1 =
_taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName1).Value;
if (connector1 is null)
return;
Publication? publication1 = _taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == internalId1);
if (!publication1.HasValue)
return;
Publication pPublication1 = (Publication)publication1;
if (minChapter is not null)
pPublication1.ignoreChaptersBelow = float.Parse(minChapter,new NumberFormatInfo() { NumberDecimalSeparator = "." });
_taskManager.AddTask(new MonitorPublicationTask(connectorName1, pPublication1, TimeSpan.Parse(reoccurrenceTime1), language1 ?? "en"));
break;
case "/Tasks/CreateDownloadChaptersTask":
variables.TryGetValue("connectorName", out string? connectorName2);
variables.TryGetValue("internalId", out string? internalId2);
variables.TryGetValue("chapters", out string? chapters);
variables.TryGetValue("language", out string? language2);
if (connectorName2 is null || internalId2 is null || chapters is null)
return;
Connector? connector2 =
_taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName2).Value;
if (connector2 is null)
return;
Publication? publication2 = _taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == internalId2);
if (publication2 is null)
return;
IEnumerable<Chapter> toDownload = connector2.SelectChapters((Publication)publication2, chapters, language2 ?? "en");
foreach(Chapter chapter in toDownload)
_taskManager.AddTask(new DownloadChapterTask(connectorName2, (Publication)publication2, chapter, "en"));
break;
case "/Tasks/Start":
variables.TryGetValue("taskType", out string? taskType1);
variables.TryGetValue("connectorName", out string? connectorName3);
variables.TryGetValue("internalId", out string? internalId3);
if (taskType1 is null)
return;
try
{
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType1);
TrangaTask? task = _taskManager
.GetTasksMatching(pTask, connectorName: connectorName3, internalId: internalId3).FirstOrDefault();
if (task is null)
return;
_taskManager.ExecuteTaskNow(task);
}
catch (ArgumentException)
{
return;
}
break;
case "/Queue/Enqueue":
variables.TryGetValue("taskType", out string? taskType2);
variables.TryGetValue("connectorName", out string? connectorName4);
variables.TryGetValue("publicationId", out string? publicationId);
if (taskType2 is null)
return;
try
{
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType2);
TrangaTask? task = _taskManager
.GetTasksMatching(pTask, connectorName: connectorName4, internalId: publicationId).FirstOrDefault();
if (task is null)
return;
_taskManager.AddTaskToQueue(task);
}
catch (ArgumentException)
{
return;
}
break;
case "/Settings/Update":
variables.TryGetValue("downloadLocation", out string? downloadLocation);
variables.TryGetValue("komgaUrl", out string? komgaUrl);
variables.TryGetValue("komgaAuth", out string? komgaAuth);
variables.TryGetValue("kavitaUrl", out string? kavitaUrl);
variables.TryGetValue("kavitaUsername", out string? kavitaUsername);
variables.TryGetValue("kavitaPassword", out string? kavitaPassword);
variables.TryGetValue("gotifyUrl", out string? gotifyUrl);
variables.TryGetValue("gotifyAppToken", out string? gotifyAppToken);
variables.TryGetValue("lunaseaWebhook", out string? lunaseaWebhook);
if (downloadLocation is not null && downloadLocation.Length > 0)
_taskManager.settings.UpdateSettings(TrangaSettings.UpdateField.DownloadLocation, downloadLocation);
if (komgaUrl is not null && komgaAuth is not null && komgaUrl.Length > 5 && komgaAuth.Length > 0)
_taskManager.settings.UpdateSettings(TrangaSettings.UpdateField.Komga, komgaUrl, komgaAuth);
if (kavitaUrl is not null && kavitaPassword is not null && kavitaUsername is not null && kavitaUrl.Length > 5 &&
kavitaUsername.Length > 0 && kavitaPassword.Length > 0)
_taskManager.settings.UpdateSettings(TrangaSettings.UpdateField.Kavita, kavitaUrl, kavitaUsername,
kavitaPassword);
if (gotifyUrl is not null && gotifyAppToken is not null && gotifyUrl.Length > 5 && gotifyAppToken.Length > 0)
_taskManager.settings.UpdateSettings(TrangaSettings.UpdateField.Gotify, gotifyUrl, gotifyAppToken);
if(lunaseaWebhook is not null && lunaseaWebhook.Length > 5)
_taskManager.settings.UpdateSettings(TrangaSettings.UpdateField.LunaSea, lunaseaWebhook);
break;
}
}
private object? HandleGet(string requestPath, Dictionary<string, string> variables)
{
switch (requestPath)
{
case "/Connectors":
return this._taskManager.GetAvailableConnectors().Keys.ToArray();
case "/Publications/Known":
variables.TryGetValue("internalId", out string? internalId1);
if(internalId1 is null)
return _taskManager.GetAllPublications();
return new [] { _taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == internalId1) };
case "/Publications/FromConnector":
variables.TryGetValue("connectorName", out string? connectorName1);
variables.TryGetValue("title", out string? title);
if (connectorName1 is null || title is null)
return null;
Connector? connector1 = _taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName1).Value;
if (connector1 is null)
return null;
if(title.Length < 4)
return null;
return connector1.GetPublications(ref _taskManager.collection, title);
case "/Publications/Chapters":
string[] yes = { "true", "yes", "1", "y" };
variables.TryGetValue("connectorName", out string? connectorName2);
variables.TryGetValue("internalId", out string? internalId2);
variables.TryGetValue("onlyNew", out string? onlyNew);
variables.TryGetValue("onlyExisting", out string? onlyExisting);
variables.TryGetValue("language", out string? language);
if (connectorName2 is null || internalId2 is null)
return null;
bool newOnly = onlyNew is not null && yes.Contains(onlyNew);
bool existingOnly = onlyExisting is not null && yes.Contains(onlyExisting);
Connector? connector2 = _taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName2).Value;
if (connector2 is null)
return null;
Publication? publication = _taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == internalId2);
if (publication is null)
return null;
if(newOnly)
return connector2.GetNewChaptersList((Publication)publication, language??"en", ref _taskManager.collection).ToArray();
else if (existingOnly)
return _taskManager.GetExistingChaptersList(connector2, (Publication)publication, language ?? "en").ToArray();
else
return connector2.GetChapters((Publication)publication, language??"en");
case "/Tasks/Types":
return Enum.GetNames(typeof(TrangaTask.Task));
case "/Tasks":
variables.TryGetValue("taskType", out string? taskType1);
variables.TryGetValue("connectorName", out string? connectorName3);
variables.TryGetValue("searchString", out string? searchString);
if (taskType1 is null)
return null;
try
{
TrangaTask.Task task = Enum.Parse<TrangaTask.Task>(taskType1);
return _taskManager.GetTasksMatching(task, connectorName:connectorName3, searchString:searchString);
}
catch (ArgumentException)
{
return null;
}
case "/Tasks/Progress":
variables.TryGetValue("taskType", out string? taskType2);
variables.TryGetValue("connectorName", out string? connectorName4);
variables.TryGetValue("publicationId", out string? publicationId);
variables.TryGetValue("chapterNumber", out string? chapterNumber);
if (taskType2 is null || connectorName4 is null || publicationId is null)
return null;
Connector? connector =
_taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName4).Value;
if (connector is null)
return null;
try
{
TrangaTask? task = null;
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType2);
if (pTask is TrangaTask.Task.MonitorPublication)
{
task = _taskManager.GetTasksMatching(pTask, connectorName: connectorName4, internalId: publicationId).FirstOrDefault();
}else if (pTask is TrangaTask.Task.DownloadChapter && chapterNumber is not null)
{
task = _taskManager.GetTasksMatching(pTask, connectorName: connectorName4, internalId: publicationId,
chapterNumber: chapterNumber).FirstOrDefault();
}
if (task is null)
return null;
return task.progress;
}
catch (ArgumentException)
{
return null;
}
case "/Tasks/RunningTasks":
return _taskManager.GetAllTasks().Where(task => task.state is TrangaTask.ExecutionState.Running);
case "/Queue/List":
return _taskManager.GetAllTasks().Where(task => task.state is TrangaTask.ExecutionState.Enqueued).OrderBy(task => task.nextExecution);
case "/Settings":
return _taskManager.settings;
case "/":
default:
return this._validRequestPaths;
}
}
}

View File

@ -1,92 +0,0 @@
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using Logging;
using Newtonsoft.Json;
using Tranga;
namespace Tranga.API;
public class Server
{
private readonly HttpListener _listener = new ();
private readonly RequestHandler _requestHandler;
private readonly TaskManager _taskManager;
internal readonly Logger? logger;
private readonly Regex _validUrl =
new (@"https?:\/\/(www\.)?[-A-z0-9]{1,256}(\.[-a-zA-Z0-9]{1,6})?(:[0-9]{1,5})?(\/{1}[A-z0-9()@:%_\+.~#?&=]+)*\/?");
public Server(int port, TaskManager taskManager, Logger? logger = null)
{
this.logger = logger;
this._taskManager = taskManager;
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
this._listener.Prefixes.Add($"http://*:{port}/");
else
this._listener.Prefixes.Add($"http://localhost:{port}/");
this._requestHandler = new RequestHandler(taskManager, this);
Thread listenThread = new Thread(Listen);
listenThread.Start();
}
private void Listen()
{
this._listener.Start();
foreach (string prefix in this._listener.Prefixes)
this.logger?.WriteLine(this.GetType().ToString(), $"Listening on {prefix}");
while (this._listener.IsListening && _taskManager._continueRunning)
{
HttpListenerContext context = this._listener.GetContextAsync().Result;
Task t = new (() =>
{
HandleContext(context);
});
t.Start();
}
}
private void HandleContext(HttpListenerContext context)
{
HttpListenerRequest request = context.Request;
HttpListenerResponse response = context.Response;
//logger?.WriteLine(this.GetType().ToString(), $"New request: {request.HttpMethod} {request.Url}");
if (!_validUrl.IsMatch(request.Url!.ToString()))
{
SendResponse(HttpStatusCode.BadRequest, response);
return;
}
if (request.HttpMethod == "OPTIONS")
{
SendResponse(HttpStatusCode.OK, response);
}
else
{
_requestHandler.HandleRequest(request, response);
}
}
internal void SendResponse(HttpStatusCode statusCode, HttpListenerResponse response, object? content = null)
{
//logger?.WriteLine(this.GetType().ToString(), $"Sending response: {statusCode}");
response.StatusCode = (int)statusCode;
response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With");
response.AddHeader("Access-Control-Allow-Methods", "GET, POST, DELETE");
response.AddHeader("Access-Control-Max-Age", "1728000");
response.AppendHeader("Access-Control-Allow-Origin", "*");
response.ContentType = "application/json";
try
{
response.OutputStream.Write(content is not null
? Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content))
: Array.Empty<byte>());
response.OutputStream.Close();
}
catch (HttpListenerException)
{
}
}
}

View File

@ -1,125 +0,0 @@
using Logging;
using Newtonsoft.Json;
using Tranga.LibraryManagers;
using Tranga.NotificationManagers;
namespace Tranga;
public class CommonObjects
{
public HashSet<LibraryManager> libraryManagers { get; init; }
public HashSet<NotificationManager> notificationManagers { get; init; }
[JsonIgnore]public Logger? logger { get; set; }
[JsonIgnore]private string settingsFilePath { get; init; }
public CommonObjects(HashSet<LibraryManager>? libraryManagers, HashSet<NotificationManager>? notificationManagers, Logger? logger, string settingsFilePath)
{
this.libraryManagers = libraryManagers??new();
this.notificationManagers = notificationManagers??new();
this.logger = logger;
this.settingsFilePath = settingsFilePath;
}
public static CommonObjects LoadSettings(string settingsFilePath, Logger? logger)
{
if (!File.Exists(settingsFilePath))
return new CommonObjects(null, null, logger, settingsFilePath);
string toRead = File.ReadAllText(settingsFilePath);
TrangaSettings.SettingsJsonObject settings = JsonConvert.DeserializeObject<TrangaSettings.SettingsJsonObject>(
toRead,
new JsonSerializerSettings
{
Converters =
{
new NotificationManager.NotificationManagerJsonConverter(),
new LibraryManager.LibraryManagerJsonConverter()
}
})!;
if(settings.co is null)
return new CommonObjects(null, null, logger, settingsFilePath);
if (logger is not null)
{
settings.co.logger = logger;
foreach (LibraryManager lm in settings.co.libraryManagers)
lm.AddLogger(logger);
foreach(NotificationManager nm in settings.co.notificationManagers)
nm.AddLogger(logger);
}
return settings.co;
}
public void ExportSettings()
{
TrangaSettings.SettingsJsonObject? settings = null;
if (File.Exists(settingsFilePath))
{
bool inUse = true;
while (inUse)
{
try
{
using FileStream stream = new (settingsFilePath, FileMode.Open, FileAccess.Read, FileShare.None);
stream.Close();
inUse = false;
}
catch (IOException)
{
inUse = true;
Thread.Sleep(50);
}
}
string toRead = File.ReadAllText(settingsFilePath);
settings = JsonConvert.DeserializeObject<TrangaSettings.SettingsJsonObject>(toRead,
new JsonSerializerSettings
{
Converters =
{
new NotificationManager.NotificationManagerJsonConverter(),
new LibraryManager.LibraryManagerJsonConverter()
}
});
}
settings = new TrangaSettings.SettingsJsonObject(settings?.ts, this);
File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(settings));
}
public void UpdateSettings(TrangaSettings.UpdateField field, params string[] values)
{
switch (field)
{
case TrangaSettings.UpdateField.Komga:
if (values.Length != 2)
return;
libraryManagers.RemoveWhere(lm => lm.GetType() == typeof(Komga));
libraryManagers.Add(new Komga(values[0], values[1], this.logger));
break;
case TrangaSettings.UpdateField.Kavita:
if (values.Length != 3)
return;
libraryManagers.RemoveWhere(lm => lm.GetType() == typeof(Kavita));
libraryManagers.Add(new Kavita(values[0], values[1], values[2], this.logger));
break;
case TrangaSettings.UpdateField.Gotify:
if (values.Length != 2)
return;
notificationManagers.RemoveWhere(nm => nm.GetType() == typeof(Gotify));
Gotify newGotify = new(values[0], values[1], this.logger);
notificationManagers.Add(newGotify);
newGotify.SendNotification("Success!", "Gotify was added to Tranga!");
break;
case TrangaSettings.UpdateField.LunaSea:
if(values.Length != 1)
return;
notificationManagers.RemoveWhere(nm => nm.GetType() == typeof(LunaSea));
LunaSea newLunaSea = new(values[0], this.logger);
notificationManagers.Add(newLunaSea);
newLunaSea.SendNotification("Success!", "LunaSea was added to Tranga!");
break;
}
ExportSettings();
}
}

View File

@ -3,7 +3,6 @@ using System.IO.Compression;
using System.Net;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using Tranga.TrangaTasks;
using static System.IO.UnixFileMode;
namespace Tranga.Connectors;
@ -12,37 +11,25 @@ namespace Tranga.Connectors;
/// Base-Class for all Connectors
/// Provides some methods to be used by all Connectors, as well as a DownloadClient
/// </summary>
public abstract class Connector
public abstract class Connector : TBaseObject
{
protected CommonObjects commonObjects;
protected TrangaSettings settings { get; }
internal DownloadClient downloadClient { get; init; } = null!;
protected Connector(TrangaSettings settings, CommonObjects commonObjects)
protected Connector(TBaseObject clone) : base(clone)
{
this.settings = settings;
this.commonObjects = commonObjects;
if (!Directory.Exists(settings.coverImageCache))
Directory.CreateDirectory(settings.coverImageCache);
}
public abstract string name { get; } //Name of the Connector (e.g. Website)
public Publication[] GetPublications(ref HashSet<Publication> publicationCollection, string publicationTitle = "")
{
Publication[] ret = GetPublicationsInternal(publicationTitle);
foreach (Publication p in ret)
publicationCollection.Add(p);
return ret;
}
/// <summary>
/// Returns all Publications with the given string.
/// If the string is empty or null, returns all Publication of the Connector
/// </summary>
/// <param name="publicationTitle">Search-Query</param>
/// <returns>Publications matching the query</returns>
protected abstract Publication[] GetPublicationsInternal(string publicationTitle = "");
protected abstract Publication[] GetPublications(string publicationTitle = "");
/// <summary>
/// Returns all Chapters of the publication in the provided language.
@ -62,14 +49,15 @@ public abstract class Connector
/// <returns>List of Chapters that were previously not in collection</returns>
public List<Chapter> GetNewChaptersList(Publication publication, string language, ref HashSet<Publication> collection)
{
Log($"Getting new Chapters for {publication}");
Chapter[] newChapters = this.GetChapters(publication, language);
collection.Add(publication);
NumberFormatInfo decimalPoint = new (){ NumberDecimalSeparator = "." };
commonObjects.logger?.WriteLine(this.GetType().ToString(), "Checking for duplicates");
Log($"Checking for duplicates {publication}");
List<Chapter> newChaptersList = newChapters.Where(nChapter =>
float.Parse(nChapter.chapterNumber, decimalPoint) > publication.ignoreChaptersBelow &&
!nChapter.CheckChapterIsDownloaded(settings.downloadLocation)).ToList();
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"{newChaptersList.Count} new chapters.");
Log($"{newChaptersList.Count} new chapters. {publication}");
return newChaptersList;
}
@ -153,9 +141,8 @@ public abstract class Connector
/// </summary>
/// <param name="publication">Publication that contains Chapter</param>
/// <param name="chapter">Chapter with Images to retrieve</param>
/// <param name="parentTask">Will be used for progress-tracking</param>
/// <param name="cancellationToken"></param>
public abstract HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null);
public abstract HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, CancellationToken? cancellationToken = null);
/// <summary>
/// Copies the already downloaded cover from cache to downloadLocation
@ -163,19 +150,19 @@ public abstract class Connector
/// <param name="publication">Publication to retrieve Cover for</param>
public void CopyCoverFromCacheToDownloadLocation(Publication publication)
{
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Cloning cover {publication.sortName} -> {publication.internalId}");
Log($"Copy cover {publication}");
//Check if Publication already has a Folder and cover
string publicationFolder = publication.CreatePublicationFolder(settings.downloadLocation);
DirectoryInfo dirInfo = new (publicationFolder);
if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover", StringComparison.InvariantCultureIgnoreCase)))
{
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Cover exists {publication.sortName}");
Log($"Cover exists {publication}");
return;
}
string fileInCache = Path.Join(settings.coverImageCache, publication.coverFileNameInCache);
string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(fileInCache).Split('.')[^1]}" );
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Cloning cover {fileInCache} -> {newFilePath}");
Log($"Cloning cover {fileInCache} -> {newFilePath}");
File.Copy(fileInCache, newFilePath, true);
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
File.SetUnixFileMode(newFilePath, GroupRead | GroupWrite | OtherRead | OtherWrite | UserRead | UserWrite);
@ -204,16 +191,15 @@ public abstract class Connector
/// </summary>
/// <param name="imageUrls">List of URLs to download Images from</param>
/// <param name="saveArchiveFilePath">Full path to save archive to (without file ending .cbz)</param>
/// <param name="parentTask">Used for progress tracking</param>
/// <param name="comicInfoPath">Path of the generate Chapter ComicInfo.xml, if it was generated</param>
/// <param name="requestType">RequestType for RateLimits</param>
/// <param name="referrer">Used in http request header</param>
/// <param name="cancellationToken"></param>
protected HttpStatusCode DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, byte requestType, DownloadChapterTask parentTask, string? comicInfoPath = null, string? referrer = null, CancellationToken? cancellationToken = null)
protected HttpStatusCode DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, byte requestType, string? comicInfoPath = null, string? referrer = null, CancellationToken? cancellationToken = null)
{
if (cancellationToken?.IsCancellationRequested ?? false)
return HttpStatusCode.RequestTimeout;
commonObjects.logger?.WriteLine("Connector", $"Downloading Images for {saveArchiveFilePath}");
Log($"Downloading Images for {saveArchiveFilePath}");
//Check if Publication Directory already exists
string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!;
if (!Directory.Exists(directoryPath))
@ -231,11 +217,10 @@ public abstract class Connector
{
string[] split = imageUrl.Split('.');
string extension = split[^1];
commonObjects.logger?.WriteLine("Connector", $"Downloading Image {chapter + 1:000}/{imageUrls.Length:000} {parentTask.publication.sortName} {parentTask.publication.internalId} Vol.{parentTask.chapter.volumeNumber} Ch.{parentTask.chapter.chapterNumber} {parentTask.progress:P2}");
Log($"Downloading image {chapter + 1:000}/{imageUrls.Length:000}"); //TODO
HttpStatusCode status = DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), requestType, referrer);
if ((int)status < 200 || (int)status >= 300)
return status;
parentTask.IncrementProgress(1.0 / imageUrls.Length);
if (cancellationToken?.IsCancellationRequested ?? false)
return HttpStatusCode.RequestTimeout;
}
@ -243,7 +228,7 @@ public abstract class Connector
if(comicInfoPath is not null)
File.Copy(comicInfoPath, Path.Join(tempFolder, "ComicInfo.xml"));
commonObjects.logger?.WriteLine("Connector", $"Creating archive {saveArchiveFilePath}");
Log($"Creating archive {saveArchiveFilePath}");
//ZIP-it and ship-it
ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath);
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
@ -265,7 +250,7 @@ public abstract class Connector
using MemoryStream ms = new();
coverResult.result.CopyTo(ms);
File.WriteAllBytes(saveImagePath, ms.ToArray());
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Saving image to {saveImagePath}");
Log($"Saving cover to {saveImagePath}");
return filename;
}
}

View File

@ -1,9 +1,8 @@
using System.Net;
using Logging;
namespace Tranga;
namespace Tranga.Connectors;
internal class DownloadClient
internal class DownloadClient : TBaseObject
{
private static readonly HttpClient Client = new()
{
@ -12,17 +11,9 @@ internal class DownloadClient
private readonly Dictionary<byte, DateTime> _lastExecutedRateLimit;
private readonly Dictionary<byte, TimeSpan> _rateLimit;
// ReSharper disable once InconsistentNaming
private readonly Logger? logger;
/// <summary>
/// Creates a httpClient
/// </summary>
/// <param name="rateLimitRequestsPerMinute">Rate limits for requests. byte is RequestType, int maximum requests per minute for RequestType</param>
/// <param name="logger"></param>
public DownloadClient(Dictionary<byte, int> rateLimitRequestsPerMinute, Logger? logger)
public DownloadClient(Dictionary<byte, int> rateLimitRequestsPerMinute, TBaseObject clone) : base(clone)
{
this.logger = logger;
_lastExecutedRateLimit = new();
_rateLimit = new();
foreach(KeyValuePair<byte, int> limit in rateLimitRequestsPerMinute)
@ -42,7 +33,7 @@ internal class DownloadClient
_lastExecutedRateLimit.TryAdd(requestType, DateTime.Now.Subtract(value));
else
{
logger?.WriteLine(this.GetType().ToString(), "RequestType not configured for rate-limit.");
Log("RequestType not configured for rate-limit.");
return new RequestResult(HttpStatusCode.NotAcceptable, Stream.Null);
}
@ -65,14 +56,13 @@ internal class DownloadClient
}
catch (HttpRequestException e)
{
logger?.WriteLine(this.GetType().ToString(), e.Message);
logger?.WriteLine(this.GetType().ToString(), $"Waiting {_rateLimit[requestType] * 2}... Retrying.");
Log("Exception:\n\t{0}\n\tWaiting {1} before retrying.", e.Message, _rateLimit[requestType] * 2);
Thread.Sleep(_rateLimit[requestType] * 2);
}
}
if (!response.IsSuccessStatusCode)
{
logger?.WriteLine(this.GetType().ToString(), $"Request-Error {response.StatusCode}: {response.ReasonPhrase}");
Log($"Request-Error {response.StatusCode}: {response.ReasonPhrase}");
return new RequestResult(response.StatusCode, Stream.Null);
}

View File

@ -2,7 +2,6 @@
using System.Net;
using System.Text.Json;
using System.Text.Json.Nodes;
using Tranga.TrangaTasks;
namespace Tranga.Connectors;
public class MangaDex : Connector
@ -18,7 +17,7 @@ public class MangaDex : Connector
Author,
}
public MangaDex(TrangaSettings settings, CommonObjects commonObjects) : base(settings, commonObjects)
public MangaDex(TBaseObject clone) : base(clone)
{
name = "MangaDex";
this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
@ -28,12 +27,12 @@ public class MangaDex : Connector
{(byte)RequestType.AtHomeServer, 40},
{(byte)RequestType.CoverUrl, 250},
{(byte)RequestType.Author, 250}
}, commonObjects.logger);
}, clone);
}
protected override Publication[] GetPublicationsInternal(string publicationTitle = "")
protected override Publication[] GetPublications(string publicationTitle = "")
{
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Publications (title={publicationTitle})");
Log($"Searching Publications. Term=\"{publicationTitle}\"");
const int limit = 100; //How many values we want returned at once
int offset = 0; //"Page"
int total = int.MaxValue; //How many total results are there, is updated on first request
@ -59,7 +58,7 @@ public class MangaDex : Connector
//Loop each Manga and extract information from JSON
foreach (JsonNode? mangeNode in mangaInResult)
{
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting publication data. {++loadedPublicationData}/{total}");
Log($"Getting publication data. {++loadedPublicationData}/{total}");
JsonObject manga = (JsonObject)mangeNode!;
JsonObject attributes = manga["attributes"]!.AsObject();
@ -146,13 +145,13 @@ public class MangaDex : Connector
}
}
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Done getting publications (title={publicationTitle})");
Log($"Retrieved {publications.Count} publications.");
return publications.ToArray();
}
public override Chapter[] GetChapters(Publication publication, string language = "")
{
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Chapters for {publication.sortName} {publication.internalId} (language={language})");
Log($"Getting chapters {publication}");
const int limit = 100; //How many values we want returned at once
int offset = 0; //"Page"
int total = int.MaxValue; //How many total results are there, is updated on first request
@ -199,19 +198,16 @@ public class MangaDex : Connector
}
//Return Chapters ordered by Chapter-Number
NumberFormatInfo chapterNumberFormatInfo = new()
{
NumberDecimalSeparator = "."
};
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Done getting {chapters.Count} Chapters for {publication.internalId}");
NumberFormatInfo chapterNumberFormatInfo = new() { NumberDecimalSeparator = "." };
Log($"Got {chapters.Count} chapters. {publication}");
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
}
public override HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null)
public override HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, CancellationToken? cancellationToken = null)
{
if (cancellationToken?.IsCancellationRequested ?? false)
return HttpStatusCode.RequestTimeout;
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
Log($"Retrieving chapter-info {chapter} {publication}");
//Request URLs for Chapter-Images
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest($"https://api.mangadex.org/at-home/server/{chapter.url}?forcePort443=false'", (byte)RequestType.AtHomeServer);
@ -233,15 +229,15 @@ public class MangaDex : Connector
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
//Download Chapter-Images
return DownloadChapterImages(imageUrls.ToArray(), chapter.GetArchiveFilePath(settings.downloadLocation), (byte)RequestType.AtHomeServer, parentTask, comicInfoPath, cancellationToken:cancellationToken);
return DownloadChapterImages(imageUrls.ToArray(), chapter.GetArchiveFilePath(settings.downloadLocation), (byte)RequestType.AtHomeServer, comicInfoPath, cancellationToken:cancellationToken);
}
private string? GetCoverUrl(string publicationId, string? posterId)
{
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting CoverUrl for {publicationId}");
Log($"Getting CoverUrl for Publication {publicationId}");
if (posterId is null)
{
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"No posterId, aborting");
Log("No cover.");
return null;
}
@ -257,12 +253,13 @@ public class MangaDex : Connector
string fileName = result["data"]!["attributes"]!["fileName"]!.GetValue<string>();
string coverUrl = $"https://uploads.mangadex.org/covers/{publicationId}/{fileName}";
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Got Cover-Url for {publicationId} -> {coverUrl}");
Log($"Cover-Url {publicationId} -> {coverUrl}");
return coverUrl;
}
private List<string> GetAuthors(IEnumerable<string> authorIds)
{
Log("Retrieving authors.");
List<string> ret = new();
foreach (string authorId in authorIds)
{
@ -276,7 +273,7 @@ public class MangaDex : Connector
string authorName = result["data"]!["attributes"]!["name"]!.GetValue<string>();
ret.Add(authorName);
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Got author {authorId} -> {authorName}");
Log($"Got author {authorId} -> {authorName}");
}
return ret;
}

View File

@ -2,7 +2,6 @@
using System.Net;
using System.Text.RegularExpressions;
using HtmlAgilityPack;
using Tranga.TrangaTasks;
namespace Tranga.Connectors;
@ -10,18 +9,18 @@ public class MangaKatana : Connector
{
public override string name { get; }
public MangaKatana(TrangaSettings settings, CommonObjects commonObjects) : base(settings, commonObjects)
public MangaKatana(TBaseObject clone) : base(clone)
{
this.name = "MangaKatana";
this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
{
{1, 60}
}, commonObjects.logger);
}, clone);
}
protected override Publication[] GetPublicationsInternal(string publicationTitle = "")
protected override Publication[] GetPublications(string publicationTitle = "")
{
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Publications (title={publicationTitle})");
Log($"Searching Publications. Term=\"{publicationTitle}\"");
string sanitizedTitle = string.Join('_', Regex.Matches(publicationTitle, "[A-z]*").Where(m => m.Value.Length > 0)).ToLower();
string requestUrl = $"https://mangakatana.com/?search={sanitizedTitle}&search_by=book_name";
DownloadClient.RequestResult requestResult =
@ -38,7 +37,9 @@ public class MangaKatana : Connector
return new [] { ParseSinglePublicationFromHtml(requestResult.result, requestResult.redirectedToUrl.Split('/')[^1]) };
}
return ParsePublicationsFromHtml(requestResult.result);
Publication[] publications = ParsePublicationsFromHtml(requestResult.result);
Log($"Retrieved {publications.Length} publications.");
return publications;
}
private Publication[] ParsePublicationsFromHtml(Stream html)
@ -137,7 +138,7 @@ public class MangaKatana : Connector
public override Chapter[] GetChapters(Publication publication, string language = "")
{
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Chapters for {publication.sortName} {publication.internalId} (language={language})");
Log($"Getting chapters {publication}");
string requestUrl = $"https://mangakatana.com/manga/{publication.publicationId}";
// Leaving this in for verification if the page exists
DownloadClient.RequestResult requestResult =
@ -151,7 +152,7 @@ public class MangaKatana : Connector
NumberDecimalSeparator = "."
};
List<Chapter> chapters = ParseChaptersFromHtml(publication, requestUrl);
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Done getting Chapters for {publication.internalId}");
Log($"Got {chapters.Count} chapters. {publication}");
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
}
@ -180,11 +181,11 @@ public class MangaKatana : Connector
return ret;
}
public override HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null)
public override HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, CancellationToken? cancellationToken = null)
{
if (cancellationToken?.IsCancellationRequested ?? false)
return HttpStatusCode.RequestTimeout;
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
Log($"Retrieving chapter-info {chapter} {publication}");
string requestUrl = chapter.url;
// Leaving this in to check if the page exists
DownloadClient.RequestResult requestResult =
@ -197,7 +198,7 @@ public class MangaKatana : Connector
string comicInfoPath = Path.GetTempFileName();
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
return DownloadChapterImages(imageUrls, chapter.GetArchiveFilePath(settings.downloadLocation), 1, parentTask, comicInfoPath, "https://mangakatana.com/", cancellationToken);
return DownloadChapterImages(imageUrls, chapter.GetArchiveFilePath(settings.downloadLocation), 1, comicInfoPath, "https://mangakatana.com/", cancellationToken);
}
private string[] ParseImageUrlsFromHtml(string mangaUrl)

View File

@ -2,26 +2,25 @@
using System.Net;
using System.Text.RegularExpressions;
using HtmlAgilityPack;
using Tranga.TrangaTasks;
namespace Tranga.Connectors;
public class Manganato : Connector
{
public override string name { get; }
public Manganato(TrangaSettings settings, CommonObjects commonObjects) : base(settings, commonObjects)
public Manganato(TBaseObject clone) : base(clone)
{
this.name = "Manganato";
this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
{
{1, 60}
}, commonObjects.logger);
}, clone);
}
protected override Publication[] GetPublicationsInternal(string publicationTitle = "")
protected override Publication[] GetPublications(string publicationTitle = "")
{
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Publications (title={publicationTitle})");
Log($"Searching Publications. Term=\"{publicationTitle}\"");
string sanitizedTitle = string.Join('_', Regex.Matches(publicationTitle, "[A-z]*")).ToLower();
string requestUrl = $"https://manganato.com/search/story/{sanitizedTitle}";
DownloadClient.RequestResult requestResult =
@ -29,7 +28,9 @@ public class Manganato : Connector
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Publication>();
return ParsePublicationsFromHtml(requestResult.result);
Publication[] publications = ParsePublicationsFromHtml(requestResult.result);
Log($"Retrieved {publications.Length} publications.");
return publications;
}
private Publication[] ParsePublicationsFromHtml(Stream html)
@ -125,7 +126,7 @@ public class Manganato : Connector
public override Chapter[] GetChapters(Publication publication, string language = "")
{
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Chapters for {publication.sortName} {publication.internalId} (language={language})");
Log($"Getting chapters {publication}");
string requestUrl = $"https://chapmanganato.com/{publication.publicationId}";
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, 1);
@ -138,7 +139,7 @@ public class Manganato : Connector
NumberDecimalSeparator = "."
};
List<Chapter> chapters = ParseChaptersFromHtml(publication, requestResult.result);
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Done getting Chapters for {publication.internalId}");
Log($"Got {chapters.Count} chapters. {publication}");
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
}
@ -167,11 +168,11 @@ public class Manganato : Connector
return ret;
}
public override HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null)
public override HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, CancellationToken? cancellationToken = null)
{
if (cancellationToken?.IsCancellationRequested ?? false)
return HttpStatusCode.RequestTimeout;
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
Log($"Retrieving chapter-info {chapter} {publication}");
string requestUrl = chapter.url;
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, 1);
@ -183,7 +184,7 @@ public class Manganato : Connector
string comicInfoPath = Path.GetTempFileName();
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
return DownloadChapterImages(imageUrls, chapter.GetArchiveFilePath(settings.downloadLocation), 1, parentTask, comicInfoPath, "https://chapmanganato.com/", cancellationToken);
return DownloadChapterImages(imageUrls, chapter.GetArchiveFilePath(settings.downloadLocation), 1, comicInfoPath, "https://chapmanganato.com/", cancellationToken);
}
private string[] ParseImageUrlsFromHtml(Stream html)

View File

@ -5,7 +5,6 @@ using System.Xml.Linq;
using HtmlAgilityPack;
using Newtonsoft.Json;
using PuppeteerSharp;
using Tranga.TrangaTasks;
namespace Tranga.Connectors;
@ -15,13 +14,13 @@ public class Mangasee : Connector
private IBrowser? _browser;
private const string ChromiumVersion = "1154303";
public Mangasee(TrangaSettings settings, CommonObjects commonObjects) : base(settings, commonObjects)
public Mangasee(TBaseObject clone) : base(clone)
{
this.name = "Mangasee";
this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
{
{ 1, 60 }
}, commonObjects.logger);
}, clone);
Task d = new Task(DownloadBrowser);
d.Start();
@ -34,31 +33,29 @@ public class Mangasee : Connector
browserFetcher.Remove(rev);
if (!browserFetcher.LocalRevisions().Contains(ChromiumVersion))
{
commonObjects.logger?.WriteLine(this.GetType().ToString(), "Downloading headless browser");
Log("Downloading headless browser");
DateTime last = DateTime.Now.Subtract(TimeSpan.FromSeconds(5));
browserFetcher.DownloadProgressChanged += (_, args) =>
{
double currentBytes = Convert.ToDouble(args.BytesReceived) / Convert.ToDouble(args.TotalBytesToReceive);
if (args.TotalBytesToReceive == args.BytesReceived)
Log("Browser downloaded.");
else if (DateTime.Now > last.AddSeconds(1))
{
commonObjects.logger?.WriteLine(this.GetType().ToString(), "Browser downloaded.");
}
else if (DateTime.Now > last.AddSeconds(5))
{
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Browser download progress: {currentBytes:P2}");
Log($"Browser download progress: {currentBytes:P2}");
last = DateTime.Now;
}
};
if (!browserFetcher.CanDownloadAsync(ChromiumVersion).Result)
{
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Can't download browser version {ChromiumVersion}");
return;
Log($"Can't download browser version {ChromiumVersion}");
throw new Exception();
}
await browserFetcher.DownloadAsync(ChromiumVersion);
}
commonObjects.logger?.WriteLine(this.GetType().ToString(), "Starting browser.");
Log("Starting Browser.");
this._browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
Headless = true,
@ -71,9 +68,9 @@ public class Mangasee : Connector
});
}
protected override Publication[] GetPublicationsInternal(string publicationTitle = "")
protected override Publication[] GetPublications(string publicationTitle = "")
{
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Getting Publications (title={publicationTitle})");
Log($"Searching Publications. Term=\"{publicationTitle}\"");
string requestUrl = $"https://mangasee123.com/_search.php";
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, 1);
@ -98,7 +95,7 @@ public class Mangasee : Connector
queryFiltered = queryFiltered.Where(item => item.Value >= publicationTitle.Split(' ').Length - 1)
.ToDictionary(item => item.Key, item => item.Value);
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Got {queryFiltered.Count} Publications (title={publicationTitle})");
Log($"Retrieved {queryFiltered.Count} publications.");
HashSet<Publication> ret = new();
List<SearchResultItem> orderedFiltered =
@ -111,7 +108,7 @@ public class Mangasee : Connector
downloadClient.MakeRequest($"https://mangasee123.com/manga/{orderedItem.i}", 1);
if ((int)requestResult.statusCode >= 200 || (int)requestResult.statusCode < 300)
{
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Retrieving Publication info: {orderedItem.s} {index++}/{orderedFiltered.Count}");
Log($"Retrieving Publication info: {orderedItem.s} {index++}/{orderedFiltered.Count}");
ret.Add(ParseSinglePublicationFromHtml(requestResult.result, orderedItem.s, orderedItem.i, orderedItem.a));
}
}
@ -216,9 +213,10 @@ public class Mangasee : Connector
public override Chapter[] GetChapters(Publication publication, string language = "")
{
Log($"Getting chapters {publication}");
XDocument doc = XDocument.Load($"https://mangasee123.com/rss/{publication.publicationId}.xml");
XElement[] chapterItems = doc.Descendants("item").ToArray();
List<Chapter> ret = new();
List<Chapter> chapters = new();
foreach (XElement chapter in chapterItems)
{
string volumeNumber = "1";
@ -227,7 +225,7 @@ public class Mangasee : Connector
string url = chapter.Descendants("link").First().Value;
url = url.Replace(Regex.Matches(url,"(-page-[0-9])")[0].ToString(),"");
ret.Add(new Chapter(publication, "", volumeNumber, chapterNumber, url));
chapters.Add(new Chapter(publication, "", volumeNumber, chapterNumber, url));
}
//Return Chapters ordered by Chapter-Number
@ -235,23 +233,23 @@ public class Mangasee : Connector
{
NumberDecimalSeparator = "."
};
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Done getting Chapters for {publication.internalId}");
return ret.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
Log($"Got {chapters.Count} chapters. {publication}");
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
}
public override HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null)
public override HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, CancellationToken? cancellationToken = null)
{
if (cancellationToken?.IsCancellationRequested ?? false)
return HttpStatusCode.RequestTimeout;
while (this._browser is null && !(cancellationToken?.IsCancellationRequested??false))
{
commonObjects.logger?.WriteLine(this.GetType().ToString(), "Waiting for headless browser to download...");
Log("Waiting for headless browser to download...");
Thread.Sleep(1000);
}
if (cancellationToken?.IsCancellationRequested??false)
return HttpStatusCode.RequestTimeout;
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
Log($"Retrieving chapter-info {chapter} {publication}");
IPage page = _browser!.NewPageAsync().Result;
IResponse response = page.GoToAsync(chapter.url).Result;
if (response.Ok)
@ -268,7 +266,7 @@ public class Mangasee : Connector
string comicInfoPath = Path.GetTempFileName();
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
return DownloadChapterImages(urls.ToArray(), chapter.GetArchiveFilePath(settings.downloadLocation), 1, parentTask, comicInfoPath, cancellationToken:cancellationToken);
return DownloadChapterImages(urls.ToArray(), chapter.GetArchiveFilePath(settings.downloadLocation), 1, comicInfoPath, cancellationToken:cancellationToken);
}
return response.Status;
}

View File

@ -1,19 +1,19 @@
using System.Text.Json.Nodes;
using Logging;
using Newtonsoft.Json;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace Tranga.LibraryManagers;
namespace Tranga.LibraryConnectors;
public class Kavita : LibraryManager
public class Kavita : LibraryConnector
{
public Kavita(string baseUrl, string username, string password, Logger? logger) : base(baseUrl, GetToken(baseUrl, username, password), logger, LibraryType.Kavita)
public Kavita(string baseUrl, string username, string password, TBaseObject clone) :
base(baseUrl, GetToken(baseUrl, username, password), LibraryType.Kavita, clone)
{
}
[JsonConstructor]
public Kavita(string baseUrl, string auth, Logger? logger) : base(baseUrl, auth, logger, LibraryType.Kavita)
public Kavita(string baseUrl, string auth, TBaseObject clone) : base(baseUrl, auth, LibraryType.Kavita, clone)
{
}
@ -42,7 +42,7 @@ public class Kavita : LibraryManager
public override void UpdateLibrary()
{
logger?.WriteLine(this.GetType().ToString(), $"Updating Libraries");
Log("Updating libraries.");
foreach (KavitaLibrary lib in GetLibraries())
NetClient.MakePost($"{baseUrl}/api/Library/scan?libraryId={lib.id}", "Bearer", auth, logger);
}
@ -53,17 +53,17 @@ public class Kavita : LibraryManager
/// <returns>Array of KavitaLibrary</returns>
private IEnumerable<KavitaLibrary> GetLibraries()
{
logger?.WriteLine(this.GetType().ToString(), $"Getting Libraries");
Log("Getting libraries.");
Stream data = NetClient.MakeRequest($"{baseUrl}/api/Library", "Bearer", auth, logger);
if (data == Stream.Null)
{
logger?.WriteLine(this.GetType().ToString(), $"No libraries returned");
Log("No libraries returned");
return Array.Empty<KavitaLibrary>();
}
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
if (result is null)
{
logger?.WriteLine(this.GetType().ToString(), $"No libraries returned");
Log("No libraries returned");
return Array.Empty<KavitaLibrary>();
}

View File

@ -1,29 +1,28 @@
using System.Text.Json.Nodes;
using Logging;
using Newtonsoft.Json;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace Tranga.LibraryManagers;
namespace Tranga.LibraryConnectors;
/// <summary>
/// Provides connectivity to Komga-API
/// Can fetch and update libraries
/// </summary>
public class Komga : LibraryManager
public class Komga : LibraryConnector
{
public Komga(string baseUrl, string username, string password, Logger? logger)
: base(baseUrl, Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}")), logger, LibraryType.Komga)
public Komga(string baseUrl, string username, string password, TBaseObject clone)
: base(baseUrl, Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}")), LibraryType.Komga, clone)
{
}
[JsonConstructor]
public Komga(string baseUrl, string auth, Logger? logger) : base(baseUrl, auth, logger, LibraryType.Komga)
public Komga(string baseUrl, string auth, TBaseObject clone) : base(baseUrl, auth, LibraryType.Komga, clone)
{
}
public override void UpdateLibrary()
{
logger?.WriteLine(this.GetType().ToString(), $"Updating Libraries");
Log("Updating libraries.");
foreach (KomgaLibrary lib in GetLibraries())
NetClient.MakePost($"{baseUrl}/api/v1/libraries/{lib.id}/scan", "Basic", auth, logger);
}
@ -34,17 +33,17 @@ public class Komga : LibraryManager
/// <returns>Array of KomgaLibraries</returns>
private IEnumerable<KomgaLibrary> GetLibraries()
{
logger?.WriteLine(this.GetType().ToString(), $"Getting Libraries");
Log("Getting Libraries");
Stream data = NetClient.MakeRequest($"{baseUrl}/api/v1/libraries", "Basic", auth, logger);
if (data == Stream.Null)
{
logger?.WriteLine(this.GetType().ToString(), $"No libraries returned");
Log("No libraries returned");
return Array.Empty<KomgaLibrary>();
}
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
if (result is null)
{
logger?.WriteLine(this.GetType().ToString(), $"No libraries returned");
Log("No libraries returned");
return Array.Empty<KomgaLibrary>();
}

View File

@ -1,12 +1,10 @@
using System.Net;
using System.Net.Http.Headers;
using Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Tranga.LibraryManagers;
namespace Tranga.LibraryConnectors;
public abstract class LibraryManager
public abstract class LibraryConnector : TBaseObject
{
public enum LibraryType : byte
{
@ -19,26 +17,15 @@ public abstract class LibraryManager
public string baseUrl { get; }
// ReSharper disable once MemberCanBeProtected.Global
public string auth { get; } //Base64 encoded, if you use your password everywhere, you have problems
protected Logger? logger;
/// <param name="baseUrl">Base-URL of Komga instance, no trailing slashes(/)</param>
/// <param name="auth">Base64 string of username and password (username):(password)</param>
/// <param name="logger"></param>
/// <param name="libraryType"></param>
protected LibraryManager(string baseUrl, string auth, Logger? logger, LibraryType libraryType)
protected LibraryConnector(string baseUrl, string auth, LibraryType libraryType, TBaseObject clone) : base(clone)
{
this.baseUrl = baseUrl;
this.auth = auth;
this.logger = logger;
this.libraryType = libraryType;
}
public abstract void UpdateLibrary();
public void AddLogger(Logger newLogger)
{
this.logger = newLogger;
}
protected static class NetClient
{
public static Stream MakeRequest(string url, string authScheme, string auth, Logger? logger)
@ -52,7 +39,7 @@ public abstract class LibraryManager
RequestUri = new Uri(url)
};
HttpResponseMessage response = client.Send(requestMessage);
logger?.WriteLine("LibraryManager", $"GET {url} -> {(int)response.StatusCode}: {response.ReasonPhrase}");
logger?.WriteLine("LibraryManager.NetClient", $"GET {url} -> {(int)response.StatusCode}: {response.ReasonPhrase}");
if(response.StatusCode is HttpStatusCode.Unauthorized && response.RequestMessage!.RequestUri!.AbsoluteUri != url)
return MakeRequest(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth, logger);
@ -78,7 +65,7 @@ public abstract class LibraryManager
RequestUri = new Uri(url)
};
HttpResponseMessage response = client.Send(requestMessage);
logger?.WriteLine("LibraryManager", $"POST {url} -> {(int)response.StatusCode}: {response.ReasonPhrase}");
logger?.WriteLine("LibraryManager.NetClient", $"POST {url} -> {(int)response.StatusCode}: {response.ReasonPhrase}");
if(response.StatusCode is HttpStatusCode.Unauthorized && response.RequestMessage!.RequestUri!.AbsoluteUri != url)
return MakePost(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth, logger);
@ -88,34 +75,4 @@ public abstract class LibraryManager
return false;
}
}
public class LibraryManagerJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(LibraryManager));
}
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
if (jo["libraryType"]!.Value<Int64>() == (Int64)LibraryType.Komga)
return jo.ToObject<Komga>(serializer)!;
if (jo["libraryType"]!.Value<Int64>() == (Int64)LibraryType.Kavita)
return jo.ToObject<Kavita>(serializer)!;
throw new Exception();
}
public override bool CanWrite => false;
/// <summary>
/// Don't call this
/// </summary>
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
throw new Exception("Dont call this");
}
}
}

View File

@ -0,0 +1,34 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Tranga.LibraryConnectors;
public class LibraryManagerJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(LibraryConnector));
}
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
if (jo["libraryType"]!.Value<Int64>() == (Int64)LibraryConnector.LibraryType.Komga)
return jo.ToObject<Komga>(serializer)!;
if (jo["libraryType"]!.Value<Int64>() == (Int64)LibraryConnector.LibraryType.Kavita)
return jo.ToObject<Kavita>(serializer)!;
throw new Exception();
}
public override bool CanWrite => false;
/// <summary>
/// Don't call this
/// </summary>
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
throw new Exception("Dont call this");
}
}

View File

@ -1,99 +0,0 @@
using System.Text.Json.Nodes;
using Logging;
using Newtonsoft.Json;
using Tranga.LibraryManagers;
using Tranga.NotificationManagers;
using Tranga.TrangaTasks;
namespace Tranga;
public static class Migrator
{
internal static readonly ushort CurrentVersion = 17;
public static void Migrate(string settingsFilePath, Logger? logger)
{
if (!File.Exists(settingsFilePath))
return;
JsonNode settingsNode = JsonNode.Parse(File.ReadAllText(settingsFilePath))!;
ushort version = settingsNode["version"] is not null
? settingsNode["version"]!.GetValue<ushort>()
: settingsNode["ts"]!["version"]!.GetValue<ushort>();
logger?.WriteLine("Migrator", $"Migrating {version} -> {CurrentVersion}");
switch (version)
{
case 15:
MoveToCommonObjects(settingsFilePath, logger);
TrangaSettings.SettingsJsonObject sjo = JsonConvert.DeserializeObject<TrangaSettings.SettingsJsonObject>(File.ReadAllText(settingsFilePath))!;
RemoveUpdateLibraryTask(sjo.ts!, logger);
break;
case 16:
MoveToCommonObjects(settingsFilePath, logger);
break;
}
TrangaSettings.SettingsJsonObject sjo2 = JsonConvert.DeserializeObject<TrangaSettings.SettingsJsonObject>(
File.ReadAllText(settingsFilePath),
new JsonSerializerSettings
{
Converters =
{
new TrangaTask.TrangaTaskJsonConverter(),
new NotificationManager.NotificationManagerJsonConverter(),
new LibraryManager.LibraryManagerJsonConverter()
}
})!;
sjo2.ts!.version = CurrentVersion;
sjo2.ts!.ExportSettings();
}
private static void RemoveUpdateLibraryTask(TrangaSettings settings, Logger? logger)
{
if (!File.Exists(settings.tasksFilePath))
return;
logger?.WriteLine("Migrator", "Removing old/deprecated UpdateLibraryTasks (v16)");
string tasksJsonString = File.ReadAllText(settings.tasksFilePath);
HashSet<TrangaTask> tasks = JsonConvert.DeserializeObject<HashSet<TrangaTask>>(tasksJsonString,
new JsonSerializerSettings { Converters = { new TrangaTask.TrangaTaskJsonConverter() } })!;
tasks.RemoveWhere(t => t.task == TrangaTask.Task.UpdateLibraries);
File.WriteAllText(settings.tasksFilePath, JsonConvert.SerializeObject(tasks));
}
public static void MoveToCommonObjects(string settingsFilePath, Logger? logger)
{
if (!File.Exists(settingsFilePath))
return;
logger?.WriteLine("Migrator", "Moving Settings to commonObjects-structure (v17)");
JsonNode node = JsonNode.Parse(File.ReadAllText(settingsFilePath))!;
TrangaSettings ts = new(
node["downloadLocation"]!.GetValue<string>(),
node["workingDirectory"]!.GetValue<string>());
JsonArray libraryManagers = node["libraryManagers"]!.AsArray();
logger?.WriteLine("Migrator", $"\tGot {libraryManagers.Count} libraryManagers.");
JsonNode? komgaNode = libraryManagers.FirstOrDefault(lm => lm["libraryType"].GetValue<byte>() == (byte)LibraryManager.LibraryType.Komga);
JsonNode? kavitaNode = libraryManagers.FirstOrDefault(lm => lm["libraryType"].GetValue<byte>() == (byte)LibraryManager.LibraryType.Kavita);
HashSet<LibraryManager> lms = new();
if (komgaNode is not null)
lms.Add(new Komga(komgaNode["baseUrl"]!.GetValue<string>(), komgaNode["auth"]!.GetValue<string>(), null));
if (kavitaNode is not null)
lms.Add(new Kavita(kavitaNode["baseUrl"]!.GetValue<string>(), kavitaNode["auth"]!.GetValue<string>(), null));
JsonArray notificationManagers = node["notificationManagers"]!.AsArray();
logger?.WriteLine("Migrator", $"\tGot {notificationManagers.Count} notificationManagers.");
JsonNode? gotifyNode = notificationManagers.FirstOrDefault(nm =>
nm["notificationManagerType"].GetValue<byte>() == (byte)NotificationManager.NotificationManagerType.Gotify);
JsonNode? lunaSeaNode = notificationManagers.FirstOrDefault(nm =>
nm["notificationManagerType"].GetValue<byte>() == (byte)NotificationManager.NotificationManagerType.LunaSea);
HashSet<NotificationManager> nms = new();
if (gotifyNode is not null)
nms.Add(new Gotify(gotifyNode["endpoint"]!.GetValue<string>(), gotifyNode["appToken"]!.GetValue<string>()));
if (lunaSeaNode is not null)
nms.Add(new LunaSea(lunaSeaNode["id"]!.GetValue<string>()));
CommonObjects co = new (lms, nms, logger, settingsFilePath);
TrangaSettings.SettingsJsonObject sjo = new(ts, co);
File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(sjo));
}
}

View File

@ -1,10 +1,9 @@
using System.Text;
using Logging;
using Newtonsoft.Json;
namespace Tranga.NotificationManagers;
namespace Tranga.NotificationConnectors;
public class Gotify : NotificationManager
public class Gotify : NotificationConnector
{
public string endpoint { get; }
// ReSharper disable once MemberCanBePrivate.Global
@ -12,7 +11,7 @@ public class Gotify : NotificationManager
private readonly HttpClient _client = new();
[JsonConstructor]
public Gotify(string endpoint, string appToken, Logger? logger = null) : base(NotificationManagerType.Gotify, logger)
public Gotify(string endpoint, string appToken, TBaseObject clone) : base(NotificationManagerType.Gotify, clone)
{
this.endpoint = endpoint;
this.appToken = appToken;
@ -20,7 +19,7 @@ public class Gotify : NotificationManager
public override void SendNotification(string title, string notificationText)
{
logger?.WriteLine(this.GetType().ToString(), $"Sending notification: {title} - {notificationText}");
Log($"Sending notification: {title} - {notificationText}");
MessageData message = new(title, notificationText);
HttpRequestMessage request = new(HttpMethod.Post, $"{endpoint}/message");
request.Headers.Add("X-Gotify-Key", this.appToken);

View File

@ -1,24 +1,23 @@
using System.Text;
using Logging;
using Newtonsoft.Json;
namespace Tranga.NotificationManagers;
namespace Tranga.NotificationConnectors;
public class LunaSea : NotificationManager
public class LunaSea : NotificationConnector
{
// ReSharper disable once MemberCanBePrivate.Global
public string id { get; init; }
private readonly HttpClient _client = new();
[JsonConstructor]
public LunaSea(string id, Logger? logger = null) : base(NotificationManagerType.LunaSea, logger)
public LunaSea(string id, TBaseObject clone) : base(NotificationManagerType.LunaSea, clone)
{
this.id = id;
}
public override void SendNotification(string title, string notificationText)
{
logger?.WriteLine(this.GetType().ToString(), $"Sending notification: {title} - {notificationText}");
Log($"Sending notification: {title} - {notificationText}");
MessageData message = new(title, notificationText);
HttpRequestMessage request = new(HttpMethod.Post, $"https://notify.lunasea.app/v1/custom/{id}");
request.Content = new StringContent(JsonConvert.SerializeObject(message, Formatting.None), Encoding.UTF8, "application/json");
@ -26,7 +25,7 @@ public class LunaSea : NotificationManager
if (!response.IsSuccessStatusCode)
{
StreamReader sr = new (response.Content.ReadAsStream());
logger?.WriteLine(this.GetType().ToString(), $"{response.StatusCode}: {sr.ReadToEnd()}");
Log($"{response.StatusCode}: {sr.ReadToEnd()}");
}
}

View File

@ -0,0 +1,15 @@
namespace Tranga.NotificationConnectors;
public abstract class NotificationConnector : TBaseObject
{
public NotificationManagerType notificationManagerType;
protected NotificationConnector(NotificationManagerType notificationManagerType, TBaseObject clone) : base(clone)
{
this.notificationManagerType = notificationManagerType;
}
public enum NotificationManagerType : byte { Gotify = 0, LunaSea = 1 }
public abstract void SendNotification(string title, string notificationText);
}

View File

@ -0,0 +1,34 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Tranga.NotificationConnectors;
public class NotificationManagerJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(NotificationConnector));
}
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue,
JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
if (jo["notificationManagerType"]!.Value<byte>() == (byte)NotificationConnector.NotificationManagerType.Gotify)
return jo.ToObject<Gotify>(serializer)!;
else if (jo["notificationManagerType"]!.Value<byte>() == (byte)NotificationConnector.NotificationManagerType.LunaSea)
return jo.ToObject<LunaSea>(serializer)!;
throw new Exception();
}
public override bool CanWrite => false;
/// <summary>
/// Don't call this
/// </summary>
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
throw new Exception("Dont call this");
}
}

View File

@ -1,56 +0,0 @@
using Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Tranga.NotificationManagers;
public abstract class NotificationManager
{
protected Logger? logger;
public NotificationManagerType notificationManagerType;
protected NotificationManager(NotificationManagerType notificationManagerType, Logger? logger = null)
{
this.notificationManagerType = notificationManagerType;
this.logger = logger;
}
public enum NotificationManagerType : byte { Gotify = 0, LunaSea = 1 }
public abstract void SendNotification(string title, string notificationText);
public void AddLogger(Logger pLogger)
{
this.logger = pLogger;
}
public class NotificationManagerJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(NotificationManager));
}
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue,
JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
if (jo["notificationManagerType"]!.Value<byte>() == (byte)NotificationManagerType.Gotify)
return jo.ToObject<Gotify>(serializer)!;
else if (jo["notificationManagerType"]!.Value<byte>() == (byte)NotificationManagerType.LunaSea)
return jo.ToObject<LunaSea>(serializer)!;
throw new Exception();
}
public override bool CanWrite => false;
/// <summary>
/// Don't call this
/// </summary>
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
throw new Exception("Dont call this");
}
}
}

View File

@ -33,7 +33,7 @@ public struct Publication
public string internalId { get; }
public float ignoreChaptersBelow { get; set; }
private static readonly Regex LegalCharacters = new Regex(@"[A-Z]*[a-z]*[0-9]* *\.*-*,*'*\'*\)*\(*~*!*");
private static readonly Regex LegalCharacters = new (@"[A-Z]*[a-z]*[0-9]* *\.*-*,*'*\'*\)*\(*~*!*");
[JsonConstructor]
public Publication(string sortName, List<string> authors, string? description, Dictionary<string,string> altTitles, string[] tags, string? posterUrl, string? coverFileNameInCache, Dictionary<string,string>? links, int? year, string? originalLanguage, string status, string publicationId, string? folderName = null, float? ignoreChaptersBelow = 0)

68
Tranga/TBaseObject.cs Normal file
View File

@ -0,0 +1,68 @@
using Logging;
using Tranga.LibraryConnectors;
using Tranga.NotificationConnectors;
namespace Tranga;
public class TBaseObject
{
protected Logger? logger { get; init; }
protected TrangaSettings settings { get; init; }
protected HashSet<NotificationConnector> notificationConnectors { get; init; }
protected HashSet<LibraryConnector> libraryConnectors { get; init; }
public TBaseObject(TBaseObject clone)
{
this.logger = clone.logger;
this.settings = clone.settings;
this.notificationConnectors = clone.notificationConnectors;
this.libraryConnectors = clone.libraryConnectors;
}
public TBaseObject(Logger? logger, TrangaSettings settings, HashSet<NotificationConnector> notificationConnectors, HashSet<LibraryConnector> libraryConnectors)
{
this.logger = logger;
this.settings = settings;
this.notificationConnectors = notificationConnectors;
this.libraryConnectors = libraryConnectors;
}
protected void Log(string message)
{
logger?.WriteLine(this.GetType().Name, message);
}
protected void Log(string fStr, params object?[] replace)
{
Log(string.Format(fStr, replace));
}
protected bool IsFileInUse(string filePath)
{
if (!File.Exists(filePath))
return false;
try
{
using FileStream stream = new (filePath, FileMode.Open, FileAccess.Read, FileShare.None);
stream.Close();
return false;
}
catch (IOException)
{
Log($"File is in use {filePath}");
return true;
}
}
protected void SendNotification(string title, string message)
{
foreach(NotificationConnector nc in notificationConnectors)
nc.SendNotification(title, message);
}
protected void UpdateLibraries()
{
foreach (LibraryConnector libraryConnector in libraryConnectors)
libraryConnector.UpdateLibrary();
}
}

View File

@ -1,385 +0,0 @@
using Newtonsoft.Json;
using Tranga.Connectors;
using Tranga.TrangaTasks;
namespace Tranga;
/// <summary>
/// Manages all TrangaTasks.
/// Provides a Threaded environment to execute Tasks, and still manage the Task-Collection
/// </summary>
public class TaskManager
{
public HashSet<Publication> collection = new();
private HashSet<TrangaTask> _allTasks = new();
private readonly Dictionary<TrangaTask, CancellationTokenSource> _runningTasks = new ();
public bool _continueRunning = true;
private readonly Connector[] _connectors;
public TrangaSettings settings { get; }
public CommonObjects commonObjects { get; init; }
public TaskManager(TrangaSettings settings, Logging.Logger? logger)
{
commonObjects = CommonObjects.LoadSettings(settings.settingsFilePath, logger);
commonObjects.logger?.WriteLine(this.GetType().ToString(), value: "\n"+
@"-----------------------------------------------------------------"+"\n"+
@" |¯¯¯¯¯¯|°|¯¯¯¯¯¯\ /¯¯¯¯¯¯| |¯¯¯\|¯¯¯| /¯¯¯¯¯¯\' /¯¯¯¯¯¯| "+"\n"+
@" | | | x <|' / ! | | '| | (/¯¯¯\° / ! | "+ "\n"+
@" ¯|__|¯ |__|\\__\\ /___/¯|_'| |___|\\__| \\_____/' /___/¯|_'| "+ "\n"+
@"-----------------------------------------------------------------");
this._connectors = new Connector[]
{
new MangaDex(settings, commonObjects),
new Manganato(settings, commonObjects),
new Mangasee(settings, commonObjects),
new MangaKatana(settings, commonObjects)
};
this.settings = settings;
ImportData();
ExportDataAndSettings();
Thread taskChecker = new(TaskCheckerThread);
taskChecker.Start();
}
/// <summary>
/// Runs continuously until shutdown.
/// Checks if tasks have to be executed (time elapsed)
/// </summary>
private void TaskCheckerThread()
{
commonObjects.logger?.WriteLine(this.GetType().ToString(), "Starting TaskCheckerThread.");
int waitingTasksCount = _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting);
while (_continueRunning)
{
foreach (TrangaTask waitingButExecute in _allTasks.Where(taskQuery =>
taskQuery.nextExecution < DateTime.Now &&
taskQuery.state is TrangaTask.ExecutionState.Waiting))
{
waitingButExecute.state = TrangaTask.ExecutionState.Enqueued;
}
foreach (TrangaTask enqueuedTask in _allTasks.Where(enqueuedTask => enqueuedTask.state is TrangaTask.ExecutionState.Enqueued).OrderBy(enqueuedTask => enqueuedTask.nextExecution))
{
switch (enqueuedTask.task)
{
case TrangaTask.Task.DownloadChapter:
case TrangaTask.Task.MonitorPublication:
if (!_allTasks.Any(taskQuery =>
{
if (taskQuery.state is not TrangaTask.ExecutionState.Running) return false;
switch (taskQuery)
{
case DownloadChapterTask dct when enqueuedTask is DownloadChapterTask eDct && dct.connectorName == eDct.connectorName:
case MonitorPublicationTask mpt when enqueuedTask is MonitorPublicationTask eMpt && mpt.connectorName == eMpt.connectorName:
return true;
default:
return false;
}
}))
{
ExecuteTaskNow(enqueuedTask);
}
break;
case TrangaTask.Task.UpdateLibraries:
ExecuteTaskNow(enqueuedTask);
break;
}
}
foreach (TrangaTask timedOutTask in _runningTasks.Keys
.Where(taskQuery => taskQuery.lastChange < DateTime.Now.Subtract(TimeSpan.FromMinutes(3))))
{
_runningTasks[timedOutTask].Cancel();
timedOutTask.state = TrangaTask.ExecutionState.Failed;
}
foreach (TrangaTask finishedTask in _allTasks
.Where(taskQuery => taskQuery.state is TrangaTask.ExecutionState.Success).ToArray())
{
if(finishedTask is DownloadChapterTask)
{
DeleteTask(finishedTask);
finishedTask.state = TrangaTask.ExecutionState.Success;
}
else
{
finishedTask.state = TrangaTask.ExecutionState.Waiting;
this._runningTasks.Remove(finishedTask);
}
}
foreach (TrangaTask failedTask in _allTasks.Where(taskQuery =>
taskQuery.state is TrangaTask.ExecutionState.Failed).ToArray())
{
DeleteTask(failedTask);
TrangaTask newTask = failedTask.Clone();
failedTask.parentTask?.AddChildTask(newTask);
AddTask(newTask);
}
if(waitingTasksCount != _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting))
ExportDataAndSettings();
waitingTasksCount = _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting);
Thread.Sleep(1000);
}
}
/// <summary>
/// Forces the execution of a given task
/// </summary>
/// <param name="task">Task to execute</param>
public void ExecuteTaskNow(TrangaTask task)
{
task.state = TrangaTask.ExecutionState.Running;
CancellationTokenSource cToken = new ();
Task t = new(() =>
{
task.Execute(this, cToken.Token);
}, cToken.Token);
_runningTasks.Add(task, cToken);
t.Start();
}
public void AddTask(TrangaTask newTask)
{
switch (newTask.task)
{
case TrangaTask.Task.UpdateLibraries:
//Only one UpdateKomgaLibrary Task
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Replacing old {newTask.task}-Task.");
if (GetTasksMatching(newTask).FirstOrDefault() is { } exists)
_allTasks.Remove(exists);
_allTasks.Add(newTask);
ExportDataAndSettings();
break;
default:
if (!GetTasksMatching(newTask).Any())
{
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Adding new Task {newTask}");
_allTasks.Add(newTask);
ExportDataAndSettings();
}
else
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Task already exists {newTask}");
break;
}
}
public void DeleteTask(TrangaTask removeTask)
{
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Removing Task {removeTask}");
if(_allTasks.Contains(removeTask))
_allTasks.Remove(removeTask);
removeTask.parentTask?.RemoveChildTask(removeTask);
if (_runningTasks.ContainsKey(removeTask))
{
_runningTasks[removeTask].Cancel();
_runningTasks.Remove(removeTask);
}
foreach(TrangaTask childTask in removeTask.childTasks)
DeleteTask(childTask);
ExportDataAndSettings();
}
// ReSharper disable once MemberCanBePrivate.Global
public IEnumerable<TrangaTask> GetTasksMatching(TrangaTask mTask)
{
switch (mTask.task)
{
case TrangaTask.Task.UpdateLibraries:
return GetTasksMatching(TrangaTask.Task.UpdateLibraries);
case TrangaTask.Task.DownloadChapter:
DownloadChapterTask dct = (DownloadChapterTask)mTask;
return GetTasksMatching(TrangaTask.Task.DownloadChapter, connectorName: dct.connectorName,
internalId: dct.publication.internalId, chapterNumber: dct.chapter.chapterNumber);
case TrangaTask.Task.MonitorPublication:
MonitorPublicationTask mpt = (MonitorPublicationTask)mTask;
return GetTasksMatching(TrangaTask.Task.MonitorPublication, connectorName: mpt.connectorName,
internalId: mpt.publication.internalId);
}
return Array.Empty<TrangaTask>();
}
public IEnumerable<TrangaTask> GetTasksMatching(TrangaTask.Task taskType, string? connectorName = null, string? searchString = null, string? internalId = null, string? chapterNumber = null)
{
switch (taskType)
{
case TrangaTask.Task.MonitorPublication:
if(connectorName is null)
return _allTasks.Where(tTask => tTask.task == taskType);
GetConnector(connectorName);//Name check
if (searchString is not null)
{
return _allTasks.Where(mTask =>
mTask is MonitorPublicationTask mpt && mpt.connectorName == connectorName &&
mpt.ToString().Contains(searchString, StringComparison.InvariantCultureIgnoreCase));
}
else if (internalId is not null)
{
return _allTasks.Where(mTask =>
mTask is MonitorPublicationTask mpt && mpt.connectorName == connectorName &&
mpt.publication.internalId == internalId);
}
else
return _allTasks.Where(tTask =>
tTask is MonitorPublicationTask mpt && mpt.connectorName == connectorName);
case TrangaTask.Task.DownloadChapter:
if(connectorName is null)
return _allTasks.Where(tTask => tTask.task == taskType);
GetConnector(connectorName);//Name check
if (searchString is not null)
{
return _allTasks.Where(mTask =>
mTask is DownloadChapterTask dct && dct.connectorName == connectorName &&
dct.ToString().Contains(searchString, StringComparison.InvariantCultureIgnoreCase));
}
else if (internalId is not null && chapterNumber is not null)
{
return _allTasks.Where(mTask =>
mTask is DownloadChapterTask dct && dct.connectorName == connectorName &&
dct.publication.internalId == internalId &&
dct.chapter.chapterNumber == chapterNumber);
}
else
return _allTasks.Where(mTask =>
mTask is DownloadChapterTask dct && dct.connectorName == connectorName);
default:
return Array.Empty<TrangaTask>();
}
}
/// <summary>
/// Removes a Task from the queue
/// </summary>
/// <param name="task"></param>
public void RemoveTaskFromQueue(TrangaTask task)
{
task.lastExecuted = DateTime.Now;
task.state = TrangaTask.ExecutionState.Waiting;
}
/// <summary>
/// Sets last execution time to start of time
/// Let taskManager handle enqueuing
/// </summary>
/// <param name="task"></param>
public void AddTaskToQueue(TrangaTask task)
{
task.lastExecuted = DateTime.UnixEpoch;
}
/// <returns>All available Connectors</returns>
public Dictionary<string, Connector> GetAvailableConnectors()
{
return this._connectors.ToDictionary(connector => connector.name, connector => connector);
}
/// <returns>All TrangaTasks in task-collection</returns>
public TrangaTask[] GetAllTasks()
{
TrangaTask[] ret = new TrangaTask[_allTasks.Count];
_allTasks.CopyTo(ret);
return ret;
}
/// <returns>All added Publications</returns>
public Publication[] GetAllPublications()
{
return this.collection.ToArray();
}
public List<Chapter> GetExistingChaptersList(Connector connector, Publication publication, string language)
{
Chapter[] newChapters = connector.GetChapters(publication, language);
return newChapters.Where(nChapter => nChapter.CheckChapterIsDownloaded(settings.downloadLocation)).ToList();
}
/// <summary>
/// Return Connector with given Name
/// </summary>
/// <param name="connectorName">Connector-name (exact)</param>
/// <exception cref="Exception">If Connector is not available</exception>
public Connector GetConnector(string? connectorName)
{
if(connectorName is null)
throw new Exception($"connectorName can not be null");
Connector? ret = this._connectors.FirstOrDefault(connector => connector.name == connectorName);
if (ret is null)
throw new Exception($"Connector {connectorName} is not an available Connector.");
return ret;
}
/// <summary>
/// Shuts down the taskManager.
/// </summary>
/// <param name="force">If force is true, tasks are aborted.</param>
public void Shutdown(bool force = false)
{
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Shutting down (forced={force})");
_continueRunning = false;
ExportDataAndSettings();
if(force)
Environment.Exit(_allTasks.Count(task => task.state is TrangaTask.ExecutionState.Enqueued or TrangaTask.ExecutionState.Running));
//Wait for tasks to finish
while(_allTasks.Any(task => task.state is TrangaTask.ExecutionState.Running or TrangaTask.ExecutionState.Enqueued))
Thread.Sleep(10);
commonObjects.logger?.WriteLine(this.GetType().ToString(), "Tasks finished. Bye!");
Environment.Exit(0);
}
private void ImportData()
{
commonObjects.logger?.WriteLine(this.GetType().ToString(), "Importing Data");
if (File.Exists(settings.tasksFilePath))
{
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Importing tasks from {settings.tasksFilePath}");
string buffer = File.ReadAllText(settings.tasksFilePath);
this._allTasks = JsonConvert.DeserializeObject<HashSet<TrangaTask>>(buffer, new JsonSerializerSettings() { Converters = { new TrangaTask.TrangaTaskJsonConverter() } })!;
}
foreach (TrangaTask task in this._allTasks.Where(tTask => tTask.parentTaskId is not null).ToArray())
{
TrangaTask? parentTask = this._allTasks.FirstOrDefault(pTask => pTask.taskId == task.parentTaskId);
if (parentTask is not null)
{
this.DeleteTask(task);
parentTask.lastExecuted = DateTime.UnixEpoch;
}
}
}
/// <summary>
/// Exports data (settings, tasks) to file
/// </summary>
private void ExportDataAndSettings()
{
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Exporting settings to {settings.settingsFilePath}");
settings.ExportSettings();
commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Exporting tasks to {settings.tasksFilePath}");
while(IsFileInUse(settings.tasksFilePath))
Thread.Sleep(50);
File.WriteAllText(settings.tasksFilePath, JsonConvert.SerializeObject(this._allTasks));
}
private bool IsFileInUse(string path)
{
if (!File.Exists(path))
return false;
try
{
using FileStream stream = new (path, FileMode.Open, FileAccess.Read, FileShare.None);
stream.Close();
}
catch (IOException)
{
return true;
}
return false;
}
}

View File

@ -1,580 +0,0 @@
using System.Globalization;
using System.Runtime.InteropServices;
using Logging;
using Tranga.API;
using Tranga.Connectors;
using Tranga.NotificationManagers;
using Tranga.TrangaTasks;
namespace Tranga;
public static class Tranga
{
public static void Main(string[] args)
{
bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
string applicationFolderPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Tranga-API");
string downloadFolderPath = isLinux ? "/Manga" : Path.Join(applicationFolderPath, "Manga");
string logsFolderPath = isLinux ? "/var/log/Tranga" : Path.Join(applicationFolderPath, "log");
string logFilePath = Path.Join(logsFolderPath, $"log-{DateTime.Now:dd-M-yyyy-HH-mm-ss}.txt");
string settingsFilePath = Path.Join(applicationFolderPath, "settings.json");
Directory.CreateDirectory(logsFolderPath);
Logger logger = isLinux
? new Logger(new[] { Logger.LoggerType.FileLogger, Logger.LoggerType.ConsoleLogger }, Console.Out, Console.Out.Encoding, logFilePath)
: new Logger(new[] { Logger.LoggerType.FileLogger }, Console.Out, Console.Out.Encoding, logFilePath);
logger.WriteLine("Tranga",value: "\n"+
"-------------------------------------------\n"+
" Starting Tranga-API\n"+
"-------------------------------------------");
logger.WriteLine("Tranga", "Migrating...");
Migrator.Migrate(settingsFilePath, logger);
TrangaSettings settings;
if (File.Exists(settingsFilePath))
{
logger.WriteLine("Tranga", $"Loading settings {settingsFilePath}");
settings = TrangaSettings.LoadSettings(settingsFilePath);
}
else
{
settings = new TrangaSettings(downloadFolderPath, applicationFolderPath);
settings.version = Migrator.CurrentVersion;
}
Directory.CreateDirectory(settings.workingDirectory);
Directory.CreateDirectory(settings.downloadLocation);
Directory.CreateDirectory(settings.coverImageCache);
logger.WriteLine("Tranga", $"Is Linux: {isLinux}");
logger.WriteLine("Tranga",$"Application-Folder: {settings.workingDirectory}");
logger.WriteLine("Tranga",$"Settings-File-Path: {settings.settingsFilePath}");
logger.WriteLine("Tranga",$"Download-Folder-Path: {settings.downloadLocation}");
logger.WriteLine("Tranga",$"Logfile-Path: {logFilePath}");
logger.WriteLine("Tranga",$"Image-Cache-Path: {settings.coverImageCache}");
logger.WriteLine("Tranga", "Loading Taskmanager.");
TaskManager taskManager = new (settings, logger);
Server _ = new (6531, taskManager);
foreach(NotificationManager nm in taskManager.commonObjects.notificationManagers)
nm.SendNotification("Tranga-API", "Started Tranga-API");
if(!isLinux)
TaskMode(taskManager, logger);
}
private static void TaskMode(TaskManager taskManager, Logger logger)
{
ConsoleKey selection = ConsoleKey.EraseEndOfFile;
PrintMenu(taskManager, taskManager.settings.downloadLocation);
while (selection != ConsoleKey.Q)
{
int taskCount = taskManager.GetAllTasks().Length;
int taskRunningCount = taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Running);
int taskEnqueuedCount =
taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Enqueued);
Console.SetCursorPosition(0,1);
Console.WriteLine($"Tasks (Running/Queue/Total)): {taskRunningCount}/{taskEnqueuedCount}/{taskCount}");
if (Console.KeyAvailable)
{
selection = Console.ReadKey().Key;
switch (selection)
{
case ConsoleKey.L:
while (!Console.KeyAvailable)
{
PrintTasks(taskManager.GetAllTasks(), logger);
Console.WriteLine("Press any key.");
Thread.Sleep(500);
}
Console.ReadKey();
break;
case ConsoleKey.C:
CreateTask(taskManager);
Console.WriteLine("Press any key.");
Console.ReadKey();
break;
case ConsoleKey.D:
DeleteTask(taskManager);
Console.WriteLine("Press any key.");
Console.ReadKey();
break;
case ConsoleKey.E:
ExecuteTaskNow(taskManager);
Console.WriteLine("Press any key.");
Console.ReadKey();
break;
case ConsoleKey.S:
SearchTasks(taskManager);
Console.WriteLine("Press any key.");
Console.ReadKey();
break;
case ConsoleKey.R:
while (!Console.KeyAvailable)
{
PrintTasks(
taskManager.GetAllTasks().Where(eTask => eTask.state == TrangaTask.ExecutionState.Running)
.ToArray(), logger);
Console.WriteLine("Press any key.");
Thread.Sleep(500);
}
Console.ReadKey();
break;
case ConsoleKey.K:
while (!Console.KeyAvailable)
{
PrintTasks(
taskManager.GetAllTasks()
.Where(qTask => qTask.state is TrangaTask.ExecutionState.Enqueued)
.ToArray(), logger);
Console.WriteLine("Press any key.");
Thread.Sleep(500);
}
Console.ReadKey();
break;
case ConsoleKey.F:
TailLog(logger);
Console.ReadKey();
break;
case ConsoleKey.G:
RemoveTaskFromQueue(taskManager, logger);
Console.WriteLine("Press any key.");
Console.ReadKey();
break;
case ConsoleKey.B:
AddTaskToQueue(taskManager, logger);
Console.WriteLine("Press any key.");
Console.ReadKey();
break;
case ConsoleKey.M:
AddMangaTaskToQueue(taskManager, logger);
Console.WriteLine("Press any key.");
Console.ReadKey();
break;
}
PrintMenu(taskManager, taskManager.settings.downloadLocation);
}
Thread.Sleep(200);
}
logger.WriteLine("Tranga_CLI", "Exiting.");
Console.Clear();
Console.WriteLine("Exiting.");
if (taskManager.GetAllTasks().Any(task => task.state == TrangaTask.ExecutionState.Running))
{
Console.WriteLine("Force quit (Even with running tasks?) y/N");
selection = Console.ReadKey().Key;
while(selection != ConsoleKey.Y && selection != ConsoleKey.N)
selection = Console.ReadKey().Key;
taskManager.Shutdown(selection == ConsoleKey.Y);
}else
// ReSharper disable once RedundantArgumentDefaultValue Better readability
taskManager.Shutdown(false);
}
private static void PrintMenu(TaskManager taskManager, string folderPath)
{
int taskCount = taskManager.GetAllTasks().Length;
int taskRunningCount = taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Running);
int taskEnqueuedCount =
taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Enqueued);
Console.Clear();
Console.WriteLine($"Download Folder: {folderPath}");
Console.WriteLine($"Tasks (Running/Queue/Total)): {taskRunningCount}/{taskEnqueuedCount}/{taskCount}");
Console.WriteLine();
Console.WriteLine($"{"C: Create Task",-30}{"L: List tasks",-30}{"B: Enqueue Task", -30}");
Console.WriteLine($"{"D: Delete Task",-30}{"S: Search Tasks", -30}{"K: List Task Queue", -30}");
Console.WriteLine($"{"E: Execute Task now",-30}{"R: List Running Tasks", -30}{"G: Remove Task from Queue", -30}");
Console.WriteLine($"{"M: New Download Manga Task",-30}{"", -30}{"", -30}");
Console.WriteLine($"{"",-30}{"F: Show Log",-30}{"Q: Exit",-30}");
}
private static void PrintTasks(TrangaTask[] tasks, Logger? logger)
{
logger?.WriteLine("Tranga_CLI", "Printing Tasks");
int taskCount = tasks.Length;
int taskRunningCount = tasks.Count(task => task.state == TrangaTask.ExecutionState.Running);
int taskEnqueuedCount = tasks.Count(task => task.state == TrangaTask.ExecutionState.Enqueued);
Console.Clear();
int tIndex = 0;
Console.WriteLine($"Tasks (Running/Queue/Total): {taskRunningCount}/{taskEnqueuedCount}/{taskCount}");
string header =
$"{"",-5}{"Task",-20} | {"Last Executed",-20} | {"Reoccurrence",-12} | {"State",-10} | {"Progress",-9} | {"Finished",-20} | {"Remaining",-12} | {"Connector",-15} | Publication/Manga ";
Console.WriteLine(header);
Console.WriteLine(new string('-', header.Length));
foreach (TrangaTask trangaTask in tasks)
{
string[] taskSplit = trangaTask.ToString().Split(", ");
Console.WriteLine($"{tIndex++:000}: {taskSplit[0],-20} | {taskSplit[1],-20} | {taskSplit[2],-12} | {taskSplit[3],-10} | {taskSplit[4],-9} | {taskSplit[5],-20} | {taskSplit[6][..12],-12} | {(taskSplit.Length > 7 ? taskSplit[7] : ""),-15} | {(taskSplit.Length > 8 ? taskSplit[8] : "")} {(taskSplit.Length > 9 ? taskSplit[9] : "")} {(taskSplit.Length > 10 ? taskSplit[10] : "")}");
}
}
private static TrangaTask[] SelectTasks(TrangaTask[] tasks, Logger? logger)
{
logger?.WriteLine("Tranga_CLI", "Menu: Select task");
if (tasks.Length < 1)
{
Console.Clear();
Console.WriteLine("There are no available Tasks.");
logger?.WriteLine("Tranga_CLI", "No available Tasks.");
return Array.Empty<TrangaTask>();
}
PrintTasks(tasks, logger);
logger?.WriteLine("Tranga_CLI", "Selecting Task to Remove (from queue)");
Console.WriteLine("Enter q to abort");
Console.WriteLine($"Select Task(s) (0-{tasks.Length - 1}):");
string? selectedTask = Console.ReadLine();
while(selectedTask is null || selectedTask.Length < 1)
selectedTask = Console.ReadLine();
if (selectedTask.Length == 1 && selectedTask.ToLower() == "q")
{
Console.Clear();
Console.WriteLine("aborted.");
logger?.WriteLine("Tranga_CLI", "aborted");
return Array.Empty<TrangaTask>();
}
if (selectedTask.Contains('-'))
{
int start = Convert.ToInt32(selectedTask.Split('-')[0]);
int end = Convert.ToInt32(selectedTask.Split('-')[1]);
return tasks[start..end];
}
else
{
int selectedTaskIndex = Convert.ToInt32(selectedTask);
return new[] { tasks[selectedTaskIndex] };
}
}
private static void AddMangaTaskToQueue(TaskManager taskManager, Logger logger)
{
Console.Clear();
logger.WriteLine("Tranga_CLI", "Menu: Add Manga Download to queue");
Connector? connector = SelectConnector(taskManager.GetAvailableConnectors().Values.ToArray(), logger);
if (connector is null)
return;
Publication? publication = SelectPublication(taskManager, connector);
if (publication is null)
return;
TimeSpan reoccurrence = SelectReoccurrence(logger);
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
TrangaTask nTask = new MonitorPublicationTask(connector.name, (Publication)publication, reoccurrence, "en");
taskManager.AddTask(nTask);
Console.WriteLine(nTask);
}
private static void AddTaskToQueue(TaskManager taskManager, Logger logger)
{
Console.Clear();
logger.WriteLine("Tranga_CLI", "Menu: Add Task to queue");
TrangaTask[] tasks = taskManager.GetAllTasks().Where(rTask =>
rTask.state is not TrangaTask.ExecutionState.Enqueued and not TrangaTask.ExecutionState.Running).ToArray();
TrangaTask[] selectedTasks = SelectTasks(tasks, logger);
logger.WriteLine("Tranga_CLI", $"Sending {selectedTasks.Length} Tasks to TaskManager");
foreach(TrangaTask task in selectedTasks)
taskManager.AddTaskToQueue(task);
}
private static void RemoveTaskFromQueue(TaskManager taskManager, Logger logger)
{
Console.Clear();
logger.WriteLine("Tranga_CLI", "Menu: Remove Task from queue");
TrangaTask[] tasks = taskManager.GetAllTasks().Where(rTask => rTask.state is TrangaTask.ExecutionState.Enqueued).ToArray();
TrangaTask[] selectedTasks = SelectTasks(tasks, logger);
logger.WriteLine("Tranga_CLI", $"Sending {selectedTasks.Length} Tasks to TaskManager");
foreach(TrangaTask task in selectedTasks)
taskManager.RemoveTaskFromQueue(task);
}
private static void TailLog(Logger logger)
{
logger.WriteLine("Tranga_CLI", "Menu: Show Log-lines");
Console.Clear();
string[] lines = logger.Tail(20);
foreach (string message in lines)
Console.Write(message);
while (!Console.KeyAvailable)
{
string[] newLines = logger.GetNewLines();
foreach(string message in newLines)
Console.Write(message);
Thread.Sleep(40);
}
}
private static void CreateTask(TaskManager taskManager)
{
taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "Menu: Creating Task");
TrangaTask.Task? tmpTask = SelectTaskType(taskManager.commonObjects.logger);
if (tmpTask is null)
return;
TrangaTask.Task task = (TrangaTask.Task)tmpTask;
Connector? connector = null;
if (task != TrangaTask.Task.UpdateLibraries)
{
connector = SelectConnector(taskManager.GetAvailableConnectors().Values.ToArray(), taskManager.commonObjects.logger);
if (connector is null)
return;
}
Publication? publication = null;
if (task != TrangaTask.Task.UpdateLibraries)
{
publication = SelectPublication(taskManager, connector!);
if (publication is null)
return;
}
if (task is TrangaTask.Task.MonitorPublication)
{
TimeSpan reoccurrence = SelectReoccurrence(taskManager.commonObjects.logger);
taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
TrangaTask newTask = new MonitorPublicationTask(connector!.name, (Publication)publication!, reoccurrence, "en");
taskManager.AddTask(newTask);
Console.WriteLine(newTask);
}else if (task is TrangaTask.Task.DownloadChapter)
{
foreach (Chapter chapter in SelectChapters(connector!, (Publication)publication!, taskManager.commonObjects.logger))
{
TrangaTask newTask = new DownloadChapterTask(connector!.name, (Publication)publication, chapter, "en");
taskManager.AddTask(newTask);
Console.WriteLine(newTask);
}
}
}
private static void ExecuteTaskNow(TaskManager taskManager)
{
taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "Menu: Executing Task");
TrangaTask[] tasks = taskManager.GetAllTasks().Where(nTask => nTask.state is not TrangaTask.ExecutionState.Running).ToArray();
TrangaTask[] selectedTasks = SelectTasks(tasks, taskManager.commonObjects.logger);
taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", $"Sending {selectedTasks.Length} Tasks to TaskManager");
foreach(TrangaTask task in selectedTasks)
taskManager.ExecuteTaskNow(task);
}
private static void DeleteTask(TaskManager taskManager)
{
taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "Menu: Delete Task");
TrangaTask[] tasks = taskManager.GetAllTasks();
TrangaTask[] selectedTasks = SelectTasks(tasks, taskManager.commonObjects.logger);
taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", $"Sending {selectedTasks.Length} Tasks to TaskManager");
foreach(TrangaTask task in selectedTasks)
taskManager.DeleteTask(task);
}
private static TrangaTask.Task? SelectTaskType(Logger? logger)
{
logger?.WriteLine("Tranga_CLI", "Menu: Select TaskType");
Console.Clear();
string[] taskNames = Enum.GetNames<TrangaTask.Task>();
int tIndex = 0;
Console.WriteLine("Available Tasks:");
foreach (string taskName in taskNames)
Console.WriteLine($"{tIndex++}: {taskName}");
Console.WriteLine("Enter q to abort");
Console.WriteLine($"Select Task (0-{taskNames.Length - 1}):");
string? selectedTask = Console.ReadLine();
while(selectedTask is null || selectedTask.Length < 1)
selectedTask = Console.ReadLine();
if (selectedTask.Length == 1 && selectedTask.ToLower() == "q")
{
Console.Clear();
Console.WriteLine("aborted.");
logger?.WriteLine("Tranga_CLI", "aborted.");
return null;
}
try
{
int selectedTaskIndex = Convert.ToInt32(selectedTask);
string selectedTaskName = taskNames[selectedTaskIndex];
return Enum.Parse<TrangaTask.Task>(selectedTaskName);
}
catch (Exception e)
{
Console.WriteLine($"Exception: {e.Message}");
logger?.WriteLine("Tranga_CLI", e.Message);
}
return null;
}
private static TimeSpan SelectReoccurrence(Logger? logger)
{
logger?.WriteLine("Tranga_CLI", "Menu: Select Reoccurrence");
Console.WriteLine("Select reoccurrence Timer (Format hh:mm:ss):");
return TimeSpan.Parse(Console.ReadLine()!, new CultureInfo("en-US"));
}
private static Chapter[] SelectChapters(Connector connector, Publication publication, Logger? logger)
{
logger?.WriteLine("Tranga_CLI", "Menu: Select Chapters");
Chapter[] availableChapters = connector.GetChapters(publication, "en");
int cIndex = 0;
Console.WriteLine("Chapters:");
System.Text.StringBuilder sb = new();
foreach(Chapter chapter in availableChapters)
{
sb.Append($"{cIndex++}: ");
if(string.IsNullOrWhiteSpace(chapter.volumeNumber) == false)
{
sb.Append($"Vol.{chapter.volumeNumber} ");
}
if(string.IsNullOrWhiteSpace(chapter.chapterNumber) == false)
{
sb.Append($"Ch.{chapter.chapterNumber} ");
}
if(string.IsNullOrWhiteSpace(chapter.name) == false)
{
sb.Append($" - {chapter.name}");
}
Console.WriteLine(sb.ToString());
sb.Clear();
}
Console.WriteLine("Enter q to abort");
Console.WriteLine($"Select Chapter(s):");
string? selectedChapters = Console.ReadLine();
while(selectedChapters is null || selectedChapters.Length < 1)
selectedChapters = Console.ReadLine();
return connector.SelectChapters(publication, selectedChapters);
}
private static Connector? SelectConnector(Connector[] connectors, Logger? logger)
{
logger?.WriteLine("Tranga_CLI", "Menu: Select Connector");
Console.Clear();
int cIndex = 0;
Console.WriteLine("Connectors:");
foreach (Connector connector in connectors)
Console.WriteLine($"{cIndex++}: {connector.name}");
Console.WriteLine("Enter q to abort");
Console.WriteLine($"Select Connector (0-{connectors.Length - 1}):");
string? selectedConnector = Console.ReadLine();
while(selectedConnector is null || selectedConnector.Length < 1)
selectedConnector = Console.ReadLine();
if (selectedConnector.Length == 1 && selectedConnector.ToLower() == "q")
{
Console.Clear();
Console.WriteLine("aborted.");
logger?.WriteLine("Tranga_CLI", "aborted.");
return null;
}
try
{
int selectedConnectorIndex = Convert.ToInt32(selectedConnector);
return connectors[selectedConnectorIndex];
}
catch (Exception e)
{
Console.WriteLine($"Exception: {e.Message}");
logger?.WriteLine("Tranga_CLI", e.Message);
}
return null;
}
private static Publication? SelectPublication(TaskManager taskManager, Connector connector)
{
taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "Menu: Select Publication");
Console.Clear();
Console.WriteLine($"Connector: {connector.name}");
Console.WriteLine("Publication search query (leave empty for all):");
string? query = Console.ReadLine();
Publication[] publications = connector.GetPublications(ref taskManager.collection, query ?? "");
if (publications.Length < 1)
{
taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "No publications returned");
Console.WriteLine($"No publications for query '{query}' returned;");
return null;
}
int pIndex = 0;
Console.WriteLine("Publications:");
foreach(Publication publication in publications)
Console.WriteLine($"{pIndex++}: {publication.sortName}");
Console.WriteLine("Enter q to abort");
Console.WriteLine($"Select publication to Download (0-{publications.Length - 1}):");
string? selectedPublication = Console.ReadLine();
while(selectedPublication is null || selectedPublication.Length < 1)
selectedPublication = Console.ReadLine();
if (selectedPublication.Length == 1 && selectedPublication.ToLower() == "q")
{
Console.Clear();
Console.WriteLine("aborted.");
taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "aborted.");
return null;
}
try
{
int selectedPublicationIndex = Convert.ToInt32(selectedPublication);
return publications[selectedPublicationIndex];
}
catch (Exception e)
{
Console.WriteLine($"Exception: {e.Message}");
taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", e.Message);
}
return null;
}
private static void SearchTasks(TaskManager taskManager)
{
taskManager.commonObjects.logger?.WriteLine("Tranga_CLI", "Menu: Search task");
Console.Clear();
Console.WriteLine("Enter search query:");
string? query = Console.ReadLine();
while (query is null || query.Length < 4)
query = Console.ReadLine();
PrintTasks(taskManager.GetAllTasks().Where(qTask =>
qTask.ToString().ToLower().Contains(query, StringComparison.OrdinalIgnoreCase)).ToArray(), taskManager.commonObjects.logger);
}
}

View File

@ -1,6 +1,7 @@
using Newtonsoft.Json;
using Tranga.LibraryManagers;
using Tranga.NotificationManagers;
using Logging;
using Newtonsoft.Json;
using Tranga.LibraryConnectors;
using Tranga.NotificationConnectors;
namespace Tranga;
@ -13,30 +14,38 @@ public class TrangaSettings
[JsonIgnore] public string coverImageCache => Path.Join(workingDirectory, "imageCache");
public ushort? version { get; set; }
public TrangaSettings(string downloadLocation, string workingDirectory)
public TrangaSettings(string? downloadLocation = null, string? workingDirectory = null)
{
downloadLocation ??= Path.Join(Directory.GetCurrentDirectory(), "Downloads");
workingDirectory ??= Directory.GetCurrentDirectory();
if (downloadLocation.Length < 1 || workingDirectory.Length < 1)
throw new ArgumentException("Download-location and working-directory paths can not be empty!");
this.workingDirectory = workingDirectory;
this.downloadLocation = downloadLocation;
}
public static TrangaSettings LoadSettings(string importFilePath)
public static TrangaSettings LoadSettings(string importFilePath, Logger? logger)
{
if (!File.Exists(importFilePath))
return new TrangaSettings(Path.Join(Directory.GetCurrentDirectory(), "Downloads"), Directory.GetCurrentDirectory());
return new TrangaSettings();
string toRead = File.ReadAllText(importFilePath);
SettingsJsonObject settings = JsonConvert.DeserializeObject<SettingsJsonObject>(toRead,
new JsonSerializerSettings { Converters = { new NotificationManager.NotificationManagerJsonConverter(), new LibraryManager.LibraryManagerJsonConverter() } })!;
return settings.ts ?? new TrangaSettings(Path.Join(Directory.GetCurrentDirectory(), "Downloads"), Directory.GetCurrentDirectory());
TrangaSettings? settings = JsonConvert.DeserializeObject<TrangaSettings>(File.ReadAllText(importFilePath),
new JsonSerializerSettings
{
Converters =
{
new NotificationManagerJsonConverter(),
new LibraryManagerJsonConverter()
}
});
return settings ?? new TrangaSettings();
}
public void ExportSettings()
{
SettingsJsonObject? settings = null;
if (File.Exists(settingsFilePath))
while (File.Exists(settingsFilePath))
{
bool inUse = true;
while (inUse)
@ -49,49 +58,10 @@ public class TrangaSettings
}
catch (IOException)
{
inUse = true;
Thread.Sleep(50);
Thread.Sleep(100);
}
}
string toRead = File.ReadAllText(settingsFilePath);
settings = JsonConvert.DeserializeObject<SettingsJsonObject>(toRead,
new JsonSerializerSettings
{
Converters =
{
new NotificationManager.NotificationManagerJsonConverter(),
new LibraryManager.LibraryManagerJsonConverter()
}
});
}
settings = new SettingsJsonObject(this, settings?.co);
File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(settings));
}
public void UpdateSettings(UpdateField field, params string[] values)
{
switch (field)
{
case UpdateField.DownloadLocation:
if (values.Length != 1)
return;
this.downloadLocation = values[0];
break;
}
ExportSettings();
}
public enum UpdateField { DownloadLocation, Komga, Kavita, Gotify, LunaSea}
internal class SettingsJsonObject
{
public TrangaSettings? ts { get; }
public CommonObjects? co { get; }
public SettingsJsonObject(TrangaSettings? ts, CommonObjects? co)
{
this.ts = ts;
this.co = co;
}
File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(this));
}
}

View File

@ -1,67 +0,0 @@
using System.Net;
using Tranga.Connectors;
using Tranga.NotificationManagers;
using Tranga.LibraryManagers;
namespace Tranga.TrangaTasks;
public class DownloadChapterTask : TrangaTask
{
public string connectorName { get; }
public Publication publication { get; }
// ReSharper disable once MemberCanBePrivate.Global
public string language { get; }
public Chapter chapter { get; }
private double _dctProgress;
public DownloadChapterTask(string connectorName, Publication publication, Chapter chapter, string language = "en", MonitorPublicationTask? parentTask = null) : base(Task.DownloadChapter, TimeSpan.Zero, parentTask)
{
this.chapter = chapter;
this.connectorName = connectorName;
this.publication = publication;
this.language = language;
}
protected override HttpStatusCode ExecuteTask(TaskManager taskManager, CancellationToken? cancellationToken = null)
{
if (cancellationToken?.IsCancellationRequested ?? false)
return HttpStatusCode.RequestTimeout;
Connector connector = taskManager.GetConnector(this.connectorName);
connector.CopyCoverFromCacheToDownloadLocation(this.publication);
HttpStatusCode downloadSuccess = connector.DownloadChapter(this.publication, this.chapter, this, cancellationToken);
if ((int)downloadSuccess >= 200 && (int)downloadSuccess < 300)
{
foreach(NotificationManager nm in taskManager.commonObjects.notificationManagers)
nm.SendNotification("Chapter downloaded", $"{this.publication.sortName} {this.chapter.chapterNumber} {this.chapter.name}");
foreach (LibraryManager lm in taskManager.commonObjects.libraryManagers)
lm.UpdateLibrary();
}
return downloadSuccess;
}
public override TrangaTask Clone()
{
return new DownloadChapterTask(this.connectorName, this.publication, this.chapter,
this.language, (MonitorPublicationTask?)this.parentTask);
}
protected override double GetProgress()
{
return _dctProgress;
}
internal void IncrementProgress(double amount)
{
this._dctProgress += amount;
this.lastChange = DateTime.Now;
if(this.parentTask is not null)
this.parentTask.lastChange = DateTime.Now;
}
public override string ToString()
{
return $"{base.ToString()}, {connectorName}, {publication.sortName} {publication.internalId}, Vol.{chapter.volumeNumber} Ch.{chapter.chapterNumber}";
}
}

View File

@ -1,61 +0,0 @@
using System.Net;
using Tranga.Connectors;
namespace Tranga.TrangaTasks;
public class MonitorPublicationTask : TrangaTask
{
public string connectorName { get; }
public Publication publication { get; }
// ReSharper disable once MemberCanBePrivate.Global
public string language { get; }
public MonitorPublicationTask(string connectorName, Publication publication, TimeSpan reoccurrence, string language = "en") : base(Task.MonitorPublication, reoccurrence)
{
this.connectorName = connectorName;
this.publication = publication;
this.language = language;
}
protected override HttpStatusCode ExecuteTask(TaskManager taskManager, CancellationToken? cancellationToken = null)
{
if (cancellationToken?.IsCancellationRequested ?? false)
return HttpStatusCode.RequestTimeout;
Connector connector = taskManager.GetConnector(this.connectorName);
//Check if Publication already has a Folder
publication.CreatePublicationFolder(taskManager.settings.downloadLocation);
List<Chapter> newChapters = connector.GetNewChaptersList(publication, language, ref taskManager.collection);
connector.CopyCoverFromCacheToDownloadLocation(publication);
publication.SaveSeriesInfoJson(taskManager.settings.downloadLocation);
foreach (Chapter newChapter in newChapters)
{
DownloadChapterTask newTask = new (this.connectorName, publication, newChapter, this.language, this);
this.childTasks.Add(newTask);
newTask.state = ExecutionState.Enqueued;
taskManager.AddTask(newTask);
}
return HttpStatusCode.OK;
}
public override TrangaTask Clone()
{
return new MonitorPublicationTask(this.connectorName, this.publication, this.reoccurrence,
this.language);
}
protected override double GetProgress()
{
if (this.childTasks.Count > 0)
return this.childTasks.Sum(ct => ct.progress) / childTasks.Count;
return 1;
}
public override string ToString()
{
return $"{base.ToString()}, {connectorName}, {publication.sortName} {publication.internalId}";
}
}

View File

@ -1,157 +0,0 @@
using System.Net;
using System.Text.Json.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using JsonConverter = Newtonsoft.Json.JsonConverter;
namespace Tranga.TrangaTasks;
/// <summary>
/// Stores information on Task, when implementing new Tasks also update the serializer
/// </summary>
[JsonDerivedType(typeof(MonitorPublicationTask), 2)]
[JsonDerivedType(typeof(UpdateLibrariesTask), 3)]
[JsonDerivedType(typeof(DownloadChapterTask), 4)]
public abstract class TrangaTask
{
// ReSharper disable once MemberCanBeProtected.Global
public TimeSpan reoccurrence { get; }
public DateTime lastExecuted { get; set; }
[Newtonsoft.Json.JsonIgnore] public ExecutionState state { get; set; }
public Task task { get; }
public string taskId { get; init; }
[Newtonsoft.Json.JsonIgnore] public TrangaTask? parentTask { get; set; }
public string? parentTaskId { get; set; }
[Newtonsoft.Json.JsonIgnore] internal HashSet<TrangaTask> childTasks { get; }
public double progress => GetProgress();
// ReSharper disable once MemberCanBePrivate.Global
[Newtonsoft.Json.JsonIgnore]public DateTime executionStarted { get; private set; }
[Newtonsoft.Json.JsonIgnore]public DateTime lastChange { get; internal set; }
// ReSharper disable once MemberCanBePrivate.Global
[Newtonsoft.Json.JsonIgnore]public DateTime executionApproximatelyFinished => lastChange.Add(GetRemainingTime());
// ReSharper disable once MemberCanBePrivate.Global
public TimeSpan executionApproximatelyRemaining => executionApproximatelyFinished.Subtract(DateTime.Now);
[Newtonsoft.Json.JsonIgnore]public DateTime nextExecution => lastExecuted.Add(reoccurrence);
public enum ExecutionState { Waiting, Enqueued, Running, Failed, Success }
protected TrangaTask(Task task, TimeSpan reoccurrence, TrangaTask? parentTask = null)
{
this.reoccurrence = reoccurrence;
this.lastExecuted = DateTime.Now.Subtract(reoccurrence);
this.task = task;
this.executionStarted = DateTime.UnixEpoch;
this.lastChange = DateTime.MaxValue;
this.taskId = Convert.ToBase64String(BitConverter.GetBytes(new Random().Next()));
this.childTasks = new();
this.parentTask = parentTask;
this.parentTaskId = parentTask?.taskId;
}
/// <summary>
/// BL for concrete Tasks
/// </summary>
/// <param name="taskManager"></param>
/// <param name="cancellationToken"></param>
protected abstract HttpStatusCode ExecuteTask(TaskManager taskManager, CancellationToken? cancellationToken = null);
public abstract TrangaTask Clone();
protected abstract double GetProgress();
/// <summary>
/// Execute the task
/// </summary>
/// <param name="taskManager">Should be the parent taskManager</param>
/// <param name="cancellationToken"></param>
public void Execute(TaskManager taskManager, CancellationToken? cancellationToken = null)
{
taskManager.commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Executing Task {this}");
this.state = ExecutionState.Running;
this.executionStarted = DateTime.Now;
this.lastChange = DateTime.Now;
if(parentTask is not null && parentTask.childTasks.All(ct => ct.state is ExecutionState.Waiting or ExecutionState.Failed))
parentTask.executionStarted = DateTime.Now;
HttpStatusCode statusCode = ExecuteTask(taskManager, cancellationToken);
if ((int)statusCode >= 200 && (int)statusCode < 300)
{
this.lastExecuted = DateTime.Now;
this.state = ExecutionState.Success;
}
else
{
this.state = ExecutionState.Failed;
this.lastExecuted = DateTime.MaxValue;
}
if (this is DownloadChapterTask)
taskManager.DeleteTask(this);
taskManager.commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Finished Executing Task {this}");
}
public void AddChildTask(TrangaTask childTask)
{
this.childTasks.Add(childTask);
}
public void RemoveChildTask(TrangaTask childTask)
{
this.childTasks.Remove(childTask);
}
private TimeSpan GetRemainingTime()
{
if(progress == 0 || state is ExecutionState.Enqueued or ExecutionState.Waiting or ExecutionState.Failed || lastChange == DateTime.MaxValue)
return DateTime.MaxValue.Subtract(lastChange).Subtract(TimeSpan.FromHours(1));
TimeSpan elapsed = lastChange.Subtract(executionStarted);
return elapsed.Divide(progress).Multiply(1 - progress);
}
public enum Task : byte
{
MonitorPublication = 2,
UpdateLibraries = 3,
DownloadChapter = 4,
}
public override string ToString()
{
return $"{task}, {lastExecuted}, {reoccurrence}, {state}, {progress:P2}, {executionApproximatelyFinished}, {executionApproximatelyRemaining}";
}
public class TrangaTaskJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(TrangaTask);
}
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
if (jo["task"]!.Value<Int64>() == (Int64)Task.MonitorPublication)
return jo.ToObject<MonitorPublicationTask>(serializer)!;
if (jo["task"]!.Value<Int64>() == (Int64)Task.UpdateLibraries)
return jo.ToObject<UpdateLibrariesTask>(serializer)!;
if (jo["task"]!.Value<Int64>() == (Int64)Task.DownloadChapter)
return jo.ToObject<DownloadChapterTask>(serializer)!;
throw new Exception();
}
public override bool CanWrite => false;
/// <summary>
/// Don't call this
/// </summary>
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
throw new Exception("Dont call this");
}
}
}

View File

@ -1,28 +0,0 @@
using System.Net;
namespace Tranga.TrangaTasks;
/// <summary>
/// LEGACY DEPRECATED
/// </summary>
public class UpdateLibrariesTask : TrangaTask
{
public UpdateLibrariesTask(TimeSpan reoccurrence) : base(Task.UpdateLibraries, reoccurrence)
{
}
protected override HttpStatusCode ExecuteTask(TaskManager taskManager, CancellationToken? cancellationToken = null)
{
return HttpStatusCode.BadRequest;
}
public override TrangaTask Clone()
{
return new UpdateLibrariesTask(this.reoccurrence);
}
protected override double GetProgress()
{
return 1;
}
}

View File

@ -1,4 +0,0 @@
FROM nginx:alpine3.17-slim
COPY . /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@ -1,168 +0,0 @@
let apiUri = `http://${window.location.host.split(':')[0]}:6531`
if(getCookie("apiUri") != ""){
apiUri = getCookie("apiUri");
}
function getCookie(cname) {
let name = cname + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for(let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
async function GetData(uri){
let request = await fetch(uri, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
let json = await request.json();
return json;
}
function PostData(uri){
fetch(uri, {
method: 'POST'
});
}
function DeleteData(uri){
fetch(uri, {
method: 'DELETE'
});
}
async function GetAvailableControllers(){
var uri = apiUri + "/Connectors";
let json = await GetData(uri);
return json;
}
async function GetPublicationFromConnector(connectorName, title){
var uri = apiUri + `/Publications/FromConnector?connectorName=${connectorName}&title=${title}`;
let json = await GetData(uri);
return json;
}
async function GetKnownPublications(){
var uri = apiUri + "/Publications/Known";
let json = await GetData(uri);
return json;
}
async function GetPublication(internalId){
var uri = apiUri + `/Publications/Known?internalId=${internalId}`;
let json = await GetData(uri);
return json;
}
async function GetChapters(internalId, connectorName, onlyNew, language){
var uri = apiUri + `/Publications/Chapters?internalId=${internalId}&connectorName=${connectorName}&onlyNew=${onlyNew}&language=${language}`;
let json = await GetData(uri);
return json;
}
async function GetTaskTypes(){
var uri = apiUri + "/Tasks/Types";
let json = await GetData(uri);
return json;
}
async function GetRunningTasks(){
var uri = apiUri + "/Tasks/RunningTasks";
let json = await GetData(uri);
return json;
}
async function GetDownloadTasks(){
var uri = apiUri + "/Tasks?taskType=MonitorPublication";
let json = await GetData(uri);
return json;
}
async function GetSettings(){
var uri = apiUri + "/Settings";
let json = await GetData(uri);
return json;
}
async function GetKomgaTask(){
var uri = apiUri + "/Tasks?taskType=UpdateLibraries";
let json = await GetData(uri);
return json;
}
function CreateMonitorTask(connectorName, internalId, reoccurrence, language){
var uri = apiUri + `/Tasks/CreateMonitorTask?connectorName=${connectorName}&internalId=${internalId}&reoccurrenceTime=${reoccurrence}&language=${language}`;
PostData(uri);
}
function CreateDownloadChaptersTask(connectorName, internalId, chapters, language){
var uri = apiUri + `/Tasks/CreateDownloadChaptersTask?connectorName=${connectorName}&internalId=${internalId}&chapters=${chapters}&language=${language}`;
PostData(uri);
}
function StartTask(taskType, connectorName, internalId){
var uri = apiUri + `/Tasks/Start?taskType=${taskType}&connectorName=${connectorName}&internalId=${internalId}`;
PostData(uri);
}
function EnqueueTask(taskType, connectorName, publicationId){
var uri = apiUri + `/Queue/Enqueue?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}`;
PostData(uri);
}
function UpdateDownloadLocation(downloadLocation){
var uri = apiUri + "/Settings/Update?"
uri += "&downloadLocation="+downloadLocation;
PostData(uri);
}
function UpdateKomga(komgaUrl, komgaAuth){
var uri = apiUri + "/Settings/Update?"
uri += `&komgaUrl=${komgaUrl}&komgaAuth=${komgaAuth}`;
PostData(uri);
}
function UpdateKavita(kavitaUrl, kavitaUser, kavitaPass){
var uri = apiUri + "/Settings/Update?"
uri += `&kavitaUrl=${kavitaUrl}&kavitaUsername=${kavitaUser}&kavitaPassword=${kavitaPass}`;
PostData(uri);
}
function UpdateGotify(gotifyUrl, gotifyAppToken){
var uri = apiUri + "/Settings/Update?"
uri += `&gotifyUrl=${gotifyUrl}&gotifyAppToken=${gotifyAppToken}`;
PostData(uri);
}
function UpdateLunaSea(lunaseaWebhook){
var uri = apiUri + "/Settings/Update?"
uri += `&lunaseaWebhook=${lunaseaWebhook}`;
PostData(uri);
}
function DeleteTask(taskType, connectorName, publicationId){
var uri = apiUri + `/Tasks?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}`;
DeleteData(uri);
}
function DequeueTask(taskType, connectorName, publicationId){
var uri = apiUri + `/Queue/Dequeue?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}`;
DeleteData(uri);
}
async function GetQueue(){
var uri = apiUri + "/Queue/List";
let json = await GetData(uri);
return json;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

View File

@ -1,179 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tranga</title>
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<wrapper>
<topbar>
<titlebox>
<img alt="website image is Blahaj" src="media/blahaj.png">
<span>Tranga</span>
</titlebox>
<spacer></spacer>
<searchdiv>
<label for="searchbox"></label><input id="searchbox" placeholder="Filter" type="text">
</searchdiv>
<img id="settingscog" src="media/settings-cogwheel.svg" height="100%" alt="settingscog">
</topbar>
<viewport>
<content>
<div id="addPublication">
<p>+</p>
</div>
<publication>
<img alt="cover" src="media/cover.jpg">
<publication-information>
<connector-name class="pill">MangaDex</connector-name>
<publication-name>Tensei Pandemic</publication-name>
</publication-information>
</publication>
</content>
<popup id="selectPublicationPopup">
<blur-background id="blurBackgroundTaskPopup"></blur-background>
<popup-window>
<popup-title>Select Publication</popup-title>
<popup-content>
<div>
<label for="connectors">Connector</label>
<select id="connectors">
<option value=""></option>
</select>
</div>
<div>
<label for="searchPublicationQuery">Search Title</label><input id="searchPublicationQuery" type="text"></addtask-setting>
</div>
<input type="submit" value="Search" style="font-weight: bolder" onclick="NewSearch();">
</popup-content>
<div id="taskSelectOutput"></div>
</popup-window>
</popup>
<popup id="createMonitorTaskPopup">
<blur-background id="blurBackgroundCreateMonitorTaskPopup"></blur-background>
<popup-window>
<popup-title>Create Task: Monitor Publication</popup-title>
<popup-content>
<div>
<span>Run every</span>
<label for="hours"></label><input id="hours" type="number" value="3" min="0" max="23"><span>hours</span>
<label for="minutes"></label><input id="minutes" type="number" value="0" min="0" max="59"><span>minutes</span>
<input type="submit" value="Create" onclick="AddMonitorTask()">
</div>
</popup-content>
</popup-window>
</popup>
<popup id="createDownloadChaptersTask">
<blur-background id="blurBackgroundCreateDownloadChaptersTask"></blur-background>
<popup-window>
<popup-title>Create Task: Download Chapter(s)</popup-title>
<popup-content>
<div>
<label for="selectedChapters">Chapters:</label><input id="selectedChapters" placeholder="Select"><input type="submit" value="Select" onclick="DownloadChapterTaskClick()">
</div>
<div id="chapterOutput">
</div>
</popup-content>
</popup-window>
</popup>
<popup id="publicationViewerPopup">
<blur-background id="blurBackgroundPublicationPopup"></blur-background>
<publication-viewer>
<img id="pubviewcover" src="media/cover.jpg" alt="cover">
<publication-information>
<publication-name id="publicationViewerName">Tensei Pandemic</publication-name>
<publication-tags id="publicationViewerTags"></publication-tags>
<publication-author id="publicationViewerAuthor">Imamura Hinata</publication-author>
<publication-description id="publicationViewerDescription">Imamura Hinata is a high school boy with a cute appearance.
Since his trauma with the first love, he wanted to be more manly than anybody else. But one day he woke up to something different…
The total opposite of his ideal male body!
Pandemic love comedy!
</publication-description>
<publication-interactions>
<publication-starttask>Start Task ▶️</publication-starttask>
<publication-delete>Delete Task ❌</publication-delete>
<publication-add id="createMonitorTaskButton">Monitor </publication-add>
<publication-add id="createDownloadChapterTaskButton">Download Chapter </publication-add>
</publication-interactions>
</publication-information>
</publication-viewer>
</popup>
<popup id="settingsPopup">
<blur-background id="blurBackgroundSettingsPopup"></blur-background>
<popup-window>
<popup-title>Settings</popup-title>
<popup-content>
<div>
<p class="title">Download Location:</p>
<span id="downloadLocation"></span>
</div>
<div>
<p class="title">API-URI</p>
<label for="settingApiUri"></label><input placeholder="https://" type="text" id="settingApiUri">
</div>
<div>
<span class="title">Komga</span>
<div>Configured: <span id="komgaConfigured">✅❌</span></div>
<label for="komgaUrl"></label><input placeholder="URL" id="komgaUrl" type="text">
<label for="komgaUsername"></label><input placeholder="Username" id="komgaUsername" type="text">
<label for="komgaPassword"></label><input placeholder="Password" id="komgaPassword" type="password">
</div>
<div>
<span class="title">Kavita</span>
<div>Configured: <span id="kavitaConfigured">✅❌</span></div>
<label for="kavitaUrl"></label><input placeholder="URL" id="kavitaUrl" type="text">
<label for="kavitaUsername"></label><input placeholder="Username" id="kavitaUsername" type="text">
<label for="kavitaPassword"></label><input placeholder="Password" id="kavitaPassword" type="password">
</div>
<div>
<span class="title">Gotify</span>
<div>Configured: <span id="gotifyConfigured">✅❌</span></div>
<label for="gotifyUrl"></label><input placeholder="URL" id="gotifyUrl" type="text">
<label for="gotifyAppToken"></label><input placeholder="App-Token" id="gotifyAppToken" type="text">
</div>
<div>
<span class="title">LunaSea</span>
<div>Configured: <span id="lunaseaConfigured">✅❌</span></div>
<label for="lunaseaWebhook"></label><input placeholder="device/:id or user/:id" id="lunaseaWebhook" type="text">
</div>
<div>
<label for="libraryUpdateTime" style="margin-right: 5px;">Update Time</label><input id="libraryUpdateTime" type="time" value="00:01:00" step="10">
<input type="submit" value="Update" onclick="UpdateLibrarySettings()">
</div>
</popup-content>
</popup-window>
</popup>
<popup id="downloadTasksPopup">
<blur-background id="blurBackgroundTasksQueuePopup"></blur-background>
<popup-window>
<popup-title>Task Progress</popup-title>
<popup-content>
</popup-content>
</popup-window>
</popup>
</viewport>
<footer>
<div onclick="ShowTasksQueue();">
<img src="media/running.svg" alt="running"><div id="tasksRunningTag">0</div>
</div>
<div onclick="ShowTasksQueue();">
<img src="media/queue.svg" alt="queue"><div id="tasksQueuedTag">0</div>
</div>
<p id="madeWith">Made with Blåhaj 🦈</p>
</footer>
</wrapper>
<script src="apiConnector.js"></script>
<script src="interaction.js"></script>
</body>
</html>

View File

@ -1,525 +0,0 @@
let publications = [];
let tasks = [];
let toEditId;
const searchBox = document.querySelector("#searchbox");
const searchPublicationQuery = document.querySelector("#searchPublicationQuery");
const selectPublication = document.querySelector("#taskSelectOutput");
const connectorSelect = document.querySelector("#connectors");
const settingsPopup = document.querySelector("#settingsPopup");
const settingsCog = document.querySelector("#settingscog");
const selectRecurrence = document.querySelector("#selectReccurrence");
const tasksContent = document.querySelector("content");
const selectPublicationPopup = document.querySelector("#selectPublicationPopup");
const createMonitorTaskButton = document.querySelector("#createMonitorTaskButton");
const createDownloadChapterTaskButton = document.querySelector("#createDownloadChapterTaskButton");
const createMonitorTaskPopup = document.querySelector("#createMonitorTaskPopup");
const createDownloadChaptersTask = document.querySelector("#createDownloadChaptersTask");
const chapterOutput = document.querySelector("#chapterOutput");
const selectedChapters = document.querySelector("#selectedChapters");
const publicationViewerPopup = document.querySelector("#publicationViewerPopup");
const publicationViewerWindow = document.querySelector("publication-viewer");
const publicationViewerDescription = document.querySelector("#publicationViewerDescription");
const publicationViewerName = document.querySelector("#publicationViewerName");
const publicationViewerTags = document.querySelector("#publicationViewerTags");
const publicationViewerAuthor = document.querySelector("#publicationViewerAuthor");
const pubviewcover = document.querySelector("#pubviewcover");
const publicationDelete = document.querySelector("publication-delete");
const publicationTaskStart = document.querySelector("publication-starttask");
const settingDownloadLocation = document.querySelector("#downloadLocation");
const settingKomgaUrl = document.querySelector("#komgaUrl");
const settingKomgaUser = document.querySelector("#komgaUsername");
const settingKomgaPass = document.querySelector("#komgaPassword");
const settingKavitaUrl = document.querySelector("#kavitaUrl");
const settingKavitaUser = document.querySelector("#kavitaUsername");
const settingKavitaPass = document.querySelector("#kavitaPassword");
const settingGotifyUrl = document.querySelector("#gotifyUrl");
const settingGotifyAppToken = document.querySelector("#gotifyAppToken");
const settingLunaseaWebhook = document.querySelector("#lunaseaWebhook");
const libraryUpdateTime = document.querySelector("#libraryUpdateTime");
const settingKomgaConfigured = document.querySelector("#komgaConfigured");
const settingKavitaConfigured = document.querySelector("#kavitaConfigured");
const settingGotifyConfigured = document.querySelector("#gotifyConfigured");
const settingLunaseaConfigured = document.querySelector("#lunaseaConfigured");
const settingApiUri = document.querySelector("#settingApiUri");
const tagTasksRunning = document.querySelector("#tasksRunningTag");
const tagTasksQueued = document.querySelector("#tasksQueuedTag");
const downloadTasksPopup = document.querySelector("#downloadTasksPopup");
const downloadTasksOutput = downloadTasksPopup.querySelector("popup-content");
searchbox.addEventListener("keyup", (event) => FilterResults());
settingsCog.addEventListener("click", () => OpenSettings());
document.querySelector("#blurBackgroundSettingsPopup").addEventListener("click", () => settingsPopup.style.display = "none");
document.querySelector("#blurBackgroundTaskPopup").addEventListener("click", () => selectPublicationPopup.style.display = "none");
document.querySelector("#blurBackgroundPublicationPopup").addEventListener("click", () => HidePublicationPopup());
document.querySelector("#blurBackgroundCreateMonitorTaskPopup").addEventListener("click", () => createMonitorTaskPopup.style.display = "none");
document.querySelector("#blurBackgroundCreateDownloadChaptersTask").addEventListener("click", () => createDownloadChaptersTask.style.display = "none");
document.querySelector("#blurBackgroundTasksQueuePopup").addEventListener("click", () => downloadTasksPopup.style.display = "none");
selectedChapters.addEventListener("keypress", (event) => {
if(event.key === "Enter"){
DownloadChapterTaskClick();
}
})
publicationDelete.addEventListener("click", () => DeleteTaskClick());
createMonitorTaskButton.addEventListener("click", () => {
HidePublicationPopup();
createMonitorTaskPopup.style.display = "block";
});
createDownloadChapterTaskButton.addEventListener("click", () => {
HidePublicationPopup();
OpenDownloadChapterTaskPopup();
});
publicationTaskStart.addEventListener("click", () => StartTaskClick());
searchPublicationQuery.addEventListener("keypress", (event) => {
if(event.key === "Enter"){
NewSearch();
}
});
let availableConnectors;
GetAvailableControllers()
.then(json => availableConnectors = json)
.then(json =>
json.forEach(connector => {
var option = document.createElement('option');
option.value = connector;
option.innerText = connector;
connectorSelect.appendChild(option);
})
);
function NewSearch(){
//Disable inputs
connectorSelect.disabled = true;
searchPublicationQuery.disabled = true;
//Waitcursor
document.body.style.cursor = "wait";
//Empty previous results
selectPublication.replaceChildren();
GetPublicationFromConnector(connectorSelect.value, searchPublicationQuery.value)
.then(json =>
json.forEach(publication => {
var option = CreatePublication(publication, connectorSelect.value);
option.addEventListener("click", (mouseEvent) => {
ShowPublicationViewerWindow(publication.internalId, mouseEvent, true);
});
selectPublication.appendChild(option);
}
))
.then(() => {
//Re-enable inputs
connectorSelect.disabled = false;
searchPublicationQuery.disabled = false;
//Cursor
document.body.style.cursor = "initial";
});
}
//Returns a new "Publication" Item to display in the tasks section
function CreatePublication(publication, connector){
var publicationElement = document.createElement('publication');
publicationElement.setAttribute("id", publication.internalId);
var img = document.createElement('img');
img.src = `imageCache/${publication.coverFileNameInCache}`;
publicationElement.appendChild(img);
var info = document.createElement('publication-information');
var connectorName = document.createElement('connector-name');
connectorName.innerText = connector;
connectorName.className = "pill";
info.appendChild(connectorName);
var publicationName = document.createElement('publication-name');
publicationName.innerText = publication.sortName;
info.appendChild(publicationName);
publicationElement.appendChild(info);
if(publications.filter(pub => pub.internalId === publication.internalId) < 1)
publications.push(publication);
return publicationElement;
}
function AddMonitorTask(){
var hours = document.querySelector("#hours").value;
var minutes = document.querySelector("#minutes").value;
CreateMonitorTask(connectorSelect.value, toEditId, `${hours}:${minutes}:00`, "en");
HidePublicationPopup();
createMonitorTaskPopup.style.display = "none";
selectPublicationPopup.style.display = "none";
}
function OpenDownloadChapterTaskPopup(){
selectedChapters.value = "";
chapterOutput.replaceChildren();
createDownloadChaptersTask.style.display = "block";
GetChapters(toEditId, connectorSelect.value, true, "en").then((json) => {
var i = 0;
json.forEach(chapter => {
var chapterDom = document.createElement("div");
var indexDom = document.createElement("span");
indexDom.className = "index";
indexDom.innerText = i++;
chapterDom.appendChild(indexDom);
var volDom = document.createElement("span");
volDom.className = "vol";
volDom.innerText = chapter.volumeNumber;
chapterDom.appendChild(volDom);
var chDom = document.createElement("span");
chDom.className = "ch";
chDom.innerText = chapter.chapterNumber;
chapterDom.appendChild(chDom);
var titleDom = document.createElement("span");
titleDom.innerText = chapter.name;
chapterDom.appendChild(titleDom);
chapterOutput.appendChild(chapterDom);
});
});
}
function DownloadChapterTaskClick(){
CreateDownloadChaptersTask(connectorSelect.value, toEditId, selectedChapters.value, "en");
HidePublicationPopup();
createDownloadChaptersTask.style.display = "none";
selectPublicationPopup.style.display = "none";
}
function DeleteTaskClick(){
taskToDelete = tasks.filter(tTask => tTask.publication.internalId === toEditId)[0];
DeleteTask("MonitorPublication", taskToDelete.connectorName, toEditId);
HidePublicationPopup();
}
function StartTaskClick(){
var toEditTask = tasks.filter(task => task.publication.internalId == toEditId)[0];
StartTask("MonitorPublication", toEditTask.connectorName, toEditId);
HidePublicationPopup();
}
function ResetContent(){
//Delete everything
tasksContent.replaceChildren();
//Add "Add new Task" Button
var add = document.createElement("div");
add.setAttribute("id", "addPublication")
var plus = document.createElement("p");
plus.innerText = "+";
add.appendChild(plus);
add.addEventListener("click", () => ShowNewTaskWindow());
tasksContent.appendChild(add);
}
function ShowPublicationViewerWindow(publicationId, event, add){
//Show popup
publicationViewerPopup.style.display = "block";
//Set position to mouse-position
if(event.clientY < window.innerHeight - publicationViewerWindow.offsetHeight)
publicationViewerWindow.style.top = `${event.clientY}px`;
else
publicationViewerWindow.style.top = `${event.clientY - publicationViewerWindow.offsetHeight}px`;
if(event.clientX < window.innerWidth - publicationViewerWindow.offsetWidth)
publicationViewerWindow.style.left = `${event.clientX}px`;
else
publicationViewerWindow.style.left = `${event.clientX - publicationViewerWindow.offsetWidth}px`;
//Edit information inside the window
var publication = publications.filter(pub => pub.internalId === publicationId)[0];
publicationViewerName.innerText = publication.sortName;
publicationViewerTags.innerText = publication.tags.join(", ");
publicationViewerDescription.innerText = publication.description;
publicationViewerAuthor.innerText = publication.authors.join(',');
pubviewcover.src = `imageCache/${publication.coverFileNameInCache}`;
toEditId = publicationId;
//Check what action should be listed
if(add){
createMonitorTaskButton.style.display = "initial";
createDownloadChapterTaskButton.style.display = "initial";
publicationDelete.style.display = "none";
publicationTaskStart.style.display = "none";
}
else{
createMonitorTaskButton.style.display = "none";
createDownloadChapterTaskButton.style.display = "none";
publicationDelete.style.display = "initial";
publicationTaskStart.style.display = "initial";
}
}
function HidePublicationPopup(){
publicationViewerPopup.style.display = "none";
}
function ShowNewTaskWindow(){
selectPublication.replaceChildren();
searchPublicationQuery.value = "";
selectPublicationPopup.style.display = "flex";
}
const fadeIn = [
{ opacity: "0" },
{ opacity: "1" }
];
const fadeInTiming = {
duration: 50,
iterations: 1,
fill: "forwards"
}
function OpenSettings(){
GetSettingsClick();
settingsPopup.style.display = "flex";
}
function GetSettingsClick(){
settingApiUri.value = "";
settingKomgaUrl.value = "";
settingKomgaUser.value = "";
settingKomgaPass.value = "";
settingKomgaConfigured.innerText = "❌";
settingKavitaUrl.value = "";
settingKavitaUser.value = "";
settingKavitaPass.value = "";
settingKavitaConfigured.innerText = "❌";
settingGotifyUrl.value = "";
settingGotifyAppToken.value = "";
settingGotifyConfigured.innerText = "❌";
settingLunaseaWebhook.value = "";
settingLunaseaConfigured.innerText = "❌";
settingApiUri.placeholder = apiUri;
GetSettings().then(json => {
settingDownloadLocation.innerText = json.downloadLocation;
json.libraryManagers.forEach(lm => {
if(lm.libraryType == 0){
settingKomgaUrl.placeholder = lm.baseUrl;
settingKomgaUser.placeholder = "User";
settingKomgaPass.placeholder = "***";
settingKomgaConfigured.innerText = "✅";
} else if(lm.libraryType == 1){
settingKavitaUrl.placeholder = lm.baseUrl;
settingKavitaUser.placeholder = "User";
settingKavitaPass.placeholder = "***";
settingKavitaConfigured.innerText = "✅";
}
});
json.notificationManagers.forEach(nm => {
if(nm.notificationManagerType == 0){
settingGotifyConfigured.innerText = "✅";
} else if(nm.notificationManagerType == 1){
settingLunaseaConfigured.innerText = "✅";
}
});
});
GetKomgaTask().then(json => {
if(json.length > 0)
libraryUpdateTime.value = json[0].reoccurrence;
});
}
function UpdateLibrarySettings(){
if(settingKomgaUrl.value != "" && settingKomgaUser.value != "" && settingKomgaPass.value != ""){
var auth = utf8_to_b64(`${settingKomgaUser.value}:${settingKomgaPass.value}`);
console.log(auth);
UpdateKomga(settingKomgaUrl.value, auth);
}
if(settingKavitaUrl.value != "" && settingKavitaUser.value != "" && settingKavitaPass.value != ""){
UpdateKavita(settingKavitaUrl.value, settingKavitaUser.value, settingKavitaPass.value);
}
if(settingGotifyUrl.value != "" && settingGotifyAppToken.value != ""){
UpdateGotify(settingGotifyUrl.value, settingGotifyAppToken.value);
}
if(settingLunaseaWebhook.value != ""){
UpdateLunaSea(settingLunaseaWebhook.value);
}
if(settingApiUri.value != ""){
apiUri = settingApiUri.value;
document.cookie = `apiUri=${apiUri};`;
}
setTimeout(() => GetSettingsClick(), 200);
}
function utf8_to_b64( str ) {
return window.btoa(unescape(encodeURIComponent( str )));
}
function FilterResults(){
if(searchBox.value.length > 0){
tasksContent.childNodes.forEach(publication => {
publication.childNodes.forEach(item => {
if(item.nodeName.toLowerCase() == "publication-information"){
item.childNodes.forEach(information => {
if(information.nodeName.toLowerCase() == "publication-name"){
if(!information.textContent.toLowerCase().includes(searchBox.value.toLowerCase())){
publication.style.display = "none";
}else{
publication.style.display = "initial";
}
}
});
}
});
});
}else{
tasksContent.childNodes.forEach(publication => publication.style.display = "initial");
}
}
function ShowTasksQueue(){
downloadTasksOutput.replaceChildren();
GetRunningTasks()
.then(json => {
tagTasksRunning.innerText = json.length;
json.forEach(task => {
if(task.task == 2 || task.task == 4) {
downloadTasksOutput.appendChild(CreateProgressChild(task));
document.querySelector(`#progress${GetValidSelector(task.taskId)}`).value = task.progress;
var finishedHours = task.executionApproximatelyRemaining.split(':')[0];
var finishedMinutes = task.executionApproximatelyRemaining.split(':')[1];
var finishedSeconds = task.executionApproximatelyRemaining.split(':')[2].split('.')[0];
document.querySelector(`#progressStr${GetValidSelector(task.taskId)}`).innerText = `${finishedHours}:${finishedMinutes}:${finishedSeconds}`;
}
});
});
GetQueue()
.then(json => {
tagTasksQueued.innerText = json.length;
json.forEach(task => {
downloadTasksOutput.appendChild(CreateProgressChild(task));
});
});
downloadTasksPopup.style.display = "flex";
}
function CreateProgressChild(task){
var child = document.createElement("div");
var img = document.createElement('img');
img.src = `imageCache/${task.publication.coverFileNameInCache}`;
child.appendChild(img);
var name = document.createElement("span");
name.innerText = task.publication.sortName;
name.className = "pubTitle";
child.appendChild(name);
var progress = document.createElement("progress");
progress.id = `progress${GetValidSelector(task.taskId)}`;
child.appendChild(progress);
var progressStr = document.createElement("span");
progressStr.innerText = " \t∞";
progressStr.className = "progressStr";
progressStr.id = `progressStr${GetValidSelector(task.taskId)}`;
child.appendChild(progressStr);
if(task.chapter != undefined){
var chapterNumber = document.createElement("span");
chapterNumber.className = "chapterNumber";
chapterNumber.innerText = `Vol.${task.chapter.volumeNumber} Ch.${task.chapter.chapterNumber}`;
child.appendChild(chapterNumber);
var chapterName = document.createElement("span");
chapterName.className = "chapterName";
chapterName.innerText = task.chapter.name;
child.appendChild(chapterName);
}
return child;
}
//Resets the tasks shown
ResetContent();
downloadTasksOutput.replaceChildren();
//Get Tasks and show them
GetDownloadTasks()
.then(json => json.forEach(task => {
var publication = CreatePublication(task.publication, task.connectorName);
publication.addEventListener("click", (event) => ShowPublicationViewerWindow(task.publication.internalId, event, false));
tasksContent.appendChild(publication);
tasks.push(task);
}));
GetRunningTasks()
.then(json => {
tagTasksRunning.innerText = json.length;
json.forEach(task => {
downloadTasksOutput.appendChild(CreateProgressChild(task));
});
});
GetQueue()
.then(json => {
tagTasksQueued.innerText = json.length;
json.forEach(task => {
downloadTasksOutput.appendChild(CreateProgressChild(task));
});
})
setInterval(() => {
//Tasks from API
var cTasks = [];
GetDownloadTasks()
.then(json => json.forEach(task => cTasks.push(task)))
.then(() => {
//Only update view if tasks-amount has changed
if(tasks.length != cTasks.length) {
//Resets the tasks shown
ResetContent();
//Add all currenttasks to view
cTasks.forEach(task => {
var publication = CreatePublication(task.publication, task.connectorName);
publication.addEventListener("click", (event) => ShowPublicationViewerWindow(task.publication.internalId, event, false));
tasksContent.appendChild(publication);
})
tasks = cTasks;
}
}
);
GetRunningTasks()
.then(json => {
tagTasksRunning.innerText = json.length;
});
GetQueue()
.then(json => {
tagTasksQueued.innerText = json.length;
});
}, 1000);
setInterval(() => {
GetRunningTasks().then((json) => {
json.forEach(task => {
if(task.task == 2 || task.task == 4){
document.querySelector(`#progress${GetValidSelector(task.taskId)}`).value = task.progress;
var finishedHours = task.executionApproximatelyRemaining.split(':')[0];
var finishedMinutes = task.executionApproximatelyRemaining.split(':')[1];
var finishedSeconds = task.executionApproximatelyRemaining.split(':')[2].split('.')[0];
document.querySelector(`#progressStr${GetValidSelector(task.taskId)}`).innerText = `${finishedHours}:${finishedMinutes}:${finishedSeconds}`;
}
});
});
},500);
function GetValidSelector(str){
var clean = [...str.matchAll(/[a-zA-Z0-9]*-*_*/g)];
return clean.join('');
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.29289 5.29289C5.68342 4.90237 6.31658 4.90237 6.70711 5.29289L12 10.5858L17.2929 5.29289C17.6834 4.90237 18.3166 4.90237 18.7071 5.29289C19.0976 5.68342 19.0976 6.31658 18.7071 6.70711L13.4142 12L18.7071 17.2929C19.0976 17.6834 19.0976 18.3166 18.7071 18.7071C18.3166 19.0976 17.6834 19.0976 17.2929 18.7071L12 13.4142L6.70711 18.7071C6.31658 19.0976 5.68342 19.0976 5.29289 18.7071C4.90237 18.3166 4.90237 17.6834 5.29289 17.2929L10.5858 12L5.29289 6.70711C4.90237 6.31658 4.90237 5.68342 5.29289 5.29289Z" fill="#0F1729"/>
</svg>

Before

Width:  |  Height:  |  Size: 804 B

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
<g fill="#000000">
<path d="M2.23 2.674a.75.75 0 00-.96 1.152L3.578 5.75 1.27 7.674a.75.75 0 00.96 1.152l3-2.5a.75.75 0 000-1.152l-3-2.5zM8.25 5a.75.75 0 000 1.5h6a.75.75 0 000-1.5h-6zM5.5 9.25a.75.75 0 01.75-.75h8a.75.75 0 010 1.5h-8a.75.75 0 01-.75-.75zM6.25 12a.75.75 0 000 1.5h8a.75.75 0 000-1.5h-8z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 545 B

View File

@ -1,53 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="800px" height="800px" viewBox="0 0 235.504 235.504"
xml:space="preserve">
<g>
<g>
<path d="M195.209,81.456l-49.227-0.15c0.737-0.886,1.351-1.868,2.284-2.583c3.282-2.497,3.911-7.166,1.427-10.438
c-2.501-3.266-7.161-3.919-10.443-1.423c-4.873,3.715-8.388,8.704-10.255,14.389l-22.191-0.064
c-9.508,0-19.588,7.398-22.938,16.851l-16.877,47.479c-1.775,5.013-1.338,9.966,1.207,13.568
c2.412,3.427,6.384,5.318,11.187,5.358l45.126,0.136c-1.509,5.186-4.701,9.622-9.352,12.424
c-4.891,2.957-10.636,3.814-16.172,2.444c-3.994-0.998-8.031,1.442-9.027,5.418c-0.99,4.012,1.445,8.035,5.432,9.032
c2.927,0.738,5.879,1.091,8.808,1.091c6.516,0,12.93-1.788,18.645-5.23c8.312-5.013,14.172-12.979,16.484-22.409
c0.232-0.905,0.232-1.823,0.124-2.713l28.296,0.092h0.049c2.925,0,5.854-0.89,8.684-2.147c0.2,0.493,0.32,1.014,0.661,1.471
c3.335,4.677,4.629,10.343,3.688,15.993c-0.95,5.627-4.028,10.536-8.688,13.862c-3.351,2.376-4.14,7.037-1.755,10.379
c1.466,2.04,3.751,3.122,6.062,3.122c1.491,0,3.006-0.429,4.312-1.367c7.919-5.61,13.16-13.966,14.771-23.52
c1.603-9.565-0.613-19.203-6.28-27.122c-0.48-0.693-1.134-1.19-1.779-1.659c1.318-1.831,2.501-3.763,3.238-5.854l16.863-47.464
c1.795-5.018,1.351-9.969-1.194-13.58C203.954,83.387,200.015,81.47,195.209,81.456z M201.979,98.405l-16.868,47.464
c-0.981,2.757-2.941,5.214-5.213,7.329c-0.337,0.16-0.706,0.229-1.026,0.465c-0.673,0.485-1.182,1.122-1.639,1.747
c-2.962,1.996-6.288,3.339-9.434,3.339v2.989l-0.044-2.989l-33.194-0.101c-0.232-0.076-0.424-0.261-0.661-0.324
c-1.435-0.353-2.805-0.145-4.095,0.309l-29.768-0.101l1.192-3.358c0.549-1.547-0.269-3.25-1.813-3.795
c-1.521-0.553-3.25,0.24-3.799,1.804l-1.899,5.334l-14.318-0.044c-2.805,0-5.063-0.998-6.336-2.813
c-1.437-2.032-1.603-4.921-0.463-8.144l16.877-47.478c2.48-6.979,10.417-12.868,17.356-12.868l12.217,0.038l-1.963,5.536
c-0.555,1.549,0.262,3.25,1.805,3.797c0.331,0.12,0.661,0.174,0.998,0.174c1.227,0,2.372-0.768,2.793-1.986l2.497-7.019
c0.064-0.164-0.048-0.322-0.016-0.487h2.512c-0.905,7.758,1.163,15.42,5.947,21.638c5.903,7.687,14.852,11.726,23.873,11.726
c6.371,0,12.771-2.001,18.186-6.129c3.266-2.488,3.911-7.167,1.426-10.441c-2.508-3.267-7.161-3.901-10.455-1.415
c-6.612,5.056-16.146,3.775-21.223-2.809c-2.445-3.194-3.487-7.133-2.958-11.117c0.061-0.503,0.353-0.916,0.481-1.402
l52.216,0.156c2.806,0,5.054,1.004,6.324,2.811C202.928,92.241,203.105,95.223,201.979,98.405z"/>
<path d="M107.997,127.194c-1.531-0.553-3.248,0.244-3.799,1.791l-4.302,12.099c-0.551,1.543,0.265,3.242,1.813,3.795
c0.331,0.116,0.659,0.16,0.998,0.16c1.214,0,2.372-0.765,2.801-1.976l4.294-12.099
C110.369,129.446,109.551,127.728,107.997,127.194z"/>
<path d="M116.6,103.014c-1.529-0.541-3.25,0.252-3.805,1.805l-4.298,12.088c-0.547,1.547,0.261,3.252,1.799,3.799
c0.329,0.12,0.659,0.172,1,0.172c1.222,0,2.368-0.769,2.809-1.983l4.294-12.09C118.955,105.268,118.139,103.555,116.6,103.014z"/>
<path d="M232.527,90.428l-14.896-0.038l0,0c-1.639,0-2.974,1.327-2.997,2.976c0,1.639,1.342,2.981,2.981,2.989l14.896,0.042l0,0
c1.643,0,2.978-1.331,2.993-2.979C235.504,91.763,234.17,90.436,232.527,90.428z"/>
<path d="M220.333,80.436c0.629,0,1.242-0.188,1.771-0.583l11.994-8.83c1.326-0.974,1.611-2.842,0.645-4.168
c-0.965-1.327-2.845-1.611-4.163-0.637l-11.998,8.833c-1.323,0.974-1.607,2.841-0.642,4.167
C218.513,80.003,219.418,80.436,220.333,80.436z"/>
<path d="M209.152,56.279c-1.547-0.549-3.25,0.269-3.787,1.805l-4.997,14.036c-0.537,1.547,0.26,3.252,1.803,3.807
c0.337,0.12,0.674,0.172,0.994,0.172c1.242,0,2.385-0.757,2.821-1.986l4.985-14.036C211.516,58.541,210.695,56.846,209.152,56.279
z"/>
<path d="M17.587,100.894h55.208c1.641,0,2.976-1.343,2.976-2.981c0-1.641-1.334-2.988-2.976-2.988H17.587
c-1.641,0-2.988,1.338-2.988,2.988C14.599,99.559,15.946,100.894,17.587,100.894z"/>
<path d="M68.471,119.328c0-1.641-1.345-2.987-2.986-2.987H10.283c-1.639,0-2.981,1.338-2.981,2.987
c0,1.639,1.342,2.974,2.981,2.974h55.202C67.119,122.301,68.471,120.967,68.471,119.328z"/>
<path d="M58.188,137.758H2.974c-1.641,0-2.974,1.335-2.974,2.989c0,1.64,1.333,2.974,2.974,2.974h55.214
c1.639,0,2.981-1.334,2.981-2.974C61.162,139.093,59.827,137.758,58.188,137.758z"/>
<path d="M169.611,28.097c11.821,0,21.403,9.584,21.403,21.41c0,11.82-9.582,21.408-21.403,21.408
c-11.822,0-21.412-9.588-21.412-21.408C148.199,37.681,157.789,28.097,169.611,28.097z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="800px" height="800px" viewBox="0 0 93.5 93.5" xml:space="preserve">
<g>
<g>
<path d="M93.5,40.899c0-2.453-1.995-4.447-4.448-4.447H81.98c-0.74-2.545-1.756-5.001-3.035-7.331l4.998-5
c0.826-0.827,1.303-1.973,1.303-3.146c0-1.19-0.462-2.306-1.303-3.146L75.67,9.555c-1.613-1.615-4.673-1.618-6.29,0l-5,5
c-2.327-1.28-4.786-2.296-7.332-3.037v-7.07C57.048,1.995,55.053,0,52.602,0H40.899c-2.453,0-4.447,1.995-4.447,4.448v7.071
c-2.546,0.741-5.005,1.757-7.333,3.037l-5-5c-1.68-1.679-4.609-1.679-6.288,0L9.555,17.83c-1.734,1.734-1.734,4.555,0,6.289
l4.999,5c-1.279,2.33-2.295,4.788-3.036,7.333h-7.07C1.995,36.452,0,38.447,0,40.899V52.6c0,2.453,1.995,4.447,4.448,4.447h7.071
c0.74,2.545,1.757,5.003,3.036,7.332l-4.998,4.999c-0.827,0.827-1.303,1.974-1.303,3.146c0,1.189,0.462,2.307,1.302,3.146
l8.274,8.273c1.614,1.615,4.674,1.619,6.29,0l5-5c2.328,1.279,4.786,2.297,7.333,3.037v7.071c0,2.453,1.995,4.448,4.447,4.448
h11.702c2.453,0,4.446-1.995,4.446-4.448V81.98c2.546-0.74,5.005-1.756,7.332-3.037l5,5c1.681,1.68,4.608,1.68,6.288,0
l8.275-8.273c1.734-1.734,1.734-4.555,0-6.289l-4.998-5.001c1.279-2.329,2.295-4.787,3.035-7.332h7.071
c2.453,0,4.448-1.995,4.448-4.446V40.899z M62.947,46.75c0,8.932-7.266,16.197-16.197,16.197c-8.931,0-16.197-7.266-16.197-16.197
c0-8.931,7.266-16.197,16.197-16.197C55.682,30.553,62.947,37.819,62.947,46.75z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" height="800px" width="800px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g id="task">
<path d="M4,23.4l-3.7-3.7l1.4-1.4L4,20.6l4.3-4.3l1.4,1.4L4,23.4z M24,21H12v-2h12V21z M4,15.4l-3.7-3.7l1.4-1.4L4,12.6l4.3-4.3
l1.4,1.4L4,15.4z M24,13H12v-2h12V13z M4,7.4L0.3,3.7l1.4-1.4L4,4.6l4.3-4.3l1.4,1.4L4,7.4z M24,5H12V3h12V5z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 603 B

View File

@ -1,603 +0,0 @@
:root{
--background-color: #030304;
--second-background-color: #fff;
--primary-color: #f5a9b8;
--secondary-color: #5bcefa;
--accent-color: #fff;
--topbar-height: 60px;
box-sizing: border-box;
}
body{
padding: 0;
margin: 0;
height: 100vh;
background-color: var(--background-color);
font-family: "Inter", sans-serif;
overflow-x: hidden;
}
wrapper {
display: flex;
flex-flow: column;
flex-wrap: nowrap;
height: 100vh;
}
background-placeholder{
background-color: var(--second-background-color);
opacity: 1;
position: absolute;
width: 100%;
height: 100%;
border-radius: 0 0 5px 0;
z-index: -1;
}
topbar {
display: flex;
align-items: center;
height: var(--topbar-height);
background-color: var(--secondary-color);
z-index: 100;
box-shadow: 0 0 20px black;
}
titlebox {
position: relative;
display: flex;
margin: 0 0 0 40px;
height: 100%;
align-items:center;
justify-content:center;
}
titlebox span{
cursor: default;
font-size: 24pt;
font-weight: bold;
background: linear-gradient(150deg, var(--primary-color), var(--accent-color));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-left: 20px;
}
titlebox img {
height: 100%;
margin-right: 10px;
cursor: grab;
}
spacer{
flex-grow: 1;
}
searchdiv{
display: block;
margin: 0 10px 0 0;
}
#searchbox {
padding: 3px 10px;
border: 0;
border-radius: 4px;
font-size: 14pt;
width: 250px;
}
#settingscog {
cursor: pointer;
margin: 0px 30px;
height: 50%;
filter: invert(100%) sepia(0%) saturate(7465%) hue-rotate(115deg) brightness(116%) contrast(101%);
}
viewport {
position: relative;
display: flex;
flex-flow: row;
flex-wrap: nowrap;
flex-grow: 1;
height: 100%;
overflow-y: scroll;
}
footer {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
width: 100%;
height: 40px;
align-items: center;
justify-content: center;
background-color: var(--primary-color);
align-content: center;
}
footer > div {
height: 100%;
margin: 0 30px;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
cursor: pointer;
}
footer > div > *{
height: 40%;
margin: 0 5px;
}
#madeWith {
flex-grow: 1;
text-align: right;
margin-right: 20px;
cursor: url("media/blahaj.png"), grab;
}
content {
position: relative;
flex-grow: 1;
border-radius: 5px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: start;
align-content: start;
}
#settingsPopup{
z-index: 10;
}
#settingsPopup popup-content{
flex-direction: column;
align-items: start;
margin: 15px 10px;
}
#settingsPopup popup-content > * {
margin: 5px 10px;
}
#settingsPopup popup-content .title {
font-weight: bolder;
}
#addPublication {
cursor: pointer;
background-color: var(--secondary-color);
width: 180px;
height: 300px;
border-radius: 5px;
margin: 10px 10px;
padding: 15px 20px;
position: relative;
}
#addPublication p{
width: 100%;
text-align: center;
font-size: 150pt;
vertical-align: middle;
line-height: 300px;
margin: 0;
color: var(--accent-color);
}
.pill {
flex-grow: 0;
height: 14pt;
font-size: 12pt;
border-radius: 9pt;
background-color: var(--primary-color);
padding: 2pt 17px;
color: black;
}
publication{
cursor: pointer;
background-color: var(--secondary-color);
width: 180px;
height: 300px;
border-radius: 5px;
margin: 10px 10px;
padding: 15px 20px;
position: relative;
}
publication::after{
content: '';
position: absolute;
left: 0; top: 0;
border-radius: 5px;
width: 100%; height: 100%;
background: linear-gradient(rgba(0,0,0,0.8), rgba(0, 0, 0, 0.7),rgba(0, 0, 0, 0.2));
}
publication-information {
display: flex;
flex-direction: column;
justify-content: start;
}
publication-information * {
z-index: 1;
color: var(--accent-color);
}
connector-name{
width: fit-content;
margin: 10px 0;
}
publication-name{
width: fit-content;
font-size: 16pt;
font-weight: bold;
}
publication img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 0;
border-radius: 5px;
}
popup{
display: none;
width: 100%;
min-height: 100%;
top: 0;
left: 0;
position: fixed;
z-index: 2;
flex-direction: column;
}
popup popup-window {
position: absolute;
z-index: 3;
left: 25%;
top: 100px;
width: 50%;
display: flex;
flex-direction: column;
background-color: var(--second-background-color);
border-radius: 3px;
overflow: hidden;
}
popup popup-window popup-title {
height: 30px;
font-size: 14pt;
font-weight: bolder;
padding: 5px 10px;
margin: 0;
display: flex;
align-items: center;
background-color: var(--primary-color);
color: var(--accent-color)
}
popup popup-window popup-content{
margin: 15px 10px;
display: flex;
align-items: center;
justify-content: space-evenly;
}
popup popup-window popup-content div > * {
margin: 2px 3px 0 0;
}
popup popup-window popup-content input, select {
padding: 3px 4px;
width: 130px;
border: 1px solid lightgrey;
background-color: var(--accent-color);
border-radius: 3px;
}
#selectPublicationPopup publication {
width: 150px;
height: 250px;
}
#createTaskPopup {
z-index: 7;
}
#createTaskPopup input {
height: 30px;
width: 200px;
}
#createMonitorTaskPopup, #createDownloadChaptersTask {
z-index: 9;
}
#createMonitorTaskPopup input[type="number"] {
width: 40px;
}
#createDownloadChaptersTask popup-content {
flex-direction: column;
align-items: start;
}
#createDownloadChaptersTask popup-content > * {
margin: 3px 0;
}
#createDownloadChaptersTask #chapterOutput {
max-height: 50vh;
overflow-y: scroll;
}
#createDownloadChaptersTask #chapterOutput .index{
display: inline-block;
width: 25px;
}
#createDownloadChaptersTask #chapterOutput .index::after{
content: ':';
}
#createDownloadChaptersTask #chapterOutput .vol::before{
content: 'Vol.';
}
#createDownloadChaptersTask #chapterOutput .vol{
display: inline-block;
width: 45px;
}
#createDownloadChaptersTask #chapterOutput .ch::before{
content: 'Ch.';
}
#createDownloadChaptersTask #chapterOutput .ch {
display: inline-block;
width: 60px;
}
#downloadTasksPopup popup-window {
left: 0;
top: 80px;
margin: 0 0 0 10px;
height: calc(100vh - 140px);
width: 400px;
max-width: 95vw;
overflow-y: scroll;
}
#downloadTasksPopup popup-content {
flex-direction: column;
align-items: start;
margin: 5px;
}
#downloadTasksPopup popup-content > div {
display: block;
height: 80px;
position: relative;
margin: 5px 0;
}
#downloadTasksPopup popup-content > div > img {
display: block;
position: absolute;
height: 100%;
width: 60px;
left: 0;
top: 0;
object-fit: cover;
border-radius: 4px;
}
#downloadTasksPopup popup-content > div > span {
display: block;
position: absolute;
width: max-content;
}
#downloadTasksPopup popup-content > div > .pubTitle {
left: 70px;
top: 0;
}
#downloadTasksPopup popup-content > div > .chapterName {
left: 70px;
top: 28pt;
}
#downloadTasksPopup popup-content > div > .chapterNumber {
left: 70px;
top: 14pt;
}
#downloadTasksPopup popup-content > div > progress {
display: block;
position: absolute;
left: 150px;
bottom: 0;
width: 200px;
}
#downloadTasksPopup popup-content > div > .progressStr {
display: block;
position: absolute;
left: 70px;
bottom: 0;
width: 70px;
}
blur-background {
width: 100%;
height: 100%;
position: absolute;
left: 0;
background-color: black;
opacity: 0.5;
}
#taskSelectOutput{
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: start;
align-content: start;
max-height: 70vh;
overflow-y: scroll;
}
#publicationViewerPopup{
z-index: 5;
}
publication-viewer{
display: block;
width: 450px;
position: absolute;
top: 200px;
left: 400px;
background-color: var(--accent-color);
border-radius: 5px;
overflow: hidden;
padding: 15px;
}
publication-viewer::after{
content: '';
position: absolute;
left: 0; top: 0;
border-radius: 5px;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.8);
backdrop-filter: blur(3px);
}
publication-viewer img {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
object-fit: cover;
border-radius: 5px;
z-index: 0;
}
publication-viewer publication-information > * {
margin: 5px 0;
}
publication-viewer publication-information publication-name {
width: initial;
overflow-x: scroll;
white-space: nowrap;
scrollbar-width: none;
}
publication-viewer publication-information publication-tags::before {
content: "Tags";
display: block;
font-weight: bolder;
}
publication-viewer publication-information publication-tags {
overflow-x: scroll;
white-space: nowrap;
scrollbar-width: none;
}
publication-viewer publication-information publication-author::before {
content: "Author: ";
font-weight: bolder;
}
publication-viewer publication-information publication-description::before {
content: "Description";
display: block;
font-weight: bolder;
}
publication-viewer publication-information publication-description {
font-size: 12pt;
margin: 5px 0;
height: 145px;
overflow-x: scroll;
}
publication-viewer publication-information publication-interactions {
display: flex;
flex-direction: row;
justify-content: end;
align-items: start;
width: 100%;
}
publication-viewer publication-information publication-interactions > * {
margin: 0 10px;
font-size: 16pt;
cursor: pointer;
}
publication-viewer publication-information publication-interactions publication-starttask {
color: var(--secondary-color);
}
publication-viewer publication-information publication-interactions publication-delete {
color: red;
}
publication-viewer publication-information publication-interactions publication-add {
color: limegreen;
}
footer-tag-popup {
display: none;
padding: 2px 4px;
position: fixed;
bottom: 58px;
left: 20px;
background-color: var(--second-background-color);
z-index: 8;
border-radius: 5px;
max-height: 400px;
}
footer-tag-content{
position: relative;
max-height: 400px;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
overflow-y: scroll;
}
footer-tag-content > * {
margin: 2px 5px;
}
footer-tag-popup::before{
content: "";
width: 0;
height: 0;
position: absolute;
border-right: 10px solid var(--second-background-color);
border-left: 10px solid transparent;
border-top: 10px solid var(--second-background-color);
border-bottom: 10px solid transparent;
left: 0;
bottom: -17px;
border-radius: 0 0 0 5px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB