Compare commits
138 Commits
Author | SHA1 | Date | |
---|---|---|---|
1cd819b21d | |||
27afedc1b4 | |||
fac0a3f7eb | |||
03ca480fe8 | |||
c2915468a5 | |||
8805c53cb8 | |||
adbbe3f6cc | |||
14b694d3be | |||
72ce75c6e0 | |||
8381951168 | |||
b24032d124 | |||
8bc23f7c69 | |||
48b7371a18 | |||
61ecefb615 | |||
8ff65bf400 | |||
932057cca0 | |||
67d06cd887 | |||
cbb012a659 | |||
e4f33bcca9 | |||
fbba7c45b9 | |||
d9b6062767 | |||
d477cd1ccd | |||
f892db7dda | |||
16c1b5c506 | |||
d5ecc1c37d | |||
1b9ebd096b | |||
8619630269 | |||
2bc92556e3 | |||
f1ab823e7f | |||
8261d02cc7 | |||
8d3b8be95c | |||
60519910de | |||
0940afe41f | |||
3dc376c19f | |||
3e56ef842b | |||
5a44e3b8b9 | |||
732c2f119c | |||
81638f4b4a | |||
c547aa6422 | |||
d80980512e | |||
f9f802155d | |||
eef0955009 | |||
ec25900ac0 | |||
e5fe14a09e | |||
5dc91095f8 | |||
985ac8fc7a | |||
c9537a9963 | |||
4fd3c03804 | |||
a1e9dd0232 | |||
aa1f9b1b56 | |||
6069578b6e | |||
a84b768e24 | |||
d1a21af15d | |||
7423ae6ace | |||
3aa7ba9d96 | |||
fdbb4570be | |||
b643a0c2a9 | |||
6fa6f897aa | |||
2bfab0298d | |||
147a20385b | |||
afa18d6a2c | |||
66980eef23 | |||
65f468a30a | |||
a91c33ee4f | |||
f39482fe4c | |||
41f47b4d6b | |||
be40091102 | |||
665092be6a | |||
653cb699d0 | |||
8dbc5446ad | |||
750df4ed52 | |||
4772ae0756 | |||
23f703d5a5 | |||
6aa0ea277b | |||
780df1cd6e | |||
0b7da2e9cb | |||
01a059d26b | |||
a8dbece237 | |||
5efa00e059 | |||
02075ed1b1 | |||
fabd16ccea | |||
79928075b0 | |||
9b8eb6a197 | |||
1d263ef45a | |||
e0877add30 | |||
046cad8072 | |||
b2ce55be96 | |||
a6e9013495 | |||
14c69631a6 | |||
ccc4e42a49 | |||
d6e75fda31 | |||
fc89537f63 | |||
fd3423d03c | |||
878f77766f | |||
08001fd684 | |||
e2917d2f2e | |||
32dc58715e | |||
add0583776 | |||
6fed0e5473 | |||
a0636ac7a2 | |||
7aeb78e2f6 | |||
5cf512f2b2 | |||
7d96b0901f | |||
68e80bc066 | |||
ad971fb065 | |||
86052472bc | |||
ec30bb40fa | |||
2fa96e9793 | |||
78e44b7704 | |||
8bf9df4419 | |||
4bd54f096d | |||
877daf0a1e | |||
6d0fcc13fb | |||
f0256494fd | |||
39fa905733 | |||
c557389967 | |||
201773af50 | |||
f85e02fb0a | |||
73d98b9c0f | |||
b0ee888c82 | |||
5c4431778e | |||
ccfa213b77 | |||
22d6389d38 | |||
f53dfb0822 | |||
a966bd788d | |||
dd651adc15 | |||
ba5ae67aa7 | |||
da4a5bed09 | |||
947b521163 | |||
5674adbd5e | |||
290819de09 | |||
0d0b68a8f9 | |||
87d2357b41 | |||
e3186aebb0 | |||
1cd37e2b1b | |||
9c267f395f | |||
e2b8888130 | |||
b6ac2682f6 |
2
.gitignore
vendored
@ -17,3 +17,5 @@ riderModule.iml
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
/.idea
|
||||
cover.jpg
|
||||
cover.png
|
13
Dockerfile
Normal 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 Tranga-API/Tranga-API.csproj
|
||||
RUN dotnet publish -c Release -o /publish
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:7.0 as runtime
|
||||
WORKDIR /publish
|
||||
COPY --from=build-env /publish .
|
||||
EXPOSE 80
|
||||
ENTRYPOINT ["dotnet", "/publish/Tranga-API.dll"]
|
35
README.md
@ -30,6 +30,9 @@
|
||||
<li><a href="#built-with">Built With</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#screenshots">Screenshots</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#getting-started">Getting Started</a>
|
||||
<ul>
|
||||
@ -51,6 +54,7 @@
|
||||
Tranga can download Chapters and Metadata from Scanlation sites such as
|
||||
|
||||
- [MangaDex.org](https://mangadex.org/)
|
||||
- [Manganato.com](https://manganato.com/)
|
||||
|
||||
and automatically start updates in [Komga](https://komga.org/) to import them.
|
||||
|
||||
@ -64,24 +68,43 @@ That is why I wanted to create my own project, in a language I understand, and t
|
||||
|
||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||
|
||||
|
||||
|
||||
### Built With
|
||||
|
||||
- .NET-Core
|
||||
- Newtonsoft.JSON
|
||||
- Love <3
|
||||
- Love <3 Blåhaj 🦈
|
||||
|
||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|  |  |
|
||||
|-----------------------------------:|:-------------------------------------------------:|
|
||||
|
||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||
|
||||
<!-- GETTING STARTED -->
|
||||
## Getting Started
|
||||
|
||||
To use head over to [releases](https://git.bernloehr.eu/glax/Tranga/releases) and download a release.
|
||||
There is two release types:
|
||||
|
||||
A CLI will guide you through setup.
|
||||
- CLI
|
||||
- Docker
|
||||
|
||||
### CLI
|
||||
|
||||
Head over to [releases](https://git.bernloehr.eu/glax/Tranga/releases) and download. The CLI will guide you through setup.
|
||||
|
||||
### Docker
|
||||
|
||||
Download [docker-compose.yaml](https://git.bernloehr.eu/glax/Tranga/src/branch/master/docker-compose.yaml) and configure to your needs.
|
||||
|
||||
Wherever you are mounting `/usr/share/Tranga-API` you also need to mount that same path + `/imageCache` in the webserver container.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
@ -90,7 +113,7 @@ A CLI will guide you through setup.
|
||||
<!-- ROADMAP -->
|
||||
## Roadmap
|
||||
|
||||
- [ ] Web-UI #1
|
||||
- [x] Web-UI #1
|
||||
- [ ] More Connectors
|
||||
- [ ] Manganato #2
|
||||
- [ ] ?
|
||||
|
@ -1,20 +0,0 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ["Tranga-API/Tranga-API.csproj", "Tranga-API/"]
|
||||
RUN dotnet restore "Tranga-API/Tranga-API.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Tranga-API"
|
||||
RUN dotnet build "Tranga-API.csproj" -c Release -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "Tranga-API.csproj" -c Release -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "Tranga-API.dll"]
|
@ -1,44 +1,66 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Logging;
|
||||
using Tranga;
|
||||
|
||||
string applicationFolderPath =
|
||||
Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Tranga-API");
|
||||
string logsFolderPath = Path.Join(applicationFolderPath, "logs");
|
||||
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(applicationFolderPath);
|
||||
Directory.CreateDirectory(logsFolderPath);
|
||||
Logger logger = new(new[] { Logger.LoggerType.FileLogger, Logger.LoggerType.ConsoleLogger }, Console.Out, Console.Out.Encoding, logFilePath);
|
||||
|
||||
Console.WriteLine($"Logfile-Path: {logFilePath}");
|
||||
Console.WriteLine($"Settings-File-Path: {settingsFilePath}");
|
||||
logger.WriteLine("Tranga", "Loading settings.");
|
||||
|
||||
Logger logger = new(new[] { Logger.LoggerType.FileLogger }, null, null, logFilePath);
|
||||
|
||||
logger.WriteLine("Tranga_CLI", "Loading Taskmanager.");
|
||||
TrangaSettings settings;
|
||||
if (File.Exists(settingsFilePath))
|
||||
settings = TrangaSettings.LoadSettings(settingsFilePath);
|
||||
else
|
||||
settings = new TrangaSettings(Directory.GetCurrentDirectory(), applicationFolderPath, null);
|
||||
settings = new TrangaSettings(downloadFolderPath, applicationFolderPath, null);
|
||||
|
||||
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.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
|
||||
app.MapGet("/GetAvailableControllers", () => taskManager.GetAvailableConnectors());
|
||||
app.UseCors(corsHeader);
|
||||
|
||||
app.MapGet("/GetKnownPublications", () => taskManager.GetAllPublications());
|
||||
app.MapGet("/Tranga/GetAvailableControllers", () => taskManager.GetAvailableConnectors().Keys.ToArray());
|
||||
|
||||
app.MapGet("/GetPublicationsFromConnector", (string connectorName, string title) =>
|
||||
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)
|
||||
@ -65,16 +87,47 @@ app.MapDelete("/Tasks/Delete", (string taskType, string? connectorName, string?
|
||||
taskManager.DeleteTask(task, connectorName, publication);
|
||||
});
|
||||
|
||||
app.MapGet("/Tasks/GetList", () => taskManager.GetAllTasks());
|
||||
app.MapGet("/Tasks/Get", (string taskType, string? connectorName, string? searchString) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
TrangaTask.Task task = Enum.Parse<TrangaTask.Task>(taskType);
|
||||
if (searchString is null || connectorName is null)
|
||||
return taskManager.GetAllTasks().Where(tTask => tTask.task == task);
|
||||
else
|
||||
return taskManager.GetAllTasks().Where(tTask =>
|
||||
tTask.task == task && tTask.connectorName == connectorName && tTask.ToString()
|
||||
.Contains(searchString, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return Array.Empty<TrangaTask>();
|
||||
}
|
||||
});
|
||||
|
||||
app.MapPost("/Tasks/Start", (string taskType, string? connectorName, string? publicationId) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType);
|
||||
TrangaTask? task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
|
||||
tTask.task == pTask && tTask.publication?.internalId == publicationId && tTask.connectorName == connectorName);
|
||||
TrangaTask? task = null;
|
||||
if (connectorName is null || publicationId is null)
|
||||
task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
|
||||
tTask.task == pTask);
|
||||
else
|
||||
task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
|
||||
tTask.task == pTask && tTask.publication?.internalId == publicationId &&
|
||||
tTask.connectorName == connectorName);
|
||||
|
||||
if (task is null)
|
||||
return;
|
||||
taskManager.ExecuteTaskNow(task);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
app.MapGet("/Tasks/GetRunningTasks",
|
||||
@ -84,23 +137,51 @@ 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.GetAllTasks().FirstOrDefault(tTask =>
|
||||
tTask.task == pTask && tTask.publication?.internalId == publicationId && tTask.connectorName == connectorName);
|
||||
TrangaTask? task = null;
|
||||
if (connectorName is null || publicationId is null)
|
||||
task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
|
||||
tTask.task == pTask);
|
||||
else
|
||||
task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
|
||||
tTask.task == pTask && tTask.publication?.internalId == publicationId &&
|
||||
tTask.connectorName == connectorName);
|
||||
|
||||
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.GetAllTasks().FirstOrDefault(tTask =>
|
||||
tTask.task == pTask && tTask.publication?.internalId == publicationId && tTask.connectorName == connectorName);
|
||||
TrangaTask? task = null;
|
||||
if (connectorName is null || publicationId is null)
|
||||
task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
|
||||
tTask.task == pTask);
|
||||
else
|
||||
task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
|
||||
tTask.task == pTask && tTask.publication?.internalId == publicationId &&
|
||||
tTask.connectorName == connectorName);
|
||||
|
||||
if (task is null)
|
||||
return;
|
||||
taskManager.RemoveTaskFromQueue(task);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
app.MapGet("/Settings/Get", () => taskManager.settings);
|
||||
|
@ -12,26 +12,17 @@
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5177",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
"applicationUrl": "http://localhost:5177"
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7036;http://localhost:5177",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
"applicationUrl": "https://localhost:7036;http://localhost:5177"
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
"launchBrowser": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +0,0 @@
|
||||
FROM mcr.microsoft.com/dotnet/runtime:7.0 AS base
|
||||
WORKDIR /app
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ["Tranga-CLI/Tranga-CLI.csproj", "Tranga-CLI/"]
|
||||
RUN dotnet restore "Tranga-CLI/Tranga-CLI.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Tranga-CLI"
|
||||
RUN dotnet build "Tranga-CLI.csproj" -c Release -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "Tranga-CLI.csproj" -c Release -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "Tranga-CLI.dll"]
|
@ -28,11 +28,7 @@ public static class Tranga_Cli
|
||||
Logger logger = new(new[] { Logger.LoggerType.FileLogger }, null, null, logFilePath);
|
||||
|
||||
logger.WriteLine("Tranga_CLI", "Loading Taskmanager.");
|
||||
TrangaSettings settings;
|
||||
if (File.Exists(settingsFilePath))
|
||||
settings = TrangaSettings.LoadSettings(settingsFilePath);
|
||||
else
|
||||
settings = new TrangaSettings(Directory.GetCurrentDirectory(), applicationFolderPath, null);
|
||||
TrangaSettings settings = File.Exists(settingsFilePath) ? TrangaSettings.LoadSettings(settingsFilePath) : new TrangaSettings(Directory.GetCurrentDirectory(), applicationFolderPath, null);
|
||||
|
||||
|
||||
logger.WriteLine("Tranga_CLI", "User Input");
|
||||
@ -85,7 +81,7 @@ public static class Tranga_Cli
|
||||
{
|
||||
TaskManager taskManager = new (settings, logger);
|
||||
ConsoleKey selection = ConsoleKey.EraseEndOfFile;
|
||||
PrintMenu(taskManager, taskManager.settings.downloadLocation, logger);
|
||||
PrintMenu(taskManager, taskManager.settings.downloadLocation);
|
||||
while (selection != ConsoleKey.Q)
|
||||
{
|
||||
int taskCount = taskManager.GetAllTasks().Length;
|
||||
@ -106,7 +102,7 @@ public static class Tranga_Cli
|
||||
Console.ReadKey();
|
||||
break;
|
||||
case ConsoleKey.C:
|
||||
CreateTask(taskManager, taskManager.settings, logger);
|
||||
CreateTask(taskManager, logger);
|
||||
Console.WriteLine("Press any key.");
|
||||
Console.ReadKey();
|
||||
break;
|
||||
@ -159,7 +155,7 @@ public static class Tranga_Cli
|
||||
Console.ReadKey();
|
||||
break;
|
||||
}
|
||||
PrintMenu(taskManager, taskManager.settings.downloadLocation, logger);
|
||||
PrintMenu(taskManager, taskManager.settings.downloadLocation);
|
||||
}
|
||||
Thread.Sleep(200);
|
||||
}
|
||||
@ -179,7 +175,7 @@ public static class Tranga_Cli
|
||||
taskManager.Shutdown(false);
|
||||
}
|
||||
|
||||
private static void PrintMenu(TaskManager taskManager, string folderPath, Logger logger)
|
||||
private static void PrintMenu(TaskManager taskManager, string folderPath)
|
||||
{
|
||||
int taskCount = taskManager.GetAllTasks().Length;
|
||||
int taskRunningCount = taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Running);
|
||||
@ -264,17 +260,17 @@ public static class Tranga_Cli
|
||||
Console.Clear();
|
||||
logger.WriteLine("Tranga_CLI", "Menu: Add Manga Download to queue");
|
||||
|
||||
Connector? connector = SelectConnector(taskManager.settings.downloadLocation, taskManager.GetAvailableConnectors().Values.ToArray(), logger);
|
||||
Connector? connector = SelectConnector(taskManager.GetAvailableConnectors().Values.ToArray(), logger);
|
||||
if (connector is null)
|
||||
return;
|
||||
|
||||
Publication? publication = SelectPublication(taskManager, connector!, logger);
|
||||
Publication? publication = SelectPublication(taskManager, connector, logger);
|
||||
if (publication is null)
|
||||
return;
|
||||
|
||||
TimeSpan reoccurrence = SelectReoccurrence(logger);
|
||||
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
|
||||
TrangaTask newTask = taskManager.AddTask(TrangaTask.Task.DownloadNewChapters, connector?.name, publication, reoccurrence, "en");
|
||||
TrangaTask newTask = taskManager.AddTask(TrangaTask.Task.DownloadNewChapters, connector.name, publication, reoccurrence, "en");
|
||||
Console.WriteLine(newTask);
|
||||
}
|
||||
|
||||
@ -327,24 +323,24 @@ public static class Tranga_Cli
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateTask(TaskManager taskManager, TrangaSettings settings, Logger logger)
|
||||
private static void CreateTask(TaskManager taskManager, Logger logger)
|
||||
{
|
||||
logger.WriteLine("Tranga_CLI", "Menu: Creating Task");
|
||||
TrangaTask.Task? tmpTask = SelectTaskType(logger);
|
||||
if (tmpTask is null)
|
||||
return;
|
||||
TrangaTask.Task task = (TrangaTask.Task)tmpTask!;
|
||||
TrangaTask.Task task = (TrangaTask.Task)tmpTask;
|
||||
|
||||
Connector? connector = null;
|
||||
if (task != TrangaTask.Task.UpdateKomgaLibrary)
|
||||
{
|
||||
connector = SelectConnector(settings.downloadLocation, taskManager.GetAvailableConnectors().Values.ToArray(), logger);
|
||||
connector = SelectConnector(taskManager.GetAvailableConnectors().Values.ToArray(), logger);
|
||||
if (connector is null)
|
||||
return;
|
||||
}
|
||||
|
||||
Publication? publication = null;
|
||||
if (task != TrangaTask.Task.UpdatePublications && task != TrangaTask.Task.UpdateKomgaLibrary)
|
||||
if (task != TrangaTask.Task.UpdateKomgaLibrary)
|
||||
{
|
||||
publication = SelectPublication(taskManager, connector!, logger);
|
||||
if (publication is null)
|
||||
@ -431,7 +427,7 @@ public static class Tranga_Cli
|
||||
return TimeSpan.Parse(Console.ReadLine()!, new CultureInfo("en-US"));
|
||||
}
|
||||
|
||||
private static Connector? SelectConnector(string folderPath, Connector[] connectors, Logger logger)
|
||||
private static Connector? SelectConnector(Connector[] connectors, Logger logger)
|
||||
{
|
||||
logger.WriteLine("Tranga_CLI", "Menu: Select Connector");
|
||||
Console.Clear();
|
||||
|
@ -1,3 +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">
|
||||
<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/=Taskmanager/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Tranga/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
@ -1,4 +1,5 @@
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Tranga;
|
||||
|
||||
@ -15,13 +16,14 @@ public struct Chapter
|
||||
public string fileName { get; }
|
||||
public string sortNumber { get; }
|
||||
|
||||
private static readonly Regex LegalCharacters = new Regex(@"([A-z]*[0-9]* *\.*-*,*\]*\[*'*\'*\)*\(*~*!*)*");
|
||||
public Chapter(string? name, string? volumeNumber, string? chapterNumber, string url)
|
||||
{
|
||||
this.name = name;
|
||||
this.volumeNumber = volumeNumber is { Length: > 0 } ? volumeNumber : "1";
|
||||
this.chapterNumber = chapterNumber;
|
||||
this.url = url;
|
||||
string chapterName = string.Concat((name ?? "").Split(Path.GetInvalidFileNameChars()));
|
||||
string chapterName = string.Concat(LegalCharacters.Matches(name ?? ""));
|
||||
NumberFormatInfo nfi = new NumberFormatInfo()
|
||||
{
|
||||
NumberDecimalSeparator = "."
|
||||
|
@ -14,9 +14,11 @@ public abstract class Connector
|
||||
internal string downloadLocation { get; } //Location of local files
|
||||
protected DownloadClient downloadClient { get; init; }
|
||||
|
||||
protected Logger? logger;
|
||||
protected readonly Logger? logger;
|
||||
|
||||
protected Connector(string downloadLocation, Logger? logger)
|
||||
protected readonly string imageCachePath;
|
||||
|
||||
protected Connector(string downloadLocation, string imageCachePath, Logger? logger)
|
||||
{
|
||||
this.downloadLocation = downloadLocation;
|
||||
this.logger = logger;
|
||||
@ -24,6 +26,9 @@ public abstract class Connector
|
||||
{
|
||||
//RequestTypes for RateLimits
|
||||
}, logger);
|
||||
this.imageCachePath = imageCachePath;
|
||||
if (!Directory.Exists(imageCachePath))
|
||||
Directory.CreateDirectory(this.imageCachePath);
|
||||
}
|
||||
|
||||
public abstract string name { get; } //Name of the Connector (e.g. Website)
|
||||
@ -47,33 +52,35 @@ public abstract class Connector
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the Chapter (+Images) from the website.
|
||||
/// Should later call DownloadChapterImages to retrieve the individual Images of the Chapter.
|
||||
/// Should later call DownloadChapterImages to retrieve the individual Images of the Chapter and create .cbz archive.
|
||||
/// </summary>
|
||||
/// <param name="publication">Publication that contains Chapter</param>
|
||||
/// <param name="chapter">Chapter with Images to retrieve</param>
|
||||
public abstract void DownloadChapter(Publication publication, Chapter chapter);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the Cover from the Website
|
||||
/// Copies the already downloaded cover from cache to downloadLocation
|
||||
/// </summary>
|
||||
/// <param name="publication">Publication to retrieve Cover for</param>
|
||||
public abstract void DownloadCover(Publication publication);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the series-info to series.json in the Publication Folder
|
||||
/// </summary>
|
||||
/// <param name="publication">Publication to save series.json for</param>
|
||||
public void SaveSeriesInfo(Publication publication)
|
||||
/// <param name="settings">TrangaSettings</param>
|
||||
public void CopyCoverFromCacheToDownloadLocation(Publication publication, TrangaSettings settings)
|
||||
{
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Saving series.json for {publication.sortName}");
|
||||
//Check if Publication already has a Folder and a series.json
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Cloning cover {publication.sortName} {publication.internalId}");
|
||||
//Check if Publication already has a Folder and cover
|
||||
string publicationFolder = Path.Join(downloadLocation, publication.folderName);
|
||||
if(!Directory.Exists(publicationFolder))
|
||||
Directory.CreateDirectory(publicationFolder);
|
||||
DirectoryInfo dirInfo = new (publicationFolder);
|
||||
if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover.")))
|
||||
{
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Cover exists {publication.sortName}");
|
||||
return;
|
||||
}
|
||||
|
||||
string seriesInfoPath = Path.Join(publicationFolder, "series.json");
|
||||
if(!File.Exists(seriesInfoPath))
|
||||
File.WriteAllText(seriesInfoPath,publication.GetSeriesInfoJson());
|
||||
string fileInCache = Path.Join(settings.coverImageCache, publication.coverFileNameInCache);
|
||||
string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(fileInCache).Split('.')[^1]}" );
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Cloning cover {fileInCache} -> {newFilePath}");
|
||||
File.Copy(fileInCache, newFilePath, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -81,9 +88,9 @@ public abstract class Connector
|
||||
/// See ComicInfo.xml
|
||||
/// </summary>
|
||||
/// <returns>XML-string</returns>
|
||||
protected static string CreateComicInfo(Publication publication, Chapter chapter, Logger? logger)
|
||||
protected static string GetComicInfoXmlString(Publication publication, Chapter chapter, Logger? logger)
|
||||
{
|
||||
logger?.WriteLine("Connector", $"Creating ComicInfo.Xml for {publication.sortName} Chapter {chapter.volumeNumber} {chapter.chapterNumber}");
|
||||
logger?.WriteLine("Connector", $"Creating ComicInfo.Xml for {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
|
||||
XElement comicInfo = new XElement("ComicInfo",
|
||||
new XElement("Tags", string.Join(',',publication.tags)),
|
||||
new XElement("LanguageISO", publication.originalLanguage),
|
||||
@ -99,16 +106,16 @@ public abstract class Connector
|
||||
/// Checks if a chapter-archive is already present
|
||||
/// </summary>
|
||||
/// <returns>true if chapter is present</returns>
|
||||
public bool ChapterIsDownloaded(Publication publication, Chapter chapter)
|
||||
public bool CheckChapterIsDownloaded(Publication publication, Chapter chapter)
|
||||
{
|
||||
return File.Exists(CreateFullFilepath(publication, chapter));
|
||||
return File.Exists(GetArchiveFilePath(publication, chapter));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates full file path of chapter-archive
|
||||
/// </summary>
|
||||
/// <returns>Filepath</returns>
|
||||
protected string CreateFullFilepath(Publication publication, Chapter chapter)
|
||||
protected string GetArchiveFilePath(Publication publication, Chapter chapter)
|
||||
{
|
||||
return Path.Join(downloadLocation, publication.folderName, $"{chapter.fileName}.cbz");
|
||||
}
|
||||
@ -118,11 +125,10 @@ public abstract class Connector
|
||||
/// </summary>
|
||||
/// <param name="imageUrl"></param>
|
||||
/// <param name="fullPath"></param>
|
||||
/// <param name="downloadClient">DownloadClient of the connector</param>
|
||||
/// <param name="requestType">Requesttype for ratelimit</param>
|
||||
protected static void DownloadImage(string imageUrl, string fullPath, DownloadClient downloadClient, byte requestType)
|
||||
/// <param name="requestType">RequestType for Rate-Limit</param>
|
||||
private void DownloadImage(string imageUrl, string fullPath, byte requestType, string? referrer = null)
|
||||
{
|
||||
DownloadClient.RequestResult requestResult = downloadClient.MakeRequest(imageUrl, requestType);
|
||||
DownloadClient.RequestResult requestResult = downloadClient.MakeRequest(imageUrl, requestType, referrer);
|
||||
byte[] buffer = new byte[requestResult.result.Length];
|
||||
requestResult.result.ReadExactly(buffer, 0, buffer.Length);
|
||||
File.WriteAllBytes(fullPath, buffer);
|
||||
@ -133,12 +139,11 @@ public abstract class Connector
|
||||
/// </summary>
|
||||
/// <param name="imageUrls">List of URLs to download Images from</param>
|
||||
/// <param name="saveArchiveFilePath">Full path to save archive to (without file ending .cbz)</param>
|
||||
/// <param name="downloadClient">DownloadClient of the connector</param>
|
||||
/// <param name="comicInfoPath">Path of the generate Chapter ComicInfo.xml, if it was generated</param>
|
||||
/// <param name="requestType">RequestType for RateLimits</param>
|
||||
protected static void DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, DownloadClient downloadClient, byte requestType, Logger? logger, string? comicInfoPath = null)
|
||||
protected void DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, byte requestType, string? comicInfoPath = null, string? referrer = null)
|
||||
{
|
||||
logger?.WriteLine("Connector", "Downloading Images");
|
||||
logger?.WriteLine("Connector", $"Downloading Images for {saveArchiveFilePath}");
|
||||
//Check if Publication Directory already exists
|
||||
string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!;
|
||||
if (!Directory.Exists(directoryPath))
|
||||
@ -156,18 +161,36 @@ public abstract class Connector
|
||||
{
|
||||
string[] split = imageUrl.Split('.');
|
||||
string extension = split[^1];
|
||||
DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), downloadClient, requestType);
|
||||
logger?.WriteLine("Connector", $"Downloading Image {chapter + 1}/{imageUrls.Length}");
|
||||
DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), requestType, referrer);
|
||||
}
|
||||
|
||||
if(comicInfoPath is not null)
|
||||
File.Copy(comicInfoPath, Path.Join(tempFolder, "ComicInfo.xml"));
|
||||
|
||||
logger?.WriteLine("Connector", "Creating archive");
|
||||
logger?.WriteLine("Connector", $"Creating archive {saveArchiveFilePath}");
|
||||
//ZIP-it and ship-it
|
||||
ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath);
|
||||
Directory.Delete(tempFolder, true); //Cleanup
|
||||
}
|
||||
|
||||
protected string SaveCoverImageToCache(string url, byte requestType)
|
||||
{
|
||||
string[] split = url.Split('/');
|
||||
string filename = split[^1];
|
||||
string saveImagePath = Path.Join(imageCachePath, filename);
|
||||
|
||||
if (File.Exists(saveImagePath))
|
||||
return filename;
|
||||
|
||||
DownloadClient.RequestResult coverResult = downloadClient.MakeRequest(url, requestType);
|
||||
using MemoryStream ms = new();
|
||||
coverResult.result.CopyTo(ms);
|
||||
File.WriteAllBytes(saveImagePath, ms.ToArray());
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Saving image to {saveImagePath}");
|
||||
return filename;
|
||||
}
|
||||
|
||||
protected class DownloadClient
|
||||
{
|
||||
private static readonly HttpClient Client = new();
|
||||
@ -179,8 +202,8 @@ public abstract class Connector
|
||||
/// <summary>
|
||||
/// Creates a httpClient
|
||||
/// </summary>
|
||||
/// <param name="delay">minimum delay between requests (to avoid spam)</param>
|
||||
/// <param name="rateLimitRequestsPerMinute">Rate limits for requests. byte is RequestType, int maximum requests per minute for RequestType</param>
|
||||
/// <param name="logger"></param>
|
||||
public DownloadClient(Dictionary<byte, int> rateLimitRequestsPerMinute, Logger? logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
@ -196,7 +219,7 @@ public abstract class Connector
|
||||
/// <param name="url"></param>
|
||||
/// <param name="requestType">For RateLimits: Same Endpoints use same type</param>
|
||||
/// <returns>RequestResult with StatusCode and Stream of received data</returns>
|
||||
public RequestResult MakeRequest(string url, byte requestType)
|
||||
public RequestResult MakeRequest(string url, byte requestType, string? referrer = null)
|
||||
{
|
||||
if (_rateLimit.TryGetValue(requestType, out TimeSpan value))
|
||||
_lastExecutedRateLimit.TryAdd(requestType, DateTime.Now.Subtract(value));
|
||||
@ -218,12 +241,15 @@ public abstract class Connector
|
||||
try
|
||||
{
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Get, url);
|
||||
if(referrer is not null)
|
||||
requestMessage.Headers.Referrer = new Uri(referrer);
|
||||
_lastExecutedRateLimit[requestType] = DateTime.Now;
|
||||
response = Client.Send(requestMessage);
|
||||
}
|
||||
catch (HttpRequestException e)
|
||||
{
|
||||
logger?.WriteLine(this.GetType().ToString(), e.Message);
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Waiting {_rateLimit[requestType] * 2}");
|
||||
Thread.Sleep(_rateLimit[requestType] * 2);
|
||||
}
|
||||
}
|
||||
|
@ -14,11 +14,11 @@ public class MangaDex : Connector
|
||||
Manga,
|
||||
Feed,
|
||||
AtHomeServer,
|
||||
Cover,
|
||||
Author
|
||||
CoverUrl,
|
||||
Author,
|
||||
}
|
||||
|
||||
public MangaDex(string downloadLocation, Logger? logger) : base(downloadLocation, logger)
|
||||
public MangaDex(string downloadLocation, string imageCachePath, Logger? logger) : base(downloadLocation, imageCachePath, logger)
|
||||
{
|
||||
name = "MangaDex";
|
||||
this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
|
||||
@ -26,7 +26,7 @@ public class MangaDex : Connector
|
||||
{(byte)RequestType.Manga, 250},
|
||||
{(byte)RequestType.Feed, 250},
|
||||
{(byte)RequestType.AtHomeServer, 40},
|
||||
{(byte)RequestType.Cover, 250},
|
||||
{(byte)RequestType.CoverUrl, 250},
|
||||
{(byte)RequestType.Author, 250}
|
||||
}, logger);
|
||||
}
|
||||
@ -38,6 +38,7 @@ public class MangaDex : Connector
|
||||
int offset = 0; //"Page"
|
||||
int total = int.MaxValue; //How many total results are there, is updated on first request
|
||||
HashSet<Publication> publications = new();
|
||||
int loadedPublicationData = 0;
|
||||
while (offset < total) //As long as we haven't requested all "Pages"
|
||||
{
|
||||
//Request next Page
|
||||
@ -58,6 +59,7 @@ public class MangaDex : Connector
|
||||
//Loop each Manga and extract information from JSON
|
||||
foreach (JsonNode? mangeNode in mangaInResult)
|
||||
{
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Getting publication data. {++loadedPublicationData}/{total}");
|
||||
JsonObject manga = (JsonObject)mangeNode!;
|
||||
JsonObject attributes = manga["attributes"]!.AsObject();
|
||||
|
||||
@ -98,6 +100,10 @@ public class MangaDex : Connector
|
||||
authorId = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue<string>() == "author")!["id"]!.GetValue<string>();
|
||||
}
|
||||
string? coverUrl = GetCoverUrl(publicationId, posterId);
|
||||
string? coverCacheName = null;
|
||||
if (coverUrl is not null)
|
||||
coverCacheName = SaveCoverImageToCache(coverUrl, (byte)RequestType.AtHomeServer);
|
||||
|
||||
string? author = GetAuthor(authorId);
|
||||
|
||||
Dictionary<string, string> linksDict = new();
|
||||
@ -127,6 +133,7 @@ public class MangaDex : Connector
|
||||
altTitlesDict,
|
||||
tags.ToArray(),
|
||||
coverUrl,
|
||||
coverCacheName,
|
||||
linksDict,
|
||||
year,
|
||||
originalLanguage,
|
||||
@ -137,12 +144,13 @@ public class MangaDex : Connector
|
||||
}
|
||||
}
|
||||
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Done getting publications (title={publicationTitle})");
|
||||
return publications.ToArray();
|
||||
}
|
||||
|
||||
public override Chapter[] GetChapters(Publication publication, string language = "")
|
||||
{
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Getting Chapters {publication.sortName} (language={language})");
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Getting Chapters for {publication.sortName} {publication.internalId} (language={language})");
|
||||
const int limit = 100; //How many values we want returned at once
|
||||
int offset = 0; //"Page"
|
||||
int total = int.MaxValue; //How many total results are there, is updated on first request
|
||||
@ -192,12 +200,13 @@ public class MangaDex : Connector
|
||||
{
|
||||
NumberDecimalSeparator = "."
|
||||
};
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Done getting Chapters for {publication.internalId}");
|
||||
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
|
||||
}
|
||||
|
||||
public override void DownloadChapter(Publication publication, Chapter chapter)
|
||||
{
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Download Chapter {publication.sortName} {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
|
||||
DownloadClient.RequestResult requestResult =
|
||||
downloadClient.MakeRequest($"https://api.mangadex.org/at-home/server/{chapter.url}?forcePort443=false'", (byte)RequestType.AtHomeServer);
|
||||
@ -216,23 +225,24 @@ public class MangaDex : Connector
|
||||
imageUrls.Add($"{baseUrl}/data/{hash}/{image!.GetValue<string>()}");
|
||||
|
||||
string comicInfoPath = Path.GetTempFileName();
|
||||
File.WriteAllText(comicInfoPath, CreateComicInfo(publication, chapter, logger));
|
||||
File.WriteAllText(comicInfoPath, GetComicInfoXmlString(publication, chapter, logger));
|
||||
|
||||
//Download Chapter-Images
|
||||
DownloadChapterImages(imageUrls.ToArray(), CreateFullFilepath(publication, chapter), downloadClient, (byte)RequestType.AtHomeServer, logger, comicInfoPath);
|
||||
DownloadChapterImages(imageUrls.ToArray(), GetArchiveFilePath(publication, chapter), (byte)RequestType.AtHomeServer, comicInfoPath);
|
||||
}
|
||||
|
||||
private string? GetCoverUrl(string publicationId, string? posterId)
|
||||
{
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Getting CoverUrl for {publicationId}");
|
||||
if (posterId is null)
|
||||
{
|
||||
logger?.WriteLine(this.GetType().ToString(), $"No posterId");
|
||||
logger?.WriteLine(this.GetType().ToString(), $"No posterId, aborting");
|
||||
return null;
|
||||
}
|
||||
|
||||
//Request information where to download Cover
|
||||
DownloadClient.RequestResult requestResult =
|
||||
downloadClient.MakeRequest($"https://api.mangadex.org/cover/{posterId}", (byte)RequestType.Cover);
|
||||
downloadClient.MakeRequest($"https://api.mangadex.org/cover/{posterId}", (byte)RequestType.CoverUrl);
|
||||
if (requestResult.statusCode != HttpStatusCode.OK)
|
||||
return null;
|
||||
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
||||
@ -242,6 +252,7 @@ public class MangaDex : Connector
|
||||
string fileName = result["data"]!["attributes"]!["fileName"]!.GetValue<string>();
|
||||
|
||||
string coverUrl = $"https://uploads.mangadex.org/covers/{publicationId}/{fileName}";
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Got Cover-Url for {publicationId} -> {coverUrl}");
|
||||
return coverUrl;
|
||||
}
|
||||
|
||||
@ -259,37 +270,7 @@ public class MangaDex : Connector
|
||||
return null;
|
||||
|
||||
string author = result["data"]!["attributes"]!["name"]!.GetValue<string>();
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Got author {authorId} -> {author}");
|
||||
return author;
|
||||
}
|
||||
|
||||
public override void DownloadCover(Publication publication)
|
||||
{
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Download cover {publication.sortName}");
|
||||
//Check if Publication already has a Folder and cover
|
||||
string publicationFolder = Path.Join(downloadLocation, publication.folderName);
|
||||
if(!Directory.Exists(publicationFolder))
|
||||
Directory.CreateDirectory(publicationFolder);
|
||||
DirectoryInfo dirInfo = new (publicationFolder);
|
||||
if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover.")))
|
||||
{
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Cover exists {publication.sortName}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (publication.posterUrl is null || publication.posterUrl!.Contains("http"))
|
||||
{
|
||||
logger?.WriteLine(this.GetType().ToString(), $"No Poster-URL in publication");
|
||||
return;
|
||||
}
|
||||
|
||||
//Get file-extension (jpg, png)
|
||||
string[] split = publication.posterUrl.Split('.');
|
||||
string extension = split[^1];
|
||||
|
||||
string outFolderPath = Path.Join(downloadLocation, publication.folderName);
|
||||
Directory.CreateDirectory(outFolderPath);
|
||||
|
||||
//Download cover-Image
|
||||
DownloadImage(publication.posterUrl, Path.Join(downloadLocation, publication.folderName, $"cover.{extension}"), this.downloadClient, (byte)RequestType.AtHomeServer);
|
||||
}
|
||||
}
|
197
Tranga/Connectors/Manganato.cs
Normal file
@ -0,0 +1,197 @@
|
||||
using System.Collections;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using HtmlAgilityPack;
|
||||
using Logging;
|
||||
|
||||
namespace Tranga.Connectors;
|
||||
|
||||
public class Manganato : Connector
|
||||
{
|
||||
public override string name { get; }
|
||||
|
||||
public Manganato(string downloadLocation, string imageCachePath, Logger? logger) : base(downloadLocation, imageCachePath, logger)
|
||||
{
|
||||
this.name = "Manganato";
|
||||
this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
|
||||
{
|
||||
{(byte)1, 100}
|
||||
}, logger);
|
||||
}
|
||||
|
||||
public override Publication[] GetPublications(string publicationTitle = "")
|
||||
{
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Getting Publications (title={publicationTitle})");
|
||||
string sanitizedTitle = publicationTitle.ToLower().Replace(' ', '_');
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Getting Publications (title={sanitizedTitle})");
|
||||
string requestUrl = $"https://manganato.com/search/story/{sanitizedTitle}";
|
||||
DownloadClient.RequestResult requestResult =
|
||||
downloadClient.MakeRequest(requestUrl, (byte)1);
|
||||
if (requestResult.statusCode != HttpStatusCode.OK)
|
||||
return Array.Empty<Publication>();
|
||||
|
||||
return ParsePublicationsFromHtml(requestResult.result);
|
||||
}
|
||||
|
||||
private Publication[] ParsePublicationsFromHtml(Stream html)
|
||||
{
|
||||
StreamReader reader = new (html);
|
||||
string htmlString = reader.ReadToEnd();
|
||||
HtmlDocument document = new ();
|
||||
document.LoadHtml(htmlString);
|
||||
IEnumerable<HtmlNode> searchResults = document.DocumentNode.Descendants("div").Where(n => n.HasClass("search-story-item"));
|
||||
List<string> urls = new();
|
||||
foreach (HtmlNode mangaResult in searchResults)
|
||||
{
|
||||
urls.Add(mangaResult.Descendants("a").First(n => n.HasClass("item-title")).GetAttributes()
|
||||
.First(a => a.Name == "href").Value);
|
||||
}
|
||||
|
||||
HashSet<Publication> ret = new();
|
||||
foreach (string url in urls)
|
||||
{
|
||||
DownloadClient.RequestResult requestResult =
|
||||
downloadClient.MakeRequest(url, (byte)1);
|
||||
if (requestResult.statusCode != HttpStatusCode.OK)
|
||||
return Array.Empty<Publication>();
|
||||
|
||||
ret.Add(ParseSinglePublicationFromHtml(requestResult.result, url.Split('/')[^1]));
|
||||
}
|
||||
|
||||
return ret.ToArray();
|
||||
}
|
||||
|
||||
private Publication ParseSinglePublicationFromHtml(Stream html, string publicationId)
|
||||
{
|
||||
StreamReader reader = new (html);
|
||||
string htmlString = reader.ReadToEnd();
|
||||
HtmlDocument document = new ();
|
||||
document.LoadHtml(htmlString);
|
||||
string status = "";
|
||||
Dictionary<string, string> altTitles = new();
|
||||
Dictionary<string, string>? links = null;
|
||||
HashSet<string> tags = new();
|
||||
string? author = null, originalLanguage = null;
|
||||
int? year = DateTime.Now.Year;
|
||||
|
||||
HtmlNode infoNode = document.DocumentNode.Descendants("div").First(d => d.HasClass("story-info-right"));
|
||||
|
||||
string sortName = infoNode.Descendants("h1").First().InnerText;
|
||||
|
||||
HtmlNode infoTable = infoNode.Descendants().First(d => d.Name == "table");
|
||||
|
||||
foreach (HtmlNode row in infoTable.Descendants("tr"))
|
||||
{
|
||||
string key = row.SelectNodes("td").First().InnerText.ToLower();
|
||||
string value = row.SelectNodes("td").Last().InnerText;
|
||||
string keySanitized = string.Concat(Regex.Matches(key, "[a-z]"));
|
||||
|
||||
switch (keySanitized)
|
||||
{
|
||||
case "alternative":
|
||||
string[] alts = value.Split(" ; ");
|
||||
for(int i = 0; i < alts.Length; i++)
|
||||
altTitles.Add(i.ToString(), alts[i]);
|
||||
break;
|
||||
case "authors":
|
||||
author = value;
|
||||
break;
|
||||
case "status":
|
||||
status = value;
|
||||
break;
|
||||
case "genres":
|
||||
string[] genres = value.Split(" - ");
|
||||
tags = genres.ToHashSet();
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
string posterUrl = document.DocumentNode.Descendants("span").First(s => s.HasClass("info-image")).Descendants("img").First()
|
||||
.GetAttributes().First(a => a.Name == "src").Value;
|
||||
|
||||
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, 1);
|
||||
|
||||
string description = document.DocumentNode.Descendants("div").First(d => d.HasClass("panel-story-info-description"))
|
||||
.InnerText.Replace("Description :", "");
|
||||
while (description.StartsWith('\n'))
|
||||
description = description.Substring(1);
|
||||
|
||||
string yearString = document.DocumentNode.Descendants("li").Last(li => li.HasClass("a-h")).Descendants("span")
|
||||
.First(s => s.HasClass("chapter-time")).InnerText;
|
||||
year = Convert.ToInt32(yearString.Split(',')[^1]) + 2000;
|
||||
|
||||
return new Publication(sortName, author, description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
|
||||
year, originalLanguage, status, publicationId);
|
||||
}
|
||||
|
||||
public override Chapter[] GetChapters(Publication publication, string language = "")
|
||||
{
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Getting Chapters for {publication.sortName} {publication.internalId} (language={language})");
|
||||
string requestUrl = $"https://chapmanganato.com/{publication.publicationId}";
|
||||
DownloadClient.RequestResult requestResult =
|
||||
downloadClient.MakeRequest(requestUrl, (byte)1);
|
||||
if (requestResult.statusCode != HttpStatusCode.OK)
|
||||
return Array.Empty<Chapter>();
|
||||
|
||||
return ParseChaptersFromHtml(requestResult.result);
|
||||
}
|
||||
|
||||
private Chapter[] ParseChaptersFromHtml(Stream html)
|
||||
{
|
||||
StreamReader reader = new (html);
|
||||
string htmlString = reader.ReadToEnd();
|
||||
HtmlDocument document = new ();
|
||||
document.LoadHtml(htmlString);
|
||||
List<Chapter> ret = new();
|
||||
|
||||
HtmlNode chapterList = document.DocumentNode.Descendants("ul").First(l => l.HasClass("row-content-chapter"));
|
||||
|
||||
foreach (HtmlNode chapterInfo in chapterList.Descendants("li"))
|
||||
{
|
||||
string fullString = chapterInfo.Descendants("a").First(d => d.HasClass("chapter-name")).InnerText;
|
||||
|
||||
string? volumeNumber = fullString.Contains("Vol.") ? fullString.Replace("Vol.", "").Split(' ')[0] : null;
|
||||
string? chapterNumber = fullString.Split(':')[0].Split(' ')[^1];
|
||||
string chapterName = string.Concat(fullString.Split(':')[1..]);
|
||||
string url = chapterInfo.Descendants("a").First(d => d.HasClass("chapter-name"))
|
||||
.GetAttributeValue("href", "");
|
||||
ret.Add(new Chapter(chapterName, volumeNumber, chapterNumber, url));
|
||||
}
|
||||
|
||||
return ret.ToArray();
|
||||
}
|
||||
|
||||
public override void DownloadChapter(Publication publication, Chapter chapter)
|
||||
{
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
|
||||
string requestUrl = chapter.url;
|
||||
DownloadClient.RequestResult requestResult =
|
||||
downloadClient.MakeRequest(requestUrl, (byte)1);
|
||||
if (requestResult.statusCode != HttpStatusCode.OK)
|
||||
return;
|
||||
|
||||
string[] imageUrls = ParseImageUrlsFromHtml(requestResult.result);
|
||||
|
||||
string comicInfoPath = Path.GetTempFileName();
|
||||
File.WriteAllText(comicInfoPath, GetComicInfoXmlString(publication, chapter, logger));
|
||||
|
||||
DownloadChapterImages(imageUrls, GetArchiveFilePath(publication, chapter), (byte)1, comicInfoPath, "https://chapmanganato.com/");
|
||||
}
|
||||
|
||||
private string[] ParseImageUrlsFromHtml(Stream html)
|
||||
{
|
||||
StreamReader reader = new (html);
|
||||
string htmlString = reader.ReadToEnd();
|
||||
HtmlDocument document = new ();
|
||||
document.LoadHtml(htmlString);
|
||||
List<string> ret = new();
|
||||
|
||||
HtmlNode imageContainer =
|
||||
document.DocumentNode.Descendants("div").First(i => i.HasClass("container-chapter-reader"));
|
||||
foreach(HtmlNode imageNode in imageContainer.Descendants("img"))
|
||||
ret.Add(imageNode.GetAttributeValue("src", ""));
|
||||
|
||||
return ret.ToArray();
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json.Nodes;
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
@ -45,9 +46,17 @@ public class Komga
|
||||
{
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Getting Libraries");
|
||||
Stream data = NetClient.MakeRequest($"{baseUrl}/api/v1/libraries", auth);
|
||||
if (data == Stream.Null)
|
||||
{
|
||||
logger?.WriteLine(this.GetType().ToString(), $"No libraries returned");
|
||||
return Array.Empty<KomgaLibrary>();
|
||||
}
|
||||
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
|
||||
if (result is null)
|
||||
{
|
||||
logger?.WriteLine(this.GetType().ToString(), $"No libraries returned");
|
||||
return Array.Empty<KomgaLibrary>();
|
||||
}
|
||||
|
||||
HashSet<KomgaLibrary> ret = new();
|
||||
|
||||
@ -89,37 +98,51 @@ public class Komga
|
||||
{
|
||||
public static Stream MakeRequest(string url, string auth)
|
||||
{
|
||||
HttpClient client = new();
|
||||
HttpRequestMessage requestMessage = new HttpRequestMessage
|
||||
HttpClientHandler clientHandler = new ();
|
||||
clientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) => true;
|
||||
HttpClient client = new(clientHandler);
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", auth);
|
||||
|
||||
HttpRequestMessage requestMessage = new ()
|
||||
{
|
||||
Method = HttpMethod.Get,
|
||||
RequestUri = new Uri(url),
|
||||
Headers =
|
||||
{
|
||||
{ "Accept", "application/json" },
|
||||
{ "Authorization", new AuthenticationHeaderValue("Basic", auth).ToString() }
|
||||
}
|
||||
RequestUri = new Uri(url)
|
||||
};
|
||||
HttpResponseMessage response = client.Send(requestMessage);
|
||||
Stream resultString = response.IsSuccessStatusCode ? response.Content.ReadAsStream() : Stream.Null;
|
||||
return resultString;
|
||||
Stream ret;
|
||||
if (response.StatusCode is HttpStatusCode.Unauthorized)
|
||||
{
|
||||
ret = MakeRequest(response.RequestMessage!.RequestUri!.AbsoluteUri, auth);
|
||||
}else
|
||||
return response.IsSuccessStatusCode ? response.Content.ReadAsStream() : Stream.Null;
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static bool MakePost(string url, string auth)
|
||||
{
|
||||
HttpClient client = new();
|
||||
HttpRequestMessage requestMessage = new HttpRequestMessage
|
||||
HttpClientHandler clientHandler = new HttpClientHandler();
|
||||
clientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) => true;
|
||||
HttpClient client = new(clientHandler)
|
||||
{
|
||||
Method = HttpMethod.Post,
|
||||
RequestUri = new Uri(url),
|
||||
Headers =
|
||||
DefaultRequestHeaders =
|
||||
{
|
||||
{ "Accept", "application/json" },
|
||||
{ "Authorization", new AuthenticationHeaderValue("Basic", auth).ToString() }
|
||||
}
|
||||
};
|
||||
HttpRequestMessage requestMessage = new HttpRequestMessage
|
||||
{
|
||||
Method = HttpMethod.Post,
|
||||
RequestUri = new Uri(url)
|
||||
};
|
||||
HttpResponseMessage response = client.Send(requestMessage);
|
||||
bool ret;
|
||||
if (response.StatusCode is HttpStatusCode.Unauthorized)
|
||||
{
|
||||
ret = MakePost(response.RequestMessage!.RequestUri!.AbsoluteUri, auth);
|
||||
}else
|
||||
return response.IsSuccessStatusCode;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Tranga;
|
||||
@ -15,6 +16,7 @@ public readonly struct Publication
|
||||
public string? description { get; }
|
||||
public string[] tags { get; }
|
||||
public string? posterUrl { get; }
|
||||
public string? coverFileNameInCache { get; }
|
||||
public Dictionary<string,string> links { get; }
|
||||
public int? year { get; }
|
||||
public string? originalLanguage { get; }
|
||||
@ -23,24 +25,39 @@ public readonly struct Publication
|
||||
public string publicationId { get; }
|
||||
public string internalId { get; }
|
||||
|
||||
public Publication(string sortName, string? author, string? description, Dictionary<string,string> altTitles, string[] tags, string? posterUrl, Dictionary<string,string>? links, int? year, string? originalLanguage, string status, string publicationId)
|
||||
private static readonly Regex LegalCharacters = new Regex(@"([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)
|
||||
{
|
||||
this.sortName = sortName;
|
||||
this.author = author;
|
||||
this.description = description;
|
||||
this.altTitles = altTitles;
|
||||
this.tags = tags;
|
||||
this.coverFileNameInCache = coverFileNameInCache;
|
||||
this.posterUrl = posterUrl;
|
||||
this.links = links ?? new Dictionary<string, string>();
|
||||
this.year = year;
|
||||
this.originalLanguage = originalLanguage;
|
||||
this.status = status;
|
||||
this.publicationId = publicationId;
|
||||
this.folderName = string.Concat(sortName.Split(Path.GetInvalidPathChars().Concat(Path.GetInvalidFileNameChars()).ToArray()));
|
||||
this.folderName = string.Concat(LegalCharacters.Matches(sortName));
|
||||
while (this.folderName.EndsWith('.'))
|
||||
this.folderName = this.folderName.Substring(0, this.folderName.Length - 1);
|
||||
string onlyLowerLetters = string.Concat(this.sortName.ToLower().Where(Char.IsLetter));
|
||||
this.internalId = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{onlyLowerLetters}{this.year}"));
|
||||
}
|
||||
|
||||
public void SaveSeriesInfoJson(string downloadDirectory)
|
||||
{
|
||||
string publicationFolder = Path.Join(downloadDirectory, this.folderName);
|
||||
if(!Directory.Exists(publicationFolder))
|
||||
Directory.CreateDirectory(publicationFolder);
|
||||
string seriesInfoPath = Path.Join(publicationFolder, "series.json");
|
||||
if(!File.Exists(seriesInfoPath))
|
||||
File.WriteAllText(seriesInfoPath,this.GetSeriesInfoJson());
|
||||
}
|
||||
|
||||
/// <returns>Serialized JSON String for series.json</returns>
|
||||
public string GetSeriesInfoJson()
|
||||
{
|
||||
@ -78,9 +95,9 @@ public readonly struct Publication
|
||||
{
|
||||
this.name = name;
|
||||
this.year = year;
|
||||
if(status == "ongoing" || status == "hiatus")
|
||||
if(status.ToLower() == "ongoing" || status.ToLower() == "hiatus")
|
||||
this.status = "Continuing";
|
||||
else if (status == "completed" || status == "cancelled")
|
||||
else if (status.ToLower() == "completed" || status.ToLower() == "cancelled")
|
||||
this.status = "Ended";
|
||||
else
|
||||
this.status = status;
|
||||
|
@ -1,129 +0,0 @@
|
||||
using Logging;
|
||||
|
||||
namespace Tranga;
|
||||
|
||||
/// <summary>
|
||||
/// Executes TrangaTasks
|
||||
/// Based on the TrangaTask.Task a method is called.
|
||||
/// The chapterCollection is updated with new Publications/Chapters.
|
||||
/// </summary>
|
||||
public static class TaskExecutor
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes TrangaTask.
|
||||
/// </summary>
|
||||
/// <param name="taskManager">Parent</param>
|
||||
/// <param name="trangaTask">Task to execute</param>
|
||||
/// <param name="chapterCollection">Current chapterCollection to update</param>
|
||||
/// <param name="logger"></param>
|
||||
/// <exception cref="ArgumentException">Is thrown when there is no Connector available with the name of the TrangaTask.connectorName</exception>
|
||||
public static void Execute(TaskManager taskManager, TrangaTask trangaTask, Logger? logger)
|
||||
{
|
||||
//Only execute task if it is not already being executed.
|
||||
if (trangaTask.state == TrangaTask.ExecutionState.Running)
|
||||
{
|
||||
logger?.WriteLine("TaskExecutor", $"Task already running {trangaTask}");
|
||||
return;
|
||||
}
|
||||
trangaTask.state = TrangaTask.ExecutionState.Running;
|
||||
logger?.WriteLine("TaskExecutor", $"Starting Task {trangaTask}");
|
||||
|
||||
//Connector is not needed for all tasks
|
||||
Connector? connector = null;
|
||||
if (trangaTask.task != TrangaTask.Task.UpdateKomgaLibrary)
|
||||
connector = taskManager.GetConnector(trangaTask.connectorName!);
|
||||
|
||||
//Call appropriate Method based on TrangaTask.Task
|
||||
switch (trangaTask.task)
|
||||
{
|
||||
case TrangaTask.Task.DownloadNewChapters:
|
||||
DownloadNewChapters(connector!, (Publication)trangaTask.publication!, trangaTask.language, ref taskManager._chapterCollection);
|
||||
break;
|
||||
case TrangaTask.Task.UpdateChapters:
|
||||
UpdateChapters(connector!, (Publication)trangaTask.publication!, trangaTask.language, ref taskManager._chapterCollection);
|
||||
break;
|
||||
case TrangaTask.Task.UpdatePublications:
|
||||
UpdatePublications(connector!, ref taskManager._chapterCollection);
|
||||
break;
|
||||
case TrangaTask.Task.UpdateKomgaLibrary:
|
||||
UpdateKomgaLibrary(taskManager);
|
||||
break;
|
||||
}
|
||||
|
||||
logger?.WriteLine("TaskExecutor", $"Task finished! {trangaTask}");
|
||||
trangaTask.lastExecuted = DateTime.Now;
|
||||
trangaTask.state = TrangaTask.ExecutionState.Waiting;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates all Komga-Libraries
|
||||
/// </summary>
|
||||
/// <param name="taskManager">Parent</param>
|
||||
private static void UpdateKomgaLibrary(TaskManager taskManager)
|
||||
{
|
||||
if (taskManager.komga is null)
|
||||
return;
|
||||
Komga komga = taskManager.komga;
|
||||
|
||||
Komga.KomgaLibrary[] allLibraries = komga.GetLibraries();
|
||||
foreach (Komga.KomgaLibrary lib in allLibraries)
|
||||
komga.UpdateLibrary(lib.id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the available Publications from a Connector (all of them)
|
||||
/// </summary>
|
||||
/// <param name="connector">Connector to receive Publications from</param>
|
||||
/// <param name="chapterCollection"></param>
|
||||
private static void UpdatePublications(Connector connector, ref Dictionary<Publication, List<Chapter>> chapterCollection)
|
||||
{
|
||||
Publication[] publications = connector.GetPublications();
|
||||
foreach (Publication publication in publications)
|
||||
chapterCollection.TryAdd(publication, new List<Chapter>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks for new Chapters and Downloads new ones.
|
||||
/// If no Chapters had been downloaded previously, download also cover and create series.json
|
||||
/// </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>
|
||||
private static void DownloadNewChapters(Connector connector, Publication publication, string language, ref Dictionary<Publication, List<Chapter>> chapterCollection)
|
||||
{
|
||||
//Check if Publication already has a Folder
|
||||
string publicationFolder = Path.Join(connector.downloadLocation, publication.folderName);
|
||||
if(!Directory.Exists(publicationFolder))
|
||||
Directory.CreateDirectory(publicationFolder);
|
||||
List<Chapter> newChapters = UpdateChapters(connector, publication, language, ref chapterCollection);
|
||||
|
||||
connector.DownloadCover(publication);
|
||||
|
||||
string seriesInfoPath = Path.Join(publicationFolder, "series.json");
|
||||
if(!File.Exists(seriesInfoPath))
|
||||
File.WriteAllText(seriesInfoPath,publication.GetSeriesInfoJson());
|
||||
|
||||
foreach(Chapter newChapter in newChapters)
|
||||
connector.DownloadChapter(publication, newChapter);
|
||||
}
|
||||
|
||||
/// <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> UpdateChapters(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.ChapterIsDownloaded(publication, nChapter)).ToList();
|
||||
|
||||
return newChaptersList;
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Tranga.Connectors;
|
||||
using Tranga.TrangaTasks;
|
||||
|
||||
namespace Tranga;
|
||||
|
||||
@ -10,7 +11,7 @@ namespace Tranga;
|
||||
/// </summary>
|
||||
public class TaskManager
|
||||
{
|
||||
public Dictionary<Publication, List<Chapter>> _chapterCollection = new();
|
||||
public Dictionary<Publication, List<Chapter>> chapterCollection = new();
|
||||
private HashSet<TrangaTask> _allTasks;
|
||||
private bool _continueRunning = true;
|
||||
private readonly Connector[] _connectors;
|
||||
@ -21,11 +22,12 @@ public class TaskManager
|
||||
|
||||
/// <param name="downloadFolderPath">Local path to save data (Manga) to</param>
|
||||
/// <param name="workingDirectory">Path to the working directory</param>
|
||||
/// <param name="imageCachePath">Path to the cover-image cache</param>
|
||||
/// <param name="komgaBaseUrl">The Url of the Komga-instance that you want to update</param>
|
||||
/// <param name="komgaUsername">The Komga username</param>
|
||||
/// <param name="komgaPassword">The Komga password</param>
|
||||
/// <param name="logger"></param>
|
||||
public TaskManager(string downloadFolderPath, string workingDirectory, string? komgaBaseUrl = null, string? komgaUsername = null, string? komgaPassword = null, Logger? logger = null)
|
||||
public TaskManager(string downloadFolderPath, string workingDirectory, string imageCachePath, string? komgaBaseUrl = null, string? komgaUsername = null, string? komgaPassword = null, Logger? logger = null)
|
||||
{
|
||||
this.logger = logger;
|
||||
_allTasks = new HashSet<TrangaTask>();
|
||||
@ -35,9 +37,13 @@ public class TaskManager
|
||||
newKomga = new Komga(komgaBaseUrl, komgaUsername, komgaPassword, logger);
|
||||
|
||||
this.settings = new TrangaSettings(downloadFolderPath, workingDirectory, newKomga);
|
||||
ExportData();
|
||||
ExportDataAndSettings();
|
||||
|
||||
this._connectors = new Connector[]{ new MangaDex(downloadFolderPath, logger) };
|
||||
this._connectors = new Connector[]
|
||||
{
|
||||
new MangaDex(downloadFolderPath, imageCachePath, logger),
|
||||
new Manganato(downloadFolderPath, imageCachePath, logger)
|
||||
};
|
||||
foreach(Connector cConnector in this._connectors)
|
||||
_taskQueue.Add(cConnector, new List<TrangaTask>());
|
||||
|
||||
@ -47,25 +53,28 @@ public class TaskManager
|
||||
|
||||
public void UpdateSettings(string? downloadLocation, string? komgaUrl, string? komgaAuth)
|
||||
{
|
||||
Komga? komga = null;
|
||||
if (komgaUrl is not null && komgaAuth is not null)
|
||||
komga = new Komga(komgaUrl, komgaAuth, null);
|
||||
settings.downloadLocation = downloadLocation ?? settings.downloadLocation;
|
||||
settings.komga = komga ?? komga;
|
||||
ExportData();
|
||||
if (komgaUrl is not null && komgaAuth is not null && komgaUrl.Length > 0 && komgaAuth.Length > 0)
|
||||
settings.komga = new Komga(komgaUrl, komgaAuth, null);
|
||||
if (downloadLocation is not null && downloadLocation.Length > 0)
|
||||
settings.downloadLocation = downloadLocation;
|
||||
ExportDataAndSettings();
|
||||
}
|
||||
|
||||
public TaskManager(TrangaSettings settings, Logger? logger = null)
|
||||
{
|
||||
this.logger = logger;
|
||||
this._connectors = new Connector[]{ new MangaDex(settings.downloadLocation, logger) };
|
||||
this._connectors = new Connector[]
|
||||
{
|
||||
new MangaDex(settings.downloadLocation, settings.coverImageCache, logger),
|
||||
new Manganato(settings.downloadLocation, settings.coverImageCache, logger)
|
||||
};
|
||||
foreach(Connector cConnector in this._connectors)
|
||||
_taskQueue.Add(cConnector, new List<TrangaTask>());
|
||||
_allTasks = new HashSet<TrangaTask>();
|
||||
|
||||
this.settings = settings;
|
||||
ImportData();
|
||||
ExportData();
|
||||
ExportDataAndSettings();
|
||||
Thread taskChecker = new(TaskCheckerThread);
|
||||
taskChecker.Start();
|
||||
}
|
||||
@ -83,7 +92,7 @@ public class TaskManager
|
||||
foreach (KeyValuePair<Connector, List<TrangaTask>> connectorTaskQueue in _taskQueue)
|
||||
{
|
||||
if(connectorTaskQueue.Value.RemoveAll(task => task.state == TrangaTask.ExecutionState.Waiting) > 0)
|
||||
ExportData();
|
||||
ExportDataAndSettings();
|
||||
|
||||
if (connectorTaskQueue.Value.Count > 0 && connectorTaskQueue.Value.All(task => task.state is TrangaTask.ExecutionState.Enqueued))
|
||||
ExecuteTaskNow(connectorTaskQueue.Value.First());
|
||||
@ -94,7 +103,7 @@ public class TaskManager
|
||||
foreach (TrangaTask task in _allTasks.Where(aTask => aTask.ShouldExecute()))
|
||||
{
|
||||
task.state = TrangaTask.ExecutionState.Enqueued;
|
||||
if(task.connectorName is null)
|
||||
if(task.task == TrangaTask.Task.UpdateKomgaLibrary)
|
||||
ExecuteTaskNow(task);
|
||||
else
|
||||
{
|
||||
@ -115,10 +124,9 @@ public class TaskManager
|
||||
if (!this._allTasks.Contains(task))
|
||||
return;
|
||||
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Forcing Execution: {task}");
|
||||
Task t = new Task(() =>
|
||||
Task t = new(() =>
|
||||
{
|
||||
TaskExecutor.Execute(this, task, logger);
|
||||
task.Execute(this, this.logger);
|
||||
});
|
||||
t.Start();
|
||||
}
|
||||
@ -137,44 +145,41 @@ public class TaskManager
|
||||
{
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Adding new Task {task} {connectorName} {publication?.sortName}");
|
||||
|
||||
TrangaTask newTask;
|
||||
TrangaTask? newTask = null;
|
||||
if (task == TrangaTask.Task.UpdateKomgaLibrary)
|
||||
{
|
||||
newTask = new TrangaTask(task, null, null, reoccurrence, language);
|
||||
|
||||
//Check if same task already exists
|
||||
// ReSharper disable once SimplifyLinqExpressionUseAll readabilty
|
||||
if (!_allTasks.Any(trangaTask => trangaTask.task == task))
|
||||
{
|
||||
newTask = new UpdateKomgaLibraryTask(task, reoccurrence);
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Removing old {task}-Task.");
|
||||
//Only one UpdateKomgaLibrary Task
|
||||
_allTasks.RemoveWhere(trangaTask => trangaTask.task is TrangaTask.Task.UpdateKomgaLibrary);
|
||||
_allTasks.Add(newTask);
|
||||
}
|
||||
}
|
||||
else
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Added new Task {newTask}");
|
||||
}else if (task == TrangaTask.Task.DownloadNewChapters)
|
||||
{
|
||||
//Get appropriate Connector from available Connectors for TrangaTask
|
||||
Connector? connector = _connectors.FirstOrDefault(c => c.name == connectorName);
|
||||
if (connectorName is null)
|
||||
throw new ArgumentException($"connectorName can not be null for task {task}");
|
||||
|
||||
//Get appropriate Connector from available Connectors for TrangaTask
|
||||
Connector? connector = _connectors.FirstOrDefault(c => c.name == connectorName);
|
||||
if (connector is null)
|
||||
throw new ArgumentException($"Connector {connectorName} is not a known connector.");
|
||||
if (publication is null)
|
||||
throw new ArgumentException($"publication can not be null for task {task}");
|
||||
Publication pub = (Publication)publication;
|
||||
newTask = new DownloadNewChaptersTask(task, connector!.name, pub, reoccurrence, language);
|
||||
|
||||
newTask = new TrangaTask(task, connector.name, publication, reoccurrence, language);
|
||||
|
||||
//Check if same task already exists
|
||||
if (!_allTasks.Any(trangaTask => trangaTask.task == task && trangaTask.connectorName == connector.name &&
|
||||
trangaTask.publication?.internalId == publication?.internalId))
|
||||
if (!_allTasks.Any(trangaTask =>
|
||||
trangaTask.task == task && trangaTask.publication?.internalId == pub.internalId &&
|
||||
trangaTask.connectorName == connector.name))
|
||||
{
|
||||
if(task != TrangaTask.Task.UpdatePublications)
|
||||
_chapterCollection.TryAdd((Publication)publication!, new List<Chapter>());
|
||||
_allTasks.Add(newTask);
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Added new Task {newTask}");
|
||||
}
|
||||
else
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Publication already exists {publication?.internalId}");
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Task already exists {newTask}");
|
||||
}
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Added new Task {newTask.ToString()}");
|
||||
ExportData();
|
||||
ExportDataAndSettings();
|
||||
|
||||
if (newTask is null)
|
||||
throw new Exception("Invalid path");
|
||||
return newTask;
|
||||
}
|
||||
|
||||
@ -210,7 +215,7 @@ public class TaskManager
|
||||
else
|
||||
logger?.WriteLine(this.GetType().ToString(), $"No Task {task} {publication?.sortName} {publication?.internalId} could be found.");
|
||||
}
|
||||
ExportData();
|
||||
ExportDataAndSettings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -254,8 +259,8 @@ public class TaskManager
|
||||
Publication[] ret = connector.GetPublications(title ?? "");
|
||||
foreach (Publication publication in ret)
|
||||
{
|
||||
if(!_chapterCollection.Any(pub => pub.Key.sortName == publication.sortName))
|
||||
this._chapterCollection.TryAdd(publication, new List<Chapter>());
|
||||
if(!chapterCollection.Any(pub => pub.Key.sortName == publication.sortName))
|
||||
this.chapterCollection.TryAdd(publication, new List<Chapter>());
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@ -263,7 +268,7 @@ public class TaskManager
|
||||
/// <returns>All added Publications</returns>
|
||||
public Publication[] GetAllPublications()
|
||||
{
|
||||
return this._chapterCollection.Keys.ToArray();
|
||||
return this.chapterCollection.Keys.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -278,7 +283,7 @@ public class TaskManager
|
||||
Connector? ret = this._connectors.FirstOrDefault(connector => connector.name == connectorName);
|
||||
if (ret is null)
|
||||
throw new Exception($"Connector {connectorName} is not an available Connector.");
|
||||
return (Connector)ret!;
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -289,7 +294,7 @@ public class TaskManager
|
||||
{
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Shutting down (forced={force})");
|
||||
_continueRunning = false;
|
||||
ExportData();
|
||||
ExportDataAndSettings();
|
||||
|
||||
if(force)
|
||||
Environment.Exit(_allTasks.Count(task => task.state is TrangaTask.ExecutionState.Enqueued or TrangaTask.ExecutionState.Running));
|
||||
@ -309,7 +314,7 @@ public class TaskManager
|
||||
{
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Importing tasks from {settings.tasksFilePath}");
|
||||
buffer = File.ReadAllText(settings.tasksFilePath);
|
||||
this._allTasks = JsonConvert.DeserializeObject<HashSet<TrangaTask>>(buffer)!;
|
||||
this._allTasks = JsonConvert.DeserializeObject<HashSet<TrangaTask>>(buffer, new JsonSerializerSettings() { Converters = { new TrangaTask.TrangaTaskJsonConverter() } })!;
|
||||
}
|
||||
|
||||
if (File.Exists(settings.knownPublicationsPath))
|
||||
@ -318,14 +323,14 @@ public class TaskManager
|
||||
buffer = File.ReadAllText(settings.knownPublicationsPath);
|
||||
Publication[] publications = JsonConvert.DeserializeObject<Publication[]>(buffer)!;
|
||||
foreach (Publication publication in publications)
|
||||
this._chapterCollection.TryAdd(publication, new List<Chapter>());
|
||||
this.chapterCollection.TryAdd(publication, new List<Chapter>());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exports data (settings, tasks) to file
|
||||
/// </summary>
|
||||
private void ExportData()
|
||||
private void ExportDataAndSettings()
|
||||
{
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Exporting settings to {settings.settingsFilePath}");
|
||||
File.WriteAllText(settings.settingsFilePath, JsonConvert.SerializeObject(settings));
|
||||
@ -334,7 +339,7 @@ public class TaskManager
|
||||
File.WriteAllText(settings.tasksFilePath, JsonConvert.SerializeObject(this._allTasks));
|
||||
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Exporting known publications to {settings.knownPublicationsPath}");
|
||||
File.WriteAllText(settings.knownPublicationsPath, JsonConvert.SerializeObject(this._chapterCollection.Keys.ToArray()));
|
||||
File.WriteAllText(settings.knownPublicationsPath, JsonConvert.SerializeObject(this.chapterCollection.Keys.ToArray()));
|
||||
}
|
||||
|
||||
|
||||
|
@ -7,6 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.46" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -9,10 +9,13 @@ public class TrangaSettings
|
||||
[JsonIgnore]public string settingsFilePath => Path.Join(workingDirectory, "settings.json");
|
||||
[JsonIgnore]public string tasksFilePath => Path.Join(workingDirectory, "tasks.json");
|
||||
[JsonIgnore]public string knownPublicationsPath => Path.Join(workingDirectory, "knownPublications.json");
|
||||
[JsonIgnore] public string coverImageCache => Path.Join(workingDirectory, "imageCache");
|
||||
public Komga? komga { get; set; }
|
||||
|
||||
public TrangaSettings(string downloadLocation, string workingDirectory, Komga? komga)
|
||||
{
|
||||
if (downloadLocation.Length < 1 || workingDirectory.Length < 1)
|
||||
throw new ArgumentException("Download-location and working-directory paths can not be empty!");
|
||||
this.workingDirectory = workingDirectory;
|
||||
this.downloadLocation = downloadLocation;
|
||||
this.komga = komga;
|
||||
|
@ -1,11 +1,14 @@
|
||||
using Newtonsoft.Json;
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Tranga.TrangaTasks;
|
||||
|
||||
namespace Tranga;
|
||||
|
||||
/// <summary>
|
||||
/// Stores information on Task
|
||||
/// Stores information on Task, when implementing new Tasks also update the serializer
|
||||
/// </summary>
|
||||
public class TrangaTask
|
||||
public abstract class TrangaTask
|
||||
{
|
||||
// ReSharper disable once CommentTypo ...Tell me why!
|
||||
// ReSharper disable once MemberCanBePrivate.Global I want it thaaat way
|
||||
@ -14,7 +17,7 @@ public class TrangaTask
|
||||
public string? connectorName { get; }
|
||||
public Task task { get; }
|
||||
public Publication? publication { get; }
|
||||
public string language { get; }
|
||||
public string? language { get; }
|
||||
[JsonIgnore]public ExecutionState state { get; set; }
|
||||
|
||||
public enum ExecutionState
|
||||
@ -24,14 +27,8 @@ public class TrangaTask
|
||||
Running
|
||||
};
|
||||
|
||||
public TrangaTask(Task task, string? connectorName, Publication? publication, TimeSpan reoccurrence, string language = "")
|
||||
protected TrangaTask(Task task, string? connectorName, Publication? publication, TimeSpan reoccurrence, string? language = null)
|
||||
{
|
||||
if(task != Task.UpdateKomgaLibrary && connectorName is null)
|
||||
throw new ArgumentException($"connectorName can not be null for task {task}");
|
||||
|
||||
if (publication is null && task != Task.UpdatePublications && task != Task.UpdateKomgaLibrary)
|
||||
throw new ArgumentException($"Publication can not be null for task {task}");
|
||||
|
||||
this.publication = publication;
|
||||
this.reoccurrence = reoccurrence;
|
||||
this.lastExecuted = DateTime.Now.Subtract(reoccurrence);
|
||||
@ -40,22 +37,77 @@ public class TrangaTask
|
||||
this.language = language;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BL for concrete Tasks
|
||||
/// </summary>
|
||||
/// <param name="taskManager"></param>
|
||||
/// <param name="logger"></param>
|
||||
protected abstract void ExecuteTask(TaskManager taskManager, Logger? logger);
|
||||
|
||||
/// <summary>
|
||||
/// Execute the task
|
||||
/// </summary>
|
||||
/// <param name="taskManager">Should be the parent taskManager</param>
|
||||
/// <param name="logger"></param>
|
||||
public void Execute(TaskManager taskManager, Logger? logger)
|
||||
{
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Executing Task {this}");
|
||||
this.state = ExecutionState.Running;
|
||||
ExecuteTask(taskManager, logger);
|
||||
this.lastExecuted = DateTime.Now;
|
||||
this.state = ExecutionState.Waiting;
|
||||
logger?.WriteLine(this.GetType().ToString(), $"Finished Executing Task {this}");
|
||||
|
||||
}
|
||||
|
||||
/// <returns>True if elapsed time since last execution is greater than set interval</returns>
|
||||
public bool ShouldExecute()
|
||||
{
|
||||
return DateTime.Now.Subtract(this.lastExecuted) > reoccurrence && state is ExecutionState.Waiting;
|
||||
}
|
||||
|
||||
public enum Task
|
||||
public enum Task : byte
|
||||
{
|
||||
UpdatePublications,
|
||||
UpdateChapters,
|
||||
DownloadNewChapters,
|
||||
UpdateKomgaLibrary
|
||||
DownloadNewChapters = 2,
|
||||
UpdateKomgaLibrary = 3
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{task}, {lastExecuted}, {reoccurrence}, {state} {(connectorName is not null ? $", {connectorName}" : "" )} {(publication is not null ? $", {publication?.sortName}": "")}";
|
||||
}
|
||||
|
||||
public class TrangaTaskJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return (objectType == typeof(TrangaTask));
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
JObject jo = JObject.Load(reader);
|
||||
if (jo["task"]!.Value<Int64>() == (Int64)Task.DownloadNewChapters)
|
||||
return jo.ToObject<DownloadNewChaptersTask>(serializer)!;
|
||||
|
||||
if (jo["task"]!.Value<Int64>() == (Int64)Task.UpdateKomgaLibrary)
|
||||
return jo.ToObject<UpdateKomgaLibraryTask>(serializer)!;
|
||||
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
public override bool CanWrite => false;
|
||||
|
||||
/// <summary>
|
||||
/// Don't call this
|
||||
/// </summary>
|
||||
/// <param name="writer"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="serializer"></param>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
throw new Exception("Dont call this");
|
||||
}
|
||||
}
|
||||
}
|
48
Tranga/TrangaTasks/DownloadNewChaptersTask.cs
Normal file
@ -0,0 +1,48 @@
|
||||
using Logging;
|
||||
|
||||
namespace Tranga.TrangaTasks;
|
||||
|
||||
public class DownloadNewChaptersTask : TrangaTask
|
||||
{
|
||||
public DownloadNewChaptersTask(Task task, string connectorName, Publication publication, TimeSpan reoccurrence, string language = "en") : base(task, connectorName, publication, reoccurrence, language)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void ExecuteTask(TaskManager taskManager, Logger? logger)
|
||||
{
|
||||
Publication pub = (Publication)this.publication!;
|
||||
Connector connector = taskManager.GetConnector(this.connectorName);
|
||||
|
||||
//Check if Publication already has a Folder
|
||||
string publicationFolder = Path.Join(connector.downloadLocation, pub.folderName);
|
||||
if(!Directory.Exists(publicationFolder))
|
||||
Directory.CreateDirectory(publicationFolder);
|
||||
List<Chapter> newChapters = UpdateChapters(connector, pub, language!, ref taskManager.chapterCollection);
|
||||
|
||||
connector.CopyCoverFromCacheToDownloadLocation(pub, taskManager.settings);
|
||||
|
||||
pub.SaveSeriesInfoJson(connector.downloadLocation);
|
||||
|
||||
foreach(Chapter newChapter in newChapters)
|
||||
connector.DownloadChapter(pub, newChapter);
|
||||
}
|
||||
|
||||
/// <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> UpdateChapters(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;
|
||||
}
|
||||
}
|
21
Tranga/TrangaTasks/UpdateKomgaLibraryTask.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using Logging;
|
||||
|
||||
namespace Tranga.TrangaTasks;
|
||||
|
||||
public class UpdateKomgaLibraryTask : TrangaTask
|
||||
{
|
||||
public UpdateKomgaLibraryTask(Task task, TimeSpan reoccurrence) : base(task, null, null, reoccurrence)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void ExecuteTask(TaskManager taskManager, Logger? logger)
|
||||
{
|
||||
if (taskManager.komga is null)
|
||||
return;
|
||||
Komga komga = taskManager.komga;
|
||||
|
||||
Komga.KomgaLibrary[] allLibraries = komga.GetLibraries();
|
||||
foreach (Komga.KomgaLibrary lib in allLibraries)
|
||||
komga.UpdateLibrary(lib.id);
|
||||
}
|
||||
}
|
4
Website/Dockerfile
Normal file
@ -0,0 +1,4 @@
|
||||
FROM nginx:alpine3.17-slim
|
||||
COPY . /usr/share/nginx/html
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
126
Website/apiConnector.js
Normal file
@ -0,0 +1,126 @@
|
||||
let apiUri = `http://${window.location.host.split(':')[0]}:6531`
|
||||
|
||||
if(getCookie("apiUri") != ""){
|
||||
apiUri = getCookie("apiUri");
|
||||
}
|
||||
function getCookie(cname) {
|
||||
let name = cname + "=";
|
||||
let decodedCookie = decodeURIComponent(document.cookie);
|
||||
let ca = decodedCookie.split(';');
|
||||
for(let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) == ' ') {
|
||||
c = c.substring(1);
|
||||
}
|
||||
if (c.indexOf(name) == 0) {
|
||||
return c.substring(name.length, c.length);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
async function GetData(uri){
|
||||
let request = await fetch(uri, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
let json = await request.json();
|
||||
return json;
|
||||
}
|
||||
|
||||
function PostData(uri){
|
||||
fetch(uri, {
|
||||
method: 'POST'
|
||||
});
|
||||
}
|
||||
|
||||
function DeleteData(uri){
|
||||
fetch(uri, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
||||
|
||||
async function GetAvailableControllers(){
|
||||
var uri = apiUri + "/Tranga/GetAvailableControllers";
|
||||
let json = await GetData(uri);
|
||||
return json;
|
||||
}
|
||||
|
||||
async function GetPublication(connectorName, title){
|
||||
var uri = apiUri + `/Tranga/GetPublicationsFromConnector?connectorName=${connectorName}&title=${title}`;
|
||||
let json = await GetData(uri);
|
||||
return json;
|
||||
}
|
||||
|
||||
async function GetKnownPublications(){
|
||||
var uri = apiUri + "/Tranga/GetKnownPublications";
|
||||
let json = await GetData(uri);
|
||||
return json;
|
||||
}
|
||||
|
||||
async function GetTaskTypes(){
|
||||
var uri = apiUri + "/Tasks/GetTaskTypes";
|
||||
let json = await GetData(uri);
|
||||
return json;
|
||||
}
|
||||
async function GetRunningTasks(){
|
||||
var uri = apiUri + "/Tasks/GetRunningTasks";
|
||||
let json = await GetData(uri);
|
||||
return json;
|
||||
}
|
||||
|
||||
async function GetDownloadTasks(){
|
||||
var uri = apiUri + "/Tasks/Get?taskType=DownloadNewChapters";
|
||||
let json = await GetData(uri);
|
||||
return json;
|
||||
}
|
||||
|
||||
async function GetSettings(){
|
||||
var uri = apiUri + "/Settings/Get";
|
||||
let json = await GetData(uri);
|
||||
return json;
|
||||
}
|
||||
|
||||
async function GetKomgaTask(){
|
||||
var uri = apiUri + "/Tasks/Get?taskType=UpdateKomgaLibrary";
|
||||
let json = await GetData(uri);
|
||||
return json;
|
||||
}
|
||||
|
||||
function CreateTask(taskType, reoccurrence, connectorName, publicationId, language){
|
||||
var uri = apiUri + `/Tasks/Create?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}&reoccurrenceTime=${reoccurrence}&language=${language}`;
|
||||
PostData(uri);
|
||||
}
|
||||
|
||||
function StartTask(taskType, connectorName, publicationId){
|
||||
var uri = apiUri + `/Tasks/Start?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}`;
|
||||
PostData(uri);
|
||||
}
|
||||
|
||||
function EnqueueTask(taskType, connectorName, publicationId){
|
||||
var uri = apiUri + `/Queue/Enqueue?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}`;
|
||||
PostData(uri);
|
||||
}
|
||||
|
||||
function UpdateSettings(downloadLocation, komgaUrl, komgaAuth){
|
||||
var uri = apiUri + `/Settings/Update?downloadLocation=${downloadLocation}&komgaUrl=${komgaUrl}&komgaAuth=${komgaAuth}`;
|
||||
PostData(uri);
|
||||
}
|
||||
|
||||
function DeleteTask(taskType, connectorName, publicationId){
|
||||
var uri = apiUri + `/Tasks/Delete?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}`;
|
||||
DeleteData(uri);
|
||||
}
|
||||
|
||||
function DequeueTask(taskType, connectorName, publicationId){
|
||||
var uri = apiUri + `/Queue/Dequeue?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}`;
|
||||
DeleteData(uri);
|
||||
}
|
||||
|
||||
async function GetQueue(){
|
||||
var uri = apiUri + "/Queue/GetList";
|
||||
let json = await GetData(uri);
|
||||
return json;
|
||||
}
|
BIN
Website/favicon.ico
Normal file
After Width: | Height: | Size: 66 KiB |
125
Website/index.html
Normal file
@ -0,0 +1,125 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Tranga</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<wrapper>
|
||||
<topbar>
|
||||
<titlebox>
|
||||
<img alt="website image is Blahaj" src="media/blahaj.png">
|
||||
<span>Tranga</span>
|
||||
</titlebox>
|
||||
<spacer></spacer>
|
||||
<searchdiv>
|
||||
<label for="searchbox"></label><input id="searchbox" placeholder="Filter" type="text">
|
||||
</searchdiv>
|
||||
<img id="settingscog" src="media/settings-cogwheel.svg" height="100%" alt="settingscog">
|
||||
</topbar>
|
||||
<viewport>
|
||||
<content>
|
||||
<div id="addPublication">
|
||||
<p>+</p>
|
||||
</div>
|
||||
<publication>
|
||||
<img src="media/cover.jpg">
|
||||
<publication-information>
|
||||
<connector-name class="pill">MangaDex</connector-name>
|
||||
<publication-name>Tensei Pandemic</publication-name>
|
||||
</publication-information>
|
||||
</publication>
|
||||
</content>
|
||||
|
||||
<popup id="addTaskPopup">
|
||||
<blur-background id="blurBackgroundTaskPopup"></blur-background>
|
||||
<addtask-window>
|
||||
<window-titlebar>
|
||||
<p>Add Task</p>
|
||||
<img id="closePopupImg" src="media/close-x.svg" alt="Close">
|
||||
</window-titlebar>
|
||||
<window-content>
|
||||
<addtask-settings>
|
||||
<addtask-setting><label for="selectReccurrence">Recurrence</label><input id="selectReccurrence" type="time" value="01:00:00" step="3600"></addtask-setting>
|
||||
<addtask-setting><label for="connectors">Connector</label>
|
||||
<select id="connectors">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</addtask-setting>
|
||||
<addtask-setting><label for="searchPublicationQuery">Search Title</label><input id="searchPublicationQuery" type="text"></addtask-setting>
|
||||
<input type="submit" value="Search" onclick="NewSearch();">
|
||||
</addtask-settings>
|
||||
<div id="taskSelectOutput"></div>
|
||||
</window-content>
|
||||
</addtask-window>
|
||||
</popup>
|
||||
<popup id="publicationViewerPopup">
|
||||
<blur-background id="blurBackgroundPublicationPopup"></blur-background>
|
||||
<publication-viewer>
|
||||
<img id="pubviewcover" src="media/cover.jpg" alt="cover">
|
||||
<publication-information>
|
||||
<publication-name id="publicationViewerName">Tensei Pandemic</publication-name>
|
||||
<publication-tags id="publicationViewerTags"></publication-tags>
|
||||
<publication-author id="publicationViewerAuthor">Imamura Hinata</publication-author>
|
||||
<publication-description id="publicationViewerDescription">Imamura Hinata is a high school boy with a cute appearance.
|
||||
Since his trauma with the first love, he wanted to be more manly than anybody else. But one day he woke up to something different…
|
||||
The total opposite of his ideal male body!
|
||||
Pandemic love comedy!
|
||||
</publication-description>
|
||||
<publication-interactions>
|
||||
<publication-starttask>Start Task ▶️</publication-starttask>
|
||||
<publication-delete>Delete Task ❌</publication-delete>
|
||||
<publication-add>Add Task ➕</publication-add>
|
||||
</publication-interactions>
|
||||
</publication-information>
|
||||
</publication-viewer>
|
||||
</popup>
|
||||
<popup id="settingsPopup">
|
||||
<blur-background id="blurBackgroundSettingsPopup"></blur-background>
|
||||
<settings>
|
||||
<span style="font-weight: bold; text-align: center; font-size: 16pt;">Settings</span>
|
||||
<div>
|
||||
<p class="title">Download Location:</p>
|
||||
<span id="downloadLocation"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="title">API-URI</p>
|
||||
<label for="settingApiUri"></label><input placeholder="https://" type="text" id="settingApiUri">
|
||||
</div>
|
||||
<komga-settings>
|
||||
<span class="title">Komga</span>
|
||||
<div>Configured: <span id="komgaConfigured">✅❌</span></div>
|
||||
<label for="komgaUrl"></label><input placeholder="URL" id="komgaUrl" type="text">
|
||||
<label for="komgaUsername"></label><input placeholder="Username" id="komgaUsername" type="text">
|
||||
<label for="komgaPassword"></label><input placeholder="Password" id="komgaPassword" type="password">
|
||||
<label for="komgaUpdateTime" style="margin-right: 5px;">Update Time</label><input id="komgaUpdateTime" type="time" value="00:01:00" step="10">
|
||||
<input type="submit" value="Update" onclick="UpdateKomgaSettings()">
|
||||
</komga-settings>
|
||||
</settings>
|
||||
</popup>
|
||||
</viewport>
|
||||
<footer>
|
||||
<div>
|
||||
<img src="media/running.svg" alt="running"><div id="tasksRunningTag">0</div>
|
||||
</div>
|
||||
<div>
|
||||
<img src="media/queue.svg" alt="queue"><div id="tasksQueuedTag">0</div>
|
||||
</div>
|
||||
<div>
|
||||
<img src="media/tasks.svg" alt="queue"><div id="totalTasksTag">0</div>
|
||||
</div>
|
||||
<p id="madeWith">Made with Blåhaj 🦈</p>
|
||||
</footer>
|
||||
</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="interaction.js"></script>
|
||||
</body>
|
||||
</html>
|
423
Website/interaction.js
Normal file
@ -0,0 +1,423 @@
|
||||
let publications = [];
|
||||
let tasks = [];
|
||||
let toEditId;
|
||||
|
||||
const searchBox = document.querySelector("#searchbox");
|
||||
const searchPublicationQuery = document.querySelector("#searchPublicationQuery");
|
||||
const selectPublication = document.querySelector("#taskSelectOutput");
|
||||
const connectorSelect = document.querySelector("#connectors");
|
||||
const settingsPopup = document.querySelector("#settingsPopup");
|
||||
const settingsCog = document.querySelector("#settingscog");
|
||||
const selectRecurrence = document.querySelector("#selectReccurrence");
|
||||
const tasksContent = document.querySelector("content");
|
||||
const addTaskPopup = document.querySelector("#addTaskPopup");
|
||||
const publicationViewerPopup = document.querySelector("#publicationViewerPopup");
|
||||
const publicationViewerWindow = document.querySelector("publication-viewer");
|
||||
const publicationViewerDescription = document.querySelector("#publicationViewerDescription");
|
||||
const publicationViewerName = document.querySelector("#publicationViewerName");
|
||||
const publicationViewerTags = document.querySelector("#publicationViewerTags");
|
||||
const publicationViewerAuthor = document.querySelector("#publicationViewerAuthor");
|
||||
const pubviewcover = document.querySelector("#pubviewcover");
|
||||
const publicationDelete = document.querySelector("publication-delete");
|
||||
const publicationAdd = document.querySelector("publication-add");
|
||||
const publicationTaskStart = document.querySelector("publication-starttask");
|
||||
const closetaskpopup = document.querySelector("#closePopupImg");
|
||||
const settingDownloadLocation = document.querySelector("#downloadLocation");
|
||||
const settingKomgaUrl = document.querySelector("#komgaUrl");
|
||||
const settingKomgaUser = document.querySelector("#komgaUsername");
|
||||
const settingKomgaPass = document.querySelector("#komgaPassword");
|
||||
const settingKomgaTime = document.querySelector("#komgaUpdateTime");
|
||||
const settingKomgaConfigured = document.querySelector("#komgaConfigured");
|
||||
const settingApiUri = document.querySelector("#settingApiUri");
|
||||
const tagTasksRunning = document.querySelector("#tasksRunningTag");
|
||||
const tagTasksQueued = document.querySelector("#tasksQueuedTag");
|
||||
const tagTasksTotal = document.querySelector("#totalTasksTag");
|
||||
const tagTaskPopup = document.querySelector("footer-tag-popup");
|
||||
const tagTasksPopupContent = document.querySelector("footer-tag-content");
|
||||
|
||||
searchbox.addEventListener("keyup", (event) => FilterResults());
|
||||
settingsCog.addEventListener("click", () => OpenSettings());
|
||||
document.querySelector("#blurBackgroundSettingsPopup").addEventListener("click", () => HideSettings());
|
||||
closetaskpopup.addEventListener("click", () => HideAddTaskPopup());
|
||||
document.querySelector("#blurBackgroundTaskPopup").addEventListener("click", () => HideAddTaskPopup());
|
||||
document.querySelector("#blurBackgroundPublicationPopup").addEventListener("click", () => HidePublicationPopup());
|
||||
publicationDelete.addEventListener("click", () => DeleteTaskClick());
|
||||
publicationAdd.addEventListener("click", () => AddTaskClick());
|
||||
publicationTaskStart.addEventListener("click", () => StartTaskClick());
|
||||
settingApiUri.addEventListener("keypress", (event) => {
|
||||
if(event.key === "Enter"){
|
||||
apiUri = settingApiUri.value;
|
||||
setTimeout(() => GetSettingsClick(), 100);
|
||||
document.cookie = `apiUri=${apiUri};`;
|
||||
}
|
||||
});
|
||||
searchPublicationQuery.addEventListener("keypress", (event) => {
|
||||
if(event.key === "Enter"){
|
||||
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;
|
||||
GetAvailableControllers()
|
||||
.then(json => availableConnectors = json)
|
||||
.then(json =>
|
||||
json.forEach(connector => {
|
||||
var option = document.createElement('option');
|
||||
option.value = connector;
|
||||
option.innerText = connector;
|
||||
connectorSelect.appendChild(option);
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
function NewSearch(){
|
||||
//Disable inputs
|
||||
selectRecurrence.disabled = true;
|
||||
connectorSelect.disabled = true;
|
||||
searchPublicationQuery.disabled = true;
|
||||
//Waitcursor
|
||||
document.body.style.cursor = "wait";
|
||||
selectRecurrence.style.cursor = "wait";
|
||||
connectorSelect.style.cursor = "wait";
|
||||
searchPublicationQuery.style.cursor = "wait";
|
||||
|
||||
//Empty previous results
|
||||
selectPublication.replaceChildren();
|
||||
GetPublication(connectorSelect.value, searchPublicationQuery.value)
|
||||
.then(json =>
|
||||
json.forEach(publication => {
|
||||
var option = CreatePublication(publication, connectorSelect.value);
|
||||
option.addEventListener("click", (mouseEvent) => {
|
||||
ShowPublicationViewerWindow(publication.internalId, mouseEvent, true);
|
||||
});
|
||||
selectPublication.appendChild(option);
|
||||
}
|
||||
))
|
||||
.then(() => {
|
||||
//Re-enable inputs
|
||||
selectRecurrence.disabled = false;
|
||||
connectorSelect.disabled = false;
|
||||
searchPublicationQuery.disabled = false;
|
||||
//Cursor
|
||||
document.body.style.cursor = "initial";
|
||||
selectRecurrence.style.cursor = "initial";
|
||||
connectorSelect.style.cursor = "initial";
|
||||
searchPublicationQuery.style.cursor = "initial";
|
||||
});
|
||||
}
|
||||
|
||||
//Returns a new "Publication" Item to display in the tasks section
|
||||
function CreatePublication(publication, connector){
|
||||
var publicationElement = document.createElement('publication');
|
||||
publicationElement.setAttribute("id", publication.internalId);
|
||||
var img = document.createElement('img');
|
||||
img.src = `imageCache/${publication.coverFileNameInCache}`;
|
||||
publicationElement.appendChild(img);
|
||||
var info = document.createElement('publication-information');
|
||||
var connectorName = document.createElement('connector-name');
|
||||
connectorName.innerText = connector;
|
||||
connectorName.className = "pill";
|
||||
info.appendChild(connectorName);
|
||||
var publicationName = document.createElement('publication-name');
|
||||
publicationName.innerText = publication.sortName;
|
||||
info.appendChild(publicationName);
|
||||
publicationElement.appendChild(info);
|
||||
if(publications.filter(pub => pub.internalId === publication.internalId) < 1)
|
||||
publications.push(publication);
|
||||
return publicationElement;
|
||||
}
|
||||
|
||||
function DeleteTaskClick(){
|
||||
taskToDelete = tasks.filter(tTask => tTask.publication.internalId === toEditId)[0];
|
||||
DeleteTask("DownloadNewChapters", taskToDelete.connectorName, toEditId);
|
||||
HidePublicationPopup();
|
||||
}
|
||||
|
||||
function AddTaskClick(){
|
||||
CreateTask("DownloadNewChapters", selectRecurrence.value, connectorSelect.value, toEditId, "en")
|
||||
HideAddTaskPopup();
|
||||
HidePublicationPopup();
|
||||
}
|
||||
|
||||
function StartTaskClick(){
|
||||
var toEditTask = tasks.filter(task => task.publication.internalId == toEditId)[0];
|
||||
StartTask("DownloadNewChapters", toEditTask.connectorName, toEditId);
|
||||
HidePublicationPopup();
|
||||
}
|
||||
|
||||
function ResetContent(){
|
||||
//Delete everything
|
||||
tasksContent.replaceChildren();
|
||||
|
||||
//Add "Add new Task" Button
|
||||
var add = document.createElement("div");
|
||||
add.setAttribute("id", "addPublication")
|
||||
var plus = document.createElement("p");
|
||||
plus.innerText = "+";
|
||||
add.appendChild(plus);
|
||||
add.addEventListener("click", () => ShowNewTaskWindow());
|
||||
tasksContent.appendChild(add);
|
||||
}
|
||||
function ShowPublicationViewerWindow(publicationId, event, add){
|
||||
//Show popup
|
||||
publicationViewerPopup.style.display = "block";
|
||||
|
||||
//Set position to mouse-position
|
||||
if(event.clientY < window.innerHeight - publicationViewerWindow.offsetHeight)
|
||||
publicationViewerWindow.style.top = `${event.clientY}px`;
|
||||
else
|
||||
publicationViewerWindow.style.top = `${event.clientY - publicationViewerWindow.offsetHeight}px`;
|
||||
|
||||
if(event.clientX < window.innerWidth - publicationViewerWindow.offsetWidth)
|
||||
publicationViewerWindow.style.left = `${event.clientX}px`;
|
||||
else
|
||||
publicationViewerWindow.style.left = `${event.clientX - publicationViewerWindow.offsetWidth}px`;
|
||||
|
||||
//Edit information inside the window
|
||||
var publication = publications.filter(pub => pub.internalId === publicationId)[0];
|
||||
publicationViewerName.innerText = publication.sortName;
|
||||
publicationViewerTags.innerText = publication.tags.join(", ");
|
||||
publicationViewerDescription.innerText = publication.description;
|
||||
publicationViewerAuthor.innerText = publication.author;
|
||||
pubviewcover.src = `imageCache/${publication.coverFileNameInCache}`;
|
||||
toEditId = publicationId;
|
||||
|
||||
//Check what action should be listed
|
||||
if(add){
|
||||
publicationAdd.style.display = "initial";
|
||||
publicationDelete.style.display = "none";
|
||||
publicationTaskStart.style.display = "none";
|
||||
}
|
||||
else{
|
||||
publicationAdd.style.display = "none";
|
||||
publicationDelete.style.display = "initial";
|
||||
publicationTaskStart.style.display = "initial";
|
||||
}
|
||||
}
|
||||
|
||||
function HidePublicationPopup(){
|
||||
publicationViewerPopup.style.display = "none";
|
||||
}
|
||||
|
||||
function ShowNewTaskWindow(){
|
||||
selectPublication.replaceChildren();
|
||||
addTaskPopup.style.display = "block";
|
||||
}
|
||||
function HideAddTaskPopup(){
|
||||
addTaskPopup.style.display = "none";
|
||||
}
|
||||
|
||||
|
||||
const fadeIn = [
|
||||
{ opacity: "0" },
|
||||
{ opacity: "1" }
|
||||
];
|
||||
|
||||
const fadeInTiming = {
|
||||
duration: 50,
|
||||
iterations: 1,
|
||||
fill: "forwards"
|
||||
}
|
||||
|
||||
function OpenSettings(){
|
||||
GetSettingsClick();
|
||||
settingsPopup.style.display = "flex";
|
||||
}
|
||||
|
||||
function HideSettings(){
|
||||
settingsPopup.style.display = "none";
|
||||
}
|
||||
|
||||
function GetSettingsClick(){
|
||||
settingApiUri.value = "";
|
||||
settingKomgaUrl.value = "";
|
||||
settingKomgaUser.value = "";
|
||||
settingKomgaPass.value = "";
|
||||
|
||||
settingApiUri.placeholder = apiUri;
|
||||
|
||||
GetSettings().then(json => {
|
||||
settingDownloadLocation.innerText = json.downloadLocation;
|
||||
if(json.komga != null) {
|
||||
settingKomgaUrl.placeholder = json.komga.baseUrl;
|
||||
settingKomgaUser.placeholder = "Configured";
|
||||
settingKomgaPass.placeholder = "***";
|
||||
}
|
||||
});
|
||||
|
||||
GetKomgaTask().then(json => {
|
||||
settingKomgaTime.value = json[0].reoccurrence;
|
||||
if(json.length > 0)
|
||||
settingKomgaConfigured.innerText = "✅";
|
||||
else
|
||||
settingKomgaConfigured.innerText = "❌";
|
||||
});
|
||||
}
|
||||
|
||||
function UpdateKomgaSettings(){
|
||||
if(settingKomgaUser.value != "" && settingKomgaPass != ""){
|
||||
var auth = utf8_to_b64(`${settingKomgaUser.value}:${settingKomgaPass.value}`);
|
||||
console.log(auth);
|
||||
|
||||
if(settingKomgaUrl.value != "")
|
||||
UpdateSettings("", settingKomgaUrl.value, auth);
|
||||
else
|
||||
UpdateSettings("", settingKomgaUrl.placeholder, auth);
|
||||
}
|
||||
CreateTask("UpdateKomgaLibrary", settingKomgaTime.value, "","","");
|
||||
setTimeout(() => GetSettingsClick(), 100);
|
||||
}
|
||||
|
||||
function utf8_to_b64( str ) {
|
||||
return window.btoa(unescape(encodeURIComponent( str )));
|
||||
}
|
||||
|
||||
|
||||
function ShowRunningTasks(event){
|
||||
GetRunningTasks()
|
||||
.then(json => {
|
||||
tagTasksPopupContent.replaceChildren();
|
||||
json.forEach(task => {
|
||||
console.log(task);
|
||||
if(task.publication != null){
|
||||
var taskname = document.createElement("footer-tag-task-name");
|
||||
taskname.innerText = task.publication.sortName;
|
||||
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");
|
||||
taskname.innerText = task.publication.sortName;
|
||||
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(){
|
||||
if(searchBox.value.length > 0){
|
||||
tasksContent.childNodes.forEach(publication => {
|
||||
publication.childNodes.forEach(item => {
|
||||
if(item.nodeName.toLowerCase() == "publication-information"){
|
||||
item.childNodes.forEach(information => {
|
||||
if(information.nodeName.toLowerCase() == "publication-name"){
|
||||
if(!information.textContent.toLowerCase().includes(searchBox.value.toLowerCase())){
|
||||
publication.style.display = "none";
|
||||
}else{
|
||||
publication.style.display = "initial";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}else{
|
||||
tasksContent.childNodes.forEach(publication => publication.style.display = "initial");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
//Resets the tasks shown
|
||||
ResetContent();
|
||||
//Get Tasks and show them
|
||||
GetDownloadTasks()
|
||||
.then(json => json.forEach(task => {
|
||||
var publication = CreatePublication(task.publication, task.connectorName);
|
||||
publication.addEventListener("click", (event) => ShowPublicationViewerWindow(task.publication.internalId, event, false));
|
||||
tasksContent.appendChild(publication);
|
||||
tasks.push(task);
|
||||
}));
|
||||
|
||||
GetRunningTasks()
|
||||
.then(json => {
|
||||
tagTasksRunning.innerText = json.length;
|
||||
});
|
||||
|
||||
GetDownloadTasks()
|
||||
.then(json => {
|
||||
tagTasksTotal.innerText = json.length;
|
||||
});
|
||||
|
||||
GetQueue()
|
||||
.then(json => {
|
||||
tagTasksQueued.innerText = json.length;
|
||||
})
|
||||
|
||||
setInterval(() => {
|
||||
//Tasks from API
|
||||
var cTasks = [];
|
||||
GetDownloadTasks()
|
||||
.then(json => json.forEach(task => cTasks.push(task)))
|
||||
.then(() => {
|
||||
//Only update view if tasks-amount has changed
|
||||
if(tasks.length != cTasks.length) {
|
||||
//Resets the tasks shown
|
||||
ResetContent();
|
||||
//Add all currenttasks to view
|
||||
cTasks.forEach(task => {
|
||||
var publication = CreatePublication(task.publication, task.connectorName);
|
||||
publication.addEventListener("click", (event) => ShowPublicationViewerWindow(task.publication.internalId, event, false));
|
||||
tasksContent.appendChild(publication);
|
||||
})
|
||||
|
||||
tasks = cTasks;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
GetRunningTasks()
|
||||
.then(json => {
|
||||
tagTasksRunning.innerText = json.length;
|
||||
});
|
||||
|
||||
GetDownloadTasks()
|
||||
.then(json => {
|
||||
tagTasksTotal.innerText = json.length;
|
||||
});
|
||||
|
||||
GetQueue()
|
||||
.then(json => {
|
||||
tagTasksQueued.innerText = json.length;
|
||||
})
|
||||
|
||||
}, 1000);
|
BIN
Website/media/blahaj.png
Normal file
After Width: | Height: | Size: 124 KiB |
4
Website/media/close-x.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.29289 5.29289C5.68342 4.90237 6.31658 4.90237 6.70711 5.29289L12 10.5858L17.2929 5.29289C17.6834 4.90237 18.3166 4.90237 18.7071 5.29289C19.0976 5.68342 19.0976 6.31658 18.7071 6.70711L13.4142 12L18.7071 17.2929C19.0976 17.6834 19.0976 18.3166 18.7071 18.7071C18.3166 19.0976 17.6834 19.0976 17.2929 18.7071L12 13.4142L6.70711 18.7071C6.31658 19.0976 5.68342 19.0976 5.29289 18.7071C4.90237 18.3166 4.90237 17.6834 5.29289 17.2929L10.5858 12L5.29289 6.70711C4.90237 6.31658 4.90237 5.68342 5.29289 5.29289Z" fill="#0F1729"/>
|
||||
</svg>
|
After Width: | Height: | Size: 804 B |
7
Website/media/queue.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
|
||||
|
||||
<g fill="#000000">
|
||||
|
After Width: | Height: | Size: 545 B |
53
Website/media/running.svg
Normal file
@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="800px" height="800px" viewBox="0 0 235.504 235.504"
|
||||
xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M195.209,81.456l-49.227-0.15c0.737-0.886,1.351-1.868,2.284-2.583c3.282-2.497,3.911-7.166,1.427-10.438
|
||||
c-2.501-3.266-7.161-3.919-10.443-1.423c-4.873,3.715-8.388,8.704-10.255,14.389l-22.191-0.064
|
||||
c-9.508,0-19.588,7.398-22.938,16.851l-16.877,47.479c-1.775,5.013-1.338,9.966,1.207,13.568
|
||||
c2.412,3.427,6.384,5.318,11.187,5.358l45.126,0.136c-1.509,5.186-4.701,9.622-9.352,12.424
|
||||
c-4.891,2.957-10.636,3.814-16.172,2.444c-3.994-0.998-8.031,1.442-9.027,5.418c-0.99,4.012,1.445,8.035,5.432,9.032
|
||||
c2.927,0.738,5.879,1.091,8.808,1.091c6.516,0,12.93-1.788,18.645-5.23c8.312-5.013,14.172-12.979,16.484-22.409
|
||||
c0.232-0.905,0.232-1.823,0.124-2.713l28.296,0.092h0.049c2.925,0,5.854-0.89,8.684-2.147c0.2,0.493,0.32,1.014,0.661,1.471
|
||||
c3.335,4.677,4.629,10.343,3.688,15.993c-0.95,5.627-4.028,10.536-8.688,13.862c-3.351,2.376-4.14,7.037-1.755,10.379
|
||||
c1.466,2.04,3.751,3.122,6.062,3.122c1.491,0,3.006-0.429,4.312-1.367c7.919-5.61,13.16-13.966,14.771-23.52
|
||||
c1.603-9.565-0.613-19.203-6.28-27.122c-0.48-0.693-1.134-1.19-1.779-1.659c1.318-1.831,2.501-3.763,3.238-5.854l16.863-47.464
|
||||
c1.795-5.018,1.351-9.969-1.194-13.58C203.954,83.387,200.015,81.47,195.209,81.456z M201.979,98.405l-16.868,47.464
|
||||
c-0.981,2.757-2.941,5.214-5.213,7.329c-0.337,0.16-0.706,0.229-1.026,0.465c-0.673,0.485-1.182,1.122-1.639,1.747
|
||||
c-2.962,1.996-6.288,3.339-9.434,3.339v2.989l-0.044-2.989l-33.194-0.101c-0.232-0.076-0.424-0.261-0.661-0.324
|
||||
c-1.435-0.353-2.805-0.145-4.095,0.309l-29.768-0.101l1.192-3.358c0.549-1.547-0.269-3.25-1.813-3.795
|
||||
c-1.521-0.553-3.25,0.24-3.799,1.804l-1.899,5.334l-14.318-0.044c-2.805,0-5.063-0.998-6.336-2.813
|
||||
c-1.437-2.032-1.603-4.921-0.463-8.144l16.877-47.478c2.48-6.979,10.417-12.868,17.356-12.868l12.217,0.038l-1.963,5.536
|
||||
c-0.555,1.549,0.262,3.25,1.805,3.797c0.331,0.12,0.661,0.174,0.998,0.174c1.227,0,2.372-0.768,2.793-1.986l2.497-7.019
|
||||
c0.064-0.164-0.048-0.322-0.016-0.487h2.512c-0.905,7.758,1.163,15.42,5.947,21.638c5.903,7.687,14.852,11.726,23.873,11.726
|
||||
c6.371,0,12.771-2.001,18.186-6.129c3.266-2.488,3.911-7.167,1.426-10.441c-2.508-3.267-7.161-3.901-10.455-1.415
|
||||
c-6.612,5.056-16.146,3.775-21.223-2.809c-2.445-3.194-3.487-7.133-2.958-11.117c0.061-0.503,0.353-0.916,0.481-1.402
|
||||
l52.216,0.156c2.806,0,5.054,1.004,6.324,2.811C202.928,92.241,203.105,95.223,201.979,98.405z"/>
|
||||
<path d="M107.997,127.194c-1.531-0.553-3.248,0.244-3.799,1.791l-4.302,12.099c-0.551,1.543,0.265,3.242,1.813,3.795
|
||||
c0.331,0.116,0.659,0.16,0.998,0.16c1.214,0,2.372-0.765,2.801-1.976l4.294-12.099
|
||||
C110.369,129.446,109.551,127.728,107.997,127.194z"/>
|
||||
<path d="M116.6,103.014c-1.529-0.541-3.25,0.252-3.805,1.805l-4.298,12.088c-0.547,1.547,0.261,3.252,1.799,3.799
|
||||
c0.329,0.12,0.659,0.172,1,0.172c1.222,0,2.368-0.769,2.809-1.983l4.294-12.09C118.955,105.268,118.139,103.555,116.6,103.014z"/>
|
||||
<path d="M232.527,90.428l-14.896-0.038l0,0c-1.639,0-2.974,1.327-2.997,2.976c0,1.639,1.342,2.981,2.981,2.989l14.896,0.042l0,0
|
||||
c1.643,0,2.978-1.331,2.993-2.979C235.504,91.763,234.17,90.436,232.527,90.428z"/>
|
||||
<path d="M220.333,80.436c0.629,0,1.242-0.188,1.771-0.583l11.994-8.83c1.326-0.974,1.611-2.842,0.645-4.168
|
||||
c-0.965-1.327-2.845-1.611-4.163-0.637l-11.998,8.833c-1.323,0.974-1.607,2.841-0.642,4.167
|
||||
C218.513,80.003,219.418,80.436,220.333,80.436z"/>
|
||||
<path d="M209.152,56.279c-1.547-0.549-3.25,0.269-3.787,1.805l-4.997,14.036c-0.537,1.547,0.26,3.252,1.803,3.807
|
||||
c0.337,0.12,0.674,0.172,0.994,0.172c1.242,0,2.385-0.757,2.821-1.986l4.985-14.036C211.516,58.541,210.695,56.846,209.152,56.279
|
||||
z"/>
|
||||
<path d="M17.587,100.894h55.208c1.641,0,2.976-1.343,2.976-2.981c0-1.641-1.334-2.988-2.976-2.988H17.587
|
||||
c-1.641,0-2.988,1.338-2.988,2.988C14.599,99.559,15.946,100.894,17.587,100.894z"/>
|
||||
<path d="M68.471,119.328c0-1.641-1.345-2.987-2.986-2.987H10.283c-1.639,0-2.981,1.338-2.981,2.987
|
||||
c0,1.639,1.342,2.974,2.981,2.974h55.202C67.119,122.301,68.471,120.967,68.471,119.328z"/>
|
||||
<path d="M58.188,137.758H2.974c-1.641,0-2.974,1.335-2.974,2.989c0,1.64,1.333,2.974,2.974,2.974h55.214
|
||||
c1.639,0,2.981-1.334,2.981-2.974C61.162,139.093,59.827,137.758,58.188,137.758z"/>
|
||||
<path d="M169.611,28.097c11.821,0,21.403,9.584,21.403,21.41c0,11.82-9.582,21.408-21.403,21.408
|
||||
c-11.822,0-21.412-9.588-21.412-21.408C148.199,37.681,157.789,28.097,169.611,28.097z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.6 KiB |
21
Website/media/settings-cogwheel.svg
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="800px" height="800px" viewBox="0 0 93.5 93.5" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M93.5,40.899c0-2.453-1.995-4.447-4.448-4.447H81.98c-0.74-2.545-1.756-5.001-3.035-7.331l4.998-5
|
||||
c0.826-0.827,1.303-1.973,1.303-3.146c0-1.19-0.462-2.306-1.303-3.146L75.67,9.555c-1.613-1.615-4.673-1.618-6.29,0l-5,5
|
||||
c-2.327-1.28-4.786-2.296-7.332-3.037v-7.07C57.048,1.995,55.053,0,52.602,0H40.899c-2.453,0-4.447,1.995-4.447,4.448v7.071
|
||||
c-2.546,0.741-5.005,1.757-7.333,3.037l-5-5c-1.68-1.679-4.609-1.679-6.288,0L9.555,17.83c-1.734,1.734-1.734,4.555,0,6.289
|
||||
l4.999,5c-1.279,2.33-2.295,4.788-3.036,7.333h-7.07C1.995,36.452,0,38.447,0,40.899V52.6c0,2.453,1.995,4.447,4.448,4.447h7.071
|
||||
c0.74,2.545,1.757,5.003,3.036,7.332l-4.998,4.999c-0.827,0.827-1.303,1.974-1.303,3.146c0,1.189,0.462,2.307,1.302,3.146
|
||||
l8.274,8.273c1.614,1.615,4.674,1.619,6.29,0l5-5c2.328,1.279,4.786,2.297,7.333,3.037v7.071c0,2.453,1.995,4.448,4.447,4.448
|
||||
h11.702c2.453,0,4.446-1.995,4.446-4.448V81.98c2.546-0.74,5.005-1.756,7.332-3.037l5,5c1.681,1.68,4.608,1.68,6.288,0
|
||||
l8.275-8.273c1.734-1.734,1.734-4.555,0-6.289l-4.998-5.001c1.279-2.329,2.295-4.787,3.035-7.332h7.071
|
||||
c2.453,0,4.448-1.995,4.448-4.446V40.899z M62.947,46.75c0,8.932-7.266,16.197-16.197,16.197c-8.931,0-16.197-7.266-16.197-16.197
|
||||
c0-8.931,7.266-16.197,16.197-16.197C55.682,30.553,62.947,37.819,62.947,46.75z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
10
Website/media/tasks.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" height="800px" width="800px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
|
||||
<g id="task">
|
||||
<path d="M4,23.4l-3.7-3.7l1.4-1.4L4,20.6l4.3-4.3l1.4,1.4L4,23.4z M24,21H12v-2h12V21z M4,15.4l-3.7-3.7l1.4-1.4L4,12.6l4.3-4.3
|
||||
l1.4,1.4L4,15.4z M24,13H12v-2h12V13z M4,7.4L0.3,3.7l1.4-1.4L4,4.6l4.3-4.3l1.4,1.4L4,7.4z M24,5H12V3h12V5z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 603 B |
518
Website/style.css
Normal file
@ -0,0 +1,518 @@
|
||||
:root{
|
||||
--background-color: #030304;
|
||||
--second-background-color: #fff;
|
||||
--primary-color: #f5a9b8;
|
||||
--secondary-color: #5bcefa;
|
||||
--accent-color: #fff;
|
||||
--topbar-height: 60px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body{
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
background-color: var(--background-color);
|
||||
font-family: "Inter", sans-serif;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
wrapper {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
flex-wrap: nowrap;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
background-placeholder{
|
||||
background-color: var(--second-background-color);
|
||||
opacity: 1;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 0 0 5px 0;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--topbar-height);
|
||||
background-color: var(--secondary-color);
|
||||
z-index: 100;
|
||||
box-shadow: 0 0 20px black;
|
||||
}
|
||||
|
||||
titlebox {
|
||||
position: relative;
|
||||
display: flex;
|
||||
margin: 0 0 0 40px;
|
||||
height: 100%;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
}
|
||||
|
||||
titlebox span{
|
||||
cursor: default;
|
||||
font-size: 24pt;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(150deg, var(--primary-color), var(--accent-color));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
titlebox img {
|
||||
height: 100%;
|
||||
margin-right: 10px;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
spacer{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
searchdiv{
|
||||
display: block;
|
||||
margin: 0 10px 0 0;
|
||||
}
|
||||
|
||||
#searchbox {
|
||||
padding: 3px 10px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
font-size: 14pt;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
#settingscog {
|
||||
cursor: pointer;
|
||||
margin: 0px 30px;
|
||||
height: 50%;
|
||||
filter: invert(100%) sepia(0%) saturate(7465%) hue-rotate(115deg) brightness(116%) contrast(101%);
|
||||
}
|
||||
|
||||
viewport {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
flex-wrap: nowrap;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--primary-color);
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
footer > div {
|
||||
height: 100%;
|
||||
margin: 0 30px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
footer > div > *{
|
||||
height: 40%;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
#madeWith {
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
margin-right: 20px;
|
||||
cursor: url("media/blahaj.png"), grab;
|
||||
}
|
||||
|
||||
content {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: start;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
settings {
|
||||
width: 50%;
|
||||
background-color: var(--accent-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
left: 25%;
|
||||
top: 25%;
|
||||
border-radius: 5px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
#settingsPopup{
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
settings > * {
|
||||
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-wrap: nowrap;
|
||||
}
|
||||
|
||||
#addPublication {
|
||||
cursor: pointer;
|
||||
background-color: var(--secondary-color);
|
||||
width: 180px;
|
||||
height: 300px;
|
||||
border-radius: 5px;
|
||||
margin: 10px 10px;
|
||||
padding: 15px 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#addPublication p{
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 150pt;
|
||||
vertical-align: middle;
|
||||
line-height: 300px;
|
||||
margin: 0;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.pill {
|
||||
flex-grow: 0;
|
||||
height: 14pt;
|
||||
font-size: 12pt;
|
||||
border-radius: 9pt;
|
||||
background-color: var(--primary-color);
|
||||
padding: 2pt 17px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
publication{
|
||||
cursor: pointer;
|
||||
background-color: var(--secondary-color);
|
||||
width: 180px;
|
||||
height: 300px;
|
||||
border-radius: 5px;
|
||||
margin: 10px 10px;
|
||||
padding: 15px 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
publication::after{
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0; top: 0;
|
||||
border-radius: 5px;
|
||||
width: 100%; height: 100%;
|
||||
background: linear-gradient(rgba(0,0,0,0.8), rgba(0, 0, 0, 0.7),rgba(0, 0, 0, 0.2));
|
||||
}
|
||||
|
||||
publication-information {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
publication-information * {
|
||||
z-index: 1;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
connector-name{
|
||||
width: fit-content;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
publication-name{
|
||||
width: fit-content;
|
||||
font-size: 16pt;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
publication img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
z-index: 0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
popup{
|
||||
display: none;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
blur-background {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
background-color: black;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
addtask-window {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
position: absolute;
|
||||
left: 12.5%;
|
||||
top: 15%;
|
||||
width: 75%;
|
||||
min-height: 70%;
|
||||
max-height: 80%;
|
||||
padding: 0;
|
||||
background-color: var(--accent-color);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
window-titlebar {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
background-color: var(--primary-color);
|
||||
border-radius: 5px 5px 0 0;
|
||||
color: var(--accent-color);
|
||||
display: flex block;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
window-titlebar p {
|
||||
margin: 0 30px;
|
||||
font-size: 14pt;
|
||||
font-weight: bolder;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
window-titlebar #closePopupImg {
|
||||
height: 70%;
|
||||
cursor: pointer;
|
||||
margin-right: 20px;
|
||||
filter: invert(100%) sepia(0%) saturate(100%) hue-rotate(115deg) brightness(116%) contrast(101%);
|
||||
}
|
||||
|
||||
window-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px 5%;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
addtask-settings{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
addtask-settings select, addtask-settings input{
|
||||
padding: 5px;
|
||||
font-size: 10pt;
|
||||
border: 1px solid rgba(0,0,0,0.2);
|
||||
border-radius: 3px;
|
||||
background-color: transparent;
|
||||
margin: 10px 0;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
addtask-settings label {
|
||||
font-weight: bolder;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
addtask-settings addtask-setting{
|
||||
margin: 0 15px;
|
||||
}
|
||||
|
||||
#taskSelectOutput{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: start;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
#publicationViewerPopup{
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
publication-viewer{
|
||||
display: block;
|
||||
width: 450px;
|
||||
position: absolute;
|
||||
top: 200px;
|
||||
left: 400px;
|
||||
background-color: var(--accent-color);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
publication-viewer::after{
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0; top: 0;
|
||||
border-radius: 5px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.8);
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
publication-viewer img {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 5px;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
publication-viewer publication-information > * {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
publication-viewer publication-information publication-name {
|
||||
width: initial;
|
||||
overflow-x: scroll;
|
||||
white-space: nowrap;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
publication-viewer publication-information publication-tags::before {
|
||||
content: "Tags";
|
||||
display: block;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
publication-viewer publication-information publication-tags {
|
||||
overflow-x: scroll;
|
||||
white-space: nowrap;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
publication-viewer publication-information publication-author::before {
|
||||
content: "Author: ";
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
publication-viewer publication-information publication-description::before {
|
||||
content: "Description";
|
||||
display: block;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
publication-viewer publication-information publication-description {
|
||||
font-size: 12pt;
|
||||
margin: 5px 0;
|
||||
height: 145px;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
publication-viewer publication-information publication-interactions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: end;
|
||||
align-items: start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
publication-viewer publication-information publication-interactions > * {
|
||||
margin: 0 10px;
|
||||
font-size: 16pt;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
publication-viewer publication-information publication-interactions publication-starttask {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
publication-viewer publication-information publication-interactions publication-delete {
|
||||
color: red;
|
||||
}
|
||||
|
||||
publication-viewer publication-information publication-interactions publication-add {
|
||||
color: limegreen;
|
||||
}
|
||||
|
||||
footer-tag-popup {
|
||||
display: none;
|
||||
padding: 2px 4px;
|
||||
position: fixed;
|
||||
bottom: 58px;
|
||||
left: 20px;
|
||||
background-color: var(--second-background-color);
|
||||
z-index: 8;
|
||||
border-radius: 5px;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
footer-tag-content{
|
||||
position: relative;
|
||||
max-height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
footer-tag-content > * {
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
footer-tag-popup::before{
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
border-right: 10px solid var(--second-background-color);
|
||||
border-left: 10px solid transparent;
|
||||
border-top: 10px solid var(--second-background-color);
|
||||
border-bottom: 10px solid transparent;
|
||||
left: 0px;
|
||||
bottom: -17px;
|
||||
border-radius: 0 0 0 5px;
|
||||
}
|
21
docker-compose.yaml
Normal file
@ -0,0 +1,21 @@
|
||||
version: '3'
|
||||
services:
|
||||
tranga-api:
|
||||
image: glax/tranga-api:latest
|
||||
container_name: tranga-api
|
||||
volumes:
|
||||
- ./tranga:/usr/share/Tranga-API #1 when replacing ./tranga replace #2 with same value
|
||||
- ./Manga:/Manga
|
||||
ports:
|
||||
- 6531:80
|
||||
restart: unless-stopped
|
||||
tranga-website:
|
||||
image: glax/tranga-website:latest
|
||||
container_name: tranga-website
|
||||
volumes:
|
||||
- ./tranga/imageCache:/usr/share/nginx/html/imageCache:ro #2 when replacing Point to same value as #1/imageCache
|
||||
ports:
|
||||
- 9555:80
|
||||
depends_on:
|
||||
- tranga-api
|
||||
restart: unless-stopped
|
BIN
screenshots/addtask.png
Normal file
After Width: | Height: | Size: 1.0 MiB |
BIN
screenshots/overview.png
Normal file
After Width: | Height: | Size: 2.6 MiB |
BIN
screenshots/publication-description.png
Normal file
After Width: | Height: | Size: 2.2 MiB |
BIN
screenshots/settings.png
Normal file
After Width: | Height: | Size: 1.7 MiB |