2023-05-18 16:42:00 +02:00
|
|
|
|
using System.IO.Compression;
|
|
|
|
|
using System.Net;
|
2023-06-01 18:28:58 +02:00
|
|
|
|
using System.Runtime.InteropServices;
|
2023-06-03 22:25:24 +02:00
|
|
|
|
using System.Text.RegularExpressions;
|
2023-05-20 00:19:04 +02:00
|
|
|
|
using System.Xml.Linq;
|
2023-05-20 22:10:24 +02:00
|
|
|
|
using Logging;
|
2023-06-05 00:35:57 +02:00
|
|
|
|
using Tranga.TrangaTasks;
|
2023-06-01 18:28:58 +02:00
|
|
|
|
using static System.IO.UnixFileMode;
|
2023-05-18 12:26:15 +02:00
|
|
|
|
|
|
|
|
|
namespace Tranga;
|
|
|
|
|
|
2023-05-19 19:52:24 +02:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Base-Class for all Connectors
|
|
|
|
|
/// Provides some methods to be used by all Connectors, as well as a DownloadClient
|
|
|
|
|
/// </summary>
|
2023-05-18 12:26:15 +02:00
|
|
|
|
public abstract class Connector
|
|
|
|
|
{
|
2023-05-19 19:50:26 +02:00
|
|
|
|
internal string downloadLocation { get; } //Location of local files
|
2023-05-22 18:15:24 +02:00
|
|
|
|
protected DownloadClient downloadClient { get; init; }
|
2023-05-19 19:50:26 +02:00
|
|
|
|
|
2023-05-31 20:29:30 +02:00
|
|
|
|
protected readonly Logger? logger;
|
2023-05-20 22:10:24 +02:00
|
|
|
|
|
2023-05-31 20:29:30 +02:00
|
|
|
|
protected readonly string imageCachePath;
|
2023-05-25 14:23:33 +02:00
|
|
|
|
|
|
|
|
|
protected Connector(string downloadLocation, string imageCachePath, Logger? logger)
|
2023-05-18 18:51:19 +02:00
|
|
|
|
{
|
|
|
|
|
this.downloadLocation = downloadLocation;
|
2023-05-20 22:10:24 +02:00
|
|
|
|
this.logger = logger;
|
2023-05-22 21:44:52 +02:00
|
|
|
|
this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
|
|
|
|
|
{
|
|
|
|
|
//RequestTypes for RateLimits
|
|
|
|
|
}, logger);
|
2023-05-25 14:23:33 +02:00
|
|
|
|
this.imageCachePath = imageCachePath;
|
2023-05-31 21:39:18 +02:00
|
|
|
|
if (!Directory.Exists(imageCachePath))
|
|
|
|
|
Directory.CreateDirectory(this.imageCachePath);
|
2023-05-18 18:51:19 +02:00
|
|
|
|
}
|
|
|
|
|
|
2023-05-19 19:52:24 +02:00
|
|
|
|
public abstract string name { get; } //Name of the Connector (e.g. Website)
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Returns all Publications with the given string.
|
|
|
|
|
/// If the string is empty or null, returns all Publication of the Connector
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="publicationTitle">Search-Query</param>
|
|
|
|
|
/// <returns>Publications matching the query</returns>
|
2023-05-18 16:41:14 +02:00
|
|
|
|
public abstract Publication[] GetPublications(string publicationTitle = "");
|
2023-05-19 19:52:24 +02:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Returns all Chapters of the publication in the provided language.
|
|
|
|
|
/// If the language is empty or null, returns all Chapters in all Languages.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="publication">Publication to get Chapters for</param>
|
|
|
|
|
/// <param name="language">Language of the Chapters</param>
|
|
|
|
|
/// <returns>Array of Chapters matching Publication and Language</returns>
|
2023-05-18 18:19:04 +02:00
|
|
|
|
public abstract Chapter[] GetChapters(Publication publication, string language = "");
|
2023-06-09 11:06:18 +02:00
|
|
|
|
|
|
|
|
|
public Chapter[] SearchChapters(Publication publication, string searchTerm, string? language = null)
|
|
|
|
|
{
|
|
|
|
|
Chapter[] availableChapters = this.GetChapters(publication, language??"en");
|
|
|
|
|
Regex volumeRegex = new ("((v(ol)*(olume)*)+ *([0-9]+(-[0-9]+)?){1})", RegexOptions.IgnoreCase);
|
|
|
|
|
Regex chapterRegex = new ("((c(h)*(hapter)*)+ *([0-9]+(-[0-9]+)?){1})", RegexOptions.IgnoreCase);
|
|
|
|
|
Regex singleResultRegex = new("([0-9]+)", RegexOptions.IgnoreCase);
|
|
|
|
|
Regex rangeResultRegex = new("([0-9]+(-[0-9]+))", RegexOptions.IgnoreCase);
|
|
|
|
|
if (volumeRegex.IsMatch(searchTerm) && chapterRegex.IsMatch(searchTerm))
|
|
|
|
|
{
|
|
|
|
|
string volume = singleResultRegex.Match(volumeRegex.Match(searchTerm).Value).Value;
|
|
|
|
|
string chapter = singleResultRegex.Match(chapterRegex.Match(searchTerm).Value).Value;
|
|
|
|
|
return availableChapters.Where(aCh => aCh.volumeNumber is not null && aCh.chapterNumber is not null &&
|
|
|
|
|
aCh.volumeNumber.Equals(volume, StringComparison.InvariantCultureIgnoreCase) &&
|
|
|
|
|
aCh.chapterNumber.Equals(chapter, StringComparison.InvariantCultureIgnoreCase))
|
|
|
|
|
.ToArray();
|
|
|
|
|
}
|
|
|
|
|
else if (volumeRegex.IsMatch(searchTerm))
|
|
|
|
|
{
|
|
|
|
|
string volume = volumeRegex.Match(searchTerm).Value;
|
|
|
|
|
if (rangeResultRegex.IsMatch(volume))
|
|
|
|
|
{
|
|
|
|
|
string range = rangeResultRegex.Match(volume).Value;
|
|
|
|
|
int start = Convert.ToInt32(range.Split('-')[0]);
|
|
|
|
|
int end = Convert.ToInt32(range.Split('-')[1]);
|
|
|
|
|
return availableChapters.Where(aCh => aCh.volumeNumber is not null &&
|
|
|
|
|
Convert.ToInt32(aCh.volumeNumber) >= start &&
|
|
|
|
|
Convert.ToInt32(aCh.volumeNumber) <= end).ToArray();
|
|
|
|
|
}
|
2023-06-11 19:17:03 +02:00
|
|
|
|
else if (singleResultRegex.IsMatch(volume))
|
|
|
|
|
{
|
|
|
|
|
string volumeNumber = singleResultRegex.Match(volume).Value;
|
2023-06-09 11:06:18 +02:00
|
|
|
|
return availableChapters.Where(aCh =>
|
|
|
|
|
aCh.volumeNumber is not null &&
|
2023-06-11 19:17:03 +02:00
|
|
|
|
aCh.volumeNumber.Equals(volumeNumber, StringComparison.InvariantCultureIgnoreCase)).ToArray();
|
|
|
|
|
}
|
2023-06-09 11:06:18 +02:00
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
else if (chapterRegex.IsMatch(searchTerm))
|
|
|
|
|
{
|
|
|
|
|
string chapter = volumeRegex.Match(searchTerm).Value;
|
|
|
|
|
if (rangeResultRegex.IsMatch(chapter))
|
|
|
|
|
{
|
|
|
|
|
string range = rangeResultRegex.Match(chapter).Value;
|
|
|
|
|
int start = Convert.ToInt32(range.Split('-')[0]);
|
|
|
|
|
int end = Convert.ToInt32(range.Split('-')[1]);
|
|
|
|
|
return availableChapters.Where(aCh => aCh.chapterNumber is not null &&
|
|
|
|
|
Convert.ToInt32(aCh.chapterNumber) >= start &&
|
|
|
|
|
Convert.ToInt32(aCh.chapterNumber) <= end).ToArray();
|
|
|
|
|
}
|
2023-06-11 19:17:03 +02:00
|
|
|
|
else if (singleResultRegex.IsMatch(chapter))
|
|
|
|
|
{
|
|
|
|
|
string chapterNumber = singleResultRegex.Match(chapter).Value;
|
2023-06-09 11:06:18 +02:00
|
|
|
|
return availableChapters.Where(aCh =>
|
|
|
|
|
aCh.chapterNumber is not null &&
|
2023-06-11 19:17:03 +02:00
|
|
|
|
aCh.chapterNumber.Equals(chapterNumber, StringComparison.InvariantCultureIgnoreCase)).ToArray();
|
|
|
|
|
}
|
2023-06-09 11:06:18 +02:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (rangeResultRegex.IsMatch(searchTerm))
|
|
|
|
|
{
|
|
|
|
|
int start = Convert.ToInt32(searchTerm.Split('-')[0]);
|
|
|
|
|
int end = Convert.ToInt32(searchTerm.Split('-')[1]);
|
|
|
|
|
return availableChapters[start..(end + 1)];
|
|
|
|
|
}
|
|
|
|
|
else if(singleResultRegex.IsMatch(searchTerm))
|
|
|
|
|
return new [] { availableChapters[Convert.ToInt32(searchTerm)] };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Array.Empty<Chapter>();
|
|
|
|
|
}
|
2023-05-19 19:52:24 +02:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Retrieves the Chapter (+Images) from the website.
|
2023-05-31 20:39:23 +02:00
|
|
|
|
/// Should later call DownloadChapterImages to retrieve the individual Images of the Chapter and create .cbz archive.
|
2023-05-19 19:52:24 +02:00
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="publication">Publication that contains Chapter</param>
|
|
|
|
|
/// <param name="chapter">Chapter with Images to retrieve</param>
|
2023-06-01 22:05:48 +02:00
|
|
|
|
/// <param name="parentTask">Will be used for progress-tracking</param>
|
2023-06-10 14:27:09 +02:00
|
|
|
|
/// <param name="cancellationToken"></param>
|
2023-06-19 22:45:33 +02:00
|
|
|
|
public abstract bool DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null);
|
2023-05-26 14:07:11 +02:00
|
|
|
|
|
2023-05-19 19:52:24 +02:00
|
|
|
|
/// <summary>
|
2023-05-31 20:39:57 +02:00
|
|
|
|
/// Copies the already downloaded cover from cache to downloadLocation
|
2023-05-19 19:52:24 +02:00
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="publication">Publication to retrieve Cover for</param>
|
2023-05-26 14:07:11 +02:00
|
|
|
|
/// <param name="settings">TrangaSettings</param>
|
2023-05-31 20:39:57 +02:00
|
|
|
|
public void CopyCoverFromCacheToDownloadLocation(Publication publication, TrangaSettings settings)
|
2023-05-19 19:52:24 +02:00
|
|
|
|
{
|
2023-06-11 19:05:08 +02:00
|
|
|
|
logger?.WriteLine(this.GetType().ToString(), $"Cloning cover {publication.sortName} -> {publication.internalId}");
|
2023-05-31 20:29:30 +02:00
|
|
|
|
//Check if Publication already has a Folder and cover
|
2023-06-01 22:59:04 +02:00
|
|
|
|
string publicationFolder = publication.CreatePublicationFolder(downloadLocation);
|
2023-05-31 20:29:30 +02:00
|
|
|
|
DirectoryInfo dirInfo = new (publicationFolder);
|
2023-06-11 19:05:08 +02:00
|
|
|
|
if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover", StringComparison.InvariantCultureIgnoreCase)))
|
2023-05-31 20:29:30 +02:00
|
|
|
|
{
|
|
|
|
|
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);
|
2023-06-01 22:32:11 +02:00
|
|
|
|
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
|
|
|
|
File.SetUnixFileMode(newFilePath, GroupRead | GroupWrite | OtherRead | OtherWrite | UserRead | UserWrite);
|
2023-05-19 19:52:24 +02:00
|
|
|
|
}
|
2023-05-20 00:19:04 +02:00
|
|
|
|
|
2023-05-20 15:05:41 +02:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Creates a string containing XML of publication and chapter.
|
|
|
|
|
/// See ComicInfo.xml
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns>XML-string</returns>
|
2023-05-31 20:29:30 +02:00
|
|
|
|
protected static string GetComicInfoXmlString(Publication publication, Chapter chapter, Logger? logger)
|
2023-05-20 00:19:04 +02:00
|
|
|
|
{
|
2023-05-26 15:09:26 +02:00
|
|
|
|
logger?.WriteLine("Connector", $"Creating ComicInfo.Xml for {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
|
2023-05-20 00:19:04 +02:00
|
|
|
|
XElement comicInfo = new XElement("ComicInfo",
|
|
|
|
|
new XElement("Tags", string.Join(',',publication.tags)),
|
|
|
|
|
new XElement("LanguageISO", publication.originalLanguage),
|
2023-05-20 02:29:54 +02:00
|
|
|
|
new XElement("Title", chapter.name),
|
2023-06-10 14:05:23 +02:00
|
|
|
|
new XElement("Writer", string.Join(',', publication.authors)),
|
2023-05-20 02:29:54 +02:00
|
|
|
|
new XElement("Volume", chapter.volumeNumber),
|
2023-06-01 22:06:10 +02:00
|
|
|
|
new XElement("Number", chapter.chapterNumber)
|
2023-05-20 00:19:04 +02:00
|
|
|
|
);
|
|
|
|
|
return comicInfo.ToString();
|
|
|
|
|
}
|
2023-05-20 01:30:23 +02:00
|
|
|
|
|
2023-05-20 15:05:41 +02:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Checks if a chapter-archive is already present
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns>true if chapter is present</returns>
|
2023-05-31 20:29:30 +02:00
|
|
|
|
public bool CheckChapterIsDownloaded(Publication publication, Chapter chapter)
|
2023-05-20 01:30:23 +02:00
|
|
|
|
{
|
2023-06-03 22:25:24 +02:00
|
|
|
|
Regex legalCharacters = new Regex(@"([A-z]*[0-9]* *\.*-*,*\]*\[*'*\'*\)*\(*~*!*)*");
|
|
|
|
|
string oldFilePath = Path.Join(downloadLocation, publication.folderName, $"{string.Concat(legalCharacters.Matches(chapter.name ?? ""))} - V{chapter.volumeNumber}C{chapter.chapterNumber} - {chapter.sortNumber}.cbz");
|
2023-06-03 22:39:27 +02:00
|
|
|
|
string oldFilePath2 = Path.Join(downloadLocation, publication.folderName, $"{string.Concat(legalCharacters.Matches(chapter.name ?? ""))} - VC{chapter.chapterNumber} - {chapter.chapterNumber}.cbz");
|
2023-06-03 22:25:24 +02:00
|
|
|
|
string newFilePath = GetArchiveFilePath(publication, chapter);
|
|
|
|
|
if (File.Exists(oldFilePath))
|
|
|
|
|
File.Move(oldFilePath, newFilePath);
|
2023-06-03 22:55:53 +02:00
|
|
|
|
else if (File.Exists(oldFilePath2))
|
2023-06-03 22:25:24 +02:00
|
|
|
|
File.Move(oldFilePath2, newFilePath);
|
|
|
|
|
return File.Exists(newFilePath);
|
2023-05-20 01:30:23 +02:00
|
|
|
|
}
|
|
|
|
|
|
2023-05-20 15:05:41 +02:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Creates full file path of chapter-archive
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns>Filepath</returns>
|
2023-05-31 20:29:30 +02:00
|
|
|
|
protected string GetArchiveFilePath(Publication publication, Chapter chapter)
|
2023-05-20 01:30:23 +02:00
|
|
|
|
{
|
2023-06-03 22:25:24 +02:00
|
|
|
|
return Path.Join(downloadLocation, publication.folderName, $"{publication.folderName} - {chapter.fileName}.cbz");
|
2023-05-20 01:30:23 +02:00
|
|
|
|
}
|
2023-05-22 18:15:24 +02:00
|
|
|
|
|
2023-05-19 20:22:13 +02:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Downloads Image from URL and saves it to the given path(incl. fileName)
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="imageUrl"></param>
|
|
|
|
|
/// <param name="fullPath"></param>
|
2023-05-31 21:18:41 +02:00
|
|
|
|
/// <param name="requestType">RequestType for Rate-Limit</param>
|
2023-06-01 22:05:48 +02:00
|
|
|
|
/// <param name="referrer">referrer used in html request header</param>
|
2023-06-19 22:45:33 +02:00
|
|
|
|
private bool DownloadImage(string imageUrl, string fullPath, byte requestType, string? referrer = null)
|
2023-05-19 19:44:59 +02:00
|
|
|
|
{
|
2023-06-01 13:13:53 +02:00
|
|
|
|
DownloadClient.RequestResult requestResult = downloadClient.MakeRequest(imageUrl, requestType, referrer);
|
2023-06-19 22:45:33 +02:00
|
|
|
|
if (!requestResult.success || requestResult.result == Stream.Null)
|
|
|
|
|
return false;
|
|
|
|
|
byte[] buffer = new byte[requestResult.result.Length];
|
|
|
|
|
requestResult.result.ReadExactly(buffer, 0, buffer.Length);
|
|
|
|
|
File.WriteAllBytes(fullPath, buffer);
|
|
|
|
|
return true;
|
2023-05-19 19:44:59 +02:00
|
|
|
|
}
|
2023-05-22 18:15:24 +02:00
|
|
|
|
|
2023-05-19 20:22:13 +02:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Downloads all Images from URLs, Compresses to zip(cbz) and saves.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="imageUrls">List of URLs to download Images from</param>
|
|
|
|
|
/// <param name="saveArchiveFilePath">Full path to save archive to (without file ending .cbz)</param>
|
2023-06-01 22:05:48 +02:00
|
|
|
|
/// <param name="parentTask">Used for progress tracking</param>
|
2023-05-20 01:06:12 +02:00
|
|
|
|
/// <param name="comicInfoPath">Path of the generate Chapter ComicInfo.xml, if it was generated</param>
|
2023-05-22 18:15:24 +02:00
|
|
|
|
/// <param name="requestType">RequestType for RateLimits</param>
|
2023-06-01 22:05:48 +02:00
|
|
|
|
/// <param name="referrer">Used in http request header</param>
|
2023-06-19 22:45:33 +02:00
|
|
|
|
protected bool DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, byte requestType, DownloadChapterTask parentTask, string? comicInfoPath = null, string? referrer = null, CancellationToken? cancellationToken = null)
|
2023-05-18 16:21:02 +02:00
|
|
|
|
{
|
2023-06-10 14:27:09 +02:00
|
|
|
|
if (cancellationToken?.IsCancellationRequested??false)
|
2023-06-19 22:45:33 +02:00
|
|
|
|
return false;
|
2023-05-26 15:09:26 +02:00
|
|
|
|
logger?.WriteLine("Connector", $"Downloading Images for {saveArchiveFilePath}");
|
2023-05-19 20:22:13 +02:00
|
|
|
|
//Check if Publication Directory already exists
|
2023-05-22 17:09:47 +02:00
|
|
|
|
string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!;
|
2023-05-19 18:20:26 +02:00
|
|
|
|
if (!Directory.Exists(directoryPath))
|
|
|
|
|
Directory.CreateDirectory(directoryPath);
|
2023-05-22 17:09:47 +02:00
|
|
|
|
|
|
|
|
|
if (File.Exists(saveArchiveFilePath)) //Don't download twice.
|
2023-06-19 22:45:33 +02:00
|
|
|
|
return false;
|
2023-05-19 18:20:26 +02:00
|
|
|
|
|
2023-05-19 20:22:13 +02:00
|
|
|
|
//Create a temporary folder to store images
|
2023-05-19 23:00:45 +02:00
|
|
|
|
string tempFolder = Directory.CreateTempSubdirectory().FullName;
|
2023-05-18 17:21:06 +02:00
|
|
|
|
|
|
|
|
|
int chapter = 0;
|
2023-05-19 20:22:13 +02:00
|
|
|
|
//Download all Images to temporary Folder
|
2023-05-18 17:42:02 +02:00
|
|
|
|
foreach (string imageUrl in imageUrls)
|
|
|
|
|
{
|
|
|
|
|
string[] split = imageUrl.Split('.');
|
2023-05-20 01:06:00 +02:00
|
|
|
|
string extension = split[^1];
|
2023-06-05 00:35:57 +02:00
|
|
|
|
logger?.WriteLine("Connector", $"Downloading Image {chapter + 1:000}/{imageUrls.Length:000} {parentTask.publication.sortName} {parentTask.publication.internalId} Vol.{parentTask.chapter.volumeNumber} Ch.{parentTask.chapter.chapterNumber} {parentTask.progress:P2}");
|
2023-06-19 22:45:33 +02:00
|
|
|
|
if (!DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), requestType, referrer))
|
|
|
|
|
return false;
|
2023-06-10 15:58:11 +02:00
|
|
|
|
parentTask.IncrementProgress(1.0 / imageUrls.Length);
|
2023-06-10 14:27:09 +02:00
|
|
|
|
if (cancellationToken?.IsCancellationRequested??false)
|
2023-06-19 22:45:33 +02:00
|
|
|
|
return false;
|
2023-05-18 17:42:02 +02:00
|
|
|
|
}
|
2023-05-19 18:20:26 +02:00
|
|
|
|
|
2023-05-20 00:19:04 +02:00
|
|
|
|
if(comicInfoPath is not null)
|
|
|
|
|
File.Copy(comicInfoPath, Path.Join(tempFolder, "ComicInfo.xml"));
|
|
|
|
|
|
2023-05-26 15:09:26 +02:00
|
|
|
|
logger?.WriteLine("Connector", $"Creating archive {saveArchiveFilePath}");
|
2023-05-19 20:22:13 +02:00
|
|
|
|
//ZIP-it and ship-it
|
2023-05-22 17:09:47 +02:00
|
|
|
|
ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath);
|
2023-06-01 18:28:58 +02:00
|
|
|
|
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
|
|
|
|
File.SetUnixFileMode(saveArchiveFilePath, GroupRead | GroupWrite | OtherRead | OtherWrite | UserRead | UserWrite);
|
2023-05-19 20:55:19 +02:00
|
|
|
|
Directory.Delete(tempFolder, true); //Cleanup
|
2023-06-19 22:45:33 +02:00
|
|
|
|
return true;
|
2023-05-18 16:21:02 +02:00
|
|
|
|
}
|
2023-05-19 19:52:24 +02:00
|
|
|
|
|
2023-06-01 13:13:53 +02:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-19 19:44:59 +02:00
|
|
|
|
protected class DownloadClient
|
2023-05-18 12:26:15 +02:00
|
|
|
|
{
|
2023-05-18 18:55:11 +02:00
|
|
|
|
private static readonly HttpClient Client = new();
|
2023-05-18 17:18:41 +02:00
|
|
|
|
|
2023-05-22 18:15:24 +02:00
|
|
|
|
private readonly Dictionary<byte, DateTime> _lastExecutedRateLimit;
|
2023-05-22 21:38:23 +02:00
|
|
|
|
private readonly Dictionary<byte, TimeSpan> _rateLimit;
|
|
|
|
|
private Logger? logger;
|
2023-05-22 18:15:24 +02:00
|
|
|
|
|
2023-05-19 20:22:13 +02:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Creates a httpClient
|
|
|
|
|
/// </summary>
|
2023-05-22 18:15:24 +02:00
|
|
|
|
/// <param name="rateLimitRequestsPerMinute">Rate limits for requests. byte is RequestType, int maximum requests per minute for RequestType</param>
|
2023-05-31 21:18:41 +02:00
|
|
|
|
/// <param name="logger"></param>
|
2023-05-22 21:38:23 +02:00
|
|
|
|
public DownloadClient(Dictionary<byte, int> rateLimitRequestsPerMinute, Logger? logger)
|
2023-05-18 17:18:41 +02:00
|
|
|
|
{
|
2023-05-22 21:38:23 +02:00
|
|
|
|
this.logger = logger;
|
2023-05-22 18:15:24 +02:00
|
|
|
|
_lastExecutedRateLimit = new();
|
2023-05-22 21:38:23 +02:00
|
|
|
|
_rateLimit = new();
|
2023-05-22 18:15:24 +02:00
|
|
|
|
foreach(KeyValuePair<byte, int> limit in rateLimitRequestsPerMinute)
|
2023-05-22 21:38:23 +02:00
|
|
|
|
_rateLimit.Add(limit.Key, TimeSpan.FromMinutes(1).Divide(limit.Value));
|
2023-05-18 17:18:41 +02:00
|
|
|
|
}
|
2023-05-22 18:15:24 +02:00
|
|
|
|
|
2023-05-19 20:22:13 +02:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Request Webpage
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="url"></param>
|
2023-05-22 18:15:24 +02:00
|
|
|
|
/// <param name="requestType">For RateLimits: Same Endpoints use same type</param>
|
2023-06-01 22:05:48 +02:00
|
|
|
|
/// <param name="referrer">Used in http request header</param>
|
2023-05-19 20:22:13 +02:00
|
|
|
|
/// <returns>RequestResult with StatusCode and Stream of received data</returns>
|
2023-06-01 13:13:53 +02:00
|
|
|
|
public RequestResult MakeRequest(string url, byte requestType, string? referrer = null)
|
2023-05-18 12:26:15 +02:00
|
|
|
|
{
|
2023-05-22 21:38:23 +02:00
|
|
|
|
if (_rateLimit.TryGetValue(requestType, out TimeSpan value))
|
2023-05-22 18:15:24 +02:00
|
|
|
|
_lastExecutedRateLimit.TryAdd(requestType, DateTime.Now.Subtract(value));
|
|
|
|
|
else
|
2023-05-22 21:38:23 +02:00
|
|
|
|
{
|
|
|
|
|
logger?.WriteLine(this.GetType().ToString(), "RequestType not configured for rate-limit.");
|
2023-06-19 22:45:33 +02:00
|
|
|
|
return new RequestResult(false, Stream.Null);
|
2023-05-22 21:38:23 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TimeSpan rateLimitTimeout = _rateLimit[requestType]
|
|
|
|
|
.Subtract(DateTime.Now.Subtract(_lastExecutedRateLimit[requestType]));
|
2023-05-22 18:15:24 +02:00
|
|
|
|
|
2023-05-22 21:41:11 +02:00
|
|
|
|
if(rateLimitTimeout > TimeSpan.Zero)
|
|
|
|
|
Thread.Sleep(rateLimitTimeout);
|
2023-05-18 17:41:44 +02:00
|
|
|
|
|
2023-05-22 21:38:23 +02:00
|
|
|
|
HttpResponseMessage? response = null;
|
|
|
|
|
while (response is null)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
HttpRequestMessage requestMessage = new(HttpMethod.Get, url);
|
2023-06-01 13:13:53 +02:00
|
|
|
|
if(referrer is not null)
|
|
|
|
|
requestMessage.Headers.Referrer = new Uri(referrer);
|
2023-05-22 21:38:23 +02:00
|
|
|
|
_lastExecutedRateLimit[requestType] = DateTime.Now;
|
|
|
|
|
response = Client.Send(requestMessage);
|
|
|
|
|
}
|
|
|
|
|
catch (HttpRequestException e)
|
|
|
|
|
{
|
2023-05-22 21:44:52 +02:00
|
|
|
|
logger?.WriteLine(this.GetType().ToString(), e.Message);
|
2023-06-01 21:16:57 +02:00
|
|
|
|
logger?.WriteLine(this.GetType().ToString(), $"Waiting {_rateLimit[requestType] * 2}... Retrying.");
|
2023-05-22 21:38:23 +02:00
|
|
|
|
Thread.Sleep(_rateLimit[requestType] * 2);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!response.IsSuccessStatusCode)
|
2023-06-19 22:45:33 +02:00
|
|
|
|
{
|
2023-05-22 21:38:23 +02:00
|
|
|
|
logger?.WriteLine(this.GetType().ToString(), $"Request-Error {response.StatusCode}: {response.ReasonPhrase}");
|
2023-06-19 22:45:33 +02:00
|
|
|
|
return new RequestResult(false, Stream.Null);
|
|
|
|
|
}
|
|
|
|
|
return new RequestResult(true, response.Content.ReadAsStream());
|
2023-05-18 12:26:15 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public struct RequestResult
|
|
|
|
|
{
|
2023-06-19 22:45:33 +02:00
|
|
|
|
public bool success { get; }
|
2023-05-18 12:26:15 +02:00
|
|
|
|
public Stream result { get; }
|
|
|
|
|
|
2023-06-19 22:45:33 +02:00
|
|
|
|
public RequestResult(bool success, Stream result)
|
2023-05-18 12:26:15 +02:00
|
|
|
|
{
|
2023-06-19 22:45:33 +02:00
|
|
|
|
this.success = success;
|
2023-05-18 12:26:15 +02:00
|
|
|
|
this.result = result;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|