Compare commits
289 Commits
Author | SHA1 | Date | |
---|---|---|---|
9fd8bf1741 | |||
d5c9c5ba96 | |||
c8e27921ab | |||
6eaba07801 | |||
41929e0c72 | |||
4fcaca1a6e | |||
0e3c7f32d7 | |||
1c94625840 | |||
32f89f9dce | |||
234735a562 | |||
8b916eb854 | |||
29e1790c93 | |||
ac4c799a74 | |||
7c62883c37 | |||
02018253bf | |||
2aec884009 | |||
b3321ff030 | |||
16c1094875 | |||
5763d50409 | |||
ad43297358 | |||
b17800e0ef | |||
89c80d2997 | |||
6485b8744f | |||
a3a96b6b55 | |||
5bce3c6fdd | |||
5fa0c98d05 | |||
b166013770 | |||
02fe849046 | |||
d42393c83a | |||
c685bd622f | |||
dc83cc2194 | |||
7784f2024e | |||
4895079887 | |||
ab1ddc6dc8 | |||
87eade10cf | |||
1f3ac41b30 | |||
6a304bb330 | |||
b0642d1251 | |||
63b5139e93 | |||
e938784388 | |||
c436389426 | |||
5099e25f3f | |||
cf6fc3b8f6 | |||
f5141d0f8e | |||
5c753e7a7d | |||
17ce820cf3 | |||
5b4a3b9d7c | |||
f73997e563 | |||
437136804d | |||
e14683d21a | |||
5ae02ee0ed | |||
a2e9a3f34a | |||
bbf05e3dec | |||
d95839e5df | |||
5a303598fe | |||
db2103963e | |||
2c1105527a | |||
ed19dcb5c3 | |||
46f06c2685 | |||
d4f47e057c | |||
61712d0537 | |||
1f8f8c09e3 | |||
0522fa6215 | |||
0383a7d686 | |||
bd189984a9 | |||
58c01b2174 | |||
459558a514 | |||
721b316209 | |||
b1befa2ecc | |||
57a4cc4ab5 | |||
655e8db2b6 | |||
7cdf77cbb9 | |||
5a9aed4969 | |||
5b41f687d0 | |||
d6a62dc315 | |||
6d91788655 | |||
14785e5672 | |||
496d502cd2 | |||
7612411917 | |||
ed1402b5ec | |||
5adaee4821 | |||
2d82fe1489 | |||
783fd8129e | |||
4f29eff48e | |||
e0e7abb62b | |||
ae63a216a5 | |||
5d98295c59 | |||
0c580933f9 | |||
06f735aadd | |||
439d69d8e0 | |||
933df58712 | |||
165bbc412e | |||
6158fa072b | |||
0d3799e00d | |||
e977bed5a5 | |||
cacd5fede2 | |||
1bca99cb6a | |||
15fc367263 | |||
8bb6fb902b | |||
a57903cd5a | |||
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 | |||
eddf50483f | |||
a71d65e666 | |||
9a640aed27 | |||
30b6c4680b | |||
7b6253de0f | |||
5aa3214ce5 | |||
9b70994f71 | |||
93cf341f2d | |||
01cb74c088 | |||
ec480dffad | |||
b7014cbff5 | |||
0cab921402 | |||
0e0ba1796e | |||
27d8565dc1 | |||
79dc44d707 | |||
bb6a0ad0d4 | |||
43db463ba6 | |||
9eb8ddbc40 | |||
972cba69ec | |||
962fe9529e | |||
da1b0cb1cd | |||
7f88e57e47 | |||
8865bf284f | |||
5fc2de5fcb | |||
4bae223d95 | |||
0486168b43 | |||
b64ab5c6d4 | |||
578fa5e6be | |||
4d33e78123 | |||
52ac3e4e4e | |||
8b99a98e24 | |||
cf171d5c38 | |||
6d49b4b934 | |||
b55d2a2d06 | |||
737eebf599 | |||
aef01b684c | |||
53bff61174 | |||
431a602a40 | |||
9afb81cee2 | |||
ea69b355b5 | |||
84dbc36bbf | |||
455c87b2e1 | |||
df991e3da6 | |||
13c96fd09a | |||
6f1a6a43ee | |||
e2afc09c4a | |||
e9db7cfacc | |||
755167c39a | |||
1cff93fbac | |||
6c775d6e0c | |||
876b1ab78b |
4
.gitignore
vendored
@ -16,4 +16,6 @@ riderModule.iml
|
|||||||
# Datasource local storage ignored files
|
# Datasource local storage ignored files
|
||||||
/dataSources/
|
/dataSources/
|
||||||
/dataSources.local.xml
|
/dataSources.local.xml
|
||||||
/.idea
|
/.idea
|
||||||
|
cover.jpg
|
||||||
|
cover.png
|
14
Dockerfile
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# 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
|
||||||
|
FROM glax/tranga-base:latest as runtime
|
||||||
|
WORKDIR /publish
|
||||||
|
COPY --from=build-env /publish .
|
||||||
|
EXPOSE 80
|
||||||
|
ENTRYPOINT ["dotnet", "/publish/Tranga-API.dll"]
|
4
Dockerfile-base
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:7.0 as runtime
|
||||||
|
WORKDIR /publish
|
||||||
|
RUN apt-get update && apt-get install -y libx11-6 libx11-xcb1 libatk1.0-0 libgtk-3-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2 libxshmfence1 libnss3
|
@ -52,7 +52,7 @@ public abstract class LoggerBase : TextWriter
|
|||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
string dateTimeString = $"{logTime.ToShortDateString()} {logTime.ToLongTimeString()}";
|
string dateTimeString = $"{logTime.ToShortDateString()} {logTime.ToLongTimeString()}";
|
||||||
return $"[{dateTimeString}] {caller,30} | {value}";
|
return $"[{dateTimeString}] {caller.Split(new char[]{'.','+'}).Last(),15} | {value}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
67
README.md
@ -30,9 +30,13 @@
|
|||||||
<li><a href="#built-with">Built With</a></li>
|
<li><a href="#built-with">Built With</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#screenshots">Screenshots</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="#getting-started">Getting Started</a>
|
<a href="#getting-started">Getting Started</a>
|
||||||
<ul>
|
<ul>
|
||||||
|
<li><a href="#prerequisites">Usage</a></li>
|
||||||
<li><a href="#prerequisites">Prerequisites</a></li>
|
<li><a href="#prerequisites">Prerequisites</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
@ -51,8 +55,10 @@
|
|||||||
Tranga can download Chapters and Metadata from Scanlation sites such as
|
Tranga can download Chapters and Metadata from Scanlation sites such as
|
||||||
|
|
||||||
- [MangaDex.org](https://mangadex.org/)
|
- [MangaDex.org](https://mangadex.org/)
|
||||||
|
- [Manganato.com](https://manganato.com/)
|
||||||
|
- [Mangasee](https://mangasee123.com/)
|
||||||
|
|
||||||
and automatically start updates in [Komga](https://komga.org/) to import them.
|
and automatically start updates in [Komga](https://komga.org/) and [Kavita](https://www.kavitareader.com/) to import them.
|
||||||
|
|
||||||
### Inspiration:
|
### Inspiration:
|
||||||
|
|
||||||
@ -64,35 +70,76 @@ 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>
|
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Built With
|
### Built With
|
||||||
|
|
||||||
- .NET-Core
|
- .NET-Core
|
||||||
- Newtonsoft.JSON
|
- Newtonsoft.JSON
|
||||||
- Love <3
|
- [PuppeteerSharp](https://www.puppeteersharp.com/)
|
||||||
|
- [Html Agility Pack (HAP)](https://html-agility-pack.net/)
|
||||||
|
- Love <3 Blåhaj 🦈
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
<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 -->
|
||||||
## 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.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
There is two ways to download Mangas:
|
||||||
|
- Downloading everything and monitor for new Chapters
|
||||||
|
- Selecting specific Volumes/Chapters
|
||||||
|
|
||||||
|
On the website you add new tasks, by selecting the blue '+' field. Next select the connector/site you want to use, and enter a search term.
|
||||||
|
After pressing 'Search', the results will be presented below - this might, depending on the result-size, take a while.
|
||||||
|
Next select the publication and a new popup will open with two options:
|
||||||
|
- "Monitor" - Download all chapters and monitor for new ones
|
||||||
|
- "Download Chapter" - Download specific chapters only
|
||||||
|
|
||||||
|
When selecting `Monitor` you will be presented with a new window and the selection of the interval you want to check for new chapters.
|
||||||
|
When selecting `Download Chapter` a list will open with all available chapters from which you can then select a range.
|
||||||
|
|
||||||
|
The syntax for selecting chapters is as follows:
|
||||||
|
- To download a single Chapter enter either the index number (the number at the very start of the line) or its absolute number like so: `c(h)(apter)[number]`, spaces are allowed.
|
||||||
|
- To download a range of chapters enter either a range of index numbers (`3-6`) or chapters (`ch 12-23`).
|
||||||
|
- For volumes the syntax is as follows: `v(ol)[number](-[number])`, again spaces allowed.
|
||||||
|
|
||||||
|
Examples: `2-12`, `c1`, `ch 2`, `chapter 3`, `v 2`, `vol3-4`, `v2c4` (note: you can only specify a single chapter with this syntax).
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
[.NET-Core 7.0](https://dotnet.microsoft.com/en-us/download/dotnet/7.0)
|
[.NET-Core 7.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/7.0)
|
||||||
|
|
||||||
<!-- ROADMAP -->
|
<!-- ROADMAP -->
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
- [ ] Web-UI #1
|
- [ ] Docker ARM support
|
||||||
- [ ] More Connectors
|
|
||||||
- [ ] Manganato #2
|
|
||||||
- [ ] ?
|
- [ ] ?
|
||||||
|
|
||||||
See the [open issues](https://git.bernloehr.eu/glax/Tranga/issues) for a full list of proposed features (and known issues).
|
See the [open issues](https://git.bernloehr.eu/glax/Tranga/issues) for a full list of proposed features (and known issues).
|
||||||
|
@ -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,81 +1,243 @@
|
|||||||
using System.Text.Json;
|
using System.Runtime.InteropServices;
|
||||||
|
using Logging;
|
||||||
using Tranga;
|
using Tranga;
|
||||||
|
using Tranga.TrangaTasks;
|
||||||
|
|
||||||
|
string applicationFolderPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Tranga-API");
|
||||||
|
string downloadFolderPath = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(applicationFolderPath, "Manga");
|
||||||
|
string logsFolderPath = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/var/logs/Tranga" : Path.Join(applicationFolderPath, "logs");
|
||||||
|
string logFilePath = Path.Join(logsFolderPath, $"log-{DateTime.Now:dd-M-yyyy-HH-mm-ss}.txt");
|
||||||
|
string settingsFilePath = Path.Join(applicationFolderPath, "settings.json");
|
||||||
|
|
||||||
|
Directory.CreateDirectory(logsFolderPath);
|
||||||
|
Logger logger = new(new[] { Logger.LoggerType.FileLogger, Logger.LoggerType.ConsoleLogger }, Console.Out, Console.Out.Encoding, logFilePath);
|
||||||
|
|
||||||
|
logger.WriteLine("Tranga", "Loading settings.");
|
||||||
|
|
||||||
|
TrangaSettings settings;
|
||||||
|
if (File.Exists(settingsFilePath))
|
||||||
|
settings = TrangaSettings.LoadSettings(settingsFilePath, logger);
|
||||||
|
else
|
||||||
|
settings = new TrangaSettings(downloadFolderPath, applicationFolderPath, new HashSet<LibraryManager>());
|
||||||
|
|
||||||
|
Directory.CreateDirectory(settings.workingDirectory);
|
||||||
|
Directory.CreateDirectory(settings.downloadLocation);
|
||||||
|
Directory.CreateDirectory(settings.coverImageCache);
|
||||||
|
|
||||||
|
logger.WriteLine("Tranga",$"Application-Folder: {settings.workingDirectory}");
|
||||||
|
logger.WriteLine("Tranga",$"Settings-File-Path: {settings.settingsFilePath}");
|
||||||
|
logger.WriteLine("Tranga",$"Download-Folder-Path: {settings.downloadLocation}");
|
||||||
|
logger.WriteLine("Tranga",$"Logfile-Path: {logFilePath}");
|
||||||
|
logger.WriteLine("Tranga",$"Image-Cache-Path: {settings.coverImageCache}");
|
||||||
|
|
||||||
|
logger.WriteLine("Tranga", "Loading Taskmanager.");
|
||||||
|
TaskManager taskManager = new (settings, logger);
|
||||||
|
|
||||||
TaskManager taskManager = new (Directory.GetCurrentDirectory());
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
var app = builder.Build();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGen();
|
||||||
|
builder.Services.AddControllers().AddNewtonsoftJson();
|
||||||
|
|
||||||
app.MapGet("/GetConnectors", () => JsonSerializer.Serialize(taskManager.GetAvailableConnectors().Values.ToArray()));
|
string corsHeader = "Tranga";
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
app.MapGet("/GetPublications", (string connectorName, string? publicationName) =>
|
|
||||||
{
|
{
|
||||||
Connector connector = taskManager.GetConnector(connectorName);
|
options.AddPolicy(name: corsHeader,
|
||||||
|
policy =>
|
||||||
Publication[] publications;
|
{
|
||||||
if (publicationName is not null)
|
policy.AllowAnyOrigin();
|
||||||
publications = connector.GetPublications(publicationName);
|
policy.WithMethods("GET", "POST", "DELETE");
|
||||||
else
|
});
|
||||||
publications = connector.GetPublications();
|
|
||||||
|
|
||||||
return JsonSerializer.Serialize(publications);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.MapGet("/ListTasks", () => JsonSerializer.Serialize(taskManager.GetAllTasks()));
|
var app = builder.Build();
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
|
||||||
app.MapGet("/CreateTask",
|
app.UseCors(corsHeader);
|
||||||
(TrangaTask.Task task, string? connectorName, string? publicationName, TimeSpan reoccurrence, string? language) =>
|
|
||||||
|
app.MapGet("/Controllers/Get", () => taskManager.GetAvailableConnectors().Keys.ToArray());
|
||||||
|
|
||||||
|
app.MapGet("/Publications/GetKnown", (string? internalId) =>
|
||||||
|
{
|
||||||
|
if(internalId is null)
|
||||||
|
return taskManager.GetAllPublications();
|
||||||
|
|
||||||
|
return new [] { taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == internalId) };
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapGet("/Publications/GetFromConnector", (string connectorName, string title) =>
|
||||||
|
{
|
||||||
|
Connector? connector = taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName).Value;
|
||||||
|
if (connector is null)
|
||||||
|
return Array.Empty<Publication>();
|
||||||
|
if(title.Length < 4)
|
||||||
|
return Array.Empty<Publication>();
|
||||||
|
return taskManager.GetPublicationsFromConnector(connector, title);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapGet("/Publications/GetChapters", (string connectorName, string internalId, string? language) =>
|
||||||
|
{
|
||||||
|
Connector? connector = taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName).Value;
|
||||||
|
if (connector is null)
|
||||||
|
return Array.Empty<Chapter>();
|
||||||
|
Publication? publication = taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == internalId);
|
||||||
|
if (publication is null)
|
||||||
|
return Array.Empty<Chapter>();
|
||||||
|
return connector.GetChapters((Publication)publication, language??"en");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapGet("/Tasks/GetTypes", () => Enum.GetNames(typeof(TrangaTask.Task)));
|
||||||
|
|
||||||
|
|
||||||
|
app.MapPost("/Tasks/CreateMonitorTask",
|
||||||
|
(string connectorName, string internalId, string reoccurrenceTime, string? language) =>
|
||||||
{
|
{
|
||||||
switch (task)
|
Connector? connector =
|
||||||
{
|
taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName).Value;
|
||||||
case TrangaTask.Task.UpdateKomgaLibrary:
|
if (connector is null)
|
||||||
taskManager.AddTask(TrangaTask.Task.UpdateKomgaLibrary, null, null, reoccurrence);
|
return;
|
||||||
break;
|
Publication? publication = taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == internalId);
|
||||||
case TrangaTask.Task.DownloadNewChapters:
|
if (publication is null)
|
||||||
try
|
return;
|
||||||
{
|
taskManager.AddTask(new DownloadNewChaptersTask(TrangaTask.Task.DownloadNewChapters, connectorName,
|
||||||
Connector connector = taskManager.GetConnector(connectorName);
|
(Publication)publication,
|
||||||
|
TimeSpan.Parse(reoccurrenceTime), language ?? "en"));
|
||||||
Publication[] publications;
|
|
||||||
if (publicationName is not null)
|
|
||||||
publications = connector.GetPublications(publicationName);
|
|
||||||
else
|
|
||||||
publications = connector.GetPublications();
|
|
||||||
|
|
||||||
Publication? publication = publications.FirstOrDefault(pub => pub.downloadUrl == publicationName);
|
|
||||||
if (publication is null)
|
|
||||||
JsonSerializer.Serialize($"Publication {publicationName} is unknown.");
|
|
||||||
taskManager.AddTask(TrangaTask.Task.DownloadNewChapters, connectorName, publication, reoccurrence, language ?? "");
|
|
||||||
return JsonSerializer.Serialize("Success");
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return JsonSerializer.Serialize(e.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
default: return JsonSerializer.Serialize("Not Implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
return JsonSerializer.Serialize("Not Implemented");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.MapGet("/RemoveTask", (TrangaTask.Task task, string? connectorName, string? publicationName) =>
|
app.MapPost("/Tasks/CreateUpdateLibraryTask", (string reoccurrenceTime) =>
|
||||||
{
|
{
|
||||||
switch (task)
|
taskManager.AddTask(new UpdateLibrariesTask(TrangaTask.Task.UpdateLibraries, TimeSpan.Parse(reoccurrenceTime)));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapPost("/Tasks/CreateDownloadChaptersTask", (string connectorName, string internalId, string chapters, string? language) => {
|
||||||
|
|
||||||
|
Connector? connector =
|
||||||
|
taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName).Value;
|
||||||
|
if (connector is null)
|
||||||
|
return;
|
||||||
|
Publication? publication = taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == internalId);
|
||||||
|
if (publication is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
IEnumerable<Chapter> toDownload = connector.SearchChapters((Publication)publication, chapters, language ?? "en");
|
||||||
|
foreach(Chapter chapter in toDownload)
|
||||||
|
taskManager.AddTask(new DownloadChapterTask(TrangaTask.Task.DownloadChapter, connectorName,
|
||||||
|
(Publication)publication, chapter, "en"));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapDelete("/Tasks/Delete", (string taskType, string? connectorName, string? publicationId) =>
|
||||||
|
{
|
||||||
|
TrangaTask.Task task = Enum.Parse<TrangaTask.Task>(taskType);
|
||||||
|
taskManager.DeleteTask(task, connectorName, publicationId);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapGet("/Tasks/Get", (string taskType, string? connectorName, string? searchString) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
case TrangaTask.Task.UpdateKomgaLibrary:
|
TrangaTask.Task task = Enum.Parse<TrangaTask.Task>(taskType);
|
||||||
taskManager.DeleteTask(TrangaTask.Task.UpdateKomgaLibrary, null, null);
|
return taskManager.GetTasksMatching(task, connectorName:connectorName, searchString:searchString);
|
||||||
return JsonSerializer.Serialize("Success");
|
}
|
||||||
case TrangaTask.Task.DownloadNewChapters:
|
catch (ArgumentException)
|
||||||
Publication? publication = taskManager.GetAllPublications().FirstOrDefault(pub => pub.downloadUrl == publicationName);
|
{
|
||||||
if (publication is null)
|
return Array.Empty<TrangaTask>();
|
||||||
JsonSerializer.Serialize($"Publication {publicationName} is unknown.");
|
|
||||||
|
|
||||||
taskManager.DeleteTask(TrangaTask.Task.DownloadNewChapters, connectorName, publication);
|
|
||||||
|
|
||||||
return JsonSerializer.Serialize("Success");
|
|
||||||
|
|
||||||
default: return JsonSerializer.Serialize("Not Implemented");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.MapGet("/Tasks/GetProgress", (string taskType, string connectorName, string publicationId, string? chapterSortNumber) =>
|
||||||
|
{
|
||||||
|
Connector? connector =
|
||||||
|
taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName).Value;
|
||||||
|
if (connector is null)
|
||||||
|
return -1f;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TrangaTask? task = null;
|
||||||
|
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType);
|
||||||
|
if (pTask is TrangaTask.Task.DownloadNewChapters)
|
||||||
|
{
|
||||||
|
task = taskManager.GetTasksMatching(pTask, connectorName: connectorName, internalId: publicationId).FirstOrDefault();
|
||||||
|
}else if (pTask is TrangaTask.Task.DownloadChapter && chapterSortNumber is not null)
|
||||||
|
{
|
||||||
|
task = taskManager.GetTasksMatching(pTask, connectorName: connectorName, internalId: publicationId,
|
||||||
|
chapterSortNumber: chapterSortNumber).FirstOrDefault();
|
||||||
|
}
|
||||||
|
if (task is null)
|
||||||
|
return -1f;
|
||||||
|
|
||||||
|
return task.progress;
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
return -1f;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapPost("/Tasks/Start", (string taskType, string? connectorName, string? internalId) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType);
|
||||||
|
TrangaTask? task = taskManager
|
||||||
|
.GetTasksMatching(pTask, connectorName: connectorName, internalId: internalId)?.FirstOrDefault();
|
||||||
|
|
||||||
|
if (task is null)
|
||||||
|
return;
|
||||||
|
taskManager.ExecuteTaskNow(task);
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapGet("/Tasks/GetRunningTasks",
|
||||||
|
() => taskManager.GetAllTasks().Where(task => task.state is TrangaTask.ExecutionState.Running));
|
||||||
|
|
||||||
|
app.MapGet("/Queue/GetList",
|
||||||
|
() => taskManager.GetAllTasks().Where(task => task.state is TrangaTask.ExecutionState.Enqueued).OrderBy(task => task.nextExecution));
|
||||||
|
|
||||||
|
app.MapPost("/Queue/Enqueue", (string taskType, string? connectorName, string? publicationId) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType);
|
||||||
|
TrangaTask? task = taskManager
|
||||||
|
.GetTasksMatching(pTask, connectorName: connectorName, internalId: publicationId)?.First();
|
||||||
|
|
||||||
|
if (task is null)
|
||||||
|
return;
|
||||||
|
taskManager.AddTaskToQueue(task);
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapDelete("/Queue/Dequeue", (string taskType, string? connectorName, string? publicationId) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType);
|
||||||
|
TrangaTask? task = taskManager
|
||||||
|
.GetTasksMatching(pTask, connectorName: connectorName, internalId: publicationId)?.First();
|
||||||
|
|
||||||
|
if (task is null)
|
||||||
|
return;
|
||||||
|
taskManager.RemoveTaskFromQueue(task);
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapGet("/Settings/Get", () => taskManager.settings);
|
||||||
|
|
||||||
|
app.MapPost("/Settings/Update",
|
||||||
|
(string? downloadLocation, string? komgaUrl, string? komgaAuth, string? kavitaUrl, string? kavitaUsername, string? kavitaPassword) =>
|
||||||
|
taskManager.UpdateSettings(downloadLocation, komgaUrl, komgaAuth, kavitaUrl, kavitaUsername, kavitaPassword));
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
@ -3,8 +3,8 @@
|
|||||||
"windowsAuthentication": false,
|
"windowsAuthentication": false,
|
||||||
"anonymousAuthentication": true,
|
"anonymousAuthentication": true,
|
||||||
"iisExpress": {
|
"iisExpress": {
|
||||||
"applicationUrl": "http://localhost:14826",
|
"applicationUrl": "http://localhost:1716",
|
||||||
"sslPort": 44333
|
"sslPort": 44391
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"profiles": {
|
"profiles": {
|
||||||
@ -12,26 +12,17 @@
|
|||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"applicationUrl": "http://localhost:5119",
|
"applicationUrl": "http://localhost:5177"
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"https": {
|
"https": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"applicationUrl": "https://localhost:7070;http://localhost:5119",
|
"applicationUrl": "https://localhost:7036;http://localhost:5177"
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"IIS Express": {
|
"IIS Express": {
|
||||||
"commandName": "IISExpress",
|
"commandName": "IISExpress",
|
||||||
"launchBrowser": true,
|
"launchBrowser": true
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,14 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Logging\Logging.csproj" />
|
||||||
<ProjectReference Include="..\Tranga\Tranga.csproj" />
|
<ProjectReference Include="..\Tranga\Tranga.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.5" />
|
||||||
|
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.6" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1 +0,0 @@
|
|||||||
[{"reoccurrence":"00:00:00","lastExecuted":"2023-05-19T17:34:40.5349215+02:00","connectorName":"MangaDex","task":0,"publication":{"sortName":null,"description":null,"tags":null,"posterUrl":null,"year":null,"originalLanguage":null,"status":null,"folderName":null,"downloadUrl":null},"language":"en"}]
|
|
@ -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"]
|
|
@ -1,6 +1,8 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Logging;
|
using Logging;
|
||||||
using Tranga;
|
using Tranga;
|
||||||
|
using Tranga.LibraryManagers;
|
||||||
|
using Tranga.TrangaTasks;
|
||||||
|
|
||||||
namespace Tranga_CLI;
|
namespace Tranga_CLI;
|
||||||
|
|
||||||
@ -14,10 +16,10 @@ public static class Tranga_Cli
|
|||||||
{
|
{
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
string applicationFolderPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Tranga");
|
string applicationFolderPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Tranga");
|
||||||
string logsFolderPath = Path.Join(applicationFolderPath, "logs");
|
string logsFolderPath = Path.Join(applicationFolderPath, "logs");
|
||||||
string logFilePath = Path.Join(logsFolderPath, $"log-{DateTime.Now:dd-M-yyyy-HH-mm-ss}.txt");
|
string logFilePath = Path.Join(logsFolderPath, $"log-{DateTime.Now:dd-M-yyyy-HH-mm-ss}.txt");
|
||||||
string settingsFilePath = Path.Join(applicationFolderPath, "data.json");
|
string settingsFilePath = Path.Join(applicationFolderPath, "settings.json");
|
||||||
|
|
||||||
Directory.CreateDirectory(applicationFolderPath);
|
Directory.CreateDirectory(applicationFolderPath);
|
||||||
Directory.CreateDirectory(logsFolderPath);
|
Directory.CreateDirectory(logsFolderPath);
|
||||||
@ -25,14 +27,10 @@ public static class Tranga_Cli
|
|||||||
Console.WriteLine($"Logfile-Path: {logFilePath}");
|
Console.WriteLine($"Logfile-Path: {logFilePath}");
|
||||||
Console.WriteLine($"Settings-File-Path: {settingsFilePath}");
|
Console.WriteLine($"Settings-File-Path: {settingsFilePath}");
|
||||||
|
|
||||||
Logger logger = new(new[] { Logger.LoggerType.FileLogger }, null, null, logFilePath);
|
Logger logger = new(new[] { Logger.LoggerType.FileLogger }, null, Console.Out.Encoding, logFilePath);
|
||||||
|
|
||||||
logger.WriteLine("Tranga_CLI", "Loading Taskmanager.");
|
logger.WriteLine("Tranga_CLI", "Loading Taskmanager.");
|
||||||
TaskManager.SettingsData settings;
|
TrangaSettings settings = File.Exists(settingsFilePath) ? TrangaSettings.LoadSettings(settingsFilePath, logger) : new TrangaSettings(Directory.GetCurrentDirectory(), applicationFolderPath, new HashSet<LibraryManager>());
|
||||||
if (File.Exists(settingsFilePath))
|
|
||||||
settings = TaskManager.LoadData(settingsFilePath);
|
|
||||||
else
|
|
||||||
settings = new TaskManager.SettingsData(Directory.GetCurrentDirectory(), settingsFilePath, null, new HashSet<TrangaTask>());
|
|
||||||
|
|
||||||
|
|
||||||
logger.WriteLine("Tranga_CLI", "User Input");
|
logger.WriteLine("Tranga_CLI", "User Input");
|
||||||
@ -40,52 +38,88 @@ public static class Tranga_Cli
|
|||||||
string? tmpPath = Console.ReadLine();
|
string? tmpPath = Console.ReadLine();
|
||||||
while(tmpPath is null)
|
while(tmpPath is null)
|
||||||
tmpPath = Console.ReadLine();
|
tmpPath = Console.ReadLine();
|
||||||
if(tmpPath.Length > 0)
|
if (tmpPath.Length > 0)
|
||||||
settings.downloadLocation = tmpPath;
|
settings.downloadLocation = tmpPath;
|
||||||
|
|
||||||
Console.WriteLine($"Komga BaseURL [{settings.komga?.baseUrl}]:");
|
Console.WriteLine($"Komga BaseURL [{settings.libraryManagers.FirstOrDefault(lm => lm.GetType() == typeof(Komga))?.baseUrl}]:");
|
||||||
string? tmpUrl = Console.ReadLine();
|
string? tmpUrlKomga = Console.ReadLine();
|
||||||
while (tmpUrl is null)
|
while (tmpUrlKomga is null)
|
||||||
tmpUrl = Console.ReadLine();
|
tmpUrlKomga = Console.ReadLine();
|
||||||
if (tmpUrl.Length > 0)
|
if (tmpUrlKomga.Length > 0)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Username:");
|
Console.WriteLine("Username:");
|
||||||
string? tmpUser = Console.ReadLine();
|
string? tmpKomgaUser = Console.ReadLine();
|
||||||
while (tmpUser is null || tmpUser.Length < 1)
|
while (tmpKomgaUser is null || tmpKomgaUser.Length < 1)
|
||||||
tmpUser = Console.ReadLine();
|
tmpKomgaUser = Console.ReadLine();
|
||||||
|
|
||||||
Console.WriteLine("Password:");
|
Console.WriteLine("Password:");
|
||||||
string tmpPass = string.Empty;
|
string tmpKomgaPass = string.Empty;
|
||||||
ConsoleKey key;
|
ConsoleKey key;
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
var keyInfo = Console.ReadKey(intercept: true);
|
var keyInfo = Console.ReadKey(intercept: true);
|
||||||
key = keyInfo.Key;
|
key = keyInfo.Key;
|
||||||
|
|
||||||
if (key == ConsoleKey.Backspace && tmpPass.Length > 0)
|
if (key == ConsoleKey.Backspace && tmpKomgaPass.Length > 0)
|
||||||
{
|
{
|
||||||
Console.Write("\b \b");
|
Console.Write("\b \b");
|
||||||
tmpPass = tmpPass[0..^1];
|
tmpKomgaPass = tmpKomgaPass[0..^1];
|
||||||
}
|
}
|
||||||
else if (!char.IsControl(keyInfo.KeyChar))
|
else if (!char.IsControl(keyInfo.KeyChar))
|
||||||
{
|
{
|
||||||
Console.Write("*");
|
Console.Write("*");
|
||||||
tmpPass += keyInfo.KeyChar;
|
tmpKomgaPass += keyInfo.KeyChar;
|
||||||
}
|
}
|
||||||
} while (key != ConsoleKey.Enter);
|
} while (key != ConsoleKey.Enter);
|
||||||
|
|
||||||
settings.komga = new Komga(tmpUrl, tmpUser, tmpPass, logger);
|
settings.libraryManagers.RemoveWhere(lm => lm.GetType() == typeof(Komga));
|
||||||
|
settings.libraryManagers.Add(new Komga(tmpUrlKomga, tmpKomgaUser, tmpKomgaPass, logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Kavita BaseURL [{settings.libraryManagers.FirstOrDefault(lm => lm.GetType() == typeof(Kavita))?.baseUrl}]:");
|
||||||
|
string? tmpUrlKavita = Console.ReadLine();
|
||||||
|
while (tmpUrlKavita is null)
|
||||||
|
tmpUrlKavita = Console.ReadLine();
|
||||||
|
if (tmpUrlKavita.Length > 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Username:");
|
||||||
|
string? tmpKavitaUser = Console.ReadLine();
|
||||||
|
while (tmpKavitaUser is null || tmpKavitaUser.Length < 1)
|
||||||
|
tmpKavitaUser = Console.ReadLine();
|
||||||
|
|
||||||
|
Console.WriteLine("Password:");
|
||||||
|
string tmpKavitaPass = string.Empty;
|
||||||
|
ConsoleKey key;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
var keyInfo = Console.ReadKey(intercept: true);
|
||||||
|
key = keyInfo.Key;
|
||||||
|
|
||||||
|
if (key == ConsoleKey.Backspace && tmpKavitaPass.Length > 0)
|
||||||
|
{
|
||||||
|
Console.Write("\b \b");
|
||||||
|
tmpKavitaPass = tmpKavitaPass[0..^1];
|
||||||
|
}
|
||||||
|
else if (!char.IsControl(keyInfo.KeyChar))
|
||||||
|
{
|
||||||
|
Console.Write("*");
|
||||||
|
tmpKavitaPass += keyInfo.KeyChar;
|
||||||
|
}
|
||||||
|
} while (key != ConsoleKey.Enter);
|
||||||
|
|
||||||
|
settings.libraryManagers.RemoveWhere(lm => lm.GetType() == typeof(Kavita));
|
||||||
|
settings.libraryManagers.Add(new Kavita(tmpUrlKavita, tmpKavitaUser, tmpKavitaPass, logger));
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.WriteLine("Tranga_CLI", "Loaded.");
|
logger.WriteLine("Tranga_CLI", "Loaded.");
|
||||||
TaskMode(settings, logger);
|
TaskMode(settings, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TaskMode(TaskManager.SettingsData settings, Logger logger)
|
private static void TaskMode(TrangaSettings settings, Logger logger)
|
||||||
{
|
{
|
||||||
TaskManager taskManager = new (settings, logger);
|
TaskManager taskManager = new (settings, logger);
|
||||||
ConsoleKey selection = ConsoleKey.EraseEndOfFile;
|
ConsoleKey selection = ConsoleKey.EraseEndOfFile;
|
||||||
PrintMenu(taskManager, taskManager.settings.downloadLocation, logger);
|
PrintMenu(taskManager, taskManager.settings.downloadLocation);
|
||||||
while (selection != ConsoleKey.Q)
|
while (selection != ConsoleKey.Q)
|
||||||
{
|
{
|
||||||
int taskCount = taskManager.GetAllTasks().Length;
|
int taskCount = taskManager.GetAllTasks().Length;
|
||||||
@ -101,12 +135,16 @@ public static class Tranga_Cli
|
|||||||
switch (selection)
|
switch (selection)
|
||||||
{
|
{
|
||||||
case ConsoleKey.L:
|
case ConsoleKey.L:
|
||||||
PrintTasks(taskManager.GetAllTasks(), logger);
|
while (!Console.KeyAvailable)
|
||||||
Console.WriteLine("Press any key.");
|
{
|
||||||
|
PrintTasks(taskManager.GetAllTasks(), logger);
|
||||||
|
Console.WriteLine("Press any key.");
|
||||||
|
Thread.Sleep(500);
|
||||||
|
}
|
||||||
Console.ReadKey();
|
Console.ReadKey();
|
||||||
break;
|
break;
|
||||||
case ConsoleKey.C:
|
case ConsoleKey.C:
|
||||||
CreateTask(taskManager, taskManager.settings, logger);
|
CreateTask(taskManager, logger);
|
||||||
Console.WriteLine("Press any key.");
|
Console.WriteLine("Press any key.");
|
||||||
Console.ReadKey();
|
Console.ReadKey();
|
||||||
break;
|
break;
|
||||||
@ -126,24 +164,33 @@ public static class Tranga_Cli
|
|||||||
Console.ReadKey();
|
Console.ReadKey();
|
||||||
break;
|
break;
|
||||||
case ConsoleKey.R:
|
case ConsoleKey.R:
|
||||||
PrintTasks(
|
while (!Console.KeyAvailable)
|
||||||
taskManager.GetAllTasks().Where(eTask => eTask.state == TrangaTask.ExecutionState.Running)
|
{
|
||||||
.ToArray(), logger);
|
PrintTasks(
|
||||||
Console.WriteLine("Press any key.");
|
taskManager.GetAllTasks().Where(eTask => eTask.state == TrangaTask.ExecutionState.Running)
|
||||||
|
.ToArray(), logger);
|
||||||
|
Console.WriteLine("Press any key.");
|
||||||
|
Thread.Sleep(500);
|
||||||
|
}
|
||||||
Console.ReadKey();
|
Console.ReadKey();
|
||||||
break;
|
break;
|
||||||
case ConsoleKey.K:
|
case ConsoleKey.K:
|
||||||
PrintTasks(
|
while (!Console.KeyAvailable)
|
||||||
taskManager.GetAllTasks().Where(qTask => qTask.state is TrangaTask.ExecutionState.Enqueued)
|
{
|
||||||
.ToArray(), logger);
|
PrintTasks(
|
||||||
Console.WriteLine("Press any key.");
|
taskManager.GetAllTasks()
|
||||||
|
.Where(qTask => qTask.state is TrangaTask.ExecutionState.Enqueued)
|
||||||
|
.ToArray(), logger);
|
||||||
|
Console.WriteLine("Press any key.");
|
||||||
|
Thread.Sleep(500);
|
||||||
|
}
|
||||||
Console.ReadKey();
|
Console.ReadKey();
|
||||||
break;
|
break;
|
||||||
case ConsoleKey.F:
|
case ConsoleKey.F:
|
||||||
TailLog(logger);
|
TailLog(logger);
|
||||||
Console.ReadKey();
|
Console.ReadKey();
|
||||||
break;
|
break;
|
||||||
case ConsoleKey.M:
|
case ConsoleKey.G:
|
||||||
RemoveTaskFromQueue(taskManager, logger);
|
RemoveTaskFromQueue(taskManager, logger);
|
||||||
Console.WriteLine("Press any key.");
|
Console.WriteLine("Press any key.");
|
||||||
Console.ReadKey();
|
Console.ReadKey();
|
||||||
@ -153,8 +200,13 @@ public static class Tranga_Cli
|
|||||||
Console.WriteLine("Press any key.");
|
Console.WriteLine("Press any key.");
|
||||||
Console.ReadKey();
|
Console.ReadKey();
|
||||||
break;
|
break;
|
||||||
|
case ConsoleKey.M:
|
||||||
|
AddMangaTaskToQueue(taskManager, logger);
|
||||||
|
Console.WriteLine("Press any key.");
|
||||||
|
Console.ReadKey();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
PrintMenu(taskManager, taskManager.settings.downloadLocation, logger);
|
PrintMenu(taskManager, taskManager.settings.downloadLocation);
|
||||||
}
|
}
|
||||||
Thread.Sleep(200);
|
Thread.Sleep(200);
|
||||||
}
|
}
|
||||||
@ -174,7 +226,7 @@ public static class Tranga_Cli
|
|||||||
taskManager.Shutdown(false);
|
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 taskCount = taskManager.GetAllTasks().Length;
|
||||||
int taskRunningCount = taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Running);
|
int taskRunningCount = taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Running);
|
||||||
@ -186,8 +238,8 @@ public static class Tranga_Cli
|
|||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine($"{"C: Create Task",-30}{"L: List tasks",-30}{"B: Enqueue Task", -30}");
|
Console.WriteLine($"{"C: Create Task",-30}{"L: List tasks",-30}{"B: Enqueue Task", -30}");
|
||||||
Console.WriteLine($"{"D: Delete Task",-30}{"S: Search Tasks", -30}{"K: List Task Queue", -30}");
|
Console.WriteLine($"{"D: Delete Task",-30}{"S: Search Tasks", -30}{"K: List Task Queue", -30}");
|
||||||
Console.WriteLine($"{"E: Execute Task now",-30}{"R: List Running Tasks", -30}{"M: Remove Task from Queue", -30}");
|
Console.WriteLine($"{"E: Execute Task now",-30}{"R: List Running Tasks", -30}{"G: Remove Task from Queue", -30}");
|
||||||
Console.WriteLine();
|
Console.WriteLine($"{"M: New Download Manga Task",-30}{"", -30}{"", -30}");
|
||||||
Console.WriteLine($"{"",-30}{"F: Show Log",-30}{"Q: Exit",-30}");
|
Console.WriteLine($"{"",-30}{"F: Show Log",-30}{"Q: Exit",-30}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,14 +253,18 @@ public static class Tranga_Cli
|
|||||||
int tIndex = 0;
|
int tIndex = 0;
|
||||||
Console.WriteLine($"Tasks (Running/Queue/Total): {taskRunningCount}/{taskEnqueuedCount}/{taskCount}");
|
Console.WriteLine($"Tasks (Running/Queue/Total): {taskRunningCount}/{taskEnqueuedCount}/{taskCount}");
|
||||||
string header =
|
string header =
|
||||||
$"{"",-5}{"Task",-20} | {"Last Executed",-20} | {"Reoccurrence",-12} | {"State",-10} | {"Connector",-15} | Publication/Manga";
|
$"{"",-5}{"Task",-20} | {"Last Executed",-20} | {"Reoccurrence",-12} | {"State",-10} | {"Progress",-9} | {"Finished",-20} | {"Remaining",-12} | {"Connector",-15} | Publication/Manga ";
|
||||||
Console.WriteLine(header);
|
Console.WriteLine(header);
|
||||||
Console.WriteLine(new string('-', header.Length));
|
Console.WriteLine(new string('-', header.Length));
|
||||||
foreach(TrangaTask trangaTask in tasks)
|
foreach (TrangaTask trangaTask in tasks)
|
||||||
Console.WriteLine($"{tIndex++:000}: {trangaTask}");
|
{
|
||||||
|
string[] taskSplit = trangaTask.ToString().Split(", ");
|
||||||
|
Console.WriteLine($"{tIndex++:000}: {taskSplit[0],-20} | {taskSplit[1],-20} | {taskSplit[2],-12} | {taskSplit[3],-10} | {taskSplit[4],-9} | {taskSplit[5],-20} | {taskSplit[6][..12],-12} | {(taskSplit.Length > 7 ? taskSplit[7] : ""),-15} | {(taskSplit.Length > 8 ? taskSplit[8] : "")} {(taskSplit.Length > 9 ? taskSplit[9] : "")} {(taskSplit.Length > 10 ? taskSplit[10] : "")}");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TrangaTask? SelectTask(TrangaTask[] tasks, Logger logger)
|
private static TrangaTask[] SelectTasks(TrangaTask[] tasks, Logger logger)
|
||||||
{
|
{
|
||||||
logger.WriteLine("Tranga_CLI", "Menu: Select task");
|
logger.WriteLine("Tranga_CLI", "Menu: Select task");
|
||||||
if (tasks.Length < 1)
|
if (tasks.Length < 1)
|
||||||
@ -216,13 +272,13 @@ public static class Tranga_Cli
|
|||||||
Console.Clear();
|
Console.Clear();
|
||||||
Console.WriteLine("There are no available Tasks.");
|
Console.WriteLine("There are no available Tasks.");
|
||||||
logger.WriteLine("Tranga_CLI", "No available Tasks.");
|
logger.WriteLine("Tranga_CLI", "No available Tasks.");
|
||||||
return null;
|
return Array.Empty<TrangaTask>();
|
||||||
}
|
}
|
||||||
PrintTasks(tasks, logger);
|
PrintTasks(tasks, logger);
|
||||||
|
|
||||||
logger.WriteLine("Tranga_CLI", "Selecting Task to Remove (from queue)");
|
logger.WriteLine("Tranga_CLI", "Selecting Task to Remove (from queue)");
|
||||||
Console.WriteLine("Enter q to abort");
|
Console.WriteLine("Enter q to abort");
|
||||||
Console.WriteLine($"Select Task (0-{tasks.Length - 1}):");
|
Console.WriteLine($"Select Task(s) (0-{tasks.Length - 1}):");
|
||||||
|
|
||||||
string? selectedTask = Console.ReadLine();
|
string? selectedTask = Console.ReadLine();
|
||||||
while(selectedTask is null || selectedTask.Length < 1)
|
while(selectedTask is null || selectedTask.Length < 1)
|
||||||
@ -233,22 +289,39 @@ public static class Tranga_Cli
|
|||||||
Console.Clear();
|
Console.Clear();
|
||||||
Console.WriteLine("aborted.");
|
Console.WriteLine("aborted.");
|
||||||
logger.WriteLine("Tranga_CLI", "aborted");
|
logger.WriteLine("Tranga_CLI", "aborted");
|
||||||
return null;
|
return Array.Empty<TrangaTask>();
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
int selectedTaskIndex = Convert.ToInt32(selectedTask);
|
|
||||||
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
|
|
||||||
return tasks[selectedTaskIndex];
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Exception: {e.Message}");
|
|
||||||
logger.WriteLine("Tranga_CLI", e.Message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
if (selectedTask.Contains('-'))
|
||||||
|
{
|
||||||
|
int start = Convert.ToInt32(selectedTask.Split('-')[0]);
|
||||||
|
int end = Convert.ToInt32(selectedTask.Split('-')[1]);
|
||||||
|
return tasks[start..end];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int selectedTaskIndex = Convert.ToInt32(selectedTask);
|
||||||
|
return new[] { tasks[selectedTaskIndex] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddMangaTaskToQueue(TaskManager taskManager, Logger logger)
|
||||||
|
{
|
||||||
|
Console.Clear();
|
||||||
|
logger.WriteLine("Tranga_CLI", "Menu: Add Manga Download to queue");
|
||||||
|
|
||||||
|
Connector? connector = SelectConnector(taskManager.GetAvailableConnectors().Values.ToArray(), logger);
|
||||||
|
if (connector is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Publication? publication = SelectPublication(taskManager, connector, 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.Value.publicationId, reoccurrence, "en");
|
||||||
|
Console.WriteLine(newTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddTaskToQueue(TaskManager taskManager, Logger logger)
|
private static void AddTaskToQueue(TaskManager taskManager, Logger logger)
|
||||||
@ -259,12 +332,10 @@ public static class Tranga_Cli
|
|||||||
TrangaTask[] tasks = taskManager.GetAllTasks().Where(rTask =>
|
TrangaTask[] tasks = taskManager.GetAllTasks().Where(rTask =>
|
||||||
rTask.state is not TrangaTask.ExecutionState.Enqueued and not TrangaTask.ExecutionState.Running).ToArray();
|
rTask.state is not TrangaTask.ExecutionState.Enqueued and not TrangaTask.ExecutionState.Running).ToArray();
|
||||||
|
|
||||||
TrangaTask? selectedTask = SelectTask(tasks, logger);
|
TrangaTask[] selectedTasks = SelectTasks(tasks, logger);
|
||||||
if (selectedTask is null)
|
logger.WriteLine("Tranga_CLI", $"Sending {selectedTasks.Length} Tasks to TaskManager");
|
||||||
return;
|
foreach(TrangaTask task in selectedTasks)
|
||||||
|
taskManager.AddTaskToQueue(task);
|
||||||
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
|
|
||||||
taskManager.AddTaskToQueue(selectedTask);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RemoveTaskFromQueue(TaskManager taskManager, Logger logger)
|
private static void RemoveTaskFromQueue(TaskManager taskManager, Logger logger)
|
||||||
@ -274,12 +345,10 @@ public static class Tranga_Cli
|
|||||||
|
|
||||||
TrangaTask[] tasks = taskManager.GetAllTasks().Where(rTask => rTask.state is TrangaTask.ExecutionState.Enqueued).ToArray();
|
TrangaTask[] tasks = taskManager.GetAllTasks().Where(rTask => rTask.state is TrangaTask.ExecutionState.Enqueued).ToArray();
|
||||||
|
|
||||||
TrangaTask? selectedTask = SelectTask(tasks, logger);
|
TrangaTask[] selectedTasks = SelectTasks(tasks, logger);
|
||||||
if (selectedTask is null)
|
logger.WriteLine("Tranga_CLI", $"Sending {selectedTasks.Length} Tasks to TaskManager");
|
||||||
return;
|
foreach(TrangaTask task in selectedTasks)
|
||||||
|
taskManager.RemoveTaskFromQueue(task);
|
||||||
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
|
|
||||||
taskManager.RemoveTaskFromQueue(selectedTask);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TailLog(Logger logger)
|
private static void TailLog(Logger logger)
|
||||||
@ -300,34 +369,48 @@ public static class Tranga_Cli
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CreateTask(TaskManager taskManager, TaskManager.SettingsData settings, Logger logger)
|
private static void CreateTask(TaskManager taskManager, Logger logger)
|
||||||
{
|
{
|
||||||
logger.WriteLine("Tranga_CLI", "Menu: Creating Task");
|
logger.WriteLine("Tranga_CLI", "Menu: Creating Task");
|
||||||
TrangaTask.Task? tmpTask = SelectTaskType(logger);
|
TrangaTask.Task? tmpTask = SelectTaskType(logger);
|
||||||
if (tmpTask is null)
|
if (tmpTask is null)
|
||||||
return;
|
return;
|
||||||
TrangaTask.Task task = (TrangaTask.Task)tmpTask!;
|
TrangaTask.Task task = (TrangaTask.Task)tmpTask;
|
||||||
|
|
||||||
Connector? connector = null;
|
Connector? connector = null;
|
||||||
if (task != TrangaTask.Task.UpdateKomgaLibrary)
|
if (task != TrangaTask.Task.UpdateLibraries)
|
||||||
{
|
{
|
||||||
connector = SelectConnector(settings.downloadLocation, taskManager.GetAvailableConnectors().Values.ToArray(), logger);
|
connector = SelectConnector(taskManager.GetAvailableConnectors().Values.ToArray(), logger);
|
||||||
if (connector is null)
|
if (connector is null)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Publication? publication = null;
|
Publication? publication = null;
|
||||||
if (task != TrangaTask.Task.UpdatePublications && task != TrangaTask.Task.UpdateKomgaLibrary)
|
if (task != TrangaTask.Task.UpdateLibraries)
|
||||||
{
|
{
|
||||||
publication = SelectPublication(connector!, logger);
|
publication = SelectPublication(taskManager, connector!, logger);
|
||||||
if (publication is null)
|
if (publication is null)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
TimeSpan reoccurrence = SelectReoccurrence(logger);
|
if (task is TrangaTask.Task.DownloadNewChapters)
|
||||||
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
|
{
|
||||||
TrangaTask newTask = taskManager.AddTask(task, connector?.name, publication, reoccurrence, "en");
|
TimeSpan reoccurrence = SelectReoccurrence(logger);
|
||||||
Console.WriteLine(newTask);
|
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
|
||||||
|
|
||||||
|
TrangaTask newTask = new DownloadNewChaptersTask(TrangaTask.Task.DownloadNewChapters, connector!.name, (Publication)publication!, reoccurrence, "en");
|
||||||
|
taskManager.AddTask(newTask);
|
||||||
|
Console.WriteLine(newTask);
|
||||||
|
}else if (task is TrangaTask.Task.DownloadChapter)
|
||||||
|
{
|
||||||
|
foreach (Chapter chapter in SelectChapters(connector!, (Publication)publication!, logger))
|
||||||
|
{
|
||||||
|
TrangaTask newTask = new DownloadChapterTask(TrangaTask.Task.DownloadChapter, connector!.name,
|
||||||
|
(Publication)publication!, chapter, "en");
|
||||||
|
taskManager.AddTask(newTask);
|
||||||
|
Console.WriteLine(newTask);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ExecuteTaskNow(TaskManager taskManager, Logger logger)
|
private static void ExecuteTaskNow(TaskManager taskManager, Logger logger)
|
||||||
@ -335,12 +418,10 @@ public static class Tranga_Cli
|
|||||||
logger.WriteLine("Tranga_CLI", "Menu: Executing Task");
|
logger.WriteLine("Tranga_CLI", "Menu: Executing Task");
|
||||||
TrangaTask[] tasks = taskManager.GetAllTasks().Where(nTask => nTask.state is not TrangaTask.ExecutionState.Running).ToArray();
|
TrangaTask[] tasks = taskManager.GetAllTasks().Where(nTask => nTask.state is not TrangaTask.ExecutionState.Running).ToArray();
|
||||||
|
|
||||||
TrangaTask? selectedTask = SelectTask(tasks, logger);
|
TrangaTask[] selectedTasks = SelectTasks(tasks, logger);
|
||||||
if (selectedTask is null)
|
logger.WriteLine("Tranga_CLI", $"Sending {selectedTasks.Length} Tasks to TaskManager");
|
||||||
return;
|
foreach(TrangaTask task in selectedTasks)
|
||||||
|
taskManager.ExecuteTaskNow(task);
|
||||||
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
|
|
||||||
taskManager.ExecuteTaskNow(selectedTask);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void DeleteTask(TaskManager taskManager, Logger logger)
|
private static void DeleteTask(TaskManager taskManager, Logger logger)
|
||||||
@ -348,12 +429,10 @@ public static class Tranga_Cli
|
|||||||
logger.WriteLine("Tranga_CLI", "Menu: Delete Task");
|
logger.WriteLine("Tranga_CLI", "Menu: Delete Task");
|
||||||
TrangaTask[] tasks = taskManager.GetAllTasks();
|
TrangaTask[] tasks = taskManager.GetAllTasks();
|
||||||
|
|
||||||
TrangaTask? selectedTask = SelectTask(tasks, logger);
|
TrangaTask[] selectedTasks = SelectTasks(tasks, logger);
|
||||||
if (selectedTask is null)
|
logger.WriteLine("Tranga_CLI", $"Sending {selectedTasks.Length} Tasks to TaskManager");
|
||||||
return;
|
foreach(TrangaTask task in selectedTasks)
|
||||||
|
taskManager.DeleteTask(task);
|
||||||
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
|
|
||||||
taskManager.DeleteTask(selectedTask.task, selectedTask.connectorName, selectedTask.publication);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TrangaTask.Task? SelectTaskType(Logger logger)
|
private static TrangaTask.Task? SelectTaskType(Logger logger)
|
||||||
@ -404,7 +483,26 @@ public static class Tranga_Cli
|
|||||||
return TimeSpan.Parse(Console.ReadLine()!, new CultureInfo("en-US"));
|
return TimeSpan.Parse(Console.ReadLine()!, new CultureInfo("en-US"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Connector? SelectConnector(string folderPath, Connector[] connectors, Logger logger)
|
private static Chapter[] SelectChapters(Connector connector, Publication publication, Logger logger)
|
||||||
|
{
|
||||||
|
logger.WriteLine("Tranga_CLI", "Menu: Select Chapters");
|
||||||
|
Chapter[] availableChapters = connector.GetChapters(publication, "en");
|
||||||
|
int cIndex = 0;
|
||||||
|
Console.WriteLine("Chapters:");
|
||||||
|
foreach(Chapter chapter in availableChapters)
|
||||||
|
Console.WriteLine($"{cIndex++}: Vol.{chapter.volumeNumber} Ch.{chapter.chapterNumber} - {chapter.name}");
|
||||||
|
|
||||||
|
Console.WriteLine("Enter q to abort");
|
||||||
|
Console.WriteLine($"Select Chapter(s):");
|
||||||
|
|
||||||
|
string? selectedChapters = Console.ReadLine();
|
||||||
|
while(selectedChapters is null || selectedChapters.Length < 1)
|
||||||
|
selectedChapters = Console.ReadLine();
|
||||||
|
|
||||||
|
return connector.SearchChapters(publication, selectedChapters);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Connector? SelectConnector(Connector[] connectors, Logger logger)
|
||||||
{
|
{
|
||||||
logger.WriteLine("Tranga_CLI", "Menu: Select Connector");
|
logger.WriteLine("Tranga_CLI", "Menu: Select Connector");
|
||||||
Console.Clear();
|
Console.Clear();
|
||||||
@ -443,7 +541,7 @@ public static class Tranga_Cli
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Publication? SelectPublication(Connector connector, Logger logger)
|
private static Publication? SelectPublication(TaskManager taskManager, Connector connector, Logger logger)
|
||||||
{
|
{
|
||||||
logger.WriteLine("Tranga_CLI", "Menu: Select Publication");
|
logger.WriteLine("Tranga_CLI", "Menu: Select Publication");
|
||||||
|
|
||||||
@ -452,7 +550,14 @@ public static class Tranga_Cli
|
|||||||
Console.WriteLine("Publication search query (leave empty for all):");
|
Console.WriteLine("Publication search query (leave empty for all):");
|
||||||
string? query = Console.ReadLine();
|
string? query = Console.ReadLine();
|
||||||
|
|
||||||
Publication[] publications = connector.GetPublications(query ?? "");
|
Publication[] publications = taskManager.GetPublicationsFromConnector(connector, query ?? "");
|
||||||
|
|
||||||
|
if (publications.Length < 1)
|
||||||
|
{
|
||||||
|
logger.WriteLine("Tranga_CLI", "No publications returned");
|
||||||
|
Console.WriteLine($"No publications for query '{query}' returned;");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
int pIndex = 0;
|
int pIndex = 0;
|
||||||
Console.WriteLine("Publications:");
|
Console.WriteLine("Publications:");
|
||||||
|
12
Tranga.sln
@ -4,10 +4,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tranga", ".\Tranga\Tranga.c
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tranga-CLI", "Tranga-CLI\Tranga-CLI.csproj", "{4899E3B2-B259-479A-B43E-042D043E9501}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tranga-CLI", "Tranga-CLI\Tranga-CLI.csproj", "{4899E3B2-B259-479A-B43E-042D043E9501}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tranga-API", "Tranga-API\Tranga-API.csproj", "{6284C936-4E90-486B-BC46-0AFAD85AD8EE}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logging", "Logging\Logging.csproj", "{415BE889-BB7D-426F-976F-8D977876A462}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logging", "Logging\Logging.csproj", "{415BE889-BB7D-426F-976F-8D977876A462}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tranga-API", "Tranga-API\Tranga-API.csproj", "{48F4E495-75BC-4402-8E03-DEC5B79D7E83}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -22,13 +22,13 @@ Global
|
|||||||
{4899E3B2-B259-479A-B43E-042D043E9501}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{4899E3B2-B259-479A-B43E-042D043E9501}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{4899E3B2-B259-479A-B43E-042D043E9501}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{4899E3B2-B259-479A-B43E-042D043E9501}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{4899E3B2-B259-479A-B43E-042D043E9501}.Release|Any CPU.Build.0 = Release|Any CPU
|
{4899E3B2-B259-479A-B43E-042D043E9501}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{6284C936-4E90-486B-BC46-0AFAD85AD8EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{6284C936-4E90-486B-BC46-0AFAD85AD8EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{6284C936-4E90-486B-BC46-0AFAD85AD8EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{6284C936-4E90-486B-BC46-0AFAD85AD8EE}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{415BE889-BB7D-426F-976F-8D977876A462}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{415BE889-BB7D-426F-976F-8D977876A462}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{415BE889-BB7D-426F-976F-8D977876A462}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{415BE889-BB7D-426F-976F-8D977876A462}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{415BE889-BB7D-426F-976F-8D977876A462}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{415BE889-BB7D-426F-976F-8D977876A462}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{415BE889-BB7D-426F-976F-8D977876A462}.Release|Any CPU.Build.0 = Release|Any CPU
|
{415BE889-BB7D-426F-976F-8D977876A462}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{48F4E495-75BC-4402-8E03-DEC5B79D7E83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{48F4E495-75BC-4402-8E03-DEC5B79D7E83}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{48F4E495-75BC-4402-8E03-DEC5B79D7E83}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{48F4E495-75BC-4402-8E03-DEC5B79D7E83}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Komga/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Komga/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Manganato/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mangasee/@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>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Tranga/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
@ -1,4 +1,5 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace Tranga;
|
namespace Tranga;
|
||||||
|
|
||||||
@ -14,20 +15,26 @@ public struct Chapter
|
|||||||
public string url { get; }
|
public string url { get; }
|
||||||
public string fileName { get; }
|
public string fileName { get; }
|
||||||
public string sortNumber { 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)
|
public Chapter(string? name, string? volumeNumber, string? chapterNumber, string url)
|
||||||
{
|
{
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.volumeNumber = volumeNumber is { Length: > 0 } ? volumeNumber : "1";
|
this.volumeNumber = volumeNumber;
|
||||||
this.chapterNumber = chapterNumber;
|
this.chapterNumber = chapterNumber;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
string chapterName = string.Concat((name ?? "").Split(Path.GetInvalidFileNameChars()));
|
|
||||||
NumberFormatInfo nfi = new NumberFormatInfo()
|
NumberFormatInfo nfi = new NumberFormatInfo()
|
||||||
{
|
{
|
||||||
NumberDecimalSeparator = "."
|
NumberDecimalSeparator = "."
|
||||||
};
|
};
|
||||||
sortNumber = decimal.Round(Convert.ToDecimal(this.volumeNumber) * Convert.ToDecimal(this.chapterNumber, nfi), 1)
|
sortNumber = decimal.Round(Convert.ToDecimal(this.volumeNumber ?? "1") * Convert.ToDecimal(this.chapterNumber, nfi), 1)
|
||||||
.ToString(nfi);
|
.ToString(nfi);
|
||||||
this.fileName = $"{chapterName} - V{volumeNumber}C{chapterNumber} - {sortNumber}";
|
|
||||||
|
string chapterName = string.Concat(LegalCharacters.Matches(name ?? ""));
|
||||||
|
string volStr = this.volumeNumber is not null ? $"Vol.{this.volumeNumber} " : "";
|
||||||
|
string chNumberStr = this.chapterNumber is not null ? $"Ch.{chapterNumber} " : "";
|
||||||
|
string chNameStr = chapterName.Length > 0 ? $"- {chapterName}" : "";
|
||||||
|
chNameStr = chNameStr.Replace("Volume", "").Replace("volume", "");
|
||||||
|
this.fileName = $"{volStr}{chNumberStr}{chNameStr}";
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,7 +1,11 @@
|
|||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
using Logging;
|
using Logging;
|
||||||
|
using Tranga.TrangaTasks;
|
||||||
|
using static System.IO.UnixFileMode;
|
||||||
|
|
||||||
namespace Tranga;
|
namespace Tranga;
|
||||||
|
|
||||||
@ -12,15 +16,23 @@ namespace Tranga;
|
|||||||
public abstract class Connector
|
public abstract class Connector
|
||||||
{
|
{
|
||||||
internal string downloadLocation { get; } //Location of local files
|
internal string downloadLocation { get; } //Location of local files
|
||||||
protected DownloadClient downloadClient { get; }
|
protected DownloadClient downloadClient { get; init; }
|
||||||
|
|
||||||
protected Logger? logger;
|
protected readonly Logger? logger;
|
||||||
|
|
||||||
protected Connector(string downloadLocation, uint downloadDelay, Logger? logger)
|
protected readonly string imageCachePath;
|
||||||
|
|
||||||
|
protected Connector(string downloadLocation, string imageCachePath, Logger? logger)
|
||||||
{
|
{
|
||||||
this.downloadLocation = downloadLocation;
|
this.downloadLocation = downloadLocation;
|
||||||
this.downloadClient = new DownloadClient(downloadDelay);
|
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
|
this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
|
||||||
|
{
|
||||||
|
//RequestTypes for RateLimits
|
||||||
|
}, logger);
|
||||||
|
this.imageCachePath = imageCachePath;
|
||||||
|
if (!Directory.Exists(imageCachePath))
|
||||||
|
Directory.CreateDirectory(this.imageCachePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract string name { get; } //Name of the Connector (e.g. Website)
|
public abstract string name { get; } //Name of the Connector (e.g. Website)
|
||||||
@ -41,36 +53,106 @@ public abstract class Connector
|
|||||||
/// <param name="language">Language of the Chapters</param>
|
/// <param name="language">Language of the Chapters</param>
|
||||||
/// <returns>Array of Chapters matching Publication and Language</returns>
|
/// <returns>Array of Chapters matching Publication and Language</returns>
|
||||||
public abstract Chapter[] GetChapters(Publication publication, string language = "");
|
public abstract Chapter[] GetChapters(Publication publication, string language = "");
|
||||||
|
|
||||||
|
public Chapter[] SearchChapters(Publication publication, string searchTerm, string? language = null)
|
||||||
|
{
|
||||||
|
Chapter[] availableChapters = this.GetChapters(publication, language??"en");
|
||||||
|
Regex volumeRegex = new ("((v(ol)*(olume)*)+ *([0-9]+(-[0-9]+)?){1})", RegexOptions.IgnoreCase);
|
||||||
|
Regex chapterRegex = new ("((c(h)*(hapter)*)+ *([0-9]+(-[0-9]+)?){1})", RegexOptions.IgnoreCase);
|
||||||
|
Regex singleResultRegex = new("([0-9]+)", RegexOptions.IgnoreCase);
|
||||||
|
Regex rangeResultRegex = new("([0-9]+(-[0-9]+))", RegexOptions.IgnoreCase);
|
||||||
|
if (volumeRegex.IsMatch(searchTerm) && chapterRegex.IsMatch(searchTerm))
|
||||||
|
{
|
||||||
|
string volume = singleResultRegex.Match(volumeRegex.Match(searchTerm).Value).Value;
|
||||||
|
string chapter = singleResultRegex.Match(chapterRegex.Match(searchTerm).Value).Value;
|
||||||
|
return availableChapters.Where(aCh => aCh.volumeNumber is not null && aCh.chapterNumber is not null &&
|
||||||
|
aCh.volumeNumber.Equals(volume, StringComparison.InvariantCultureIgnoreCase) &&
|
||||||
|
aCh.chapterNumber.Equals(chapter, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
else if (volumeRegex.IsMatch(searchTerm))
|
||||||
|
{
|
||||||
|
string volume = volumeRegex.Match(searchTerm).Value;
|
||||||
|
if (rangeResultRegex.IsMatch(volume))
|
||||||
|
{
|
||||||
|
string range = rangeResultRegex.Match(volume).Value;
|
||||||
|
int start = Convert.ToInt32(range.Split('-')[0]);
|
||||||
|
int end = Convert.ToInt32(range.Split('-')[1]);
|
||||||
|
return availableChapters.Where(aCh => aCh.volumeNumber is not null &&
|
||||||
|
Convert.ToInt32(aCh.volumeNumber) >= start &&
|
||||||
|
Convert.ToInt32(aCh.volumeNumber) <= end).ToArray();
|
||||||
|
}
|
||||||
|
else if(singleResultRegex.IsMatch(volume))
|
||||||
|
return availableChapters.Where(aCh =>
|
||||||
|
aCh.volumeNumber is not null &&
|
||||||
|
aCh.volumeNumber.Equals(volume, StringComparison.InvariantCultureIgnoreCase)).ToArray();
|
||||||
|
|
||||||
|
}
|
||||||
|
else if (chapterRegex.IsMatch(searchTerm))
|
||||||
|
{
|
||||||
|
string chapter = volumeRegex.Match(searchTerm).Value;
|
||||||
|
if (rangeResultRegex.IsMatch(chapter))
|
||||||
|
{
|
||||||
|
string range = rangeResultRegex.Match(chapter).Value;
|
||||||
|
int start = Convert.ToInt32(range.Split('-')[0]);
|
||||||
|
int end = Convert.ToInt32(range.Split('-')[1]);
|
||||||
|
return availableChapters.Where(aCh => aCh.chapterNumber is not null &&
|
||||||
|
Convert.ToInt32(aCh.chapterNumber) >= start &&
|
||||||
|
Convert.ToInt32(aCh.chapterNumber) <= end).ToArray();
|
||||||
|
}
|
||||||
|
else if(singleResultRegex.IsMatch(chapter))
|
||||||
|
return availableChapters.Where(aCh =>
|
||||||
|
aCh.chapterNumber is not null &&
|
||||||
|
aCh.chapterNumber.Equals(chapter, StringComparison.InvariantCultureIgnoreCase)).ToArray();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (rangeResultRegex.IsMatch(searchTerm))
|
||||||
|
{
|
||||||
|
int start = Convert.ToInt32(searchTerm.Split('-')[0]);
|
||||||
|
int end = Convert.ToInt32(searchTerm.Split('-')[1]);
|
||||||
|
return availableChapters[start..(end + 1)];
|
||||||
|
}
|
||||||
|
else if(singleResultRegex.IsMatch(searchTerm))
|
||||||
|
return new [] { availableChapters[Convert.ToInt32(searchTerm)] };
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.Empty<Chapter>();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves the Chapter (+Images) from the website.
|
/// Retrieves the Chapter (+Images) from the website.
|
||||||
/// 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>
|
/// </summary>
|
||||||
/// <param name="publication">Publication that contains Chapter</param>
|
/// <param name="publication">Publication that contains Chapter</param>
|
||||||
/// <param name="chapter">Chapter with Images to retrieve</param>
|
/// <param name="chapter">Chapter with Images to retrieve</param>
|
||||||
public abstract void DownloadChapter(Publication publication, Chapter chapter);
|
/// <param name="parentTask">Will be used for progress-tracking</param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
/// <summary>
|
public abstract void DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null);
|
||||||
/// Retrieves the Cover from the Website
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="publication">Publication to retrieve Cover for</param>
|
|
||||||
public abstract void DownloadCover(Publication publication);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Saves the series-info to series.json in the Publication Folder
|
/// Copies the already downloaded cover from cache to downloadLocation
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="publication">Publication to save series.json for</param>
|
/// <param name="publication">Publication to retrieve Cover 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}");
|
logger?.WriteLine(this.GetType().ToString(), $"Cloning cover {publication.sortName} {publication.internalId}");
|
||||||
//Check if Publication already has a Folder and a series.json
|
//Check if Publication already has a Folder and cover
|
||||||
string publicationFolder = Path.Join(downloadLocation, publication.folderName);
|
string publicationFolder = publication.CreatePublicationFolder(downloadLocation);
|
||||||
if(!Directory.Exists(publicationFolder))
|
DirectoryInfo dirInfo = new (publicationFolder);
|
||||||
Directory.CreateDirectory(publicationFolder);
|
if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover.")))
|
||||||
|
{
|
||||||
string seriesInfoPath = Path.Join(publicationFolder, "series.json");
|
logger?.WriteLine(this.GetType().ToString(), $"Cover exists {publication.sortName}");
|
||||||
if(!File.Exists(seriesInfoPath))
|
return;
|
||||||
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);
|
||||||
|
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
File.SetUnixFileMode(newFilePath, GroupRead | GroupWrite | OtherRead | OtherWrite | UserRead | UserWrite);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -78,15 +160,16 @@ public abstract class Connector
|
|||||||
/// See ComicInfo.xml
|
/// See ComicInfo.xml
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>XML-string</returns>
|
/// <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",
|
XElement comicInfo = new XElement("ComicInfo",
|
||||||
new XElement("Tags", string.Join(',',publication.tags)),
|
new XElement("Tags", string.Join(',',publication.tags)),
|
||||||
new XElement("LanguageISO", publication.originalLanguage),
|
new XElement("LanguageISO", publication.originalLanguage),
|
||||||
new XElement("Title", chapter.name),
|
new XElement("Title", chapter.name),
|
||||||
|
new XElement("Writer", string.Join(',', publication.authors)),
|
||||||
new XElement("Volume", chapter.volumeNumber),
|
new XElement("Volume", chapter.volumeNumber),
|
||||||
new XElement("Number", chapter.chapterNumber) //TODO check if this is correct at some point
|
new XElement("Number", chapter.chapterNumber)
|
||||||
);
|
);
|
||||||
return comicInfo.ToString();
|
return comicInfo.ToString();
|
||||||
}
|
}
|
||||||
@ -95,52 +178,67 @@ public abstract class Connector
|
|||||||
/// Checks if a chapter-archive is already present
|
/// Checks if a chapter-archive is already present
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>true if chapter is present</returns>
|
/// <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));
|
Regex legalCharacters = new Regex(@"([A-z]*[0-9]* *\.*-*,*\]*\[*'*\'*\)*\(*~*!*)*");
|
||||||
|
string oldFilePath = Path.Join(downloadLocation, publication.folderName, $"{string.Concat(legalCharacters.Matches(chapter.name ?? ""))} - V{chapter.volumeNumber}C{chapter.chapterNumber} - {chapter.sortNumber}.cbz");
|
||||||
|
string oldFilePath2 = Path.Join(downloadLocation, publication.folderName, $"{string.Concat(legalCharacters.Matches(chapter.name ?? ""))} - VC{chapter.chapterNumber} - {chapter.chapterNumber}.cbz");
|
||||||
|
string newFilePath = GetArchiveFilePath(publication, chapter);
|
||||||
|
if (File.Exists(oldFilePath))
|
||||||
|
File.Move(oldFilePath, newFilePath);
|
||||||
|
else if (File.Exists(oldFilePath2))
|
||||||
|
File.Move(oldFilePath2, newFilePath);
|
||||||
|
return File.Exists(newFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates full file path of chapter-archive
|
/// Creates full file path of chapter-archive
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Filepath</returns>
|
/// <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);
|
return Path.Join(downloadLocation, publication.folderName, $"{publication.folderName} - {chapter.fileName}.cbz");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Downloads Image from URL and saves it to the given path(incl. fileName)
|
/// Downloads Image from URL and saves it to the given path(incl. fileName)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="imageUrl"></param>
|
/// <param name="imageUrl"></param>
|
||||||
/// <param name="fullPath"></param>
|
/// <param name="fullPath"></param>
|
||||||
/// <param name="downloadClient">DownloadClient of the connector</param>
|
/// <param name="requestType">RequestType for Rate-Limit</param>
|
||||||
protected static void DownloadImage(string imageUrl, string fullPath, DownloadClient downloadClient)
|
/// <param name="referrer">referrer used in html request header</param>
|
||||||
|
private void DownloadImage(string imageUrl, string fullPath, byte requestType, string? referrer = null)
|
||||||
{
|
{
|
||||||
DownloadClient.RequestResult requestResult = downloadClient.MakeRequest(imageUrl);
|
DownloadClient.RequestResult requestResult = downloadClient.MakeRequest(imageUrl, requestType, referrer);
|
||||||
byte[] buffer = new byte[requestResult.result.Length];
|
if (requestResult.result != Stream.Null)
|
||||||
requestResult.result.ReadExactly(buffer, 0, buffer.Length);
|
{
|
||||||
File.WriteAllBytes(fullPath, buffer);
|
byte[] buffer = new byte[requestResult.result.Length];
|
||||||
|
requestResult.result.ReadExactly(buffer, 0, buffer.Length);
|
||||||
|
File.WriteAllBytes(fullPath, buffer);
|
||||||
|
}else
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), "No Stream-Content in result.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Downloads all Images from URLs, Compresses to zip(cbz) and saves.
|
/// Downloads all Images from URLs, Compresses to zip(cbz) and saves.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="imageUrls">List of URLs to download Images from</param>
|
/// <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="saveArchiveFilePath">Full path to save archive to (without file ending .cbz)</param>
|
||||||
/// <param name="downloadClient">DownloadClient of the connector</param>
|
/// <param name="parentTask">Used for progress tracking</param>
|
||||||
/// <param name="comicInfoPath">Path of the generate Chapter ComicInfo.xml, if it was generated</param>
|
/// <param name="comicInfoPath">Path of the generate Chapter ComicInfo.xml, if it was generated</param>
|
||||||
protected static void DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, DownloadClient downloadClient, Logger? logger, string? comicInfoPath = null)
|
/// <param name="requestType">RequestType for RateLimits</param>
|
||||||
|
/// <param name="referrer">Used in http request header</param>
|
||||||
|
protected void DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, byte requestType, DownloadChapterTask parentTask, string? comicInfoPath = null, string? referrer = null, CancellationToken? cancellationToken = null)
|
||||||
{
|
{
|
||||||
logger?.WriteLine("Connector", "Downloading Images");
|
if (cancellationToken?.IsCancellationRequested??false)
|
||||||
|
return;
|
||||||
|
logger?.WriteLine("Connector", $"Downloading Images for {saveArchiveFilePath}");
|
||||||
//Check if Publication Directory already exists
|
//Check if Publication Directory already exists
|
||||||
string[] splitPath = saveArchiveFilePath.Split(Path.DirectorySeparatorChar);
|
string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!;
|
||||||
string directoryPath = Path.Combine(splitPath.Take(splitPath.Length - 1).ToArray());
|
|
||||||
if (!Directory.Exists(directoryPath))
|
if (!Directory.Exists(directoryPath))
|
||||||
Directory.CreateDirectory(directoryPath);
|
Directory.CreateDirectory(directoryPath);
|
||||||
|
|
||||||
string fullPath = $"{saveArchiveFilePath}.cbz";
|
if (File.Exists(saveArchiveFilePath)) //Don't download twice.
|
||||||
if (File.Exists(fullPath)) //Don't download twice.
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
//Create a temporary folder to store images
|
//Create a temporary folder to store images
|
||||||
@ -152,48 +250,107 @@ public abstract class Connector
|
|||||||
{
|
{
|
||||||
string[] split = imageUrl.Split('.');
|
string[] split = imageUrl.Split('.');
|
||||||
string extension = split[^1];
|
string extension = split[^1];
|
||||||
DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), downloadClient);
|
logger?.WriteLine("Connector", $"Downloading Image {chapter + 1:000}/{imageUrls.Length:000} {parentTask.publication.sortName} {parentTask.publication.internalId} Vol.{parentTask.chapter.volumeNumber} Ch.{parentTask.chapter.chapterNumber} {parentTask.progress:P2}");
|
||||||
|
DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), requestType, referrer);
|
||||||
|
parentTask.IncrementProgress(1.0 / imageUrls.Length);
|
||||||
|
if (cancellationToken?.IsCancellationRequested??false)
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(comicInfoPath is not null)
|
if(comicInfoPath is not null)
|
||||||
File.Copy(comicInfoPath, Path.Join(tempFolder, "ComicInfo.xml"));
|
File.Copy(comicInfoPath, Path.Join(tempFolder, "ComicInfo.xml"));
|
||||||
|
|
||||||
logger?.WriteLine("Connector", "Creating archive");
|
logger?.WriteLine("Connector", $"Creating archive {saveArchiveFilePath}");
|
||||||
//ZIP-it and ship-it
|
//ZIP-it and ship-it
|
||||||
ZipFile.CreateFromDirectory(tempFolder, fullPath);
|
ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath);
|
||||||
|
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
File.SetUnixFileMode(saveArchiveFilePath, GroupRead | GroupWrite | OtherRead | OtherWrite | UserRead | UserWrite);
|
||||||
Directory.Delete(tempFolder, true); //Cleanup
|
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
|
protected class DownloadClient
|
||||||
{
|
{
|
||||||
private readonly TimeSpan _requestSpeed;
|
|
||||||
private DateTime _lastRequest;
|
|
||||||
private static readonly HttpClient Client = new();
|
private static readonly HttpClient Client = new();
|
||||||
|
|
||||||
|
private readonly Dictionary<byte, DateTime> _lastExecutedRateLimit;
|
||||||
|
private readonly Dictionary<byte, TimeSpan> _rateLimit;
|
||||||
|
private Logger? logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a httpClient
|
/// Creates a httpClient
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="delay">minimum delay between requests (to avoid spam)</param>
|
/// <param name="rateLimitRequestsPerMinute">Rate limits for requests. byte is RequestType, int maximum requests per minute for RequestType</param>
|
||||||
public DownloadClient(uint delay)
|
/// <param name="logger"></param>
|
||||||
|
public DownloadClient(Dictionary<byte, int> rateLimitRequestsPerMinute, Logger? logger)
|
||||||
{
|
{
|
||||||
_requestSpeed = TimeSpan.FromMilliseconds(delay);
|
this.logger = logger;
|
||||||
_lastRequest = DateTime.Now.Subtract(_requestSpeed);
|
_lastExecutedRateLimit = new();
|
||||||
|
_rateLimit = new();
|
||||||
|
foreach(KeyValuePair<byte, int> limit in rateLimitRequestsPerMinute)
|
||||||
|
_rateLimit.Add(limit.Key, TimeSpan.FromMinutes(1).Divide(limit.Value));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Request Webpage
|
/// Request Webpage
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="url"></param>
|
/// <param name="url"></param>
|
||||||
|
/// <param name="requestType">For RateLimits: Same Endpoints use same type</param>
|
||||||
|
/// <param name="referrer">Used in http request header</param>
|
||||||
/// <returns>RequestResult with StatusCode and Stream of received data</returns>
|
/// <returns>RequestResult with StatusCode and Stream of received data</returns>
|
||||||
public RequestResult MakeRequest(string url)
|
public RequestResult MakeRequest(string url, byte requestType, string? referrer = null)
|
||||||
{
|
{
|
||||||
while((DateTime.Now - _lastRequest) < _requestSpeed)
|
if (_rateLimit.TryGetValue(requestType, out TimeSpan value))
|
||||||
Thread.Sleep(10);
|
_lastExecutedRateLimit.TryAdd(requestType, DateTime.Now.Subtract(value));
|
||||||
_lastRequest = DateTime.Now;
|
else
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), "RequestType not configured for rate-limit.");
|
||||||
|
return new RequestResult(HttpStatusCode.NotAcceptable, Stream.Null);
|
||||||
|
}
|
||||||
|
|
||||||
HttpRequestMessage requestMessage = new(HttpMethod.Get, url);
|
TimeSpan rateLimitTimeout = _rateLimit[requestType]
|
||||||
HttpResponseMessage response = Client.Send(requestMessage);
|
.Subtract(DateTime.Now.Subtract(_lastExecutedRateLimit[requestType]));
|
||||||
|
|
||||||
|
if(rateLimitTimeout > TimeSpan.Zero)
|
||||||
|
Thread.Sleep(rateLimitTimeout);
|
||||||
|
|
||||||
|
HttpResponseMessage? response = null;
|
||||||
|
while (response is null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HttpRequestMessage requestMessage = new(HttpMethod.Get, url);
|
||||||
|
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}... Retrying.");
|
||||||
|
Thread.Sleep(_rateLimit[requestType] * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
Stream resultString = response.IsSuccessStatusCode ? response.Content.ReadAsStream() : Stream.Null;
|
Stream resultString = response.IsSuccessStatusCode ? response.Content.ReadAsStream() : Stream.Null;
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Request-Error {response.StatusCode}: {response.ReasonPhrase}");
|
||||||
return new RequestResult(response.StatusCode, resultString);
|
return new RequestResult(response.StatusCode, resultString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,20 +3,33 @@ using System.Net;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using Logging;
|
using Logging;
|
||||||
|
using Tranga.TrangaTasks;
|
||||||
|
|
||||||
namespace Tranga.Connectors;
|
namespace Tranga.Connectors;
|
||||||
public class MangaDex : Connector
|
public class MangaDex : Connector
|
||||||
{
|
{
|
||||||
public override string name { get; }
|
public override string name { get; }
|
||||||
|
|
||||||
public MangaDex(string downloadLocation, uint downloadDelay, Logger? logger) : base(downloadLocation, downloadDelay, logger)
|
private enum RequestType : byte
|
||||||
{
|
{
|
||||||
name = "MangaDex";
|
Manga,
|
||||||
|
Feed,
|
||||||
|
AtHomeServer,
|
||||||
|
CoverUrl,
|
||||||
|
Author,
|
||||||
}
|
}
|
||||||
|
|
||||||
public MangaDex(string downloadLocation, Logger? logger) : base(downloadLocation, 750, logger)
|
public MangaDex(string downloadLocation, string imageCachePath, Logger? logger) : base(downloadLocation, imageCachePath, logger)
|
||||||
{
|
{
|
||||||
name = "MangaDex";
|
name = "MangaDex";
|
||||||
|
this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
|
||||||
|
{
|
||||||
|
{(byte)RequestType.Manga, 250},
|
||||||
|
{(byte)RequestType.Feed, 250},
|
||||||
|
{(byte)RequestType.AtHomeServer, 40},
|
||||||
|
{(byte)RequestType.CoverUrl, 250},
|
||||||
|
{(byte)RequestType.Author, 250}
|
||||||
|
}, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Publication[] GetPublications(string publicationTitle = "")
|
public override Publication[] GetPublications(string publicationTitle = "")
|
||||||
@ -26,12 +39,13 @@ public class MangaDex : Connector
|
|||||||
int offset = 0; //"Page"
|
int offset = 0; //"Page"
|
||||||
int total = int.MaxValue; //How many total results are there, is updated on first request
|
int total = int.MaxValue; //How many total results are there, is updated on first request
|
||||||
HashSet<Publication> publications = new();
|
HashSet<Publication> publications = new();
|
||||||
|
int loadedPublicationData = 0;
|
||||||
while (offset < total) //As long as we haven't requested all "Pages"
|
while (offset < total) //As long as we haven't requested all "Pages"
|
||||||
{
|
{
|
||||||
//Request next Page
|
//Request next Page
|
||||||
DownloadClient.RequestResult requestResult =
|
DownloadClient.RequestResult requestResult =
|
||||||
downloadClient.MakeRequest(
|
downloadClient.MakeRequest(
|
||||||
$"https://api.mangadex.org/manga?limit={limit}&title={publicationTitle}&offset={offset}");
|
$"https://api.mangadex.org/manga?limit={limit}&title={publicationTitle}&offset={offset}", (byte)RequestType.Manga);
|
||||||
if (requestResult.statusCode != HttpStatusCode.OK)
|
if (requestResult.statusCode != HttpStatusCode.OK)
|
||||||
break;
|
break;
|
||||||
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
||||||
@ -46,9 +60,12 @@ public class MangaDex : Connector
|
|||||||
//Loop each Manga and extract information from JSON
|
//Loop each Manga and extract information from JSON
|
||||||
foreach (JsonNode? mangeNode in mangaInResult)
|
foreach (JsonNode? mangeNode in mangaInResult)
|
||||||
{
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Getting publication data. {++loadedPublicationData}/{total}");
|
||||||
JsonObject manga = (JsonObject)mangeNode!;
|
JsonObject manga = (JsonObject)mangeNode!;
|
||||||
JsonObject attributes = manga["attributes"]!.AsObject();
|
JsonObject attributes = manga["attributes"]!.AsObject();
|
||||||
|
|
||||||
|
string publicationId = manga["id"]!.GetValue<string>();
|
||||||
|
|
||||||
string title = attributes["title"]!.AsObject().ContainsKey("en") && attributes["title"]!["en"] is not null
|
string title = attributes["title"]!.AsObject().ContainsKey("en") && attributes["title"]!["en"] is not null
|
||||||
? attributes["title"]!["en"]!.GetValue<string>()
|
? attributes["title"]!["en"]!.GetValue<string>()
|
||||||
: attributes["title"]![((IDictionary<string, JsonNode?>)attributes["title"]!.AsObject()).Keys.First()]!.GetValue<string>();
|
: attributes["title"]![((IDictionary<string, JsonNode?>)attributes["title"]!.AsObject()).Keys.First()]!.GetValue<string>();
|
||||||
@ -58,14 +75,12 @@ public class MangaDex : Connector
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
JsonArray altTitlesObject = attributes["altTitles"]!.AsArray();
|
JsonArray altTitlesObject = attributes["altTitles"]!.AsArray();
|
||||||
string[,] altTitles = new string[altTitlesObject.Count, 2];
|
Dictionary<string, string> altTitlesDict = new();
|
||||||
int titleIndex = 0;
|
|
||||||
foreach (JsonNode? altTitleNode in altTitlesObject)
|
foreach (JsonNode? altTitleNode in altTitlesObject)
|
||||||
{
|
{
|
||||||
JsonObject altTitleObject = (JsonObject)altTitleNode!;
|
JsonObject altTitleObject = (JsonObject)altTitleNode!;
|
||||||
string key = ((IDictionary<string, JsonNode?>)altTitleObject).Keys.ToArray()[0];
|
string key = ((IDictionary<string, JsonNode?>)altTitleObject).Keys.ToArray()[0];
|
||||||
altTitles[titleIndex, 0] = key;
|
altTitlesDict.TryAdd(key, altTitleObject[key]!.GetValue<string>());
|
||||||
altTitles[titleIndex++, 1] = altTitleObject[key]!.GetValue<string>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonArray tagsObject = attributes["tags"]!.AsArray();
|
JsonArray tagsObject = attributes["tags"]!.AsArray();
|
||||||
@ -77,23 +92,30 @@ public class MangaDex : Connector
|
|||||||
tags.Add(tagObject["attributes"]!["name"]!["en"]!.GetValue<string>());
|
tags.Add(tagObject["attributes"]!["name"]!["en"]!.GetValue<string>());
|
||||||
}
|
}
|
||||||
|
|
||||||
string? poster = null;
|
string? posterId = null;
|
||||||
|
HashSet<string> authorIds = new();
|
||||||
if (manga.ContainsKey("relationships") && manga["relationships"] is not null)
|
if (manga.ContainsKey("relationships") && manga["relationships"] is not null)
|
||||||
{
|
{
|
||||||
JsonArray relationships = manga["relationships"]!.AsArray();
|
JsonArray relationships = manga["relationships"]!.AsArray();
|
||||||
poster = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue<string>() == "cover_art")!["id"]!.GetValue<string>();
|
posterId = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue<string>() == "cover_art")!["id"]!.GetValue<string>();
|
||||||
|
foreach (JsonNode node in relationships.Where(relationship =>
|
||||||
|
relationship!["type"]!.GetValue<string>() == "author"))
|
||||||
|
authorIds.Add(node!["id"]!.GetValue<string>());
|
||||||
}
|
}
|
||||||
|
string? coverUrl = GetCoverUrl(publicationId, posterId);
|
||||||
|
string? coverCacheName = null;
|
||||||
|
if (coverUrl is not null)
|
||||||
|
coverCacheName = SaveCoverImageToCache(coverUrl, (byte)RequestType.AtHomeServer);
|
||||||
|
|
||||||
string[,]? links = null;
|
List<string> authors = GetAuthors(authorIds);
|
||||||
|
|
||||||
|
Dictionary<string, string> linksDict = new();
|
||||||
if (attributes.ContainsKey("links") && attributes["links"] is not null)
|
if (attributes.ContainsKey("links") && attributes["links"] is not null)
|
||||||
{
|
{
|
||||||
JsonObject linksObject = attributes["links"]!.AsObject();
|
JsonObject linksObject = attributes["links"]!.AsObject();
|
||||||
links = new string[linksObject.Count, 2];
|
|
||||||
int linkIndex = 0;
|
|
||||||
foreach (string key in ((IDictionary<string, JsonNode?>)linksObject).Keys)
|
foreach (string key in ((IDictionary<string, JsonNode?>)linksObject).Keys)
|
||||||
{
|
{
|
||||||
links[linkIndex, 0] = key;
|
linksDict.Add(key, linksObject[key]!.GetValue<string>());
|
||||||
links[linkIndex++, 1] = linksObject[key]!.GetValue<string>();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,28 +129,31 @@ public class MangaDex : Connector
|
|||||||
|
|
||||||
string status = attributes["status"]!.GetValue<string>();
|
string status = attributes["status"]!.GetValue<string>();
|
||||||
|
|
||||||
Publication pub = new Publication(
|
Publication pub = new (
|
||||||
title,
|
title,
|
||||||
|
authors,
|
||||||
description,
|
description,
|
||||||
altTitles,
|
altTitlesDict,
|
||||||
tags.ToArray(),
|
tags.ToArray(),
|
||||||
poster,
|
coverUrl,
|
||||||
links,
|
coverCacheName,
|
||||||
|
linksDict,
|
||||||
year,
|
year,
|
||||||
originalLanguage,
|
originalLanguage,
|
||||||
status,
|
status,
|
||||||
manga["id"]!.GetValue<string>()
|
publicationId
|
||||||
);
|
);
|
||||||
publications.Add(pub); //Add Publication (Manga) to result
|
publications.Add(pub); //Add Publication (Manga) to result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Done getting publications (title={publicationTitle})");
|
||||||
return publications.ToArray();
|
return publications.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Chapter[] GetChapters(Publication publication, string language = "")
|
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
|
const int limit = 100; //How many values we want returned at once
|
||||||
int offset = 0; //"Page"
|
int offset = 0; //"Page"
|
||||||
int total = int.MaxValue; //How many total results are there, is updated on first request
|
int total = int.MaxValue; //How many total results are there, is updated on first request
|
||||||
@ -139,7 +164,7 @@ public class MangaDex : Connector
|
|||||||
//Request next "Page"
|
//Request next "Page"
|
||||||
DownloadClient.RequestResult requestResult =
|
DownloadClient.RequestResult requestResult =
|
||||||
downloadClient.MakeRequest(
|
downloadClient.MakeRequest(
|
||||||
$"https://api.mangadex.org/manga/{publication.downloadUrl}/feed?limit={limit}&offset={offset}&translatedLanguage%5B%5D={language}");
|
$"https://api.mangadex.org/manga/{publication.publicationId}/feed?limit={limit}&offset={offset}&translatedLanguage%5B%5D={language}", (byte)RequestType.Feed);
|
||||||
if (requestResult.statusCode != HttpStatusCode.OK)
|
if (requestResult.statusCode != HttpStatusCode.OK)
|
||||||
break;
|
break;
|
||||||
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
||||||
@ -178,15 +203,18 @@ public class MangaDex : Connector
|
|||||||
{
|
{
|
||||||
NumberDecimalSeparator = "."
|
NumberDecimalSeparator = "."
|
||||||
};
|
};
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Done getting Chapters for {publication.internalId}");
|
||||||
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
|
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void DownloadChapter(Publication publication, Chapter chapter)
|
public override void DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null)
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Download Chapter {publication.sortName} {chapter.volumeNumber}-{chapter.chapterNumber}");
|
if (cancellationToken?.IsCancellationRequested??false)
|
||||||
|
return;
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
|
||||||
//Request URLs for Chapter-Images
|
//Request URLs for Chapter-Images
|
||||||
DownloadClient.RequestResult requestResult =
|
DownloadClient.RequestResult requestResult =
|
||||||
downloadClient.MakeRequest($"https://api.mangadex.org/at-home/server/{chapter.url}?forcePort443=false'");
|
downloadClient.MakeRequest($"https://api.mangadex.org/at-home/server/{chapter.url}?forcePort443=false'", (byte)RequestType.AtHomeServer);
|
||||||
if (requestResult.statusCode != HttpStatusCode.OK)
|
if (requestResult.statusCode != HttpStatusCode.OK)
|
||||||
return;
|
return;
|
||||||
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
||||||
@ -202,45 +230,54 @@ public class MangaDex : Connector
|
|||||||
imageUrls.Add($"{baseUrl}/data/{hash}/{image!.GetValue<string>()}");
|
imageUrls.Add($"{baseUrl}/data/{hash}/{image!.GetValue<string>()}");
|
||||||
|
|
||||||
string comicInfoPath = Path.GetTempFileName();
|
string comicInfoPath = Path.GetTempFileName();
|
||||||
File.WriteAllText(comicInfoPath, CreateComicInfo(publication, chapter, logger));
|
File.WriteAllText(comicInfoPath, GetComicInfoXmlString(publication, chapter, logger));
|
||||||
|
|
||||||
//Download Chapter-Images
|
//Download Chapter-Images
|
||||||
DownloadChapterImages(imageUrls.ToArray(), CreateFullFilepath(publication, chapter), downloadClient, logger, comicInfoPath);
|
DownloadChapterImages(imageUrls.ToArray(), GetArchiveFilePath(publication, chapter), (byte)RequestType.AtHomeServer, parentTask, comicInfoPath, cancellationToken:cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void DownloadCover(Publication publication)
|
private string? GetCoverUrl(string publicationId, string? posterId)
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Download cover {publication.sortName}");
|
logger?.WriteLine(this.GetType().ToString(), $"Getting CoverUrl for {publicationId}");
|
||||||
//Check if Publication already has a Folder and cover
|
if (posterId is null)
|
||||||
string publicationFolder = Path.Join(downloadLocation, publication.folderName);
|
{
|
||||||
if(!Directory.Exists(publicationFolder))
|
logger?.WriteLine(this.GetType().ToString(), $"No posterId, aborting");
|
||||||
Directory.CreateDirectory(publicationFolder);
|
return null;
|
||||||
DirectoryInfo dirInfo = new (publicationFolder);
|
}
|
||||||
foreach(FileInfo fileInfo in dirInfo.EnumerateFiles())
|
|
||||||
if (fileInfo.Name.Contains("cover."))
|
|
||||||
return;
|
|
||||||
|
|
||||||
//Request information where to download Cover
|
//Request information where to download Cover
|
||||||
DownloadClient.RequestResult requestResult =
|
DownloadClient.RequestResult requestResult =
|
||||||
downloadClient.MakeRequest($"https://api.mangadex.org/cover/{publication.posterUrl}");
|
downloadClient.MakeRequest($"https://api.mangadex.org/cover/{posterId}", (byte)RequestType.CoverUrl);
|
||||||
if (requestResult.statusCode != HttpStatusCode.OK)
|
if (requestResult.statusCode != HttpStatusCode.OK)
|
||||||
return;
|
return null;
|
||||||
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
||||||
if (result is null)
|
if (result is null)
|
||||||
return;
|
return null;
|
||||||
|
|
||||||
string fileName = result["data"]!["attributes"]!["fileName"]!.GetValue<string>();
|
string fileName = result["data"]!["attributes"]!["fileName"]!.GetValue<string>();
|
||||||
|
|
||||||
string coverUrl = $"https://uploads.mangadex.org/covers/{publication.downloadUrl}/{fileName}";
|
string coverUrl = $"https://uploads.mangadex.org/covers/{publicationId}/{fileName}";
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Got Cover-Url for {publicationId} -> {coverUrl}");
|
||||||
//Get file-extension (jpg, png)
|
return coverUrl;
|
||||||
string[] split = coverUrl.Split('.');
|
}
|
||||||
string extension = split[^1];
|
|
||||||
|
|
||||||
string outFolderPath = Path.Join(downloadLocation, publication.folderName);
|
private List<string> GetAuthors(IEnumerable<string> authorIds)
|
||||||
Directory.CreateDirectory(outFolderPath);
|
{
|
||||||
|
List<string> ret = new();
|
||||||
//Download cover-Image
|
foreach (string authorId in authorIds)
|
||||||
DownloadImage(coverUrl, Path.Join(downloadLocation, publication.folderName, $"cover.{extension}"), this.downloadClient);
|
{
|
||||||
|
DownloadClient.RequestResult requestResult =
|
||||||
|
downloadClient.MakeRequest($"https://api.mangadex.org/author/{authorId}", (byte)RequestType.Author);
|
||||||
|
if (requestResult.statusCode != HttpStatusCode.OK)
|
||||||
|
return ret;
|
||||||
|
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
||||||
|
if (result is null)
|
||||||
|
return ret;
|
||||||
|
|
||||||
|
string authorName = result["data"]!["attributes"]!["name"]!.GetValue<string>();
|
||||||
|
ret.Add(authorName);
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Got author {authorId} -> {authorName}");
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
206
Tranga/Connectors/Manganato.cs
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using HtmlAgilityPack;
|
||||||
|
using Logging;
|
||||||
|
using Tranga.TrangaTasks;
|
||||||
|
|
||||||
|
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, 60}
|
||||||
|
}, logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Publication[] GetPublications(string publicationTitle = "")
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Getting Publications (title={publicationTitle})");
|
||||||
|
string sanitizedTitle = string.Concat(Regex.Matches(publicationTitle, "[A-z]* *")).ToLower().Replace(' ', '_');
|
||||||
|
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[] authors = Array.Empty<string>();
|
||||||
|
string originalLanguage = "";
|
||||||
|
|
||||||
|
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":
|
||||||
|
authors = value.Split('-');
|
||||||
|
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;
|
||||||
|
int year = Convert.ToInt32(yearString.Split(',')[^1]) + 2000;
|
||||||
|
|
||||||
|
return new Publication(sortName, authors.ToList(), 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 Chapters ordered by Chapter-Number
|
||||||
|
NumberFormatInfo chapterNumberFormatInfo = new()
|
||||||
|
{
|
||||||
|
NumberDecimalSeparator = "."
|
||||||
|
};
|
||||||
|
List<Chapter> chapters = ParseChaptersFromHtml(requestResult.result);
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Done getting Chapters for {publication.internalId}");
|
||||||
|
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<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("Chapter ")[1].Replace('-','.');
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
ret.Reverse();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null)
|
||||||
|
{
|
||||||
|
if (cancellationToken?.IsCancellationRequested??false)
|
||||||
|
return;
|
||||||
|
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, parentTask, comicInfoPath, "https://chapmanganato.com/", cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
244
Tranga/Connectors/Mangasee.cs
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
using HtmlAgilityPack;
|
||||||
|
using Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using PuppeteerSharp;
|
||||||
|
using Tranga.TrangaTasks;
|
||||||
|
|
||||||
|
namespace Tranga.Connectors;
|
||||||
|
|
||||||
|
public class Mangasee : Connector
|
||||||
|
{
|
||||||
|
public override string name { get; }
|
||||||
|
private IBrowser? _browser = null;
|
||||||
|
private const string ChromiumVersion = "1153303";
|
||||||
|
|
||||||
|
public Mangasee(string downloadLocation, string imageCachePath, Logger? logger) : base(downloadLocation,
|
||||||
|
imageCachePath, logger)
|
||||||
|
{
|
||||||
|
this.name = "Mangasee";
|
||||||
|
this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
|
||||||
|
{
|
||||||
|
{ (byte)1, 60 }
|
||||||
|
}, logger);
|
||||||
|
|
||||||
|
Task d = new Task(DownloadBrowser);
|
||||||
|
d.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void DownloadBrowser()
|
||||||
|
{
|
||||||
|
BrowserFetcher browserFetcher = new BrowserFetcher();
|
||||||
|
foreach(string rev in browserFetcher.LocalRevisions().Where(rev => rev != ChromiumVersion))
|
||||||
|
browserFetcher.Remove(rev);
|
||||||
|
if (!browserFetcher.LocalRevisions().Contains(ChromiumVersion))
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), "Downloading headless browser");
|
||||||
|
DateTime last = DateTime.Now.Subtract(TimeSpan.FromSeconds(5));
|
||||||
|
browserFetcher.DownloadProgressChanged += (sender, args) =>
|
||||||
|
{
|
||||||
|
double currentBytes = Convert.ToDouble(args.BytesReceived) / Convert.ToDouble(args.TotalBytesToReceive);
|
||||||
|
if (args.TotalBytesToReceive == args.BytesReceived)
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), "Browser downloaded.");
|
||||||
|
}
|
||||||
|
else if (DateTime.Now > last.AddSeconds(5))
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Browser download progress: {currentBytes:P2}");
|
||||||
|
last = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
if (!browserFetcher.CanDownloadAsync(ChromiumVersion).Result)
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Can't download browser version {ChromiumVersion}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await browserFetcher.DownloadAsync(ChromiumVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), "Starting browser.");
|
||||||
|
this._browser = await Puppeteer.LaunchAsync(new LaunchOptions
|
||||||
|
{
|
||||||
|
Headless = true,
|
||||||
|
ExecutablePath = browserFetcher.GetExecutablePath(ChromiumVersion),
|
||||||
|
Args = new [] {
|
||||||
|
"--disable-gpu",
|
||||||
|
"--disable-dev-shm-usage",
|
||||||
|
"--disable-setuid-sandbox",
|
||||||
|
"--no-sandbox"}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Publication[] GetPublications(string publicationTitle = "")
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Getting Publications (title={publicationTitle})");
|
||||||
|
string sanitizedTitle = string.Concat(Regex.Matches(publicationTitle, "[A-z]* *")).ToLower().Replace(' ', '+');
|
||||||
|
string requestUrl = $"https://mangasee123.com/_search.php";
|
||||||
|
DownloadClient.RequestResult requestResult =
|
||||||
|
downloadClient.MakeRequest(requestUrl, (byte)1);
|
||||||
|
if (requestResult.statusCode != HttpStatusCode.OK)
|
||||||
|
return Array.Empty<Publication>();
|
||||||
|
|
||||||
|
return ParsePublicationsFromHtml(requestResult.result, publicationTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Publication[] ParsePublicationsFromHtml(Stream html, string publicationTitle)
|
||||||
|
{
|
||||||
|
string jsonString = new StreamReader(html).ReadToEnd();
|
||||||
|
List<SearchResultItem> result = JsonConvert.DeserializeObject<List<SearchResultItem>>(jsonString)!;
|
||||||
|
Dictionary<SearchResultItem, int> queryFiltered = new();
|
||||||
|
foreach (SearchResultItem resultItem in result)
|
||||||
|
{
|
||||||
|
foreach (string term in publicationTitle.Split(' '))
|
||||||
|
if (resultItem.i.Contains(term, StringComparison.CurrentCultureIgnoreCase))
|
||||||
|
if (!queryFiltered.TryAdd(resultItem, 0))
|
||||||
|
queryFiltered[resultItem]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
queryFiltered = queryFiltered.Where(item => item.Value >= publicationTitle.Split(' ').Length - 1)
|
||||||
|
.ToDictionary(item => item.Key, item => item.Value);
|
||||||
|
|
||||||
|
HashSet<Publication> ret = new();
|
||||||
|
List<SearchResultItem> orderedFiltered =
|
||||||
|
queryFiltered.OrderBy(item => item.Value).ToDictionary(item => item.Key, item => item.Value).Keys.ToList();
|
||||||
|
|
||||||
|
foreach (SearchResultItem orderedItem in orderedFiltered)
|
||||||
|
{
|
||||||
|
DownloadClient.RequestResult requestResult =
|
||||||
|
downloadClient.MakeRequest($"https://mangasee123.com/manga/{orderedItem.i}", (byte)1);
|
||||||
|
if (requestResult.statusCode != HttpStatusCode.OK)
|
||||||
|
return Array.Empty<Publication>();
|
||||||
|
ret.Add(ParseSinglePublicationFromHtml(requestResult.result, orderedItem.s, orderedItem.i, orderedItem.a));
|
||||||
|
}
|
||||||
|
return ret.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Publication ParseSinglePublicationFromHtml(Stream html, string sortName, string publicationId, string[] a)
|
||||||
|
{
|
||||||
|
StreamReader reader = new (html);
|
||||||
|
string htmlString = reader.ReadToEnd();
|
||||||
|
HtmlDocument document = new ();
|
||||||
|
document.LoadHtml(htmlString);
|
||||||
|
|
||||||
|
string originalLanguage = "", status = "";
|
||||||
|
Dictionary<string, string> altTitles = new(), links = new();
|
||||||
|
HashSet<string> tags = new();
|
||||||
|
|
||||||
|
HtmlNode posterNode =
|
||||||
|
document.DocumentNode.Descendants("img").First(img => img.HasClass("img-fluid") && img.HasClass("bottom-5"));
|
||||||
|
string posterUrl = posterNode.GetAttributeValue("src", "");
|
||||||
|
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, 1);
|
||||||
|
|
||||||
|
HtmlNode attributes = document.DocumentNode.Descendants("div")
|
||||||
|
.First(div => div.HasClass("col-md-9") && div.HasClass("col-sm-8") && div.HasClass("top-5"))
|
||||||
|
.Descendants("ul").First();
|
||||||
|
|
||||||
|
HtmlNode[] authorsNodes = attributes.Descendants("li")
|
||||||
|
.First(node => node.InnerText.Contains("author(s):", StringComparison.CurrentCultureIgnoreCase))
|
||||||
|
.Descendants("a").ToArray();
|
||||||
|
List<string> authors = new();
|
||||||
|
foreach(HtmlNode authorNode in authorsNodes)
|
||||||
|
authors.Add(authorNode.InnerText);
|
||||||
|
|
||||||
|
HtmlNode[] genreNodes = attributes.Descendants("li")
|
||||||
|
.First(node => node.InnerText.Contains("genre(s):", StringComparison.CurrentCultureIgnoreCase))
|
||||||
|
.Descendants("a").ToArray();
|
||||||
|
foreach (HtmlNode genreNode in genreNodes)
|
||||||
|
tags.Add(genreNode.InnerText);
|
||||||
|
|
||||||
|
HtmlNode yearNode = attributes.Descendants("li")
|
||||||
|
.First(node => node.InnerText.Contains("released:", StringComparison.CurrentCultureIgnoreCase))
|
||||||
|
.Descendants("a").First();
|
||||||
|
int year = Convert.ToInt32(yearNode.InnerText);
|
||||||
|
|
||||||
|
HtmlNode[] statusNodes = attributes.Descendants("li")
|
||||||
|
.First(node => node.InnerText.Contains("status:", StringComparison.CurrentCultureIgnoreCase))
|
||||||
|
.Descendants("a").ToArray();
|
||||||
|
foreach(HtmlNode statusNode in statusNodes)
|
||||||
|
if (statusNode.InnerText.Contains("publish", StringComparison.CurrentCultureIgnoreCase))
|
||||||
|
status = statusNode.InnerText.Split(' ')[0];
|
||||||
|
|
||||||
|
HtmlNode descriptionNode = attributes.Descendants("li").First(node => node.InnerText.Contains("description:", StringComparison.CurrentCultureIgnoreCase)).Descendants("div").First();
|
||||||
|
string description = descriptionNode.InnerText;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
foreach(string at in a)
|
||||||
|
altTitles.Add((i++).ToString(), at);
|
||||||
|
|
||||||
|
return new Publication(sortName, authors, description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
|
||||||
|
year, originalLanguage, status, publicationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReSharper disable once ClassNeverInstantiated.Local Will be instantiated during deserialization
|
||||||
|
private class SearchResultItem
|
||||||
|
{
|
||||||
|
#pragma warning disable CS8618 //Will always be set
|
||||||
|
public string i { get; set; }
|
||||||
|
public string s { get; set; }
|
||||||
|
public string[] a { get; set; }
|
||||||
|
#pragma warning restore CS8618
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Chapter[] GetChapters(Publication publication, string language = "")
|
||||||
|
{
|
||||||
|
XDocument doc = XDocument.Load($"https://mangasee123.com/rss/{publication.publicationId}.xml");
|
||||||
|
XElement[] chapterItems = doc.Descendants("item").ToArray();
|
||||||
|
List<Chapter> ret = new();
|
||||||
|
foreach (XElement chapter in chapterItems)
|
||||||
|
{
|
||||||
|
string? volumeNumber = "1";
|
||||||
|
string chapterName = chapter.Descendants("title").First().Value;
|
||||||
|
string chapterNumber = Regex.Matches(chapterName, "[0-9]+")[^1].ToString();
|
||||||
|
|
||||||
|
string url = chapter.Descendants("link").First().Value;
|
||||||
|
url = url.Replace(Regex.Matches(url,"(-page-[0-9])")[0].ToString(),"");
|
||||||
|
ret.Add(new Chapter("", volumeNumber, chapterNumber, url));
|
||||||
|
}
|
||||||
|
|
||||||
|
//Return Chapters ordered by Chapter-Number
|
||||||
|
NumberFormatInfo chapterNumberFormatInfo = new()
|
||||||
|
{
|
||||||
|
NumberDecimalSeparator = "."
|
||||||
|
};
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Done getting Chapters for {publication.internalId}");
|
||||||
|
return ret.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null)
|
||||||
|
{
|
||||||
|
if (cancellationToken?.IsCancellationRequested??false)
|
||||||
|
return;
|
||||||
|
while (this._browser is null && !(cancellationToken?.IsCancellationRequested??false))
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), "Waiting for headless browser to download...");
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
}
|
||||||
|
if (cancellationToken?.IsCancellationRequested??false)
|
||||||
|
return;
|
||||||
|
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
|
||||||
|
IPage page = _browser!.NewPageAsync().Result;
|
||||||
|
IResponse response = page.GoToAsync(chapter.url).Result;
|
||||||
|
if (response.Ok)
|
||||||
|
{
|
||||||
|
HtmlDocument document = new ();
|
||||||
|
document.LoadHtml(page.GetContentAsync().Result);
|
||||||
|
|
||||||
|
HtmlNode gallery = document.DocumentNode.Descendants("div").First(div => div.HasClass("ImageGallery"));
|
||||||
|
HtmlNode[] images = gallery.Descendants("img").Where(img => img.HasClass("img-fluid")).ToArray();
|
||||||
|
List<string> urls = new();
|
||||||
|
foreach(HtmlNode galleryImage in images)
|
||||||
|
urls.Add(galleryImage.GetAttributeValue("src", ""));
|
||||||
|
|
||||||
|
string comicInfoPath = Path.GetTempFileName();
|
||||||
|
File.WriteAllText(comicInfoPath, GetComicInfoXmlString(publication, chapter, logger));
|
||||||
|
|
||||||
|
DownloadChapterImages(urls.ToArray(), GetArchiveFilePath(publication, chapter), (byte)1, parentTask, comicInfoPath, cancellationToken:cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
125
Tranga/Komga.cs
@ -1,125 +0,0 @@
|
|||||||
using System.Net.Http.Headers;
|
|
||||||
using System.Text.Json.Nodes;
|
|
||||||
using Logging;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using JsonSerializer = System.Text.Json.JsonSerializer;
|
|
||||||
|
|
||||||
namespace Tranga;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provides connectivity to Komga-API
|
|
||||||
/// Can fetch and update libraries
|
|
||||||
/// </summary>
|
|
||||||
public class Komga
|
|
||||||
{
|
|
||||||
public string baseUrl { get; }
|
|
||||||
public string auth { get; } //Base64 encoded, if you use your password everywhere, you have problems
|
|
||||||
|
|
||||||
private Logger? logger;
|
|
||||||
|
|
||||||
/// <param name="baseUrl">Base-URL of Komga instance, no trailing slashes(/)</param>
|
|
||||||
/// <param name="username">Komga Username</param>
|
|
||||||
/// <param name="password">Komga password, will be base64 encoded. yea</param>
|
|
||||||
public Komga(string baseUrl, string username, string password, Logger? logger)
|
|
||||||
{
|
|
||||||
this.baseUrl = baseUrl;
|
|
||||||
this.auth = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}"));
|
|
||||||
this.logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <param name="baseUrl">Base-URL of Komga instance, no trailing slashes(/)</param>
|
|
||||||
/// <param name="auth">Base64 string of username and password (username):(password)</param>
|
|
||||||
[JsonConstructor]
|
|
||||||
public Komga(string baseUrl, string auth, Logger? logger)
|
|
||||||
{
|
|
||||||
this.baseUrl = baseUrl;
|
|
||||||
this.auth = auth;
|
|
||||||
this.logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fetches all libraries available to the user
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Array of KomgaLibraries</returns>
|
|
||||||
public KomgaLibrary[] GetLibraries()
|
|
||||||
{
|
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Getting Libraries");
|
|
||||||
Stream data = NetClient.MakeRequest($"{baseUrl}/api/v1/libraries", auth);
|
|
||||||
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
|
|
||||||
if (result is null)
|
|
||||||
return Array.Empty<KomgaLibrary>();
|
|
||||||
|
|
||||||
HashSet<KomgaLibrary> ret = new();
|
|
||||||
|
|
||||||
foreach (JsonNode? jsonNode in result)
|
|
||||||
{
|
|
||||||
var jObject = (JsonObject?)jsonNode;
|
|
||||||
string libraryId = jObject!["id"]!.GetValue<string>();
|
|
||||||
string libraryName = jObject!["name"]!.GetValue<string>();
|
|
||||||
ret.Add(new KomgaLibrary(libraryId, libraryName));
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates library with given id
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="libraryId">Id of the Komga-Library</param>
|
|
||||||
/// <returns>true if successful</returns>
|
|
||||||
public bool UpdateLibrary(string libraryId)
|
|
||||||
{
|
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Updating Libraries");
|
|
||||||
return NetClient.MakePost($"{baseUrl}/api/v1/libraries/{libraryId}/scan", auth);
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct KomgaLibrary
|
|
||||||
{
|
|
||||||
public string id { get; }
|
|
||||||
public string name { get; }
|
|
||||||
|
|
||||||
public KomgaLibrary(string id, string name)
|
|
||||||
{
|
|
||||||
this.id = id;
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class NetClient
|
|
||||||
{
|
|
||||||
public static Stream MakeRequest(string url, string auth)
|
|
||||||
{
|
|
||||||
HttpClient client = new();
|
|
||||||
HttpRequestMessage requestMessage = new HttpRequestMessage
|
|
||||||
{
|
|
||||||
Method = HttpMethod.Get,
|
|
||||||
RequestUri = new Uri(url),
|
|
||||||
Headers =
|
|
||||||
{
|
|
||||||
{ "Accept", "application/json" },
|
|
||||||
{ "Authorization", new AuthenticationHeaderValue("Basic", auth).ToString() }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
HttpResponseMessage response = client.Send(requestMessage);
|
|
||||||
Stream resultString = response.IsSuccessStatusCode ? response.Content.ReadAsStream() : Stream.Null;
|
|
||||||
return resultString;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool MakePost(string url, string auth)
|
|
||||||
{
|
|
||||||
HttpClient client = new();
|
|
||||||
HttpRequestMessage requestMessage = new HttpRequestMessage
|
|
||||||
{
|
|
||||||
Method = HttpMethod.Post,
|
|
||||||
RequestUri = new Uri(url),
|
|
||||||
Headers =
|
|
||||||
{
|
|
||||||
{ "Accept", "application/json" },
|
|
||||||
{ "Authorization", new AuthenticationHeaderValue("Basic", auth).ToString() }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
HttpResponseMessage response = client.Send(requestMessage);
|
|
||||||
return response.IsSuccessStatusCode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
119
Tranga/LibraryManager.cs
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Tranga.LibraryManagers;
|
||||||
|
|
||||||
|
namespace Tranga;
|
||||||
|
|
||||||
|
public abstract class LibraryManager
|
||||||
|
{
|
||||||
|
public enum LibraryType : byte
|
||||||
|
{
|
||||||
|
Komga = 0,
|
||||||
|
Kavita = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
public LibraryType libraryType { get; }
|
||||||
|
public string baseUrl { get; }
|
||||||
|
public string auth { get; } //Base64 encoded, if you use your password everywhere, you have problems
|
||||||
|
protected Logger? logger;
|
||||||
|
|
||||||
|
/// <param name="baseUrl">Base-URL of Komga instance, no trailing slashes(/)</param>
|
||||||
|
/// <param name="auth">Base64 string of username and password (username):(password)</param>
|
||||||
|
/// <param name="logger"></param>
|
||||||
|
protected LibraryManager(string baseUrl, string auth, Logger? logger, LibraryType libraryType)
|
||||||
|
{
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
this.auth = auth;
|
||||||
|
this.logger = logger;
|
||||||
|
this.libraryType = libraryType;
|
||||||
|
}
|
||||||
|
public abstract void UpdateLibrary();
|
||||||
|
|
||||||
|
public void AddLogger(Logger newLogger)
|
||||||
|
{
|
||||||
|
this.logger = newLogger;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static class NetClient
|
||||||
|
{
|
||||||
|
public static Stream MakeRequest(string url, string authScheme, string auth, Logger? logger)
|
||||||
|
{
|
||||||
|
HttpClient client = new();
|
||||||
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authScheme, auth);
|
||||||
|
|
||||||
|
HttpRequestMessage requestMessage = new ()
|
||||||
|
{
|
||||||
|
Method = HttpMethod.Get,
|
||||||
|
RequestUri = new Uri(url)
|
||||||
|
};
|
||||||
|
HttpResponseMessage response = client.Send(requestMessage);
|
||||||
|
logger?.WriteLine("LibraryManager", $"GET {url} -> {(int)response.StatusCode}: {response.ReasonPhrase}");
|
||||||
|
|
||||||
|
if(response.StatusCode is HttpStatusCode.Unauthorized && response.RequestMessage!.RequestUri!.AbsoluteUri != url)
|
||||||
|
return MakeRequest(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth, logger);
|
||||||
|
else if (response.IsSuccessStatusCode)
|
||||||
|
return response.Content.ReadAsStream();
|
||||||
|
else
|
||||||
|
return Stream.Null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool MakePost(string url, string authScheme, string auth, Logger? logger)
|
||||||
|
{
|
||||||
|
HttpClient client = new()
|
||||||
|
{
|
||||||
|
DefaultRequestHeaders =
|
||||||
|
{
|
||||||
|
{ "Accept", "application/json" },
|
||||||
|
{ "Authorization", new AuthenticationHeaderValue(authScheme, auth).ToString() }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
HttpRequestMessage requestMessage = new ()
|
||||||
|
{
|
||||||
|
Method = HttpMethod.Post,
|
||||||
|
RequestUri = new Uri(url)
|
||||||
|
};
|
||||||
|
HttpResponseMessage response = client.Send(requestMessage);
|
||||||
|
logger?.WriteLine("LibraryManager", $"POST {url} -> {(int)response.StatusCode}: {response.ReasonPhrase}");
|
||||||
|
|
||||||
|
if(response.StatusCode is HttpStatusCode.Unauthorized && response.RequestMessage!.RequestUri!.AbsoluteUri != url)
|
||||||
|
return MakePost(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth, logger);
|
||||||
|
else if (response.IsSuccessStatusCode)
|
||||||
|
return true;
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LibraryManagerJsonConverter : JsonConverter
|
||||||
|
{
|
||||||
|
public override bool CanConvert(Type objectType)
|
||||||
|
{
|
||||||
|
return (objectType == typeof(LibraryManager));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
JObject jo = JObject.Load(reader);
|
||||||
|
if (jo["libraryType"]!.Value<Int64>() == (Int64)LibraryType.Komga)
|
||||||
|
return jo.ToObject<Komga>(serializer)!;
|
||||||
|
|
||||||
|
if (jo["libraryType"]!.Value<Int64>() == (Int64)LibraryType.Kavita)
|
||||||
|
return jo.ToObject<Kavita>(serializer)!;
|
||||||
|
|
||||||
|
throw new Exception();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanWrite => false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Don't call this
|
||||||
|
/// </summary>
|
||||||
|
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
throw new Exception("Dont call this");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
94
Tranga/LibraryManagers/Kavita.cs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||||
|
|
||||||
|
namespace Tranga.LibraryManagers;
|
||||||
|
|
||||||
|
public class Kavita : LibraryManager
|
||||||
|
{
|
||||||
|
|
||||||
|
public Kavita(string baseUrl, string username, string password, Logger? logger) : base(baseUrl, GetToken(baseUrl, username, password), logger, LibraryType.Kavita)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
public Kavita(string baseUrl, string auth, Logger? logger) : base(baseUrl, auth, logger, LibraryType.Kavita)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetToken(string baseUrl, string username, string password)
|
||||||
|
{
|
||||||
|
HttpClient client = new()
|
||||||
|
{
|
||||||
|
DefaultRequestHeaders =
|
||||||
|
{
|
||||||
|
{ "Accept", "application/json" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
HttpRequestMessage requestMessage = new ()
|
||||||
|
{
|
||||||
|
Method = HttpMethod.Post,
|
||||||
|
RequestUri = new Uri($"{baseUrl}/api/Account/login"),
|
||||||
|
Content = new StringContent($"{{\"username\":\"{username}\",\"password\":\"{password}\"}}", System.Text.Encoding.UTF8, "application/json")
|
||||||
|
};
|
||||||
|
|
||||||
|
HttpResponseMessage response = client.Send(requestMessage);
|
||||||
|
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(response.Content.ReadAsStream());
|
||||||
|
if (result is not null)
|
||||||
|
return result!["token"]!.GetValue<string>();
|
||||||
|
else return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void UpdateLibrary()
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Updating Libraries");
|
||||||
|
foreach (KavitaLibrary lib in GetLibraries())
|
||||||
|
NetClient.MakePost($"{baseUrl}/api/Library/scan?libraryId={lib.id}", "Bearer", auth, logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches all libraries available to the user
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Array of KavitaLibrary</returns>
|
||||||
|
private IEnumerable<KavitaLibrary> GetLibraries()
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Getting Libraries");
|
||||||
|
Stream data = NetClient.MakeRequest($"{baseUrl}/api/Library", "Bearer", auth, logger);
|
||||||
|
if (data == Stream.Null)
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"No libraries returned");
|
||||||
|
return Array.Empty<KavitaLibrary>();
|
||||||
|
}
|
||||||
|
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
|
||||||
|
if (result is null)
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"No libraries returned");
|
||||||
|
return Array.Empty<KavitaLibrary>();
|
||||||
|
}
|
||||||
|
|
||||||
|
HashSet<KavitaLibrary> ret = new();
|
||||||
|
|
||||||
|
foreach (JsonNode? jsonNode in result)
|
||||||
|
{
|
||||||
|
var jObject = (JsonObject?)jsonNode;
|
||||||
|
int libraryId = jObject!["id"]!.GetValue<int>();
|
||||||
|
string libraryName = jObject!["name"]!.GetValue<string>();
|
||||||
|
ret.Add(new KavitaLibrary(libraryId, libraryName));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct KavitaLibrary
|
||||||
|
{
|
||||||
|
public int id { get; }
|
||||||
|
public string name { get; }
|
||||||
|
|
||||||
|
public KavitaLibrary(int id, string name)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
75
Tranga/LibraryManagers/Komga.cs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||||
|
|
||||||
|
namespace Tranga.LibraryManagers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides connectivity to Komga-API
|
||||||
|
/// Can fetch and update libraries
|
||||||
|
/// </summary>
|
||||||
|
public class Komga : LibraryManager
|
||||||
|
{
|
||||||
|
public Komga(string baseUrl, string username, string password, Logger? logger)
|
||||||
|
: base(baseUrl, Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}")), logger, LibraryType.Komga)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
public Komga(string baseUrl, string auth, Logger? logger) : base(baseUrl, auth, logger, LibraryType.Komga)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void UpdateLibrary()
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Updating Libraries");
|
||||||
|
foreach (KomgaLibrary lib in GetLibraries())
|
||||||
|
NetClient.MakePost($"{baseUrl}/api/v1/libraries/{lib.id}/scan", "Basic", auth, logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches all libraries available to the user
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Array of KomgaLibraries</returns>
|
||||||
|
private IEnumerable<KomgaLibrary> GetLibraries()
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Getting Libraries");
|
||||||
|
Stream data = NetClient.MakeRequest($"{baseUrl}/api/v1/libraries", "Basic", auth, logger);
|
||||||
|
if (data == Stream.Null)
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"No libraries returned");
|
||||||
|
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();
|
||||||
|
|
||||||
|
foreach (JsonNode? jsonNode in result)
|
||||||
|
{
|
||||||
|
var jObject = (JsonObject?)jsonNode;
|
||||||
|
string libraryId = jObject!["id"]!.GetValue<string>();
|
||||||
|
string libraryName = jObject!["name"]!.GetValue<string>();
|
||||||
|
ret.Add(new KomgaLibrary(libraryId, libraryName));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct KomgaLibrary
|
||||||
|
{
|
||||||
|
public string id { get; }
|
||||||
|
public string name { get; }
|
||||||
|
|
||||||
|
public KomgaLibrary(string id, string name)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,8 @@
|
|||||||
using Newtonsoft.Json;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using static System.IO.UnixFileMode;
|
||||||
|
|
||||||
namespace Tranga;
|
namespace Tranga;
|
||||||
|
|
||||||
@ -8,32 +12,74 @@ namespace Tranga;
|
|||||||
public readonly struct Publication
|
public readonly struct Publication
|
||||||
{
|
{
|
||||||
public string sortName { get; }
|
public string sortName { get; }
|
||||||
// ReSharper disable UnusedAutoPropertyAccessor.Global we need it, trust
|
public List<string> authors { get; }
|
||||||
[JsonIgnore]public string[,] altTitles { get; }
|
public Dictionary<string,string> altTitles { get; }
|
||||||
// ReSharper disable trice MemberCanBePrivate.Global, trust
|
// ReSharper disable trice MemberCanBePrivate.Global, trust
|
||||||
public string? description { get; }
|
public string? description { get; }
|
||||||
public string[] tags { get; }
|
public string[] tags { get; }
|
||||||
public string? posterUrl { get; }
|
public string? posterUrl { get; }
|
||||||
[JsonIgnore]public string[,]? links { get; }
|
public string? coverFileNameInCache { get; }
|
||||||
|
public Dictionary<string,string> links { get; }
|
||||||
public int? year { get; }
|
public int? year { get; }
|
||||||
public string? originalLanguage { get; }
|
public string? originalLanguage { get; }
|
||||||
public string status { get; }
|
public string status { get; }
|
||||||
public string folderName { get; }
|
public string folderName { get; }
|
||||||
public string downloadUrl { get; }
|
public string publicationId { get; }
|
||||||
|
public string internalId { get; }
|
||||||
|
|
||||||
public Publication(string sortName, string? description, string[,] altTitles, string[] tags, string? posterUrl, string[,]? links, int? year, string? originalLanguage, string status, string downloadUrl)
|
private static readonly Regex LegalCharacters = new Regex(@"[A-Z]*[a-z]*[0-9]* *\.*-*,*'*\'*\)*\(*~*!*");
|
||||||
|
|
||||||
|
[JsonConstructor] //Legacy
|
||||||
|
public Publication(string sortName, string? author, string? description, Dictionary<string, string> altTitles,
|
||||||
|
string[] tags, string? posterUrl, string? coverFileNameInCache, Dictionary<string, string>? links, int? year,
|
||||||
|
string? originalLanguage, string status, string publicationId)
|
||||||
|
{
|
||||||
|
List<string> pAuthors = new();
|
||||||
|
if(author is not null)
|
||||||
|
pAuthors.Add(author);
|
||||||
|
this = new Publication(sortName, pAuthors, description, altTitles, tags, posterUrl,
|
||||||
|
coverFileNameInCache, links, year, originalLanguage, status, publicationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Publication(string sortName, List<string> authors, string? description, Dictionary<string,string> altTitles, string[] tags, string? posterUrl, string? coverFileNameInCache, Dictionary<string,string>? links, int? year, string? originalLanguage, string status, string publicationId)
|
||||||
{
|
{
|
||||||
this.sortName = sortName;
|
this.sortName = sortName;
|
||||||
|
this.authors = authors;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
this.altTitles = altTitles;
|
this.altTitles = altTitles;
|
||||||
this.tags = tags;
|
this.tags = tags;
|
||||||
|
this.coverFileNameInCache = coverFileNameInCache;
|
||||||
this.posterUrl = posterUrl;
|
this.posterUrl = posterUrl;
|
||||||
this.links = links;
|
this.links = links ?? new Dictionary<string, string>();
|
||||||
this.year = year;
|
this.year = year;
|
||||||
this.originalLanguage = originalLanguage;
|
this.originalLanguage = originalLanguage;
|
||||||
this.status = status;
|
this.status = status;
|
||||||
this.downloadUrl = downloadUrl;
|
this.publicationId = publicationId;
|
||||||
this.folderName = string.Concat(sortName.Split(Path.GetInvalidPathChars().Concat(Path.GetInvalidFileNameChars()).ToArray()));
|
this.folderName = string.Concat(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 string CreatePublicationFolder(string downloadDirectory)
|
||||||
|
{
|
||||||
|
string publicationFolder = Path.Join(downloadDirectory, this.folderName);
|
||||||
|
if(!Directory.Exists(publicationFolder))
|
||||||
|
Directory.CreateDirectory(publicationFolder);
|
||||||
|
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
File.SetUnixFileMode(publicationFolder, GroupRead | GroupWrite | GroupExecute | OtherRead | OtherWrite | OtherExecute | UserRead | UserWrite | UserExecute);
|
||||||
|
return publicationFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveSeriesInfoJson(string downloadDirectory)
|
||||||
|
{
|
||||||
|
string publicationFolder = CreatePublicationFolder(downloadDirectory);
|
||||||
|
string seriesInfoPath = Path.Join(publicationFolder, "series.json");
|
||||||
|
if(!File.Exists(seriesInfoPath))
|
||||||
|
File.WriteAllText(seriesInfoPath,this.GetSeriesInfoJson());
|
||||||
|
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
File.SetUnixFileMode(seriesInfoPath, GroupRead | GroupWrite | OtherRead | OtherWrite | UserRead | UserWrite);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <returns>Serialized JSON String for series.json</returns>
|
/// <returns>Serialized JSON String for series.json</returns>
|
||||||
@ -73,9 +119,9 @@ public readonly struct Publication
|
|||||||
{
|
{
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.year = year;
|
this.year = year;
|
||||||
if(status == "ongoing" || status == "hiatus")
|
if(status.ToLower() == "ongoing" || status.ToLower() == "hiatus")
|
||||||
this.status = "Continuing";
|
this.status = "Continuing";
|
||||||
else if (status == "completed" || status == "cancelled")
|
else if (status.ToLower() == "completed" || status.ToLower() == "cancelled" || status.ToLower() == "discontinued")
|
||||||
this.status = "Ended";
|
this.status = "Ended";
|
||||||
else
|
else
|
||||||
this.status = status;
|
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, Dictionary<Publication, List<Chapter>> chapterCollection, 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, chapterCollection);
|
|
||||||
break;
|
|
||||||
case TrangaTask.Task.UpdateChapters:
|
|
||||||
UpdateChapters(connector!, (Publication)trangaTask.publication!, trangaTask.language, chapterCollection);
|
|
||||||
break;
|
|
||||||
case TrangaTask.Task.UpdatePublications:
|
|
||||||
UpdatePublications(connector!, 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, 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, Dictionary<Publication, List<Chapter>> chapterCollection)
|
|
||||||
{
|
|
||||||
List<Chapter> newChapters = UpdateChapters(connector, publication, language, chapterCollection);
|
|
||||||
connector.DownloadCover(publication);
|
|
||||||
|
|
||||||
//Check if Publication already has a Folder and a series.json
|
|
||||||
string publicationFolder = Path.Join(connector.downloadLocation, publication.folderName);
|
|
||||||
if(!Directory.Exists(publicationFolder))
|
|
||||||
Directory.CreateDirectory(publicationFolder);
|
|
||||||
|
|
||||||
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, 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,8 @@
|
|||||||
using Logging;
|
using Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Tranga.Connectors;
|
using Tranga.Connectors;
|
||||||
|
using Tranga.LibraryManagers;
|
||||||
|
using Tranga.TrangaTasks;
|
||||||
|
|
||||||
namespace Tranga;
|
namespace Tranga;
|
||||||
|
|
||||||
@ -10,51 +12,68 @@ namespace Tranga;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class TaskManager
|
public class TaskManager
|
||||||
{
|
{
|
||||||
private readonly Dictionary<Publication, List<Chapter>> _chapterCollection = new();
|
public Dictionary<Publication, List<Chapter>> chapterCollection = new();
|
||||||
private readonly HashSet<TrangaTask> _allTasks;
|
private HashSet<TrangaTask> _allTasks = new();
|
||||||
private bool _continueRunning = true;
|
private bool _continueRunning = true;
|
||||||
private readonly Connector[] _connectors;
|
private readonly Connector[] _connectors;
|
||||||
private readonly Dictionary<Connector, List<TrangaTask>> _taskQueue = new();
|
public TrangaSettings settings { get; }
|
||||||
public SettingsData settings { get; }
|
|
||||||
private Logger? logger { get; }
|
private Logger? logger { get; }
|
||||||
|
|
||||||
public Komga? komga { get; }
|
private readonly Dictionary<DownloadChapterTask, CancellationTokenSource> _runningDownloadChapterTasks = new();
|
||||||
|
|
||||||
/// <param name="downloadFolderPath">Local path to save data (Manga) to</param>
|
/// <param name="downloadFolderPath">Local path to save data (Manga) to</param>
|
||||||
/// <param name="settingsFilePath">Path to the settings file (data.json)</param>
|
/// <param name="workingDirectory">Path to the working directory</param>
|
||||||
/// <param name="komgaBaseUrl">The Url of the Komga-instance that you want to update</param>
|
/// <param name="imageCachePath">Path to the cover-image cache</param>
|
||||||
/// <param name="komgaUsername">The Komga username</param>
|
/// <param name="libraryManagers"></param>
|
||||||
/// <param name="komgaPassword">The Komga password</param>
|
|
||||||
/// <param name="logger"></param>
|
/// <param name="logger"></param>
|
||||||
public TaskManager(string downloadFolderPath, string? settingsFilePath = null, string? komgaBaseUrl = null, string? komgaUsername = null, string? komgaPassword = null, Logger? logger = null)
|
public TaskManager(string downloadFolderPath, string workingDirectory, string imageCachePath, HashSet<LibraryManager> libraryManagers, Logger? logger = null)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
_allTasks = new HashSet<TrangaTask>();
|
|
||||||
if (komgaBaseUrl != null && komgaUsername != null && komgaPassword != null)
|
this.settings = new TrangaSettings(downloadFolderPath, workingDirectory, libraryManagers);
|
||||||
this.komga = new Komga(komgaBaseUrl, komgaUsername, komgaPassword, logger);
|
ExportDataAndSettings();
|
||||||
|
|
||||||
this.settings = new SettingsData(downloadFolderPath, settingsFilePath, this.komga, this._allTasks);
|
this._connectors = new Connector[]
|
||||||
ExportData();
|
{
|
||||||
|
new MangaDex(downloadFolderPath, imageCachePath, logger),
|
||||||
this._connectors = new Connector[]{ new MangaDex(downloadFolderPath, logger) };
|
new Manganato(downloadFolderPath, imageCachePath, logger),
|
||||||
foreach(Connector cConnector in this._connectors)
|
new Mangasee(downloadFolderPath, imageCachePath, logger)
|
||||||
_taskQueue.Add(cConnector, new List<TrangaTask>());
|
};
|
||||||
|
|
||||||
Thread taskChecker = new(TaskCheckerThread);
|
Thread taskChecker = new(TaskCheckerThread);
|
||||||
taskChecker.Start();
|
taskChecker.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public TaskManager(SettingsData settings, Logger? logger = null)
|
public void UpdateSettings(string? downloadLocation, string? komgaUrl, string? komgaAuth, string? kavitaUrl, string? kavitaUsername, string? kavitaPassword)
|
||||||
|
{
|
||||||
|
if (komgaUrl is not null && komgaAuth is not null && komgaUrl.Length > 0 && komgaAuth.Length > 0)
|
||||||
|
{
|
||||||
|
settings.libraryManagers.RemoveWhere(lm => lm.GetType() == typeof(Komga));
|
||||||
|
settings.libraryManagers.Add(new Komga(komgaUrl, komgaAuth, logger));
|
||||||
|
}
|
||||||
|
if (kavitaUrl is not null && kavitaUsername is not null && kavitaPassword is not null && kavitaUrl.Length > 0 && kavitaUsername.Length > 0 && kavitaPassword.Length > 0)
|
||||||
|
{
|
||||||
|
settings.libraryManagers.RemoveWhere(lm => lm.GetType() == typeof(Kavita));
|
||||||
|
settings.libraryManagers.Add(new Kavita(kavitaUrl, kavitaUsername, kavitaPassword, logger));
|
||||||
|
}
|
||||||
|
if (downloadLocation is not null && downloadLocation.Length > 0)
|
||||||
|
settings.downloadLocation = downloadLocation;
|
||||||
|
ExportDataAndSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TaskManager(TrangaSettings settings, Logger? logger = null)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this._connectors = new Connector[]{ new MangaDex(settings.downloadLocation, logger) };
|
this._connectors = new Connector[]
|
||||||
this.settings = settings;
|
{
|
||||||
ExportData();
|
new MangaDex(settings.downloadLocation, settings.coverImageCache, logger),
|
||||||
|
new Manganato(settings.downloadLocation, settings.coverImageCache, logger),
|
||||||
|
new Mangasee(settings.downloadLocation, settings.coverImageCache, logger)
|
||||||
|
};
|
||||||
|
|
||||||
foreach(Connector cConnector in this._connectors)
|
this.settings = settings;
|
||||||
_taskQueue.Add(cConnector, new List<TrangaTask>());
|
ImportData();
|
||||||
this.komga = settings.komga;
|
ExportDataAndSettings();
|
||||||
_allTasks = settings.allTasks;
|
|
||||||
Thread taskChecker = new(TaskCheckerThread);
|
Thread taskChecker = new(TaskCheckerThread);
|
||||||
taskChecker.Start();
|
taskChecker.Start();
|
||||||
}
|
}
|
||||||
@ -66,31 +85,67 @@ public class TaskManager
|
|||||||
private void TaskCheckerThread()
|
private void TaskCheckerThread()
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), "Starting TaskCheckerThread.");
|
logger?.WriteLine(this.GetType().ToString(), "Starting TaskCheckerThread.");
|
||||||
|
int allTasksWaitingLength = _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting);
|
||||||
while (_continueRunning)
|
while (_continueRunning)
|
||||||
{
|
{
|
||||||
//Check if previous tasks have finished and execute new tasks
|
TrangaTask[] tmp = _allTasks.Where(taskQuery =>
|
||||||
foreach (KeyValuePair<Connector, List<TrangaTask>> connectorTaskQueue in _taskQueue)
|
taskQuery.nextExecution < DateTime.Now &&
|
||||||
{
|
taskQuery.state is TrangaTask.ExecutionState.Waiting or TrangaTask.ExecutionState.Enqueued)
|
||||||
if(connectorTaskQueue.Value.RemoveAll(task => task.state == TrangaTask.ExecutionState.Waiting) > 0)
|
.OrderBy(tmpTask => tmpTask.nextExecution).ToArray();
|
||||||
ExportData();
|
foreach (TrangaTask task in tmp)
|
||||||
|
|
||||||
if (connectorTaskQueue.Value.Count > 0 && connectorTaskQueue.Value.All(task => task.state is TrangaTask.ExecutionState.Enqueued))
|
|
||||||
ExecuteTaskNow(connectorTaskQueue.Value.First());
|
|
||||||
}
|
|
||||||
|
|
||||||
//Check if task should be executed
|
|
||||||
//Depending on type execute immediately or enqueue
|
|
||||||
foreach (TrangaTask task in _allTasks.Where(aTask => aTask.ShouldExecute()))
|
|
||||||
{
|
{
|
||||||
task.state = TrangaTask.ExecutionState.Enqueued;
|
task.state = TrangaTask.ExecutionState.Enqueued;
|
||||||
if(task.connectorName is null)
|
switch (task.task)
|
||||||
ExecuteTaskNow(task);
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Task due: {task}");
|
case TrangaTask.Task.DownloadNewChapters:
|
||||||
_taskQueue[GetConnector(task.connectorName!)].Add(task);
|
if (!_allTasks.Any(taskQuery => taskQuery.task == TrangaTask.Task.DownloadNewChapters &&
|
||||||
|
taskQuery.state is TrangaTask.ExecutionState.Running &&
|
||||||
|
((DownloadNewChaptersTask)taskQuery).connectorName == ((DownloadNewChaptersTask)task).connectorName))
|
||||||
|
{
|
||||||
|
ExecuteTaskNow(task);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TrangaTask.Task.DownloadChapter:
|
||||||
|
if (!_allTasks.Any(taskQuery =>
|
||||||
|
taskQuery.task == TrangaTask.Task.DownloadChapter &&
|
||||||
|
taskQuery.state is TrangaTask.ExecutionState.Running &&
|
||||||
|
((DownloadChapterTask)taskQuery).connectorName ==
|
||||||
|
((DownloadChapterTask)task).connectorName))
|
||||||
|
{
|
||||||
|
ExecuteTaskNow(task);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TrangaTask.Task.UpdateLibraries:
|
||||||
|
ExecuteTaskNow(task);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HashSet<DownloadChapterTask> toRemove = new();
|
||||||
|
foreach (KeyValuePair<DownloadChapterTask, CancellationTokenSource> removeTask in _runningDownloadChapterTasks)
|
||||||
|
{
|
||||||
|
if (removeTask.Key.GetType() == typeof(DownloadChapterTask) &&
|
||||||
|
DateTime.Now.Subtract(removeTask.Key.lastChange) > TimeSpan.FromMinutes(3))//3 Minutes since last update to task -> remove
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Disposing failed task {removeTask.Key}.");
|
||||||
|
removeTask.Key.parentTask?.DecrementProgress(removeTask.Key.progress);
|
||||||
|
removeTask.Value.Cancel();
|
||||||
|
toRemove.Add(removeTask.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (DownloadChapterTask taskToRemove in toRemove)
|
||||||
|
{
|
||||||
|
DeleteTask(taskToRemove);
|
||||||
|
DownloadChapterTask newTask = new DownloadChapterTask(taskToRemove.task, taskToRemove.connectorName,
|
||||||
|
taskToRemove.publication, taskToRemove.chapter, taskToRemove.language,
|
||||||
|
taskToRemove.parentTask);
|
||||||
|
AddTask(newTask);
|
||||||
|
taskToRemove.parentTask?.ReplaceFailedChildTask(taskToRemove, newTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(allTasksWaitingLength != _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting))
|
||||||
|
ExportDataAndSettings();
|
||||||
|
allTasksWaitingLength = _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting);
|
||||||
Thread.Sleep(1000);
|
Thread.Sleep(1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -101,66 +156,86 @@ public class TaskManager
|
|||||||
/// <param name="task">Task to execute</param>
|
/// <param name="task">Task to execute</param>
|
||||||
public void ExecuteTaskNow(TrangaTask task)
|
public void ExecuteTaskNow(TrangaTask task)
|
||||||
{
|
{
|
||||||
if (!this._allTasks.Contains(task))
|
task.state = TrangaTask.ExecutionState.Running;
|
||||||
return;
|
CancellationTokenSource cToken = new ();
|
||||||
|
Task t = new(() =>
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Forcing Execution: {task}");
|
|
||||||
Task t = new Task(() =>
|
|
||||||
{
|
{
|
||||||
TaskExecutor.Execute(this, task, this._chapterCollection, logger);
|
task.Execute(this, this.logger, cToken.Token);
|
||||||
});
|
}, cToken.Token);
|
||||||
|
if(task.GetType() == typeof(DownloadChapterTask))
|
||||||
|
_runningDownloadChapterTasks.Add((DownloadChapterTask)task, cToken);
|
||||||
t.Start();
|
t.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public void AddTask(TrangaTask newTask)
|
||||||
/// Creates and adds a new Task to the task-Collection
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="task">TrangaTask.Task to later execute</param>
|
|
||||||
/// <param name="connectorName">Name of the connector to use</param>
|
|
||||||
/// <param name="publication">Publication to execute Task on, can be null in case of unrelated Task</param>
|
|
||||||
/// <param name="reoccurrence">Time-Interval between Executions</param>
|
|
||||||
/// <param name="language">language, should Task require parameter. Can be empty</param>
|
|
||||||
/// <exception cref="ArgumentException">Is thrown when connectorName is not a available Connector</exception>
|
|
||||||
public TrangaTask AddTask(TrangaTask.Task task, string? connectorName, Publication? publication, TimeSpan reoccurrence,
|
|
||||||
string language = "")
|
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Adding new Task {task} {connectorName} {publication?.sortName}");
|
logger?.WriteLine(this.GetType().ToString(), $"Adding new Task {newTask}");
|
||||||
if (task != TrangaTask.Task.UpdateKomgaLibrary && connectorName is null)
|
|
||||||
throw new ArgumentException($"connectorName can not be null for task {task}");
|
|
||||||
|
|
||||||
TrangaTask newTask;
|
switch (newTask.task)
|
||||||
if (task == TrangaTask.Task.UpdateKomgaLibrary)
|
|
||||||
{
|
{
|
||||||
newTask = new TrangaTask(task, null, null, reoccurrence, language);
|
case TrangaTask.Task.UpdateLibraries:
|
||||||
|
//Only one UpdateKomgaLibrary Task
|
||||||
//Check if same task already exists
|
logger?.WriteLine(this.GetType().ToString(), $"Removing old {newTask.task}-Task.");
|
||||||
// ReSharper disable once SimplifyLinqExpressionUseAll readabilty
|
_allTasks.RemoveWhere(trangaTask => trangaTask.task is TrangaTask.Task.UpdateLibraries);
|
||||||
if (!_allTasks.Any(trangaTask => trangaTask.task == task))
|
break;
|
||||||
{
|
case TrangaTask.Task.DownloadNewChapters:
|
||||||
_allTasks.Add(newTask);
|
IEnumerable<TrangaTask> matchingdnc =
|
||||||
}
|
_allTasks.Where(mTask => mTask.GetType() == typeof(DownloadNewChaptersTask));
|
||||||
|
if (!matchingdnc.Any(mTask =>
|
||||||
|
((DownloadNewChaptersTask)mTask).publication.internalId == ((DownloadNewChaptersTask)newTask).publication.internalId &&
|
||||||
|
((DownloadNewChaptersTask)mTask).connectorName == ((DownloadNewChaptersTask)newTask).connectorName))
|
||||||
|
_allTasks.Add(newTask);
|
||||||
|
else
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Task already exists {newTask}");
|
||||||
|
break;
|
||||||
|
case TrangaTask.Task.DownloadChapter:
|
||||||
|
IEnumerable<TrangaTask> matchingdc =
|
||||||
|
_allTasks.Where(mTask => mTask.GetType() == typeof(DownloadChapterTask));
|
||||||
|
if (!matchingdc.Any(mTask =>
|
||||||
|
((DownloadChapterTask)mTask).publication.internalId == ((DownloadChapterTask)newTask).publication.internalId &&
|
||||||
|
((DownloadChapterTask)mTask).connectorName == ((DownloadChapterTask)newTask).connectorName &&
|
||||||
|
((DownloadChapterTask)mTask).chapter.sortNumber == ((DownloadChapterTask)newTask).chapter.sortNumber))
|
||||||
|
_allTasks.Add(newTask);
|
||||||
|
else
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Task already exists {newTask}");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
else
|
ExportDataAndSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteTask(TrangaTask removeTask)
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Removing Task {removeTask}");
|
||||||
|
_allTasks.Remove(removeTask);
|
||||||
|
if (removeTask.GetType() == typeof(DownloadChapterTask))
|
||||||
|
_runningDownloadChapterTasks.Remove((DownloadChapterTask)removeTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TrangaTask? AddTask(TrangaTask.Task taskType, string? connectorName, string? internalId,
|
||||||
|
TimeSpan reoccurrenceTime, string? language = "en")
|
||||||
|
{
|
||||||
|
TrangaTask? newTask = null;
|
||||||
|
switch (taskType)
|
||||||
{
|
{
|
||||||
//Get appropriate Connector from available Connectors for TrangaTask
|
case TrangaTask.Task.UpdateLibraries:
|
||||||
Connector? connector = _connectors.FirstOrDefault(c => c.name == connectorName);
|
newTask = new UpdateLibrariesTask(taskType, reoccurrenceTime);
|
||||||
if (connector is null)
|
break;
|
||||||
throw new ArgumentException($"Connector {connectorName} is not a known connector.");
|
case TrangaTask.Task.DownloadNewChapters:
|
||||||
|
if (connectorName is null)
|
||||||
newTask = new TrangaTask(task, connector.name, publication, reoccurrence, language);
|
logger?.WriteLine(this.GetType().ToString(), $"Value connectorName can not be null.");
|
||||||
|
if(internalId is null)
|
||||||
//Check if same task already exists
|
logger?.WriteLine(this.GetType().ToString(), $"Value internalId can not be null.");
|
||||||
if (!_allTasks.Any(trangaTask => trangaTask.task == task && trangaTask.connectorName == connector.name &&
|
if(language is null)
|
||||||
trangaTask.publication?.downloadUrl == publication?.downloadUrl))
|
logger?.WriteLine(this.GetType().ToString(), $"Value language can not be null.");
|
||||||
{
|
if (connectorName is null || internalId is null || language is null)
|
||||||
if(task != TrangaTask.Task.UpdatePublications)
|
return null;
|
||||||
_chapterCollection.Add((Publication)publication!, new List<Chapter>());
|
GetConnector(connectorName); //Check if connectorName is valid
|
||||||
_allTasks.Add(newTask);
|
Publication publication = GetAllPublications().First(pub => pub.internalId == internalId);
|
||||||
}
|
newTask = new DownloadNewChaptersTask(taskType, connectorName!, publication, reoccurrenceTime, language!);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Added new Task {newTask.ToString()}");
|
if(newTask is not null)
|
||||||
ExportData();
|
AddTask(newTask);
|
||||||
|
|
||||||
return newTask;
|
return newTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,27 +244,91 @@ public class TaskManager
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="task">TrangaTask.Task type</param>
|
/// <param name="task">TrangaTask.Task type</param>
|
||||||
/// <param name="connectorName">Name of Connector that was used</param>
|
/// <param name="connectorName">Name of Connector that was used</param>
|
||||||
/// <param name="publication">Publication that was used</param>
|
/// <param name="publicationId">Publication that was used</param>
|
||||||
public void DeleteTask(TrangaTask.Task task, string? connectorName, Publication? publication)
|
public void DeleteTask(TrangaTask.Task task, string? connectorName, string? publicationId)
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Removing Task {task} {publication?.sortName}");
|
logger?.WriteLine(this.GetType().ToString(), $"Removing Task {task} {publicationId}");
|
||||||
if (task == TrangaTask.Task.UpdateKomgaLibrary)
|
|
||||||
|
switch (task)
|
||||||
{
|
{
|
||||||
_allTasks.RemoveWhere(uTask => uTask.task == TrangaTask.Task.UpdateKomgaLibrary);
|
case TrangaTask.Task.UpdateLibraries:
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Removed Task {task}");
|
//Only one UpdateKomgaLibrary Task
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Removing old {task}-Task.");
|
||||||
|
_allTasks.RemoveWhere(trangaTask => trangaTask.task is TrangaTask.Task.UpdateLibraries);
|
||||||
|
break;
|
||||||
|
case TrangaTask.Task.DownloadNewChapters:
|
||||||
|
if (connectorName is null || publicationId is null)
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), "connectorName and publication can not be null");
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_allTasks.RemoveWhere(mTask =>
|
||||||
|
mTask.GetType() == typeof(DownloadNewChaptersTask) &&
|
||||||
|
((DownloadNewChaptersTask)mTask).publication.internalId == publicationId &&
|
||||||
|
((DownloadNewChaptersTask)mTask).connectorName == connectorName!);
|
||||||
|
_allTasks.RemoveWhere(mTask =>
|
||||||
|
mTask.GetType() == typeof(DownloadChapterTask) &&
|
||||||
|
((DownloadChapterTask)mTask).publication.internalId == publicationId &&
|
||||||
|
((DownloadChapterTask)mTask).connectorName == connectorName!);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
else if (connectorName is null)
|
ExportDataAndSettings();
|
||||||
throw new ArgumentException($"connectorName can not be null for Task {task}");
|
}
|
||||||
else
|
|
||||||
|
public IEnumerable<TrangaTask> GetTasksMatching(TrangaTask.Task taskType, string? connectorName = null, string? searchString = null, string? internalId = null, string? chapterSortNumber = null)
|
||||||
|
{
|
||||||
|
switch (taskType)
|
||||||
{
|
{
|
||||||
if(_allTasks.RemoveWhere(trangaTask =>
|
case TrangaTask.Task.UpdateLibraries:
|
||||||
trangaTask.task == task && trangaTask.connectorName == connectorName &&
|
return _allTasks.Where(tTask => tTask.task == TrangaTask.Task.UpdateLibraries);
|
||||||
trangaTask.publication?.downloadUrl == publication?.downloadUrl) > 0)
|
case TrangaTask.Task.DownloadNewChapters:
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Removed Task {task} {publication?.sortName} {publication?.downloadUrl}.");
|
if(connectorName is null)
|
||||||
else
|
return _allTasks.Where(tTask => tTask.task == taskType);
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"No Task {task} {publication?.sortName} {publication?.downloadUrl} could be found.");
|
GetConnector(connectorName);//Name check
|
||||||
|
IEnumerable<TrangaTask> matchingdnc = _allTasks.Where(tTask => tTask.GetType() == typeof(DownloadNewChaptersTask));
|
||||||
|
if (searchString is not null)
|
||||||
|
{
|
||||||
|
return matchingdnc.Where(mTask =>
|
||||||
|
((DownloadNewChaptersTask)mTask).connectorName == connectorName &&
|
||||||
|
((DownloadNewChaptersTask)mTask).ToString().Contains(searchString, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
}
|
||||||
|
else if (internalId is not null)
|
||||||
|
{
|
||||||
|
return matchingdnc.Where(mTask =>
|
||||||
|
((DownloadNewChaptersTask)mTask).connectorName == connectorName &&
|
||||||
|
((DownloadNewChaptersTask)mTask).publication.internalId == internalId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return _allTasks.Where(tTask =>
|
||||||
|
tTask.GetType() == typeof(DownloadNewChaptersTask) &&
|
||||||
|
((DownloadNewChaptersTask)tTask).connectorName == connectorName);
|
||||||
|
|
||||||
|
case TrangaTask.Task.DownloadChapter:
|
||||||
|
if(connectorName is null)
|
||||||
|
return _allTasks.Where(tTask => tTask.task == taskType);
|
||||||
|
GetConnector(connectorName);//Name check
|
||||||
|
IEnumerable<TrangaTask> matchingdc = _allTasks.Where(tTask => tTask.GetType() == typeof(DownloadChapterTask));
|
||||||
|
if (searchString is not null)
|
||||||
|
{
|
||||||
|
return matchingdc.Where(mTask =>
|
||||||
|
((DownloadChapterTask)mTask).connectorName == connectorName &&
|
||||||
|
((DownloadChapterTask)mTask).ToString().Contains(searchString, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
}
|
||||||
|
else if (internalId is not null && chapterSortNumber is not null)
|
||||||
|
{
|
||||||
|
return matchingdc.Where(mTask =>
|
||||||
|
((DownloadChapterTask)mTask).connectorName == connectorName &&
|
||||||
|
((DownloadChapterTask)mTask).publication.publicationId == internalId &&
|
||||||
|
((DownloadChapterTask)mTask).chapter.sortNumber == chapterSortNumber);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return _allTasks.Where(tTask =>
|
||||||
|
tTask.GetType() == typeof(DownloadChapterTask) &&
|
||||||
|
((DownloadChapterTask)tTask).connectorName == connectorName);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return Array.Empty<TrangaTask>();
|
||||||
}
|
}
|
||||||
ExportData();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -199,8 +338,6 @@ public class TaskManager
|
|||||||
public void RemoveTaskFromQueue(TrangaTask task)
|
public void RemoveTaskFromQueue(TrangaTask task)
|
||||||
{
|
{
|
||||||
task.lastExecuted = DateTime.Now;
|
task.lastExecuted = DateTime.Now;
|
||||||
foreach (List<TrangaTask> taskList in this._taskQueue.Values)
|
|
||||||
taskList.Remove(task);
|
|
||||||
task.state = TrangaTask.ExecutionState.Waiting;
|
task.state = TrangaTask.ExecutionState.Waiting;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,11 +364,22 @@ public class TaskManager
|
|||||||
_allTasks.CopyTo(ret);
|
_allTasks.CopyTo(ret);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Publication[] GetPublicationsFromConnector(Connector connector, string? title = null)
|
||||||
|
{
|
||||||
|
Publication[] ret = connector.GetPublications(title ?? "");
|
||||||
|
foreach (Publication publication in ret)
|
||||||
|
{
|
||||||
|
if(chapterCollection.All(pub => pub.Key.internalId != publication.internalId))
|
||||||
|
this.chapterCollection.TryAdd(publication, new List<Chapter>());
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
/// <returns>All added Publications</returns>
|
/// <returns>All added Publications</returns>
|
||||||
public Publication[] GetAllPublications()
|
public Publication[] GetAllPublications()
|
||||||
{
|
{
|
||||||
return this._chapterCollection.Keys.ToArray();
|
return this.chapterCollection.Keys.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -246,7 +394,7 @@ public class TaskManager
|
|||||||
Connector? ret = this._connectors.FirstOrDefault(connector => connector.name == connectorName);
|
Connector? ret = this._connectors.FirstOrDefault(connector => connector.name == connectorName);
|
||||||
if (ret is null)
|
if (ret is null)
|
||||||
throw new Exception($"Connector {connectorName} is not an available Connector.");
|
throw new Exception($"Connector {connectorName} is not an available Connector.");
|
||||||
return (Connector)ret!;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -257,7 +405,7 @@ public class TaskManager
|
|||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Shutting down (forced={force})");
|
logger?.WriteLine(this.GetType().ToString(), $"Shutting down (forced={force})");
|
||||||
_continueRunning = false;
|
_continueRunning = false;
|
||||||
ExportData();
|
ExportDataAndSettings();
|
||||||
|
|
||||||
if(force)
|
if(force)
|
||||||
Environment.Exit(_allTasks.Count(task => task.state is TrangaTask.ExecutionState.Enqueued or TrangaTask.ExecutionState.Running));
|
Environment.Exit(_allTasks.Count(task => task.state is TrangaTask.ExecutionState.Enqueued or TrangaTask.ExecutionState.Running));
|
||||||
@ -269,47 +417,71 @@ public class TaskManager
|
|||||||
Environment.Exit(0);
|
Environment.Exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private void ImportData()
|
||||||
/// Loads stored data (settings, tasks) from file
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="importFilePath">working directory, filename has to be data.json</param>
|
|
||||||
public static SettingsData LoadData(string importFilePath)
|
|
||||||
{
|
{
|
||||||
if (!File.Exists(importFilePath))
|
logger?.WriteLine(this.GetType().ToString(), "Importing Data");
|
||||||
return new SettingsData(Directory.GetCurrentDirectory(), null, null, new HashSet<TrangaTask>());
|
string buffer;
|
||||||
|
if (File.Exists(settings.tasksFilePath))
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Importing tasks from {settings.tasksFilePath}");
|
||||||
|
buffer = File.ReadAllText(settings.tasksFilePath);
|
||||||
|
this._allTasks = JsonConvert.DeserializeObject<HashSet<TrangaTask>>(buffer, new JsonSerializerSettings() { Converters = { new TrangaTask.TrangaTaskJsonConverter() } })!;
|
||||||
|
}
|
||||||
|
|
||||||
string toRead = File.ReadAllText(importFilePath);
|
foreach (TrangaTask task in this._allTasks.Where(task => task.GetType() == typeof(DownloadChapterTask)))
|
||||||
SettingsData data = JsonConvert.DeserializeObject<SettingsData>(toRead)!;
|
{
|
||||||
|
DownloadChapterTask dcTask = (DownloadChapterTask)task;
|
||||||
|
IEnumerable<TrangaTask> dncTasks = this._allTasks.Where(pTask => pTask.GetType() == typeof(DownloadNewChaptersTask));
|
||||||
|
DownloadNewChaptersTask? parentTask = (DownloadNewChaptersTask?)dncTasks.FirstOrDefault(pTask => pTask.taskId.Equals(dcTask.parentTaskId));
|
||||||
|
dcTask.parentTask = parentTask;
|
||||||
|
parentTask?.AddChildTask(dcTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return data;
|
if (File.Exists(settings.knownPublicationsPath))
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Importing known publications from {settings.knownPublicationsPath}");
|
||||||
|
buffer = File.ReadAllText(settings.knownPublicationsPath);
|
||||||
|
Publication[] publications = JsonConvert.DeserializeObject<Publication[]>(buffer)!;
|
||||||
|
foreach (Publication publication in publications)
|
||||||
|
this.chapterCollection.TryAdd(publication, new List<Chapter>());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exports data (settings, tasks) to file
|
/// Exports data (settings, tasks) to file
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void ExportData()
|
private void ExportDataAndSettings()
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Exporting data to {settings.settingsFilePath}");
|
logger?.WriteLine(this.GetType().ToString(), $"Exporting settings to {settings.settingsFilePath}");
|
||||||
|
while(IsFileInUse(settings.settingsFilePath))
|
||||||
string serializedData = JsonConvert.SerializeObject(settings);
|
Thread.Sleep(50);
|
||||||
File.WriteAllText(settings.settingsFilePath, serializedData);
|
File.WriteAllText(settings.settingsFilePath, JsonConvert.SerializeObject(settings));
|
||||||
|
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Exporting tasks to {settings.tasksFilePath}");
|
||||||
|
while(IsFileInUse(settings.tasksFilePath))
|
||||||
|
Thread.Sleep(50);
|
||||||
|
File.WriteAllText(settings.tasksFilePath, JsonConvert.SerializeObject(this._allTasks));
|
||||||
|
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Exporting known publications to {settings.knownPublicationsPath}");
|
||||||
|
while(IsFileInUse(settings.knownPublicationsPath))
|
||||||
|
Thread.Sleep(50);
|
||||||
|
File.WriteAllText(settings.knownPublicationsPath, JsonConvert.SerializeObject(this.chapterCollection.Keys.ToArray()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SettingsData
|
private bool IsFileInUse(string path)
|
||||||
{
|
{
|
||||||
public string downloadLocation { get; set; }
|
if (!File.Exists(path))
|
||||||
public string settingsFilePath { get; }
|
return false;
|
||||||
public Komga? komga { get; set; }
|
try
|
||||||
public HashSet<TrangaTask> allTasks { get; }
|
|
||||||
|
|
||||||
public SettingsData(string downloadLocation, string? settingsFilePath, Komga? komga, HashSet<TrangaTask> allTasks)
|
|
||||||
{
|
{
|
||||||
this.settingsFilePath = settingsFilePath ??
|
using FileStream stream = new (path, FileMode.Open, FileAccess.Read, FileShare.None);
|
||||||
Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
stream.Close();
|
||||||
"Tranga", "data.json");
|
|
||||||
this.downloadLocation = downloadLocation;
|
|
||||||
this.komga = komga;
|
|
||||||
this.allTasks = allTasks;
|
|
||||||
}
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,7 +7,9 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="HtmlAgilityPack" Version="1.11.46" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
<PackageReference Include="PuppeteerSharp" Version="10.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
39
Tranga/TrangaSettings.cs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
using Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Tranga.LibraryManagers;
|
||||||
|
|
||||||
|
namespace Tranga;
|
||||||
|
|
||||||
|
public class TrangaSettings
|
||||||
|
{
|
||||||
|
public string downloadLocation { get; set; }
|
||||||
|
public string workingDirectory { get; set; }
|
||||||
|
[JsonIgnore]public string settingsFilePath => Path.Join(workingDirectory, "settings.json");
|
||||||
|
[JsonIgnore]public string tasksFilePath => Path.Join(workingDirectory, "tasks.json");
|
||||||
|
[JsonIgnore]public string knownPublicationsPath => Path.Join(workingDirectory, "knownPublications.json");
|
||||||
|
[JsonIgnore] public string coverImageCache => Path.Join(workingDirectory, "imageCache");
|
||||||
|
public HashSet<LibraryManager> libraryManagers { get; }
|
||||||
|
|
||||||
|
public TrangaSettings(string downloadLocation, string workingDirectory, HashSet<LibraryManager> libraryManagers)
|
||||||
|
{
|
||||||
|
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.libraryManagers = libraryManagers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TrangaSettings LoadSettings(string importFilePath, Logger? logger)
|
||||||
|
{
|
||||||
|
if (!File.Exists(importFilePath))
|
||||||
|
return new TrangaSettings(Path.Join(Directory.GetCurrentDirectory(), "Downloads"), Directory.GetCurrentDirectory(), new HashSet<LibraryManager>());
|
||||||
|
|
||||||
|
string toRead = File.ReadAllText(importFilePath);
|
||||||
|
TrangaSettings settings = JsonConvert.DeserializeObject<TrangaSettings>(toRead, new JsonSerializerSettings() { Converters = { new LibraryManager.LibraryManagerJsonConverter()} })!;
|
||||||
|
if(logger is not null)
|
||||||
|
foreach(LibraryManager lm in settings.libraryManagers)
|
||||||
|
lm.AddLogger(logger);
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
}
|
@ -1,21 +1,41 @@
|
|||||||
using Newtonsoft.Json;
|
using System.Globalization;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Tranga.TrangaTasks;
|
||||||
|
using JsonConverter = Newtonsoft.Json.JsonConverter;
|
||||||
|
|
||||||
namespace Tranga;
|
namespace Tranga;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stores information on Task
|
/// Stores information on Task, when implementing new Tasks also update the serializer
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class TrangaTask
|
[JsonDerivedType(typeof(DownloadNewChaptersTask), 2)]
|
||||||
|
[JsonDerivedType(typeof(UpdateLibrariesTask), 3)]
|
||||||
|
[JsonDerivedType(typeof(DownloadChapterTask), 4)]
|
||||||
|
public abstract class TrangaTask
|
||||||
{
|
{
|
||||||
// ReSharper disable once CommentTypo ...Tell me why!
|
// ReSharper disable once CommentTypo ...Tell me why!
|
||||||
// ReSharper disable once MemberCanBePrivate.Global I want it thaaat way
|
// ReSharper disable once MemberCanBePrivate.Global I want it thaaat way
|
||||||
public TimeSpan reoccurrence { get; }
|
public TimeSpan reoccurrence { get; }
|
||||||
public DateTime lastExecuted { get; set; }
|
public DateTime lastExecuted { get; set; }
|
||||||
public string? connectorName { get; }
|
|
||||||
public Task task { get; }
|
public Task task { get; }
|
||||||
public Publication? publication { get; }
|
public string taskId { get; set; }
|
||||||
public string language { get; }
|
[Newtonsoft.Json.JsonIgnore]public ExecutionState state { get; set; }
|
||||||
[JsonIgnore]public ExecutionState state { get; set; }
|
[Newtonsoft.Json.JsonIgnore]public double progress { get; protected set; }
|
||||||
|
[Newtonsoft.Json.JsonIgnore]public DateTime nextExecution => lastExecuted.Add(reoccurrence);
|
||||||
|
[Newtonsoft.Json.JsonIgnore]public DateTime executionStarted { get; private set; }
|
||||||
|
|
||||||
|
[Newtonsoft.Json.JsonIgnore]
|
||||||
|
public DateTime executionApproximatelyFinished => this.progress != 0
|
||||||
|
? this.executionStarted.Add(DateTime.Now.Subtract(this.executionStarted) / this.progress)
|
||||||
|
: DateTime.MaxValue;
|
||||||
|
|
||||||
|
[Newtonsoft.Json.JsonIgnore]
|
||||||
|
public TimeSpan executionApproximatelyRemaining => this.executionApproximatelyFinished.Subtract(DateTime.Now);
|
||||||
|
|
||||||
|
[Newtonsoft.Json.JsonIgnore]public DateTime lastChange { get; private set; }
|
||||||
|
|
||||||
public enum ExecutionState
|
public enum ExecutionState
|
||||||
{
|
{
|
||||||
@ -24,38 +44,105 @@ public class TrangaTask
|
|||||||
Running
|
Running
|
||||||
};
|
};
|
||||||
|
|
||||||
public TrangaTask(Task task, string? connectorName, Publication? publication, TimeSpan reoccurrence, string language = "")
|
protected TrangaTask(Task task, TimeSpan reoccurrence)
|
||||||
{
|
{
|
||||||
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.reoccurrence = reoccurrence;
|
||||||
this.lastExecuted = DateTime.Now.Subtract(reoccurrence);
|
this.lastExecuted = DateTime.Now.Subtract(reoccurrence);
|
||||||
this.connectorName = connectorName;
|
|
||||||
this.task = task;
|
this.task = task;
|
||||||
this.language = language;
|
this.progress = 0f;
|
||||||
|
this.executionStarted = DateTime.Now;
|
||||||
|
this.lastChange = DateTime.MaxValue;
|
||||||
|
this.taskId = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(this.executionStarted.ToString(CultureInfo.InvariantCulture)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public double IncrementProgress(double amount)
|
||||||
|
{
|
||||||
|
this.progress += amount;
|
||||||
|
this.lastChange = DateTime.Now;
|
||||||
|
return this.progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double DecrementProgress(double amount)
|
||||||
|
{
|
||||||
|
this.progress -= amount;
|
||||||
|
this.lastChange = DateTime.Now;
|
||||||
|
return this.progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// BL for concrete Tasks
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="taskManager"></param>
|
||||||
|
/// <param name="logger"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
protected abstract void ExecuteTask(TaskManager taskManager, Logger? logger, CancellationToken? cancellationToken = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute the task
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="taskManager">Should be the parent taskManager</param>
|
||||||
|
/// <param name="logger"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
public void Execute(TaskManager taskManager, Logger? logger, CancellationToken? cancellationToken = null)
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Executing Task {this}");
|
||||||
|
this.state = ExecutionState.Running;
|
||||||
|
this.executionStarted = DateTime.Now;
|
||||||
|
this.lastChange = DateTime.Now;
|
||||||
|
ExecuteTask(taskManager, logger, cancellationToken);
|
||||||
|
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>
|
/// <returns>True if elapsed time since last execution is greater than set interval</returns>
|
||||||
public bool ShouldExecute()
|
public bool ShouldExecute()
|
||||||
{
|
{
|
||||||
return DateTime.Now.Subtract(this.lastExecuted) > reoccurrence && state is ExecutionState.Waiting;
|
return nextExecution < DateTime.Now && state is ExecutionState.Waiting;
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Task
|
public enum Task : byte
|
||||||
{
|
{
|
||||||
UpdatePublications,
|
DownloadNewChapters = 2,
|
||||||
UpdateChapters,
|
UpdateLibraries = 3,
|
||||||
DownloadNewChapters,
|
DownloadChapter = 4
|
||||||
UpdateKomgaLibrary
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{task,-20} | {lastExecuted,-20} | {reoccurrence,-12} | {state,-10} | {connectorName,-15} | {publication?.sortName}";
|
return $"{task}, {lastExecuted}, {reoccurrence}, {state}, {progress:P2}, {executionApproximatelyFinished}, {executionApproximatelyRemaining}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TrangaTaskJsonConverter : JsonConverter
|
||||||
|
{
|
||||||
|
public override bool CanConvert(Type objectType)
|
||||||
|
{
|
||||||
|
return (objectType == typeof(TrangaTask));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
JObject jo = JObject.Load(reader);
|
||||||
|
if (jo["task"]!.Value<Int64>() == (Int64)Task.DownloadNewChapters)
|
||||||
|
return jo.ToObject<DownloadNewChaptersTask>(serializer)!;
|
||||||
|
|
||||||
|
if (jo["task"]!.Value<Int64>() == (Int64)Task.UpdateLibraries)
|
||||||
|
return jo.ToObject<UpdateLibrariesTask>(serializer)!;
|
||||||
|
|
||||||
|
if (jo["task"]!.Value<Int64>() == (Int64)Task.DownloadChapter)
|
||||||
|
return jo.ToObject<DownloadChapterTask>(serializer)!;
|
||||||
|
|
||||||
|
throw new Exception();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanWrite => false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Don't call this
|
||||||
|
/// </summary>
|
||||||
|
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
throw new Exception("Dont call this");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
43
Tranga/TrangaTasks/DownloadChapterTask.cs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
using Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Tranga.TrangaTasks;
|
||||||
|
|
||||||
|
public class DownloadChapterTask : TrangaTask
|
||||||
|
{
|
||||||
|
public string connectorName { get; }
|
||||||
|
public Publication publication { get; }
|
||||||
|
public string language { get; }
|
||||||
|
public Chapter chapter { get; }
|
||||||
|
[JsonIgnore]public DownloadNewChaptersTask? parentTask { get; set; }
|
||||||
|
public string? parentTaskId { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public DownloadChapterTask(Task task, string connectorName, Publication publication, Chapter chapter, string language = "en", DownloadNewChaptersTask? parentTask = null) : base(task, TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
this.chapter = chapter;
|
||||||
|
this.connectorName = connectorName;
|
||||||
|
this.publication = publication;
|
||||||
|
this.language = language;
|
||||||
|
this.parentTask = parentTask;
|
||||||
|
this.parentTaskId = parentTask?.taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ExecuteTask(TaskManager taskManager, Logger? logger, CancellationToken? cancellationToken = null)
|
||||||
|
{
|
||||||
|
if (cancellationToken?.IsCancellationRequested??false)
|
||||||
|
return;
|
||||||
|
if(this.parentTask is not null)
|
||||||
|
this.parentTask.state = ExecutionState.Running;
|
||||||
|
Connector connector = taskManager.GetConnector(this.connectorName);
|
||||||
|
connector.DownloadChapter(this.publication, this.chapter, this, cancellationToken);
|
||||||
|
if(this.parentTask is not null)
|
||||||
|
this.parentTask.state = ExecutionState.Waiting;
|
||||||
|
taskManager.DeleteTask(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{base.ToString()}, {connectorName}, {publication.sortName} {publication.internalId}, Vol.{chapter.volumeNumber} Ch.{chapter.chapterNumber}";
|
||||||
|
}
|
||||||
|
}
|
81
Tranga/TrangaTasks/DownloadNewChaptersTask.cs
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
using Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Tranga.TrangaTasks;
|
||||||
|
|
||||||
|
public class DownloadNewChaptersTask : TrangaTask
|
||||||
|
{
|
||||||
|
public string connectorName { get; }
|
||||||
|
public Publication publication { get; }
|
||||||
|
public string language { get; }
|
||||||
|
[JsonIgnore]private HashSet<DownloadChapterTask> childTasks { get; }
|
||||||
|
[JsonIgnore]public new double progress => childTasks.Count > 0 ? childTasks.Sum(childTask => childTask.progress) / childTasks.Count : 0;
|
||||||
|
|
||||||
|
public DownloadNewChaptersTask(Task task, string connectorName, Publication publication, TimeSpan reoccurrence, string language = "en") : base(task, reoccurrence)
|
||||||
|
{
|
||||||
|
this.connectorName = connectorName;
|
||||||
|
this.publication = publication;
|
||||||
|
this.language = language;
|
||||||
|
this.childTasks = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ExecuteTask(TaskManager taskManager, Logger? logger, CancellationToken? cancellationToken = null)
|
||||||
|
{
|
||||||
|
if (cancellationToken?.IsCancellationRequested??false)
|
||||||
|
return;
|
||||||
|
Publication pub = publication!;
|
||||||
|
Connector connector = taskManager.GetConnector(this.connectorName);
|
||||||
|
|
||||||
|
//Check if Publication already has a Folder
|
||||||
|
pub.CreatePublicationFolder(taskManager.settings.downloadLocation);
|
||||||
|
List<Chapter> newChapters = GetNewChaptersList(connector, pub, language!, ref taskManager.chapterCollection);
|
||||||
|
|
||||||
|
connector.CopyCoverFromCacheToDownloadLocation(pub, taskManager.settings);
|
||||||
|
|
||||||
|
pub.SaveSeriesInfoJson(connector.downloadLocation);
|
||||||
|
|
||||||
|
foreach (Chapter newChapter in newChapters)
|
||||||
|
{
|
||||||
|
DownloadChapterTask newTask = new (Task.DownloadChapter, this.connectorName!, pub, newChapter, this.language, this);
|
||||||
|
taskManager.AddTask(newTask);
|
||||||
|
this.childTasks.Add(newTask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReplaceFailedChildTask(DownloadChapterTask failed, DownloadChapterTask newTask)
|
||||||
|
{
|
||||||
|
if (!this.childTasks.Contains(failed))
|
||||||
|
throw new ArgumentException($"Task {failed} is not childTask of {this}");
|
||||||
|
this.childTasks.Remove(failed);
|
||||||
|
this.childTasks.Add(newTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddChildTask(DownloadChapterTask childTask)
|
||||||
|
{
|
||||||
|
this.childTasks.Add(childTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the available Chapters of a Publication
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="connector">Connector to use</param>
|
||||||
|
/// <param name="publication">Publication to check</param>
|
||||||
|
/// <param name="language">Language to receive chapters for</param>
|
||||||
|
/// <param name="chapterCollection"></param>
|
||||||
|
/// <returns>List of Chapters that were previously not in collection</returns>
|
||||||
|
private static List<Chapter> GetNewChaptersList(Connector connector, Publication publication, string language, ref Dictionary<Publication, List<Chapter>> chapterCollection)
|
||||||
|
{
|
||||||
|
List<Chapter> newChaptersList = new();
|
||||||
|
chapterCollection.TryAdd(publication, newChaptersList); //To ensure publication is actually in collection
|
||||||
|
|
||||||
|
Chapter[] newChapters = connector.GetChapters(publication, language);
|
||||||
|
newChaptersList = newChapters.Where(nChapter => !connector.CheckChapterIsDownloaded(publication, nChapter)).ToList();
|
||||||
|
|
||||||
|
return newChaptersList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{base.ToString()}, {connectorName}, {publication.sortName} {publication.internalId}";
|
||||||
|
}
|
||||||
|
}
|
19
Tranga/TrangaTasks/UpdateLibrariesTask.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using Logging;
|
||||||
|
|
||||||
|
namespace Tranga.TrangaTasks;
|
||||||
|
|
||||||
|
public class UpdateLibrariesTask : TrangaTask
|
||||||
|
{
|
||||||
|
public UpdateLibrariesTask(Task task, TimeSpan reoccurrence) : base(task, reoccurrence)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ExecuteTask(TaskManager taskManager, Logger? logger, CancellationToken? cancellationToken = null)
|
||||||
|
{
|
||||||
|
if (cancellationToken?.IsCancellationRequested??false)
|
||||||
|
return;
|
||||||
|
foreach(LibraryManager lm in taskManager.settings.libraryManagers)
|
||||||
|
lm.UpdateLibrary();
|
||||||
|
this.progress = 1f;
|
||||||
|
}
|
||||||
|
}
|
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;"]
|
157
Website/apiConnector.js
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
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 + "/Controllers/Get";
|
||||||
|
let json = await GetData(uri);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function GetPublicationFromConnector(connectorName, title){
|
||||||
|
var uri = apiUri + `/Publications/GetFromConnector?connectorName=${connectorName}&title=${title}`;
|
||||||
|
let json = await GetData(uri);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function GetKnownPublications(){
|
||||||
|
var uri = apiUri + "/Publications/GetKnown";
|
||||||
|
let json = await GetData(uri);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function GetPublication(internalId){
|
||||||
|
var uri = apiUri + `/Publications/GetKnown?internalId=${internalId}`;
|
||||||
|
let json = await GetData(uri);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function GetChapters(internalId, connectorName, language){
|
||||||
|
var uri = apiUri + `/Publications/GetChapters?internalId=${internalId}&connectorName=${connectorName}&language=${language}`;
|
||||||
|
let json = await GetData(uri);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function GetTaskTypes(){
|
||||||
|
var uri = apiUri + "/Tasks/GetTypes";
|
||||||
|
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=UpdateLibraries";
|
||||||
|
let json = await GetData(uri);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateMonitorTask(connectorName, internalId, reoccurrence, language){
|
||||||
|
var uri = apiUri + `/Tasks/CreateMonitorTask?connectorName=${connectorName}&internalId=${internalId}&reoccurrenceTime=${reoccurrence}&language=${language}`;
|
||||||
|
PostData(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateUpdateLibraryTask(reoccurrence){
|
||||||
|
var uri = apiUri + `/Tasks/CreateUpdateLibraryTask?reoccurrenceTime=${reoccurrence}`;
|
||||||
|
PostData(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateDownloadChaptersTask(connectorName, internalId, chapters, language){
|
||||||
|
var uri = apiUri + `/Tasks/CreateDownloadChaptersTask?connectorName=${connectorName}&internalId=${internalId}&chapters=${chapters}&language=${language}`;
|
||||||
|
PostData(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StartTask(taskType, connectorName, internalId){
|
||||||
|
var uri = apiUri + `/Tasks/Start?taskType=${taskType}&connectorName=${connectorName}&internalId=${internalId}`;
|
||||||
|
PostData(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EnqueueTask(taskType, connectorName, publicationId){
|
||||||
|
var uri = apiUri + `/Queue/Enqueue?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}`;
|
||||||
|
PostData(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpdateSettings(downloadLocation, komgaUrl, komgaAuth, kavitaUrl, kavitaUser, kavitaPass){
|
||||||
|
var uri = apiUri + "/Settings/Update?"
|
||||||
|
if(downloadLocation != ""){
|
||||||
|
uri += "&downloadLocation="+downloadLocation;
|
||||||
|
}
|
||||||
|
if(komgaUrl != "" && komgaAuth != ""){
|
||||||
|
uri += `&komgaUrl=${komgaUrl}&komgaAuth=${komgaAuth}`;
|
||||||
|
}
|
||||||
|
if(kavitaUrl != "" && kavitaUser != "" && kavitaPass != ""){
|
||||||
|
uri += `&kavitaUrl=${kavitaUrl}&kavitaUsername=${kavitaUser}&kavitaPassword=${kavitaPass}`;
|
||||||
|
}
|
||||||
|
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 |
168
Website/index.html
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Tranga</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<wrapper>
|
||||||
|
<topbar>
|
||||||
|
<titlebox>
|
||||||
|
<img alt="website image is Blahaj" src="media/blahaj.png">
|
||||||
|
<span>Tranga</span>
|
||||||
|
</titlebox>
|
||||||
|
<spacer></spacer>
|
||||||
|
<searchdiv>
|
||||||
|
<label for="searchbox"></label><input id="searchbox" placeholder="Filter" type="text">
|
||||||
|
</searchdiv>
|
||||||
|
<img id="settingscog" src="media/settings-cogwheel.svg" height="100%" alt="settingscog">
|
||||||
|
</topbar>
|
||||||
|
<viewport>
|
||||||
|
<content>
|
||||||
|
<div id="addPublication">
|
||||||
|
<p>+</p>
|
||||||
|
</div>
|
||||||
|
<publication>
|
||||||
|
<img alt="cover" src="media/cover.jpg">
|
||||||
|
<publication-information>
|
||||||
|
<connector-name class="pill">MangaDex</connector-name>
|
||||||
|
<publication-name>Tensei Pandemic</publication-name>
|
||||||
|
</publication-information>
|
||||||
|
</publication>
|
||||||
|
</content>
|
||||||
|
|
||||||
|
<popup id="selectPublicationPopup">
|
||||||
|
<blur-background id="blurBackgroundTaskPopup"></blur-background>
|
||||||
|
<popup-window>
|
||||||
|
<popup-title>Select Publication</popup-title>
|
||||||
|
<popup-content>
|
||||||
|
<div>
|
||||||
|
<label for="connectors">Connector</label>
|
||||||
|
<select id="connectors">
|
||||||
|
<option value=""></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="searchPublicationQuery">Search Title</label><input id="searchPublicationQuery" type="text"></addtask-setting>
|
||||||
|
</div>
|
||||||
|
<input type="submit" value="Search" style="font-weight: bolder" onclick="NewSearch();">
|
||||||
|
</popup-content>
|
||||||
|
<div id="taskSelectOutput"></div>
|
||||||
|
</popup-window>
|
||||||
|
</popup>
|
||||||
|
|
||||||
|
<popup id="createMonitorTaskPopup">
|
||||||
|
<blur-background id="blurBackgroundCreateMonitorTaskPopup"></blur-background>
|
||||||
|
<popup-window>
|
||||||
|
<popup-title>Create Task: Monitor Publication</popup-title>
|
||||||
|
<popup-content>
|
||||||
|
<div>
|
||||||
|
<span>Run every</span>
|
||||||
|
<label for="hours"></label><input id="hours" type="number" value="3" min="0" max="23"><span>hours</span>
|
||||||
|
<label for="minutes"></label><input id="minutes" type="number" value="0" min="0" max="59"><span>minutes</span>
|
||||||
|
<input type="submit" value="Create" onclick="AddMonitorTask()">
|
||||||
|
</div>
|
||||||
|
</popup-content>
|
||||||
|
</popup-window>
|
||||||
|
</popup>
|
||||||
|
|
||||||
|
<popup id="createDownloadChaptersTask">
|
||||||
|
<blur-background id="blurBackgroundCreateDownloadChaptersTask"></blur-background>
|
||||||
|
<popup-window>
|
||||||
|
<popup-title>Create Task: Download Chapter(s)</popup-title>
|
||||||
|
<popup-content>
|
||||||
|
<div>
|
||||||
|
<label for="selectedChapters">Chapters:</label><input id="selectedChapters" placeholder="Select"><input type="submit" value="Select" onclick="DownloadChapterTaskClick()">
|
||||||
|
</div>
|
||||||
|
<div id="chapterOutput">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</popup-content>
|
||||||
|
</popup-window>
|
||||||
|
</popup>
|
||||||
|
|
||||||
|
<popup id="publicationViewerPopup">
|
||||||
|
<blur-background id="blurBackgroundPublicationPopup"></blur-background>
|
||||||
|
<publication-viewer>
|
||||||
|
<img id="pubviewcover" src="media/cover.jpg" alt="cover">
|
||||||
|
<publication-information>
|
||||||
|
<publication-name id="publicationViewerName">Tensei Pandemic</publication-name>
|
||||||
|
<publication-tags id="publicationViewerTags"></publication-tags>
|
||||||
|
<publication-author id="publicationViewerAuthor">Imamura Hinata</publication-author>
|
||||||
|
<publication-description id="publicationViewerDescription">Imamura Hinata is a high school boy with a cute appearance.
|
||||||
|
Since his trauma with the first love, he wanted to be more manly than anybody else. But one day he woke up to something different…
|
||||||
|
The total opposite of his ideal male body!
|
||||||
|
Pandemic love comedy!
|
||||||
|
</publication-description>
|
||||||
|
<publication-interactions>
|
||||||
|
<publication-starttask>Start Task ▶️</publication-starttask>
|
||||||
|
<publication-delete>Delete Task ❌</publication-delete>
|
||||||
|
<publication-add id="createMonitorTaskButton">Monitor ➕</publication-add>
|
||||||
|
<publication-add id="createDownloadChapterTaskButton">Download Chapter ➕</publication-add>
|
||||||
|
</publication-interactions>
|
||||||
|
</publication-information>
|
||||||
|
</publication-viewer>
|
||||||
|
</popup>
|
||||||
|
|
||||||
|
<popup id="settingsPopup">
|
||||||
|
<blur-background id="blurBackgroundSettingsPopup"></blur-background>
|
||||||
|
<popup-window>
|
||||||
|
<popup-title>Settings</popup-title>
|
||||||
|
<popup-content>
|
||||||
|
<div>
|
||||||
|
<p class="title">Download Location:</p>
|
||||||
|
<span id="downloadLocation"></span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="title">API-URI</p>
|
||||||
|
<label for="settingApiUri"></label><input placeholder="https://" type="text" id="settingApiUri">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="title">Komga</span>
|
||||||
|
<div>Configured: <span id="komgaConfigured">✅❌</span></div>
|
||||||
|
<label for="komgaUrl"></label><input placeholder="URL" id="komgaUrl" type="text">
|
||||||
|
<label for="komgaUsername"></label><input placeholder="Username" id="komgaUsername" type="text">
|
||||||
|
<label for="komgaPassword"></label><input placeholder="Password" id="komgaPassword" type="password">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="title">Kavita</span>
|
||||||
|
<div>Configured: <span id="kavitaConfigured">✅❌</span></div>
|
||||||
|
<label for="kavitaUrl"></label><input placeholder="URL" id="kavitaUrl" type="text">
|
||||||
|
<label for="kavitaUsername"></label><input placeholder="Username" id="kavitaUsername" type="text">
|
||||||
|
<label for="kavitaPassword"></label><input placeholder="Password" id="kavitaPassword" type="password">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="libraryUpdateTime" style="margin-right: 5px;">Update Time</label><input id="libraryUpdateTime" type="time" value="00:01:00" step="10">
|
||||||
|
<input type="submit" value="Update" onclick="UpdateLibrarySettings()">
|
||||||
|
</div>
|
||||||
|
</popup-content>
|
||||||
|
</popup-window>
|
||||||
|
</popup>
|
||||||
|
|
||||||
|
<popup id="downloadTasksPopup">
|
||||||
|
<blur-background id="blurBackgroundTasksQueuePopup"></blur-background>
|
||||||
|
<popup-window>
|
||||||
|
<popup-title>Task Progress</popup-title>
|
||||||
|
<popup-content>
|
||||||
|
|
||||||
|
</popup-content>
|
||||||
|
</popup-window>
|
||||||
|
</popup>
|
||||||
|
</viewport>
|
||||||
|
<footer>
|
||||||
|
<div onclick="ShowTasksQueue();">
|
||||||
|
<img src="media/running.svg" alt="running"><div id="tasksRunningTag">0</div>
|
||||||
|
</div>
|
||||||
|
<div onclick="ShowTasksQueue();">
|
||||||
|
<img src="media/queue.svg" alt="queue"><div id="tasksQueuedTag">0</div>
|
||||||
|
</div>
|
||||||
|
<p id="madeWith">Made with Blåhaj 🦈</p>
|
||||||
|
</footer>
|
||||||
|
</wrapper>
|
||||||
|
|
||||||
|
<script src="apiConnector.js"></script>
|
||||||
|
<script src="interaction.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
491
Website/interaction.js
Normal file
@ -0,0 +1,491 @@
|
|||||||
|
let publications = [];
|
||||||
|
let tasks = [];
|
||||||
|
let toEditId;
|
||||||
|
|
||||||
|
const searchBox = document.querySelector("#searchbox");
|
||||||
|
const searchPublicationQuery = document.querySelector("#searchPublicationQuery");
|
||||||
|
const selectPublication = document.querySelector("#taskSelectOutput");
|
||||||
|
const connectorSelect = document.querySelector("#connectors");
|
||||||
|
const settingsPopup = document.querySelector("#settingsPopup");
|
||||||
|
const settingsCog = document.querySelector("#settingscog");
|
||||||
|
const selectRecurrence = document.querySelector("#selectReccurrence");
|
||||||
|
const tasksContent = document.querySelector("content");
|
||||||
|
const selectPublicationPopup = document.querySelector("#selectPublicationPopup");
|
||||||
|
const createMonitorTaskButton = document.querySelector("#createMonitorTaskButton");
|
||||||
|
const createDownloadChapterTaskButton = document.querySelector("#createDownloadChapterTaskButton");
|
||||||
|
const createMonitorTaskPopup = document.querySelector("#createMonitorTaskPopup");
|
||||||
|
const createDownloadChaptersTask = document.querySelector("#createDownloadChaptersTask");
|
||||||
|
const chapterOutput = document.querySelector("#chapterOutput");
|
||||||
|
const selectedChapters = document.querySelector("#selectedChapters");
|
||||||
|
const publicationViewerPopup = document.querySelector("#publicationViewerPopup");
|
||||||
|
const publicationViewerWindow = document.querySelector("publication-viewer");
|
||||||
|
const publicationViewerDescription = document.querySelector("#publicationViewerDescription");
|
||||||
|
const publicationViewerName = document.querySelector("#publicationViewerName");
|
||||||
|
const publicationViewerTags = document.querySelector("#publicationViewerTags");
|
||||||
|
const publicationViewerAuthor = document.querySelector("#publicationViewerAuthor");
|
||||||
|
const pubviewcover = document.querySelector("#pubviewcover");
|
||||||
|
const publicationDelete = document.querySelector("publication-delete");
|
||||||
|
const publicationTaskStart = document.querySelector("publication-starttask");
|
||||||
|
const settingDownloadLocation = document.querySelector("#downloadLocation");
|
||||||
|
const settingKomgaUrl = document.querySelector("#komgaUrl");
|
||||||
|
const settingKomgaUser = document.querySelector("#komgaUsername");
|
||||||
|
const settingKomgaPass = document.querySelector("#komgaPassword");
|
||||||
|
const settingKavitaUrl = document.querySelector("#kavitaUrl");
|
||||||
|
const settingKavitaUser = document.querySelector("#kavitaUsername");
|
||||||
|
const settingKavitaPass = document.querySelector("#kavitaPassword");
|
||||||
|
const libraryUpdateTime = document.querySelector("#libraryUpdateTime");
|
||||||
|
const settingKomgaConfigured = document.querySelector("#komgaConfigured");
|
||||||
|
const settingKavitaConfigured = document.querySelector("#kavitaConfigured");
|
||||||
|
const settingApiUri = document.querySelector("#settingApiUri");
|
||||||
|
const tagTasksRunning = document.querySelector("#tasksRunningTag");
|
||||||
|
const tagTasksQueued = document.querySelector("#tasksQueuedTag");
|
||||||
|
const downloadTasksPopup = document.querySelector("#downloadTasksPopup");
|
||||||
|
const downloadTasksOutput = downloadTasksPopup.querySelector("popup-content");
|
||||||
|
|
||||||
|
searchbox.addEventListener("keyup", (event) => FilterResults());
|
||||||
|
settingsCog.addEventListener("click", () => OpenSettings());
|
||||||
|
document.querySelector("#blurBackgroundSettingsPopup").addEventListener("click", () => settingsPopup.style.display = "none");
|
||||||
|
document.querySelector("#blurBackgroundTaskPopup").addEventListener("click", () => selectPublicationPopup.style.display = "none");
|
||||||
|
document.querySelector("#blurBackgroundPublicationPopup").addEventListener("click", () => HidePublicationPopup());
|
||||||
|
document.querySelector("#blurBackgroundCreateMonitorTaskPopup").addEventListener("click", () => createMonitorTaskPopup.style.display = "none");
|
||||||
|
document.querySelector("#blurBackgroundCreateDownloadChaptersTask").addEventListener("click", () => createDownloadChaptersTask.style.display = "none");
|
||||||
|
document.querySelector("#blurBackgroundTasksQueuePopup").addEventListener("click", () => downloadTasksPopup.style.display = "none");
|
||||||
|
selectedChapters.addEventListener("keypress", (event) => {
|
||||||
|
if(event.key === "Enter"){
|
||||||
|
DownloadChapterTaskClick();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
publicationDelete.addEventListener("click", () => DeleteTaskClick());
|
||||||
|
createMonitorTaskButton.addEventListener("click", () => {
|
||||||
|
HidePublicationPopup();
|
||||||
|
createMonitorTaskPopup.style.display = "block";
|
||||||
|
});
|
||||||
|
createDownloadChapterTaskButton.addEventListener("click", () => {
|
||||||
|
HidePublicationPopup();
|
||||||
|
OpenDownloadChapterTaskPopup();
|
||||||
|
});
|
||||||
|
publicationTaskStart.addEventListener("click", () => StartTaskClick());
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
let availableConnectors;
|
||||||
|
GetAvailableControllers()
|
||||||
|
.then(json => availableConnectors = json)
|
||||||
|
.then(json =>
|
||||||
|
json.forEach(connector => {
|
||||||
|
var option = document.createElement('option');
|
||||||
|
option.value = connector;
|
||||||
|
option.innerText = connector;
|
||||||
|
connectorSelect.appendChild(option);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
function NewSearch(){
|
||||||
|
//Disable inputs
|
||||||
|
connectorSelect.disabled = true;
|
||||||
|
searchPublicationQuery.disabled = true;
|
||||||
|
//Waitcursor
|
||||||
|
document.body.style.cursor = "wait";
|
||||||
|
|
||||||
|
//Empty previous results
|
||||||
|
selectPublication.replaceChildren();
|
||||||
|
GetPublicationFromConnector(connectorSelect.value, searchPublicationQuery.value)
|
||||||
|
.then(json =>
|
||||||
|
json.forEach(publication => {
|
||||||
|
var option = CreatePublication(publication, connectorSelect.value);
|
||||||
|
option.addEventListener("click", (mouseEvent) => {
|
||||||
|
ShowPublicationViewerWindow(publication.internalId, mouseEvent, true);
|
||||||
|
});
|
||||||
|
selectPublication.appendChild(option);
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.then(() => {
|
||||||
|
//Re-enable inputs
|
||||||
|
connectorSelect.disabled = false;
|
||||||
|
searchPublicationQuery.disabled = false;
|
||||||
|
//Cursor
|
||||||
|
document.body.style.cursor = "initial";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//Returns a new "Publication" Item to display in the tasks section
|
||||||
|
function CreatePublication(publication, connector){
|
||||||
|
var publicationElement = document.createElement('publication');
|
||||||
|
publicationElement.setAttribute("id", publication.internalId);
|
||||||
|
var img = document.createElement('img');
|
||||||
|
img.src = `imageCache/${publication.coverFileNameInCache}`;
|
||||||
|
publicationElement.appendChild(img);
|
||||||
|
var info = document.createElement('publication-information');
|
||||||
|
var connectorName = document.createElement('connector-name');
|
||||||
|
connectorName.innerText = connector;
|
||||||
|
connectorName.className = "pill";
|
||||||
|
info.appendChild(connectorName);
|
||||||
|
var publicationName = document.createElement('publication-name');
|
||||||
|
publicationName.innerText = publication.sortName;
|
||||||
|
info.appendChild(publicationName);
|
||||||
|
publicationElement.appendChild(info);
|
||||||
|
if(publications.filter(pub => pub.internalId === publication.internalId) < 1)
|
||||||
|
publications.push(publication);
|
||||||
|
return publicationElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AddMonitorTask(){
|
||||||
|
var hours = document.querySelector("#hours").value;
|
||||||
|
var minutes = document.querySelector("#minutes").value;
|
||||||
|
CreateMonitorTask(connectorSelect.value, toEditId, `${hours}:${minutes}:00`, "en");
|
||||||
|
HidePublicationPopup();
|
||||||
|
createMonitorTaskPopup.style.display = "none";
|
||||||
|
selectPublicationPopup.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
function OpenDownloadChapterTaskPopup(){
|
||||||
|
selectedChapters.value = "";
|
||||||
|
chapterOutput.replaceChildren();
|
||||||
|
createDownloadChaptersTask.style.display = "block";
|
||||||
|
GetChapters(toEditId, connectorSelect.value, "en").then((json) => {
|
||||||
|
var i = 0;
|
||||||
|
json.forEach(chapter => {
|
||||||
|
var chapterDom = document.createElement("div");
|
||||||
|
var indexDom = document.createElement("span");
|
||||||
|
indexDom.className = "index";
|
||||||
|
indexDom.innerText = i++;
|
||||||
|
chapterDom.appendChild(indexDom);
|
||||||
|
|
||||||
|
var volDom = document.createElement("span");
|
||||||
|
volDom.className = "vol";
|
||||||
|
volDom.innerText = chapter.volumeNumber;
|
||||||
|
chapterDom.appendChild(volDom);
|
||||||
|
|
||||||
|
var chDom = document.createElement("span");
|
||||||
|
chDom.className = "ch";
|
||||||
|
chDom.innerText = chapter.chapterNumber;
|
||||||
|
chapterDom.appendChild(chDom);
|
||||||
|
|
||||||
|
var titleDom = document.createElement("span");
|
||||||
|
titleDom.innerText = chapter.name;
|
||||||
|
chapterDom.appendChild(titleDom);
|
||||||
|
chapterOutput.appendChild(chapterDom);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function DownloadChapterTaskClick(){
|
||||||
|
CreateDownloadChaptersTask(connectorSelect.value, toEditId, selectedChapters.value, "en");
|
||||||
|
HidePublicationPopup();
|
||||||
|
createDownloadChaptersTask.style.display = "none";
|
||||||
|
selectPublicationPopup.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeleteTaskClick(){
|
||||||
|
taskToDelete = tasks.filter(tTask => tTask.publication.internalId === toEditId)[0];
|
||||||
|
DeleteTask("DownloadNewChapters", taskToDelete.connectorName, toEditId);
|
||||||
|
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.authors.join(',');
|
||||||
|
pubviewcover.src = `imageCache/${publication.coverFileNameInCache}`;
|
||||||
|
toEditId = publicationId;
|
||||||
|
|
||||||
|
//Check what action should be listed
|
||||||
|
if(add){
|
||||||
|
createMonitorTaskButton.style.display = "initial";
|
||||||
|
createDownloadChapterTaskButton.style.display = "initial";
|
||||||
|
publicationDelete.style.display = "none";
|
||||||
|
publicationTaskStart.style.display = "none";
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
createMonitorTaskButton.style.display = "none";
|
||||||
|
createDownloadChapterTaskButton.style.display = "none";
|
||||||
|
publicationDelete.style.display = "initial";
|
||||||
|
publicationTaskStart.style.display = "initial";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function HidePublicationPopup(){
|
||||||
|
publicationViewerPopup.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
function ShowNewTaskWindow(){
|
||||||
|
selectPublication.replaceChildren();
|
||||||
|
searchPublicationQuery.value = "";
|
||||||
|
selectPublicationPopup.style.display = "flex";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const fadeIn = [
|
||||||
|
{ opacity: "0" },
|
||||||
|
{ opacity: "1" }
|
||||||
|
];
|
||||||
|
|
||||||
|
const fadeInTiming = {
|
||||||
|
duration: 50,
|
||||||
|
iterations: 1,
|
||||||
|
fill: "forwards"
|
||||||
|
}
|
||||||
|
|
||||||
|
function OpenSettings(){
|
||||||
|
GetSettingsClick();
|
||||||
|
settingsPopup.style.display = "flex";
|
||||||
|
}
|
||||||
|
|
||||||
|
function GetSettingsClick(){
|
||||||
|
settingApiUri.value = "";
|
||||||
|
settingKomgaUrl.value = "";
|
||||||
|
settingKomgaUser.value = "";
|
||||||
|
settingKomgaPass.value = "";
|
||||||
|
settingKavitaUrl.value = "";
|
||||||
|
settingKavitaUser.value = "";
|
||||||
|
settingKavitaPass.value = "";
|
||||||
|
settingKomgaConfigured.innerText = "❌";
|
||||||
|
settingKavitaConfigured.innerText = "❌";
|
||||||
|
|
||||||
|
settingApiUri.placeholder = apiUri;
|
||||||
|
|
||||||
|
GetSettings().then(json => {
|
||||||
|
settingDownloadLocation.innerText = json.downloadLocation;
|
||||||
|
json.libraryManagers.forEach(lm => {
|
||||||
|
if(lm.libraryType == 0){
|
||||||
|
settingKomgaUrl.placeholder = lm.baseUrl;
|
||||||
|
settingKomgaUser.placeholder = "User";
|
||||||
|
settingKomgaPass.placeholder = "***";
|
||||||
|
settingKomgaConfigured.innerText = "✅";
|
||||||
|
} else if(lm.libraryType == 1){
|
||||||
|
settingKavitaUrl.placeholder = lm.baseUrl;
|
||||||
|
settingKavitaUser.placeholder = "User";
|
||||||
|
settingKavitaPass.placeholder = "***";
|
||||||
|
settingKavitaConfigured.innerText = "✅";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
GetKomgaTask().then(json => {
|
||||||
|
if(json.length > 0)
|
||||||
|
libraryUpdateTime.value = json[0].reoccurrence;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpdateLibrarySettings(){
|
||||||
|
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, "", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(settingKavitaUrl.value != "" && settingKavitaUser.value != "" && settingKavitaPass.value != ""){
|
||||||
|
UpdateSettings("", "", "", settingKavitaUrl.value, settingKavitaUser.value, settingKavitaPass.value);
|
||||||
|
}
|
||||||
|
CreateUpdateLibraryTask(libraryUpdateTime.value);
|
||||||
|
setTimeout(() => GetSettingsClick(), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function utf8_to_b64( str ) {
|
||||||
|
return window.btoa(unescape(encodeURIComponent( str )));
|
||||||
|
}
|
||||||
|
|
||||||
|
function FilterResults(){
|
||||||
|
if(searchBox.value.length > 0){
|
||||||
|
tasksContent.childNodes.forEach(publication => {
|
||||||
|
publication.childNodes.forEach(item => {
|
||||||
|
if(item.nodeName.toLowerCase() == "publication-information"){
|
||||||
|
item.childNodes.forEach(information => {
|
||||||
|
if(information.nodeName.toLowerCase() == "publication-name"){
|
||||||
|
if(!information.textContent.toLowerCase().includes(searchBox.value.toLowerCase())){
|
||||||
|
publication.style.display = "none";
|
||||||
|
}else{
|
||||||
|
publication.style.display = "initial";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
tasksContent.childNodes.forEach(publication => publication.style.display = "initial");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ShowTasksQueue(){
|
||||||
|
|
||||||
|
downloadTasksOutput.replaceChildren();
|
||||||
|
GetRunningTasks()
|
||||||
|
.then(json => {
|
||||||
|
tagTasksRunning.innerText = json.length;
|
||||||
|
json.forEach(task => {
|
||||||
|
downloadTasksOutput.appendChild(CreateProgressChild(task));
|
||||||
|
document.querySelector(`#progress${task.taskId.replaceAll('=','')}`).value = task.progress;
|
||||||
|
document.querySelector(`#progressStr${task.taskId.replaceAll('=','')}`).innerText = task.progress.toLocaleString(undefined,{style: 'percent', minimumFractionDigits:2});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
GetQueue()
|
||||||
|
.then(json => {
|
||||||
|
tagTasksQueued.innerText = json.length;
|
||||||
|
json.forEach(task => {
|
||||||
|
downloadTasksOutput.appendChild(CreateProgressChild(task));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
downloadTasksPopup.style.display = "flex";
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateProgressChild(task){
|
||||||
|
var child = document.createElement("div");
|
||||||
|
var img = document.createElement('img');
|
||||||
|
img.src = `imageCache/${task.publication.coverFileNameInCache}`;
|
||||||
|
child.appendChild(img);
|
||||||
|
|
||||||
|
var name = document.createElement("span");
|
||||||
|
name.innerText = task.publication.sortName;
|
||||||
|
name.className = "pubTitle";
|
||||||
|
child.appendChild(name);
|
||||||
|
|
||||||
|
|
||||||
|
var progress = document.createElement("progress");
|
||||||
|
progress.id = `progress${task.taskId.replaceAll('=','')}`;
|
||||||
|
child.appendChild(progress);
|
||||||
|
|
||||||
|
var progressStr = document.createElement("span");
|
||||||
|
progressStr.innerText = "00.00%";
|
||||||
|
progressStr.className = "progressStr";
|
||||||
|
progressStr.id = `progressStr${task.taskId.replaceAll('=','')}`;
|
||||||
|
child.appendChild(progressStr);
|
||||||
|
|
||||||
|
if(task.chapter != undefined){
|
||||||
|
var chapterNumber = document.createElement("span");
|
||||||
|
chapterNumber.className = "chapterNumber";
|
||||||
|
chapterNumber.innerText = `Vol.${task.chapter.volumeNumber} Ch.${task.chapter.chapterNumber}`;
|
||||||
|
child.appendChild(chapterNumber);
|
||||||
|
|
||||||
|
var chapterName = document.createElement("span");
|
||||||
|
chapterName.className = "chapterName";
|
||||||
|
chapterName.innerText = task.chapter.name;
|
||||||
|
child.appendChild(chapterName);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Resets the tasks shown
|
||||||
|
ResetContent();
|
||||||
|
downloadTasksOutput.replaceChildren();
|
||||||
|
//Get Tasks and show them
|
||||||
|
GetDownloadTasks()
|
||||||
|
.then(json => json.forEach(task => {
|
||||||
|
var publication = CreatePublication(task.publication, task.connectorName);
|
||||||
|
publication.addEventListener("click", (event) => ShowPublicationViewerWindow(task.publication.internalId, event, false));
|
||||||
|
tasksContent.appendChild(publication);
|
||||||
|
tasks.push(task);
|
||||||
|
}));
|
||||||
|
|
||||||
|
GetRunningTasks()
|
||||||
|
.then(json => {
|
||||||
|
tagTasksRunning.innerText = json.length;
|
||||||
|
json.forEach(task => {
|
||||||
|
downloadTasksOutput.appendChild(CreateProgressChild(task));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
GetQueue()
|
||||||
|
.then(json => {
|
||||||
|
tagTasksQueued.innerText = json.length;
|
||||||
|
json.forEach(task => {
|
||||||
|
downloadTasksOutput.appendChild(CreateProgressChild(task));
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
//Tasks from API
|
||||||
|
var cTasks = [];
|
||||||
|
GetDownloadTasks()
|
||||||
|
.then(json => json.forEach(task => cTasks.push(task)))
|
||||||
|
.then(() => {
|
||||||
|
//Only update view if tasks-amount has changed
|
||||||
|
if(tasks.length != cTasks.length) {
|
||||||
|
//Resets the tasks shown
|
||||||
|
ResetContent();
|
||||||
|
//Add all currenttasks to view
|
||||||
|
cTasks.forEach(task => {
|
||||||
|
var publication = CreatePublication(task.publication, task.connectorName);
|
||||||
|
publication.addEventListener("click", (event) => ShowPublicationViewerWindow(task.publication.internalId, event, false));
|
||||||
|
tasksContent.appendChild(publication);
|
||||||
|
})
|
||||||
|
|
||||||
|
tasks = cTasks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
GetRunningTasks()
|
||||||
|
.then(json => {
|
||||||
|
tagTasksRunning.innerText = json.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
GetQueue()
|
||||||
|
.then(json => {
|
||||||
|
tagTasksQueued.innerText = json.length;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
GetRunningTasks().then((json) => {
|
||||||
|
json.forEach(task => {
|
||||||
|
document.querySelector(`#progress${task.taskId.replaceAll('=','')}`).value = task.progress;
|
||||||
|
document.querySelector(`#progressStr${task.taskId.replaceAll('=','')}`).innerText = task.progress.toLocaleString(undefined,{style: 'percent', minimumFractionDigits:2});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},500);
|
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 |
601
Website/style.css
Normal file
@ -0,0 +1,601 @@
|
|||||||
|
:root{
|
||||||
|
--background-color: #030304;
|
||||||
|
--second-background-color: #fff;
|
||||||
|
--primary-color: #f5a9b8;
|
||||||
|
--secondary-color: #5bcefa;
|
||||||
|
--accent-color: #fff;
|
||||||
|
--topbar-height: 60px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body{
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
background-placeholder{
|
||||||
|
background-color: var(--second-background-color);
|
||||||
|
opacity: 1;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 0 0 5px 0;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: var(--topbar-height);
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
z-index: 100;
|
||||||
|
box-shadow: 0 0 20px black;
|
||||||
|
}
|
||||||
|
|
||||||
|
titlebox {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
margin: 0 0 0 40px;
|
||||||
|
height: 100%;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
titlebox span{
|
||||||
|
cursor: default;
|
||||||
|
font-size: 24pt;
|
||||||
|
font-weight: bold;
|
||||||
|
background: linear-gradient(150deg, var(--primary-color), var(--accent-color));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
titlebox img {
|
||||||
|
height: 100%;
|
||||||
|
margin-right: 10px;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
spacer{
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchdiv{
|
||||||
|
display: block;
|
||||||
|
margin: 0 10px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchbox {
|
||||||
|
padding: 3px 10px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14pt;
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settingscog {
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0px 30px;
|
||||||
|
height: 50%;
|
||||||
|
filter: invert(100%) sepia(0%) saturate(7465%) hue-rotate(115deg) brightness(116%) contrast(101%);
|
||||||
|
}
|
||||||
|
|
||||||
|
viewport {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer > div {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0 30px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer > div > *{
|
||||||
|
height: 40%;
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#madeWith {
|
||||||
|
flex-grow: 1;
|
||||||
|
text-align: right;
|
||||||
|
margin-right: 20px;
|
||||||
|
cursor: url("media/blahaj.png"), grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
content {
|
||||||
|
position: relative;
|
||||||
|
flex-grow: 1;
|
||||||
|
border-radius: 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: start;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settingsPopup{
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settingsPopup popup-content{
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: start;
|
||||||
|
margin: 15px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settingsPopup popup-content > * {
|
||||||
|
margin: 5px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settingsPopup popup-content .title {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
#addPublication {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
width: 180px;
|
||||||
|
height: 300px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 10px 10px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#addPublication p{
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 150pt;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 300px;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill {
|
||||||
|
flex-grow: 0;
|
||||||
|
height: 14pt;
|
||||||
|
font-size: 12pt;
|
||||||
|
border-radius: 9pt;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
padding: 2pt 17px;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication{
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
width: 180px;
|
||||||
|
height: 300px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 10px 10px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication::after{
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0; top: 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
background: linear-gradient(rgba(0,0,0,0.8), rgba(0, 0, 0, 0.7),rgba(0, 0, 0, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-information {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-information * {
|
||||||
|
z-index: 1;
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
connector-name{
|
||||||
|
width: fit-content;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-name{
|
||||||
|
width: fit-content;
|
||||||
|
font-size: 16pt;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication img {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
z-index: 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
popup{
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 2;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
popup popup-window {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
left: 25%;
|
||||||
|
top: 100px;
|
||||||
|
width: 50%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--second-background-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
popup popup-window popup-title {
|
||||||
|
height: 30px;
|
||||||
|
font-size: 14pt;
|
||||||
|
font-weight: bolder;
|
||||||
|
padding: 5px 10px;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: var(--accent-color)
|
||||||
|
}
|
||||||
|
|
||||||
|
popup popup-window popup-content{
|
||||||
|
margin: 15px 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
popup popup-window popup-content div > * {
|
||||||
|
margin: 2px 3px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
popup popup-window popup-content input, select {
|
||||||
|
padding: 3px 4px;
|
||||||
|
width: 130px;
|
||||||
|
border: 1px solid lightgrey;
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#selectPublicationPopup publication {
|
||||||
|
width: 150px;
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#createTaskPopup {
|
||||||
|
z-index: 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
#createTaskPopup input {
|
||||||
|
height: 30px;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#createMonitorTaskPopup, #createDownloadChaptersTask {
|
||||||
|
z-index: 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
#createMonitorTaskPopup input[type="number"] {
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#createDownloadChaptersTask popup-content {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
#createDownloadChaptersTask popup-content > * {
|
||||||
|
margin: 3px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#createDownloadChaptersTask #chapterOutput {
|
||||||
|
max-height: 50vh;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
#createDownloadChaptersTask #chapterOutput .index{
|
||||||
|
display: inline-block;
|
||||||
|
width: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#createDownloadChaptersTask #chapterOutput .index::after{
|
||||||
|
content: ':';
|
||||||
|
}
|
||||||
|
|
||||||
|
#createDownloadChaptersTask #chapterOutput .vol::before{
|
||||||
|
content: 'Vol.';
|
||||||
|
}
|
||||||
|
|
||||||
|
#createDownloadChaptersTask #chapterOutput .vol{
|
||||||
|
display: inline-block;
|
||||||
|
width: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#createDownloadChaptersTask #chapterOutput .ch::before{
|
||||||
|
content: 'Ch.';
|
||||||
|
}
|
||||||
|
|
||||||
|
#createDownloadChaptersTask #chapterOutput .ch {
|
||||||
|
display: inline-block;
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#downloadTasksPopup popup-window {
|
||||||
|
left: 0;
|
||||||
|
top: 80px;
|
||||||
|
margin: 0 0 0 10px;
|
||||||
|
height: calc(100vh - 140px);
|
||||||
|
width: 400px;
|
||||||
|
max-width: 95vw;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
#downloadTasksPopup popup-content {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: start;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#downloadTasksPopup popup-content > div {
|
||||||
|
display: block;
|
||||||
|
height: 80px;
|
||||||
|
position: relative;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#downloadTasksPopup popup-content > div > img {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 60px;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#downloadTasksPopup popup-content > div > span {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
#downloadTasksPopup popup-content > div > .pubTitle {
|
||||||
|
left: 70px;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#downloadTasksPopup popup-content > div > .chapterName {
|
||||||
|
left: 70px;
|
||||||
|
top: 28pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
#downloadTasksPopup popup-content > div > .chapterNumber {
|
||||||
|
left: 70px;
|
||||||
|
top: 14pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
#downloadTasksPopup popup-content > div > progress {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: 150px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#downloadTasksPopup popup-content > div > .progressStr {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: 70px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
blur-background {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
background-color: black;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#taskSelectOutput{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: start;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
#publicationViewerPopup{
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer{
|
||||||
|
display: block;
|
||||||
|
width: 450px;
|
||||||
|
position: absolute;
|
||||||
|
top: 200px;
|
||||||
|
left: 400px;
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer::after{
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0; top: 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0,0,0,0.8);
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer img {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 5px;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer publication-information > * {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer publication-information publication-name {
|
||||||
|
width: initial;
|
||||||
|
overflow-x: scroll;
|
||||||
|
white-space: nowrap;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer publication-information publication-tags::before {
|
||||||
|
content: "Tags";
|
||||||
|
display: block;
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer publication-information publication-tags {
|
||||||
|
overflow-x: scroll;
|
||||||
|
white-space: nowrap;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer publication-information publication-author::before {
|
||||||
|
content: "Author: ";
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer publication-information publication-description::before {
|
||||||
|
content: "Description";
|
||||||
|
display: block;
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer publication-information publication-description {
|
||||||
|
font-size: 12pt;
|
||||||
|
margin: 5px 0;
|
||||||
|
height: 145px;
|
||||||
|
overflow-x: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer publication-information publication-interactions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: end;
|
||||||
|
align-items: start;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer publication-information publication-interactions > * {
|
||||||
|
margin: 0 10px;
|
||||||
|
font-size: 16pt;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer publication-information publication-interactions publication-starttask {
|
||||||
|
color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer publication-information publication-interactions publication-delete {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication-viewer publication-information publication-interactions publication-add {
|
||||||
|
color: limegreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer-tag-popup {
|
||||||
|
display: none;
|
||||||
|
padding: 2px 4px;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 58px;
|
||||||
|
left: 20px;
|
||||||
|
background-color: var(--second-background-color);
|
||||||
|
z-index: 8;
|
||||||
|
border-radius: 5px;
|
||||||
|
max-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer-tag-content{
|
||||||
|
position: relative;
|
||||||
|
max-height: 400px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer-tag-content > * {
|
||||||
|
margin: 2px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer-tag-popup::before{
|
||||||
|
content: "";
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
position: absolute;
|
||||||
|
border-right: 10px solid var(--second-background-color);
|
||||||
|
border-left: 10px solid transparent;
|
||||||
|
border-top: 10px solid var(--second-background-color);
|
||||||
|
border-bottom: 10px solid transparent;
|
||||||
|
left: 0;
|
||||||
|
bottom: -17px;
|
||||||
|
border-radius: 0 0 0 5px;
|
||||||
|
}
|
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 |