mirror of
https://github.com/C9Glax/tranga.git
synced 2025-06-15 07:47:54 +02:00
Trash everything and writing everything from scratch
This commit is contained in:
@ -1,365 +0,0 @@
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using Tranga;
|
||||
using Tranga.Connectors;
|
||||
using Tranga.TrangaTasks;
|
||||
|
||||
namespace Tranga.API;
|
||||
|
||||
public class RequestHandler
|
||||
{
|
||||
private TaskManager _taskManager;
|
||||
private Server _parent;
|
||||
|
||||
private List<ValueTuple<HttpMethod, string, string[]>> _validRequestPaths = new()
|
||||
{
|
||||
new(HttpMethod.Get, "/", Array.Empty<string>()),
|
||||
new(HttpMethod.Get, "/Connectors", Array.Empty<string>()),
|
||||
new(HttpMethod.Get, "/Publications/Known", new[] { "internalId?" }),
|
||||
new(HttpMethod.Get, "/Publications/FromConnector", new[] { "connectorName", "title" }),
|
||||
new(HttpMethod.Get, "/Publications/Chapters",
|
||||
new[] { "connectorName", "internalId", "onlyNew?", "onlyExisting?", "language?" }),
|
||||
new(HttpMethod.Get, "/Tasks/Types", Array.Empty<string>()),
|
||||
new(HttpMethod.Post, "/Tasks/CreateMonitorTask",
|
||||
new[] { "connectorName", "internalId", "reoccurrenceTime", "language?", "ignoreChaptersBelow?" }),
|
||||
new(HttpMethod.Post, "/Tasks/CreateDownloadChaptersTask",
|
||||
new[] { "connectorName", "internalId", "chapters", "language?" }),
|
||||
new(HttpMethod.Get, "/Tasks", new[] { "taskType", "connectorName?", "publicationId?" }),
|
||||
new(HttpMethod.Delete, "/Tasks", new[] { "taskType", "connectorName?", "searchString?" }),
|
||||
new(HttpMethod.Get, "/Tasks/Progress",
|
||||
new[] { "taskType", "connectorName", "publicationId", "chapterSortNumber?" }),
|
||||
new(HttpMethod.Post, "/Tasks/Start", new[] { "taskType", "connectorName?", "internalId?" }),
|
||||
new(HttpMethod.Get, "/Tasks/RunningTasks", Array.Empty<string>()),
|
||||
new(HttpMethod.Get, "/Queue/List", Array.Empty<string>()),
|
||||
new(HttpMethod.Post, "/Queue/Enqueue", new[] { "taskType", "connectorName?", "publicationId?" }),
|
||||
new(HttpMethod.Delete, "/Queue/Dequeue", new[] { "taskType", "connectorName?", "publicationId?" }),
|
||||
new(HttpMethod.Get, "/Settings", Array.Empty<string>()),
|
||||
new(HttpMethod.Post, "/Settings/Update", new[]
|
||||
{
|
||||
"downloadLocation?", "komgaUrl?", "komgaAuth?", "kavitaUrl?", "kavitaUsername?",
|
||||
"kavitaPassword?", "gotifyUrl?", "gotifyAppToken?", "lunaseaWebhook?"
|
||||
})
|
||||
};
|
||||
|
||||
public RequestHandler(TaskManager taskManager, Server parent)
|
||||
{
|
||||
this._taskManager = taskManager;
|
||||
this._parent = parent;
|
||||
}
|
||||
|
||||
internal void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
|
||||
{
|
||||
string requestPath = request.Url!.LocalPath;
|
||||
if (requestPath.Contains("favicon"))
|
||||
{
|
||||
_parent.SendResponse(HttpStatusCode.NoContent, response);
|
||||
return;
|
||||
}
|
||||
if (!this._validRequestPaths.Any(path => path.Item1.Method == request.HttpMethod && path.Item2 == requestPath))
|
||||
{
|
||||
_parent.SendResponse(HttpStatusCode.BadRequest, response);
|
||||
return;
|
||||
}
|
||||
Dictionary<string, string> variables = GetRequestVariables(request.Url!.Query);
|
||||
object? responseObject = null;
|
||||
switch (request.HttpMethod)
|
||||
{
|
||||
case "GET":
|
||||
responseObject = this.HandleGet(requestPath, variables);
|
||||
break;
|
||||
case "POST":
|
||||
this.HandlePost(requestPath, variables);
|
||||
break;
|
||||
case "DELETE":
|
||||
this.HandleDelete(requestPath, variables);
|
||||
break;
|
||||
}
|
||||
_parent.SendResponse(HttpStatusCode.OK, response, responseObject);
|
||||
}
|
||||
|
||||
private Dictionary<string, string> GetRequestVariables(string query)
|
||||
{
|
||||
Dictionary<string, string> ret = new();
|
||||
Regex queryRex = new (@"\?{1}&?([A-z0-9-=]+=[A-z0-9-=]+)+(&[A-z0-9-=]+=[A-z0-9-=]+)*");
|
||||
if (!queryRex.IsMatch(query))
|
||||
return ret;
|
||||
query = query.Substring(1);
|
||||
foreach (string kvpair in query.Split('&').Where(str => str.Length >= 3))
|
||||
{
|
||||
string var = kvpair.Split('=')[0];
|
||||
string val = Regex.Replace(kvpair.Substring(var.Length + 1), "%20", " ");
|
||||
val = Regex.Replace(val, "%[0-9]{2}", "");
|
||||
ret.Add(var, val);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private void HandleDelete(string requestPath, Dictionary<string, string> variables)
|
||||
{
|
||||
switch (requestPath)
|
||||
{
|
||||
case "/Tasks":
|
||||
variables.TryGetValue("taskType", out string? taskType1);
|
||||
variables.TryGetValue("connectorName", out string? connectorName1);
|
||||
variables.TryGetValue("publicationId", out string? publicationId1);
|
||||
if(taskType1 is null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
TrangaTask.Task task = Enum.Parse<TrangaTask.Task>(taskType1);
|
||||
foreach(TrangaTask tTask in _taskManager.GetTasksMatching(task, connectorName1, internalId: publicationId1))
|
||||
_taskManager.DeleteTask(tTask);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "/Queue/Dequeue":
|
||||
variables.TryGetValue("taskType", out string? taskType2);
|
||||
variables.TryGetValue("connectorName", out string? connectorName2);
|
||||
variables.TryGetValue("publicationId", out string? publicationId2);
|
||||
if(taskType2 is null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType2);
|
||||
TrangaTask? task = _taskManager
|
||||
.GetTasksMatching(pTask, connectorName: connectorName2, internalId: publicationId2).FirstOrDefault();
|
||||
|
||||
if (task is null)
|
||||
return;
|
||||
_taskManager.RemoveTaskFromQueue(task);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePost(string requestPath, Dictionary<string, string> variables)
|
||||
{
|
||||
switch (requestPath)
|
||||
{
|
||||
|
||||
case "/Tasks/CreateMonitorTask":
|
||||
variables.TryGetValue("connectorName", out string? connectorName1);
|
||||
variables.TryGetValue("internalId", out string? internalId1);
|
||||
variables.TryGetValue("reoccurrenceTime", out string? reoccurrenceTime1);
|
||||
variables.TryGetValue("language", out string? language1);
|
||||
variables.TryGetValue("ignoreChaptersBelow", out string? minChapter);
|
||||
if (connectorName1 is null || internalId1 is null || reoccurrenceTime1 is null)
|
||||
return;
|
||||
Connector? connector1 =
|
||||
_taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName1).Value;
|
||||
if (connector1 is null)
|
||||
return;
|
||||
Publication? publication1 = _taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == internalId1);
|
||||
if (!publication1.HasValue)
|
||||
return;
|
||||
Publication pPublication1 = (Publication)publication1;
|
||||
if (minChapter is not null)
|
||||
pPublication1.ignoreChaptersBelow = float.Parse(minChapter,new NumberFormatInfo() { NumberDecimalSeparator = "." });
|
||||
_taskManager.AddTask(new MonitorPublicationTask(connectorName1, pPublication1, TimeSpan.Parse(reoccurrenceTime1), language1 ?? "en"));
|
||||
break;
|
||||
case "/Tasks/CreateDownloadChaptersTask":
|
||||
variables.TryGetValue("connectorName", out string? connectorName2);
|
||||
variables.TryGetValue("internalId", out string? internalId2);
|
||||
variables.TryGetValue("chapters", out string? chapters);
|
||||
variables.TryGetValue("language", out string? language2);
|
||||
if (connectorName2 is null || internalId2 is null || chapters is null)
|
||||
return;
|
||||
Connector? connector2 =
|
||||
_taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName2).Value;
|
||||
if (connector2 is null)
|
||||
return;
|
||||
Publication? publication2 = _taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == internalId2);
|
||||
if (publication2 is null)
|
||||
return;
|
||||
|
||||
IEnumerable<Chapter> toDownload = connector2.SelectChapters((Publication)publication2, chapters, language2 ?? "en");
|
||||
foreach(Chapter chapter in toDownload)
|
||||
_taskManager.AddTask(new DownloadChapterTask(connectorName2, (Publication)publication2, chapter, "en"));
|
||||
break;
|
||||
case "/Tasks/Start":
|
||||
variables.TryGetValue("taskType", out string? taskType1);
|
||||
variables.TryGetValue("connectorName", out string? connectorName3);
|
||||
variables.TryGetValue("internalId", out string? internalId3);
|
||||
if (taskType1 is null)
|
||||
return;
|
||||
try
|
||||
{
|
||||
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType1);
|
||||
TrangaTask? task = _taskManager
|
||||
.GetTasksMatching(pTask, connectorName: connectorName3, internalId: internalId3).FirstOrDefault();
|
||||
|
||||
if (task is null)
|
||||
return;
|
||||
_taskManager.ExecuteTaskNow(task);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "/Queue/Enqueue":
|
||||
variables.TryGetValue("taskType", out string? taskType2);
|
||||
variables.TryGetValue("connectorName", out string? connectorName4);
|
||||
variables.TryGetValue("publicationId", out string? publicationId);
|
||||
if (taskType2 is null)
|
||||
return;
|
||||
try
|
||||
{
|
||||
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType2);
|
||||
TrangaTask? task = _taskManager
|
||||
.GetTasksMatching(pTask, connectorName: connectorName4, internalId: publicationId).FirstOrDefault();
|
||||
|
||||
if (task is null)
|
||||
return;
|
||||
_taskManager.AddTaskToQueue(task);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "/Settings/Update":
|
||||
variables.TryGetValue("downloadLocation", out string? downloadLocation);
|
||||
variables.TryGetValue("komgaUrl", out string? komgaUrl);
|
||||
variables.TryGetValue("komgaAuth", out string? komgaAuth);
|
||||
variables.TryGetValue("kavitaUrl", out string? kavitaUrl);
|
||||
variables.TryGetValue("kavitaUsername", out string? kavitaUsername);
|
||||
variables.TryGetValue("kavitaPassword", out string? kavitaPassword);
|
||||
variables.TryGetValue("gotifyUrl", out string? gotifyUrl);
|
||||
variables.TryGetValue("gotifyAppToken", out string? gotifyAppToken);
|
||||
variables.TryGetValue("lunaseaWebhook", out string? lunaseaWebhook);
|
||||
|
||||
if (downloadLocation is not null && downloadLocation.Length > 0)
|
||||
_taskManager.settings.UpdateSettings(TrangaSettings.UpdateField.DownloadLocation, downloadLocation);
|
||||
if (komgaUrl is not null && komgaAuth is not null && komgaUrl.Length > 5 && komgaAuth.Length > 0)
|
||||
_taskManager.settings.UpdateSettings(TrangaSettings.UpdateField.Komga, komgaUrl, komgaAuth);
|
||||
if (kavitaUrl is not null && kavitaPassword is not null && kavitaUsername is not null && kavitaUrl.Length > 5 &&
|
||||
kavitaUsername.Length > 0 && kavitaPassword.Length > 0)
|
||||
_taskManager.settings.UpdateSettings(TrangaSettings.UpdateField.Kavita, kavitaUrl, kavitaUsername,
|
||||
kavitaPassword);
|
||||
if (gotifyUrl is not null && gotifyAppToken is not null && gotifyUrl.Length > 5 && gotifyAppToken.Length > 0)
|
||||
_taskManager.settings.UpdateSettings(TrangaSettings.UpdateField.Gotify, gotifyUrl, gotifyAppToken);
|
||||
if(lunaseaWebhook is not null && lunaseaWebhook.Length > 5)
|
||||
_taskManager.settings.UpdateSettings(TrangaSettings.UpdateField.LunaSea, lunaseaWebhook);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private object? HandleGet(string requestPath, Dictionary<string, string> variables)
|
||||
{
|
||||
switch (requestPath)
|
||||
{
|
||||
case "/Connectors":
|
||||
return this._taskManager.GetAvailableConnectors().Keys.ToArray();
|
||||
case "/Publications/Known":
|
||||
variables.TryGetValue("internalId", out string? internalId1);
|
||||
if(internalId1 is null)
|
||||
return _taskManager.GetAllPublications();
|
||||
return new [] { _taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == internalId1) };
|
||||
case "/Publications/FromConnector":
|
||||
variables.TryGetValue("connectorName", out string? connectorName1);
|
||||
variables.TryGetValue("title", out string? title);
|
||||
if (connectorName1 is null || title is null)
|
||||
return null;
|
||||
Connector? connector1 = _taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName1).Value;
|
||||
if (connector1 is null)
|
||||
return null;
|
||||
if(title.Length < 4)
|
||||
return null;
|
||||
return connector1.GetPublications(ref _taskManager.collection, title);
|
||||
case "/Publications/Chapters":
|
||||
string[] yes = { "true", "yes", "1", "y" };
|
||||
variables.TryGetValue("connectorName", out string? connectorName2);
|
||||
variables.TryGetValue("internalId", out string? internalId2);
|
||||
variables.TryGetValue("onlyNew", out string? onlyNew);
|
||||
variables.TryGetValue("onlyExisting", out string? onlyExisting);
|
||||
variables.TryGetValue("language", out string? language);
|
||||
if (connectorName2 is null || internalId2 is null)
|
||||
return null;
|
||||
bool newOnly = onlyNew is not null && yes.Contains(onlyNew);
|
||||
bool existingOnly = onlyExisting is not null && yes.Contains(onlyExisting);
|
||||
|
||||
Connector? connector2 = _taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName2).Value;
|
||||
if (connector2 is null)
|
||||
return null;
|
||||
Publication? publication = _taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == internalId2);
|
||||
if (publication is null)
|
||||
return null;
|
||||
|
||||
if(newOnly)
|
||||
return connector2.GetNewChaptersList((Publication)publication, language??"en", ref _taskManager.collection).ToArray();
|
||||
else if (existingOnly)
|
||||
return _taskManager.GetExistingChaptersList(connector2, (Publication)publication, language ?? "en").ToArray();
|
||||
else
|
||||
return connector2.GetChapters((Publication)publication, language??"en");
|
||||
case "/Tasks/Types":
|
||||
return Enum.GetNames(typeof(TrangaTask.Task));
|
||||
case "/Tasks":
|
||||
variables.TryGetValue("taskType", out string? taskType1);
|
||||
variables.TryGetValue("connectorName", out string? connectorName3);
|
||||
variables.TryGetValue("searchString", out string? searchString);
|
||||
if (taskType1 is null)
|
||||
return null;
|
||||
try
|
||||
{
|
||||
TrangaTask.Task task = Enum.Parse<TrangaTask.Task>(taskType1);
|
||||
return _taskManager.GetTasksMatching(task, connectorName:connectorName3, searchString:searchString);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
case "/Tasks/Progress":
|
||||
variables.TryGetValue("taskType", out string? taskType2);
|
||||
variables.TryGetValue("connectorName", out string? connectorName4);
|
||||
variables.TryGetValue("publicationId", out string? publicationId);
|
||||
variables.TryGetValue("chapterNumber", out string? chapterNumber);
|
||||
if (taskType2 is null || connectorName4 is null || publicationId is null)
|
||||
return null;
|
||||
Connector? connector =
|
||||
_taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName4).Value;
|
||||
if (connector is null)
|
||||
return null;
|
||||
try
|
||||
{
|
||||
TrangaTask? task = null;
|
||||
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType2);
|
||||
if (pTask is TrangaTask.Task.MonitorPublication)
|
||||
{
|
||||
task = _taskManager.GetTasksMatching(pTask, connectorName: connectorName4, internalId: publicationId).FirstOrDefault();
|
||||
}else if (pTask is TrangaTask.Task.DownloadChapter && chapterNumber is not null)
|
||||
{
|
||||
task = _taskManager.GetTasksMatching(pTask, connectorName: connectorName4, internalId: publicationId,
|
||||
chapterNumber: chapterNumber).FirstOrDefault();
|
||||
}
|
||||
if (task is null)
|
||||
return null;
|
||||
|
||||
return task.progress;
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
case "/Tasks/RunningTasks":
|
||||
return _taskManager.GetAllTasks().Where(task => task.state is TrangaTask.ExecutionState.Running);
|
||||
case "/Queue/List":
|
||||
return _taskManager.GetAllTasks().Where(task => task.state is TrangaTask.ExecutionState.Enqueued).OrderBy(task => task.nextExecution);
|
||||
case "/Settings":
|
||||
return _taskManager.settings;
|
||||
case "/":
|
||||
default:
|
||||
return this._validRequestPaths;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
using System.Net;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Tranga;
|
||||
|
||||
namespace Tranga.API;
|
||||
|
||||
public class Server
|
||||
{
|
||||
private readonly HttpListener _listener = new ();
|
||||
private readonly RequestHandler _requestHandler;
|
||||
private readonly TaskManager _taskManager;
|
||||
internal readonly Logger? logger;
|
||||
|
||||
private readonly Regex _validUrl =
|
||||
new (@"https?:\/\/(www\.)?[-A-z0-9]{1,256}(\.[-a-zA-Z0-9]{1,6})?(:[0-9]{1,5})?(\/{1}[A-z0-9()@:%_\+.~#?&=]+)*\/?");
|
||||
public Server(int port, TaskManager taskManager, Logger? logger = null)
|
||||
{
|
||||
this.logger = logger;
|
||||
this._taskManager = taskManager;
|
||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
this._listener.Prefixes.Add($"http://*:{port}/");
|
||||
else
|
||||
this._listener.Prefixes.Add($"http://localhost:{port}/");
|
||||
this._requestHandler = new RequestHandler(taskManager, this);
|
||||
Thread listenThread = new Thread(Listen);
|
||||
listenThread.Start();
|
||||
}
|
||||
|
||||
private void Listen()
|
||||
{
|
||||
this._listener.Start();
|
||||
foreach (string prefix in this._listener.Prefixes)
|
||||
this.logger?.WriteLine(this.GetType().ToString(), $"Listening on {prefix}");
|
||||
while (this._listener.IsListening && _taskManager._continueRunning)
|
||||
{
|
||||
HttpListenerContext context = this._listener.GetContextAsync().Result;
|
||||
Task t = new (() =>
|
||||
{
|
||||
HandleContext(context);
|
||||
});
|
||||
t.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleContext(HttpListenerContext context)
|
||||
{
|
||||
HttpListenerRequest request = context.Request;
|
||||
HttpListenerResponse response = context.Response;
|
||||
//logger?.WriteLine(this.GetType().ToString(), $"New request: {request.HttpMethod} {request.Url}");
|
||||
|
||||
if (!_validUrl.IsMatch(request.Url!.ToString()))
|
||||
{
|
||||
SendResponse(HttpStatusCode.BadRequest, response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.HttpMethod == "OPTIONS")
|
||||
{
|
||||
SendResponse(HttpStatusCode.OK, response);
|
||||
}
|
||||
else
|
||||
{
|
||||
_requestHandler.HandleRequest(request, response);
|
||||
}
|
||||
}
|
||||
|
||||
internal void SendResponse(HttpStatusCode statusCode, HttpListenerResponse response, object? content = null)
|
||||
{
|
||||
//logger?.WriteLine(this.GetType().ToString(), $"Sending response: {statusCode}");
|
||||
response.StatusCode = (int)statusCode;
|
||||
response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With");
|
||||
response.AddHeader("Access-Control-Allow-Methods", "GET, POST, DELETE");
|
||||
response.AddHeader("Access-Control-Max-Age", "1728000");
|
||||
response.AppendHeader("Access-Control-Allow-Origin", "*");
|
||||
response.ContentType = "application/json";
|
||||
try
|
||||
{
|
||||
response.OutputStream.Write(content is not null
|
||||
? Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content))
|
||||
: Array.Empty<byte>());
|
||||
response.OutputStream.Close();
|
||||
}
|
||||
catch (HttpListenerException)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>();
|
||||
}
|
||||
|
@ -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>();
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
34
Tranga/LibraryConnectors/LibraryManagerJsonConverter.cs
Normal file
34
Tranga/LibraryConnectors/LibraryManagerJsonConverter.cs
Normal 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");
|
||||
}
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Tranga.LibraryManagers;
|
||||
using Tranga.NotificationManagers;
|
||||
using Tranga.TrangaTasks;
|
||||
|
||||
namespace Tranga;
|
||||
|
||||
public static class Migrator
|
||||
{
|
||||
internal static readonly ushort CurrentVersion = 17;
|
||||
public static void Migrate(string settingsFilePath, Logger? logger)
|
||||
{
|
||||
if (!File.Exists(settingsFilePath))
|
||||
return;
|
||||
JsonNode settingsNode = JsonNode.Parse(File.ReadAllText(settingsFilePath))!;
|
||||
ushort version = settingsNode["version"] is not null
|
||||
? settingsNode["version"]!.GetValue<ushort>()
|
||||
: settingsNode["ts"]!["version"]!.GetValue<ushort>();
|
||||
logger?.WriteLine("Migrator", $"Migrating {version} -> {CurrentVersion}");
|
||||
switch (version)
|
||||
{
|
||||
case 15:
|
||||
MoveToCommonObjects(settingsFilePath, logger);
|
||||
TrangaSettings.SettingsJsonObject sjo = JsonConvert.DeserializeObject<TrangaSettings.SettingsJsonObject>(File.ReadAllText(settingsFilePath))!;
|
||||
RemoveUpdateLibraryTask(sjo.ts!, logger);
|
||||
break;
|
||||
case 16:
|
||||
MoveToCommonObjects(settingsFilePath, logger);
|
||||
break;
|
||||
}
|
||||
|
||||
TrangaSettings.SettingsJsonObject sjo2 = JsonConvert.DeserializeObject<TrangaSettings.SettingsJsonObject>(
|
||||
File.ReadAllText(settingsFilePath),
|
||||
new JsonSerializerSettings
|
||||
{
|
||||
Converters =
|
||||
{
|
||||
new TrangaTask.TrangaTaskJsonConverter(),
|
||||
new NotificationManager.NotificationManagerJsonConverter(),
|
||||
new LibraryManager.LibraryManagerJsonConverter()
|
||||
}
|
||||
})!;
|
||||
sjo2.ts!.version = CurrentVersion;
|
||||
sjo2.ts!.ExportSettings();
|
||||
}
|
||||
|
||||
private static void RemoveUpdateLibraryTask(TrangaSettings settings, Logger? logger)
|
||||
{
|
||||
if (!File.Exists(settings.tasksFilePath))
|
||||
return;
|
||||
|
||||
logger?.WriteLine("Migrator", "Removing old/deprecated UpdateLibraryTasks (v16)");
|
||||
string tasksJsonString = File.ReadAllText(settings.tasksFilePath);
|
||||
HashSet<TrangaTask> tasks = JsonConvert.DeserializeObject<HashSet<TrangaTask>>(tasksJsonString,
|
||||
new JsonSerializerSettings { Converters = { new TrangaTask.TrangaTaskJsonConverter() } })!;
|
||||
tasks.RemoveWhere(t => t.task == TrangaTask.Task.UpdateLibraries);
|
||||
File.WriteAllText(settings.tasksFilePath, JsonConvert.SerializeObject(tasks));
|
||||
}
|
||||
|
||||
public static void MoveToCommonObjects(string settingsFilePath, Logger? logger)
|
||||
{
|
||||
if (!File.Exists(settingsFilePath))
|
||||
return;
|
||||
|
||||
logger?.WriteLine("Migrator", "Moving Settings to commonObjects-structure (v17)");
|
||||
JsonNode node = JsonNode.Parse(File.ReadAllText(settingsFilePath))!;
|
||||
TrangaSettings ts = new(
|
||||
node["downloadLocation"]!.GetValue<string>(),
|
||||
node["workingDirectory"]!.GetValue<string>());
|
||||
JsonArray libraryManagers = node["libraryManagers"]!.AsArray();
|
||||
logger?.WriteLine("Migrator", $"\tGot {libraryManagers.Count} libraryManagers.");
|
||||
JsonNode? komgaNode = libraryManagers.FirstOrDefault(lm => lm["libraryType"].GetValue<byte>() == (byte)LibraryManager.LibraryType.Komga);
|
||||
JsonNode? kavitaNode = libraryManagers.FirstOrDefault(lm => lm["libraryType"].GetValue<byte>() == (byte)LibraryManager.LibraryType.Kavita);
|
||||
HashSet<LibraryManager> lms = new();
|
||||
if (komgaNode is not null)
|
||||
lms.Add(new Komga(komgaNode["baseUrl"]!.GetValue<string>(), komgaNode["auth"]!.GetValue<string>(), null));
|
||||
if (kavitaNode is not null)
|
||||
lms.Add(new Kavita(kavitaNode["baseUrl"]!.GetValue<string>(), kavitaNode["auth"]!.GetValue<string>(), null));
|
||||
|
||||
JsonArray notificationManagers = node["notificationManagers"]!.AsArray();
|
||||
logger?.WriteLine("Migrator", $"\tGot {notificationManagers.Count} notificationManagers.");
|
||||
JsonNode? gotifyNode = notificationManagers.FirstOrDefault(nm =>
|
||||
nm["notificationManagerType"].GetValue<byte>() == (byte)NotificationManager.NotificationManagerType.Gotify);
|
||||
JsonNode? lunaSeaNode = notificationManagers.FirstOrDefault(nm =>
|
||||
nm["notificationManagerType"].GetValue<byte>() == (byte)NotificationManager.NotificationManagerType.LunaSea);
|
||||
HashSet<NotificationManager> nms = new();
|
||||
if (gotifyNode is not null)
|
||||
nms.Add(new Gotify(gotifyNode["endpoint"]!.GetValue<string>(), gotifyNode["appToken"]!.GetValue<string>()));
|
||||
if (lunaSeaNode is not null)
|
||||
nms.Add(new LunaSea(lunaSeaNode["id"]!.GetValue<string>()));
|
||||
|
||||
CommonObjects co = new (lms, nms, logger, settingsFilePath);
|
||||
|
||||
TrangaSettings.SettingsJsonObject sjo = new(ts, co);
|
||||
File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(sjo));
|
||||
}
|
||||
}
|
@ -1,10 +1,9 @@
|
||||
using System.Text;
|
||||
using 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);
|
@ -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()}");
|
||||
}
|
||||
}
|
||||
|
15
Tranga/NotificationConnectors/NotificationConnector.cs
Normal file
15
Tranga/NotificationConnectors/NotificationConnector.cs
Normal 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);
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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
68
Tranga/TBaseObject.cs
Normal 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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
580
Tranga/Tranga.cs
580
Tranga/Tranga.cs
@ -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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
using System.Net;
|
||||
using Tranga.Connectors;
|
||||
using Tranga.NotificationManagers;
|
||||
using Tranga.LibraryManagers;
|
||||
|
||||
namespace Tranga.TrangaTasks;
|
||||
|
||||
public class DownloadChapterTask : TrangaTask
|
||||
{
|
||||
public string connectorName { get; }
|
||||
public Publication publication { get; }
|
||||
// ReSharper disable once MemberCanBePrivate.Global
|
||||
public string language { get; }
|
||||
public Chapter chapter { get; }
|
||||
|
||||
private double _dctProgress;
|
||||
|
||||
public DownloadChapterTask(string connectorName, Publication publication, Chapter chapter, string language = "en", MonitorPublicationTask? parentTask = null) : base(Task.DownloadChapter, TimeSpan.Zero, parentTask)
|
||||
{
|
||||
this.chapter = chapter;
|
||||
this.connectorName = connectorName;
|
||||
this.publication = publication;
|
||||
this.language = language;
|
||||
}
|
||||
|
||||
protected override HttpStatusCode ExecuteTask(TaskManager taskManager, CancellationToken? cancellationToken = null)
|
||||
{
|
||||
if (cancellationToken?.IsCancellationRequested ?? false)
|
||||
return HttpStatusCode.RequestTimeout;
|
||||
Connector connector = taskManager.GetConnector(this.connectorName);
|
||||
connector.CopyCoverFromCacheToDownloadLocation(this.publication);
|
||||
HttpStatusCode downloadSuccess = connector.DownloadChapter(this.publication, this.chapter, this, cancellationToken);
|
||||
if ((int)downloadSuccess >= 200 && (int)downloadSuccess < 300)
|
||||
{
|
||||
foreach(NotificationManager nm in taskManager.commonObjects.notificationManagers)
|
||||
nm.SendNotification("Chapter downloaded", $"{this.publication.sortName} {this.chapter.chapterNumber} {this.chapter.name}");
|
||||
|
||||
foreach (LibraryManager lm in taskManager.commonObjects.libraryManagers)
|
||||
lm.UpdateLibrary();
|
||||
}
|
||||
return downloadSuccess;
|
||||
}
|
||||
|
||||
public override TrangaTask Clone()
|
||||
{
|
||||
return new DownloadChapterTask(this.connectorName, this.publication, this.chapter,
|
||||
this.language, (MonitorPublicationTask?)this.parentTask);
|
||||
}
|
||||
|
||||
protected override double GetProgress()
|
||||
{
|
||||
return _dctProgress;
|
||||
}
|
||||
|
||||
internal void IncrementProgress(double amount)
|
||||
{
|
||||
this._dctProgress += amount;
|
||||
this.lastChange = DateTime.Now;
|
||||
if(this.parentTask is not null)
|
||||
this.parentTask.lastChange = DateTime.Now;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()}, {connectorName}, {publication.sortName} {publication.internalId}, Vol.{chapter.volumeNumber} Ch.{chapter.chapterNumber}";
|
||||
}
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
using System.Net;
|
||||
using Tranga.Connectors;
|
||||
|
||||
namespace Tranga.TrangaTasks;
|
||||
|
||||
public class MonitorPublicationTask : TrangaTask
|
||||
{
|
||||
public string connectorName { get; }
|
||||
public Publication publication { get; }
|
||||
// ReSharper disable once MemberCanBePrivate.Global
|
||||
public string language { get; }
|
||||
public MonitorPublicationTask(string connectorName, Publication publication, TimeSpan reoccurrence, string language = "en") : base(Task.MonitorPublication, reoccurrence)
|
||||
{
|
||||
this.connectorName = connectorName;
|
||||
this.publication = publication;
|
||||
this.language = language;
|
||||
}
|
||||
|
||||
protected override HttpStatusCode ExecuteTask(TaskManager taskManager, CancellationToken? cancellationToken = null)
|
||||
{
|
||||
if (cancellationToken?.IsCancellationRequested ?? false)
|
||||
return HttpStatusCode.RequestTimeout;
|
||||
Connector connector = taskManager.GetConnector(this.connectorName);
|
||||
|
||||
//Check if Publication already has a Folder
|
||||
publication.CreatePublicationFolder(taskManager.settings.downloadLocation);
|
||||
List<Chapter> newChapters = connector.GetNewChaptersList(publication, language, ref taskManager.collection);
|
||||
|
||||
connector.CopyCoverFromCacheToDownloadLocation(publication);
|
||||
|
||||
publication.SaveSeriesInfoJson(taskManager.settings.downloadLocation);
|
||||
|
||||
foreach (Chapter newChapter in newChapters)
|
||||
{
|
||||
DownloadChapterTask newTask = new (this.connectorName, publication, newChapter, this.language, this);
|
||||
this.childTasks.Add(newTask);
|
||||
newTask.state = ExecutionState.Enqueued;
|
||||
taskManager.AddTask(newTask);
|
||||
}
|
||||
|
||||
return HttpStatusCode.OK;
|
||||
}
|
||||
|
||||
public override TrangaTask Clone()
|
||||
{
|
||||
return new MonitorPublicationTask(this.connectorName, this.publication, this.reoccurrence,
|
||||
this.language);
|
||||
}
|
||||
|
||||
protected override double GetProgress()
|
||||
{
|
||||
if (this.childTasks.Count > 0)
|
||||
return this.childTasks.Sum(ct => ct.progress) / childTasks.Count;
|
||||
return 1;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()}, {connectorName}, {publication.sortName} {publication.internalId}";
|
||||
}
|
||||
}
|
@ -1,157 +0,0 @@
|
||||
using System.Net;
|
||||
using System.Text.Json.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using JsonConverter = Newtonsoft.Json.JsonConverter;
|
||||
|
||||
namespace Tranga.TrangaTasks;
|
||||
|
||||
/// <summary>
|
||||
/// Stores information on Task, when implementing new Tasks also update the serializer
|
||||
/// </summary>
|
||||
[JsonDerivedType(typeof(MonitorPublicationTask), 2)]
|
||||
[JsonDerivedType(typeof(UpdateLibrariesTask), 3)]
|
||||
[JsonDerivedType(typeof(DownloadChapterTask), 4)]
|
||||
public abstract class TrangaTask
|
||||
{
|
||||
// ReSharper disable once MemberCanBeProtected.Global
|
||||
public TimeSpan reoccurrence { get; }
|
||||
public DateTime lastExecuted { get; set; }
|
||||
[Newtonsoft.Json.JsonIgnore] public ExecutionState state { get; set; }
|
||||
public Task task { get; }
|
||||
public string taskId { get; init; }
|
||||
[Newtonsoft.Json.JsonIgnore] public TrangaTask? parentTask { get; set; }
|
||||
public string? parentTaskId { get; set; }
|
||||
[Newtonsoft.Json.JsonIgnore] internal HashSet<TrangaTask> childTasks { get; }
|
||||
public double progress => GetProgress();
|
||||
// ReSharper disable once MemberCanBePrivate.Global
|
||||
[Newtonsoft.Json.JsonIgnore]public DateTime executionStarted { get; private set; }
|
||||
[Newtonsoft.Json.JsonIgnore]public DateTime lastChange { get; internal set; }
|
||||
// ReSharper disable once MemberCanBePrivate.Global
|
||||
[Newtonsoft.Json.JsonIgnore]public DateTime executionApproximatelyFinished => lastChange.Add(GetRemainingTime());
|
||||
// ReSharper disable once MemberCanBePrivate.Global
|
||||
public TimeSpan executionApproximatelyRemaining => executionApproximatelyFinished.Subtract(DateTime.Now);
|
||||
[Newtonsoft.Json.JsonIgnore]public DateTime nextExecution => lastExecuted.Add(reoccurrence);
|
||||
|
||||
public enum ExecutionState { Waiting, Enqueued, Running, Failed, Success }
|
||||
|
||||
protected TrangaTask(Task task, TimeSpan reoccurrence, TrangaTask? parentTask = null)
|
||||
{
|
||||
this.reoccurrence = reoccurrence;
|
||||
this.lastExecuted = DateTime.Now.Subtract(reoccurrence);
|
||||
this.task = task;
|
||||
this.executionStarted = DateTime.UnixEpoch;
|
||||
this.lastChange = DateTime.MaxValue;
|
||||
this.taskId = Convert.ToBase64String(BitConverter.GetBytes(new Random().Next()));
|
||||
this.childTasks = new();
|
||||
this.parentTask = parentTask;
|
||||
this.parentTaskId = parentTask?.taskId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BL for concrete Tasks
|
||||
/// </summary>
|
||||
/// <param name="taskManager"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
protected abstract HttpStatusCode ExecuteTask(TaskManager taskManager, CancellationToken? cancellationToken = null);
|
||||
|
||||
public abstract TrangaTask Clone();
|
||||
|
||||
protected abstract double GetProgress();
|
||||
|
||||
/// <summary>
|
||||
/// Execute the task
|
||||
/// </summary>
|
||||
/// <param name="taskManager">Should be the parent taskManager</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
public void Execute(TaskManager taskManager, CancellationToken? cancellationToken = null)
|
||||
{
|
||||
taskManager.commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Executing Task {this}");
|
||||
this.state = ExecutionState.Running;
|
||||
this.executionStarted = DateTime.Now;
|
||||
this.lastChange = DateTime.Now;
|
||||
if(parentTask is not null && parentTask.childTasks.All(ct => ct.state is ExecutionState.Waiting or ExecutionState.Failed))
|
||||
parentTask.executionStarted = DateTime.Now;
|
||||
|
||||
HttpStatusCode statusCode = ExecuteTask(taskManager, cancellationToken);
|
||||
|
||||
if ((int)statusCode >= 200 && (int)statusCode < 300)
|
||||
{
|
||||
this.lastExecuted = DateTime.Now;
|
||||
this.state = ExecutionState.Success;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.state = ExecutionState.Failed;
|
||||
this.lastExecuted = DateTime.MaxValue;
|
||||
}
|
||||
|
||||
if (this is DownloadChapterTask)
|
||||
taskManager.DeleteTask(this);
|
||||
|
||||
taskManager.commonObjects.logger?.WriteLine(this.GetType().ToString(), $"Finished Executing Task {this}");
|
||||
}
|
||||
|
||||
public void AddChildTask(TrangaTask childTask)
|
||||
{
|
||||
this.childTasks.Add(childTask);
|
||||
}
|
||||
|
||||
public void RemoveChildTask(TrangaTask childTask)
|
||||
{
|
||||
this.childTasks.Remove(childTask);
|
||||
}
|
||||
|
||||
private TimeSpan GetRemainingTime()
|
||||
{
|
||||
if(progress == 0 || state is ExecutionState.Enqueued or ExecutionState.Waiting or ExecutionState.Failed || lastChange == DateTime.MaxValue)
|
||||
return DateTime.MaxValue.Subtract(lastChange).Subtract(TimeSpan.FromHours(1));
|
||||
TimeSpan elapsed = lastChange.Subtract(executionStarted);
|
||||
return elapsed.Divide(progress).Multiply(1 - progress);
|
||||
}
|
||||
|
||||
public enum Task : byte
|
||||
{
|
||||
MonitorPublication = 2,
|
||||
UpdateLibraries = 3,
|
||||
DownloadChapter = 4,
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{task}, {lastExecuted}, {reoccurrence}, {state}, {progress:P2}, {executionApproximatelyFinished}, {executionApproximatelyRemaining}";
|
||||
}
|
||||
|
||||
public class TrangaTaskJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(TrangaTask);
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
JObject jo = JObject.Load(reader);
|
||||
if (jo["task"]!.Value<Int64>() == (Int64)Task.MonitorPublication)
|
||||
return jo.ToObject<MonitorPublicationTask>(serializer)!;
|
||||
|
||||
if (jo["task"]!.Value<Int64>() == (Int64)Task.UpdateLibraries)
|
||||
return jo.ToObject<UpdateLibrariesTask>(serializer)!;
|
||||
|
||||
if (jo["task"]!.Value<Int64>() == (Int64)Task.DownloadChapter)
|
||||
return jo.ToObject<DownloadChapterTask>(serializer)!;
|
||||
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
public override bool CanWrite => false;
|
||||
|
||||
/// <summary>
|
||||
/// Don't call this
|
||||
/// </summary>
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
throw new Exception("Dont call this");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
using System.Net;
|
||||
|
||||
namespace Tranga.TrangaTasks;
|
||||
|
||||
/// <summary>
|
||||
/// LEGACY DEPRECATED
|
||||
/// </summary>
|
||||
public class UpdateLibrariesTask : TrangaTask
|
||||
{
|
||||
public UpdateLibrariesTask(TimeSpan reoccurrence) : base(Task.UpdateLibraries, reoccurrence)
|
||||
{
|
||||
}
|
||||
|
||||
protected override HttpStatusCode ExecuteTask(TaskManager taskManager, CancellationToken? cancellationToken = null)
|
||||
{
|
||||
return HttpStatusCode.BadRequest;
|
||||
}
|
||||
|
||||
public override TrangaTask Clone()
|
||||
{
|
||||
return new UpdateLibrariesTask(this.reoccurrence);
|
||||
}
|
||||
|
||||
protected override double GetProgress()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user