60 Commits
0.7 ... 1.1.1

Author SHA1 Message Date
732c2f119c More logging 2023-05-26 15:09:26 +02:00
81638f4b4a Path.join joins paths ya know 2023-05-26 14:51:11 +02:00
c547aa6422 favicon <3 2023-05-26 14:49:17 +02:00
d80980512e #28 is a pain in the buttcheeks 2023-05-26 14:47:13 +02:00
f9f802155d #28 why was there a '!' 2023-05-26 14:43:47 +02:00
eef0955009 #28 wrong filesnames 2023-05-26 14:36:02 +02:00
ec25900ac0 resolves #29 start manual execution 2023-05-26 14:32:08 +02:00
e5fe14a09e #28 2023-05-26 14:31:34 +02:00
5dc91095f8 #28 2023-05-26 14:30:52 +02:00
985ac8fc7a Fix #28 coverimages 2023-05-26 14:07:11 +02:00
c9537a9963 #24 2023-05-26 13:39:42 +02:00
4fd3c03804 Styling 2023-05-26 13:30:20 +02:00
a1e9dd0232 resolves #27 Foldernames ending in '.' 2023-05-25 22:22:57 +02:00
aa1f9b1b56 background fade 2023-05-25 22:17:47 +02:00
6069578b6e style choices 2023-05-25 22:15:06 +02:00
a84b768e24 design choices 2023-05-25 22:05:29 +02:00
d1a21af15d resolves #26 2023-05-25 21:58:45 +02:00
7423ae6ace Update README.md 2023-05-25 18:36:21 +02:00
3aa7ba9d96 screenshots 2023-05-25 18:28:43 +02:00
fdbb4570be Adjusted settings style 2023-05-25 18:18:31 +02:00
b643a0c2a9 Fix Wrong API uri for GetRunningTasks
Added GetQueue function

Added display for running, queued, total tasks
2023-05-25 18:09:18 +02:00
6fa6f897aa More legal characters 2023-05-25 17:34:24 +02:00
2bfab0298d border-radius 2023-05-25 17:33:55 +02:00
147a20385b illegal filenames 2023-05-25 16:55:58 +02:00
afa18d6a2c Illlegal characters on linux 2023-05-25 16:47:24 +02:00
66980eef23 Position publication viewer always withing display 2023-05-25 16:05:54 +02:00
65f468a30a popup position now "fixed"
changed publicationviewer width
2023-05-25 16:05:40 +02:00
a91c33ee4f image sizing 2023-05-25 15:50:00 +02:00
f39482fe4c Corrected image path in publication preview 2023-05-25 15:48:38 +02:00
41f47b4d6b styling 2023-05-25 15:47:59 +02:00
be40091102 Publication background fade 2023-05-25 15:41:24 +02:00
665092be6a image scaling 2023-05-25 15:40:03 +02:00
653cb699d0 Removed Sidebar
Moved settings tab to popup
Added footer
2023-05-25 15:34:10 +02:00
8dbc5446ad depends_on compose 2023-05-25 14:46:05 +02:00
750df4ed52 Wrong return value 2023-05-25 14:38:43 +02:00
4772ae0756 No unnecessary downloads of covers if they already exist 2023-05-25 14:35:33 +02:00
23f703d5a5 imageCache readonly for website 2023-05-25 14:30:33 +02:00
6aa0ea277b #22 2023-05-25 14:28:56 +02:00
780df1cd6e Created Image-Cache 2023-05-25 14:25:23 +02:00
0b7da2e9cb Merge remote-tracking branch 'origin/master'
# Conflicts:
#	Website/interaction.js
2023-05-25 13:59:06 +02:00
01a059d26b Base 64 images #22 2023-05-25 13:58:54 +02:00
a8dbece237 Base 64 images #22 2023-05-25 13:58:10 +02:00
5efa00e059 Added field posterBase64 to Publication #22 2023-05-25 13:50:48 +02:00
02075ed1b1 Renamed RequestType Cover to CoverUrl 2023-05-25 13:50:08 +02:00
fabd16ccea Remove unnecessary output from dockerfile 2023-05-25 13:49:29 +02:00
79928075b0 docker-compose.yaml 2023-05-25 10:49:24 +02:00
9b8eb6a197 add seconds field to addtask recurrence 2023-05-25 10:47:12 +02:00
1d263ef45a Configurable API-location 2023-05-25 10:42:19 +02:00
e0877add30 Paths for Linux 2023-05-25 10:25:24 +02:00
046cad8072 Dockerfile for Tranga 2023-05-25 10:25:11 +02:00
b2ce55be96 Port 2023-05-25 01:25:21 +02:00
a6e9013495 Latest alpine image 2023-05-25 00:05:31 +02:00
14c69631a6 Corrected port 2023-05-24 23:53:39 +02:00
ccc4e42a49 Komga update can now be configured in seconds 2023-05-24 23:53:32 +02:00
d6e75fda31 Fixed empty returns if some value were null 2023-05-24 23:52:40 +02:00
fc89537f63 Fixed Authorization on redirect 2023-05-24 23:52:25 +02:00
fd3423d03c Correct Port 2023-05-24 22:56:15 +02:00
878f77766f Fix CORS 2023-05-24 22:56:10 +02:00
08001fd684 SSL cert error 2023-05-24 22:55:32 +02:00
e2917d2f2e Changed CORS policy allow all origins
Added Dockerfile to website
Changed Ports
2023-05-24 22:30:11 +02:00
28 changed files with 777 additions and 339 deletions

13
Dockerfile Normal file
View File

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

View File

@ -30,6 +30,9 @@
<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>
@ -64,24 +67,43 @@ That is why I wanted to create my own project, in a language I understand, and t
<p align="right">(<a href="#readme-top">back to top</a>)</p> <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 - 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
![image](screenshots/overview.png)
![image](screenshots/addtask.png)
| ![image](screenshots/settings.png) | ![image](screenshots/publication-description.png) |
|-----------------------------------:|:-------------------------------------------------:|
<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.
### Prerequisites ### Prerequisites
@ -90,7 +112,7 @@ A CLI will guide you through setup.
<!-- ROADMAP --> <!-- ROADMAP -->
## Roadmap ## Roadmap
- [ ] Web-UI #1 - [x] Web-UI #1
- [ ] More Connectors - [ ] More Connectors
- [ ] Manganato #2 - [ ] Manganato #2
- [ ] ? - [ ] ?

View File

@ -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"]

View File

@ -1,28 +1,35 @@
using System.Runtime.InteropServices;
using Logging; using Logging;
using Tranga; using Tranga;
string applicationFolderPath = string applicationFolderPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Tranga-API");
Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Tranga-API"); string downloadFolderPath = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(applicationFolderPath, "Manga");
string downloadFolderPath = Path.Join(applicationFolderPath, "Manga"); string logsFolderPath = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/var/logs/Tranga" : 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, "settings.json"); string settingsFilePath = Path.Join(applicationFolderPath, "settings.json");
Directory.CreateDirectory(applicationFolderPath);
Directory.CreateDirectory(logsFolderPath); Directory.CreateDirectory(logsFolderPath);
Logger logger = new(new[] { Logger.LoggerType.FileLogger, Logger.LoggerType.ConsoleLogger }, Console.Out, Console.Out.Encoding, logFilePath);
Console.WriteLine($"Logfile-Path: {logFilePath}");
Console.WriteLine($"Settings-File-Path: {settingsFilePath}"); logger.WriteLine("Tranga", "Loading settings.");
Logger logger = new(new[] { Logger.LoggerType.FileLogger }, null, null, logFilePath);
logger.WriteLine("Tranga_CLI", "Loading Taskmanager.");
TrangaSettings settings; TrangaSettings settings;
if (File.Exists(settingsFilePath)) if (File.Exists(settingsFilePath))
settings = TrangaSettings.LoadSettings(settingsFilePath); settings = TrangaSettings.LoadSettings(settingsFilePath);
else else
settings = new TrangaSettings(downloadFolderPath, applicationFolderPath, null); settings = new TrangaSettings(downloadFolderPath, applicationFolderPath, null);
Directory.CreateDirectory(settings.workingDirectory);
Directory.CreateDirectory(settings.downloadLocation);
Directory.CreateDirectory(settings.coverImageCache);
logger.WriteLine("Tranga",$"Application-Folder: {settings.workingDirectory}");
logger.WriteLine("Tranga",$"Settings-File-Path: {settings.settingsFilePath}");
logger.WriteLine("Tranga",$"Download-Folder-Path: {settings.downloadLocation}");
logger.WriteLine("Tranga",$"Logfile-Path: {logFilePath}");
logger.WriteLine("Tranga",$"Image-Cache-Path: {settings.coverImageCache}");
logger.WriteLine("Tranga", "Loading Taskmanager.");
TaskManager taskManager = new (settings, logger); TaskManager taskManager = new (settings, logger);
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@ -36,7 +43,7 @@ builder.Services.AddCors(options =>
options.AddPolicy(name: corsHeader, options.AddPolicy(name: corsHeader,
policy => policy =>
{ {
policy.WithOrigins("http://localhost", "http://127.0.0.1", "http://localhost:63342"); policy.AllowAnyOrigin();
policy.WithMethods("GET", "POST", "DELETE"); policy.WithMethods("GET", "POST", "DELETE");
}); });
}); });
@ -82,23 +89,45 @@ app.MapDelete("/Tasks/Delete", (string taskType, string? connectorName, string?
app.MapGet("/Tasks/Get", (string taskType, string? connectorName, string? searchString) => app.MapGet("/Tasks/Get", (string taskType, string? connectorName, string? searchString) =>
{ {
TrangaTask.Task task = Enum.Parse<TrangaTask.Task>(taskType); try
if (searchString is null) {
return taskManager.GetAllTasks().Where(tTask => tTask.task == task && tTask.connectorName == connectorName); TrangaTask.Task task = Enum.Parse<TrangaTask.Task>(taskType);
else if (searchString is null || connectorName is null)
return taskManager.GetAllTasks().Where(tTask => return taskManager.GetAllTasks().Where(tTask => tTask.task == task);
tTask.task == task && tTask.connectorName == connectorName && tTask.ToString() else
.Contains(searchString, StringComparison.InvariantCultureIgnoreCase)); return taskManager.GetAllTasks().Where(tTask =>
tTask.task == task && tTask.connectorName == connectorName && tTask.ToString()
.Contains(searchString, StringComparison.InvariantCultureIgnoreCase));
}
catch (ArgumentException)
{
return Array.Empty<TrangaTask>();
}
}); });
app.MapPost("/Tasks/Start", (string taskType, string? connectorName, string? publicationId) => app.MapPost("/Tasks/Start", (string taskType, string? connectorName, string? publicationId) =>
{ {
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType); try
TrangaTask? task = taskManager.GetAllTasks().FirstOrDefault(tTask => {
tTask.task == pTask && tTask.publication?.internalId == publicationId && tTask.connectorName == connectorName); TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType);
if (task is null) TrangaTask? task = null;
if (connectorName is null || publicationId is null)
task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
tTask.task == pTask);
else
task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
tTask.task == pTask && tTask.publication?.internalId == publicationId &&
tTask.connectorName == connectorName);
if (task is null)
return;
taskManager.ExecuteTaskNow(task);
}
catch (ArgumentException)
{
return; return;
taskManager.ExecuteTaskNow(task); }
}); });
app.MapGet("/Tasks/GetRunningTasks", app.MapGet("/Tasks/GetRunningTasks",
@ -109,22 +138,50 @@ app.MapGet("/Queue/GetList",
app.MapPost("/Queue/Enqueue", (string taskType, string? connectorName, string? publicationId) => app.MapPost("/Queue/Enqueue", (string taskType, string? connectorName, string? publicationId) =>
{ {
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType); try
TrangaTask? task = taskManager.GetAllTasks().FirstOrDefault(tTask => {
tTask.task == pTask && tTask.publication?.internalId == publicationId && tTask.connectorName == connectorName); TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType);
if (task is null) TrangaTask? task = null;
if (connectorName is null || publicationId is null)
task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
tTask.task == pTask);
else
task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
tTask.task == pTask && tTask.publication?.internalId == publicationId &&
tTask.connectorName == connectorName);
if (task is null)
return;
taskManager.AddTaskToQueue(task);
}
catch (ArgumentException)
{
return; return;
taskManager.AddTaskToQueue(task); }
}); });
app.MapDelete("/Queue/Dequeue", (string taskType, string? connectorName, string? publicationId) => app.MapDelete("/Queue/Dequeue", (string taskType, string? connectorName, string? publicationId) =>
{ {
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType); try
TrangaTask? task = taskManager.GetAllTasks().FirstOrDefault(tTask => {
tTask.task == pTask && tTask.publication?.internalId == publicationId && tTask.connectorName == connectorName); TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType);
if (task is null) TrangaTask? task = null;
if (connectorName is null || publicationId is null)
task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
tTask.task == pTask);
else
task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
tTask.task == pTask && tTask.publication?.internalId == publicationId &&
tTask.connectorName == connectorName);
if (task is null)
return;
taskManager.RemoveTaskFromQueue(task);
}
catch (ArgumentException)
{
return; return;
taskManager.RemoveTaskFromQueue(task); }
}); });
app.MapGet("/Settings/Get", () => taskManager.settings); app.MapGet("/Settings/Get", () => taskManager.settings);

View File

@ -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"]

View File

@ -1,3 +1,4 @@
<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/=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>

View File

@ -1,4 +1,5 @@
using System.Globalization; using System.Globalization;
using System.Text.RegularExpressions;
namespace Tranga; namespace Tranga;
@ -14,14 +15,15 @@ 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 is { Length: > 0 } ? volumeNumber : "1";
this.chapterNumber = chapterNumber; this.chapterNumber = chapterNumber;
this.url = url; this.url = url;
string chapterName = string.Concat((name ?? "").Split(Path.GetInvalidFileNameChars())); string chapterName = string.Concat(LegalCharacters.Matches(name ?? ""));
NumberFormatInfo nfi = new NumberFormatInfo() NumberFormatInfo nfi = new NumberFormatInfo()
{ {
NumberDecimalSeparator = "." NumberDecimalSeparator = "."

View File

@ -16,7 +16,9 @@ public abstract class Connector
protected Logger? logger; protected Logger? logger;
protected Connector(string downloadLocation, Logger? logger) protected string imageCachePath;
protected Connector(string downloadLocation, string imageCachePath, Logger? logger)
{ {
this.downloadLocation = downloadLocation; this.downloadLocation = downloadLocation;
this.logger = logger; this.logger = logger;
@ -24,6 +26,7 @@ public abstract class Connector
{ {
//RequestTypes for RateLimits //RequestTypes for RateLimits
}, logger); }, logger);
this.imageCachePath = 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)
@ -52,12 +55,13 @@ public abstract class Connector
/// <param name="publication">Publication that contains Chapter</param> /// <param name="publication">Publication that contains Chapter</param>
/// <param name="chapter">Chapter with Images to retrieve</param> /// <param name="chapter">Chapter with Images to retrieve</param>
public abstract void DownloadChapter(Publication publication, Chapter chapter); public abstract void DownloadChapter(Publication publication, Chapter chapter);
/// <summary> /// <summary>
/// Retrieves the Cover from the Website /// Retrieves the Cover from the Website
/// </summary> /// </summary>
/// <param name="publication">Publication to retrieve Cover for</param> /// <param name="publication">Publication to retrieve Cover for</param>
public abstract void DownloadCover(Publication publication); /// <param name="settings">TrangaSettings</param>
public abstract void CloneCoverFromCache(Publication publication, TrangaSettings settings);
/// <summary> /// <summary>
/// Saves the series-info to series.json in the Publication Folder /// Saves the series-info to series.json in the Publication Folder
@ -83,7 +87,7 @@ public abstract class Connector
/// <returns>XML-string</returns> /// <returns>XML-string</returns>
protected static string CreateComicInfo(Publication publication, Chapter chapter, Logger? logger) protected static string CreateComicInfo(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),
@ -134,11 +138,12 @@ public abstract class Connector
/// <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="downloadClient">DownloadClient of the connector</param>
/// <param name="logger"></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>
/// <param name="requestType">RequestType for RateLimits</param> /// <param name="requestType">RequestType for RateLimits</param>
protected static void DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, DownloadClient downloadClient, byte requestType, Logger? logger, string? comicInfoPath = null) protected static void DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, DownloadClient downloadClient, byte requestType, Logger? logger, string? comicInfoPath = null)
{ {
logger?.WriteLine("Connector", "Downloading Images"); logger?.WriteLine("Connector", $"Downloading Images for {saveArchiveFilePath}");
//Check if Publication Directory already exists //Check if Publication Directory already exists
string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!; string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!;
if (!Directory.Exists(directoryPath)) if (!Directory.Exists(directoryPath))
@ -156,13 +161,14 @@ public abstract class Connector
{ {
string[] split = imageUrl.Split('.'); string[] split = imageUrl.Split('.');
string extension = split[^1]; string extension = split[^1];
logger?.WriteLine("Connector", $"Downloading Image {chapter + 1}/{imageUrls.Length}");
DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), downloadClient, requestType); DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), downloadClient, requestType);
} }
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, saveArchiveFilePath); ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath);
Directory.Delete(tempFolder, true); //Cleanup Directory.Delete(tempFolder, true); //Cleanup
@ -224,6 +230,7 @@ public abstract class Connector
catch (HttpRequestException e) catch (HttpRequestException e)
{ {
logger?.WriteLine(this.GetType().ToString(), e.Message); logger?.WriteLine(this.GetType().ToString(), e.Message);
logger?.WriteLine(this.GetType().ToString(), $"Waiting {_rateLimit[requestType] * 2}");
Thread.Sleep(_rateLimit[requestType] * 2); Thread.Sleep(_rateLimit[requestType] * 2);
} }
} }

View File

@ -14,11 +14,11 @@ public class MangaDex : Connector
Manga, Manga,
Feed, Feed,
AtHomeServer, AtHomeServer,
Cover, CoverUrl,
Author Author,
} }
public MangaDex(string downloadLocation, Logger? logger) : base(downloadLocation, 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>() this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
@ -26,7 +26,7 @@ public class MangaDex : Connector
{(byte)RequestType.Manga, 250}, {(byte)RequestType.Manga, 250},
{(byte)RequestType.Feed, 250}, {(byte)RequestType.Feed, 250},
{(byte)RequestType.AtHomeServer, 40}, {(byte)RequestType.AtHomeServer, 40},
{(byte)RequestType.Cover, 250}, {(byte)RequestType.CoverUrl, 250},
{(byte)RequestType.Author, 250} {(byte)RequestType.Author, 250}
}, logger); }, logger);
} }
@ -55,6 +55,7 @@ public class MangaDex : Connector
total = result["total"]!.GetValue<int>(); //Update the total number of Publications total = result["total"]!.GetValue<int>(); //Update the total number of Publications
JsonArray mangaInResult = result["data"]!.AsArray(); //Manga-data-Array JsonArray mangaInResult = result["data"]!.AsArray(); //Manga-data-Array
logger?.WriteLine(this.GetType().ToString(), $"Getting publication data.");
//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)
{ {
@ -98,6 +99,10 @@ public class MangaDex : Connector
authorId = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue<string>() == "author")!["id"]!.GetValue<string>(); authorId = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue<string>() == "author")!["id"]!.GetValue<string>();
} }
string? coverUrl = GetCoverUrl(publicationId, posterId); string? coverUrl = GetCoverUrl(publicationId, posterId);
string? coverCacheName = null;
if (coverUrl is not null)
coverCacheName = SaveImage(coverUrl);
string? author = GetAuthor(authorId); string? author = GetAuthor(authorId);
Dictionary<string, string> linksDict = new(); Dictionary<string, string> linksDict = new();
@ -127,6 +132,7 @@ public class MangaDex : Connector
altTitlesDict, altTitlesDict,
tags.ToArray(), tags.ToArray(),
coverUrl, coverUrl,
coverCacheName,
linksDict, linksDict,
year, year,
originalLanguage, originalLanguage,
@ -137,12 +143,13 @@ public class MangaDex : Connector
} }
} }
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
@ -192,12 +199,13 @@ 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)
{ {
logger?.WriteLine(this.GetType().ToString(), $"Download Chapter {publication.sortName} {chapter.volumeNumber}-{chapter.chapterNumber}"); logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
//Request URLs for Chapter-Images //Request URLs for Chapter-Images
DownloadClient.RequestResult requestResult = DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest($"https://api.mangadex.org/at-home/server/{chapter.url}?forcePort443=false'", (byte)RequestType.AtHomeServer); downloadClient.MakeRequest($"https://api.mangadex.org/at-home/server/{chapter.url}?forcePort443=false'", (byte)RequestType.AtHomeServer);
@ -224,15 +232,16 @@ public class MangaDex : Connector
private string? GetCoverUrl(string publicationId, string? posterId) private string? GetCoverUrl(string publicationId, string? posterId)
{ {
logger?.WriteLine(this.GetType().ToString(), $"Getting CoverUrl for {publicationId}");
if (posterId is null) if (posterId is null)
{ {
logger?.WriteLine(this.GetType().ToString(), $"No posterId"); logger?.WriteLine(this.GetType().ToString(), $"No posterId, aborting");
return null; return null;
} }
//Request information where to download Cover //Request information where to download Cover
DownloadClient.RequestResult requestResult = DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest($"https://api.mangadex.org/cover/{posterId}", (byte)RequestType.Cover); downloadClient.MakeRequest($"https://api.mangadex.org/cover/{posterId}", (byte)RequestType.CoverUrl);
if (requestResult.statusCode != HttpStatusCode.OK) if (requestResult.statusCode != HttpStatusCode.OK)
return null; return null;
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result); JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
@ -242,6 +251,7 @@ public class MangaDex : Connector
string fileName = result["data"]!["attributes"]!["fileName"]!.GetValue<string>(); string fileName = result["data"]!["attributes"]!["fileName"]!.GetValue<string>();
string coverUrl = $"https://uploads.mangadex.org/covers/{publicationId}/{fileName}"; string coverUrl = $"https://uploads.mangadex.org/covers/{publicationId}/{fileName}";
logger?.WriteLine(this.GetType().ToString(), $"Got Cover-Url for {publicationId} -> {coverUrl}");
return coverUrl; return coverUrl;
} }
@ -259,14 +269,16 @@ public class MangaDex : Connector
return null; return null;
string author = result["data"]!["attributes"]!["name"]!.GetValue<string>(); string author = result["data"]!["attributes"]!["name"]!.GetValue<string>();
logger?.WriteLine(this.GetType().ToString(), $"Got author {authorId} -> {author}");
return author; return author;
} }
public override void DownloadCover(Publication publication) public override void CloneCoverFromCache(Publication publication, TrangaSettings settings)
{ {
logger?.WriteLine(this.GetType().ToString(), $"Download cover {publication.sortName}"); logger?.WriteLine(this.GetType().ToString(), $"Cloning cover {publication.sortName}");
//Check if Publication already has a Folder and cover //Check if Publication already has a Folder and cover
string publicationFolder = Path.Join(downloadLocation, publication.folderName); string publicationFolder = Path.Join(downloadLocation, publication.folderName);
if(!Directory.Exists(publicationFolder)) if(!Directory.Exists(publicationFolder))
Directory.CreateDirectory(publicationFolder); Directory.CreateDirectory(publicationFolder);
DirectoryInfo dirInfo = new (publicationFolder); DirectoryInfo dirInfo = new (publicationFolder);
@ -276,20 +288,26 @@ public class MangaDex : Connector
return; return;
} }
if (publication.posterUrl is null || publication.posterUrl!.Contains("http")) string fileInCache = Path.Join(settings.coverImageCache, publication.coverFileNameInCache);
{ string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(fileInCache).Split('.')[^1]}" );
logger?.WriteLine(this.GetType().ToString(), $"No Poster-URL in publication"); logger?.WriteLine(this.GetType().ToString(), $"Cloning cover {fileInCache} -> {newFilePath}");
return; File.Copy(fileInCache, newFilePath, true);
} }
//Get file-extension (jpg, png) private string SaveImage(string url)
string[] split = publication.posterUrl.Split('.'); {
string extension = split[^1]; string[] split = url.Split('/');
string filename = split[^1];
string saveImagePath = Path.Join(imageCachePath, filename);
string outFolderPath = Path.Join(downloadLocation, publication.folderName); if (File.Exists(saveImagePath))
Directory.CreateDirectory(outFolderPath); return filename;
//Download cover-Image DownloadClient.RequestResult coverResult = downloadClient.MakeRequest(url, (byte)RequestType.AtHomeServer);
DownloadImage(publication.posterUrl, Path.Join(downloadLocation, publication.folderName, $"cover.{extension}"), this.downloadClient, (byte)RequestType.AtHomeServer); using MemoryStream ms = new();
coverResult.result.CopyTo(ms);
File.WriteAllBytes(saveImagePath, ms.ToArray());
logger?.WriteLine(this.GetType().ToString(), $"Saving image to {saveImagePath}");
return filename;
} }
} }

View File

@ -1,4 +1,5 @@
using System.Net.Http.Headers; using System.Net;
using System.Net.Http.Headers;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using Logging; using Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -45,9 +46,17 @@ public class Komga
{ {
logger?.WriteLine(this.GetType().ToString(), $"Getting Libraries"); logger?.WriteLine(this.GetType().ToString(), $"Getting Libraries");
Stream data = NetClient.MakeRequest($"{baseUrl}/api/v1/libraries", auth); Stream data = NetClient.MakeRequest($"{baseUrl}/api/v1/libraries", auth);
if (data == Stream.Null)
{
logger?.WriteLine(this.GetType().ToString(), $"No libraries returned");
return Array.Empty<KomgaLibrary>();
}
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data); JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
if (result is null) if (result is null)
{
logger?.WriteLine(this.GetType().ToString(), $"No libraries returned");
return Array.Empty<KomgaLibrary>(); return Array.Empty<KomgaLibrary>();
}
HashSet<KomgaLibrary> ret = new(); HashSet<KomgaLibrary> ret = new();
@ -89,37 +98,51 @@ public class Komga
{ {
public static Stream MakeRequest(string url, string auth) public static Stream MakeRequest(string url, string auth)
{ {
HttpClient client = new(); HttpClientHandler clientHandler = new ();
HttpRequestMessage requestMessage = new HttpRequestMessage clientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) => true;
HttpClient client = new(clientHandler);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", auth);
HttpRequestMessage requestMessage = new ()
{ {
Method = HttpMethod.Get, Method = HttpMethod.Get,
RequestUri = new Uri(url), RequestUri = new Uri(url)
Headers =
{
{ "Accept", "application/json" },
{ "Authorization", new AuthenticationHeaderValue("Basic", auth).ToString() }
}
}; };
HttpResponseMessage response = client.Send(requestMessage); HttpResponseMessage response = client.Send(requestMessage);
Stream resultString = response.IsSuccessStatusCode ? response.Content.ReadAsStream() : Stream.Null; Stream ret;
return resultString; if (response.StatusCode is HttpStatusCode.Unauthorized)
{
ret = MakeRequest(response.RequestMessage!.RequestUri!.AbsoluteUri, auth);
}else
return response.IsSuccessStatusCode ? response.Content.ReadAsStream() : Stream.Null;
return ret;
} }
public static bool MakePost(string url, string auth) public static bool MakePost(string url, string auth)
{ {
HttpClient client = new(); HttpClientHandler clientHandler = new HttpClientHandler();
HttpRequestMessage requestMessage = new HttpRequestMessage clientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) => true;
HttpClient client = new(clientHandler)
{ {
Method = HttpMethod.Post, DefaultRequestHeaders =
RequestUri = new Uri(url),
Headers =
{ {
{ "Accept", "application/json" }, { "Accept", "application/json" },
{ "Authorization", new AuthenticationHeaderValue("Basic", auth).ToString() } { "Authorization", new AuthenticationHeaderValue("Basic", auth).ToString() }
} }
}; };
HttpRequestMessage requestMessage = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri(url)
};
HttpResponseMessage response = client.Send(requestMessage); HttpResponseMessage response = client.Send(requestMessage);
return response.IsSuccessStatusCode; bool ret;
if (response.StatusCode is HttpStatusCode.Unauthorized)
{
ret = MakePost(response.RequestMessage!.RequestUri!.AbsoluteUri, auth);
}else
return response.IsSuccessStatusCode;
return ret;
} }
} }
} }

View File

@ -1,4 +1,5 @@
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Tranga; namespace Tranga;
@ -15,6 +16,7 @@ public readonly struct Publication
public string? description { get; } public string? description { get; }
public string[] tags { get; } public string[] tags { get; }
public string? posterUrl { get; } public string? posterUrl { get; }
public string? coverFileNameInCache { get; }
public Dictionary<string,string> links { get; } public Dictionary<string,string> links { get; }
public int? year { get; } public int? year { get; }
public string? originalLanguage { get; } public string? originalLanguage { get; }
@ -23,20 +25,25 @@ public readonly struct Publication
public string publicationId { get; } public string publicationId { get; }
public string internalId { get; } public string internalId { get; }
public Publication(string sortName, string? author, string? description, Dictionary<string,string> altTitles, string[] tags, string? posterUrl, Dictionary<string,string>? links, int? year, string? originalLanguage, string status, string publicationId) private static readonly Regex LegalCharacters = new Regex(@"([A-z]*[0-9]* *\.*-*,*\]*\[*'*\'*\)*\(*~*!*)*");
public Publication(string sortName, string? author, string? description, Dictionary<string,string> altTitles, string[] tags, string? posterUrl, string? coverFileNameInCache, Dictionary<string,string>? links, int? year, string? originalLanguage, string status, string publicationId)
{ {
this.sortName = sortName; this.sortName = sortName;
this.author = author; this.author = author;
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 ?? new Dictionary<string, string>(); 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.publicationId = publicationId; 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)); string onlyLowerLetters = string.Concat(this.sortName.ToLower().Where(Char.IsLetter));
this.internalId = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{onlyLowerLetters}{this.year}")); this.internalId = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{onlyLowerLetters}{this.year}"));
} }

View File

@ -37,7 +37,7 @@ public static class TaskExecutor
switch (trangaTask.task) switch (trangaTask.task)
{ {
case TrangaTask.Task.DownloadNewChapters: case TrangaTask.Task.DownloadNewChapters:
DownloadNewChapters(connector!, (Publication)trangaTask.publication!, trangaTask.language, ref taskManager._chapterCollection); DownloadNewChapters(connector!, (Publication)trangaTask.publication!, trangaTask.language, ref taskManager._chapterCollection, taskManager.settings);
break; break;
case TrangaTask.Task.UpdateChapters: case TrangaTask.Task.UpdateChapters:
UpdateChapters(connector!, (Publication)trangaTask.publication!, trangaTask.language, ref taskManager._chapterCollection); UpdateChapters(connector!, (Publication)trangaTask.publication!, trangaTask.language, ref taskManager._chapterCollection);
@ -90,7 +90,7 @@ public static class TaskExecutor
/// <param name="publication">Publication to check</param> /// <param name="publication">Publication to check</param>
/// <param name="language">Language to receive chapters for</param> /// <param name="language">Language to receive chapters for</param>
/// <param name="chapterCollection"></param> /// <param name="chapterCollection"></param>
private static void DownloadNewChapters(Connector connector, Publication publication, string language, ref Dictionary<Publication, List<Chapter>> chapterCollection) private static void DownloadNewChapters(Connector connector, Publication publication, string language, ref Dictionary<Publication, List<Chapter>> chapterCollection, TrangaSettings settings)
{ {
//Check if Publication already has a Folder //Check if Publication already has a Folder
string publicationFolder = Path.Join(connector.downloadLocation, publication.folderName); string publicationFolder = Path.Join(connector.downloadLocation, publication.folderName);
@ -98,7 +98,7 @@ public static class TaskExecutor
Directory.CreateDirectory(publicationFolder); Directory.CreateDirectory(publicationFolder);
List<Chapter> newChapters = UpdateChapters(connector, publication, language, ref chapterCollection); List<Chapter> newChapters = UpdateChapters(connector, publication, language, ref chapterCollection);
connector.DownloadCover(publication); connector.CloneCoverFromCache(publication, settings);
string seriesInfoPath = Path.Join(publicationFolder, "series.json"); string seriesInfoPath = Path.Join(publicationFolder, "series.json");
if(!File.Exists(seriesInfoPath)) if(!File.Exists(seriesInfoPath))

View File

@ -21,11 +21,12 @@ public class TaskManager
/// <param name="downloadFolderPath">Local path to save data (Manga) to</param> /// <param name="downloadFolderPath">Local path to save data (Manga) to</param>
/// <param name="workingDirectory">Path to the working directory</param> /// <param name="workingDirectory">Path to the working directory</param>
/// <param name="imageCachePath">Path to the cover-image cache</param>
/// <param name="komgaBaseUrl">The Url of the Komga-instance that you want to update</param> /// <param name="komgaBaseUrl">The Url of the Komga-instance that you want to update</param>
/// <param name="komgaUsername">The Komga username</param> /// <param name="komgaUsername">The Komga username</param>
/// <param name="komgaPassword">The Komga password</param> /// <param name="komgaPassword">The Komga password</param>
/// <param name="logger"></param> /// <param name="logger"></param>
public TaskManager(string downloadFolderPath, string workingDirectory, string? komgaBaseUrl = null, string? komgaUsername = null, string? komgaPassword = null, Logger? logger = null) public TaskManager(string downloadFolderPath, string workingDirectory, string imageCachePath, string? komgaBaseUrl = null, string? komgaUsername = null, string? komgaPassword = null, Logger? logger = null)
{ {
this.logger = logger; this.logger = logger;
_allTasks = new HashSet<TrangaTask>(); _allTasks = new HashSet<TrangaTask>();
@ -37,7 +38,7 @@ public class TaskManager
this.settings = new TrangaSettings(downloadFolderPath, workingDirectory, newKomga); this.settings = new TrangaSettings(downloadFolderPath, workingDirectory, newKomga);
ExportData(); ExportData();
this._connectors = new Connector[]{ new MangaDex(downloadFolderPath, logger) }; this._connectors = new Connector[]{ new MangaDex(downloadFolderPath, imageCachePath, logger) };
foreach(Connector cConnector in this._connectors) foreach(Connector cConnector in this._connectors)
_taskQueue.Add(cConnector, new List<TrangaTask>()); _taskQueue.Add(cConnector, new List<TrangaTask>());
@ -58,7 +59,7 @@ public class TaskManager
public TaskManager(TrangaSettings settings, Logger? logger = null) 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[]{ new MangaDex(settings.downloadLocation, settings.coverImageCache, logger) };
foreach(Connector cConnector in this._connectors) foreach(Connector cConnector in this._connectors)
_taskQueue.Add(cConnector, new List<TrangaTask>()); _taskQueue.Add(cConnector, new List<TrangaTask>());
_allTasks = new HashSet<TrangaTask>(); _allTasks = new HashSet<TrangaTask>();
@ -140,14 +141,11 @@ public class TaskManager
TrangaTask newTask; TrangaTask newTask;
if (task == TrangaTask.Task.UpdateKomgaLibrary) if (task == TrangaTask.Task.UpdateKomgaLibrary)
{ {
newTask = new TrangaTask(task, null, null, reoccurrence, language); newTask = new TrangaTask(task, null, null, reoccurrence);
logger?.WriteLine(this.GetType().ToString(), $"Removing old {task}-Task.");
//Check if same task already exists //Only one UpdateKomgaLibrary Task
// ReSharper disable once SimplifyLinqExpressionUseAll readabilty _allTasks.RemoveWhere(trangaTask => trangaTask.task is TrangaTask.Task.UpdateKomgaLibrary);
if (!_allTasks.Any(trangaTask => trangaTask.task == task)) _allTasks.Add(newTask);
{
_allTasks.Add(newTask);
}
} }
else else
{ {

View File

@ -9,6 +9,7 @@ public class TrangaSettings
[JsonIgnore]public string settingsFilePath => Path.Join(workingDirectory, "settings.json"); [JsonIgnore]public string settingsFilePath => Path.Join(workingDirectory, "settings.json");
[JsonIgnore]public string tasksFilePath => Path.Join(workingDirectory, "tasks.json"); [JsonIgnore]public string tasksFilePath => Path.Join(workingDirectory, "tasks.json");
[JsonIgnore]public string knownPublicationsPath => Path.Join(workingDirectory, "knownPublications.json"); [JsonIgnore]public string knownPublicationsPath => Path.Join(workingDirectory, "knownPublications.json");
[JsonIgnore] public string coverImageCache => Path.Join(workingDirectory, "imageCache");
public Komga? komga { get; set; } public Komga? komga { get; set; }
public TrangaSettings(string downloadLocation, string workingDirectory, Komga? komga) public TrangaSettings(string downloadLocation, string workingDirectory, Komga? komga)

4
Website/Dockerfile Normal file
View File

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

View File

@ -1,4 +1,23 @@
const apiUri = "http://localhost:5177"; 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){ async function GetData(uri){
let request = await fetch(uri, { let request = await fetch(uri, {
@ -47,7 +66,7 @@ async function GetTaskTypes(){
return json; return json;
} }
async function GetRunningTasks(){ async function GetRunningTasks(){
var uri = apiUri + "/Tranga/GetRunningTasks"; var uri = apiUri + "/Tasks/GetRunningTasks";
let json = await GetData(uri); let json = await GetData(uri);
return json; return json;
} }
@ -98,4 +117,10 @@ function DeleteTask(taskType, connectorName, publicationId){
function DequeueTask(taskType, connectorName, publicationId){ function DequeueTask(taskType, connectorName, publicationId){
var uri = apiUri + `/Queue/Dequeue?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}`; var uri = apiUri + `/Queue/Dequeue?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}`;
DeleteData(uri); DeleteData(uri);
}
async function GetQueue(){
var uri = apiUri + "/Queue/GetList";
let json = await GetData(uri);
return json;
} }

BIN
Website/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -6,92 +6,116 @@
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
</head> </head>
<body> <body>
<topbar> <wrapper>
<titlebox> <topbar>
<img src="media/blahaj.png"> <titlebox>
<span>Tranga</span> <img src="media/blahaj.png">
</titlebox> <span>Tranga</span>
<spacer></spacer> </titlebox>
<searchdiv>
<input id="searchbox" placeholder="Filter" type="text">
</searchdiv>
<img id="settingscog" src="media/settings-cogwheel.svg" height="100%" alt="settingscog">
</topbar>
<viewport>
<sidebar>
<background-placeholder></background-placeholder>
<spacer></spacer> <spacer></spacer>
<p style="text-align: center">Made with Blåhaj 🦈</p> <searchdiv>
</sidebar> <input id="searchbox" placeholder="Filter" type="text">
<content> </searchdiv>
<div id="addPublication"> <img id="settingscog" src="media/settings-cogwheel.svg" height="100%" alt="settingscog">
<p>+</p> </topbar>
<viewport>
<content>
<div id="addPublication">
<p>+</p>
</div>
<publication>
<img src="media/cover.jpg">
<publication-information>
<connector-name class="pill">MangaDex</connector-name>
<publication-name>Tensei Pandemic</publication-name>
</publication-information>
</publication>
</content>
<popup id="addTaskPopup">
<blur-background id="blurBackgroundTaskPopup"></blur-background>
<addtask-window>
<window-titlebar>
<p>Add Task</p>
<img id="closePopupImg" src="media/close-x.svg" alt="Close">
</window-titlebar>
<window-content>
<addtask-settings>
<addtask-setting><label for="selectReccurrence">Recurrence</label><input id="selectReccurrence" type="time" value="01:00:00" step="3600"></addtask-setting>
<addtask-setting><label for="connectors">Connector</label>
<select id="connectors">
<option value=""></option>
</select>
</addtask-setting>
<addtask-setting><label for="searchPublicationQuery">Search Title</label><input id="searchPublicationQuery" type="text"></addtask-setting>
<input type="submit" value="Search" onclick="NewSearch();">
</addtask-settings>
<div id="taskSelectOutput"></div>
</window-content>
</addtask-window>
</popup>
<popup id="publicationViewerPopup">
<blur-background id="blurBackgroundPublicationPopup"></blur-background>
<publication-viewer>
<img id="pubviewcover" src="media/cover.jpg" alt="cover">
<publication-information>
<publication-name id="publicationViewerName">Tensei Pandemic</publication-name>
<publication-author id="publicationViewerAuthor">Imamura Hinata</publication-author>
<publication-description id="publicationViewerDescription">Imamura Hinata is a high school boy with a cute appearance.
Since his trauma with the first love, he wanted to be more manly than anybody else. But one day he woke up to something different…
The total opposite of his ideal male body!
Pandemic love comedy!
</publication-description>
<publication-interactions>
<publication-starttask>Start Task ▶️</publication-starttask>
<publication-delete>Delete Task ❌</publication-delete>
<publication-add>Add Task </publication-add>
</publication-interactions>
</publication-information>
</publication-viewer>
</popup>
<popup id="settingsPopup">
<blur-background id="blurBackgroundSettingsPopup"></blur-background>
<settings>
<span style="font-weight: bold; text-align: center; font-size: 16pt;">Settings</span>
<div>
<p class="title">Download Location:</p>
<span id="downloadLocation"></span>
</div>
<div>
<p class="title">API-URI</p>
<label for="settingApiUri"></label><input placeholder="https://" type="text" id="settingApiUri">
</div>
<komga-settings>
<span class="title">Komga</span>
<div>Configured: <span id="komgaConfigured">✅❌</span></div>
<label for="komgaUrl"></label><input placeholder="URL" id="komgaUrl" type="text">
<label for="komgaUsername"></label><input placeholder="Username" id="komgaUsername" type="text">
<label for="komgaPassword"></label><input placeholder="Password" id="komgaPassword" type="password">
<label for="komgaUpdateTime" style="margin-right: 5px;">Update Time</label><input id="komgaUpdateTime" type="time" value="00:01:00" step="10">
<input type="submit" value="Update" onclick="UpdateKomgaSettings()">
</komga-settings>
</settings>
</popup>
</viewport>
<footer>
<div>
<img src="media/running.svg" alt="running"><div id="tasksRunningTag">0</div>
</div> </div>
<publication> <div>
<img src="media/cover.jpg"> <img src="media/queue.svg" alt="queue"><div id="tasksQueuedTag">0</div>
<publication-information> </div>
<connector-name class="pill">MangaDex</connector-name> <div>
<publication-name>Tensei Pandemic</publication-name> <img src="media/tasks.svg" alt="queue"><div id="totalTasksTag">0</div>
</publication-information> </div>
</publication> <p id="madeWith">Made with Blåhaj 🦈</p>
</content> </footer>
</wrapper>
<popup id="addTaskPopup"> <footer-tag-popup>
<blur-background id="blurBackgroundTaskPopup"></blur-background> <footer-tag-content>
<addtask-window> <footer-tag-task-name>Test</footer-tag-task-name>
<window-titlebar> </footer-tag-content>
<p>Add Task</p> </footer-tag-popup>
<img id="closePopupImg" src="media/close-x.svg" alt="Close">
</window-titlebar>
<window-content>
<addtask-settings>
<addtask-setting><label for="selectReccurrence">Recurrence</label><input id="selectReccurrence" type="time" value="01:00" step="3600"></addtask-setting>
<addtask-setting><label for="connectors">Connector</label>
<select id="connectors">
<option value=""></option>
</select>
</addtask-setting>
<addtask-setting><label for="searchPublicationQuery">Search Title</label><input id="searchPublicationQuery" type="text"></addtask-setting>
<input type="submit" value="Search" onclick="NewSearch();">
</addtask-settings>
<div id="taskSelectOutput"></div>
</window-content>
</addtask-window>
</popup>
<popup id="publicationViewerPopup">
<blur-background id="blurBackgroundPublicationPopup"></blur-background>
<publication-viewer>
<img id="pubviewcover" src="media/cover.jpg" alt="cover">
<publication-information>
<publication-name id="publicationViewerName">Tensei Pandemic</publication-name>
<publication-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-delete>Delete Task ❌</publication-delete>
<publication-add>Add Task </publication-add>
</publication-information>
</publication-viewer>
</popup>
</viewport>
<settingstab id="settingstab">
<span class="title">Download Location:</span>
<span id="downloadLocation"></span>
<komga-settings>
<span class="title">Komga</span>
<div>Configured: <span id="komgaConfigured">✅❌</span></div>
<label for="komgaUrl"></label><input placeholder="URL" id="komgaUrl" type="text">
<label for="komgaUsername"></label><input placeholder="Username" id="komgaUsername" type="text">
<label for="komgaPassword"></label><input placeholder="Password" id="komgaPassword" type="password">
<div><label for="komgaUpdateTime" style="margin-right: 5px;">Update Time</label><input id="komgaUpdateTime" type="time" value="00:01"></div>
<input type="submit" value="Update" onclick="UpdateSettingsClick()">
</komga-settings>
</settingstab>
<script src="apiConnector.js"></script> <script src="apiConnector.js"></script>
<script src="interaction.js"></script> <script src="interaction.js"></script>

View File

@ -1,31 +1,11 @@
const slideInRight = [ let publications = [];
{ right: "-20rem" },
{ right: "0" }
];
const slideInRightTiming = {
duration: 200,
iterations: 1,
fill: "forwards",
easing: "ease-out"
}
const slideOutRightTiming = {
direction: "reverse",
duration: 200,
iterations: 1,
fill: "forwards",
easing: "ease-in"
}
let publications = [];
let tasks = []; let tasks = [];
let toEditId; let toEditId;
const searchPublicationQuery = document.querySelector("#searchPublicationQuery"); const searchPublicationQuery = document.querySelector("#searchPublicationQuery");
const selectPublication = document.querySelector("#taskSelectOutput"); const selectPublication = document.querySelector("#taskSelectOutput");
const connectorSelect = document.querySelector("#connectors"); const connectorSelect = document.querySelector("#connectors");
const settingsTab = document.querySelector("#settingstab"); const settingsPopup = document.querySelector("#settingsPopup");
const settingsCog = document.querySelector("#settingscog"); const settingsCog = document.querySelector("#settingscog");
const selectRecurrence = document.querySelector("#selectReccurrence"); const selectRecurrence = document.querySelector("#selectReccurrence");
const tasksContent = document.querySelector("content"); const tasksContent = document.querySelector("content");
@ -38,6 +18,7 @@ const publicationViewerAuthor = document.querySelector("#publicationViewerAuthor
const pubviewcover = document.querySelector("#pubviewcover"); const pubviewcover = document.querySelector("#pubviewcover");
const publicationDelete = document.querySelector("publication-delete"); const publicationDelete = document.querySelector("publication-delete");
const publicationAdd = document.querySelector("publication-add"); const publicationAdd = document.querySelector("publication-add");
const publicationTaskStart = document.querySelector("publication-starttask");
const closetaskpopup = document.querySelector("#closePopupImg"); const closetaskpopup = document.querySelector("#closePopupImg");
const settingDownloadLocation = document.querySelector("#downloadLocation"); const settingDownloadLocation = document.querySelector("#downloadLocation");
const settingKomgaUrl = document.querySelector("#komgaUrl"); const settingKomgaUrl = document.querySelector("#komgaUrl");
@ -45,14 +26,39 @@ const settingKomgaUser = document.querySelector("#komgaUsername");
const settingKomgaPass = document.querySelector("#komgaPassword"); const settingKomgaPass = document.querySelector("#komgaPassword");
const settingKomgaTime = document.querySelector("#komgaUpdateTime"); const settingKomgaTime = document.querySelector("#komgaUpdateTime");
const settingKomgaConfigured = document.querySelector("#komgaConfigured"); const settingKomgaConfigured = document.querySelector("#komgaConfigured");
const settingApiUri = document.querySelector("#settingApiUri");
const tagTasksRunning = document.querySelector("#tasksRunningTag");
const tagTasksQueued = document.querySelector("#tasksQueuedTag");
const tagTasksTotal = document.querySelector("#totalTasksTag");
const tagTaskPopup = document.querySelector("footer-tag-popup");
const tagTasksPopupContent = document.querySelector("footer-tag-content");
settingsCog.addEventListener("click", () => OpenSettings()); settingsCog.addEventListener("click", () => OpenSettings());
document.querySelector("#blurBackgroundSettingsPopup").addEventListener("click", () => HideSettings());
closetaskpopup.addEventListener("click", () => HideAddTaskPopup()); closetaskpopup.addEventListener("click", () => HideAddTaskPopup());
document.querySelector("#blurBackgroundTaskPopup").addEventListener("click", () => HideAddTaskPopup()); document.querySelector("#blurBackgroundTaskPopup").addEventListener("click", () => HideAddTaskPopup());
document.querySelector("#blurBackgroundPublicationPopup").addEventListener("click", () => HidePublicationPopup()); document.querySelector("#blurBackgroundPublicationPopup").addEventListener("click", () => HidePublicationPopup());
publicationDelete.addEventListener("click", () => DeleteTaskClick()); publicationDelete.addEventListener("click", () => DeleteTaskClick());
publicationAdd.addEventListener("click", () => AddTaskClick()); publicationAdd.addEventListener("click", () => AddTaskClick());
publicationTaskStart.addEventListener("click", () => StartTaskClick());
settingApiUri.addEventListener("keypress", (event) => {
if(event.key === "Enter"){
apiUri = settingApiUri.value;
setTimeout(() => GetSettingsClick(), 100);
document.cookie = `apiUri=${apiUri};`;
}
});
searchPublicationQuery.addEventListener("keypress", (event) => {
if(event.key === "Enter"){
NewSearch();
}
});
tagTasksRunning.addEventListener("mouseover", (event) => ShowRunningTasks(event));
tagTasksRunning.addEventListener("mouseout", () => CloseTasksPopup());
tagTasksQueued.addEventListener("mouseover", (event) => ShowQueuedTasks(event));
tagTasksQueued.addEventListener("mouseout", () => CloseTasksPopup());
tagTasksTotal.addEventListener("mouseover", (event) => ShowAllTasks(event));
tagTasksTotal.addEventListener("mouseout", () => CloseTasksPopup());
let availableConnectors; let availableConnectors;
GetAvailableControllers() GetAvailableControllers()
@ -66,11 +72,6 @@ GetAvailableControllers()
}) })
); );
searchPublicationQuery.addEventListener("keypress", (event) => {
if(event.key === "Enter"){
NewSearch();
}
});
function NewSearch(){ function NewSearch(){
//Disable inputs //Disable inputs
@ -103,7 +104,7 @@ function CreatePublication(publication, connector){
var publicationElement = document.createElement('publication'); var publicationElement = document.createElement('publication');
publicationElement.setAttribute("id", publication.internalId); publicationElement.setAttribute("id", publication.internalId);
var img = document.createElement('img'); var img = document.createElement('img');
img.src = publication.posterUrl; img.src = `imageCache/${publication.coverFileNameInCache}`;
publicationElement.appendChild(img); publicationElement.appendChild(img);
var info = document.createElement('publication-information'); var info = document.createElement('publication-information');
var connectorName = document.createElement('connector-name'); var connectorName = document.createElement('connector-name');
@ -131,13 +132,10 @@ function AddTaskClick(){
HidePublicationPopup(); HidePublicationPopup();
} }
let slideIn = true; function StartTaskClick(){
function slide() { var toEditTask = tasks.filter(task => task.publication.internalId == toEditId)[0];
if (slideIn) StartTask("DownloadNewChapters", toEditTask.connectorName, toEditId);
settingsTab.animate(slideInRight, slideInRightTiming); HidePublicationPopup();
else
settingsTab.animate(slideInRight, slideOutRightTiming);
slideIn = !slideIn;
} }
function ResetContent(){ function ResetContent(){
@ -154,30 +152,39 @@ function ResetContent(){
tasksContent.appendChild(add); tasksContent.appendChild(add);
} }
function ShowPublicationViewerWindow(publicationId, event, add){ function ShowPublicationViewerWindow(publicationId, event, add){
//Show popup
publicationViewerPopup.style.display = "block";
//Set position to mouse-position //Set position to mouse-position
publicationViewerWindow.style.top = `${event.clientY - 60}px`; if(event.clientY < window.innerHeight - publicationViewerWindow.offsetHeight)
publicationViewerWindow.style.left = `${event.clientX}px`; 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 //Edit information inside the window
var publication = publications.filter(pub => pub.internalId === publicationId)[0]; var publication = publications.filter(pub => pub.internalId === publicationId)[0];
publicationViewerName.innerText = publication.sortName; publicationViewerName.innerText = publication.sortName;
publicationViewerDescription.innerText = publication.description; publicationViewerDescription.innerText = publication.description;
publicationViewerAuthor.innerText = publication.author; publicationViewerAuthor.innerText = publication.author;
pubviewcover.src = publication.posterUrl; pubviewcover.src = `imageCache/${publication.coverFileNameInCache}`;
toEditId = publicationId; toEditId = publicationId;
//Check what action should be listed //Check what action should be listed
if(add){ if(add){
publicationAdd.style.display = "block"; publicationAdd.style.display = "initial";
publicationDelete.style.display = "none"; publicationDelete.style.display = "none";
publicationTaskStart.style.display = "none";
} }
else{ else{
publicationAdd.style.display = "none"; publicationAdd.style.display = "none";
publicationDelete.style.display = "block"; publicationDelete.style.display = "initial";
publicationTaskStart.style.display = "initial";
} }
//Show popup
publicationViewerPopup.style.display = "block";
} }
function HidePublicationPopup(){ function HidePublicationPopup(){
@ -206,14 +213,21 @@ const fadeInTiming = {
function OpenSettings(){ function OpenSettings(){
GetSettingsClick(); GetSettingsClick();
slide(); settingsPopup.style.display = "flex";
}
function HideSettings(){
settingsPopup.style.display = "none";
} }
function GetSettingsClick(){ function GetSettingsClick(){
settingApiUri.value = "";
settingKomgaUrl.value = ""; settingKomgaUrl.value = "";
settingKomgaUser.value = ""; settingKomgaUser.value = "";
settingKomgaPass.value = ""; settingKomgaPass.value = "";
settingApiUri.placeholder = apiUri;
GetSettings().then(json => { GetSettings().then(json => {
settingDownloadLocation.innerText = json.downloadLocation; settingDownloadLocation.innerText = json.downloadLocation;
if(json.komga != null) if(json.komga != null)
@ -221,6 +235,7 @@ function GetSettingsClick(){
}); });
GetKomgaTask().then(json => { GetKomgaTask().then(json => {
settingKomgaTime.value = json[0].reoccurrence;
if(json.length > 0) if(json.length > 0)
settingKomgaConfigured.innerText = "✅"; settingKomgaConfigured.innerText = "✅";
else else
@ -228,18 +243,80 @@ function GetSettingsClick(){
}); });
} }
function UpdateSettingsClick(){ function UpdateKomgaSettings(){
var auth = utf8_to_b64(`${settingKomgaUser.value}:${settingKomgaPass.value}`); if(settingKomgaUser.value != "" && settingKomgaPass != ""){
console.log(auth); var auth = utf8_to_b64(`${settingKomgaUser.value}:${settingKomgaPass.value}`);
UpdateSettings("", settingKomgaUrl.value, auth); console.log(auth);
if(settingKomgaUrl.value != "")
UpdateSettings("", settingKomgaUrl.value, auth);
else
UpdateSettings("", settingKomgaUrl.placeholder, auth);
}
CreateTask("UpdateKomgaLibrary", settingKomgaTime.value, "","",""); CreateTask("UpdateKomgaLibrary", settingKomgaTime.value, "","","");
setTimeout(() => GetSettingsClick(), 500); setTimeout(() => GetSettingsClick(), 100);
} }
function utf8_to_b64( str ) { function utf8_to_b64( str ) {
return window.btoa(unescape(encodeURIComponent( str ))); return window.btoa(unescape(encodeURIComponent( str )));
} }
function ShowRunningTasks(event){
GetRunningTasks()
.then(json => {
tagTasksPopupContent.replaceChildren();
json.forEach(task => {
console.log(task);
if(task.publication != null){
var taskname = document.createElement("footer-tag-task-name");
taskname.innerText = task.publication.sortName;
tagTasksPopupContent.appendChild(taskname);
}
});
if(tagTasksPopupContent.children.length > 0){
tagTaskPopup.style.display = "block";
tagTaskPopup.style.left = `${tagTasksRunning.offsetLeft - 20}px`;
}
});
}
function ShowQueuedTasks(event){
GetQueue()
.then(json => {
tagTasksPopupContent.replaceChildren();
json.forEach(task => {
var taskname = document.createElement("footer-tag-task-name");
taskname.innerText = task.publication.sortName;
tagTasksPopupContent.appendChild(taskname);
});
if(json.length > 0){
tagTaskPopup.style.display = "block";
tagTaskPopup.style.left = `${tagTasksQueued.offsetLeft- 20}px`;
}
});
}
function ShowAllTasks(event){
GetDownloadTasks()
.then(json => {
tagTasksPopupContent.replaceChildren();
json.forEach(task => {
var taskname = document.createElement("footer-tag-task-name");
taskname.innerText = task.publication.sortName;
tagTasksPopupContent.appendChild(taskname);
});
if(json.length > 0){
tagTaskPopup.style.display = "block";
tagTaskPopup.style.left = `${tagTasksTotal.offsetLeft - 20}px`;
}
});
}
function CloseTasksPopup(){
tagTaskPopup.style.display = "none";
}
//Resets the tasks shown //Resets the tasks shown
ResetContent(); ResetContent();
//Get Tasks and show them //Get Tasks and show them
@ -251,6 +328,21 @@ GetDownloadTasks()
tasks.push(task); tasks.push(task);
})); }));
GetRunningTasks()
.then(json => {
tagTasksRunning.innerText = json.length;
});
GetDownloadTasks()
.then(json => {
tagTasksTotal.innerText = json.length;
});
GetQueue()
.then(json => {
tagTasksQueued.innerText = json.length;
})
setInterval(() => { setInterval(() => {
//Tasks from API //Tasks from API
var cTasks = []; var cTasks = [];
@ -272,6 +364,20 @@ setInterval(() => {
} }
} }
); );
GetRunningTasks()
.then(json => {
tagTasksRunning.innerText = json.length;
});
GetDownloadTasks()
.then(json => {
tagTasksTotal.innerText = json.length;
});
GetQueue()
.then(json => {
tagTasksQueued.innerText = json.length;
})
}, 1000); }, 1000);

7
Website/media/queue.svg Normal file
View 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
View 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

10
Website/media/tasks.svg Normal file
View 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

View File

@ -1,5 +1,5 @@
:root{ :root{
--background-color: #eee; --background-color: #030304;
--second-background-color: #fff; --second-background-color: #fff;
--primary-color: #f5a9b8; --primary-color: #f5a9b8;
--secondary-color: #5bcefa; --secondary-color: #5bcefa;
@ -11,15 +11,19 @@
body{ body{
padding: 0; padding: 0;
margin: 0; margin: 0;
display: flex;
flex-flow: column;
flex-wrap: nowrap;
height: 100vh; height: 100vh;
background-color: var(--background-color); background-color: var(--background-color);
font-family: "Inter", sans-serif; font-family: "Inter", sans-serif;
overflow-x: hidden; overflow-x: hidden;
} }
wrapper {
display: flex;
flex-flow: column;
flex-wrap: nowrap;
height: 100vh;
}
background-placeholder{ background-placeholder{
background-color: var(--second-background-color); background-color: var(--second-background-color);
opacity: 1; opacity: 1;
@ -35,6 +39,8 @@ topbar {
align-items: center; align-items: center;
height: var(--topbar-height); height: var(--topbar-height);
background-color: var(--secondary-color); background-color: var(--secondary-color);
z-index: 100;
box-shadow: 0 0 20px black;
} }
titlebox { titlebox {
@ -70,9 +76,9 @@ searchdiv{
} }
#searchbox { #searchbox {
padding: 6px 8px; padding: 3px 10px;
border: 0; border: 0;
border-radius: 3px; border-radius: 4px;
font-size: 14pt; font-size: 14pt;
width: 250px; width: 250px;
} }
@ -80,7 +86,7 @@ searchdiv{
#settingscog { #settingscog {
cursor: pointer; cursor: pointer;
margin: 0px 30px; margin: 0px 30px;
height: calc(100% - 40px); height: 50%;
filter: invert(100%) sepia(0%) saturate(7465%) hue-rotate(115deg) brightness(116%) contrast(101%); filter: invert(100%) sepia(0%) saturate(7465%) hue-rotate(115deg) brightness(116%) contrast(101%);
} }
@ -90,22 +96,45 @@ viewport {
flex-flow: row; flex-flow: row;
flex-wrap: nowrap; flex-wrap: nowrap;
flex-grow: 1; flex-grow: 1;
height: 100%;
overflow-y: scroll;
} }
sidebar{ footer {
position: relative;
width: 20rem;
margin-bottom: 20px;
border-radius: 0 0 5px 0;
display: flex; display: flex;
flex-direction: column; 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;
}
footer > div > *{
height: 40%;
margin: 0 5px;
}
#madeWith {
flex-grow: 1;
text-align: right;
margin-right: 20px;
} }
content { content {
position: relative; position: relative;
flex-grow: 1; flex-grow: 1;
margin: 0 10px 10px 10px;
border-radius: 5px; border-radius: 5px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -114,28 +143,39 @@ content {
align-content: start; align-content: start;
} }
settingstab{ settings {
position: absolute; width: 50%;
right: -20rem; background-color: var(--accent-color);
bottom: 0;
background-color: rgba(0,0,0,0.5);
width: 20rem;
height: calc(100% - var(--topbar-height) - 40px);
margin-bottom: 10px;
border-radius: 5px 0 0 5px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex-wrap: nowrap; z-index: 10;
position: absolute;
left: 25%;
top: 25%;
border-radius: 5px;
padding: 10px 0;
} }
settingstab > * { #settingsPopup{
margin: 0 20px; z-index: 10;
} }
settingstab .title { settings > * {
font-size: 14pt; margin: 0 20%;
}
settings input {
margin: 3px 0;
padding: 3px;
border-radius: 3px;
border: 1px solid rgba(0,0,0,0.2);
width: 100%;
}
settings .title {
font-weight: bolder; font-weight: bolder;
margin-top: 20px; font-size: 14pt;
margin: 15px 0 2px 0;
} }
komga-settings { komga-settings {
@ -145,16 +185,6 @@ komga-settings {
flex-wrap: nowrap; flex-wrap: nowrap;
} }
komga-settings > * {
margin: 2px 0;
}
komga-settings input {
padding: 3px;
border-radius: 3px;
border: 0;
}
#addPublication { #addPublication {
cursor: pointer; cursor: pointer;
background-color: var(--secondary-color); background-color: var(--secondary-color);
@ -182,7 +212,8 @@ komga-settings input {
font-size: 12pt; font-size: 12pt;
border-radius: 9pt; border-radius: 9pt;
background-color: var(--primary-color); background-color: var(--primary-color);
padding: 2pt 20px; padding: 2pt 17px;
color: black;
} }
publication{ publication{
@ -202,7 +233,7 @@ publication::after{
left: 0; top: 0; left: 0; top: 0;
border-radius: 5px; border-radius: 5px;
width: 100%; height: 100%; width: 100%; height: 100%;
background: linear-gradient(rgba(0, 0, 0, 0.5),rgba(0, 0, 0, 0.1)); background: linear-gradient(rgba(0,0,0,0.8), rgba(0, 0, 0, 0.7),rgba(0, 0, 0, 0.2));
} }
publication-information { publication-information {
@ -233,7 +264,9 @@ publication img {
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover;
z-index: 0; z-index: 0;
border-radius: 5px;
} }
popup{ popup{
@ -242,7 +275,7 @@ popup{
min-height: 100%; min-height: 100%;
top: 0; top: 0;
left: 0; left: 0;
position: absolute; position: fixed;
z-index: 2; z-index: 2;
} }
@ -267,7 +300,6 @@ addtask-window {
max-height: 80%; max-height: 80%;
padding: 0; padding: 0;
background-color: var(--accent-color); background-color: var(--accent-color);
z-index: 5;
border-radius: 5px; border-radius: 5px;
} }
@ -338,12 +370,12 @@ addtask-settings addtask-setting{
} }
#publicationViewerPopup{ #publicationViewerPopup{
z-index: 6; z-index: 5;
} }
publication-viewer{ publication-viewer{
display: block; display: block;
width: 500px; width: 450px;
height: 300px; height: 300px;
position: absolute; position: absolute;
top: 200px; top: 200px;
@ -351,9 +383,6 @@ publication-viewer{
background-color: var(--accent-color); background-color: var(--accent-color);
border-radius: 5px; border-radius: 5px;
overflow: hidden; overflow: hidden;
}
publication-viewer{
padding: 30px; padding: 30px;
} }
@ -371,8 +400,9 @@ publication-viewer img {
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
min-height: 100%; height: 100%;
max-width: 100%; width: 100%;
object-fit: cover;
border-radius: 5px; border-radius: 5px;
z-index: 0; z-index: 0;
} }
@ -402,20 +432,69 @@ publication-viewer publication-information publication-description {
overflow-x: scroll; overflow-x: scroll;
} }
publication-viewer publication-information publication-delete { publication-viewer publication-information publication-interactions {
display: flex;
flex-direction: row;
position: absolute; position: absolute;
bottom: 0px; justify-content: end;
right: 0px; align-items: start;
color: red; bottom: 0;
margin: 20px; left: 0;
width: 100%;
}
publication-viewer publication-information publication-interactions > * {
margin: 0 10px 10px 10px;
font-size: 16pt; font-size: 16pt;
} }
publication-viewer publication-information publication-add { publication-viewer publication-information publication-interactions publication-starttask {
position: absolute; color: var(--secondary-color);
bottom: 0px; }
right: 0px;
publication-viewer publication-information publication-interactions publication-delete {
color: red;
}
publication-viewer publication-information publication-interactions publication-add {
color: limegreen; color: limegreen;
margin: 20px; }
font-size: 16pt;
footer-tag-popup {
display: none;
padding: 2px 4px;
position: fixed;
bottom: 58px;
left: 20px;
background-color: var(--second-background-color);
z-index: 8;
border-radius: 5px;
max-height: 400px;
}
footer-tag-content{
position: relative;
max-height: 400px;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
overflow-y: scroll;
}
footer-tag-content > * {
margin: 2px 0;
}
footer-tag-popup::before{
content: "";
width: 0;
height: 0;
position: absolute;
border-right: 10px solid var(--second-background-color);
border-left: 10px solid transparent;
border-top: 10px solid var(--second-background-color);
border-bottom: 10px solid transparent;
left: 0px;
bottom: -17px;
border-radius: 0 0 0 5px;
} }

19
docker-compose.yaml Normal file
View File

@ -0,0 +1,19 @@
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

BIN
screenshots/addtask.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
screenshots/overview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
screenshots/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB