using System.IO.Compression; using System.Net; using System.Xml.Linq; namespace Tranga; /// <summary> /// Base-Class for all Connectors /// Provides some methods to be used by all Connectors, as well as a DownloadClient /// </summary> public abstract class Connector { internal string downloadLocation { get; } //Location of local files protected DownloadClient downloadClient { get; } protected Connector(string downloadLocation, uint downloadDelay) { this.downloadLocation = downloadLocation; this.downloadClient = new DownloadClient(downloadDelay); } 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> public abstract Publication[] GetPublications(string publicationTitle = ""); /// <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> public abstract Chapter[] GetChapters(Publication publication, string language = ""); /// <summary> /// Retrieves the Chapter (+Images) from the website. /// Should later call DownloadChapterImages to retrieve the individual Images of the Chapter. /// </summary> /// <param name="publication">Publication that contains Chapter</param> /// <param name="chapter">Chapter with Images to retrieve</param> public abstract void DownloadChapter(Publication publication, Chapter chapter); /// <summary> /// Retrieves the Cover from the Website /// </summary> /// <param name="publication">Publication to retrieve Cover for</param> public abstract void DownloadCover(Publication publication); /// <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) { //Check if Publication already has a Folder and a series.json string publicationFolder = Path.Join(downloadLocation, publication.folderName); if(!Directory.Exists(publicationFolder)) Directory.CreateDirectory(publicationFolder); string seriesInfoPath = Path.Join(publicationFolder, "series.json"); if(!File.Exists(seriesInfoPath)) File.WriteAllText(seriesInfoPath,publication.GetSeriesInfoJson()); } protected static string CreateComicInfo(Publication publication, Chapter chapter) { XElement comicInfo = new XElement("ComicInfo", new XElement("Tags", string.Join(',',publication.tags)), new XElement("LanguageISO", publication.originalLanguage), new XElement("Title", chapter.name), new XElement("Volume", chapter.volumeNumber), new XElement("Number", chapter.chapterNumber) ); return comicInfo.ToString(); } public bool ChapterIsDownloaded(Publication publication, Chapter chapter) { return File.Exists(CreateFullFilepath(publication, chapter)); } protected string CreateFullFilepath(Publication publication, Chapter chapter) { return Path.Join(downloadLocation, publication.folderName, chapter.fileName); } /// <summary> /// Downloads Image from URL and saves it to the given path(incl. fileName) /// </summary> /// <param name="imageUrl"></param> /// <param name="fullPath"></param> /// <param name="downloadClient">DownloadClient of the connector</param> protected static void DownloadImage(string imageUrl, string fullPath, DownloadClient downloadClient) { DownloadClient.RequestResult requestResult = downloadClient.MakeRequest(imageUrl); byte[] buffer = new byte[requestResult.result.Length]; requestResult.result.ReadExactly(buffer, 0, buffer.Length); File.WriteAllBytes(fullPath, buffer); } /// <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> /// <param name="downloadClient">DownloadClient of the connector</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, string? comicInfoPath = null) { //Check if Publication Directory already exists string[] splitPath = saveArchiveFilePath.Split(Path.DirectorySeparatorChar); string directoryPath = Path.Combine(splitPath.Take(splitPath.Length - 1).ToArray()); if (!Directory.Exists(directoryPath)) Directory.CreateDirectory(directoryPath); string fullPath = $"{saveArchiveFilePath}.cbz"; if (File.Exists(fullPath)) //Don't download twice. return; //Create a temporary folder to store images string tempFolder = Directory.CreateTempSubdirectory().FullName; int chapter = 0; //Download all Images to temporary Folder foreach (string imageUrl in imageUrls) { string[] split = imageUrl.Split('.'); string extension = split[^1]; DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), downloadClient); } if(comicInfoPath is not null) File.Copy(comicInfoPath, Path.Join(tempFolder, "ComicInfo.xml")); //ZIP-it and ship-it ZipFile.CreateFromDirectory(tempFolder, fullPath); Directory.Delete(tempFolder, true); //Cleanup } protected class DownloadClient { private readonly TimeSpan _requestSpeed; private DateTime _lastRequest; private static readonly HttpClient Client = new(); /// <summary> /// Creates a httpClient /// </summary> /// <param name="delay">minimum delay between requests (to avoid spam)</param> public DownloadClient(uint delay) { _requestSpeed = TimeSpan.FromMilliseconds(delay); _lastRequest = DateTime.Now.Subtract(_requestSpeed); } /// <summary> /// Request Webpage /// </summary> /// <param name="url"></param> /// <returns>RequestResult with StatusCode and Stream of received data</returns> public RequestResult MakeRequest(string url) { while((DateTime.Now - _lastRequest) < _requestSpeed) Thread.Sleep(10); _lastRequest = DateTime.Now; HttpRequestMessage requestMessage = new(HttpMethod.Get, url); HttpResponseMessage response = Client.Send(requestMessage); Stream resultString = response.IsSuccessStatusCode ? response.Content.ReadAsStream() : Stream.Null; return new RequestResult(response.StatusCode, resultString); } public struct RequestResult { public HttpStatusCode statusCode { get; } public Stream result { get; } public RequestResult(HttpStatusCode statusCode, Stream result) { this.statusCode = statusCode; this.result = result; } } } }