82 Commits
1.4 ... 1.5

Author SHA1 Message Date
cdd2d94ba1 Wrote my own Http-Server.
ASP-NET can **** my **** and *** :)
2023-06-20 23:15:56 +02:00
d5b7645cd2 "Thread-safe" message adding.. 2023-06-20 23:15:22 +02:00
9af5c1603e Using HttpStatusCode to signify Task-Success
When DownloadChapterTask returns notfound, do not retry.
2023-06-20 15:46:54 +02:00
1035939309 Fix overflow 2023-06-20 15:18:58 +02:00
3b542c04f6 ReShaper cleanup,
Remove unnecessary using directives
2023-06-20 14:59:08 +02:00
a809b7c285 Added timeout to Connector DownloadClient 2023-06-20 14:58:02 +02:00
e883277400 Renamed DownloadNewChaptersTask to MonitorPublicationTask
Added TrangaTask.Clone() method
Rewrote TrangaTask.progress for the billionth+1 time.
Removed Increment and DecrementProgress methods
Removed TrangaTask.ReplaceFailedChildTask method
Changed return type of TrangaTask.ExecuteTask to bool, signifying success.
Added Failed Execution state to TrangaTask
Replaced taskManager failed-task logic
Removed TaskManager bulky AddTask and DeleteTask methods
Removed TaskManager bulky Constructor
2023-06-20 14:57:44 +02:00
23dfdc0933 Connector DownloadChapter, DownloadImage, DownloadChapterImages returns successState.
RequestResult replace HttpStatusCode with success-status boolean.
DownloadChapterTask: Only send Notification when Chapter download successful
2023-06-19 22:45:33 +02:00
edc24fff5b Moved notification to DownloadChapterTask, sends when parentTask exists. 2023-06-19 22:34:34 +02:00
6cdccdf66b Fix infinite loop of DownloadNewChaptersTask 2023-06-19 22:32:32 +02:00
a4c9168551 Selector for task-sanitizer 2023-06-19 17:17:47 +02:00
821a1b7c3a Unique IDs for TrangaTask now based on Random-generator 2023-06-19 17:17:24 +02:00
b2b4256972 Startup message api 2023-06-19 16:46:12 +02:00
d2f46e4637 #21 Deserialization of LunaSea Object 2023-06-19 11:27:07 +02:00
303fc293ba Fixed Bug on AddTask where no new UpdateLibraryTask would be added 2023-06-15 22:32:55 +02:00
36c145da26 Gotify change to normal priority 2023-06-15 21:24:01 +02:00
c822c74f42 website fix taskSelectOutput overflow issue 2023-06-15 21:16:56 +02:00
dda4054d34 API: Fix nullable bug on Getchapters 2023-06-15 21:15:44 +02:00
5b2546fdbc removed unnecessary log 2023-06-15 19:07:25 +02:00
c11e3993ea Added successmessage to NotificationManager 2023-06-15 19:06:53 +02:00
02a382a99a Website: Added connector NotificationManager LunaSea
Added Update Method for TrangaSettings for LunaSea
#21
2023-06-15 18:57:50 +02:00
c6c8f5cdf6 TrangaSettings nullable library and notificationManagers will initialize a new Hashset 2023-06-15 18:50:50 +02:00
84842aed3c Added connector NotificationManager LunaSea 2023-06-15 18:50:19 +02:00
d9ced11cd1 Website: Added gotify config 2023-06-15 18:38:47 +02:00
25c90782dc Moved UpdateSettings to TrangaSettings
Added NotificaitonManager
Added Gotify
Added Notification on MonitorTask download new chapters
2023-06-15 18:25:32 +02:00
e789c429cd TaskManager when deleting task also remove from parent. 2023-06-15 18:24:19 +02:00
93de471836 Added TrangaTask.RemoveChildTask 2023-06-15 18:22:59 +02:00
8b58e7dd13 Website: On Download Chapters only show chapters that have not yet been downloaded
API: Added new variables to /Publications/GetChapters: onlyNew and onlyExisting. API will return only new, only existing or all chapters depending on variables.
#19
2023-06-15 17:14:20 +02:00
b571bfa43d Moved GetNewChaptersList to taskManager and added GetExistingChaptersList 2023-06-15 17:07:32 +02:00
088d1c4647 Derived Constructor 2023-06-15 17:06:41 +02:00
f280c01802 Browser Version for both windows and linux 2023-06-15 16:30:07 +02:00
1be10b310d Fix Regex Bug on downlaod volumes 2023-06-11 19:17:03 +02:00
a0469f3145 Cancel DownloadChapter-Task on removal 2023-06-11 19:16:05 +02:00
fcd81f03b3 resolves #17 no cover image 2023-06-11 19:05:08 +02:00
76604d84d8 Better way of handling progress, and childProgress. 2023-06-11 18:24:26 +02:00
af822febbe fixed nullable warning 2023-06-11 18:01:04 +02:00
8e207c3119 Better way of handling progress, and childProgress. 2023-06-11 17:27:33 +02:00
b6f8c8aab5 TaskType check 2023-06-11 17:05:24 +02:00
36f7cbd3e9 Better way of handling progress, and childProgress.
More reliable taskFinishTime
2023-06-11 17:04:33 +02:00
3b2643d949 Website show remaining time instead of percentage 2023-06-11 16:38:12 +02:00
9fd8bf1741 website uses taskId 2023-06-10 16:00:41 +02:00
d5c9c5ba96 Redid progress calcuation on DownloadNewChaptersTask and DownloadChapterTask 2023-06-10 16:00:16 +02:00
c8e27921ab Added taskId to trangaTask and parentTaskId to DownloadChapterTask as unique identifier to attach ChildTasks to ParentTask on deserialization. 2023-06-10 15:59:42 +02:00
6eaba07801 Changed progress type from float to double 2023-06-10 15:58:11 +02:00
41929e0c72 DownloadChapterTask sets execution of parentTask 2023-06-10 15:04:37 +02:00
4fcaca1a6e Multiple authors resolves #7 2023-06-10 14:45:04 +02:00
0e3c7f32d7 Added CancellationToken to TrangaTask #14 2023-06-10 14:34:30 +02:00
1c94625840 Added CancellationToken to TrangaTask #14 2023-06-10 14:27:09 +02:00
32f89f9dce Multiple authors resolves #7 2023-06-10 14:05:23 +02:00
234735a562 Order of tasks closes #15
Also API /Queue/Get orders in order of nextExecution
2023-06-10 00:45:55 +02:00
8b916eb854 invalid Ids 2023-06-10 00:23:23 +02:00
29e1790c93 website tasks-width now max 95vw 2023-06-10 00:10:16 +02:00
ac4c799a74 Better indication if tasks have started. 2023-06-10 00:07:41 +02:00
7c62883c37 invalid id 2023-06-10 00:02:51 +02:00
02018253bf wrong nesting ... 2023-06-10 00:01:38 +02:00
2aec884009 Moved update interval for task-progress to own interval, progress gets continually updated. 2023-06-09 23:58:04 +02:00
b3321ff030 unnecessary log 2023-06-09 23:48:39 +02:00
16c1094875 Replaced Task-Progress-Tracking Window with more fancy one 2023-06-09 23:46:10 +02:00
5763d50409 #14 temporary workaround for disposing tasks 2023-06-09 23:45:53 +02:00
ad43297358 API: Updated /Tasks/GetProgress to return progress of specific task (by sortNumber) 2023-06-09 23:43:57 +02:00
b17800e0ef Decrement progress of parenttask when childtask fails 2023-06-09 23:43:19 +02:00
89c80d2997 Fixed bug where tasks would instantly failed when launched #14 2023-06-09 23:42:54 +02:00
6485b8744f API: Updated /Tasks/GetProgress to return progress of specific task (by sortNumber) 2023-06-09 23:42:18 +02:00
a3a96b6b55 Added DecrementProgress function to TrangaTask 2023-06-09 23:38:28 +02:00
5bce3c6fdd Website: Monitor task creation styling 2023-06-09 22:15:29 +02:00
5fa0c98d05 Documentation how to create tasks #11 2023-06-09 11:26:51 +02:00
b166013770 resolves #13 Website: Clear previous results 2023-06-09 11:12:43 +02:00
02fe849046 Better downloadChapter selection 2023-06-09 11:06:18 +02:00
d42393c83a Website + API ability to download specific volumes 2023-06-08 19:53:05 +02:00
c685bd622f Website:
New task-Creation dialog
Redesigned Settings dialog
2023-06-08 19:25:28 +02:00
dc83cc2194 Fixed Range on CLI downloadchaptertask creation 2023-06-08 19:25:03 +02:00
7784f2024e API changes:
/Tranga/GetAvailableControllers => /Controllers/Get
/Tranga/GetKnownPublications =>/Publications/GetKnown
/Tranga/GetPublicationsFromConnector => /Publications/GetFromConnector
/Tasks/GetTaskTypes => /Tasks/GetTypes
/Tasks/GetTaskProgress => /Tasks/GetProgress
/Tasks/Create is now split in 3:
    /Tasks/CreateMonitorTask
    /Tasks/CreateUpdateLibraryTask
    /Tasks/CreateDownloadChaptersTask
2023-06-08 19:24:46 +02:00
4895079887 Remove DownloadChapterTask from _runningDownloadChapterTasks after completion 2023-06-07 15:01:24 +02:00
ab1ddc6dc8 Less cluttered log 2023-06-07 00:31:27 +02:00
87eade10cf #40 task timeout criteria 2023-06-07 00:27:53 +02:00
1f3ac41b30 removed unnecessary cast 2023-06-07 00:24:58 +02:00
6a304bb330 #40 task timeout 2023-06-07 00:24:27 +02:00
b0642d1251 removed unnecessary check 2023-06-06 22:11:57 +02:00
63b5139e93 Split error message for better logging 2023-06-06 22:11:38 +02:00
e938784388 Created own base image for tranga-api (to stop apt always updating) 2023-06-06 22:11:26 +02:00
c436389426 renamed wrong variable names publicationId -> internalId 2023-06-06 21:57:10 +02:00
5099e25f3f Fixed wrong comparison on add new task 2023-06-06 21:56:51 +02:00
38 changed files with 1845 additions and 986 deletions

View File

@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>Tranga_API</RootNamespace> <Nullable>enable</Nullable>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup> </PropertyGroup>
@ -15,14 +15,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Logging\Logging.csproj" />
<ProjectReference Include="..\Tranga\Tranga.csproj" /> <ProjectReference Include="..\Tranga\Tranga.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.5" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.6" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>
</Project> </Project>

13
API/Dockerfile Normal file
View File

@ -0,0 +1,13 @@
# syntax=docker/dockerfile:1
FROM mcr.microsoft.com/dotnet/sdk:7.0 as build-env
WORKDIR /src
COPY . /src/
RUN dotnet restore API/API.csproj
RUN dotnet publish -c Release -o /publish
FROM glax/tranga-base:dev as runtime
WORKDIR /publish
COPY --from=build-env /publish .
EXPOSE 6531
ENTRYPOINT ["dotnet", "/publish/API.dll"]

46
API/Program.cs Normal file
View File

@ -0,0 +1,46 @@
using System.Runtime.InteropServices;
using Logging;
using Tranga;
namespace API;
public class Program
{
public static void Main(string[] args)
{
string applicationFolderPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Tranga-API");
string downloadFolderPath = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(applicationFolderPath, "Manga");
string logsFolderPath = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/var/logs/Tranga" : Path.Join(applicationFolderPath, "logs");
string logFilePath = Path.Join(logsFolderPath, $"log-{DateTime.Now:dd-M-yyyy-HH-mm-ss}.txt");
string settingsFilePath = Path.Join(applicationFolderPath, "settings.json");
Directory.CreateDirectory(logsFolderPath);
Logger logger = new(new[] { Logger.LoggerType.FileLogger, Logger.LoggerType.ConsoleLogger }, Console.Out, Console.Out.Encoding, logFilePath);
logger.WriteLine("Tranga", "Loading settings.");
TrangaSettings settings;
if (File.Exists(settingsFilePath))
settings = TrangaSettings.LoadSettings(settingsFilePath, logger);
else
settings = new TrangaSettings(downloadFolderPath, applicationFolderPath, new HashSet<LibraryManager>(), new HashSet<NotificationManager>());
Directory.CreateDirectory(settings.workingDirectory);
Directory.CreateDirectory(settings.downloadLocation);
Directory.CreateDirectory(settings.coverImageCache);
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 server = new (6531, taskManager, logger);
foreach(NotificationManager nm in taskManager.settings.notificationManagers)
nm.SendNotification("Tranga-API", "Started Tranga-API");
}
}

361
API/RequestHandler.cs Normal file
View File

@ -0,0 +1,361 @@
using System.Net;
using System.Text.RegularExpressions;
using Tranga;
using Tranga.TrangaTasks;
namespace 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?" }),
new(HttpMethod.Post, "/Tasks/CreateUpdateLibraryTask", new[] { "reoccurrenceTime" }),
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-z]+=[A-z]+)+(&[A-z]+=[A-z]+)*");
if (!queryRex.IsMatch(query))
return ret;
query = query.Substring(1);
foreach(string kvpair in query.Split('&'))
ret.Add(kvpair.Split('=')[0], kvpair.Split('=')[1]);
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);
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 is null)
return;
_taskManager.AddTask(new MonitorPublicationTask(connectorName1, (Publication)publication1, TimeSpan.Parse(reoccurrenceTime1), language1 ?? "en"));
break;
case "/Tasks/CreateUpdateLibraryTask":
variables.TryGetValue("reoccurrenceTime", out string? reoccurrenceTime2);
if (reoccurrenceTime2 is null)
return;
_taskManager.AddTask(new UpdateLibrariesTask(TimeSpan.Parse(reoccurrenceTime2)));
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.SearchChapters((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, _parent.logger, downloadLocation);
if (komgaUrl is not null && komgaAuth is not null && komgaUrl.Length > 5 && komgaAuth.Length > 0)
_taskManager.settings.UpdateSettings(TrangaSettings.UpdateField.Komga, _parent.logger, 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, _parent.logger, kavitaUrl, kavitaUsername,
kavitaPassword);
if (gotifyUrl is not null && gotifyAppToken is not null && gotifyUrl.Length > 5 && gotifyAppToken.Length > 0)
_taskManager.settings.UpdateSettings(TrangaSettings.UpdateField.Gotify, _parent.logger, gotifyUrl, gotifyAppToken);
if(lunaseaWebhook is not null && lunaseaWebhook.Length > 5)
_taskManager.settings.UpdateSettings(TrangaSettings.UpdateField.LunaSea, _parent.logger, 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 _taskManager.GetPublicationsFromConnector(connector1, 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 _taskManager.GetNewChaptersList(connector2, (Publication)publication, language??"en").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("chapterSortNumber", out string? chapterSortNumber);
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 && chapterSortNumber is not null)
{
task = _taskManager.GetTasksMatching(pTask, connectorName: connectorName4, internalId: publicationId,
chapterSortNumber: chapterSortNumber).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;
}
}
}

72
API/Server.cs Normal file
View File

@ -0,0 +1,72 @@
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using Logging;
using Newtonsoft.Json;
using Tranga;
namespace API;
public class Server
{
private readonly HttpListener _listener = new ();
private readonly RequestHandler _requestHandler;
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._listener.Prefixes.Add($"http://*:{port}/");
this._requestHandler = new RequestHandler(taskManager, this);
Listen();
}
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)
{
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;
}
_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";
response.OutputStream.Write(content is not null
? Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content))
: Array.Empty<byte>());
response.OutputStream.Close();
}
}

View File

@ -6,9 +6,9 @@ COPY . /src/
RUN dotnet restore Tranga-API/Tranga-API.csproj RUN dotnet restore Tranga-API/Tranga-API.csproj
RUN dotnet publish -c Release -o /publish RUN dotnet publish -c Release -o /publish
FROM mcr.microsoft.com/dotnet/aspnet:7.0 as runtime #FROM mcr.microsoft.com/dotnet/aspnet:7.0 as runtime
FROM glax/tranga-base:latest as runtime
WORKDIR /publish WORKDIR /publish
COPY --from=build-env /publish . COPY --from=build-env /publish .
EXPOSE 80 EXPOSE 80
RUN apt-get update && apt-get install -y libx11-6 libx11-xcb1 libatk1.0-0 libgtk-3-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2 libxshmfence1 libnss3
ENTRYPOINT ["dotnet", "/publish/Tranga-API.dll"] ENTRYPOINT ["dotnet", "/publish/Tranga-API.dll"]

8
Dockerfile-base Normal file
View File

@ -0,0 +1,8 @@
# syntax=docker/dockerfile:1
#FROM mcr.microsoft.com/dotnet/aspnet:7.0 as runtime
FROM mcr.microsoft.com/dotnet/runtime:7.0 as runtime
WORKDIR /publish
RUN apt-get update
RUN apt-get install -y libx11-6 libx11-xcb1 libatk1.0-0 libgtk-3-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2 libxshmfence1 libnss3
RUN apt-get autopurge -y
RUN apt-get autoclean -y

View File

@ -14,7 +14,8 @@ public class MemoryLogger : LoggerBase
protected override void Write(LogMessage value) protected override void Write(LogMessage value)
{ {
_logMessages.Add(value.logTime, value); while(!_logMessages.TryAdd(value.logTime, value))
Thread.Sleep(10);
} }
public string[] GetLogMessage() public string[] GetLogMessage()

View File

@ -36,6 +36,7 @@
<li> <li>
<a href="#getting-started">Getting Started</a> <a href="#getting-started">Getting Started</a>
<ul> <ul>
<li><a href="#prerequisites">Usage</a></li>
<li><a href="#prerequisites">Prerequisites</a></li> <li><a href="#prerequisites">Prerequisites</a></li>
</ul> </ul>
</li> </li>
@ -109,6 +110,28 @@ Download [docker-compose.yaml](https://git.bernloehr.eu/glax/Tranga/src/branch/m
Wherever you are mounting `/usr/share/Tranga-API` you also need to mount that same path + `/imageCache` in the webserver container. Wherever you are mounting `/usr/share/Tranga-API` you also need to mount that same path + `/imageCache` in the webserver container.
### Usage
There is two ways to download Mangas:
- Downloading everything and monitor for new Chapters
- Selecting specific Volumes/Chapters
On the website you add new tasks, by selecting the blue '+' field. Next select the connector/site you want to use, and enter a search term.
After pressing 'Search', the results will be presented below - this might, depending on the result-size, take a while.
Next select the publication and a new popup will open with two options:
- "Monitor" - Download all chapters and monitor for new ones
- "Download Chapter" - Download specific chapters only
When selecting `Monitor` you will be presented with a new window and the selection of the interval you want to check for new chapters.
When selecting `Download Chapter` a list will open with all available chapters from which you can then select a range.
The syntax for selecting chapters is as follows:
- To download a single Chapter enter either the index number (the number at the very start of the line) or its absolute number like so: `c(h)(apter)[number]`, spaces are allowed.
- To download a range of chapters enter either a range of index numbers (`3-6`) or chapters (`ch 12-23`).
- For volumes the syntax is as follows: `v(ol)[number](-[number])`, again spaces allowed.
Examples: `2-12`, `c1`, `ch 2`, `chapter 3`, `v 2`, `vol3-4`, `v2c4` (note: you can only specify a single chapter with this syntax).
### Prerequisites ### Prerequisites
[.NET-Core 7.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/7.0) [.NET-Core 7.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/7.0)

View File

@ -1,185 +0,0 @@
using System.Runtime.InteropServices;
using Logging;
using Tranga;
string applicationFolderPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Tranga-API");
string downloadFolderPath = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(applicationFolderPath, "Manga");
string logsFolderPath = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/var/logs/Tranga" : Path.Join(applicationFolderPath, "logs");
string logFilePath = Path.Join(logsFolderPath, $"log-{DateTime.Now:dd-M-yyyy-HH-mm-ss}.txt");
string settingsFilePath = Path.Join(applicationFolderPath, "settings.json");
Directory.CreateDirectory(logsFolderPath);
Logger logger = new(new[] { Logger.LoggerType.FileLogger, Logger.LoggerType.ConsoleLogger }, Console.Out, Console.Out.Encoding, logFilePath);
logger.WriteLine("Tranga", "Loading settings.");
TrangaSettings settings;
if (File.Exists(settingsFilePath))
settings = TrangaSettings.LoadSettings(settingsFilePath, logger);
else
settings = new TrangaSettings(downloadFolderPath, applicationFolderPath, new HashSet<LibraryManager>());
Directory.CreateDirectory(settings.workingDirectory);
Directory.CreateDirectory(settings.downloadLocation);
Directory.CreateDirectory(settings.coverImageCache);
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);
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddControllers().AddNewtonsoftJson();
string corsHeader = "Tranga";
builder.Services.AddCors(options =>
{
options.AddPolicy(name: corsHeader,
policy =>
{
policy.AllowAnyOrigin();
policy.WithMethods("GET", "POST", "DELETE");
});
});
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.UseCors(corsHeader);
app.MapGet("/Tranga/GetAvailableControllers", () => taskManager.GetAvailableConnectors().Keys.ToArray());
app.MapGet("/Tranga/GetKnownPublications", () => taskManager.GetAllPublications());
app.MapGet("/Tranga/GetPublicationsFromConnector", (string connectorName, string title) =>
{
Connector? connector = taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName).Value;
if (connector is null)
return Array.Empty<Publication>();
if(title.Length < 4)
return Array.Empty<Publication>();
return taskManager.GetPublicationsFromConnector(connector, title);
});
app.MapGet("/Tasks/GetTaskTypes", () => Enum.GetNames(typeof(TrangaTask.Task)));
app.MapPost("/Tasks/Create", (string taskType, string? connectorName, string? publicationId, string reoccurrenceTime, string? language) =>
{
TrangaTask.Task task = Enum.Parse<TrangaTask.Task>(taskType);
taskManager.AddTask(task, connectorName, publicationId, TimeSpan.Parse(reoccurrenceTime), language??"");
});
app.MapDelete("/Tasks/Delete", (string taskType, string? connectorName, string? publicationId) =>
{
TrangaTask.Task task = Enum.Parse<TrangaTask.Task>(taskType);
taskManager.DeleteTask(task, connectorName, publicationId);
});
app.MapGet("/Tasks/Get", (string taskType, string? connectorName, string? searchString) =>
{
try
{
TrangaTask.Task task = Enum.Parse<TrangaTask.Task>(taskType);
return taskManager.GetTasksMatching(task, connectorName:connectorName, searchString:searchString);
}
catch (ArgumentException)
{
return Array.Empty<TrangaTask>();
}
});
app.MapGet("/Tasks/GetTaskProgress", (string taskType, string? connectorName, string? publicationId) =>
{
try
{
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType);
TrangaTask? task = taskManager
.GetTasksMatching(pTask, connectorName: connectorName, internalId: publicationId)?.First();
if (task is null)
return -1f;
return task.progress;
}
catch (ArgumentException)
{
return -1f;
}
});
app.MapPost("/Tasks/Start", (string taskType, string? connectorName, string? internalId) =>
{
try
{
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType);
TrangaTask? task = taskManager
.GetTasksMatching(pTask, connectorName: connectorName, internalId: internalId)?.FirstOrDefault();
if (task is null)
return;
taskManager.ExecuteTaskNow(task);
}
catch (ArgumentException)
{
return;
}
});
app.MapGet("/Tasks/GetRunningTasks",
() => taskManager.GetAllTasks().Where(task => task.state is TrangaTask.ExecutionState.Running));
app.MapGet("/Queue/GetList",
() => taskManager.GetAllTasks().Where(task => task.state is TrangaTask.ExecutionState.Enqueued));
app.MapPost("/Queue/Enqueue", (string taskType, string? connectorName, string? publicationId) =>
{
try
{
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType);
TrangaTask? task = taskManager
.GetTasksMatching(pTask, connectorName: connectorName, internalId: publicationId)?.First();
if (task is null)
return;
taskManager.AddTaskToQueue(task);
}
catch (ArgumentException)
{
return;
}
});
app.MapDelete("/Queue/Dequeue", (string taskType, string? connectorName, string? publicationId) =>
{
try
{
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType);
TrangaTask? task = taskManager
.GetTasksMatching(pTask, connectorName: connectorName, internalId: publicationId)?.First();
if (task is null)
return;
taskManager.RemoveTaskFromQueue(task);
}
catch (ArgumentException)
{
return;
}
});
app.MapGet("/Settings/Get", () => taskManager.settings);
app.MapPost("/Settings/Update",
(string? downloadLocation, string? komgaUrl, string? komgaAuth, string? kavitaUrl, string? kavitaUsername, string? kavitaPassword) =>
taskManager.UpdateSettings(downloadLocation, komgaUrl, komgaAuth, kavitaUrl, kavitaUsername, kavitaPassword));
app.Run();

View File

@ -1,28 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:1716",
"sslPort": 44391
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5177"
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7036;http://localhost:5177"
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true
}
}
}

View File

@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -1,9 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@ -9,12 +9,6 @@
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Tranga\Tranga.csproj" /> <ProjectReference Include="..\Tranga\Tranga.csproj" />
</ItemGroup> </ItemGroup>

View File

@ -2,6 +2,7 @@
using Logging; using Logging;
using Tranga; using Tranga;
using Tranga.LibraryManagers; using Tranga.LibraryManagers;
using Tranga.NotificationManagers;
using Tranga.TrangaTasks; using Tranga.TrangaTasks;
namespace Tranga_CLI; namespace Tranga_CLI;
@ -30,7 +31,7 @@ public static class Tranga_Cli
Logger logger = new(new[] { Logger.LoggerType.FileLogger }, null, Console.Out.Encoding, logFilePath); Logger logger = new(new[] { Logger.LoggerType.FileLogger }, null, Console.Out.Encoding, logFilePath);
logger.WriteLine("Tranga_CLI", "Loading Taskmanager."); logger.WriteLine("Tranga_CLI", "Loading Taskmanager.");
TrangaSettings settings = File.Exists(settingsFilePath) ? TrangaSettings.LoadSettings(settingsFilePath, logger) : new TrangaSettings(Directory.GetCurrentDirectory(), applicationFolderPath, new HashSet<LibraryManager>()); TrangaSettings settings = File.Exists(settingsFilePath) ? TrangaSettings.LoadSettings(settingsFilePath, logger) : new TrangaSettings(Directory.GetCurrentDirectory(), applicationFolderPath, new HashSet<LibraryManager>(), new HashSet<NotificationManager>());
logger.WriteLine("Tranga_CLI", "User Input"); logger.WriteLine("Tranga_CLI", "User Input");
@ -39,7 +40,7 @@ public static class Tranga_Cli
while(tmpPath is null) while(tmpPath is null)
tmpPath = Console.ReadLine(); tmpPath = Console.ReadLine();
if (tmpPath.Length > 0) if (tmpPath.Length > 0)
settings.downloadLocation = tmpPath; settings.UpdateSettings(TrangaSettings.UpdateField.DownloadLocation, logger, tmpPath);
Console.WriteLine($"Komga BaseURL [{settings.libraryManagers.FirstOrDefault(lm => lm.GetType() == typeof(Komga))?.baseUrl}]:"); Console.WriteLine($"Komga BaseURL [{settings.libraryManagers.FirstOrDefault(lm => lm.GetType() == typeof(Komga))?.baseUrl}]:");
string? tmpUrlKomga = Console.ReadLine(); string? tmpUrlKomga = Console.ReadLine();
@ -72,8 +73,7 @@ public static class Tranga_Cli
} }
} while (key != ConsoleKey.Enter); } while (key != ConsoleKey.Enter);
settings.libraryManagers.RemoveWhere(lm => lm.GetType() == typeof(Komga)); settings.UpdateSettings(TrangaSettings.UpdateField.Komga, logger, tmpUrlKomga, tmpKomgaUser, tmpKomgaPass);
settings.libraryManagers.Add(new Komga(tmpUrlKomga, tmpKomgaUser, tmpKomgaPass, logger));
} }
Console.WriteLine($"Kavita BaseURL [{settings.libraryManagers.FirstOrDefault(lm => lm.GetType() == typeof(Kavita))?.baseUrl}]:"); Console.WriteLine($"Kavita BaseURL [{settings.libraryManagers.FirstOrDefault(lm => lm.GetType() == typeof(Kavita))?.baseUrl}]:");
@ -107,11 +107,26 @@ public static class Tranga_Cli
} }
} while (key != ConsoleKey.Enter); } while (key != ConsoleKey.Enter);
settings.libraryManagers.RemoveWhere(lm => lm.GetType() == typeof(Kavita)); settings.UpdateSettings(TrangaSettings.UpdateField.Kavita, logger, tmpUrlKavita, tmpKavitaUser, tmpKavitaPass);
settings.libraryManagers.Add(new Kavita(tmpUrlKavita, tmpKavitaUser, tmpKavitaPass, logger)); }
Console.WriteLine($"Gotify BaseURL [{((Gotify?)settings.notificationManagers.FirstOrDefault(lm => lm.GetType() == typeof(Gotify)))?.endpoint}]:");
string? tmpGotifyUrl = Console.ReadLine();
while (tmpGotifyUrl is null)
tmpGotifyUrl = Console.ReadLine();
if (tmpGotifyUrl.Length > 0)
{
Console.WriteLine("AppToken:");
string? tmpGotifyAppToken = Console.ReadLine();
while (tmpGotifyAppToken is null || tmpGotifyAppToken.Length < 1)
tmpGotifyAppToken = Console.ReadLine();
settings.UpdateSettings(TrangaSettings.UpdateField.Gotify, logger, tmpGotifyUrl, tmpGotifyAppToken);
} }
logger.WriteLine("Tranga_CLI", "Loaded."); logger.WriteLine("Tranga_CLI", "Loaded.");
foreach(NotificationManager nm in settings.notificationManagers)
nm.SendNotification("Tranga", "Loaded.");
TaskMode(settings, logger); TaskMode(settings, logger);
} }
@ -320,8 +335,9 @@ public static class Tranga_Cli
TimeSpan reoccurrence = SelectReoccurrence(logger); TimeSpan reoccurrence = SelectReoccurrence(logger);
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager"); logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
TrangaTask? newTask = taskManager.AddTask(TrangaTask.Task.DownloadNewChapters, connector.name, publication.Value.publicationId, reoccurrence, "en"); TrangaTask nTask = new MonitorPublicationTask(connector.name, (Publication)publication, reoccurrence, "en");
Console.WriteLine(newTask); taskManager.AddTask(nTask);
Console.WriteLine(nTask);
} }
private static void AddTaskToQueue(TaskManager taskManager, Logger logger) private static void AddTaskToQueue(TaskManager taskManager, Logger logger)
@ -393,20 +409,19 @@ public static class Tranga_Cli
return; return;
} }
if (task is TrangaTask.Task.DownloadNewChapters) if (task is TrangaTask.Task.MonitorPublication)
{ {
TimeSpan reoccurrence = SelectReoccurrence(logger); TimeSpan reoccurrence = SelectReoccurrence(logger);
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager"); logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
TrangaTask newTask = new DownloadNewChaptersTask(TrangaTask.Task.DownloadNewChapters, connector!.name, (Publication)publication!, reoccurrence, "en"); TrangaTask newTask = new MonitorPublicationTask(connector!.name, (Publication)publication!, reoccurrence, "en");
taskManager.AddTask(newTask); taskManager.AddTask(newTask);
Console.WriteLine(newTask); Console.WriteLine(newTask);
}else if (task is TrangaTask.Task.DownloadChapter) }else if (task is TrangaTask.Task.DownloadChapter)
{ {
foreach (Chapter chapter in SelectChapters(connector!, (Publication)publication!, logger)) foreach (Chapter chapter in SelectChapters(connector!, (Publication)publication!, logger))
{ {
TrangaTask newTask = new DownloadChapterTask(TrangaTask.Task.DownloadChapter, connector!.name, TrangaTask newTask = new DownloadChapterTask(connector!.name, (Publication)publication, chapter, "en");
(Publication)publication!, chapter, "en");
taskManager.AddTask(newTask); taskManager.AddTask(newTask);
Console.WriteLine(newTask); Console.WriteLine(newTask);
} }
@ -499,22 +514,7 @@ public static class Tranga_Cli
while(selectedChapters is null || selectedChapters.Length < 1) while(selectedChapters is null || selectedChapters.Length < 1)
selectedChapters = Console.ReadLine(); selectedChapters = Console.ReadLine();
if (selectedChapters.Length == 1 && selectedChapters.ToLower() == "q") return connector.SearchChapters(publication, selectedChapters);
{
Console.Clear();
Console.WriteLine("aborted.");
logger.WriteLine("Tranga_CLI", "aborted.");
return Array.Empty<Chapter>();
}
if (selectedChapters.Contains('-'))
{
int start = Convert.ToInt32(selectedChapters.Split('-')[0]);
int end = Convert.ToInt32(selectedChapters.Split('-')[1]);
return availableChapters[start..end];
}
else
return new Chapter[] { availableChapters[Convert.ToInt32(selectedChapters)] };
} }
private static Connector? SelectConnector(Connector[] connectors, Logger logger) private static Connector? SelectConnector(Connector[] connectors, Logger logger)

View File

@ -6,7 +6,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tranga-CLI", "Tranga-CLI\Tr
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logging", "Logging\Logging.csproj", "{415BE889-BB7D-426F-976F-8D977876A462}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logging", "Logging\Logging.csproj", "{415BE889-BB7D-426F-976F-8D977876A462}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tranga-API", "Tranga-API\Tranga-API.csproj", "{48F4E495-75BC-4402-8E03-DEC5B79D7E83}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API", "API\API.csproj", "{A8AB1F5F-D174-49DC-AED2-0909B93BA7B6}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -26,9 +26,9 @@ Global
{415BE889-BB7D-426F-976F-8D977876A462}.Debug|Any CPU.Build.0 = Debug|Any CPU {415BE889-BB7D-426F-976F-8D977876A462}.Debug|Any CPU.Build.0 = Debug|Any CPU
{415BE889-BB7D-426F-976F-8D977876A462}.Release|Any CPU.ActiveCfg = Release|Any CPU {415BE889-BB7D-426F-976F-8D977876A462}.Release|Any CPU.ActiveCfg = Release|Any CPU
{415BE889-BB7D-426F-976F-8D977876A462}.Release|Any CPU.Build.0 = Release|Any CPU {415BE889-BB7D-426F-976F-8D977876A462}.Release|Any CPU.Build.0 = Release|Any CPU
{48F4E495-75BC-4402-8E03-DEC5B79D7E83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A8AB1F5F-D174-49DC-AED2-0909B93BA7B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{48F4E495-75BC-4402-8E03-DEC5B79D7E83}.Debug|Any CPU.Build.0 = Debug|Any CPU {A8AB1F5F-D174-49DC-AED2-0909B93BA7B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{48F4E495-75BC-4402-8E03-DEC5B79D7E83}.Release|Any CPU.ActiveCfg = Release|Any CPU {A8AB1F5F-D174-49DC-AED2-0909B93BA7B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{48F4E495-75BC-4402-8E03-DEC5B79D7E83}.Release|Any CPU.Build.0 = Release|Any CPU {A8AB1F5F-D174-49DC-AED2-0909B93BA7B6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Gotify/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Komga/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Komga/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Manganato/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Manganato/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mangasee/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Mangasee/@EntryIndexedValue">True</s:Boolean>

View File

@ -20,7 +20,7 @@ public abstract class Connector
protected readonly Logger? logger; protected readonly Logger? logger;
protected readonly string imageCachePath; private readonly string _imageCachePath;
protected Connector(string downloadLocation, string imageCachePath, Logger? logger) protected Connector(string downloadLocation, string imageCachePath, Logger? logger)
{ {
@ -30,9 +30,9 @@ public abstract class Connector
{ {
//RequestTypes for RateLimits //RequestTypes for RateLimits
}, logger); }, logger);
this.imageCachePath = imageCachePath; this._imageCachePath = imageCachePath;
if (!Directory.Exists(imageCachePath)) if (!Directory.Exists(imageCachePath))
Directory.CreateDirectory(this.imageCachePath); Directory.CreateDirectory(this._imageCachePath);
} }
public abstract string name { get; } //Name of the Connector (e.g. Website) public abstract string name { get; } //Name of the Connector (e.g. Website)
@ -53,6 +53,78 @@ public abstract class Connector
/// <param name="language">Language of the Chapters</param> /// <param name="language">Language of the Chapters</param>
/// <returns>Array of Chapters matching Publication and Language</returns> /// <returns>Array of Chapters matching Publication and Language</returns>
public abstract Chapter[] GetChapters(Publication publication, string language = ""); public abstract Chapter[] GetChapters(Publication publication, string language = "");
public Chapter[] SearchChapters(Publication publication, string searchTerm, string? language = null)
{
Chapter[] availableChapters = this.GetChapters(publication, language??"en");
Regex volumeRegex = new ("((v(ol)*(olume)*)+ *([0-9]+(-[0-9]+)?){1})", RegexOptions.IgnoreCase);
Regex chapterRegex = new ("((c(h)*(hapter)*)+ *([0-9]+(-[0-9]+)?){1})", RegexOptions.IgnoreCase);
Regex singleResultRegex = new("([0-9]+)", RegexOptions.IgnoreCase);
Regex rangeResultRegex = new("([0-9]+(-[0-9]+))", RegexOptions.IgnoreCase);
if (volumeRegex.IsMatch(searchTerm) && chapterRegex.IsMatch(searchTerm))
{
string volume = singleResultRegex.Match(volumeRegex.Match(searchTerm).Value).Value;
string chapter = singleResultRegex.Match(chapterRegex.Match(searchTerm).Value).Value;
return availableChapters.Where(aCh => aCh.volumeNumber is not null && aCh.chapterNumber is not null &&
aCh.volumeNumber.Equals(volume, StringComparison.InvariantCultureIgnoreCase) &&
aCh.chapterNumber.Equals(chapter, StringComparison.InvariantCultureIgnoreCase))
.ToArray();
}
else if (volumeRegex.IsMatch(searchTerm))
{
string volume = volumeRegex.Match(searchTerm).Value;
if (rangeResultRegex.IsMatch(volume))
{
string range = rangeResultRegex.Match(volume).Value;
int start = Convert.ToInt32(range.Split('-')[0]);
int end = Convert.ToInt32(range.Split('-')[1]);
return availableChapters.Where(aCh => aCh.volumeNumber is not null &&
Convert.ToInt32(aCh.volumeNumber) >= start &&
Convert.ToInt32(aCh.volumeNumber) <= end).ToArray();
}
else if (singleResultRegex.IsMatch(volume))
{
string volumeNumber = singleResultRegex.Match(volume).Value;
return availableChapters.Where(aCh =>
aCh.volumeNumber is not null &&
aCh.volumeNumber.Equals(volumeNumber, StringComparison.InvariantCultureIgnoreCase)).ToArray();
}
}
else if (chapterRegex.IsMatch(searchTerm))
{
string chapter = volumeRegex.Match(searchTerm).Value;
if (rangeResultRegex.IsMatch(chapter))
{
string range = rangeResultRegex.Match(chapter).Value;
int start = Convert.ToInt32(range.Split('-')[0]);
int end = Convert.ToInt32(range.Split('-')[1]);
return availableChapters.Where(aCh => aCh.chapterNumber is not null &&
Convert.ToInt32(aCh.chapterNumber) >= start &&
Convert.ToInt32(aCh.chapterNumber) <= end).ToArray();
}
else if (singleResultRegex.IsMatch(chapter))
{
string chapterNumber = singleResultRegex.Match(chapter).Value;
return availableChapters.Where(aCh =>
aCh.chapterNumber is not null &&
aCh.chapterNumber.Equals(chapterNumber, StringComparison.InvariantCultureIgnoreCase)).ToArray();
}
}
else
{
if (rangeResultRegex.IsMatch(searchTerm))
{
int start = Convert.ToInt32(searchTerm.Split('-')[0]);
int end = Convert.ToInt32(searchTerm.Split('-')[1]);
return availableChapters[start..(end + 1)];
}
else if(singleResultRegex.IsMatch(searchTerm))
return new [] { availableChapters[Convert.ToInt32(searchTerm)] };
}
return Array.Empty<Chapter>();
}
/// <summary> /// <summary>
/// Retrieves the Chapter (+Images) from the website. /// Retrieves the Chapter (+Images) from the website.
@ -61,7 +133,8 @@ public abstract class Connector
/// <param name="publication">Publication that contains Chapter</param> /// <param name="publication">Publication that contains Chapter</param>
/// <param name="chapter">Chapter with Images to retrieve</param> /// <param name="chapter">Chapter with Images to retrieve</param>
/// <param name="parentTask">Will be used for progress-tracking</param> /// <param name="parentTask">Will be used for progress-tracking</param>
public abstract void DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask); /// <param name="cancellationToken"></param>
public abstract HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null);
/// <summary> /// <summary>
/// Copies the already downloaded cover from cache to downloadLocation /// Copies the already downloaded cover from cache to downloadLocation
@ -70,11 +143,11 @@ public abstract class Connector
/// <param name="settings">TrangaSettings</param> /// <param name="settings">TrangaSettings</param>
public void CopyCoverFromCacheToDownloadLocation(Publication publication, TrangaSettings settings) public void CopyCoverFromCacheToDownloadLocation(Publication publication, TrangaSettings settings)
{ {
logger?.WriteLine(this.GetType().ToString(), $"Cloning cover {publication.sortName} {publication.internalId}"); logger?.WriteLine(this.GetType().ToString(), $"Cloning cover {publication.sortName} -> {publication.internalId}");
//Check if Publication already has a Folder and cover //Check if Publication already has a Folder and cover
string publicationFolder = publication.CreatePublicationFolder(downloadLocation); string publicationFolder = publication.CreatePublicationFolder(downloadLocation);
DirectoryInfo dirInfo = new (publicationFolder); DirectoryInfo dirInfo = new (publicationFolder);
if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover."))) if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover", StringComparison.InvariantCultureIgnoreCase)))
{ {
logger?.WriteLine(this.GetType().ToString(), $"Cover exists {publication.sortName}"); logger?.WriteLine(this.GetType().ToString(), $"Cover exists {publication.sortName}");
return; return;
@ -100,7 +173,7 @@ public abstract class Connector
new XElement("Tags", string.Join(',',publication.tags)), new XElement("Tags", string.Join(',',publication.tags)),
new XElement("LanguageISO", publication.originalLanguage), new XElement("LanguageISO", publication.originalLanguage),
new XElement("Title", chapter.name), new XElement("Title", chapter.name),
new XElement("Writer", publication.author), new XElement("Writer", string.Join(',', publication.authors)),
new XElement("Volume", chapter.volumeNumber), new XElement("Volume", chapter.volumeNumber),
new XElement("Number", chapter.chapterNumber) new XElement("Number", chapter.chapterNumber)
); );
@ -140,16 +213,15 @@ public abstract class Connector
/// <param name="fullPath"></param> /// <param name="fullPath"></param>
/// <param name="requestType">RequestType for Rate-Limit</param> /// <param name="requestType">RequestType for Rate-Limit</param>
/// <param name="referrer">referrer used in html request header</param> /// <param name="referrer">referrer used in html request header</param>
private void DownloadImage(string imageUrl, string fullPath, byte requestType, string? referrer = null) private HttpStatusCode DownloadImage(string imageUrl, string fullPath, byte requestType, string? referrer = null)
{ {
DownloadClient.RequestResult requestResult = downloadClient.MakeRequest(imageUrl, requestType, referrer); DownloadClient.RequestResult requestResult = downloadClient.MakeRequest(imageUrl, requestType, referrer);
if (requestResult.result != Stream.Null) if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.result == Stream.Null)
{ return requestResult.statusCode;
byte[] buffer = new byte[requestResult.result.Length]; byte[] buffer = new byte[requestResult.result.Length];
requestResult.result.ReadExactly(buffer, 0, buffer.Length); requestResult.result.ReadExactly(buffer, 0, buffer.Length);
File.WriteAllBytes(fullPath, buffer); File.WriteAllBytes(fullPath, buffer);
}else return requestResult.statusCode;
logger?.WriteLine(this.GetType().ToString(), "No Stream-Content in result.");
} }
/// <summary> /// <summary>
@ -161,8 +233,11 @@ public abstract class Connector
/// <param name="comicInfoPath">Path of the generate Chapter ComicInfo.xml, if it was generated</param> /// <param name="comicInfoPath">Path of the generate Chapter ComicInfo.xml, if it was generated</param>
/// <param name="requestType">RequestType for RateLimits</param> /// <param name="requestType">RequestType for RateLimits</param>
/// <param name="referrer">Used in http request header</param> /// <param name="referrer">Used in http request header</param>
protected void DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, byte requestType, DownloadChapterTask parentTask, string? comicInfoPath = null, string? referrer = null) /// <param name="cancellationToken"></param>
protected HttpStatusCode DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, byte requestType, DownloadChapterTask parentTask, string? comicInfoPath = null, string? referrer = null, CancellationToken? cancellationToken = null)
{ {
if (cancellationToken?.IsCancellationRequested ?? false)
return HttpStatusCode.RequestTimeout;
logger?.WriteLine("Connector", $"Downloading Images for {saveArchiveFilePath}"); logger?.WriteLine("Connector", $"Downloading Images for {saveArchiveFilePath}");
//Check if Publication Directory already exists //Check if Publication Directory already exists
string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!; string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!;
@ -170,7 +245,7 @@ public abstract class Connector
Directory.CreateDirectory(directoryPath); Directory.CreateDirectory(directoryPath);
if (File.Exists(saveArchiveFilePath)) //Don't download twice. if (File.Exists(saveArchiveFilePath)) //Don't download twice.
return; return HttpStatusCode.OK;
//Create a temporary folder to store images //Create a temporary folder to store images
string tempFolder = Directory.CreateTempSubdirectory().FullName; string tempFolder = Directory.CreateTempSubdirectory().FullName;
@ -182,8 +257,12 @@ public abstract class Connector
string[] split = imageUrl.Split('.'); string[] split = imageUrl.Split('.');
string extension = split[^1]; string extension = split[^1];
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}"); 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}");
DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), requestType, referrer); HttpStatusCode status = DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), requestType, referrer);
parentTask.IncrementProgress(1f / imageUrls.Length); if ((int)status < 200 || (int)status >= 300)
return status;
parentTask.IncrementProgress(1.0 / imageUrls.Length);
if (cancellationToken?.IsCancellationRequested ?? false)
return HttpStatusCode.RequestTimeout;
} }
if(comicInfoPath is not null) if(comicInfoPath is not null)
@ -195,13 +274,14 @@ public abstract class Connector
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
File.SetUnixFileMode(saveArchiveFilePath, GroupRead | GroupWrite | OtherRead | OtherWrite | UserRead | UserWrite); File.SetUnixFileMode(saveArchiveFilePath, GroupRead | GroupWrite | OtherRead | OtherWrite | UserRead | UserWrite);
Directory.Delete(tempFolder, true); //Cleanup Directory.Delete(tempFolder, true); //Cleanup
return HttpStatusCode.OK;
} }
protected string SaveCoverImageToCache(string url, byte requestType) protected string SaveCoverImageToCache(string url, byte requestType)
{ {
string[] split = url.Split('/'); string[] split = url.Split('/');
string filename = split[^1]; string filename = split[^1];
string saveImagePath = Path.Join(imageCachePath, filename); string saveImagePath = Path.Join(_imageCachePath, filename);
if (File.Exists(saveImagePath)) if (File.Exists(saveImagePath))
return filename; return filename;
@ -216,11 +296,15 @@ public abstract class Connector
protected class DownloadClient protected class DownloadClient
{ {
private static readonly HttpClient Client = new(); private static readonly HttpClient Client = new()
{
Timeout = TimeSpan.FromSeconds(60)
};
private readonly Dictionary<byte, DateTime> _lastExecutedRateLimit; private readonly Dictionary<byte, DateTime> _lastExecutedRateLimit;
private readonly Dictionary<byte, TimeSpan> _rateLimit; private readonly Dictionary<byte, TimeSpan> _rateLimit;
private Logger? logger; // ReSharper disable once InconsistentNaming
private readonly Logger? logger;
/// <summary> /// <summary>
/// Creates a httpClient /// Creates a httpClient
@ -277,10 +361,12 @@ public abstract class Connector
Thread.Sleep(_rateLimit[requestType] * 2); Thread.Sleep(_rateLimit[requestType] * 2);
} }
} }
Stream resultString = response.IsSuccessStatusCode ? response.Content.ReadAsStream() : Stream.Null;
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{
logger?.WriteLine(this.GetType().ToString(), $"Request-Error {response.StatusCode}: {response.ReasonPhrase}"); logger?.WriteLine(this.GetType().ToString(), $"Request-Error {response.StatusCode}: {response.ReasonPhrase}");
return new RequestResult(response.StatusCode, resultString); return new RequestResult(response.StatusCode, Stream.Null);
}
return new RequestResult(response.StatusCode, response.Content.ReadAsStream());
} }
public struct RequestResult public struct RequestResult

View File

@ -46,7 +46,7 @@ public class MangaDex : Connector
DownloadClient.RequestResult requestResult = DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest( downloadClient.MakeRequest(
$"https://api.mangadex.org/manga?limit={limit}&title={publicationTitle}&offset={offset}", (byte)RequestType.Manga); $"https://api.mangadex.org/manga?limit={limit}&title={publicationTitle}&offset={offset}", (byte)RequestType.Manga);
if (requestResult.statusCode != HttpStatusCode.OK) if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
break; break;
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result); JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
@ -93,19 +93,21 @@ public class MangaDex : Connector
} }
string? posterId = null; string? posterId = null;
string? authorId = null; HashSet<string> authorIds = new();
if (manga.ContainsKey("relationships") && manga["relationships"] is not null) if (manga.ContainsKey("relationships") && manga["relationships"] is not null)
{ {
JsonArray relationships = manga["relationships"]!.AsArray(); JsonArray relationships = manga["relationships"]!.AsArray();
posterId = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue<string>() == "cover_art")!["id"]!.GetValue<string>(); posterId = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue<string>() == "cover_art")!["id"]!.GetValue<string>();
authorId = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue<string>() == "author")!["id"]!.GetValue<string>(); foreach (JsonNode? node in relationships.Where(relationship =>
relationship!["type"]!.GetValue<string>() == "author"))
authorIds.Add(node!["id"]!.GetValue<string>());
} }
string? coverUrl = GetCoverUrl(publicationId, posterId); string? coverUrl = GetCoverUrl(publicationId, posterId);
string? coverCacheName = null; string? coverCacheName = null;
if (coverUrl is not null) if (coverUrl is not null)
coverCacheName = SaveCoverImageToCache(coverUrl, (byte)RequestType.AtHomeServer); coverCacheName = SaveCoverImageToCache(coverUrl, (byte)RequestType.AtHomeServer);
string? author = GetAuthor(authorId); List<string> authors = GetAuthors(authorIds);
Dictionary<string, string> linksDict = new(); Dictionary<string, string> linksDict = new();
if (attributes.ContainsKey("links") && attributes["links"] is not null) if (attributes.ContainsKey("links") && attributes["links"] is not null)
@ -129,7 +131,7 @@ public class MangaDex : Connector
Publication pub = new ( Publication pub = new (
title, title,
author, authors,
description, description,
altTitlesDict, altTitlesDict,
tags.ToArray(), tags.ToArray(),
@ -163,7 +165,7 @@ public class MangaDex : Connector
DownloadClient.RequestResult requestResult = DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest( downloadClient.MakeRequest(
$"https://api.mangadex.org/manga/{publication.publicationId}/feed?limit={limit}&offset={offset}&translatedLanguage%5B%5D={language}", (byte)RequestType.Feed); $"https://api.mangadex.org/manga/{publication.publicationId}/feed?limit={limit}&offset={offset}&translatedLanguage%5B%5D={language}", (byte)RequestType.Feed);
if (requestResult.statusCode != HttpStatusCode.OK) if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
break; break;
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result); JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
@ -205,17 +207,19 @@ public class MangaDex : Connector
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray(); return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
} }
public override void DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask) public override HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null)
{ {
if (cancellationToken?.IsCancellationRequested ?? false)
return HttpStatusCode.RequestTimeout;
logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}"); logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
//Request URLs for Chapter-Images //Request URLs for Chapter-Images
DownloadClient.RequestResult requestResult = DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest($"https://api.mangadex.org/at-home/server/{chapter.url}?forcePort443=false'", (byte)RequestType.AtHomeServer); downloadClient.MakeRequest($"https://api.mangadex.org/at-home/server/{chapter.url}?forcePort443=false'", (byte)RequestType.AtHomeServer);
if (requestResult.statusCode != HttpStatusCode.OK) if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return; return requestResult.statusCode;
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result); JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
if (result is null) if (result is null)
return; return HttpStatusCode.NoContent;
string baseUrl = result["baseUrl"]!.GetValue<string>(); string baseUrl = result["baseUrl"]!.GetValue<string>();
string hash = result["chapter"]!["hash"]!.GetValue<string>(); string hash = result["chapter"]!["hash"]!.GetValue<string>();
@ -229,7 +233,7 @@ public class MangaDex : Connector
File.WriteAllText(comicInfoPath, GetComicInfoXmlString(publication, chapter, logger)); File.WriteAllText(comicInfoPath, GetComicInfoXmlString(publication, chapter, logger));
//Download Chapter-Images //Download Chapter-Images
DownloadChapterImages(imageUrls.ToArray(), GetArchiveFilePath(publication, chapter), (byte)RequestType.AtHomeServer, parentTask, comicInfoPath); return DownloadChapterImages(imageUrls.ToArray(), GetArchiveFilePath(publication, chapter), (byte)RequestType.AtHomeServer, parentTask, comicInfoPath, cancellationToken:cancellationToken);
} }
private string? GetCoverUrl(string publicationId, string? posterId) private string? GetCoverUrl(string publicationId, string? posterId)
@ -244,7 +248,7 @@ public class MangaDex : Connector
//Request information where to download Cover //Request information where to download Cover
DownloadClient.RequestResult requestResult = DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest($"https://api.mangadex.org/cover/{posterId}", (byte)RequestType.CoverUrl); downloadClient.MakeRequest($"https://api.mangadex.org/cover/{posterId}", (byte)RequestType.CoverUrl);
if (requestResult.statusCode != HttpStatusCode.OK) if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return null; return null;
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result); JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
if (result is null) if (result is null)
@ -257,21 +261,23 @@ public class MangaDex : Connector
return coverUrl; return coverUrl;
} }
private string? GetAuthor(string? authorId) private List<string> GetAuthors(IEnumerable<string> authorIds)
{ {
if (authorId is null) List<string> ret = new();
return null; foreach (string authorId in authorIds)
{
DownloadClient.RequestResult requestResult = DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest($"https://api.mangadex.org/author/{authorId}", (byte)RequestType.Author); downloadClient.MakeRequest($"https://api.mangadex.org/author/{authorId}", (byte)RequestType.Author);
if (requestResult.statusCode != HttpStatusCode.OK) if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return null; return ret;
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result); JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
if (result is null) if (result is null)
return null; return ret;
string author = result["data"]!["attributes"]!["name"]!.GetValue<string>(); string authorName = result["data"]!["attributes"]!["name"]!.GetValue<string>();
logger?.WriteLine(this.GetType().ToString(), $"Got author {authorId} -> {author}"); ret.Add(authorName);
return author; logger?.WriteLine(this.GetType().ToString(), $"Got author {authorId} -> {authorName}");
}
return ret;
} }
} }

View File

@ -1,4 +1,5 @@
using System.Net; using System.Globalization;
using System.Net;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using HtmlAgilityPack; using HtmlAgilityPack;
using Logging; using Logging;
@ -26,7 +27,7 @@ public class Manganato : Connector
string requestUrl = $"https://manganato.com/search/story/{sanitizedTitle}"; string requestUrl = $"https://manganato.com/search/story/{sanitizedTitle}";
DownloadClient.RequestResult requestResult = DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, (byte)1); downloadClient.MakeRequest(requestUrl, (byte)1);
if (requestResult.statusCode != HttpStatusCode.OK) if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Publication>(); return Array.Empty<Publication>();
return ParsePublicationsFromHtml(requestResult.result); return ParsePublicationsFromHtml(requestResult.result);
@ -51,7 +52,7 @@ public class Manganato : Connector
{ {
DownloadClient.RequestResult requestResult = DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest(url, (byte)1); downloadClient.MakeRequest(url, (byte)1);
if (requestResult.statusCode != HttpStatusCode.OK) if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Publication>(); return Array.Empty<Publication>();
ret.Add(ParseSinglePublicationFromHtml(requestResult.result, url.Split('/')[^1])); ret.Add(ParseSinglePublicationFromHtml(requestResult.result, url.Split('/')[^1]));
@ -70,8 +71,8 @@ public class Manganato : Connector
Dictionary<string, string> altTitles = new(); Dictionary<string, string> altTitles = new();
Dictionary<string, string>? links = null; Dictionary<string, string>? links = null;
HashSet<string> tags = new(); HashSet<string> tags = new();
string? author = null, originalLanguage = null; string[] authors = Array.Empty<string>();
int? year = DateTime.Now.Year; string originalLanguage = "";
HtmlNode infoNode = document.DocumentNode.Descendants("div").First(d => d.HasClass("story-info-right")); HtmlNode infoNode = document.DocumentNode.Descendants("div").First(d => d.HasClass("story-info-right"));
@ -93,7 +94,7 @@ public class Manganato : Connector
altTitles.Add(i.ToString(), alts[i]); altTitles.Add(i.ToString(), alts[i]);
break; break;
case "authors": case "authors":
author = value; authors = value.Split('-');
break; break;
case "status": case "status":
status = value; status = value;
@ -118,9 +119,9 @@ public class Manganato : Connector
string yearString = document.DocumentNode.Descendants("li").Last(li => li.HasClass("a-h")).Descendants("span") string yearString = document.DocumentNode.Descendants("li").Last(li => li.HasClass("a-h")).Descendants("span")
.First(s => s.HasClass("chapter-time")).InnerText; .First(s => s.HasClass("chapter-time")).InnerText;
year = Convert.ToInt32(yearString.Split(',')[^1]) + 2000; int year = Convert.ToInt32(yearString.Split(',')[^1]) + 2000;
return new Publication(sortName, author, description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links, return new Publication(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
year, originalLanguage, status, publicationId); year, originalLanguage, status, publicationId);
} }
@ -130,13 +131,20 @@ public class Manganato : Connector
string requestUrl = $"https://chapmanganato.com/{publication.publicationId}"; string requestUrl = $"https://chapmanganato.com/{publication.publicationId}";
DownloadClient.RequestResult requestResult = DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, (byte)1); downloadClient.MakeRequest(requestUrl, (byte)1);
if (requestResult.statusCode != HttpStatusCode.OK) if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Chapter>(); return Array.Empty<Chapter>();
return ParseChaptersFromHtml(requestResult.result); //Return Chapters ordered by Chapter-Number
NumberFormatInfo chapterNumberFormatInfo = new()
{
NumberDecimalSeparator = "."
};
List<Chapter> chapters = ParseChaptersFromHtml(requestResult.result);
logger?.WriteLine(this.GetType().ToString(), $"Done getting Chapters for {publication.internalId}");
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
} }
private Chapter[] ParseChaptersFromHtml(Stream html) private List<Chapter> ParseChaptersFromHtml(Stream html)
{ {
StreamReader reader = new (html); StreamReader reader = new (html);
string htmlString = reader.ReadToEnd(); string htmlString = reader.ReadToEnd();
@ -158,24 +166,26 @@ public class Manganato : Connector
ret.Add(new Chapter(chapterName, volumeNumber, chapterNumber, url)); ret.Add(new Chapter(chapterName, volumeNumber, chapterNumber, url));
} }
ret.Reverse(); ret.Reverse();
return ret.ToArray(); return ret;
} }
public override void DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask) public override HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null)
{ {
if (cancellationToken?.IsCancellationRequested ?? false)
return HttpStatusCode.RequestTimeout;
logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}"); logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
string requestUrl = chapter.url; string requestUrl = chapter.url;
DownloadClient.RequestResult requestResult = DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, (byte)1); downloadClient.MakeRequest(requestUrl, (byte)1);
if (requestResult.statusCode != HttpStatusCode.OK) if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return; return requestResult.statusCode;
string[] imageUrls = ParseImageUrlsFromHtml(requestResult.result); string[] imageUrls = ParseImageUrlsFromHtml(requestResult.result);
string comicInfoPath = Path.GetTempFileName(); string comicInfoPath = Path.GetTempFileName();
File.WriteAllText(comicInfoPath, GetComicInfoXmlString(publication, chapter, logger)); File.WriteAllText(comicInfoPath, GetComicInfoXmlString(publication, chapter, logger));
DownloadChapterImages(imageUrls, GetArchiveFilePath(publication, chapter), (byte)1, parentTask, comicInfoPath, "https://chapmanganato.com/"); return DownloadChapterImages(imageUrls, GetArchiveFilePath(publication, chapter), (byte)1, parentTask, comicInfoPath, "https://chapmanganato.com/", cancellationToken);
} }
private string[] ParseImageUrlsFromHtml(Stream html) private string[] ParseImageUrlsFromHtml(Stream html)

View File

@ -1,4 +1,5 @@
using System.Net; using System.Globalization;
using System.Net;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Xml.Linq; using System.Xml.Linq;
using HtmlAgilityPack; using HtmlAgilityPack;
@ -13,7 +14,7 @@ public class Mangasee : Connector
{ {
public override string name { get; } public override string name { get; }
private IBrowser? _browser = null; private IBrowser? _browser = null;
private const string ChromiumVersion = "1153303"; private const string ChromiumVersion = "1154303";
public Mangasee(string downloadLocation, string imageCachePath, Logger? logger) : base(downloadLocation, public Mangasee(string downloadLocation, string imageCachePath, Logger? logger) : base(downloadLocation,
imageCachePath, logger) imageCachePath, logger)
@ -79,7 +80,7 @@ public class Mangasee : Connector
string requestUrl = $"https://mangasee123.com/_search.php"; string requestUrl = $"https://mangasee123.com/_search.php";
DownloadClient.RequestResult requestResult = DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, (byte)1); downloadClient.MakeRequest(requestUrl, (byte)1);
if (requestResult.statusCode != HttpStatusCode.OK) if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Publication>(); return Array.Empty<Publication>();
return ParsePublicationsFromHtml(requestResult.result, publicationTitle); return ParsePublicationsFromHtml(requestResult.result, publicationTitle);
@ -109,7 +110,7 @@ public class Mangasee : Connector
{ {
DownloadClient.RequestResult requestResult = DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest($"https://mangasee123.com/manga/{orderedItem.i}", (byte)1); downloadClient.MakeRequest($"https://mangasee123.com/manga/{orderedItem.i}", (byte)1);
if (requestResult.statusCode != HttpStatusCode.OK) if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Publication>(); return Array.Empty<Publication>();
ret.Add(ParseSinglePublicationFromHtml(requestResult.result, orderedItem.s, orderedItem.i, orderedItem.a)); ret.Add(ParseSinglePublicationFromHtml(requestResult.result, orderedItem.s, orderedItem.i, orderedItem.a));
} }
@ -140,10 +141,9 @@ public class Mangasee : Connector
HtmlNode[] authorsNodes = attributes.Descendants("li") HtmlNode[] authorsNodes = attributes.Descendants("li")
.First(node => node.InnerText.Contains("author(s):", StringComparison.CurrentCultureIgnoreCase)) .First(node => node.InnerText.Contains("author(s):", StringComparison.CurrentCultureIgnoreCase))
.Descendants("a").ToArray(); .Descendants("a").ToArray();
string[] authors = new string[authorsNodes.Length]; List<string> authors = new();
for (int j = 0; j < authors.Length; j++) foreach(HtmlNode authorNode in authorsNodes)
authors[j] = authorsNodes[j].InnerText; authors.Add(authorNode.InnerText);
string author = string.Join(" - ", authors);
HtmlNode[] genreNodes = attributes.Descendants("li") HtmlNode[] genreNodes = attributes.Descendants("li")
.First(node => node.InnerText.Contains("genre(s):", StringComparison.CurrentCultureIgnoreCase)) .First(node => node.InnerText.Contains("genre(s):", StringComparison.CurrentCultureIgnoreCase))
@ -170,7 +170,7 @@ public class Mangasee : Connector
foreach(string at in a) foreach(string at in a)
altTitles.Add((i++).ToString(), at); altTitles.Add((i++).ToString(), at);
return new Publication(sortName, author, description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links, return new Publication(sortName, authors, description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
year, originalLanguage, status, publicationId); year, originalLanguage, status, publicationId);
} }
@ -200,20 +200,29 @@ public class Mangasee : Connector
ret.Add(new Chapter("", volumeNumber, chapterNumber, url)); ret.Add(new Chapter("", volumeNumber, chapterNumber, url));
} }
ret.Reverse(); //Return Chapters ordered by Chapter-Number
return ret.ToArray(); NumberFormatInfo chapterNumberFormatInfo = new()
{
NumberDecimalSeparator = "."
};
logger?.WriteLine(this.GetType().ToString(), $"Done getting Chapters for {publication.internalId}");
return ret.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
} }
public override void DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask) public override HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null)
{ {
while (this._browser is null) if (cancellationToken?.IsCancellationRequested ?? false)
return HttpStatusCode.RequestTimeout;
while (this._browser is null && !(cancellationToken?.IsCancellationRequested??false))
{ {
logger?.WriteLine(this.GetType().ToString(), "Waiting for headless browser to download..."); logger?.WriteLine(this.GetType().ToString(), "Waiting for headless browser to download...");
Thread.Sleep(1000); Thread.Sleep(1000);
} }
if (cancellationToken?.IsCancellationRequested??false)
return HttpStatusCode.RequestTimeout;
logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}"); logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
IPage page = _browser.NewPageAsync().Result; IPage page = _browser!.NewPageAsync().Result;
IResponse response = page.GoToAsync(chapter.url).Result; IResponse response = page.GoToAsync(chapter.url).Result;
if (response.Ok) if (response.Ok)
{ {
@ -229,7 +238,8 @@ public class Mangasee : Connector
string comicInfoPath = Path.GetTempFileName(); string comicInfoPath = Path.GetTempFileName();
File.WriteAllText(comicInfoPath, GetComicInfoXmlString(publication, chapter, logger)); File.WriteAllText(comicInfoPath, GetComicInfoXmlString(publication, chapter, logger));
DownloadChapterImages(urls.ToArray(), GetArchiveFilePath(publication, chapter), (byte)1, parentTask, comicInfoPath); return DownloadChapterImages(urls.ToArray(), GetArchiveFilePath(publication, chapter), (byte)1, parentTask, comicInfoPath, cancellationToken:cancellationToken);
} }
return response.Status;
} }
} }

View File

@ -23,6 +23,7 @@ public abstract class LibraryManager
/// <param name="baseUrl">Base-URL of Komga instance, no trailing slashes(/)</param> /// <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="auth">Base64 string of username and password (username):(password)</param>
/// <param name="logger"></param> /// <param name="logger"></param>
/// <param name="libraryType"></param>
protected LibraryManager(string baseUrl, string auth, Logger? logger, LibraryType libraryType) protected LibraryManager(string baseUrl, string auth, Logger? logger, LibraryType libraryType)
{ {
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
@ -49,9 +50,8 @@ public abstract class LibraryManager
Method = HttpMethod.Get, Method = HttpMethod.Get,
RequestUri = new Uri(url) RequestUri = new Uri(url)
}; };
logger?.WriteLine("LibraryManager", $"GET {url}");
HttpResponseMessage response = client.Send(requestMessage); HttpResponseMessage response = client.Send(requestMessage);
logger?.WriteLine("LibraryManager", $"{(int)response.StatusCode} {response.StatusCode}: {response.ReasonPhrase}"); logger?.WriteLine("LibraryManager", $"GET {url} -> {(int)response.StatusCode}: {response.ReasonPhrase}");
if(response.StatusCode is HttpStatusCode.Unauthorized && response.RequestMessage!.RequestUri!.AbsoluteUri != url) if(response.StatusCode is HttpStatusCode.Unauthorized && response.RequestMessage!.RequestUri!.AbsoluteUri != url)
return MakeRequest(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth, logger); return MakeRequest(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth, logger);
@ -76,9 +76,8 @@ public abstract class LibraryManager
Method = HttpMethod.Post, Method = HttpMethod.Post,
RequestUri = new Uri(url) RequestUri = new Uri(url)
}; };
logger?.WriteLine("LibraryManager", $"POST {url}");
HttpResponseMessage response = client.Send(requestMessage); HttpResponseMessage response = client.Send(requestMessage);
logger?.WriteLine("LibraryManager", $"{(int)response.StatusCode} {response.StatusCode}: {response.ReasonPhrase}"); logger?.WriteLine("LibraryManager", $"POST {url} -> {(int)response.StatusCode}: {response.ReasonPhrase}");
if(response.StatusCode is HttpStatusCode.Unauthorized && response.RequestMessage!.RequestUri!.AbsoluteUri != url) if(response.StatusCode is HttpStatusCode.Unauthorized && response.RequestMessage!.RequestUri!.AbsoluteUri != url)
return MakePost(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth, logger); return MakePost(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth, logger);

View File

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

View File

@ -0,0 +1,49 @@
using System.Text;
using Logging;
using Newtonsoft.Json;
namespace Tranga.NotificationManagers;
public class Gotify : NotificationManager
{
public string endpoint { get; }
public string appToken { get; }
private readonly HttpClient _client = new();
public Gotify(string endpoint, string appToken, Logger? logger = null) : base(NotificationManagerType.Gotify, logger)
{
this.endpoint = endpoint;
this.appToken = appToken;
}
public override void SendNotification(string title, string notificationText)
{
logger?.WriteLine(this.GetType().ToString(), $"Sending notification: {title} - {notificationText}");
MessageData message = new(title, notificationText);
HttpRequestMessage request = new(HttpMethod.Post, $"{endpoint}/message");
request.Headers.Add("X-Gotify-Key", this.appToken);
request.Content = new StringContent(JsonConvert.SerializeObject(message, Formatting.None), Encoding.UTF8, "application/json");
HttpResponseMessage response = _client.Send(request);
if (!response.IsSuccessStatusCode)
{
StreamReader sr = new (response.Content.ReadAsStream());
logger?.WriteLine(this.GetType().ToString(), $"{response.StatusCode}: {sr.ReadToEnd()}");
}
}
private class MessageData
{
public string message { get; }
public long priority { get; }
public string title { get; }
public Dictionary<string, object> extras { get; }
public MessageData(string title, string message)
{
this.title = title;
this.message = message;
this.extras = new();
this.priority = 4;
}
}
}

View File

@ -0,0 +1,43 @@
using System.Text;
using Logging;
using Newtonsoft.Json;
namespace Tranga.NotificationManagers;
public class LunaSea : NotificationManager
{
public string webhook { get; }
private readonly HttpClient _client = new();
public LunaSea(string webhook, Logger? logger = null) : base(NotificationManagerType.LunaSea, logger)
{
this.webhook = webhook;
}
public override void SendNotification(string title, string notificationText)
{
logger?.WriteLine(this.GetType().ToString(), $"Sending notification: {title} - {notificationText}");
MessageData message = new(title, notificationText);
HttpRequestMessage request = new(HttpMethod.Post, webhook);
request.Content = new StringContent(JsonConvert.SerializeObject(message, Formatting.None), Encoding.UTF8, "application/json");
HttpResponseMessage response = _client.Send(request);
if (!response.IsSuccessStatusCode)
{
StreamReader sr = new (response.Content.ReadAsStream());
logger?.WriteLine(this.GetType().ToString(), $"{response.StatusCode}: {sr.ReadToEnd()}");
}
}
private class MessageData
{
public string title { get; }
public string body { get; }
public string image { get; }
public MessageData(string title, string body)
{
this.title = title;
this.body = body;
this.image = "";
}
}
}

View File

@ -12,7 +12,7 @@ namespace Tranga;
public readonly struct Publication public readonly struct Publication
{ {
public string sortName { get; } public string sortName { get; }
public string? author { get; } public List<string> authors { get; }
public Dictionary<string,string> altTitles { get; } public Dictionary<string,string> altTitles { get; }
// ReSharper disable trice MemberCanBePrivate.Global, trust // ReSharper disable trice MemberCanBePrivate.Global, trust
public string? description { get; } public string? description { get; }
@ -29,10 +29,22 @@ public readonly struct Publication
private static readonly Regex LegalCharacters = new Regex(@"[A-Z]*[a-z]*[0-9]* *\.*-*,*'*\'*\)*\(*~*!*"); private static readonly Regex LegalCharacters = new Regex(@"[A-Z]*[a-z]*[0-9]* *\.*-*,*'*\'*\)*\(*~*!*");
public Publication(string sortName, string? author, string? description, Dictionary<string,string> altTitles, string[] tags, string? posterUrl, string? coverFileNameInCache, Dictionary<string,string>? links, int? year, string? originalLanguage, string status, string publicationId) [JsonConstructor] //Legacy
public Publication(string sortName, string? author, string? description, Dictionary<string, string> altTitles,
string[] tags, string? posterUrl, string? coverFileNameInCache, Dictionary<string, string>? links, int? year,
string? originalLanguage, string status, string publicationId)
{
List<string> pAuthors = new();
if(author is not null)
pAuthors.Add(author);
this = new Publication(sortName, pAuthors, description, altTitles, tags, posterUrl,
coverFileNameInCache, links, year, originalLanguage, status, publicationId);
}
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)
{ {
this.sortName = sortName; this.sortName = sortName;
this.author = author; this.authors = authors;
this.description = description; this.description = description;
this.altTitles = altTitles; this.altTitles = altTitles;
this.tags = tags; this.tags = tags;

View File

@ -1,7 +1,6 @@
using Logging; using Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using Tranga.Connectors; using Tranga.Connectors;
using Tranga.LibraryManagers;
using Tranga.TrangaTasks; using Tranga.TrangaTasks;
namespace Tranga; namespace Tranga;
@ -13,51 +12,13 @@ namespace Tranga;
public class TaskManager public class TaskManager
{ {
public Dictionary<Publication, List<Chapter>> chapterCollection = new(); public Dictionary<Publication, List<Chapter>> chapterCollection = new();
private HashSet<TrangaTask> _allTasks = new HashSet<TrangaTask>(); private HashSet<TrangaTask> _allTasks = new();
private bool _continueRunning = true; private bool _continueRunning = true;
private readonly Connector[] _connectors; private readonly Connector[] _connectors;
public TrangaSettings settings { get; } public TrangaSettings settings { get; }
private Logger? logger { get; } private Logger? logger { get; }
/// <param name="downloadFolderPath">Local path to save data (Manga) to</param> private readonly Dictionary<DownloadChapterTask, CancellationTokenSource> _runningDownloadChapterTasks = new();
/// <param name="workingDirectory">Path to the working directory</param>
/// <param name="imageCachePath">Path to the cover-image cache</param>
/// <param name="libraryManagers"></param>
/// <param name="logger"></param>
public TaskManager(string downloadFolderPath, string workingDirectory, string imageCachePath, HashSet<LibraryManager> libraryManagers, Logger? logger = null)
{
this.logger = logger;
this.settings = new TrangaSettings(downloadFolderPath, workingDirectory, libraryManagers);
ExportDataAndSettings();
this._connectors = new Connector[]
{
new MangaDex(downloadFolderPath, imageCachePath, logger),
new Manganato(downloadFolderPath, imageCachePath, logger),
new Mangasee(downloadFolderPath, imageCachePath, logger)
};
Thread taskChecker = new(TaskCheckerThread);
taskChecker.Start();
}
public void UpdateSettings(string? downloadLocation, string? komgaUrl, string? komgaAuth, string? kavitaUrl, string? kavitaUsername, string? kavitaPassword)
{
if (komgaUrl is not null && komgaAuth is not null && komgaUrl.Length > 0 && komgaAuth.Length > 0)
{
settings.libraryManagers.RemoveWhere(lm => lm.GetType() == typeof(Komga));
settings.libraryManagers.Add(new Komga(komgaUrl, komgaAuth, logger));
}
if (kavitaUrl is not null && kavitaUsername is not null && kavitaPassword is not null && kavitaUrl.Length > 0 && kavitaUsername.Length > 0 && kavitaPassword.Length > 0)
{
settings.libraryManagers.RemoveWhere(lm => lm.GetType() == typeof(Kavita));
settings.libraryManagers.Add(new Kavita(kavitaUrl, kavitaUsername, kavitaPassword, logger));
}
if (downloadLocation is not null && downloadLocation.Length > 0)
settings.downloadLocation = downloadLocation;
ExportDataAndSettings();
}
public TaskManager(TrangaSettings settings, Logger? logger = null) public TaskManager(TrangaSettings settings, Logger? logger = null)
{ {
@ -83,43 +44,64 @@ public class TaskManager
private void TaskCheckerThread() private void TaskCheckerThread()
{ {
logger?.WriteLine(this.GetType().ToString(), "Starting TaskCheckerThread."); logger?.WriteLine(this.GetType().ToString(), "Starting TaskCheckerThread.");
int allTasksWaitingLength = _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting); int waitingTasksCount = _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting);
while (_continueRunning) while (_continueRunning)
{ {
TrangaTask[] tmp = _allTasks.Where(taskQuery => foreach (TrangaTask waitingButExecute in _allTasks.Where(taskQuery =>
taskQuery.nextExecution < DateTime.Now && taskQuery.nextExecution < DateTime.Now &&
taskQuery.state is TrangaTask.ExecutionState.Waiting or TrangaTask.ExecutionState.Enqueued).ToArray(); taskQuery.state is TrangaTask.ExecutionState.Waiting))
foreach (TrangaTask task in tmp)
{ {
task.state = TrangaTask.ExecutionState.Enqueued; waitingButExecute.state = TrangaTask.ExecutionState.Enqueued;
switch (task.task) }
foreach (TrangaTask enqueuedTask in _allTasks.Where(enqueuedTask => enqueuedTask.state is TrangaTask.ExecutionState.Enqueued))
{
switch (enqueuedTask.task)
{ {
case TrangaTask.Task.DownloadNewChapters:
if (!_allTasks.Any(taskQuery => taskQuery.task == TrangaTask.Task.DownloadNewChapters &&
taskQuery.state is TrangaTask.ExecutionState.Running &&
((DownloadNewChaptersTask)taskQuery).connectorName == ((DownloadNewChaptersTask)task).connectorName))
{
ExecuteTaskNow(task);
}
break;
case TrangaTask.Task.DownloadChapter: case TrangaTask.Task.DownloadChapter:
case TrangaTask.Task.MonitorPublication:
if (!_allTasks.Any(taskQuery => if (!_allTasks.Any(taskQuery =>
taskQuery.task == TrangaTask.Task.DownloadChapter && {
taskQuery.state is TrangaTask.ExecutionState.Running && if (taskQuery.state is not TrangaTask.ExecutionState.Running) return false;
((DownloadChapterTask)taskQuery).connectorName == switch (taskQuery)
((DownloadChapterTask)task).connectorName)) {
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(task); ExecuteTaskNow(enqueuedTask);
} }
break; break;
case TrangaTask.Task.UpdateLibraries: case TrangaTask.Task.UpdateLibraries:
ExecuteTaskNow(task); ExecuteTaskNow(enqueuedTask);
break; break;
} }
} }
if(allTasksWaitingLength != _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting))
TrangaTask[] failedDownloadChapterTasks = _allTasks.Where(taskQuery =>
taskQuery.state is TrangaTask.ExecutionState.Failed && taskQuery is DownloadChapterTask).ToArray();
foreach (TrangaTask failedDownloadChapterTask in failedDownloadChapterTasks)
{
DeleteTask(failedDownloadChapterTask);
TrangaTask newTask = failedDownloadChapterTask.Clone();
failedDownloadChapterTask.parentTask?.AddChildTask(newTask);
AddTask(newTask);
}
TrangaTask[] successfulDownloadChapterTasks = _allTasks.Where(taskQuery =>
taskQuery.state is TrangaTask.ExecutionState.Success && taskQuery is DownloadChapterTask).ToArray();
foreach(TrangaTask successfulDownloadChapterTask in successfulDownloadChapterTasks)
{
DeleteTask(successfulDownloadChapterTask);
}
if(waitingTasksCount != _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting))
ExportDataAndSettings(); ExportDataAndSettings();
allTasksWaitingLength = _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting); waitingTasksCount = _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting);
Thread.Sleep(1000); Thread.Sleep(1000);
} }
} }
@ -131,10 +113,13 @@ public class TaskManager
public void ExecuteTaskNow(TrangaTask task) public void ExecuteTaskNow(TrangaTask task)
{ {
task.state = TrangaTask.ExecutionState.Running; task.state = TrangaTask.ExecutionState.Running;
CancellationTokenSource cToken = new ();
Task t = new(() => Task t = new(() =>
{ {
task.Execute(this, this.logger); task.Execute(this, this.logger, cToken.Token);
}); }, cToken.Token);
if(task is DownloadChapterTask chapterTask)
_runningDownloadChapterTasks.Add(chapterTask, cToken);
t.Start(); t.Start();
} }
@ -146,26 +131,23 @@ public class TaskManager
{ {
case TrangaTask.Task.UpdateLibraries: case TrangaTask.Task.UpdateLibraries:
//Only one UpdateKomgaLibrary Task //Only one UpdateKomgaLibrary Task
logger?.WriteLine(this.GetType().ToString(), $"Removing old {newTask.task}-Task."); logger?.WriteLine(this.GetType().ToString(), $"Replacing old {newTask.task}-Task.");
_allTasks.RemoveWhere(trangaTask => trangaTask.task is TrangaTask.Task.UpdateLibraries); _allTasks.RemoveWhere(trangaTask => trangaTask.task is TrangaTask.Task.UpdateLibraries);
_allTasks.Add(newTask);
break; break;
case TrangaTask.Task.DownloadNewChapters: case TrangaTask.Task.MonitorPublication:
IEnumerable<TrangaTask> matchingdnc = if (!_allTasks.Any(mTask => mTask is MonitorPublicationTask mpt && newTask is MonitorPublicationTask nMpt &&
_allTasks.Where(mTask => mTask.GetType() == typeof(DownloadNewChaptersTask)); mpt.publication.internalId == nMpt.publication.internalId &&
if (matchingdnc.All(mTask => mpt.connectorName == nMpt.connectorName))
((DownloadNewChaptersTask)mTask).publication.internalId != ((DownloadNewChaptersTask)newTask).publication.publicationId &&
((DownloadNewChaptersTask)mTask).connectorName != ((DownloadNewChaptersTask)newTask).connectorName))
_allTasks.Add(newTask); _allTasks.Add(newTask);
else else
logger?.WriteLine(this.GetType().ToString(), $"Task already exists {newTask}"); logger?.WriteLine(this.GetType().ToString(), $"Task already exists {newTask}");
break; break;
case TrangaTask.Task.DownloadChapter: case TrangaTask.Task.DownloadChapter:
IEnumerable<TrangaTask> matchingdc = if (!_allTasks.Any(mTask => mTask is DownloadChapterTask dct && newTask is DownloadChapterTask nDct &&
_allTasks.Where(mTask => mTask.GetType() == typeof(DownloadChapterTask)); dct.publication.internalId == nDct.publication.internalId &&
if (!matchingdc.Any(mTask => dct.connectorName == nDct.connectorName &&
((DownloadChapterTask)mTask).publication.internalId == ((DownloadChapterTask)newTask).publication.internalId && dct.chapter.sortNumber == nDct.chapter.sortNumber))
((DownloadChapterTask)mTask).connectorName == ((DownloadChapterTask)newTask).connectorName &&
((DownloadChapterTask)mTask).chapter.sortNumber == ((DownloadChapterTask)newTask).chapter.sortNumber))
_allTasks.Add(newTask); _allTasks.Add(newTask);
else else
logger?.WriteLine(this.GetType().ToString(), $"Task already exists {newTask}"); logger?.WriteLine(this.GetType().ToString(), $"Task already exists {newTask}");
@ -178,115 +160,60 @@ public class TaskManager
{ {
logger?.WriteLine(this.GetType().ToString(), $"Removing Task {removeTask}"); logger?.WriteLine(this.GetType().ToString(), $"Removing Task {removeTask}");
_allTasks.Remove(removeTask); _allTasks.Remove(removeTask);
} removeTask.parentTask?.RemoveChildTask(removeTask);
if (removeTask is DownloadChapterTask cRemoveTask && _runningDownloadChapterTasks.ContainsKey(cRemoveTask))
public TrangaTask? AddTask(TrangaTask.Task taskType, string? connectorName, string? publicationId,
TimeSpan reoccurrenceTime, string? language = "en")
{
TrangaTask? newTask = null;
switch (taskType)
{ {
case TrangaTask.Task.UpdateLibraries: _runningDownloadChapterTasks[cRemoveTask].Cancel();
newTask = new UpdateLibrariesTask(taskType, reoccurrenceTime); _runningDownloadChapterTasks.Remove(cRemoveTask);
break;
case TrangaTask.Task.DownloadNewChapters:
if(connectorName is null || publicationId is null || language is null)
logger?.WriteLine(this.GetType().ToString(), $"Values connectorName, publicationName and language can not be null.");
GetConnector(connectorName); //Check if connectorName is valid
Publication publication = GetAllPublications().First(pub => pub.internalId == publicationId);
newTask = new DownloadNewChaptersTask(taskType, connectorName!, publication, reoccurrenceTime, language!);
break;
} }
if(newTask is not null)
AddTask(newTask);
return newTask;
} }
/// <summary> public IEnumerable<TrangaTask> GetTasksMatching(TrangaTask.Task taskType, string? connectorName = null, string? searchString = null, string? internalId = null, string? chapterSortNumber = null)
/// Removes Task from task-collection
/// </summary>
/// <param name="task">TrangaTask.Task type</param>
/// <param name="connectorName">Name of Connector that was used</param>
/// <param name="publicationId">Publication that was used</param>
public void DeleteTask(TrangaTask.Task task, string? connectorName, string? publicationId)
{
logger?.WriteLine(this.GetType().ToString(), $"Removing Task {task} {publicationId}");
switch (task)
{
case TrangaTask.Task.UpdateLibraries:
//Only one UpdateKomgaLibrary Task
logger?.WriteLine(this.GetType().ToString(), $"Removing old {task}-Task.");
_allTasks.RemoveWhere(trangaTask => trangaTask.task is TrangaTask.Task.UpdateLibraries);
break;
case TrangaTask.Task.DownloadNewChapters:
if (connectorName is null || publicationId is null)
logger?.WriteLine(this.GetType().ToString(), "connectorName and publication can not be null");
else
{
_allTasks.RemoveWhere(mTask =>
mTask.GetType() == typeof(DownloadNewChaptersTask) &&
((DownloadNewChaptersTask)mTask).publication.internalId == publicationId &&
((DownloadNewChaptersTask)mTask).connectorName == connectorName!);
_allTasks.RemoveWhere(mTask =>
mTask.GetType() == typeof(DownloadChapterTask) &&
((DownloadChapterTask)mTask).publication.internalId == publicationId &&
((DownloadChapterTask)mTask).connectorName == connectorName!);
}
break;
}
ExportDataAndSettings();
}
public IEnumerable<TrangaTask> GetTasksMatching(TrangaTask.Task taskType, string? connectorName = null, string? searchString = null, string? internalId = null)
{ {
switch (taskType) switch (taskType)
{ {
case TrangaTask.Task.UpdateLibraries: case TrangaTask.Task.UpdateLibraries:
return _allTasks.Where(tTask => tTask.task == TrangaTask.Task.UpdateLibraries); return _allTasks.Where(tTask => tTask.task == TrangaTask.Task.UpdateLibraries);
case TrangaTask.Task.DownloadNewChapters: case TrangaTask.Task.MonitorPublication:
if(connectorName is null) if(connectorName is null)
return _allTasks.Where(tTask => tTask.task == taskType); return _allTasks.Where(tTask => tTask.task == taskType);
GetConnector(connectorName);//Name check GetConnector(connectorName);//Name check
IEnumerable<TrangaTask> matchingdnc = _allTasks.Where(tTask => tTask.GetType() == typeof(DownloadNewChaptersTask));
if (searchString is not null) if (searchString is not null)
{ {
return matchingdnc.Where(mTask => return _allTasks.Where(mTask =>
((DownloadNewChaptersTask)mTask).connectorName == connectorName && mTask is MonitorPublicationTask mpt && mpt.connectorName == connectorName &&
((DownloadNewChaptersTask)mTask).ToString().Contains(searchString, StringComparison.InvariantCultureIgnoreCase)); mpt.ToString().Contains(searchString, StringComparison.InvariantCultureIgnoreCase));
} }
else if (internalId is not null) else if (internalId is not null)
{ {
return matchingdnc.Where(mTask => return _allTasks.Where(mTask =>
((DownloadNewChaptersTask)mTask).connectorName == connectorName && mTask is MonitorPublicationTask mpt && mpt.connectorName == connectorName &&
((DownloadNewChaptersTask)mTask).publication.internalId == internalId); mpt.publication.internalId == internalId);
} }
else else
return _allTasks.Where(tTask => return _allTasks.Where(tTask =>
tTask.GetType() == typeof(DownloadNewChaptersTask) && tTask is MonitorPublicationTask mpt && mpt.connectorName == connectorName);
((DownloadNewChaptersTask)tTask).connectorName == connectorName);
case TrangaTask.Task.DownloadChapter: case TrangaTask.Task.DownloadChapter:
if(connectorName is null) if(connectorName is null)
return _allTasks.Where(tTask => tTask.task == taskType); return _allTasks.Where(tTask => tTask.task == taskType);
GetConnector(connectorName);//Name check GetConnector(connectorName);//Name check
IEnumerable<TrangaTask> matchingdc = _allTasks.Where(tTask => tTask.GetType() == typeof(DownloadChapterTask));
if (searchString is not null) if (searchString is not null)
{ {
return matchingdc.Where(mTask => return _allTasks.Where(mTask =>
((DownloadChapterTask)mTask).connectorName == connectorName && mTask is DownloadChapterTask dct && dct.connectorName == connectorName &&
((DownloadChapterTask)mTask).ToString().Contains(searchString, StringComparison.InvariantCultureIgnoreCase)); dct.ToString().Contains(searchString, StringComparison.InvariantCultureIgnoreCase));
} }
else if (internalId is not null) else if (internalId is not null && chapterSortNumber is not null)
{ {
return matchingdc.Where(mTask => return _allTasks.Where(mTask =>
((DownloadChapterTask)mTask).connectorName == connectorName && mTask is DownloadChapterTask dct && dct.connectorName == connectorName &&
((DownloadChapterTask)mTask).publication.publicationId == internalId); dct.publication.publicationId == internalId &&
dct.chapter.sortNumber == chapterSortNumber);
} }
else else
return _allTasks.Where(tTask => return _allTasks.Where(mTask =>
tTask.GetType() == typeof(DownloadChapterTask) && mTask is DownloadChapterTask dct && dct.connectorName == connectorName);
((DownloadChapterTask)tTask).connectorName == connectorName);
default: default:
return Array.Empty<TrangaTask>(); return Array.Empty<TrangaTask>();
@ -343,6 +270,31 @@ public class TaskManager
{ {
return this.chapterCollection.Keys.ToArray(); return this.chapterCollection.Keys.ToArray();
} }
/// <summary>
/// Updates the available Chapters of a Publication
/// </summary>
/// <param name="connector">Connector to use</param>
/// <param name="publication">Publication to check</param>
/// <param name="language">Language to receive chapters for</param>
/// <returns>List of Chapters that were previously not in collection</returns>
public List<Chapter> GetNewChaptersList(Connector connector, Publication publication, string language)
{
List<Chapter> newChaptersList = new();
chapterCollection.TryAdd(publication, newChaptersList); //To ensure publication is actually in collection
Chapter[] newChapters = connector.GetChapters(publication, language);
newChaptersList = newChapters.Where(nChapter => !connector.CheckChapterIsDownloaded(publication, nChapter)).ToList();
return newChaptersList;
}
public List<Chapter> GetExistingChaptersList(Connector connector, Publication publication, string language)
{
Chapter[] newChapters = connector.GetChapters(publication, language);
return newChapters.Where(nChapter => connector.CheckChapterIsDownloaded(publication, nChapter)).ToList();
}
/// <summary> /// <summary>
/// Return Connector with given Name /// Return Connector with given Name
@ -390,6 +342,17 @@ public class TaskManager
this._allTasks = JsonConvert.DeserializeObject<HashSet<TrangaTask>>(buffer, new JsonSerializerSettings() { Converters = { new TrangaTask.TrangaTaskJsonConverter() } })!; 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))
{
TrangaTask? parentTask = this._allTasks.FirstOrDefault(pTask => pTask.taskId == task.parentTaskId);
if (parentTask is not null)
{
task.parentTask = parentTask;
parentTask.AddChildTask(task);
}
}
if (File.Exists(settings.knownPublicationsPath)) if (File.Exists(settings.knownPublicationsPath))
{ {
logger?.WriteLine(this.GetType().ToString(), $"Importing known publications from {settings.knownPublicationsPath}"); logger?.WriteLine(this.GetType().ToString(), $"Importing known publications from {settings.knownPublicationsPath}");

View File

@ -1,6 +1,7 @@
using Logging; using Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using Tranga.LibraryManagers; using Tranga.LibraryManagers;
using Tranga.NotificationManagers;
namespace Tranga; namespace Tranga;
@ -8,32 +9,79 @@ public class TrangaSettings
{ {
public string downloadLocation { get; set; } public string downloadLocation { get; set; }
public string workingDirectory { get; set; } public string workingDirectory { get; set; }
[JsonIgnore]public string settingsFilePath => Path.Join(workingDirectory, "settings.json"); [JsonIgnore] public string settingsFilePath => Path.Join(workingDirectory, "settings.json");
[JsonIgnore]public string tasksFilePath => Path.Join(workingDirectory, "tasks.json"); [JsonIgnore] public string tasksFilePath => Path.Join(workingDirectory, "tasks.json");
[JsonIgnore]public string knownPublicationsPath => Path.Join(workingDirectory, "knownPublications.json"); [JsonIgnore] public string knownPublicationsPath => Path.Join(workingDirectory, "knownPublications.json");
[JsonIgnore] public string coverImageCache => Path.Join(workingDirectory, "imageCache"); [JsonIgnore] public string coverImageCache => Path.Join(workingDirectory, "imageCache");
public HashSet<LibraryManager> libraryManagers { get; } public HashSet<LibraryManager> libraryManagers { get; }
public HashSet<NotificationManager> notificationManagers { get; }
public TrangaSettings(string downloadLocation, string workingDirectory, HashSet<LibraryManager> libraryManagers) public TrangaSettings(string downloadLocation, string workingDirectory, HashSet<LibraryManager>? libraryManagers,
HashSet<NotificationManager>? notificationManagers)
{ {
if (downloadLocation.Length < 1 || workingDirectory.Length < 1) if (downloadLocation.Length < 1 || workingDirectory.Length < 1)
throw new ArgumentException("Download-location and working-directory paths can not be empty!"); throw new ArgumentException("Download-location and working-directory paths can not be empty!");
this.workingDirectory = workingDirectory; this.workingDirectory = workingDirectory;
this.downloadLocation = downloadLocation; this.downloadLocation = downloadLocation;
this.libraryManagers = libraryManagers; this.libraryManagers = libraryManagers??new();
this.notificationManagers = notificationManagers??new();
} }
public static TrangaSettings LoadSettings(string importFilePath, Logger? logger) public static TrangaSettings LoadSettings(string importFilePath, Logger? logger)
{ {
if (!File.Exists(importFilePath)) if (!File.Exists(importFilePath))
return new TrangaSettings(Path.Join(Directory.GetCurrentDirectory(), "Downloads"), Directory.GetCurrentDirectory(), new HashSet<LibraryManager>()); return new TrangaSettings(Path.Join(Directory.GetCurrentDirectory(), "Downloads"),
Directory.GetCurrentDirectory(), new HashSet<LibraryManager>(), new HashSet<NotificationManager>());
string toRead = File.ReadAllText(importFilePath); string toRead = File.ReadAllText(importFilePath);
TrangaSettings settings = JsonConvert.DeserializeObject<TrangaSettings>(toRead, new JsonSerializerSettings() { Converters = { new LibraryManager.LibraryManagerJsonConverter()} })!; TrangaSettings settings = JsonConvert.DeserializeObject<TrangaSettings>(toRead,
if(logger is not null) new JsonSerializerSettings { Converters = { new NotificationManager.NotificationManagerJsonConverter(), new LibraryManager.LibraryManagerJsonConverter() } })!;
foreach(LibraryManager lm in settings.libraryManagers) if (logger is not null)
foreach (LibraryManager lm in settings.libraryManagers)
lm.AddLogger(logger); lm.AddLogger(logger);
return settings; return settings;
} }
public void UpdateSettings(UpdateField field, Logger? logger = null, params string[] values)
{
switch (field)
{
case UpdateField.DownloadLocation:
if (values.Length != 1)
return;
this.downloadLocation = values[0];
break;
case UpdateField.Komga:
if (values.Length != 2)
return;
libraryManagers.RemoveWhere(lm => lm.GetType() == typeof(Komga));
libraryManagers.Add(new Komga(values[0], values[1], logger));
break;
case UpdateField.Kavita:
if (values.Length != 3)
return;
libraryManagers.RemoveWhere(lm => lm.GetType() == typeof(Kavita));
libraryManagers.Add(new Kavita(values[0], values[1], values[2], logger));
break;
case UpdateField.Gotify:
if (values.Length != 2)
return;
notificationManagers.RemoveWhere(nm => nm.GetType() == typeof(Gotify));
Gotify newGotify = new(values[0], values[1], logger);
notificationManagers.Add(newGotify);
newGotify.SendNotification("Success!", "Gotify was added to Tranga!");
break;
case UpdateField.LunaSea:
if(values.Length != 1)
return;
notificationManagers.RemoveWhere(nm => nm.GetType() == typeof(LunaSea));
LunaSea newLunaSea = new(values[0], logger);
notificationManagers.Add(newLunaSea);
newLunaSea.SendNotification("Success!", "LunaSea was added to Tranga!");
break;
}
}
public enum UpdateField { DownloadLocation, Komga, Kavita, Gotify, LunaSea}
} }

View File

@ -1,4 +1,5 @@
using System.Text.Json.Serialization; using System.Net;
using System.Text.Json.Serialization;
using Logging; using Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@ -10,7 +11,7 @@ namespace Tranga;
/// <summary> /// <summary>
/// Stores information on Task, when implementing new Tasks also update the serializer /// Stores information on Task, when implementing new Tasks also update the serializer
/// </summary> /// </summary>
[JsonDerivedType(typeof(DownloadNewChaptersTask), 2)] [JsonDerivedType(typeof(MonitorPublicationTask), 2)]
[JsonDerivedType(typeof(UpdateLibrariesTask), 3)] [JsonDerivedType(typeof(UpdateLibrariesTask), 3)]
[JsonDerivedType(typeof(DownloadChapterTask), 4)] [JsonDerivedType(typeof(DownloadChapterTask), 4)]
public abstract class TrangaTask public abstract class TrangaTask
@ -19,40 +20,32 @@ public abstract class TrangaTask
// ReSharper disable once MemberCanBePrivate.Global I want it thaaat way // ReSharper disable once MemberCanBePrivate.Global I want it thaaat way
public TimeSpan reoccurrence { get; } public TimeSpan reoccurrence { get; }
public DateTime lastExecuted { get; set; } public DateTime lastExecuted { get; set; }
[Newtonsoft.Json.JsonIgnore] public ExecutionState state { get; set; }
public Task task { get; } public Task task { get; }
[Newtonsoft.Json.JsonIgnore]public ExecutionState state { get; set; } public string taskId { get; }
[Newtonsoft.Json.JsonIgnore]public float progress { get; protected set; } [Newtonsoft.Json.JsonIgnore] public TrangaTask? parentTask { get; set; }
public string? parentTaskId { get; set; }
[Newtonsoft.Json.JsonIgnore] protected HashSet<TrangaTask> childTasks { get; }
public double progress => GetProgress();
[Newtonsoft.Json.JsonIgnore]public DateTime executionStarted { get; private set; }
[Newtonsoft.Json.JsonIgnore]public DateTime lastChange { get; private set; }
[Newtonsoft.Json.JsonIgnore]public DateTime executionApproximatelyFinished => progress != 0 ? lastChange.Add(GetRemainingTime()) : DateTime.MaxValue;
public TimeSpan executionApproximatelyRemaining => executionApproximatelyFinished.Subtract(DateTime.Now);
[Newtonsoft.Json.JsonIgnore]public DateTime nextExecution => lastExecuted.Add(reoccurrence); [Newtonsoft.Json.JsonIgnore]public DateTime nextExecution => lastExecuted.Add(reoccurrence);
[Newtonsoft.Json.JsonIgnore]public DateTime executionStarted { get; protected set; }
[Newtonsoft.Json.JsonIgnore] public enum ExecutionState { Waiting, Enqueued, Running, Failed, Success }
public DateTime executionApproximatelyFinished => this.progress != 0
? this.executionStarted.Add(DateTime.Now.Subtract(this.executionStarted) / this.progress)
: DateTime.MaxValue;
[Newtonsoft.Json.JsonIgnore]
public TimeSpan executionApproximatelyRemaining => this.executionApproximatelyFinished.Subtract(DateTime.Now);
public enum ExecutionState protected TrangaTask(Task task, TimeSpan reoccurrence, TrangaTask? parentTask = null)
{
Waiting,
Enqueued,
Running
};
protected TrangaTask(Task task, TimeSpan reoccurrence)
{ {
this.reoccurrence = reoccurrence; this.reoccurrence = reoccurrence;
this.lastExecuted = DateTime.Now.Subtract(reoccurrence); this.lastExecuted = DateTime.Now.Subtract(reoccurrence);
this.task = task; this.task = task;
this.progress = 0f; this.executionStarted = DateTime.UnixEpoch;
this.executionStarted = DateTime.Now; this.lastChange = DateTime.MaxValue;
} this.taskId = Convert.ToBase64String(BitConverter.GetBytes(new Random().Next()));
this.childTasks = new();
public float IncrementProgress(float amount) this.parentTask = parentTask;
{ this.parentTaskId = parentTask?.taskId;
this.progress += amount;
return this.progress;
} }
/// <summary> /// <summary>
@ -60,35 +53,71 @@ public abstract class TrangaTask
/// </summary> /// </summary>
/// <param name="taskManager"></param> /// <param name="taskManager"></param>
/// <param name="logger"></param> /// <param name="logger"></param>
protected abstract void ExecuteTask(TaskManager taskManager, Logger? logger); /// <param name="cancellationToken"></param>
protected abstract HttpStatusCode ExecuteTask(TaskManager taskManager, Logger? logger, CancellationToken? cancellationToken = null);
public abstract TrangaTask Clone();
protected abstract double GetProgress();
/// <summary> /// <summary>
/// Execute the task /// Execute the task
/// </summary> /// </summary>
/// <param name="taskManager">Should be the parent taskManager</param> /// <param name="taskManager">Should be the parent taskManager</param>
/// <param name="logger"></param> /// <param name="logger"></param>
public void Execute(TaskManager taskManager, Logger? logger) /// <param name="cancellationToken"></param>
public void Execute(TaskManager taskManager, Logger? logger, CancellationToken? cancellationToken = null)
{ {
logger?.WriteLine(this.GetType().ToString(), $"Executing Task {this}"); logger?.WriteLine(this.GetType().ToString(), $"Executing Task {this}");
this.state = ExecutionState.Running; this.state = ExecutionState.Running;
this.executionStarted = DateTime.Now; this.executionStarted = DateTime.Now;
ExecuteTask(taskManager, logger); this.lastChange = DateTime.Now;
this.lastExecuted = DateTime.Now; HttpStatusCode statusCode = ExecuteTask(taskManager, logger, cancellationToken);
this.state = ExecutionState.Waiting; while(childTasks.Any(ct => ct.state is ExecutionState.Enqueued or ExecutionState.Running))
Thread.Sleep(1000);
if ((int)statusCode >= 200 && (int)statusCode < 300)
{
this.lastExecuted = DateTime.Now;
if (this is DownloadChapterTask)
this.state = ExecutionState.Success;
else
this.state = ExecutionState.Waiting;
}
else
{
if (this is DownloadChapterTask && statusCode == HttpStatusCode.NotFound)
this.state = ExecutionState.Success;
else
this.state = ExecutionState.Failed;
this.lastExecuted = DateTime.MaxValue;
}
logger?.WriteLine(this.GetType().ToString(), $"Finished Executing Task {this}"); logger?.WriteLine(this.GetType().ToString(), $"Finished Executing Task {this}");
} }
/// <returns>True if elapsed time since last execution is greater than set interval</returns> public void AddChildTask(TrangaTask childTask)
public bool ShouldExecute()
{ {
return nextExecution < DateTime.Now && state is ExecutionState.Waiting; this.childTasks.Add(childTask);
}
public void RemoveChildTask(TrangaTask childTask)
{
this.childTasks.Remove(childTask);
}
private TimeSpan GetRemainingTime()
{
if(progress == 0 || lastChange == DateTime.MaxValue || executionStarted == DateTime.UnixEpoch)
return TimeSpan.Zero;
TimeSpan elapsed = lastChange.Subtract(executionStarted);
return elapsed.Divide(progress).Subtract(elapsed);
} }
public enum Task : byte public enum Task : byte
{ {
DownloadNewChapters = 2, MonitorPublication = 2,
UpdateLibraries = 3, UpdateLibraries = 3,
DownloadChapter = 4 DownloadChapter = 4,
DownloadNewChapters = 2 //legacy
} }
public override string ToString() public override string ToString()
@ -100,14 +129,14 @@ public abstract class TrangaTask
{ {
public override bool CanConvert(Type objectType) public override bool CanConvert(Type objectType)
{ {
return (objectType == typeof(TrangaTask)); return objectType == typeof(TrangaTask);
} }
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{ {
JObject jo = JObject.Load(reader); JObject jo = JObject.Load(reader);
if (jo["task"]!.Value<Int64>() == (Int64)Task.DownloadNewChapters) if (jo["task"]!.Value<Int64>() == (Int64)Task.MonitorPublication)
return jo.ToObject<DownloadNewChaptersTask>(serializer)!; return jo.ToObject<MonitorPublicationTask>(serializer)!;
if (jo["task"]!.Value<Int64>() == (Int64)Task.UpdateLibraries) if (jo["task"]!.Value<Int64>() == (Int64)Task.UpdateLibraries)
return jo.ToObject<UpdateLibrariesTask>(serializer)!; return jo.ToObject<UpdateLibrariesTask>(serializer)!;

View File

@ -1,5 +1,5 @@
using Logging; using System.Net;
using Newtonsoft.Json; using Logging;
namespace Tranga.TrangaTasks; namespace Tranga.TrangaTasks;
@ -9,30 +9,44 @@ public class DownloadChapterTask : TrangaTask
public Publication publication { get; } public Publication publication { get; }
public string language { get; } public string language { get; }
public Chapter chapter { get; } public Chapter chapter { get; }
[JsonIgnore]private DownloadNewChaptersTask? parentTask { get; init; }
private double _dctProgress = 0;
public DownloadChapterTask(Task task, string connectorName, Publication publication, Chapter chapter, string language = "en", DownloadNewChaptersTask? parentTask = null) : base(task, TimeSpan.Zero)
public DownloadChapterTask(string connectorName, Publication publication, Chapter chapter, string language = "en", MonitorPublicationTask? parentTask = null) : base(Task.DownloadChapter, TimeSpan.Zero, parentTask)
{ {
this.chapter = chapter; this.chapter = chapter;
this.connectorName = connectorName; this.connectorName = connectorName;
this.publication = publication; this.publication = publication;
this.language = language; this.language = language;
this.parentTask = parentTask;
} }
protected override void ExecuteTask(TaskManager taskManager, Logger? logger) protected override HttpStatusCode ExecuteTask(TaskManager taskManager, Logger? logger, CancellationToken? cancellationToken = null)
{ {
Publication pub = (Publication)this.publication!; if (cancellationToken?.IsCancellationRequested ?? false)
return HttpStatusCode.RequestTimeout;
Connector connector = taskManager.GetConnector(this.connectorName); Connector connector = taskManager.GetConnector(this.connectorName);
connector.DownloadChapter(pub, this.chapter, this); connector.CopyCoverFromCacheToDownloadLocation(this.publication, taskManager.settings);
taskManager.DeleteTask(this); HttpStatusCode downloadSuccess = connector.DownloadChapter(this.publication, this.chapter, this, cancellationToken);
if((int)downloadSuccess >= 200 && (int)downloadSuccess < 300 && parentTask is not null)
foreach(NotificationManager nm in taskManager.settings.notificationManagers)
nm.SendNotification("New Chapter downloaded", $"{this.publication.sortName} {this.chapter.chapterNumber} {this.chapter.name}");
return downloadSuccess;
} }
public new float IncrementProgress(float amount) public override TrangaTask Clone()
{ {
this.progress += amount; return new DownloadChapterTask(this.connectorName, this.publication, this.chapter,
parentTask?.IncrementProgress(amount); this.language, (MonitorPublicationTask?)this.parentTask);
return this.progress; }
protected override double GetProgress()
{
return _dctProgress;
}
internal void IncrementProgress(double amount)
{
this._dctProgress += amount;
} }
public override string ToString() public override string ToString()

View File

@ -1,68 +0,0 @@
using Logging;
using Newtonsoft.Json;
namespace Tranga.TrangaTasks;
public class DownloadNewChaptersTask : TrangaTask
{
public string connectorName { get; }
public Publication publication { get; }
public string language { get; }
[JsonIgnore]private int childTaskAmount { get; set; }
public DownloadNewChaptersTask(Task task, string connectorName, Publication publication, TimeSpan reoccurrence, string language = "en") : base(task, reoccurrence)
{
this.connectorName = connectorName;
this.publication = publication;
this.language = language;
childTaskAmount = 0;
}
public new float IncrementProgress(float amount)
{
this.progress += amount / this.childTaskAmount;
return this.progress;
}
protected override void ExecuteTask(TaskManager taskManager, Logger? logger)
{
Publication pub = publication!;
Connector connector = taskManager.GetConnector(this.connectorName);
//Check if Publication already has a Folder
pub.CreatePublicationFolder(taskManager.settings.downloadLocation);
List<Chapter> newChapters = GetNewChaptersList(connector, pub, language!, ref taskManager.chapterCollection);
this.childTaskAmount = newChapters.Count;
connector.CopyCoverFromCacheToDownloadLocation(pub, taskManager.settings);
pub.SaveSeriesInfoJson(connector.downloadLocation);
foreach (Chapter newChapter in newChapters)
taskManager.AddTask(new DownloadChapterTask(Task.DownloadChapter, this.connectorName!, pub, newChapter, this.language, this));
}
/// <summary>
/// Updates the available Chapters of a Publication
/// </summary>
/// <param name="connector">Connector to use</param>
/// <param name="publication">Publication to check</param>
/// <param name="language">Language to receive chapters for</param>
/// <param name="chapterCollection"></param>
/// <returns>List of Chapters that were previously not in collection</returns>
private static List<Chapter> GetNewChaptersList(Connector connector, Publication publication, string language, ref Dictionary<Publication, List<Chapter>> chapterCollection)
{
List<Chapter> newChaptersList = new();
chapterCollection.TryAdd(publication, newChaptersList); //To ensure publication is actually in collection
Chapter[] newChapters = connector.GetChapters(publication, language);
newChaptersList = newChapters.Where(nChapter => !connector.CheckChapterIsDownloaded(publication, nChapter)).ToList();
return newChaptersList;
}
public override string ToString()
{
return $"{base.ToString()}, {connectorName}, {publication.sortName} {publication.internalId}";
}
}

View File

@ -0,0 +1,60 @@
using System.Net;
using Logging;
namespace Tranga.TrangaTasks;
public class MonitorPublicationTask : TrangaTask
{
public string connectorName { get; }
public Publication publication { get; }
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, Logger? logger, 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 = taskManager.GetNewChaptersList(connector, publication, language);
connector.CopyCoverFromCacheToDownloadLocation(publication, taskManager.settings);
publication.SaveSeriesInfoJson(connector.downloadLocation);
foreach (Chapter newChapter in newChapters)
{
DownloadChapterTask newTask = new (this.connectorName, publication, newChapter, this.language, this);
this.childTasks.Add(newTask);
newTask.state = ExecutionState.Enqueued;
taskManager.AddTask(newTask);
}
return HttpStatusCode.OK;
}
public override TrangaTask Clone()
{
return new MonitorPublicationTask(this.connectorName, this.publication, this.reoccurrence,
this.language);
}
protected override double GetProgress()
{
if (this.childTasks.Count > 0)
return this.childTasks.Sum(ct => ct.progress) / childTasks.Count;
return 1;
}
public override string ToString()
{
return $"{base.ToString()}, {connectorName}, {publication.sortName} {publication.internalId}";
}
}

View File

@ -1,17 +1,30 @@
using Logging; using System.Net;
using Logging;
namespace Tranga.TrangaTasks; namespace Tranga.TrangaTasks;
public class UpdateLibrariesTask : TrangaTask public class UpdateLibrariesTask : TrangaTask
{ {
public UpdateLibrariesTask(Task task, TimeSpan reoccurrence) : base(task, reoccurrence) public UpdateLibrariesTask(TimeSpan reoccurrence) : base(Task.UpdateLibraries, reoccurrence)
{ {
} }
protected override void ExecuteTask(TaskManager taskManager, Logger? logger) protected override HttpStatusCode ExecuteTask(TaskManager taskManager, Logger? logger, CancellationToken? cancellationToken = null)
{ {
if (cancellationToken?.IsCancellationRequested ?? false)
return HttpStatusCode.RequestTimeout;
foreach(LibraryManager lm in taskManager.settings.libraryManagers) foreach(LibraryManager lm in taskManager.settings.libraryManagers)
lm.UpdateLibrary(); lm.UpdateLibrary();
this.progress = 1f; return HttpStatusCode.OK;
}
public override TrangaTask Clone()
{
return new UpdateLibrariesTask(this.reoccurrence);
}
protected override double GetProgress()
{
return 1;
} }
} }

View File

@ -43,54 +43,76 @@ function DeleteData(uri){
} }
async function GetAvailableControllers(){ async function GetAvailableControllers(){
var uri = apiUri + "/Tranga/GetAvailableControllers"; var uri = apiUri + "/Connectors";
let json = await GetData(uri); let json = await GetData(uri);
return json; return json;
} }
async function GetPublication(connectorName, title){ async function GetPublicationFromConnector(connectorName, title){
var uri = apiUri + `/Tranga/GetPublicationsFromConnector?connectorName=${connectorName}&title=${title}`; var uri = apiUri + `/Publications/FromConnector?connectorName=${connectorName}&title=${title}`;
let json = await GetData(uri); let json = await GetData(uri);
return json; return json;
} }
async function GetKnownPublications(){ async function GetKnownPublications(){
var uri = apiUri + "/Tranga/GetKnownPublications"; var uri = apiUri + "/Publications/Known";
let json = await GetData(uri);
return json;
}
async function GetPublication(internalId){
var uri = apiUri + `/Publications/Known?internalId=${internalId}`;
let json = await GetData(uri);
return json;
}
async function GetChapters(internalId, connectorName, onlyNew, language){
var uri = apiUri + `/Publications/Chapters?internalId=${internalId}&connectorName=${connectorName}&onlyNew=${onlyNew}&language=${language}`;
let json = await GetData(uri); let json = await GetData(uri);
return json; return json;
} }
async function GetTaskTypes(){ async function GetTaskTypes(){
var uri = apiUri + "/Tasks/GetTaskTypes"; var uri = apiUri + "/Tasks/Types";
let json = await GetData(uri); let json = await GetData(uri);
return json; return json;
} }
async function GetRunningTasks(){ async function GetRunningTasks(){
var uri = apiUri + "/Tasks/GetRunningTasks"; var uri = apiUri + "/Tasks/RunningTasks";
let json = await GetData(uri); let json = await GetData(uri);
return json; return json;
} }
async function GetDownloadTasks(){ async function GetDownloadTasks(){
var uri = apiUri + "/Tasks/Get?taskType=DownloadNewChapters"; var uri = apiUri + "/Tasks?taskType=DownloadNewChapters";
let json = await GetData(uri); let json = await GetData(uri);
return json; return json;
} }
async function GetSettings(){ async function GetSettings(){
var uri = apiUri + "/Settings/Get"; var uri = apiUri + "/Settings";
let json = await GetData(uri); let json = await GetData(uri);
return json; return json;
} }
async function GetKomgaTask(){ async function GetKomgaTask(){
var uri = apiUri + "/Tasks/Get?taskType=UpdateLibraries"; var uri = apiUri + "/Tasks?taskType=UpdateLibraries";
let json = await GetData(uri); let json = await GetData(uri);
return json; return json;
} }
function CreateTask(taskType, reoccurrence, connectorName, publicationId, language){ function CreateMonitorTask(connectorName, internalId, reoccurrence, language){
var uri = apiUri + `/Tasks/Create?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}&reoccurrenceTime=${reoccurrence}&language=${language}`; var uri = apiUri + `/Tasks/CreateMonitorTask?connectorName=${connectorName}&internalId=${internalId}&reoccurrenceTime=${reoccurrence}&language=${language}`;
PostData(uri);
}
function CreateUpdateLibraryTask(reoccurrence){
var uri = apiUri + `/Tasks/CreateUpdateLibraryTask?reoccurrenceTime=${reoccurrence}`;
PostData(uri);
}
function CreateDownloadChaptersTask(connectorName, internalId, chapters, language){
var uri = apiUri + `/Tasks/CreateDownloadChaptersTask?connectorName=${connectorName}&internalId=${internalId}&chapters=${chapters}&language=${language}`;
PostData(uri); PostData(uri);
} }
@ -104,22 +126,38 @@ function EnqueueTask(taskType, connectorName, publicationId){
PostData(uri); PostData(uri);
} }
function UpdateSettings(downloadLocation, komgaUrl, komgaAuth, kavitaUrl, kavitaUser, kavitaPass){ function UpdateDownloadLocation(downloadLocation){
var uri = apiUri + "/Settings/Update?" var uri = apiUri + "/Settings/Update?"
if(downloadLocation != ""){ uri += "&downloadLocation="+downloadLocation;
uri += "&downloadLocation="+downloadLocation; PostData(uri);
} }
if(komgaUrl != "" && komgaAuth != ""){
uri += `&komgaUrl=${komgaUrl}&komgaAuth=${komgaAuth}`; function UpdateKomga(komgaUrl, komgaAuth){
} var uri = apiUri + "/Settings/Update?"
if(kavitaUrl != "" && kavitaUser != "" && kavitaPass != ""){ uri += `&komgaUrl=${komgaUrl}&komgaAuth=${komgaAuth}`;
uri += `&kavitaUrl=${kavitaUrl}&kavitaUsername=${kavitaUser}&kavitaPassword=${kavitaPass}`; PostData(uri);
} }
function UpdateKavita(kavitaUrl, kavitaUser, kavitaPass){
var uri = apiUri + "/Settings/Update?"
uri += `&kavitaUrl=${kavitaUrl}&kavitaUsername=${kavitaUser}&kavitaPassword=${kavitaPass}`;
PostData(uri);
}
function UpdateGotify(gotifyUrl, gotifyAppToken){
var uri = apiUri + "/Settings/Update?"
uri += `&gotifyUrl=${gotifyUrl}&gotifyAppToken=${gotifyAppToken}`;
PostData(uri);
}
function UpdateLunaSea(lunaseaWebhook){
var uri = apiUri + "/Settings/Update?"
uri += `&lunaseaWebhook=${lunaseaWebhook}`;
PostData(uri); PostData(uri);
} }
function DeleteTask(taskType, connectorName, publicationId){ function DeleteTask(taskType, connectorName, publicationId){
var uri = apiUri + `/Tasks/Delete?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}`; var uri = apiUri + `/Tasks?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}`;
DeleteData(uri); DeleteData(uri);
} }
@ -129,7 +167,7 @@ function DequeueTask(taskType, connectorName, publicationId){
} }
async function GetQueue(){ async function GetQueue(){
var uri = apiUri + "/Queue/GetList"; var uri = apiUri + "/Queue/List";
let json = await GetData(uri); let json = await GetData(uri);
return json; return json;
} }

View File

@ -25,36 +25,64 @@
<p>+</p> <p>+</p>
</div> </div>
<publication> <publication>
<img src="media/cover.jpg"> <img alt="cover" src="media/cover.jpg">
<publication-information> <publication-information>
<connector-name class="pill">MangaDex</connector-name> <connector-name class="pill">MangaDex</connector-name>
<publication-name>Tensei Pandemic</publication-name> <publication-name>Tensei Pandemic</publication-name>
</publication-information> </publication-information>
</publication> </publication>
</content> </content>
<popup id="addTaskPopup"> <popup id="selectPublicationPopup">
<blur-background id="blurBackgroundTaskPopup"></blur-background> <blur-background id="blurBackgroundTaskPopup"></blur-background>
<addtask-window> <popup-window>
<window-titlebar> <popup-title>Select Publication</popup-title>
<p>Add Task</p> <popup-content>
<img id="closePopupImg" src="media/close-x.svg" alt="Close"> <div>
</window-titlebar> <label for="connectors">Connector</label>
<window-content> <select id="connectors">
<addtask-settings> <option value=""></option>
<addtask-setting><label for="selectReccurrence">Recurrence</label><input id="selectReccurrence" type="time" value="01:00:00" step="3600"></addtask-setting> </select>
<addtask-setting><label for="connectors">Connector</label> </div>
<select id="connectors"> <div>
<option value=""></option> <label for="searchPublicationQuery">Search Title</label><input id="searchPublicationQuery" type="text"></addtask-setting>
</select> </div>
</addtask-setting> <input type="submit" value="Search" style="font-weight: bolder" onclick="NewSearch();">
<addtask-setting><label for="searchPublicationQuery">Search Title</label><input id="searchPublicationQuery" type="text"></addtask-setting> </popup-content>
<input type="submit" value="Search" onclick="NewSearch();"> <div id="taskSelectOutput"></div>
</addtask-settings> </popup-window>
<div id="taskSelectOutput"></div>
</window-content>
</addtask-window>
</popup> </popup>
<popup id="createMonitorTaskPopup">
<blur-background id="blurBackgroundCreateMonitorTaskPopup"></blur-background>
<popup-window>
<popup-title>Create Task: Monitor Publication</popup-title>
<popup-content>
<div>
<span>Run every</span>
<label for="hours"></label><input id="hours" type="number" value="3" min="0" max="23"><span>hours</span>
<label for="minutes"></label><input id="minutes" type="number" value="0" min="0" max="59"><span>minutes</span>
<input type="submit" value="Create" onclick="AddMonitorTask()">
</div>
</popup-content>
</popup-window>
</popup>
<popup id="createDownloadChaptersTask">
<blur-background id="blurBackgroundCreateDownloadChaptersTask"></blur-background>
<popup-window>
<popup-title>Create Task: Download Chapter(s)</popup-title>
<popup-content>
<div>
<label for="selectedChapters">Chapters:</label><input id="selectedChapters" placeholder="Select"><input type="submit" value="Select" onclick="DownloadChapterTaskClick()">
</div>
<div id="chapterOutput">
</div>
</popup-content>
</popup-window>
</popup>
<popup id="publicationViewerPopup"> <popup id="publicationViewerPopup">
<blur-background id="blurBackgroundPublicationPopup"></blur-background> <blur-background id="blurBackgroundPublicationPopup"></blur-background>
<publication-viewer> <publication-viewer>
@ -71,62 +99,79 @@
<publication-interactions> <publication-interactions>
<publication-starttask>Start Task ▶️</publication-starttask> <publication-starttask>Start Task ▶️</publication-starttask>
<publication-delete>Delete Task ❌</publication-delete> <publication-delete>Delete Task ❌</publication-delete>
<publication-add>Add Task </publication-add> <publication-add id="createMonitorTaskButton">Monitor </publication-add>
<publication-add id="createDownloadChapterTaskButton">Download Chapter </publication-add>
</publication-interactions> </publication-interactions>
</publication-information> </publication-information>
</publication-viewer> </publication-viewer>
</popup> </popup>
<popup id="settingsPopup"> <popup id="settingsPopup">
<blur-background id="blurBackgroundSettingsPopup"></blur-background> <blur-background id="blurBackgroundSettingsPopup"></blur-background>
<settings> <popup-window>
<span style="font-weight: bold; text-align: center; font-size: 16pt;">Settings</span> <popup-title>Settings</popup-title>
<div> <popup-content>
<p class="title">Download Location:</p> <div>
<span id="downloadLocation"></span> <p class="title">Download Location:</p>
</div> <span id="downloadLocation"></span>
<div> </div>
<p class="title">API-URI</p> <div>
<label for="settingApiUri"></label><input placeholder="https://" type="text" id="settingApiUri"> <p class="title">API-URI</p>
</div> <label for="settingApiUri"></label><input placeholder="https://" type="text" id="settingApiUri">
<komga-settings> </div>
<span class="title">Komga</span> <div>
<div>Configured: <span id="komgaConfigured">✅❌</span></div> <span class="title">Komga</span>
<label for="komgaUrl"></label><input placeholder="URL" id="komgaUrl" type="text"> <div>Configured: <span id="komgaConfigured">✅❌</span></div>
<label for="komgaUsername"></label><input placeholder="Username" id="komgaUsername" type="text"> <label for="komgaUrl"></label><input placeholder="URL" id="komgaUrl" type="text">
<label for="komgaPassword"></label><input placeholder="Password" id="komgaPassword" type="password"> <label for="komgaUsername"></label><input placeholder="Username" id="komgaUsername" type="text">
</komga-settings> <label for="komgaPassword"></label><input placeholder="Password" id="komgaPassword" type="password">
<kavita-settings> </div>
<span class="title">Kavita</span> <div>
<div>Configured: <span id="kavitaConfigured">✅❌</span></div> <span class="title">Kavita</span>
<label for="kavitaUrl"></label><input placeholder="URL" id="kavitaUrl" type="text"> <div>Configured: <span id="kavitaConfigured">✅❌</span></div>
<label for="kavitaUsername"></label><input placeholder="Username" id="kavitaUsername" type="text"> <label for="kavitaUrl"></label><input placeholder="URL" id="kavitaUrl" type="text">
<label for="kavitaPassword"></label><input placeholder="Password" id="kavitaPassword" type="password"> <label for="kavitaUsername"></label><input placeholder="Username" id="kavitaUsername" type="text">
</kavita-settings> <label for="kavitaPassword"></label><input placeholder="Password" id="kavitaPassword" type="password">
<div> </div>
<label for="libraryUpdateTime" style="margin-right: 5px;">Update Time</label><input id="libraryUpdateTime" type="time" value="00:01:00" step="10"> <div>
<input type="submit" value="Update" onclick="UpdateLibrarySettings()"> <span class="title">Gotify</span>
</div> <div>Configured: <span id="gotifyConfigured">✅❌</span></div>
</settings> <label for="gotifyUrl"></label><input placeholder="URL" id="gotifyUrl" type="text">
<label for="gotifyAppToken"></label><input placeholder="App-Token" id="gotifyAppToken" type="text">
</div>
<div>
<span class="title">LunaSea</span>
<div>Configured: <span id="lunaseaConfigured">✅❌</span></div>
<label for="lunaseaWebhook"></label><input placeholder="Webhook-Url" id="lunaseaWebhook" type="text">
</div>
<div>
<label for="libraryUpdateTime" style="margin-right: 5px;">Update Time</label><input id="libraryUpdateTime" type="time" value="00:01:00" step="10">
<input type="submit" value="Update" onclick="UpdateLibrarySettings()">
</div>
</popup-content>
</popup-window>
</popup>
<popup id="downloadTasksPopup">
<blur-background id="blurBackgroundTasksQueuePopup"></blur-background>
<popup-window>
<popup-title>Task Progress</popup-title>
<popup-content>
</popup-content>
</popup-window>
</popup> </popup>
</viewport> </viewport>
<footer> <footer>
<div> <div onclick="ShowTasksQueue();">
<img src="media/running.svg" alt="running"><div id="tasksRunningTag">0</div> <img src="media/running.svg" alt="running"><div id="tasksRunningTag">0</div>
</div> </div>
<div> <div onclick="ShowTasksQueue();">
<img src="media/queue.svg" alt="queue"><div id="tasksQueuedTag">0</div> <img src="media/queue.svg" alt="queue"><div id="tasksQueuedTag">0</div>
</div> </div>
<div>
<img src="media/tasks.svg" alt="queue"><div id="totalTasksTag">0</div>
</div>
<p id="madeWith">Made with Blåhaj 🦈</p> <p id="madeWith">Made with Blåhaj 🦈</p>
</footer> </footer>
</wrapper> </wrapper>
<footer-tag-popup>
<footer-tag-content>
<footer-tag-task-name>Test</footer-tag-task-name>
</footer-tag-content>
</footer-tag-popup>
<script src="apiConnector.js"></script> <script src="apiConnector.js"></script>
<script src="interaction.js"></script> <script src="interaction.js"></script>

View File

@ -10,7 +10,13 @@ const settingsPopup = document.querySelector("#settingsPopup");
const settingsCog = document.querySelector("#settingscog"); const settingsCog = document.querySelector("#settingscog");
const selectRecurrence = document.querySelector("#selectReccurrence"); const selectRecurrence = document.querySelector("#selectReccurrence");
const tasksContent = document.querySelector("content"); const tasksContent = document.querySelector("content");
const addTaskPopup = document.querySelector("#addTaskPopup"); const selectPublicationPopup = document.querySelector("#selectPublicationPopup");
const createMonitorTaskButton = document.querySelector("#createMonitorTaskButton");
const createDownloadChapterTaskButton = document.querySelector("#createDownloadChapterTaskButton");
const createMonitorTaskPopup = document.querySelector("#createMonitorTaskPopup");
const createDownloadChaptersTask = document.querySelector("#createDownloadChaptersTask");
const chapterOutput = document.querySelector("#chapterOutput");
const selectedChapters = document.querySelector("#selectedChapters");
const publicationViewerPopup = document.querySelector("#publicationViewerPopup"); const publicationViewerPopup = document.querySelector("#publicationViewerPopup");
const publicationViewerWindow = document.querySelector("publication-viewer"); const publicationViewerWindow = document.querySelector("publication-viewer");
const publicationViewerDescription = document.querySelector("#publicationViewerDescription"); const publicationViewerDescription = document.querySelector("#publicationViewerDescription");
@ -19,9 +25,7 @@ const publicationViewerTags = document.querySelector("#publicationViewerTags");
const publicationViewerAuthor = document.querySelector("#publicationViewerAuthor"); const publicationViewerAuthor = document.querySelector("#publicationViewerAuthor");
const pubviewcover = document.querySelector("#pubviewcover"); const pubviewcover = document.querySelector("#pubviewcover");
const publicationDelete = document.querySelector("publication-delete"); const publicationDelete = document.querySelector("publication-delete");
const publicationAdd = document.querySelector("publication-add");
const publicationTaskStart = document.querySelector("publication-starttask"); const publicationTaskStart = document.querySelector("publication-starttask");
const closetaskpopup = document.querySelector("#closePopupImg");
const settingDownloadLocation = document.querySelector("#downloadLocation"); const settingDownloadLocation = document.querySelector("#downloadLocation");
const settingKomgaUrl = document.querySelector("#komgaUrl"); const settingKomgaUrl = document.querySelector("#komgaUrl");
const settingKomgaUser = document.querySelector("#komgaUsername"); const settingKomgaUser = document.querySelector("#komgaUsername");
@ -29,24 +33,42 @@ const settingKomgaPass = document.querySelector("#komgaPassword");
const settingKavitaUrl = document.querySelector("#kavitaUrl"); const settingKavitaUrl = document.querySelector("#kavitaUrl");
const settingKavitaUser = document.querySelector("#kavitaUsername"); const settingKavitaUser = document.querySelector("#kavitaUsername");
const settingKavitaPass = document.querySelector("#kavitaPassword"); const settingKavitaPass = document.querySelector("#kavitaPassword");
const settingGotifyUrl = document.querySelector("#gotifyUrl");
const settingGotifyAppToken = document.querySelector("#gotifyAppToken");
const settingLunaseaWebhook = document.querySelector("#lunaseaWebhook");
const libraryUpdateTime = document.querySelector("#libraryUpdateTime"); const libraryUpdateTime = document.querySelector("#libraryUpdateTime");
const settingKomgaConfigured = document.querySelector("#komgaConfigured"); const settingKomgaConfigured = document.querySelector("#komgaConfigured");
const settingKavitaConfigured = document.querySelector("#kavitaConfigured"); const settingKavitaConfigured = document.querySelector("#kavitaConfigured");
const settingGotifyConfigured = document.querySelector("#gotifyConfigured");
const settingLunaseaConfigured = document.querySelector("#lunaseaConfigured");
const settingApiUri = document.querySelector("#settingApiUri"); const settingApiUri = document.querySelector("#settingApiUri");
const tagTasksRunning = document.querySelector("#tasksRunningTag"); const tagTasksRunning = document.querySelector("#tasksRunningTag");
const tagTasksQueued = document.querySelector("#tasksQueuedTag"); const tagTasksQueued = document.querySelector("#tasksQueuedTag");
const tagTasksTotal = document.querySelector("#totalTasksTag"); const downloadTasksPopup = document.querySelector("#downloadTasksPopup");
const tagTaskPopup = document.querySelector("footer-tag-popup"); const downloadTasksOutput = downloadTasksPopup.querySelector("popup-content");
const tagTasksPopupContent = document.querySelector("footer-tag-content");
searchbox.addEventListener("keyup", (event) => FilterResults()); searchbox.addEventListener("keyup", (event) => FilterResults());
settingsCog.addEventListener("click", () => OpenSettings()); settingsCog.addEventListener("click", () => OpenSettings());
document.querySelector("#blurBackgroundSettingsPopup").addEventListener("click", () => HideSettings()); document.querySelector("#blurBackgroundSettingsPopup").addEventListener("click", () => settingsPopup.style.display = "none");
closetaskpopup.addEventListener("click", () => HideAddTaskPopup()); document.querySelector("#blurBackgroundTaskPopup").addEventListener("click", () => selectPublicationPopup.style.display = "none");
document.querySelector("#blurBackgroundTaskPopup").addEventListener("click", () => HideAddTaskPopup());
document.querySelector("#blurBackgroundPublicationPopup").addEventListener("click", () => HidePublicationPopup()); document.querySelector("#blurBackgroundPublicationPopup").addEventListener("click", () => HidePublicationPopup());
document.querySelector("#blurBackgroundCreateMonitorTaskPopup").addEventListener("click", () => createMonitorTaskPopup.style.display = "none");
document.querySelector("#blurBackgroundCreateDownloadChaptersTask").addEventListener("click", () => createDownloadChaptersTask.style.display = "none");
document.querySelector("#blurBackgroundTasksQueuePopup").addEventListener("click", () => downloadTasksPopup.style.display = "none");
selectedChapters.addEventListener("keypress", (event) => {
if(event.key === "Enter"){
DownloadChapterTaskClick();
}
})
publicationDelete.addEventListener("click", () => DeleteTaskClick()); publicationDelete.addEventListener("click", () => DeleteTaskClick());
publicationAdd.addEventListener("click", () => AddTaskClick()); createMonitorTaskButton.addEventListener("click", () => {
HidePublicationPopup();
createMonitorTaskPopup.style.display = "block";
});
createDownloadChapterTaskButton.addEventListener("click", () => {
HidePublicationPopup();
OpenDownloadChapterTaskPopup();
});
publicationTaskStart.addEventListener("click", () => StartTaskClick()); publicationTaskStart.addEventListener("click", () => StartTaskClick());
settingApiUri.addEventListener("keypress", (event) => { settingApiUri.addEventListener("keypress", (event) => {
if(event.key === "Enter"){ if(event.key === "Enter"){
@ -60,12 +82,7 @@ searchPublicationQuery.addEventListener("keypress", (event) => {
NewSearch(); NewSearch();
} }
}); });
tagTasksRunning.addEventListener("mouseover", (event) => ShowRunningTasks(event));
tagTasksRunning.addEventListener("mouseout", () => CloseTasksPopup());
tagTasksQueued.addEventListener("mouseover", (event) => ShowQueuedTasks(event));
tagTasksQueued.addEventListener("mouseout", () => CloseTasksPopup());
tagTasksTotal.addEventListener("mouseover", (event) => ShowAllTasks(event));
tagTasksTotal.addEventListener("mouseout", () => CloseTasksPopup());
let availableConnectors; let availableConnectors;
GetAvailableControllers() GetAvailableControllers()
@ -82,18 +99,14 @@ GetAvailableControllers()
function NewSearch(){ function NewSearch(){
//Disable inputs //Disable inputs
selectRecurrence.disabled = true;
connectorSelect.disabled = true; connectorSelect.disabled = true;
searchPublicationQuery.disabled = true; searchPublicationQuery.disabled = true;
//Waitcursor //Waitcursor
document.body.style.cursor = "wait"; document.body.style.cursor = "wait";
selectRecurrence.style.cursor = "wait";
connectorSelect.style.cursor = "wait";
searchPublicationQuery.style.cursor = "wait";
//Empty previous results //Empty previous results
selectPublication.replaceChildren(); selectPublication.replaceChildren();
GetPublication(connectorSelect.value, searchPublicationQuery.value) GetPublicationFromConnector(connectorSelect.value, searchPublicationQuery.value)
.then(json => .then(json =>
json.forEach(publication => { json.forEach(publication => {
var option = CreatePublication(publication, connectorSelect.value); var option = CreatePublication(publication, connectorSelect.value);
@ -105,14 +118,10 @@ function NewSearch(){
)) ))
.then(() => { .then(() => {
//Re-enable inputs //Re-enable inputs
selectRecurrence.disabled = false;
connectorSelect.disabled = false; connectorSelect.disabled = false;
searchPublicationQuery.disabled = false; searchPublicationQuery.disabled = false;
//Cursor //Cursor
document.body.style.cursor = "initial"; document.body.style.cursor = "initial";
selectRecurrence.style.cursor = "initial";
connectorSelect.style.cursor = "initial";
searchPublicationQuery.style.cursor = "initial";
}); });
} }
@ -137,18 +146,59 @@ function CreatePublication(publication, connector){
return publicationElement; return publicationElement;
} }
function AddMonitorTask(){
var hours = document.querySelector("#hours").value;
var minutes = document.querySelector("#minutes").value;
CreateMonitorTask(connectorSelect.value, toEditId, `${hours}:${minutes}:00`, "en");
HidePublicationPopup();
createMonitorTaskPopup.style.display = "none";
selectPublicationPopup.style.display = "none";
}
function OpenDownloadChapterTaskPopup(){
selectedChapters.value = "";
chapterOutput.replaceChildren();
createDownloadChaptersTask.style.display = "block";
GetChapters(toEditId, connectorSelect.value, true, "en").then((json) => {
var i = 0;
json.forEach(chapter => {
var chapterDom = document.createElement("div");
var indexDom = document.createElement("span");
indexDom.className = "index";
indexDom.innerText = i++;
chapterDom.appendChild(indexDom);
var volDom = document.createElement("span");
volDom.className = "vol";
volDom.innerText = chapter.volumeNumber;
chapterDom.appendChild(volDom);
var chDom = document.createElement("span");
chDom.className = "ch";
chDom.innerText = chapter.chapterNumber;
chapterDom.appendChild(chDom);
var titleDom = document.createElement("span");
titleDom.innerText = chapter.name;
chapterDom.appendChild(titleDom);
chapterOutput.appendChild(chapterDom);
});
});
}
function DownloadChapterTaskClick(){
CreateDownloadChaptersTask(connectorSelect.value, toEditId, selectedChapters.value, "en");
HidePublicationPopup();
createDownloadChaptersTask.style.display = "none";
selectPublicationPopup.style.display = "none";
}
function DeleteTaskClick(){ function DeleteTaskClick(){
taskToDelete = tasks.filter(tTask => tTask.publication.internalId === toEditId)[0]; taskToDelete = tasks.filter(tTask => tTask.publication.internalId === toEditId)[0];
DeleteTask("DownloadNewChapters", taskToDelete.connectorName, toEditId); DeleteTask("DownloadNewChapters", taskToDelete.connectorName, toEditId);
HidePublicationPopup(); HidePublicationPopup();
} }
function AddTaskClick(){
CreateTask("DownloadNewChapters", selectRecurrence.value, connectorSelect.value, toEditId, "en")
HideAddTaskPopup();
HidePublicationPopup();
}
function StartTaskClick(){ function StartTaskClick(){
var toEditTask = tasks.filter(task => task.publication.internalId == toEditId)[0]; var toEditTask = tasks.filter(task => task.publication.internalId == toEditId)[0];
StartTask("DownloadNewChapters", toEditTask.connectorName, toEditId); StartTask("DownloadNewChapters", toEditTask.connectorName, toEditId);
@ -188,18 +238,20 @@ function ShowPublicationViewerWindow(publicationId, event, add){
publicationViewerName.innerText = publication.sortName; publicationViewerName.innerText = publication.sortName;
publicationViewerTags.innerText = publication.tags.join(", "); publicationViewerTags.innerText = publication.tags.join(", ");
publicationViewerDescription.innerText = publication.description; publicationViewerDescription.innerText = publication.description;
publicationViewerAuthor.innerText = publication.author; publicationViewerAuthor.innerText = publication.authors.join(',');
pubviewcover.src = `imageCache/${publication.coverFileNameInCache}`; pubviewcover.src = `imageCache/${publication.coverFileNameInCache}`;
toEditId = publicationId; toEditId = publicationId;
//Check what action should be listed //Check what action should be listed
if(add){ if(add){
publicationAdd.style.display = "initial"; createMonitorTaskButton.style.display = "initial";
createDownloadChapterTaskButton.style.display = "initial";
publicationDelete.style.display = "none"; publicationDelete.style.display = "none";
publicationTaskStart.style.display = "none"; publicationTaskStart.style.display = "none";
} }
else{ else{
publicationAdd.style.display = "none"; createMonitorTaskButton.style.display = "none";
createDownloadChapterTaskButton.style.display = "none";
publicationDelete.style.display = "initial"; publicationDelete.style.display = "initial";
publicationTaskStart.style.display = "initial"; publicationTaskStart.style.display = "initial";
} }
@ -211,10 +263,8 @@ function HidePublicationPopup(){
function ShowNewTaskWindow(){ function ShowNewTaskWindow(){
selectPublication.replaceChildren(); selectPublication.replaceChildren();
addTaskPopup.style.display = "block"; searchPublicationQuery.value = "";
} selectPublicationPopup.style.display = "flex";
function HideAddTaskPopup(){
addTaskPopup.style.display = "none";
} }
@ -234,20 +284,21 @@ function OpenSettings(){
settingsPopup.style.display = "flex"; settingsPopup.style.display = "flex";
} }
function HideSettings(){
settingsPopup.style.display = "none";
}
function GetSettingsClick(){ function GetSettingsClick(){
settingApiUri.value = ""; settingApiUri.value = "";
settingKomgaUrl.value = ""; settingKomgaUrl.value = "";
settingKomgaUser.value = ""; settingKomgaUser.value = "";
settingKomgaPass.value = ""; settingKomgaPass.value = "";
settingKomgaConfigured.innerText = "❌";
settingKavitaUrl.value = ""; settingKavitaUrl.value = "";
settingKavitaUser.value = ""; settingKavitaUser.value = "";
settingKavitaPass.value = ""; settingKavitaPass.value = "";
settingKomgaConfigured.innerText = "❌";
settingKavitaConfigured.innerText = "❌"; settingKavitaConfigured.innerText = "❌";
settingGotifyUrl.value = "";
settingGotifyAppToken.value = "";
settingGotifyConfigured.innerText = "❌";
settingLunaseaWebhook.value = "";
settingLunaseaConfigured.innerText = "❌";
settingApiUri.placeholder = apiUri; settingApiUri.placeholder = apiUri;
@ -266,6 +317,13 @@ function GetSettingsClick(){
settingKavitaConfigured.innerText = "✅"; settingKavitaConfigured.innerText = "✅";
} }
}); });
json.notificationManagers.forEach(nm => {
if(nm.notificationManagerType == 0){
settingGotifyConfigured.innerText = "✅";
} else if(nm.notificationManagerType == 1){
settingLunaseaConfigured.innerText = "✅";
}
});
}); });
GetKomgaTask().then(json => { GetKomgaTask().then(json => {
@ -275,20 +333,26 @@ function GetSettingsClick(){
} }
function UpdateLibrarySettings(){ function UpdateLibrarySettings(){
if(settingKomgaUser.value != "" && settingKomgaPass != ""){ if(settingKomgaUrl.value != "" && settingKomgaUser.value != "" && settingKomgaPass.value != ""){
var auth = utf8_to_b64(`${settingKomgaUser.value}:${settingKomgaPass.value}`); var auth = utf8_to_b64(`${settingKomgaUser.value}:${settingKomgaPass.value}`);
console.log(auth); console.log(auth);
UpdateKomga(settingKomgaUrl.value, auth);
if(settingKomgaUrl.value != "") CreateUpdateLibraryTask(libraryUpdateTime.value);
UpdateSettings("", settingKomgaUrl.value, auth, "", "");
else
UpdateSettings("", settingKomgaUrl.placeholder, auth, "", "");
} }
if(settingKavitaUrl.value != "" && settingKavitaUser.value != "" && settingKavitaPass.value != ""){ if(settingKavitaUrl.value != "" && settingKavitaUser.value != "" && settingKavitaPass.value != ""){
UpdateSettings("", "", "", settingKavitaUrl.value, settingKavitaUser.value, settingKavitaPass.value); UpdateKavita(settingKavitaUrl.value, settingKavitaUser.value, settingKavitaPass.value);
CreateUpdateLibraryTask(libraryUpdateTime.value);
} }
CreateTask("UpdateLibraries", libraryUpdateTime.value, "","","");
if(settingGotifyUrl.value != "" && settingGotifyAppToken.value != ""){
UpdateGotify(settingGotifyUrl.value, settingGotifyAppToken.value);
}
if(settingLunaseaWebhook.value != ""){
UpdateLunaSea(settingLunaseaWebhook.value);
}
setTimeout(() => GetSettingsClick(), 200); setTimeout(() => GetSettingsClick(), 200);
} }
@ -296,66 +360,6 @@ function utf8_to_b64( str ) {
return window.btoa(unescape(encodeURIComponent( str ))); return window.btoa(unescape(encodeURIComponent( str )));
} }
function ShowRunningTasks(event){
GetRunningTasks()
.then(json => {
tagTasksPopupContent.replaceChildren();
json.forEach(task => {
if(task.publication != null){
var taskname = document.createElement("footer-tag-task-name");
if(task.task == 2)
taskname.innerText = `${task.publication.sortName} - ${task.progress.toLocaleString(undefined,{style: 'percent', minimumFractionDigits:2})}`;
else if(task.task == 4)
taskname.innerText = `${task.publication.sortName} Vol.${task.chapter.volumeNumber} Ch.${task.chapter.chapterNumber} - ${task.progress.toLocaleString(undefined,{style: 'percent', minimumFractionDigits:2})}`;
tagTasksPopupContent.appendChild(taskname);
}
});
if(tagTasksPopupContent.children.length > 0){
tagTaskPopup.style.display = "block";
tagTaskPopup.style.left = `${tagTasksRunning.offsetLeft - 20}px`;
}
});
}
function ShowQueuedTasks(event){
GetQueue()
.then(json => {
tagTasksPopupContent.replaceChildren();
json.forEach(task => {
var taskname = document.createElement("footer-tag-task-name");
if(task.task == 2)
taskname.innerText = `${task.publication.sortName}`;
else if(task.task == 4)
taskname.innerText = `${task.publication.sortName} Vol.${task.chapter.volumeNumber} Ch.${task.chapter.chapterNumber}`;
tagTasksPopupContent.appendChild(taskname);
});
if(json.length > 0){
tagTaskPopup.style.display = "block";
tagTaskPopup.style.left = `${tagTasksQueued.offsetLeft- 20}px`;
}
});
}
function ShowAllTasks(event){
GetDownloadTasks()
.then(json => {
tagTasksPopupContent.replaceChildren();
json.forEach(task => {
var taskname = document.createElement("footer-tag-task-name");
taskname.innerText = task.publication.sortName;
tagTasksPopupContent.appendChild(taskname);
});
if(json.length > 0){
tagTaskPopup.style.display = "block";
tagTaskPopup.style.left = `${tagTasksTotal.offsetLeft - 20}px`;
}
});
}
function CloseTasksPopup(){
tagTaskPopup.style.display = "none";
}
function FilterResults(){ function FilterResults(){
if(searchBox.value.length > 0){ if(searchBox.value.length > 0){
tasksContent.childNodes.forEach(publication => { tasksContent.childNodes.forEach(publication => {
@ -376,12 +380,77 @@ function FilterResults(){
}else{ }else{
tasksContent.childNodes.forEach(publication => publication.style.display = "initial"); tasksContent.childNodes.forEach(publication => publication.style.display = "initial");
} }
}
function ShowTasksQueue(){
downloadTasksOutput.replaceChildren();
GetRunningTasks()
.then(json => {
tagTasksRunning.innerText = json.length;
json.forEach(task => {
if(task.task == 2 || task.task == 4) {
downloadTasksOutput.appendChild(CreateProgressChild(task));
document.querySelector(`#progress${GetValidSelector(task.taskId)}`).value = task.progress;
var finishedHours = task.executionApproximatelyRemaining.split(':')[0];
var finishedMinutes = task.executionApproximatelyRemaining.split(':')[1];
var finishedSeconds = task.executionApproximatelyRemaining.split(':')[2].split('.')[0];
document.querySelector(`#progressStr${GetValidSelector(task.taskId)}`).innerText = `${finishedHours}:${finishedMinutes}:${finishedSeconds}`;
}
});
});
GetQueue()
.then(json => {
tagTasksQueued.innerText = json.length;
json.forEach(task => {
downloadTasksOutput.appendChild(CreateProgressChild(task));
});
});
downloadTasksPopup.style.display = "flex";
}
function CreateProgressChild(task){
var child = document.createElement("div");
var img = document.createElement('img');
img.src = `imageCache/${task.publication.coverFileNameInCache}`;
child.appendChild(img);
var name = document.createElement("span");
name.innerText = task.publication.sortName;
name.className = "pubTitle";
child.appendChild(name);
var progress = document.createElement("progress");
progress.id = `progress${GetValidSelector(task.taskId)}`;
child.appendChild(progress);
var progressStr = document.createElement("span");
progressStr.innerText = " \t∞";
progressStr.className = "progressStr";
progressStr.id = `progressStr${GetValidSelector(task.taskId)}`;
child.appendChild(progressStr);
if(task.chapter != undefined){
var chapterNumber = document.createElement("span");
chapterNumber.className = "chapterNumber";
chapterNumber.innerText = `Vol.${task.chapter.volumeNumber} Ch.${task.chapter.chapterNumber}`;
child.appendChild(chapterNumber);
var chapterName = document.createElement("span");
chapterName.className = "chapterName";
chapterName.innerText = task.chapter.name;
child.appendChild(chapterName);
}
return child;
} }
//Resets the tasks shown //Resets the tasks shown
ResetContent(); ResetContent();
downloadTasksOutput.replaceChildren();
//Get Tasks and show them //Get Tasks and show them
GetDownloadTasks() GetDownloadTasks()
.then(json => json.forEach(task => { .then(json => json.forEach(task => {
@ -394,16 +463,17 @@ GetDownloadTasks()
GetRunningTasks() GetRunningTasks()
.then(json => { .then(json => {
tagTasksRunning.innerText = json.length; tagTasksRunning.innerText = json.length;
}); json.forEach(task => {
downloadTasksOutput.appendChild(CreateProgressChild(task));
GetDownloadTasks() });
.then(json => {
tagTasksTotal.innerText = json.length;
}); });
GetQueue() GetQueue()
.then(json => { .then(json => {
tagTasksQueued.innerText = json.length; tagTasksQueued.innerText = json.length;
json.forEach(task => {
downloadTasksOutput.appendChild(CreateProgressChild(task));
});
}) })
setInterval(() => { setInterval(() => {
@ -432,15 +502,28 @@ setInterval(() => {
.then(json => { .then(json => {
tagTasksRunning.innerText = json.length; tagTasksRunning.innerText = json.length;
}); });
GetDownloadTasks()
.then(json => {
tagTasksTotal.innerText = json.length;
});
GetQueue() GetQueue()
.then(json => { .then(json => {
tagTasksQueued.innerText = json.length; tagTasksQueued.innerText = json.length;
}) });
}, 1000);
}, 1000);
setInterval(() => {
GetRunningTasks().then((json) => {
json.forEach(task => {
if(task.task == 2 || task.task == 4){
document.querySelector(`#progress${GetValidSelector(task.taskId)}`).value = task.progress;
var finishedHours = task.executionApproximatelyRemaining.split(':')[0];
var finishedMinutes = task.executionApproximatelyRemaining.split(':')[1];
var finishedSeconds = task.executionApproximatelyRemaining.split(':')[2].split('.')[0];
document.querySelector(`#progressStr${GetValidSelector(task.taskId)}`).innerText = `${finishedHours}:${finishedMinutes}:${finishedSeconds}`;
}
});
});
},500);
function GetValidSelector(str){
var clean = [...str.matchAll(/[a-zA-Z0-9]*-*_*/g)];
return clean.join('');
}

View File

@ -147,46 +147,22 @@ content {
align-content: start; align-content: start;
} }
settings {
width: 50%;
background-color: var(--accent-color);
display: flex;
flex-direction: column;
z-index: 10;
position: absolute;
left: 25%;
top: 100px;
border-radius: 5px;
padding: 10px 0;
}
#settingsPopup{ #settingsPopup{
z-index: 10; z-index: 10;
} }
settings > * { #settingsPopup popup-content{
margin: 0 20%;
}
settings input {
margin: 3px 0;
padding: 3px;
border-radius: 3px;
border: 1px solid rgba(0,0,0,0.2);
width: 100%;
}
settings .title {
font-weight: bolder;
font-size: 14pt;
margin: 15px 0 2px 0;
}
komga-settings {
margin-top: 20px;
display: flex;
flex-direction: column; flex-direction: column;
flex-wrap: nowrap; align-items: start;
margin: 15px 10px;
}
#settingsPopup popup-content > * {
margin: 5px 10px;
}
#settingsPopup popup-content .title {
font-weight: bolder;
} }
#addPublication { #addPublication {
@ -281,6 +257,186 @@ popup{
left: 0; left: 0;
position: fixed; position: fixed;
z-index: 2; z-index: 2;
flex-direction: column;
}
popup popup-window {
position: absolute;
z-index: 3;
left: 25%;
top: 100px;
width: 50%;
display: flex;
flex-direction: column;
background-color: var(--second-background-color);
border-radius: 3px;
overflow: hidden;
}
popup popup-window popup-title {
height: 30px;
font-size: 14pt;
font-weight: bolder;
padding: 5px 10px;
margin: 0;
display: flex;
align-items: center;
background-color: var(--primary-color);
color: var(--accent-color)
}
popup popup-window popup-content{
margin: 15px 10px;
display: flex;
align-items: center;
justify-content: space-evenly;
}
popup popup-window popup-content div > * {
margin: 2px 3px 0 0;
}
popup popup-window popup-content input, select {
padding: 3px 4px;
width: 130px;
border: 1px solid lightgrey;
background-color: var(--accent-color);
border-radius: 3px;
}
#selectPublicationPopup publication {
width: 150px;
height: 250px;
}
#createTaskPopup {
z-index: 7;
}
#createTaskPopup input {
height: 30px;
width: 200px;
}
#createMonitorTaskPopup, #createDownloadChaptersTask {
z-index: 9;
}
#createMonitorTaskPopup input[type="number"] {
width: 40px;
}
#createDownloadChaptersTask popup-content {
flex-direction: column;
align-items: start;
}
#createDownloadChaptersTask popup-content > * {
margin: 3px 0;
}
#createDownloadChaptersTask #chapterOutput {
max-height: 50vh;
overflow-y: scroll;
}
#createDownloadChaptersTask #chapterOutput .index{
display: inline-block;
width: 25px;
}
#createDownloadChaptersTask #chapterOutput .index::after{
content: ':';
}
#createDownloadChaptersTask #chapterOutput .vol::before{
content: 'Vol.';
}
#createDownloadChaptersTask #chapterOutput .vol{
display: inline-block;
width: 45px;
}
#createDownloadChaptersTask #chapterOutput .ch::before{
content: 'Ch.';
}
#createDownloadChaptersTask #chapterOutput .ch {
display: inline-block;
width: 60px;
}
#downloadTasksPopup popup-window {
left: 0;
top: 80px;
margin: 0 0 0 10px;
height: calc(100vh - 140px);
width: 400px;
max-width: 95vw;
overflow-y: scroll;
}
#downloadTasksPopup popup-content {
flex-direction: column;
align-items: start;
margin: 5px;
}
#downloadTasksPopup popup-content > div {
display: block;
height: 80px;
position: relative;
margin: 5px 0;
}
#downloadTasksPopup popup-content > div > img {
display: block;
position: absolute;
height: 100%;
width: 60px;
left: 0;
top: 0;
object-fit: cover;
border-radius: 4px;
}
#downloadTasksPopup popup-content > div > span {
display: block;
position: absolute;
width: max-content;
}
#downloadTasksPopup popup-content > div > .pubTitle {
left: 70px;
top: 0;
}
#downloadTasksPopup popup-content > div > .chapterName {
left: 70px;
top: 28pt;
}
#downloadTasksPopup popup-content > div > .chapterNumber {
left: 70px;
top: 14pt;
}
#downloadTasksPopup popup-content > div > progress {
display: block;
position: absolute;
left: 150px;
bottom: 0;
width: 200px;
}
#downloadTasksPopup popup-content > div > .progressStr {
display: block;
position: absolute;
left: 70px;
bottom: 0;
width: 70px;
} }
blur-background { blur-background {
@ -292,85 +448,14 @@ blur-background {
opacity: 0.5; opacity: 0.5;
} }
addtask-window {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
position: absolute;
left: 12.5%;
top: 15%;
width: 75%;
min-height: 70%;
max-height: 80%;
padding: 0;
background-color: var(--accent-color);
border-radius: 5px;
}
window-titlebar {
width: 100%;
height: 60px;
background-color: var(--primary-color);
border-radius: 5px 5px 0 0;
color: var(--accent-color);
display: flex block;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
window-titlebar p {
margin: 0 30px;
font-size: 14pt;
font-weight: bolder;
letter-spacing: 1px;
}
window-titlebar #closePopupImg {
height: 70%;
cursor: pointer;
margin-right: 20px;
filter: invert(100%) sepia(0%) saturate(100%) hue-rotate(115deg) brightness(116%) contrast(101%);
}
window-content {
display: flex;
flex-direction: column;
padding: 20px 5%;
overflow-x: scroll;
}
addtask-settings{
display: flex;
justify-content: center;
align-items: center;
}
addtask-settings select, addtask-settings input{
padding: 5px;
font-size: 10pt;
border: 1px solid rgba(0,0,0,0.2);
border-radius: 3px;
background-color: transparent;
margin: 10px 0;
width: 150px;
}
addtask-settings label {
font-weight: bolder;
margin: 0 5px;
}
addtask-settings addtask-setting{
margin: 0 15px;
}
#taskSelectOutput{ #taskSelectOutput{
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: start; justify-content: start;
align-content: start; align-content: start;
max-height: 70vh;
overflow-y: scroll;
} }
#publicationViewerPopup{ #publicationViewerPopup{
@ -512,7 +597,7 @@ footer-tag-popup::before{
border-left: 10px solid transparent; border-left: 10px solid transparent;
border-top: 10px solid var(--second-background-color); border-top: 10px solid var(--second-background-color);
border-bottom: 10px solid transparent; border-bottom: 10px solid transparent;
left: 0px; left: 0;
bottom: -17px; bottom: -17px;
border-radius: 0 0 0 5px; border-radius: 0 0 0 5px;
} }

View File

@ -7,7 +7,7 @@ services:
- ./tranga:/usr/share/Tranga-API #1 when replacing ./tranga replace #2 with same value - ./tranga:/usr/share/Tranga-API #1 when replacing ./tranga replace #2 with same value
- ./Manga:/Manga - ./Manga:/Manga
ports: ports:
- 6531:80 - "6531:6531"
restart: unless-stopped restart: unless-stopped
tranga-website: tranga-website:
image: glax/tranga-website:latest image: glax/tranga-website:latest
@ -15,7 +15,7 @@ services:
volumes: volumes:
- ./tranga/imageCache:/usr/share/nginx/html/imageCache:ro #2 when replacing Point to same value as #1/imageCache - ./tranga/imageCache:/usr/share/nginx/html/imageCache:ro #2 when replacing Point to same value as #1/imageCache
ports: ports:
- 9555:80 - "9555:80"
depends_on: depends_on:
- tranga-api - tranga-api
restart: unless-stopped restart: unless-stopped