mirror of
https://github.com/C9Glax/tranga.git
synced 2025-06-14 15:27:53 +02:00
Compare commits
36 Commits
Author | SHA1 | Date | |
---|---|---|---|
1cd819b21d | |||
27afedc1b4 | |||
fac0a3f7eb | |||
03ca480fe8 | |||
c2915468a5 | |||
8805c53cb8 | |||
adbbe3f6cc | |||
14b694d3be | |||
72ce75c6e0 | |||
8381951168 | |||
b24032d124 | |||
8bc23f7c69 | |||
48b7371a18 | |||
61ecefb615 | |||
8ff65bf400 | |||
932057cca0 | |||
67d06cd887 | |||
cbb012a659 | |||
e4f33bcca9 | |||
fbba7c45b9 | |||
d9b6062767 | |||
d477cd1ccd | |||
f892db7dda | |||
16c1b5c506 | |||
d5ecc1c37d | |||
1b9ebd096b | |||
8619630269 | |||
2bc92556e3 | |||
f1ab823e7f | |||
8261d02cc7 | |||
8d3b8be95c | |||
60519910de | |||
0940afe41f | |||
3dc376c19f | |||
3e56ef842b | |||
5a44e3b8b9 |
@ -54,6 +54,7 @@
|
|||||||
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/)
|
||||||
|
|
||||||
and automatically start updates in [Komga](https://komga.org/) to import them.
|
and automatically start updates in [Komga](https://komga.org/) to import them.
|
||||||
|
|
||||||
|
@ -12,26 +12,17 @@
|
|||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"applicationUrl": "http://localhost:5177",
|
"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:7036;http://localhost:5177",
|
"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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,11 +28,7 @@ public static class Tranga_Cli
|
|||||||
Logger logger = new(new[] { Logger.LoggerType.FileLogger }, null, null, logFilePath);
|
Logger logger = new(new[] { Logger.LoggerType.FileLogger }, null, null, logFilePath);
|
||||||
|
|
||||||
logger.WriteLine("Tranga_CLI", "Loading Taskmanager.");
|
logger.WriteLine("Tranga_CLI", "Loading Taskmanager.");
|
||||||
TrangaSettings settings;
|
TrangaSettings settings = File.Exists(settingsFilePath) ? TrangaSettings.LoadSettings(settingsFilePath) : new TrangaSettings(Directory.GetCurrentDirectory(), applicationFolderPath, null);
|
||||||
if (File.Exists(settingsFilePath))
|
|
||||||
settings = TrangaSettings.LoadSettings(settingsFilePath);
|
|
||||||
else
|
|
||||||
settings = new TrangaSettings(Directory.GetCurrentDirectory(), applicationFolderPath, null);
|
|
||||||
|
|
||||||
|
|
||||||
logger.WriteLine("Tranga_CLI", "User Input");
|
logger.WriteLine("Tranga_CLI", "User Input");
|
||||||
@ -85,7 +81,7 @@ public static class Tranga_Cli
|
|||||||
{
|
{
|
||||||
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;
|
||||||
@ -106,7 +102,7 @@ public static class Tranga_Cli
|
|||||||
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;
|
||||||
@ -159,7 +155,7 @@ public static class Tranga_Cli
|
|||||||
Console.ReadKey();
|
Console.ReadKey();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
PrintMenu(taskManager, taskManager.settings.downloadLocation, logger);
|
PrintMenu(taskManager, taskManager.settings.downloadLocation);
|
||||||
}
|
}
|
||||||
Thread.Sleep(200);
|
Thread.Sleep(200);
|
||||||
}
|
}
|
||||||
@ -179,7 +175,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);
|
||||||
@ -264,17 +260,17 @@ public static class Tranga_Cli
|
|||||||
Console.Clear();
|
Console.Clear();
|
||||||
logger.WriteLine("Tranga_CLI", "Menu: Add Manga Download to queue");
|
logger.WriteLine("Tranga_CLI", "Menu: Add Manga Download to queue");
|
||||||
|
|
||||||
Connector? connector = SelectConnector(taskManager.settings.downloadLocation, taskManager.GetAvailableConnectors().Values.ToArray(), logger);
|
Connector? connector = SelectConnector(taskManager.GetAvailableConnectors().Values.ToArray(), logger);
|
||||||
if (connector is null)
|
if (connector is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Publication? publication = SelectPublication(taskManager, connector!, logger);
|
Publication? publication = SelectPublication(taskManager, connector, logger);
|
||||||
if (publication is null)
|
if (publication is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
TimeSpan reoccurrence = SelectReoccurrence(logger);
|
TimeSpan reoccurrence = SelectReoccurrence(logger);
|
||||||
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
|
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
|
||||||
TrangaTask newTask = taskManager.AddTask(TrangaTask.Task.DownloadNewChapters, connector?.name, publication, reoccurrence, "en");
|
TrangaTask newTask = taskManager.AddTask(TrangaTask.Task.DownloadNewChapters, connector.name, publication, reoccurrence, "en");
|
||||||
Console.WriteLine(newTask);
|
Console.WriteLine(newTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -327,24 +323,24 @@ public static class Tranga_Cli
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CreateTask(TaskManager taskManager, TrangaSettings settings, Logger logger)
|
private static void CreateTask(TaskManager taskManager, Logger logger)
|
||||||
{
|
{
|
||||||
logger.WriteLine("Tranga_CLI", "Menu: Creating Task");
|
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.UpdateKomgaLibrary)
|
||||||
{
|
{
|
||||||
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.UpdateKomgaLibrary)
|
||||||
{
|
{
|
||||||
publication = SelectPublication(taskManager, connector!, logger);
|
publication = SelectPublication(taskManager, connector!, logger);
|
||||||
if (publication is null)
|
if (publication is null)
|
||||||
@ -431,7 +427,7 @@ 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 Connector? SelectConnector(Connector[] connectors, Logger logger)
|
||||||
{
|
{
|
||||||
logger.WriteLine("Tranga_CLI", "Menu: Select Connector");
|
logger.WriteLine("Tranga_CLI", "Menu: Select Connector");
|
||||||
Console.Clear();
|
Console.Clear();
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=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/=Taskmanager/@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>
|
@ -14,9 +14,9 @@ public abstract class Connector
|
|||||||
internal string downloadLocation { get; } //Location of local files
|
internal string downloadLocation { get; } //Location of local files
|
||||||
protected DownloadClient downloadClient { get; init; }
|
protected DownloadClient downloadClient { get; init; }
|
||||||
|
|
||||||
protected Logger? logger;
|
protected readonly Logger? logger;
|
||||||
|
|
||||||
protected string imageCachePath;
|
protected readonly string imageCachePath;
|
||||||
|
|
||||||
protected Connector(string downloadLocation, string imageCachePath, Logger? logger)
|
protected Connector(string downloadLocation, string imageCachePath, Logger? logger)
|
||||||
{
|
{
|
||||||
@ -27,6 +27,8 @@ public abstract class Connector
|
|||||||
//RequestTypes for RateLimits
|
//RequestTypes for RateLimits
|
||||||
}, logger);
|
}, logger);
|
||||||
this.imageCachePath = imageCachePath;
|
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)
|
||||||
@ -50,34 +52,35 @@ public abstract class Connector
|
|||||||
|
|
||||||
/// <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);
|
public abstract void DownloadChapter(Publication publication, Chapter chapter);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves the Cover from the Website
|
/// Copies the already downloaded cover from cache to downloadLocation
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="publication">Publication to retrieve Cover for</param>
|
/// <param name="publication">Publication to retrieve Cover for</param>
|
||||||
/// <param name="settings">TrangaSettings</param>
|
/// <param name="settings">TrangaSettings</param>
|
||||||
public abstract void CloneCoverFromCache(Publication publication, TrangaSettings settings);
|
public void CopyCoverFromCacheToDownloadLocation(Publication publication, TrangaSettings settings)
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Saves the series-info to series.json in the Publication Folder
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="publication">Publication to save series.json for</param>
|
|
||||||
public void SaveSeriesInfo(Publication publication)
|
|
||||||
{
|
{
|
||||||
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 = Path.Join(downloadLocation, publication.folderName);
|
||||||
if(!Directory.Exists(publicationFolder))
|
if(!Directory.Exists(publicationFolder))
|
||||||
Directory.CreateDirectory(publicationFolder);
|
Directory.CreateDirectory(publicationFolder);
|
||||||
|
DirectoryInfo dirInfo = new (publicationFolder);
|
||||||
string seriesInfoPath = Path.Join(publicationFolder, "series.json");
|
if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover.")))
|
||||||
if(!File.Exists(seriesInfoPath))
|
{
|
||||||
File.WriteAllText(seriesInfoPath,publication.GetSeriesInfoJson());
|
logger?.WriteLine(this.GetType().ToString(), $"Cover exists {publication.sortName}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string fileInCache = Path.Join(settings.coverImageCache, publication.coverFileNameInCache);
|
||||||
|
string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(fileInCache).Split('.')[^1]}" );
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Cloning cover {fileInCache} -> {newFilePath}");
|
||||||
|
File.Copy(fileInCache, newFilePath, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -85,7 +88,7 @@ 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} {publication.internalId} {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",
|
||||||
@ -103,16 +106,16 @@ 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));
|
return File.Exists(GetArchiveFilePath(publication, chapter));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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}.cbz");
|
return Path.Join(downloadLocation, publication.folderName, $"{chapter.fileName}.cbz");
|
||||||
}
|
}
|
||||||
@ -122,11 +125,10 @@ public abstract class Connector
|
|||||||
/// </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>
|
||||||
/// <param name="requestType">Requesttype for ratelimit</param>
|
private void DownloadImage(string imageUrl, string fullPath, byte requestType, string? referrer = null)
|
||||||
protected static void DownloadImage(string imageUrl, string fullPath, DownloadClient downloadClient, byte requestType)
|
|
||||||
{
|
{
|
||||||
DownloadClient.RequestResult requestResult = downloadClient.MakeRequest(imageUrl, requestType);
|
DownloadClient.RequestResult requestResult = downloadClient.MakeRequest(imageUrl, requestType, referrer);
|
||||||
byte[] buffer = new byte[requestResult.result.Length];
|
byte[] buffer = new byte[requestResult.result.Length];
|
||||||
requestResult.result.ReadExactly(buffer, 0, buffer.Length);
|
requestResult.result.ReadExactly(buffer, 0, buffer.Length);
|
||||||
File.WriteAllBytes(fullPath, buffer);
|
File.WriteAllBytes(fullPath, buffer);
|
||||||
@ -137,11 +139,9 @@ public abstract class Connector
|
|||||||
/// </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="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 void DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, byte requestType, string? comicInfoPath = null, string? referrer = null)
|
||||||
{
|
{
|
||||||
logger?.WriteLine("Connector", $"Downloading Images for {saveArchiveFilePath}");
|
logger?.WriteLine("Connector", $"Downloading Images for {saveArchiveFilePath}");
|
||||||
//Check if Publication Directory already exists
|
//Check if Publication Directory already exists
|
||||||
@ -162,7 +162,7 @@ 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}");
|
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}"), requestType, referrer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(comicInfoPath is not null)
|
if(comicInfoPath is not null)
|
||||||
@ -174,6 +174,23 @@ public abstract class Connector
|
|||||||
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 static readonly HttpClient Client = new();
|
private static readonly HttpClient Client = new();
|
||||||
@ -185,8 +202,8 @@ public abstract class Connector
|
|||||||
/// <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>
|
/// <param name="rateLimitRequestsPerMinute">Rate limits for requests. byte is RequestType, int maximum requests per minute for RequestType</param>
|
||||||
|
/// <param name="logger"></param>
|
||||||
public DownloadClient(Dictionary<byte, int> rateLimitRequestsPerMinute, Logger? logger)
|
public DownloadClient(Dictionary<byte, int> rateLimitRequestsPerMinute, Logger? logger)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
@ -202,7 +219,7 @@ public abstract class Connector
|
|||||||
/// <param name="url"></param>
|
/// <param name="url"></param>
|
||||||
/// <param name="requestType">For RateLimits: Same Endpoints use same type</param>
|
/// <param name="requestType">For RateLimits: Same Endpoints use same type</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, byte requestType)
|
public RequestResult MakeRequest(string url, byte requestType, string? referrer = null)
|
||||||
{
|
{
|
||||||
if (_rateLimit.TryGetValue(requestType, out TimeSpan value))
|
if (_rateLimit.TryGetValue(requestType, out TimeSpan value))
|
||||||
_lastExecutedRateLimit.TryAdd(requestType, DateTime.Now.Subtract(value));
|
_lastExecutedRateLimit.TryAdd(requestType, DateTime.Now.Subtract(value));
|
||||||
@ -224,6 +241,8 @@ public abstract class Connector
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
HttpRequestMessage requestMessage = new(HttpMethod.Get, url);
|
HttpRequestMessage requestMessage = new(HttpMethod.Get, url);
|
||||||
|
if(referrer is not null)
|
||||||
|
requestMessage.Headers.Referrer = new Uri(referrer);
|
||||||
_lastExecutedRateLimit[requestType] = DateTime.Now;
|
_lastExecutedRateLimit[requestType] = DateTime.Now;
|
||||||
response = Client.Send(requestMessage);
|
response = Client.Send(requestMessage);
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ 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
|
||||||
@ -55,10 +56,10 @@ 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)
|
||||||
{
|
{
|
||||||
|
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();
|
||||||
|
|
||||||
@ -101,7 +102,7 @@ public class MangaDex : Connector
|
|||||||
string? coverUrl = GetCoverUrl(publicationId, posterId);
|
string? coverUrl = GetCoverUrl(publicationId, posterId);
|
||||||
string? coverCacheName = null;
|
string? coverCacheName = null;
|
||||||
if (coverUrl is not null)
|
if (coverUrl is not null)
|
||||||
coverCacheName = SaveImage(coverUrl);
|
coverCacheName = SaveCoverImageToCache(coverUrl, (byte)RequestType.AtHomeServer);
|
||||||
|
|
||||||
string? author = GetAuthor(authorId);
|
string? author = GetAuthor(authorId);
|
||||||
|
|
||||||
@ -224,10 +225,10 @@ 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, (byte)RequestType.AtHomeServer, logger, comicInfoPath);
|
DownloadChapterImages(imageUrls.ToArray(), GetArchiveFilePath(publication, chapter), (byte)RequestType.AtHomeServer, comicInfoPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string? GetCoverUrl(string publicationId, string? posterId)
|
private string? GetCoverUrl(string publicationId, string? posterId)
|
||||||
@ -272,42 +273,4 @@ public class MangaDex : Connector
|
|||||||
logger?.WriteLine(this.GetType().ToString(), $"Got author {authorId} -> {author}");
|
logger?.WriteLine(this.GetType().ToString(), $"Got author {authorId} -> {author}");
|
||||||
return author;
|
return author;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void CloneCoverFromCache(Publication publication, TrangaSettings settings)
|
|
||||||
{
|
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Cloning cover {publication.sortName}");
|
|
||||||
//Check if Publication already has a Folder and cover
|
|
||||||
string publicationFolder = Path.Join(downloadLocation, publication.folderName);
|
|
||||||
|
|
||||||
if(!Directory.Exists(publicationFolder))
|
|
||||||
Directory.CreateDirectory(publicationFolder);
|
|
||||||
DirectoryInfo dirInfo = new (publicationFolder);
|
|
||||||
if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover.")))
|
|
||||||
{
|
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Cover exists {publication.sortName}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string SaveImage(string url)
|
|
||||||
{
|
|
||||||
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, (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;
|
|
||||||
}
|
|
||||||
}
|
}
|
197
Tranga/Connectors/Manganato.cs
Normal file
197
Tranga/Connectors/Manganato.cs
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using HtmlAgilityPack;
|
||||||
|
using Logging;
|
||||||
|
|
||||||
|
namespace Tranga.Connectors;
|
||||||
|
|
||||||
|
public class Manganato : Connector
|
||||||
|
{
|
||||||
|
public override string name { get; }
|
||||||
|
|
||||||
|
public Manganato(string downloadLocation, string imageCachePath, Logger? logger) : base(downloadLocation, imageCachePath, logger)
|
||||||
|
{
|
||||||
|
this.name = "Manganato";
|
||||||
|
this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
|
||||||
|
{
|
||||||
|
{(byte)1, 100}
|
||||||
|
}, logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Publication[] GetPublications(string publicationTitle = "")
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Getting Publications (title={publicationTitle})");
|
||||||
|
string sanitizedTitle = publicationTitle.ToLower().Replace(' ', '_');
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Getting Publications (title={sanitizedTitle})");
|
||||||
|
string requestUrl = $"https://manganato.com/search/story/{sanitizedTitle}";
|
||||||
|
DownloadClient.RequestResult requestResult =
|
||||||
|
downloadClient.MakeRequest(requestUrl, (byte)1);
|
||||||
|
if (requestResult.statusCode != HttpStatusCode.OK)
|
||||||
|
return Array.Empty<Publication>();
|
||||||
|
|
||||||
|
return ParsePublicationsFromHtml(requestResult.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Publication[] ParsePublicationsFromHtml(Stream html)
|
||||||
|
{
|
||||||
|
StreamReader reader = new (html);
|
||||||
|
string htmlString = reader.ReadToEnd();
|
||||||
|
HtmlDocument document = new ();
|
||||||
|
document.LoadHtml(htmlString);
|
||||||
|
IEnumerable<HtmlNode> searchResults = document.DocumentNode.Descendants("div").Where(n => n.HasClass("search-story-item"));
|
||||||
|
List<string> urls = new();
|
||||||
|
foreach (HtmlNode mangaResult in searchResults)
|
||||||
|
{
|
||||||
|
urls.Add(mangaResult.Descendants("a").First(n => n.HasClass("item-title")).GetAttributes()
|
||||||
|
.First(a => a.Name == "href").Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
HashSet<Publication> ret = new();
|
||||||
|
foreach (string url in urls)
|
||||||
|
{
|
||||||
|
DownloadClient.RequestResult requestResult =
|
||||||
|
downloadClient.MakeRequest(url, (byte)1);
|
||||||
|
if (requestResult.statusCode != HttpStatusCode.OK)
|
||||||
|
return Array.Empty<Publication>();
|
||||||
|
|
||||||
|
ret.Add(ParseSinglePublicationFromHtml(requestResult.result, url.Split('/')[^1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Publication ParseSinglePublicationFromHtml(Stream html, string publicationId)
|
||||||
|
{
|
||||||
|
StreamReader reader = new (html);
|
||||||
|
string htmlString = reader.ReadToEnd();
|
||||||
|
HtmlDocument document = new ();
|
||||||
|
document.LoadHtml(htmlString);
|
||||||
|
string status = "";
|
||||||
|
Dictionary<string, string> altTitles = new();
|
||||||
|
Dictionary<string, string>? links = null;
|
||||||
|
HashSet<string> tags = new();
|
||||||
|
string? author = null, originalLanguage = null;
|
||||||
|
int? year = DateTime.Now.Year;
|
||||||
|
|
||||||
|
HtmlNode infoNode = document.DocumentNode.Descendants("div").First(d => d.HasClass("story-info-right"));
|
||||||
|
|
||||||
|
string sortName = infoNode.Descendants("h1").First().InnerText;
|
||||||
|
|
||||||
|
HtmlNode infoTable = infoNode.Descendants().First(d => d.Name == "table");
|
||||||
|
|
||||||
|
foreach (HtmlNode row in infoTable.Descendants("tr"))
|
||||||
|
{
|
||||||
|
string key = row.SelectNodes("td").First().InnerText.ToLower();
|
||||||
|
string value = row.SelectNodes("td").Last().InnerText;
|
||||||
|
string keySanitized = string.Concat(Regex.Matches(key, "[a-z]"));
|
||||||
|
|
||||||
|
switch (keySanitized)
|
||||||
|
{
|
||||||
|
case "alternative":
|
||||||
|
string[] alts = value.Split(" ; ");
|
||||||
|
for(int i = 0; i < alts.Length; i++)
|
||||||
|
altTitles.Add(i.ToString(), alts[i]);
|
||||||
|
break;
|
||||||
|
case "authors":
|
||||||
|
author = value;
|
||||||
|
break;
|
||||||
|
case "status":
|
||||||
|
status = value;
|
||||||
|
break;
|
||||||
|
case "genres":
|
||||||
|
string[] genres = value.Split(" - ");
|
||||||
|
tags = genres.ToHashSet();
|
||||||
|
break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string posterUrl = document.DocumentNode.Descendants("span").First(s => s.HasClass("info-image")).Descendants("img").First()
|
||||||
|
.GetAttributes().First(a => a.Name == "src").Value;
|
||||||
|
|
||||||
|
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, 1);
|
||||||
|
|
||||||
|
string description = document.DocumentNode.Descendants("div").First(d => d.HasClass("panel-story-info-description"))
|
||||||
|
.InnerText.Replace("Description :", "");
|
||||||
|
while (description.StartsWith('\n'))
|
||||||
|
description = description.Substring(1);
|
||||||
|
|
||||||
|
string yearString = document.DocumentNode.Descendants("li").Last(li => li.HasClass("a-h")).Descendants("span")
|
||||||
|
.First(s => s.HasClass("chapter-time")).InnerText;
|
||||||
|
year = Convert.ToInt32(yearString.Split(',')[^1]) + 2000;
|
||||||
|
|
||||||
|
return new Publication(sortName, author, description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
|
||||||
|
year, originalLanguage, status, publicationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Chapter[] GetChapters(Publication publication, string language = "")
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Getting Chapters for {publication.sortName} {publication.internalId} (language={language})");
|
||||||
|
string requestUrl = $"https://chapmanganato.com/{publication.publicationId}";
|
||||||
|
DownloadClient.RequestResult requestResult =
|
||||||
|
downloadClient.MakeRequest(requestUrl, (byte)1);
|
||||||
|
if (requestResult.statusCode != HttpStatusCode.OK)
|
||||||
|
return Array.Empty<Chapter>();
|
||||||
|
|
||||||
|
return ParseChaptersFromHtml(requestResult.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Chapter[] ParseChaptersFromHtml(Stream html)
|
||||||
|
{
|
||||||
|
StreamReader reader = new (html);
|
||||||
|
string htmlString = reader.ReadToEnd();
|
||||||
|
HtmlDocument document = new ();
|
||||||
|
document.LoadHtml(htmlString);
|
||||||
|
List<Chapter> ret = new();
|
||||||
|
|
||||||
|
HtmlNode chapterList = document.DocumentNode.Descendants("ul").First(l => l.HasClass("row-content-chapter"));
|
||||||
|
|
||||||
|
foreach (HtmlNode chapterInfo in chapterList.Descendants("li"))
|
||||||
|
{
|
||||||
|
string fullString = chapterInfo.Descendants("a").First(d => d.HasClass("chapter-name")).InnerText;
|
||||||
|
|
||||||
|
string? volumeNumber = fullString.Contains("Vol.") ? fullString.Replace("Vol.", "").Split(' ')[0] : null;
|
||||||
|
string? chapterNumber = fullString.Split(':')[0].Split(' ')[^1];
|
||||||
|
string chapterName = string.Concat(fullString.Split(':')[1..]);
|
||||||
|
string url = chapterInfo.Descendants("a").First(d => d.HasClass("chapter-name"))
|
||||||
|
.GetAttributeValue("href", "");
|
||||||
|
ret.Add(new Chapter(chapterName, volumeNumber, chapterNumber, url));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void DownloadChapter(Publication publication, Chapter chapter)
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
|
||||||
|
string requestUrl = chapter.url;
|
||||||
|
DownloadClient.RequestResult requestResult =
|
||||||
|
downloadClient.MakeRequest(requestUrl, (byte)1);
|
||||||
|
if (requestResult.statusCode != HttpStatusCode.OK)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string[] imageUrls = ParseImageUrlsFromHtml(requestResult.result);
|
||||||
|
|
||||||
|
string comicInfoPath = Path.GetTempFileName();
|
||||||
|
File.WriteAllText(comicInfoPath, GetComicInfoXmlString(publication, chapter, logger));
|
||||||
|
|
||||||
|
DownloadChapterImages(imageUrls, GetArchiveFilePath(publication, chapter), (byte)1, comicInfoPath, "https://chapmanganato.com/");
|
||||||
|
}
|
||||||
|
|
||||||
|
private string[] ParseImageUrlsFromHtml(Stream html)
|
||||||
|
{
|
||||||
|
StreamReader reader = new (html);
|
||||||
|
string htmlString = reader.ReadToEnd();
|
||||||
|
HtmlDocument document = new ();
|
||||||
|
document.LoadHtml(htmlString);
|
||||||
|
List<string> ret = new();
|
||||||
|
|
||||||
|
HtmlNode imageContainer =
|
||||||
|
document.DocumentNode.Descendants("div").First(i => i.HasClass("container-chapter-reader"));
|
||||||
|
foreach(HtmlNode imageNode in imageContainer.Descendants("img"))
|
||||||
|
ret.Add(imageNode.GetAttributeValue("src", ""));
|
||||||
|
|
||||||
|
return ret.ToArray();
|
||||||
|
}
|
||||||
|
}
|
@ -47,6 +47,16 @@ public readonly struct Publication
|
|||||||
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}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SaveSeriesInfoJson(string downloadDirectory)
|
||||||
|
{
|
||||||
|
string publicationFolder = Path.Join(downloadDirectory, this.folderName);
|
||||||
|
if(!Directory.Exists(publicationFolder))
|
||||||
|
Directory.CreateDirectory(publicationFolder);
|
||||||
|
string seriesInfoPath = Path.Join(publicationFolder, "series.json");
|
||||||
|
if(!File.Exists(seriesInfoPath))
|
||||||
|
File.WriteAllText(seriesInfoPath,this.GetSeriesInfoJson());
|
||||||
|
}
|
||||||
|
|
||||||
/// <returns>Serialized JSON String for series.json</returns>
|
/// <returns>Serialized JSON String for series.json</returns>
|
||||||
public string GetSeriesInfoJson()
|
public string GetSeriesInfoJson()
|
||||||
@ -85,9 +95,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")
|
||||||
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, Logger? logger)
|
|
||||||
{
|
|
||||||
//Only execute task if it is not already being executed.
|
|
||||||
if (trangaTask.state == TrangaTask.ExecutionState.Running)
|
|
||||||
{
|
|
||||||
logger?.WriteLine("TaskExecutor", $"Task already running {trangaTask}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
trangaTask.state = TrangaTask.ExecutionState.Running;
|
|
||||||
logger?.WriteLine("TaskExecutor", $"Starting Task {trangaTask}");
|
|
||||||
|
|
||||||
//Connector is not needed for all tasks
|
|
||||||
Connector? connector = null;
|
|
||||||
if (trangaTask.task != TrangaTask.Task.UpdateKomgaLibrary)
|
|
||||||
connector = taskManager.GetConnector(trangaTask.connectorName!);
|
|
||||||
|
|
||||||
//Call appropriate Method based on TrangaTask.Task
|
|
||||||
switch (trangaTask.task)
|
|
||||||
{
|
|
||||||
case TrangaTask.Task.DownloadNewChapters:
|
|
||||||
DownloadNewChapters(connector!, (Publication)trangaTask.publication!, trangaTask.language, ref taskManager._chapterCollection, taskManager.settings);
|
|
||||||
break;
|
|
||||||
case TrangaTask.Task.UpdateChapters:
|
|
||||||
UpdateChapters(connector!, (Publication)trangaTask.publication!, trangaTask.language, ref taskManager._chapterCollection);
|
|
||||||
break;
|
|
||||||
case TrangaTask.Task.UpdatePublications:
|
|
||||||
UpdatePublications(connector!, ref taskManager._chapterCollection);
|
|
||||||
break;
|
|
||||||
case TrangaTask.Task.UpdateKomgaLibrary:
|
|
||||||
UpdateKomgaLibrary(taskManager);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger?.WriteLine("TaskExecutor", $"Task finished! {trangaTask}");
|
|
||||||
trangaTask.lastExecuted = DateTime.Now;
|
|
||||||
trangaTask.state = TrangaTask.ExecutionState.Waiting;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates all Komga-Libraries
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="taskManager">Parent</param>
|
|
||||||
private static void UpdateKomgaLibrary(TaskManager taskManager)
|
|
||||||
{
|
|
||||||
if (taskManager.komga is null)
|
|
||||||
return;
|
|
||||||
Komga komga = taskManager.komga;
|
|
||||||
|
|
||||||
Komga.KomgaLibrary[] allLibraries = komga.GetLibraries();
|
|
||||||
foreach (Komga.KomgaLibrary lib in allLibraries)
|
|
||||||
komga.UpdateLibrary(lib.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates the available Publications from a Connector (all of them)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="connector">Connector to receive Publications from</param>
|
|
||||||
/// <param name="chapterCollection"></param>
|
|
||||||
private static void UpdatePublications(Connector connector, ref Dictionary<Publication, List<Chapter>> chapterCollection)
|
|
||||||
{
|
|
||||||
Publication[] publications = connector.GetPublications();
|
|
||||||
foreach (Publication publication in publications)
|
|
||||||
chapterCollection.TryAdd(publication, new List<Chapter>());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks for new Chapters and Downloads new ones.
|
|
||||||
/// If no Chapters had been downloaded previously, download also cover and create series.json
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="connector">Connector to use</param>
|
|
||||||
/// <param name="publication">Publication to check</param>
|
|
||||||
/// <param name="language">Language to receive chapters for</param>
|
|
||||||
/// <param name="chapterCollection"></param>
|
|
||||||
private static void DownloadNewChapters(Connector connector, Publication publication, string language, ref Dictionary<Publication, List<Chapter>> chapterCollection, TrangaSettings settings)
|
|
||||||
{
|
|
||||||
//Check if Publication already has a Folder
|
|
||||||
string publicationFolder = Path.Join(connector.downloadLocation, publication.folderName);
|
|
||||||
if(!Directory.Exists(publicationFolder))
|
|
||||||
Directory.CreateDirectory(publicationFolder);
|
|
||||||
List<Chapter> newChapters = UpdateChapters(connector, publication, language, ref chapterCollection);
|
|
||||||
|
|
||||||
connector.CloneCoverFromCache(publication, settings);
|
|
||||||
|
|
||||||
string seriesInfoPath = Path.Join(publicationFolder, "series.json");
|
|
||||||
if(!File.Exists(seriesInfoPath))
|
|
||||||
File.WriteAllText(seriesInfoPath,publication.GetSeriesInfoJson());
|
|
||||||
|
|
||||||
foreach(Chapter newChapter in newChapters)
|
|
||||||
connector.DownloadChapter(publication, newChapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates the available Chapters of a Publication
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="connector">Connector to use</param>
|
|
||||||
/// <param name="publication">Publication to check</param>
|
|
||||||
/// <param name="language">Language to receive chapters for</param>
|
|
||||||
/// <param name="chapterCollection"></param>
|
|
||||||
/// <returns>List of Chapters that were previously not in collection</returns>
|
|
||||||
private static List<Chapter> UpdateChapters(Connector connector, Publication publication, string language, ref Dictionary<Publication, List<Chapter>> chapterCollection)
|
|
||||||
{
|
|
||||||
List<Chapter> newChaptersList = new();
|
|
||||||
chapterCollection.TryAdd(publication, newChaptersList); //To ensure publication is actually in collection
|
|
||||||
|
|
||||||
Chapter[] newChapters = connector.GetChapters(publication, language);
|
|
||||||
newChaptersList = newChapters.Where(nChapter => !connector.ChapterIsDownloaded(publication, nChapter)).ToList();
|
|
||||||
|
|
||||||
return newChaptersList;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +1,7 @@
|
|||||||
using Logging;
|
using Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Tranga.Connectors;
|
using Tranga.Connectors;
|
||||||
|
using Tranga.TrangaTasks;
|
||||||
|
|
||||||
namespace Tranga;
|
namespace Tranga;
|
||||||
|
|
||||||
@ -10,7 +11,7 @@ namespace Tranga;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class TaskManager
|
public class TaskManager
|
||||||
{
|
{
|
||||||
public Dictionary<Publication, List<Chapter>> _chapterCollection = new();
|
public Dictionary<Publication, List<Chapter>> chapterCollection = new();
|
||||||
private HashSet<TrangaTask> _allTasks;
|
private HashSet<TrangaTask> _allTasks;
|
||||||
private bool _continueRunning = true;
|
private bool _continueRunning = true;
|
||||||
private readonly Connector[] _connectors;
|
private readonly Connector[] _connectors;
|
||||||
@ -36,9 +37,13 @@ public class TaskManager
|
|||||||
newKomga = new Komga(komgaBaseUrl, komgaUsername, komgaPassword, logger);
|
newKomga = new Komga(komgaBaseUrl, komgaUsername, komgaPassword, logger);
|
||||||
|
|
||||||
this.settings = new TrangaSettings(downloadFolderPath, workingDirectory, newKomga);
|
this.settings = new TrangaSettings(downloadFolderPath, workingDirectory, newKomga);
|
||||||
ExportData();
|
ExportDataAndSettings();
|
||||||
|
|
||||||
this._connectors = new Connector[]{ new MangaDex(downloadFolderPath, imageCachePath, logger) };
|
this._connectors = new Connector[]
|
||||||
|
{
|
||||||
|
new MangaDex(downloadFolderPath, imageCachePath, logger),
|
||||||
|
new Manganato(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>());
|
||||||
|
|
||||||
@ -48,25 +53,28 @@ public class TaskManager
|
|||||||
|
|
||||||
public void UpdateSettings(string? downloadLocation, string? komgaUrl, string? komgaAuth)
|
public void UpdateSettings(string? downloadLocation, string? komgaUrl, string? komgaAuth)
|
||||||
{
|
{
|
||||||
Komga? komga = null;
|
if (komgaUrl is not null && komgaAuth is not null && komgaUrl.Length > 0 && komgaAuth.Length > 0)
|
||||||
if (komgaUrl is not null && komgaAuth is not null)
|
settings.komga = new Komga(komgaUrl, komgaAuth, null);
|
||||||
komga = new Komga(komgaUrl, komgaAuth, null);
|
if (downloadLocation is not null && downloadLocation.Length > 0)
|
||||||
settings.downloadLocation = downloadLocation ?? settings.downloadLocation;
|
settings.downloadLocation = downloadLocation;
|
||||||
settings.komga = komga ?? komga;
|
ExportDataAndSettings();
|
||||||
ExportData();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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, settings.coverImageCache, logger) };
|
this._connectors = new Connector[]
|
||||||
|
{
|
||||||
|
new MangaDex(settings.downloadLocation, settings.coverImageCache, logger),
|
||||||
|
new Manganato(settings.downloadLocation, settings.coverImageCache, logger)
|
||||||
|
};
|
||||||
foreach(Connector cConnector in this._connectors)
|
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>();
|
||||||
|
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
ImportData();
|
ImportData();
|
||||||
ExportData();
|
ExportDataAndSettings();
|
||||||
Thread taskChecker = new(TaskCheckerThread);
|
Thread taskChecker = new(TaskCheckerThread);
|
||||||
taskChecker.Start();
|
taskChecker.Start();
|
||||||
}
|
}
|
||||||
@ -84,7 +92,7 @@ public class TaskManager
|
|||||||
foreach (KeyValuePair<Connector, List<TrangaTask>> connectorTaskQueue in _taskQueue)
|
foreach (KeyValuePair<Connector, List<TrangaTask>> connectorTaskQueue in _taskQueue)
|
||||||
{
|
{
|
||||||
if(connectorTaskQueue.Value.RemoveAll(task => task.state == TrangaTask.ExecutionState.Waiting) > 0)
|
if(connectorTaskQueue.Value.RemoveAll(task => task.state == TrangaTask.ExecutionState.Waiting) > 0)
|
||||||
ExportData();
|
ExportDataAndSettings();
|
||||||
|
|
||||||
if (connectorTaskQueue.Value.Count > 0 && connectorTaskQueue.Value.All(task => task.state is TrangaTask.ExecutionState.Enqueued))
|
if (connectorTaskQueue.Value.Count > 0 && connectorTaskQueue.Value.All(task => task.state is TrangaTask.ExecutionState.Enqueued))
|
||||||
ExecuteTaskNow(connectorTaskQueue.Value.First());
|
ExecuteTaskNow(connectorTaskQueue.Value.First());
|
||||||
@ -95,7 +103,7 @@ public class TaskManager
|
|||||||
foreach (TrangaTask task in _allTasks.Where(aTask => aTask.ShouldExecute()))
|
foreach (TrangaTask task in _allTasks.Where(aTask => aTask.ShouldExecute()))
|
||||||
{
|
{
|
||||||
task.state = TrangaTask.ExecutionState.Enqueued;
|
task.state = TrangaTask.ExecutionState.Enqueued;
|
||||||
if(task.connectorName is null)
|
if(task.task == TrangaTask.Task.UpdateKomgaLibrary)
|
||||||
ExecuteTaskNow(task);
|
ExecuteTaskNow(task);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -116,10 +124,9 @@ public class TaskManager
|
|||||||
if (!this._allTasks.Contains(task))
|
if (!this._allTasks.Contains(task))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Forcing Execution: {task}");
|
Task t = new(() =>
|
||||||
Task t = new Task(() =>
|
|
||||||
{
|
{
|
||||||
TaskExecutor.Execute(this, task, logger);
|
task.Execute(this, this.logger);
|
||||||
});
|
});
|
||||||
t.Start();
|
t.Start();
|
||||||
}
|
}
|
||||||
@ -138,41 +145,41 @@ public class TaskManager
|
|||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Adding new Task {task} {connectorName} {publication?.sortName}");
|
logger?.WriteLine(this.GetType().ToString(), $"Adding new Task {task} {connectorName} {publication?.sortName}");
|
||||||
|
|
||||||
TrangaTask newTask;
|
TrangaTask? newTask = null;
|
||||||
if (task == TrangaTask.Task.UpdateKomgaLibrary)
|
if (task == TrangaTask.Task.UpdateKomgaLibrary)
|
||||||
{
|
{
|
||||||
newTask = new TrangaTask(task, null, null, reoccurrence);
|
newTask = new UpdateKomgaLibraryTask(task, reoccurrence);
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Removing old {task}-Task.");
|
logger?.WriteLine(this.GetType().ToString(), $"Removing old {task}-Task.");
|
||||||
//Only one UpdateKomgaLibrary Task
|
//Only one UpdateKomgaLibrary Task
|
||||||
_allTasks.RemoveWhere(trangaTask => trangaTask.task is TrangaTask.Task.UpdateKomgaLibrary);
|
_allTasks.RemoveWhere(trangaTask => trangaTask.task is TrangaTask.Task.UpdateKomgaLibrary);
|
||||||
_allTasks.Add(newTask);
|
_allTasks.Add(newTask);
|
||||||
}
|
logger?.WriteLine(this.GetType().ToString(), $"Added new Task {newTask}");
|
||||||
else
|
}else if (task == TrangaTask.Task.DownloadNewChapters)
|
||||||
{
|
{
|
||||||
if(connectorName is null)
|
|
||||||
throw new ArgumentException($"connectorName can not be null for task {task}");
|
|
||||||
|
|
||||||
//Get appropriate Connector from available Connectors for TrangaTask
|
//Get appropriate Connector from available Connectors for TrangaTask
|
||||||
Connector? connector = _connectors.FirstOrDefault(c => c.name == connectorName);
|
Connector? connector = _connectors.FirstOrDefault(c => c.name == connectorName);
|
||||||
if (connector is null)
|
if (connectorName is null)
|
||||||
throw new ArgumentException($"Connector {connectorName} is not a known connector.");
|
throw new ArgumentException($"connectorName can not be null for task {task}");
|
||||||
|
|
||||||
newTask = new TrangaTask(task, connector.name, publication, reoccurrence, language);
|
if (publication is null)
|
||||||
|
throw new ArgumentException($"publication can not be null for task {task}");
|
||||||
//Check if same task already exists
|
Publication pub = (Publication)publication;
|
||||||
if (!_allTasks.Any(trangaTask => trangaTask.task == task && trangaTask.connectorName == connector.name &&
|
newTask = new DownloadNewChaptersTask(task, connector!.name, pub, reoccurrence, language);
|
||||||
trangaTask.publication?.internalId == publication?.internalId))
|
|
||||||
|
if (!_allTasks.Any(trangaTask =>
|
||||||
|
trangaTask.task == task && trangaTask.publication?.internalId == pub.internalId &&
|
||||||
|
trangaTask.connectorName == connector.name))
|
||||||
{
|
{
|
||||||
if(task != TrangaTask.Task.UpdatePublications)
|
|
||||||
_chapterCollection.TryAdd((Publication)publication!, new List<Chapter>());
|
|
||||||
_allTasks.Add(newTask);
|
_allTasks.Add(newTask);
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Added new Task {newTask}");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Publication already exists {publication?.internalId}");
|
logger?.WriteLine(this.GetType().ToString(), $"Task already exists {newTask}");
|
||||||
}
|
}
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Added new Task {newTask.ToString()}");
|
ExportDataAndSettings();
|
||||||
ExportData();
|
|
||||||
|
if (newTask is null)
|
||||||
|
throw new Exception("Invalid path");
|
||||||
return newTask;
|
return newTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,7 +215,7 @@ public class TaskManager
|
|||||||
else
|
else
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"No Task {task} {publication?.sortName} {publication?.internalId} could be found.");
|
logger?.WriteLine(this.GetType().ToString(), $"No Task {task} {publication?.sortName} {publication?.internalId} could be found.");
|
||||||
}
|
}
|
||||||
ExportData();
|
ExportDataAndSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -252,8 +259,8 @@ public class TaskManager
|
|||||||
Publication[] ret = connector.GetPublications(title ?? "");
|
Publication[] ret = connector.GetPublications(title ?? "");
|
||||||
foreach (Publication publication in ret)
|
foreach (Publication publication in ret)
|
||||||
{
|
{
|
||||||
if(!_chapterCollection.Any(pub => pub.Key.sortName == publication.sortName))
|
if(!chapterCollection.Any(pub => pub.Key.sortName == publication.sortName))
|
||||||
this._chapterCollection.TryAdd(publication, new List<Chapter>());
|
this.chapterCollection.TryAdd(publication, new List<Chapter>());
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@ -261,7 +268,7 @@ public class TaskManager
|
|||||||
/// <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>
|
||||||
@ -276,7 +283,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>
|
||||||
@ -287,7 +294,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));
|
||||||
@ -307,7 +314,7 @@ public class TaskManager
|
|||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Importing tasks from {settings.tasksFilePath}");
|
logger?.WriteLine(this.GetType().ToString(), $"Importing tasks from {settings.tasksFilePath}");
|
||||||
buffer = File.ReadAllText(settings.tasksFilePath);
|
buffer = File.ReadAllText(settings.tasksFilePath);
|
||||||
this._allTasks = JsonConvert.DeserializeObject<HashSet<TrangaTask>>(buffer)!;
|
this._allTasks = JsonConvert.DeserializeObject<HashSet<TrangaTask>>(buffer, new JsonSerializerSettings() { Converters = { new TrangaTask.TrangaTaskJsonConverter() } })!;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (File.Exists(settings.knownPublicationsPath))
|
if (File.Exists(settings.knownPublicationsPath))
|
||||||
@ -316,14 +323,14 @@ public class TaskManager
|
|||||||
buffer = File.ReadAllText(settings.knownPublicationsPath);
|
buffer = File.ReadAllText(settings.knownPublicationsPath);
|
||||||
Publication[] publications = JsonConvert.DeserializeObject<Publication[]>(buffer)!;
|
Publication[] publications = JsonConvert.DeserializeObject<Publication[]>(buffer)!;
|
||||||
foreach (Publication publication in publications)
|
foreach (Publication publication in publications)
|
||||||
this._chapterCollection.TryAdd(publication, new List<Chapter>());
|
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 settings to {settings.settingsFilePath}");
|
logger?.WriteLine(this.GetType().ToString(), $"Exporting settings to {settings.settingsFilePath}");
|
||||||
File.WriteAllText(settings.settingsFilePath, JsonConvert.SerializeObject(settings));
|
File.WriteAllText(settings.settingsFilePath, JsonConvert.SerializeObject(settings));
|
||||||
@ -332,7 +339,7 @@ public class TaskManager
|
|||||||
File.WriteAllText(settings.tasksFilePath, JsonConvert.SerializeObject(this._allTasks));
|
File.WriteAllText(settings.tasksFilePath, JsonConvert.SerializeObject(this._allTasks));
|
||||||
|
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Exporting known publications to {settings.knownPublicationsPath}");
|
logger?.WriteLine(this.GetType().ToString(), $"Exporting known publications to {settings.knownPublicationsPath}");
|
||||||
File.WriteAllText(settings.knownPublicationsPath, JsonConvert.SerializeObject(this._chapterCollection.Keys.ToArray()));
|
File.WriteAllText(settings.knownPublicationsPath, JsonConvert.SerializeObject(this.chapterCollection.Keys.ToArray()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
</PropertyGroup>
|
</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" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
@ -14,6 +14,8 @@ public class TrangaSettings
|
|||||||
|
|
||||||
public TrangaSettings(string downloadLocation, string workingDirectory, Komga? komga)
|
public TrangaSettings(string downloadLocation, string workingDirectory, Komga? komga)
|
||||||
{
|
{
|
||||||
|
if (downloadLocation.Length < 1 || workingDirectory.Length < 1)
|
||||||
|
throw new ArgumentException("Download-location and working-directory paths can not be empty!");
|
||||||
this.workingDirectory = workingDirectory;
|
this.workingDirectory = workingDirectory;
|
||||||
this.downloadLocation = downloadLocation;
|
this.downloadLocation = downloadLocation;
|
||||||
this.komga = komga;
|
this.komga = komga;
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
using Newtonsoft.Json;
|
using Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Tranga.TrangaTasks;
|
||||||
|
|
||||||
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
|
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
|
||||||
@ -14,7 +17,7 @@ public class TrangaTask
|
|||||||
public string? connectorName { get; }
|
public string? connectorName { get; }
|
||||||
public Task task { get; }
|
public Task task { get; }
|
||||||
public Publication? publication { get; }
|
public Publication? publication { get; }
|
||||||
public string language { get; }
|
public string? language { get; }
|
||||||
[JsonIgnore]public ExecutionState state { get; set; }
|
[JsonIgnore]public ExecutionState state { get; set; }
|
||||||
|
|
||||||
public enum ExecutionState
|
public enum ExecutionState
|
||||||
@ -24,14 +27,8 @@ public class TrangaTask
|
|||||||
Running
|
Running
|
||||||
};
|
};
|
||||||
|
|
||||||
public TrangaTask(Task task, string? connectorName, Publication? publication, TimeSpan reoccurrence, string language = "")
|
protected TrangaTask(Task task, string? connectorName, Publication? publication, TimeSpan reoccurrence, string? language = null)
|
||||||
{
|
{
|
||||||
if(task != Task.UpdateKomgaLibrary && connectorName is null)
|
|
||||||
throw new ArgumentException($"connectorName can not be null for task {task}");
|
|
||||||
|
|
||||||
if (publication is null && task != Task.UpdatePublications && task != Task.UpdateKomgaLibrary)
|
|
||||||
throw new ArgumentException($"Publication can not be null for task {task}");
|
|
||||||
|
|
||||||
this.publication = publication;
|
this.publication = publication;
|
||||||
this.reoccurrence = reoccurrence;
|
this.reoccurrence = reoccurrence;
|
||||||
this.lastExecuted = DateTime.Now.Subtract(reoccurrence);
|
this.lastExecuted = DateTime.Now.Subtract(reoccurrence);
|
||||||
@ -39,6 +36,29 @@ public class TrangaTask
|
|||||||
this.task = task;
|
this.task = task;
|
||||||
this.language = language;
|
this.language = language;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// BL for concrete Tasks
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="taskManager"></param>
|
||||||
|
/// <param name="logger"></param>
|
||||||
|
protected abstract void ExecuteTask(TaskManager taskManager, Logger? logger);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute the task
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="taskManager">Should be the parent taskManager</param>
|
||||||
|
/// <param name="logger"></param>
|
||||||
|
public void Execute(TaskManager taskManager, Logger? logger)
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Executing Task {this}");
|
||||||
|
this.state = ExecutionState.Running;
|
||||||
|
ExecuteTask(taskManager, logger);
|
||||||
|
this.lastExecuted = DateTime.Now;
|
||||||
|
this.state = ExecutionState.Waiting;
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Finished Executing Task {this}");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/// <returns>True if elapsed time since last execution is greater than set interval</returns>
|
/// <returns>True if elapsed time since last execution is greater than set interval</returns>
|
||||||
public bool ShouldExecute()
|
public bool ShouldExecute()
|
||||||
@ -46,16 +66,48 @@ public class TrangaTask
|
|||||||
return DateTime.Now.Subtract(this.lastExecuted) > reoccurrence && state is ExecutionState.Waiting;
|
return DateTime.Now.Subtract(this.lastExecuted) > reoccurrence && state is ExecutionState.Waiting;
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Task
|
public enum Task : byte
|
||||||
{
|
{
|
||||||
UpdatePublications,
|
DownloadNewChapters = 2,
|
||||||
UpdateChapters,
|
UpdateKomgaLibrary = 3
|
||||||
DownloadNewChapters,
|
|
||||||
UpdateKomgaLibrary
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{task}, {lastExecuted}, {reoccurrence}, {state} {(connectorName is not null ? $", {connectorName}" : "" )} {(publication is not null ? $", {publication?.sortName}": "")}";
|
return $"{task}, {lastExecuted}, {reoccurrence}, {state} {(connectorName is not null ? $", {connectorName}" : "" )} {(publication is not null ? $", {publication?.sortName}": "")}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class TrangaTaskJsonConverter : JsonConverter
|
||||||
|
{
|
||||||
|
public override bool CanConvert(Type objectType)
|
||||||
|
{
|
||||||
|
return (objectType == typeof(TrangaTask));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
JObject jo = JObject.Load(reader);
|
||||||
|
if (jo["task"]!.Value<Int64>() == (Int64)Task.DownloadNewChapters)
|
||||||
|
return jo.ToObject<DownloadNewChaptersTask>(serializer)!;
|
||||||
|
|
||||||
|
if (jo["task"]!.Value<Int64>() == (Int64)Task.UpdateKomgaLibrary)
|
||||||
|
return jo.ToObject<UpdateKomgaLibraryTask>(serializer)!;
|
||||||
|
|
||||||
|
throw new Exception();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanWrite => false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Don't call this
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="writer"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
/// <param name="serializer"></param>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
throw new Exception("Dont call this");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
48
Tranga/TrangaTasks/DownloadNewChaptersTask.cs
Normal file
48
Tranga/TrangaTasks/DownloadNewChaptersTask.cs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
using Logging;
|
||||||
|
|
||||||
|
namespace Tranga.TrangaTasks;
|
||||||
|
|
||||||
|
public class DownloadNewChaptersTask : TrangaTask
|
||||||
|
{
|
||||||
|
public DownloadNewChaptersTask(Task task, string connectorName, Publication publication, TimeSpan reoccurrence, string language = "en") : base(task, connectorName, publication, reoccurrence, language)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ExecuteTask(TaskManager taskManager, Logger? logger)
|
||||||
|
{
|
||||||
|
Publication pub = (Publication)this.publication!;
|
||||||
|
Connector connector = taskManager.GetConnector(this.connectorName);
|
||||||
|
|
||||||
|
//Check if Publication already has a Folder
|
||||||
|
string publicationFolder = Path.Join(connector.downloadLocation, pub.folderName);
|
||||||
|
if(!Directory.Exists(publicationFolder))
|
||||||
|
Directory.CreateDirectory(publicationFolder);
|
||||||
|
List<Chapter> newChapters = UpdateChapters(connector, pub, language!, ref taskManager.chapterCollection);
|
||||||
|
|
||||||
|
connector.CopyCoverFromCacheToDownloadLocation(pub, taskManager.settings);
|
||||||
|
|
||||||
|
pub.SaveSeriesInfoJson(connector.downloadLocation);
|
||||||
|
|
||||||
|
foreach(Chapter newChapter in newChapters)
|
||||||
|
connector.DownloadChapter(pub, newChapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the available Chapters of a Publication
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="connector">Connector to use</param>
|
||||||
|
/// <param name="publication">Publication to check</param>
|
||||||
|
/// <param name="language">Language to receive chapters for</param>
|
||||||
|
/// <param name="chapterCollection"></param>
|
||||||
|
/// <returns>List of Chapters that were previously not in collection</returns>
|
||||||
|
private static List<Chapter> UpdateChapters(Connector connector, Publication publication, string language, ref Dictionary<Publication, List<Chapter>> chapterCollection)
|
||||||
|
{
|
||||||
|
List<Chapter> newChaptersList = new();
|
||||||
|
chapterCollection.TryAdd(publication, newChaptersList); //To ensure publication is actually in collection
|
||||||
|
|
||||||
|
Chapter[] newChapters = connector.GetChapters(publication, language);
|
||||||
|
newChaptersList = newChapters.Where(nChapter => !connector.CheckChapterIsDownloaded(publication, nChapter)).ToList();
|
||||||
|
|
||||||
|
return newChaptersList;
|
||||||
|
}
|
||||||
|
}
|
21
Tranga/TrangaTasks/UpdateKomgaLibraryTask.cs
Normal file
21
Tranga/TrangaTasks/UpdateKomgaLibraryTask.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using Logging;
|
||||||
|
|
||||||
|
namespace Tranga.TrangaTasks;
|
||||||
|
|
||||||
|
public class UpdateKomgaLibraryTask : TrangaTask
|
||||||
|
{
|
||||||
|
public UpdateKomgaLibraryTask(Task task, TimeSpan reoccurrence) : base(task, null, null, reoccurrence)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ExecuteTask(TaskManager taskManager, Logger? logger)
|
||||||
|
{
|
||||||
|
if (taskManager.komga is null)
|
||||||
|
return;
|
||||||
|
Komga komga = taskManager.komga;
|
||||||
|
|
||||||
|
Komga.KomgaLibrary[] allLibraries = komga.GetLibraries();
|
||||||
|
foreach (Komga.KomgaLibrary lib in allLibraries)
|
||||||
|
komga.UpdateLibrary(lib.id);
|
||||||
|
}
|
||||||
|
}
|
@ -4,17 +4,18 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Tranga</title>
|
<title>Tranga</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<wrapper>
|
<wrapper>
|
||||||
<topbar>
|
<topbar>
|
||||||
<titlebox>
|
<titlebox>
|
||||||
<img src="media/blahaj.png">
|
<img alt="website image is Blahaj" src="media/blahaj.png">
|
||||||
<span>Tranga</span>
|
<span>Tranga</span>
|
||||||
</titlebox>
|
</titlebox>
|
||||||
<spacer></spacer>
|
<spacer></spacer>
|
||||||
<searchdiv>
|
<searchdiv>
|
||||||
<input id="searchbox" placeholder="Filter" type="text">
|
<label for="searchbox"></label><input id="searchbox" placeholder="Filter" type="text">
|
||||||
</searchdiv>
|
</searchdiv>
|
||||||
<img id="settingscog" src="media/settings-cogwheel.svg" height="100%" alt="settingscog">
|
<img id="settingscog" src="media/settings-cogwheel.svg" height="100%" alt="settingscog">
|
||||||
</topbar>
|
</topbar>
|
||||||
@ -60,6 +61,7 @@
|
|||||||
<img id="pubviewcover" src="media/cover.jpg" alt="cover">
|
<img id="pubviewcover" src="media/cover.jpg" alt="cover">
|
||||||
<publication-information>
|
<publication-information>
|
||||||
<publication-name id="publicationViewerName">Tensei Pandemic</publication-name>
|
<publication-name id="publicationViewerName">Tensei Pandemic</publication-name>
|
||||||
|
<publication-tags id="publicationViewerTags"></publication-tags>
|
||||||
<publication-author id="publicationViewerAuthor">Imamura Hinata</publication-author>
|
<publication-author id="publicationViewerAuthor">Imamura Hinata</publication-author>
|
||||||
<publication-description id="publicationViewerDescription">Imamura Hinata is a high school boy with a cute appearance.
|
<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…
|
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…
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
let tasks = [];
|
let tasks = [];
|
||||||
let toEditId;
|
let toEditId;
|
||||||
|
|
||||||
|
const searchBox = document.querySelector("#searchbox");
|
||||||
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");
|
||||||
@ -14,6 +15,7 @@ const publicationViewerPopup = document.querySelector("#publicationViewerPopup")
|
|||||||
const publicationViewerWindow = document.querySelector("publication-viewer");
|
const publicationViewerWindow = document.querySelector("publication-viewer");
|
||||||
const publicationViewerDescription = document.querySelector("#publicationViewerDescription");
|
const publicationViewerDescription = document.querySelector("#publicationViewerDescription");
|
||||||
const publicationViewerName = document.querySelector("#publicationViewerName");
|
const publicationViewerName = document.querySelector("#publicationViewerName");
|
||||||
|
const publicationViewerTags = document.querySelector("#publicationViewerTags");
|
||||||
const publicationViewerAuthor = document.querySelector("#publicationViewerAuthor");
|
const publicationViewerAuthor = document.querySelector("#publicationViewerAuthor");
|
||||||
const pubviewcover = document.querySelector("#pubviewcover");
|
const pubviewcover = document.querySelector("#pubviewcover");
|
||||||
const publicationDelete = document.querySelector("publication-delete");
|
const publicationDelete = document.querySelector("publication-delete");
|
||||||
@ -33,6 +35,7 @@ const tagTasksTotal = document.querySelector("#totalTasksTag");
|
|||||||
const tagTaskPopup = document.querySelector("footer-tag-popup");
|
const tagTaskPopup = document.querySelector("footer-tag-popup");
|
||||||
const tagTasksPopupContent = document.querySelector("footer-tag-content");
|
const tagTasksPopupContent = document.querySelector("footer-tag-content");
|
||||||
|
|
||||||
|
searchbox.addEventListener("keyup", (event) => FilterResults());
|
||||||
settingsCog.addEventListener("click", () => OpenSettings());
|
settingsCog.addEventListener("click", () => OpenSettings());
|
||||||
document.querySelector("#blurBackgroundSettingsPopup").addEventListener("click", () => HideSettings());
|
document.querySelector("#blurBackgroundSettingsPopup").addEventListener("click", () => HideSettings());
|
||||||
closetaskpopup.addEventListener("click", () => HideAddTaskPopup());
|
closetaskpopup.addEventListener("click", () => HideAddTaskPopup());
|
||||||
@ -78,6 +81,11 @@ function NewSearch(){
|
|||||||
selectRecurrence.disabled = true;
|
selectRecurrence.disabled = true;
|
||||||
connectorSelect.disabled = true;
|
connectorSelect.disabled = true;
|
||||||
searchPublicationQuery.disabled = true;
|
searchPublicationQuery.disabled = true;
|
||||||
|
//Waitcursor
|
||||||
|
document.body.style.cursor = "wait";
|
||||||
|
selectRecurrence.style.cursor = "wait";
|
||||||
|
connectorSelect.style.cursor = "wait";
|
||||||
|
searchPublicationQuery.style.cursor = "wait";
|
||||||
|
|
||||||
//Empty previous results
|
//Empty previous results
|
||||||
selectPublication.replaceChildren();
|
selectPublication.replaceChildren();
|
||||||
@ -96,6 +104,11 @@ function NewSearch(){
|
|||||||
selectRecurrence.disabled = false;
|
selectRecurrence.disabled = false;
|
||||||
connectorSelect.disabled = false;
|
connectorSelect.disabled = false;
|
||||||
searchPublicationQuery.disabled = false;
|
searchPublicationQuery.disabled = false;
|
||||||
|
//Cursor
|
||||||
|
document.body.style.cursor = "initial";
|
||||||
|
selectRecurrence.style.cursor = "initial";
|
||||||
|
connectorSelect.style.cursor = "initial";
|
||||||
|
searchPublicationQuery.style.cursor = "initial";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,6 +182,7 @@ function ShowPublicationViewerWindow(publicationId, event, add){
|
|||||||
//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;
|
||||||
|
publicationViewerTags.innerText = publication.tags.join(", ");
|
||||||
publicationViewerDescription.innerText = publication.description;
|
publicationViewerDescription.innerText = publication.description;
|
||||||
publicationViewerAuthor.innerText = publication.author;
|
publicationViewerAuthor.innerText = publication.author;
|
||||||
pubviewcover.src = `imageCache/${publication.coverFileNameInCache}`;
|
pubviewcover.src = `imageCache/${publication.coverFileNameInCache}`;
|
||||||
@ -230,8 +244,11 @@ function GetSettingsClick(){
|
|||||||
|
|
||||||
GetSettings().then(json => {
|
GetSettings().then(json => {
|
||||||
settingDownloadLocation.innerText = json.downloadLocation;
|
settingDownloadLocation.innerText = json.downloadLocation;
|
||||||
if(json.komga != null)
|
if(json.komga != null) {
|
||||||
settingKomgaUrl.placeholder = json.komga.baseUrl;
|
settingKomgaUrl.placeholder = json.komga.baseUrl;
|
||||||
|
settingKomgaUser.placeholder = "Configured";
|
||||||
|
settingKomgaPass.placeholder = "***";
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
GetKomgaTask().then(json => {
|
GetKomgaTask().then(json => {
|
||||||
@ -296,7 +313,6 @@ function ShowQueuedTasks(event){
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function ShowAllTasks(event){
|
function ShowAllTasks(event){
|
||||||
GetDownloadTasks()
|
GetDownloadTasks()
|
||||||
.then(json => {
|
.then(json => {
|
||||||
@ -317,6 +333,30 @@ function CloseTasksPopup(){
|
|||||||
tagTaskPopup.style.display = "none";
|
tagTaskPopup.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FilterResults(){
|
||||||
|
if(searchBox.value.length > 0){
|
||||||
|
tasksContent.childNodes.forEach(publication => {
|
||||||
|
publication.childNodes.forEach(item => {
|
||||||
|
if(item.nodeName.toLowerCase() == "publication-information"){
|
||||||
|
item.childNodes.forEach(information => {
|
||||||
|
if(information.nodeName.toLowerCase() == "publication-name"){
|
||||||
|
if(!information.textContent.toLowerCase().includes(searchBox.value.toLowerCase())){
|
||||||
|
publication.style.display = "none";
|
||||||
|
}else{
|
||||||
|
publication.style.display = "initial";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
tasksContent.childNodes.forEach(publication => publication.style.display = "initial");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
//Resets the tasks shown
|
//Resets the tasks shown
|
||||||
ResetContent();
|
ResetContent();
|
||||||
//Get Tasks and show them
|
//Get Tasks and show them
|
||||||
|
@ -53,6 +53,7 @@ titlebox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
titlebox span{
|
titlebox span{
|
||||||
|
cursor: default;
|
||||||
font-size: 24pt;
|
font-size: 24pt;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
background: linear-gradient(150deg, var(--primary-color), var(--accent-color));
|
background: linear-gradient(150deg, var(--primary-color), var(--accent-color));
|
||||||
@ -64,6 +65,7 @@ titlebox span{
|
|||||||
titlebox img {
|
titlebox img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
cursor: grab;
|
||||||
}
|
}
|
||||||
|
|
||||||
spacer{
|
spacer{
|
||||||
@ -119,6 +121,7 @@ footer > div {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer > div > *{
|
footer > div > *{
|
||||||
@ -130,6 +133,7 @@ footer > div > *{
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
|
cursor: url("media/blahaj.png"), grab;
|
||||||
}
|
}
|
||||||
|
|
||||||
content {
|
content {
|
||||||
@ -376,14 +380,13 @@ addtask-settings addtask-setting{
|
|||||||
publication-viewer{
|
publication-viewer{
|
||||||
display: block;
|
display: block;
|
||||||
width: 450px;
|
width: 450px;
|
||||||
height: 300px;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 200px;
|
top: 200px;
|
||||||
left: 400px;
|
left: 400px;
|
||||||
background-color: var(--accent-color);
|
background-color: var(--accent-color);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 30px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
publication-viewer::after{
|
publication-viewer::after{
|
||||||
@ -391,7 +394,8 @@ publication-viewer::after{
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0; top: 0;
|
left: 0; top: 0;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
width: 100%; height: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
background: rgba(0,0,0,0.8);
|
background: rgba(0,0,0,0.8);
|
||||||
backdrop-filter: blur(3px);
|
backdrop-filter: blur(3px);
|
||||||
}
|
}
|
||||||
@ -407,16 +411,32 @@ publication-viewer img {
|
|||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
publication-viewer publication-information publication-name{
|
publication-viewer publication-information > * {
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
publication-viewer publication-information publication-author {
|
publication-viewer publication-information publication-name {
|
||||||
margin: 5px 0;
|
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 {
|
publication-viewer publication-information publication-author::before {
|
||||||
content: "Author: ";
|
content: "Author: ";
|
||||||
|
font-weight: bolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
publication-viewer publication-information publication-description::before {
|
publication-viewer publication-information publication-description::before {
|
||||||
@ -428,24 +448,22 @@ publication-viewer publication-information publication-description::before {
|
|||||||
publication-viewer publication-information publication-description {
|
publication-viewer publication-information publication-description {
|
||||||
font-size: 12pt;
|
font-size: 12pt;
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
max-height: 200px;
|
height: 145px;
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
publication-viewer publication-information publication-interactions {
|
publication-viewer publication-information publication-interactions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
position: absolute;
|
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
publication-viewer publication-information publication-interactions > * {
|
publication-viewer publication-information publication-interactions > * {
|
||||||
margin: 0 10px 10px 10px;
|
margin: 0 10px;
|
||||||
font-size: 16pt;
|
font-size: 16pt;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
publication-viewer publication-information publication-interactions publication-starttask {
|
publication-viewer publication-information publication-interactions publication-starttask {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
services:
|
version: '3'
|
||||||
|
services:
|
||||||
tranga-api:
|
tranga-api:
|
||||||
image: glax/tranga-api:latest
|
image: glax/tranga-api:latest
|
||||||
container_name: tranga-api
|
container_name: tranga-api
|
||||||
@ -16,4 +17,5 @@
|
|||||||
ports:
|
ports:
|
||||||
- 9555:80
|
- 9555:80
|
||||||
depends_on:
|
depends_on:
|
||||||
- tranga-api
|
- tranga-api
|
||||||
|
restart: unless-stopped
|
Reference in New Issue
Block a user