18 Commits
0.6.1 ... 0.6.2

Author SHA1 Message Date
eddf50483f Fixed some nullable types 2023-05-22 21:44:52 +02:00
a71d65e666 Fix negative sleep time 2023-05-22 21:41:11 +02:00
9a640aed27 Rewrote CoverDownload check if exists. 2023-05-22 21:38:44 +02:00
30b6c4680b Better Rate-Limits
Added Logger to DownloadClient
2023-05-22 21:38:23 +02:00
7b6253de0f Create Publication Folder at start of DownloadNewChapters 2023-05-22 21:37:30 +02:00
5aa3214ce5 TrangaTask.ToString() rewrite for logs-readability.
LogMessages only include class-name without path
2023-05-22 21:37:02 +02:00
9b70994f71 Adjusted RateLimit 2023-05-22 18:55:26 +02:00
93cf341f2d Fixed Publication.InternalId 2023-05-22 18:28:42 +02:00
01cb74c088 First attempt at #18 Rate Limits 2023-05-22 18:15:59 +02:00
ec480dffad Merge pull request 'closes #7' (#17) from Issue_7 into master
Reviewed-on: #17
2023-05-22 17:21:42 +02:00
b7014cbff5 Merge pull request 'fixes #14' (#16) from Issue_14_ChapterIsDownlaoded into master
Reviewed-on: #16
2023-05-22 17:21:19 +02:00
0cab921402 Merge pull request 'fixes #11' (#15) from Issue_11 into master
Reviewed-on: #15
2023-05-22 17:20:54 +02:00
0e0ba1796e closes #7 2023-05-22 17:20:07 +02:00
27d8565dc1 fixes #14 2023-05-22 17:09:47 +02:00
79dc44d707 fixes 11 2023-05-22 17:04:31 +02:00
bb6a0ad0d4 Merge pull request 'fixes #9' (#13) from Issue_9 into master
Reviewed-on: #13
2023-05-22 16:53:40 +02:00
43db463ba6 fixes #9 2023-05-22 16:52:52 +02:00
9eb8ddbc40 Changed Publication:
downloadUrl is now publicationId, internal to Connector
posterUrl is now a URL to the file, instead of an id
2023-05-22 16:45:55 +02:00
8 changed files with 189 additions and 82 deletions

View File

@ -52,7 +52,7 @@ public abstract class LoggerBase : TextWriter
public override string ToString() public override string ToString()
{ {
string dateTimeString = $"{logTime.ToShortDateString()} {logTime.ToLongTimeString()}"; string dateTimeString = $"{logTime.ToShortDateString()} {logTime.ToLongTimeString()}";
return $"[{dateTimeString}] {caller,30} | {value}"; return $"[{dateTimeString}] {caller.Split(new char[]{'.','+'}).Last(),15} | {value}";
} }
} }
} }

View File

@ -209,8 +209,12 @@ public static class Tranga_Cli
$"{"",-5}{"Task",-20} | {"Last Executed",-20} | {"Reoccurrence",-12} | {"State",-10} | {"Connector",-15} | Publication/Manga"; $"{"",-5}{"Task",-20} | {"Last Executed",-20} | {"Reoccurrence",-12} | {"State",-10} | {"Connector",-15} | Publication/Manga";
Console.WriteLine(header); Console.WriteLine(header);
Console.WriteLine(new string('-', header.Length)); Console.WriteLine(new string('-', header.Length));
foreach(TrangaTask trangaTask in tasks) foreach (TrangaTask trangaTask in tasks)
Console.WriteLine($"{tIndex++:000}: {trangaTask}"); {
string[] taskSplit = trangaTask.ToString().Split(", ");
Console.WriteLine($"{tIndex++:000}: {taskSplit[0],-20} | {taskSplit[1],-20} | {taskSplit[2],-12} | {taskSplit[3],-10} | {(taskSplit.Length > 4 ? taskSplit[4] : ""),-15} | {(taskSplit.Length > 5 ? taskSplit[5] : "")}");
}
} }
private static TrangaTask? SelectTask(TrangaTask[] tasks, Logger logger) private static TrangaTask? SelectTask(TrangaTask[] tasks, Logger logger)
@ -244,7 +248,6 @@ public static class Tranga_Cli
try try
{ {
int selectedTaskIndex = Convert.ToInt32(selectedTask); int selectedTaskIndex = Convert.ToInt32(selectedTask);
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
return tasks[selectedTaskIndex]; return tasks[selectedTaskIndex];
} }
catch (Exception e) catch (Exception e)
@ -477,6 +480,13 @@ public static class Tranga_Cli
string? query = Console.ReadLine(); string? query = Console.ReadLine();
Publication[] publications = taskManager.GetPublicationsFromConnector(connector, query ?? ""); Publication[] publications = taskManager.GetPublicationsFromConnector(connector, query ?? "");
if (publications.Length < 1)
{
logger.WriteLine("Tranga_CLI", "No publications returned");
Console.WriteLine($"No publications for query '{query}' returned;");
return null;
}
int pIndex = 0; int pIndex = 0;
Console.WriteLine("Publications:"); Console.WriteLine("Publications:");

View File

@ -12,15 +12,18 @@ namespace Tranga;
public abstract class Connector public abstract class Connector
{ {
internal string downloadLocation { get; } //Location of local files internal string downloadLocation { get; } //Location of local files
protected DownloadClient downloadClient { get; } protected DownloadClient downloadClient { get; init; }
protected Logger? logger; protected Logger? logger;
protected Connector(string downloadLocation, uint downloadDelay, Logger? logger) protected Connector(string downloadLocation, Logger? logger)
{ {
this.downloadLocation = downloadLocation; this.downloadLocation = downloadLocation;
this.downloadClient = new DownloadClient(downloadDelay);
this.logger = logger; this.logger = logger;
this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
{
//RequestTypes for RateLimits
}, logger);
} }
public abstract string name { get; } //Name of the Connector (e.g. Website) public abstract string name { get; } //Name of the Connector (e.g. Website)
@ -85,6 +88,7 @@ public abstract class Connector
new XElement("Tags", string.Join(',',publication.tags)), new XElement("Tags", string.Join(',',publication.tags)),
new XElement("LanguageISO", publication.originalLanguage), new XElement("LanguageISO", publication.originalLanguage),
new XElement("Title", chapter.name), new XElement("Title", chapter.name),
new XElement("Writer", publication.author),
new XElement("Volume", chapter.volumeNumber), new XElement("Volume", chapter.volumeNumber),
new XElement("Number", chapter.chapterNumber) //TODO check if this is correct at some point new XElement("Number", chapter.chapterNumber) //TODO check if this is correct at some point
); );
@ -106,23 +110,24 @@ public abstract class Connector
/// <returns>Filepath</returns> /// <returns>Filepath</returns>
protected string CreateFullFilepath(Publication publication, Chapter chapter) protected string CreateFullFilepath(Publication publication, Chapter chapter)
{ {
return Path.Join(downloadLocation, publication.folderName, chapter.fileName); return Path.Join(downloadLocation, publication.folderName, $"{chapter.fileName}.cbz");
} }
/// <summary> /// <summary>
/// Downloads Image from URL and saves it to the given path(incl. fileName) /// Downloads Image from URL and saves it to the given path(incl. fileName)
/// </summary> /// </summary>
/// <param name="imageUrl"></param> /// <param name="imageUrl"></param>
/// <param name="fullPath"></param> /// <param name="fullPath"></param>
/// <param name="downloadClient">DownloadClient of the connector</param> /// <param name="downloadClient">DownloadClient of the connector</param>
protected static void DownloadImage(string imageUrl, string fullPath, DownloadClient downloadClient) /// <param name="requestType">Requesttype for ratelimit</param>
protected static void DownloadImage(string imageUrl, string fullPath, DownloadClient downloadClient, byte requestType)
{ {
DownloadClient.RequestResult requestResult = downloadClient.MakeRequest(imageUrl); DownloadClient.RequestResult requestResult = downloadClient.MakeRequest(imageUrl, requestType);
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);
} }
/// <summary> /// <summary>
/// Downloads all Images from URLs, Compresses to zip(cbz) and saves. /// Downloads all Images from URLs, Compresses to zip(cbz) and saves.
/// </summary> /// </summary>
@ -130,17 +135,16 @@ public abstract class Connector
/// <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="comicInfoPath">Path of the generate Chapter ComicInfo.xml, if it was generated</param> /// <param name="comicInfoPath">Path of the generate Chapter ComicInfo.xml, if it was generated</param>
protected static void DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, DownloadClient downloadClient, Logger? logger, string? comicInfoPath = null) /// <param name="requestType">RequestType for RateLimits</param>
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");
//Check if Publication Directory already exists //Check if Publication Directory already exists
string[] splitPath = saveArchiveFilePath.Split(Path.DirectorySeparatorChar); string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!;
string directoryPath = Path.Combine(splitPath.Take(splitPath.Length - 1).ToArray());
if (!Directory.Exists(directoryPath)) if (!Directory.Exists(directoryPath))
Directory.CreateDirectory(directoryPath); Directory.CreateDirectory(directoryPath);
string fullPath = $"{saveArchiveFilePath}.cbz"; if (File.Exists(saveArchiveFilePath)) //Don't download twice.
if (File.Exists(fullPath)) //Don't download twice.
return; return;
//Create a temporary folder to store images //Create a temporary folder to store images
@ -152,7 +156,7 @@ public abstract class Connector
{ {
string[] split = imageUrl.Split('.'); string[] split = imageUrl.Split('.');
string extension = split[^1]; string extension = split[^1];
DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), downloadClient); DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), downloadClient, requestType);
} }
if(comicInfoPath is not null) if(comicInfoPath is not null)
@ -160,40 +164,72 @@ public abstract class Connector
logger?.WriteLine("Connector", "Creating archive"); logger?.WriteLine("Connector", "Creating archive");
//ZIP-it and ship-it //ZIP-it and ship-it
ZipFile.CreateFromDirectory(tempFolder, fullPath); ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath);
Directory.Delete(tempFolder, true); //Cleanup Directory.Delete(tempFolder, true); //Cleanup
} }
protected class DownloadClient protected class DownloadClient
{ {
private readonly TimeSpan _requestSpeed;
private DateTime _lastRequest;
private static readonly HttpClient Client = new(); private static readonly HttpClient Client = new();
private readonly Dictionary<byte, DateTime> _lastExecutedRateLimit;
private readonly Dictionary<byte, TimeSpan> _rateLimit;
private Logger? logger;
/// <summary> /// <summary>
/// Creates a httpClient /// Creates a httpClient
/// </summary> /// </summary>
/// <param name="delay">minimum delay between requests (to avoid spam)</param> /// <param name="delay">minimum delay between requests (to avoid spam)</param>
public DownloadClient(uint delay) /// <param name="rateLimitRequestsPerMinute">Rate limits for requests. byte is RequestType, int maximum requests per minute for RequestType</param>
public DownloadClient(Dictionary<byte, int> rateLimitRequestsPerMinute, Logger? logger)
{ {
_requestSpeed = TimeSpan.FromMilliseconds(delay); this.logger = logger;
_lastRequest = DateTime.Now.Subtract(_requestSpeed); _lastExecutedRateLimit = new();
_rateLimit = new();
foreach(KeyValuePair<byte, int> limit in rateLimitRequestsPerMinute)
_rateLimit.Add(limit.Key, TimeSpan.FromMinutes(1).Divide(limit.Value));
} }
/// <summary> /// <summary>
/// Request Webpage /// Request Webpage
/// </summary> /// </summary>
/// <param name="url"></param> /// <param name="url"></param>
/// <param name="requestType">For RateLimits: Same Endpoints use same type</param>
/// <returns>RequestResult with StatusCode and Stream of received data</returns> /// <returns>RequestResult with StatusCode and Stream of received data</returns>
public RequestResult MakeRequest(string url) public RequestResult MakeRequest(string url, byte requestType)
{ {
while((DateTime.Now - _lastRequest) < _requestSpeed) if (_rateLimit.TryGetValue(requestType, out TimeSpan value))
Thread.Sleep(10); _lastExecutedRateLimit.TryAdd(requestType, DateTime.Now.Subtract(value));
_lastRequest = DateTime.Now; else
{
logger?.WriteLine(this.GetType().ToString(), "RequestType not configured for rate-limit.");
return new RequestResult(HttpStatusCode.NotAcceptable, Stream.Null);
}
HttpRequestMessage requestMessage = new(HttpMethod.Get, url); TimeSpan rateLimitTimeout = _rateLimit[requestType]
HttpResponseMessage response = Client.Send(requestMessage); .Subtract(DateTime.Now.Subtract(_lastExecutedRateLimit[requestType]));
if(rateLimitTimeout > TimeSpan.Zero)
Thread.Sleep(rateLimitTimeout);
HttpResponseMessage? response = null;
while (response is null)
{
try
{
HttpRequestMessage requestMessage = new(HttpMethod.Get, url);
_lastExecutedRateLimit[requestType] = DateTime.Now;
response = Client.Send(requestMessage);
}
catch (HttpRequestException e)
{
logger?.WriteLine(this.GetType().ToString(), e.Message);
Thread.Sleep(_rateLimit[requestType] * 2);
}
}
Stream resultString = response.IsSuccessStatusCode ? response.Content.ReadAsStream() : Stream.Null; Stream resultString = response.IsSuccessStatusCode ? response.Content.ReadAsStream() : Stream.Null;
if (!response.IsSuccessStatusCode)
logger?.WriteLine(this.GetType().ToString(), $"Request-Error {response.StatusCode}: {response.ReasonPhrase}");
return new RequestResult(response.StatusCode, resultString); return new RequestResult(response.StatusCode, resultString);
} }

View File

@ -9,14 +9,26 @@ public class MangaDex : Connector
{ {
public override string name { get; } public override string name { get; }
public MangaDex(string downloadLocation, uint downloadDelay, Logger? logger) : base(downloadLocation, downloadDelay, logger) private enum RequestType : byte
{ {
name = "MangaDex"; Manga,
Feed,
AtHomeServer,
Cover,
Author
} }
public MangaDex(string downloadLocation, Logger? logger) : base(downloadLocation, 750, logger) public MangaDex(string downloadLocation, Logger? logger) : base(downloadLocation, logger)
{ {
name = "MangaDex"; name = "MangaDex";
this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
{
{(byte)RequestType.Manga, 250},
{(byte)RequestType.Feed, 250},
{(byte)RequestType.AtHomeServer, 40},
{(byte)RequestType.Cover, 250},
{(byte)RequestType.Author, 250}
}, logger);
} }
public override Publication[] GetPublications(string publicationTitle = "") public override Publication[] GetPublications(string publicationTitle = "")
@ -31,7 +43,7 @@ public class MangaDex : Connector
//Request next Page //Request next Page
DownloadClient.RequestResult requestResult = DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest( downloadClient.MakeRequest(
$"https://api.mangadex.org/manga?limit={limit}&title={publicationTitle}&offset={offset}"); $"https://api.mangadex.org/manga?limit={limit}&title={publicationTitle}&offset={offset}", (byte)RequestType.Manga);
if (requestResult.statusCode != HttpStatusCode.OK) if (requestResult.statusCode != HttpStatusCode.OK)
break; break;
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result); JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
@ -49,6 +61,8 @@ public class MangaDex : Connector
JsonObject manga = (JsonObject)mangeNode!; JsonObject manga = (JsonObject)mangeNode!;
JsonObject attributes = manga["attributes"]!.AsObject(); JsonObject attributes = manga["attributes"]!.AsObject();
string publicationId = manga["id"]!.GetValue<string>();
string title = attributes["title"]!.AsObject().ContainsKey("en") && attributes["title"]!["en"] is not null string title = attributes["title"]!.AsObject().ContainsKey("en") && attributes["title"]!["en"] is not null
? attributes["title"]!["en"]!.GetValue<string>() ? attributes["title"]!["en"]!.GetValue<string>()
: attributes["title"]![((IDictionary<string, JsonNode?>)attributes["title"]!.AsObject()).Keys.First()]!.GetValue<string>(); : attributes["title"]![((IDictionary<string, JsonNode?>)attributes["title"]!.AsObject()).Keys.First()]!.GetValue<string>();
@ -75,12 +89,16 @@ public class MangaDex : Connector
tags.Add(tagObject["attributes"]!["name"]!["en"]!.GetValue<string>()); tags.Add(tagObject["attributes"]!["name"]!["en"]!.GetValue<string>());
} }
string? poster = null; string? posterId = null;
string? authorId = null;
if (manga.ContainsKey("relationships") && manga["relationships"] is not null) if (manga.ContainsKey("relationships") && manga["relationships"] is not null)
{ {
JsonArray relationships = manga["relationships"]!.AsArray(); JsonArray relationships = manga["relationships"]!.AsArray();
poster = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue<string>() == "cover_art")!["id"]!.GetValue<string>(); posterId = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue<string>() == "cover_art")!["id"]!.GetValue<string>();
authorId = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue<string>() == "author")!["id"]!.GetValue<string>();
} }
string? coverUrl = GetCoverUrl(publicationId, posterId);
string? author = GetAuthor(authorId);
Dictionary<string, string> linksDict = new(); Dictionary<string, string> linksDict = new();
if (attributes.ContainsKey("links") && attributes["links"] is not null) if (attributes.ContainsKey("links") && attributes["links"] is not null)
@ -102,17 +120,18 @@ public class MangaDex : Connector
string status = attributes["status"]!.GetValue<string>(); string status = attributes["status"]!.GetValue<string>();
Publication pub = new Publication( Publication pub = new (
title, title,
author,
description, description,
altTitlesDict, altTitlesDict,
tags.ToArray(), tags.ToArray(),
poster, coverUrl,
linksDict, linksDict,
year, year,
originalLanguage, originalLanguage,
status, status,
manga["id"]!.GetValue<string>() publicationId
); );
publications.Add(pub); //Add Publication (Manga) to result publications.Add(pub); //Add Publication (Manga) to result
} }
@ -134,7 +153,7 @@ public class MangaDex : Connector
//Request next "Page" //Request next "Page"
DownloadClient.RequestResult requestResult = DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest( downloadClient.MakeRequest(
$"https://api.mangadex.org/manga/{publication.downloadUrl}/feed?limit={limit}&offset={offset}&translatedLanguage%5B%5D={language}"); $"https://api.mangadex.org/manga/{publication.publicationId}/feed?limit={limit}&offset={offset}&translatedLanguage%5B%5D={language}", (byte)RequestType.Feed);
if (requestResult.statusCode != HttpStatusCode.OK) if (requestResult.statusCode != HttpStatusCode.OK)
break; break;
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result); JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
@ -181,7 +200,7 @@ public class MangaDex : Connector
logger?.WriteLine(this.GetType().ToString(), $"Download Chapter {publication.sortName} {chapter.volumeNumber}-{chapter.chapterNumber}"); logger?.WriteLine(this.GetType().ToString(), $"Download Chapter {publication.sortName} {chapter.volumeNumber}-{chapter.chapterNumber}");
//Request URLs for Chapter-Images //Request URLs for Chapter-Images
DownloadClient.RequestResult requestResult = DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest($"https://api.mangadex.org/at-home/server/{chapter.url}?forcePort443=false'"); downloadClient.MakeRequest($"https://api.mangadex.org/at-home/server/{chapter.url}?forcePort443=false'", (byte)RequestType.AtHomeServer);
if (requestResult.statusCode != HttpStatusCode.OK) if (requestResult.statusCode != HttpStatusCode.OK)
return; return;
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result); JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
@ -200,7 +219,47 @@ public class MangaDex : Connector
File.WriteAllText(comicInfoPath, CreateComicInfo(publication, chapter, logger)); File.WriteAllText(comicInfoPath, CreateComicInfo(publication, chapter, logger));
//Download Chapter-Images //Download Chapter-Images
DownloadChapterImages(imageUrls.ToArray(), CreateFullFilepath(publication, chapter), downloadClient, logger, comicInfoPath); DownloadChapterImages(imageUrls.ToArray(), CreateFullFilepath(publication, chapter), downloadClient, (byte)RequestType.AtHomeServer, logger, comicInfoPath);
}
private string? GetCoverUrl(string publicationId, string? posterId)
{
if (posterId is null)
{
logger?.WriteLine(this.GetType().ToString(), $"No posterId");
return null;
}
//Request information where to download Cover
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest($"https://api.mangadex.org/cover/{posterId}", (byte)RequestType.Cover);
if (requestResult.statusCode != HttpStatusCode.OK)
return null;
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
if (result is null)
return null;
string fileName = result["data"]!["attributes"]!["fileName"]!.GetValue<string>();
string coverUrl = $"https://uploads.mangadex.org/covers/{publicationId}/{fileName}";
return coverUrl;
}
private string? GetAuthor(string? authorId)
{
if (authorId is null)
return null;
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest($"https://api.mangadex.org/author/{authorId}", (byte)RequestType.Author);
if (requestResult.statusCode != HttpStatusCode.OK)
return null;
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
if (result is null)
return null;
string author = result["data"]!["attributes"]!["name"]!.GetValue<string>();
return author;
} }
public override void DownloadCover(Publication publication) public override void DownloadCover(Publication publication)
@ -211,34 +270,26 @@ public class MangaDex : Connector
if(!Directory.Exists(publicationFolder)) if(!Directory.Exists(publicationFolder))
Directory.CreateDirectory(publicationFolder); Directory.CreateDirectory(publicationFolder);
DirectoryInfo dirInfo = new (publicationFolder); DirectoryInfo dirInfo = new (publicationFolder);
foreach(FileInfo fileInfo in dirInfo.EnumerateFiles()) if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover.")))
if (fileInfo.Name.Contains("cover.")) {
{ logger?.WriteLine(this.GetType().ToString(), $"Cover exists {publication.sortName}");
logger?.WriteLine(this.GetType().ToString(), $"Cover exists {publication.sortName}");
return;
}
//Request information where to download Cover
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest($"https://api.mangadex.org/cover/{publication.posterUrl}");
if (requestResult.statusCode != HttpStatusCode.OK)
return; return;
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result); }
if (result is null)
if (publication.posterUrl is null || publication.posterUrl!.Contains("http"))
{
logger?.WriteLine(this.GetType().ToString(), $"No Poster-URL in publication");
return; return;
}
string fileName = result["data"]!["attributes"]!["fileName"]!.GetValue<string>();
string coverUrl = $"https://uploads.mangadex.org/covers/{publication.downloadUrl}/{fileName}";
//Get file-extension (jpg, png) //Get file-extension (jpg, png)
string[] split = coverUrl.Split('.'); string[] split = publication.posterUrl.Split('.');
string extension = split[^1]; string extension = split[^1];
string outFolderPath = Path.Join(downloadLocation, publication.folderName); string outFolderPath = Path.Join(downloadLocation, publication.folderName);
Directory.CreateDirectory(outFolderPath); Directory.CreateDirectory(outFolderPath);
//Download cover-Image //Download cover-Image
DownloadImage(coverUrl, Path.Join(downloadLocation, publication.folderName, $"cover.{extension}"), this.downloadClient); DownloadImage(publication.posterUrl, Path.Join(downloadLocation, publication.folderName, $"cover.{extension}"), this.downloadClient, (byte)RequestType.AtHomeServer);
} }
} }

View File

@ -9,7 +9,7 @@ namespace Tranga;
public readonly struct Publication public readonly struct Publication
{ {
public string sortName { get; } public string sortName { get; }
// ReSharper disable UnusedAutoPropertyAccessor.Global we need it, trust public string? author { get; }
public Dictionary<string,string> altTitles { get; } public Dictionary<string,string> altTitles { get; }
// ReSharper disable trice MemberCanBePrivate.Global, trust // ReSharper disable trice MemberCanBePrivate.Global, trust
public string? description { get; } public string? description { get; }
@ -20,12 +20,13 @@ public readonly struct Publication
public string? originalLanguage { get; } public string? originalLanguage { get; }
public string status { get; } public string status { get; }
public string folderName { get; } public string folderName { get; }
public string downloadUrl { get; } public string publicationId { get; }
[JsonIgnore]public string internalId { get; } public string internalId { get; }
public Publication(string sortName, string? description, Dictionary<string,string> altTitles, string[] tags, string? posterUrl, Dictionary<string,string>? links, int? year, string? originalLanguage, string status, string downloadUrl) 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)
{ {
this.sortName = sortName; this.sortName = sortName;
this.author = author;
this.description = description; this.description = description;
this.altTitles = altTitles; this.altTitles = altTitles;
this.tags = tags; this.tags = tags;
@ -34,10 +35,10 @@ public readonly struct Publication
this.year = year; this.year = year;
this.originalLanguage = originalLanguage; this.originalLanguage = originalLanguage;
this.status = status; this.status = status;
this.downloadUrl = downloadUrl; this.publicationId = publicationId;
this.folderName = string.Concat(sortName.Split(Path.GetInvalidPathChars().Concat(Path.GetInvalidFileNameChars()).ToArray())); this.folderName = string.Concat(sortName.Split(Path.GetInvalidPathChars().Concat(Path.GetInvalidFileNameChars()).ToArray()));
string onlyLowerAscii = this.sortName.ToLower().Where(Char.IsAscii).ToString()!; string onlyLowerLetters = string.Concat(this.sortName.ToLower().Where(Char.IsLetter));
this.internalId = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{onlyLowerAscii}{this.year}")); this.internalId = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{onlyLowerLetters}{this.year}"));
} }
/// <returns>Serialized JSON String for series.json</returns> /// <returns>Serialized JSON String for series.json</returns>

View File

@ -92,13 +92,13 @@ public static class TaskExecutor
/// <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)
{ {
List<Chapter> newChapters = UpdateChapters(connector, publication, language, ref chapterCollection); //Check if Publication already has a Folder
connector.DownloadCover(publication);
//Check if Publication already has a Folder and a series.json
string publicationFolder = Path.Join(connector.downloadLocation, publication.folderName); string publicationFolder = Path.Join(connector.downloadLocation, publication.folderName);
if(!Directory.Exists(publicationFolder)) if(!Directory.Exists(publicationFolder))
Directory.CreateDirectory(publicationFolder); Directory.CreateDirectory(publicationFolder);
List<Chapter> newChapters = UpdateChapters(connector, publication, language, ref chapterCollection);
connector.DownloadCover(publication);
string seriesInfoPath = Path.Join(publicationFolder, "series.json"); string seriesInfoPath = Path.Join(publicationFolder, "series.json");
if(!File.Exists(seriesInfoPath)) if(!File.Exists(seriesInfoPath))

View File

@ -25,7 +25,7 @@ public class TaskManager
/// <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 = null, string? komgaBaseUrl = null, string? komgaUsername = null, string? komgaPassword = null, Logger? logger = null) public TaskManager(string downloadFolderPath, string workingDirectory, 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>();
@ -163,12 +163,14 @@ public class TaskManager
//Check if same task already exists //Check if same task already exists
if (!_allTasks.Any(trangaTask => trangaTask.task == task && trangaTask.connectorName == connector.name && if (!_allTasks.Any(trangaTask => trangaTask.task == task && trangaTask.connectorName == connector.name &&
trangaTask.publication?.downloadUrl == publication?.downloadUrl)) trangaTask.publication?.internalId == publication?.internalId))
{ {
if(task != TrangaTask.Task.UpdatePublications) if(task != TrangaTask.Task.UpdatePublications)
_chapterCollection.TryAdd((Publication)publication!, new List<Chapter>()); _chapterCollection.TryAdd((Publication)publication!, new List<Chapter>());
_allTasks.Add(newTask); _allTasks.Add(newTask);
} }
else
logger?.WriteLine(this.GetType().ToString(), $"Publication already exists {publication?.internalId}");
} }
logger?.WriteLine(this.GetType().ToString(), $"Added new Task {newTask.ToString()}"); logger?.WriteLine(this.GetType().ToString(), $"Added new Task {newTask.ToString()}");
ExportData(); ExportData();
@ -188,18 +190,25 @@ public class TaskManager
if (task == TrangaTask.Task.UpdateKomgaLibrary) if (task == TrangaTask.Task.UpdateKomgaLibrary)
{ {
_allTasks.RemoveWhere(uTask => uTask.task == TrangaTask.Task.UpdateKomgaLibrary); _allTasks.RemoveWhere(uTask => uTask.task == TrangaTask.Task.UpdateKomgaLibrary);
logger?.WriteLine(this.GetType().ToString(), $"Removed Task {task}"); logger?.WriteLine(this.GetType().ToString(), $"Removed Task {task} from all Tasks.");
} }
else if (connectorName is null) else if (connectorName is null)
throw new ArgumentException($"connectorName can not be null for Task {task}"); throw new ArgumentException($"connectorName can not be null for Task {task}");
else else
{ {
foreach (List<TrangaTask> taskQueue in this._taskQueue.Values)
if(taskQueue.RemoveAll(trangaTask =>
trangaTask.task == task && trangaTask.connectorName == connectorName &&
trangaTask.publication?.internalId == publication?.internalId) > 0)
logger?.WriteLine(this.GetType().ToString(), $"Removed Task {task} {publication?.sortName} {publication?.internalId} from Queue.");
else
logger?.WriteLine(this.GetType().ToString(), $"Task {task} {publication?.sortName} {publication?.internalId} was not in Queue.");
if(_allTasks.RemoveWhere(trangaTask => if(_allTasks.RemoveWhere(trangaTask =>
trangaTask.task == task && trangaTask.connectorName == connectorName && trangaTask.task == task && trangaTask.connectorName == connectorName &&
trangaTask.publication?.downloadUrl == publication?.downloadUrl) > 0) trangaTask.publication?.internalId == publication?.internalId) > 0)
logger?.WriteLine(this.GetType().ToString(), $"Removed Task {task} {publication?.sortName} {publication?.downloadUrl}."); logger?.WriteLine(this.GetType().ToString(), $"Removed Task {task} {publication?.sortName} {publication?.internalId} from all Tasks.");
else else
logger?.WriteLine(this.GetType().ToString(), $"No Task {task} {publication?.sortName} {publication?.downloadUrl} could be found."); logger?.WriteLine(this.GetType().ToString(), $"No Task {task} {publication?.sortName} {publication?.internalId} could be found.");
} }
ExportData(); ExportData();
} }

View File

@ -56,6 +56,6 @@ public class TrangaTask
public override string ToString() public override string ToString()
{ {
return $"{task,-20} | {lastExecuted,-20} | {reoccurrence,-12} | {state,-10} | {connectorName,-15} | {publication?.sortName}"; return $"{task}, {lastExecuted}, {reoccurrence}, {state} {(connectorName is not null ? $", {connectorName}" : "" )} {(publication is not null ? $", {publication?.sortName}": "")}";
} }
} }