From fb6b00759482a5412860c6434326957fa0b485ad Mon Sep 17 00:00:00 2001 From: RedBuld Date: Thu, 1 Jan 2026 19:25:28 +0300 Subject: [PATCH 01/13] split builds --- Core/Configs/Options.cs | 6 + Core/Logic/Builders/CbzBuilder.cs | 126 ++++++- Core/Logic/Builders/Fb2Builder.cs | 357 ++++++++++++++---- .../LibSocial/NewLibSocialGetterBase.cs | 8 +- 4 files changed, 405 insertions(+), 92 deletions(-) diff --git a/Core/Configs/Options.cs b/Core/Configs/Options.cs index 6b7d8989..56480a40 100644 --- a/Core/Configs/Options.cs +++ b/Core/Configs/Options.cs @@ -74,6 +74,12 @@ public Options(IEnumerable url) { [Option("book-name-pattern", Required = false, HelpText = "Шаблон имени файла.", Default = "{Author.Name} - {Book.Title}")] public string BookNamePattern { get; set; } = "{Author.Name} - {Book.Title}"; + + [Option("split-volumes", Required = false, HelpText = "Разбивать по томам (если поддерживается)")] + public bool SplitVolumes { get; set; } + + [Option("split-chapters", Required = false, HelpText = "Разбивать по главам (если поддерживается)")] + public bool SplitChapters { get; set; } public bool HasAdditionalType(AdditionalTypeEnum type) => Additional && (AdditionalTypes == default || !AdditionalTypes.Any() || AdditionalTypes.Contains(type)); diff --git a/Core/Logic/Builders/CbzBuilder.cs b/Core/Logic/Builders/CbzBuilder.cs index 190a28a2..b39c7b60 100644 --- a/Core/Logic/Builders/CbzBuilder.cs +++ b/Core/Logic/Builders/CbzBuilder.cs @@ -1,34 +1,140 @@ using System.IO; using System.IO.Compression; +using System.Linq; using System.Threading.Tasks; using Core.Configs; using Core.Types.Book; using Microsoft.Extensions.Logging; -namespace Core.Logic.Builders; +namespace Core.Logic.Builders; -public class CbzBuilder(Options options, ILogger logger) : BuilderBase(options, logger) { +public class CbzBuilder(Options options, ILogger logger) : BuilderBase(options, logger) +{ protected override string Extension => "cbz"; - protected override async Task BuildInternal(Book book, string fileName) { - if (File.Exists(fileName)) { + protected override async Task BuildInternal(Book book, string fileName) + { + if (File.Exists(fileName)) + { File.Delete(fileName); } - + using var archive = ZipFile.Open(fileName, ZipArchiveMode.Create); - + var c = 0; - foreach (var chapter in book.Chapters) { - if (chapter.Images == default) { + foreach (var chapter in book.Chapters) + { + if (chapter.Images == default) + { continue; } - foreach (var image in chapter.Images) { - var entry = archive.CreateEntry($"{++c}{image.Extension}", CompressionLevel.Optimal); + foreach (var image in chapter.Images) + { + var image_name = $"{chapter.VolumeNumber}-{chapter.ChapterNumber}-{++c}{image.Extension}"; + var entry = archive.CreateEntry(image_name, CompressionLevel.Optimal); await using var entryStream = entry.Open(); await using var fileStream = image.GetStream(); await fileStream.CopyToAsync(entryStream); } } } + + protected override async Task SplitBuild(Book book, string fileName) + { + if (options.SplitVolumes && options.SplitChapters) + { + foreach (var volumeNumber in book.Volumes) + { + var volume_name = $"{fileName}.v{volumeNumber}.{Extension}"; + using var volume_archive = ZipFile.Open(volume_name, ZipArchiveMode.Create); + + foreach (var chapter in book.Chapters.Where(c => c.VolumeNumber == volumeNumber)) + { + if (chapter.Images == default) + { + continue; + } + + var chapter_name = $"{fileName}.v{chapter.VolumeNumber}.c{chapter.ChapterNumber}.{Extension}"; + + using var chapter_archive = ZipFile.Open(chapter_name, ZipArchiveMode.Create); + + var c = 0; + foreach (var image in chapter.Images) + { + var image_name = $"{chapter.VolumeNumber}-{chapter.ChapterNumber}-{++c}{image.Extension}"; + var volume_entry = volume_archive.CreateEntry(image_name, CompressionLevel.Optimal); + var chapter_entry = chapter_archive.CreateEntry(image_name, CompressionLevel.Optimal); + await using var volume_entryStream = volume_entry.Open(); + await using var chapter_entryStream = chapter_entry.Open(); + await using var volume_fileStream = image.GetStream(); + await using var chapter_fileStream = image.GetStream(); + await volume_fileStream.CopyToAsync(volume_entryStream); + await chapter_fileStream.CopyToAsync(chapter_entryStream); + } + Logger.LogInformation($"Книга {chapter_name} успешно сохранена"); + chapter_archive.Dispose(); + } + Logger.LogInformation($"Книга {volume_name} успешно сохранена"); + volume_archive.Dispose(); + } + } + else if (options.SplitVolumes) + { + foreach (var volumeNumber in book.Volumes) + { + var volume_name = $"{fileName}.v{volumeNumber}.{Extension}"; + + using var volume_archive = ZipFile.Open(volume_name, ZipArchiveMode.Create); + + var c = 0; + foreach (var chapter in book.Chapters.Where(c => c.VolumeNumber == volumeNumber)) + { + if (chapter.Images == default) + { + continue; + } + foreach (var image in chapter.Images) + { + var image_name = $"{chapter.VolumeNumber}-{chapter.ChapterNumber}-{++c}{image.Extension}"; + var volume_entry = volume_archive.CreateEntry(image_name, CompressionLevel.Optimal); + await using var volume_entryStream = volume_entry.Open(); + await using var fileStream = image.GetStream(); + await fileStream.CopyToAsync(volume_entryStream); + } + } + Logger.LogInformation($"Книга {volume_name} успешно сохранена"); + volume_archive.Dispose(); + } + } + else if (options.SplitChapters) + { + foreach (var chapter in book.Chapters) + { + var chapter_name = $"{fileName}.v{chapter.VolumeNumber}.c{chapter.ChapterNumber}.{Extension}"; + + using var chapter_archive = ZipFile.Open(chapter_name, ZipArchiveMode.Create); + if (chapter.Images == default) + { + continue; + } + var c = 0; + foreach (var image in chapter.Images) + { + var image_name = $"{chapter.VolumeNumber}-{chapter.ChapterNumber}-{++c}{image.Extension}"; + var chapter_entry = chapter_archive.CreateEntry(image_name, CompressionLevel.Optimal); + await using var chapter_entryStream = chapter_entry.Open(); + await using var fileStream = image.GetStream(); + await fileStream.CopyToAsync(chapter_entryStream); + } + Logger.LogInformation($"Книга {chapter_name} успешно сохранена"); + chapter_archive.Dispose(); + } + } + else + { + await BuildInternal(book, fileName); + } + } } \ No newline at end of file diff --git a/Core/Logic/Builders/Fb2Builder.cs b/Core/Logic/Builders/Fb2Builder.cs index 1fc5a606..f9dc7bb7 100644 --- a/Core/Logic/Builders/Fb2Builder.cs +++ b/Core/Logic/Builders/Fb2Builder.cs @@ -14,11 +14,12 @@ using HtmlAgilityPack; using Microsoft.Extensions.Logging; -namespace Core.Logic.Builders; +namespace Core.Logic.Builders; -public class Fb2Builder : BuilderBase { +public class Fb2Builder : BuilderBase +{ protected override string Extension => "fb2"; - + private readonly XNamespace _ns = "http://www.gribuser.ru/xml/fictionbook/2.0"; private readonly XNamespace _xlink = "http://www.w3.org/1999/xlink"; @@ -26,6 +27,7 @@ public class Fb2Builder : BuilderBase { private readonly XElement _titleInfo; private readonly XElement _body; private readonly XElement _documentInfo; + private readonly XElement _customInfo; private readonly List _images = new(); private readonly Dictionary _map = new() { @@ -48,22 +50,27 @@ public class Fb2Builder : BuilderBase { {"u", "u"}, }; - public Fb2Builder(Options options, ILogger logger) : base(options, logger) { + public Fb2Builder(Options options, ILogger logger) : base(options, logger) + { _description = CreateXElement("description"); _titleInfo = CreateXElement("title-info"); _documentInfo = CreateXElement("document-info"); + _customInfo = CreateXElement("custom-info"); _body = CreateXElement("body"); } - private XElement CreateXElement(string name) { + private XElement CreateXElement(string name) + { return new XElement(_ns + name); } - private XElement CreateAuthor(Author author) { + private XElement CreateAuthor(Author author) + { var authorElem = CreateXElement("author"); var parts = author.Name.Split(" "); - if (parts.Length == 2) { + if (parts.Length == 2) + { var firstName = CreateXElement("first-name"); firstName.Value = parts[0]; authorElem.Add(firstName); @@ -71,13 +78,16 @@ private XElement CreateAuthor(Author author) { var lastName = CreateXElement("last-name"); lastName.Value = parts[1]; authorElem.Add(lastName); - } else { + } + else + { var firstName = CreateXElement("first-name"); firstName.Value = author.Name; authorElem.Add(firstName); } - if (author.Url != default) { + if (author.Url != default) + { var homePageElem = CreateXElement("home-page"); homePageElem.Value = author.Url.ToString(); authorElem.Add(homePageElem); @@ -86,17 +96,19 @@ private XElement CreateAuthor(Author author) { return authorElem; } - private XElement CreateTitle(string text) { + private XElement CreateTitle(string text) + { var p = CreateXElement("p"); p.Value = text.HtmlDecode().ReplaceNewLine(); - + var title = CreateXElement("title"); - + title.Add(p); return title; } - private static async Task WriteBinary(XmlWriter writer, TempFile tempFile) { + private static async Task WriteBinary(XmlWriter writer, TempFile tempFile) + { const int bufferSize = 1000; var buffer = new byte[bufferSize]; int readBytes; @@ -107,32 +119,36 @@ private static async Task WriteBinary(XmlWriter writer, TempFile tempFile) { await writer.WriteAttributeStringAsync(null, "content-type", null, "image/" + tempFile.Extension.TrimStart('.')); using var br = new BinaryReader(inputFile); - do { + do + { readBytes = br.Read(buffer, 0, bufferSize); await writer.WriteBase64Async(buffer, 0, readBytes); } while (bufferSize <= readBytes); await writer.WriteEndElementAsync(); } - + /// /// Добавление автора книги /// /// Автор /// - private void AddAuthor(Author author) { + private void AddAuthor(Author author) + { var authorElem = CreateAuthor(author); _titleInfo.Add(authorElem); _documentInfo.Add(authorElem); } - + /// /// Добавление со-авторов книги /// /// Со-авторы /// - private void AddCoAuthors(IEnumerable coAuthors) { - foreach (var coAuthor in coAuthors) { + private void AddCoAuthors(IEnumerable coAuthors) + { + foreach (var coAuthor in coAuthors) + { var coAuthorElem = CreateAuthor(coAuthor); _titleInfo.Add(coAuthorElem); _documentInfo.Add(coAuthorElem); @@ -144,7 +160,8 @@ private void AddCoAuthors(IEnumerable coAuthors) { /// /// Название книги /// - private void WithTitle(string title) { + private void WithTitle(string title) + { var bookTitle = CreateXElement("book-title"); bookTitle.Value = title.ReplaceNewLine(); @@ -157,43 +174,52 @@ private void WithTitle(string title) { /// /// Обложка /// - private void WithCover(TempFile cover) { - if (cover != default) { + private void WithCover(TempFile cover) + { + if (cover != default) + { var coverPage = CreateXElement("coverpage"); - + var imageElem = CreateXElement("image"); imageElem.SetAttributeValue(_xlink + "href", "#" + cover.FullName); - + coverPage.Add(imageElem); _titleInfo.Add(coverPage); _images.Add(cover); } } - private void WithBookUrl(Uri url) { - if (url != default) { + private void WithBookUrl(Uri url) + { + if (url != default) + { var srcUrlElem = CreateXElement("src-url"); srcUrlElem.Value = url.ToString().CleanInvalidXmlChars(); _documentInfo.Add(srcUrlElem); } } - private void WithAnnotation(string annotation) { - if (!string.IsNullOrWhiteSpace(annotation)) { + private void WithAnnotation(string annotation) + { + if (!string.IsNullOrWhiteSpace(annotation)) + { _titleInfo.Add(CreateAnnotation(annotation)); } } - private XElement CreateAnnotation(string annotation) { + private XElement CreateAnnotation(string annotation) + { var a = CreateXElement("annotation"); var doc = annotation.AsHtmlDoc(); - foreach (var node in doc.DocumentNode.ChildNodes) { - if (!string.IsNullOrWhiteSpace(node.InnerText)) { + foreach (var node in doc.DocumentNode.ChildNodes) + { + if (!string.IsNullOrWhiteSpace(node.InnerText)) + { ProcessSection(a, node, "p"); } } - + return a; } @@ -202,97 +228,130 @@ private XElement CreateAnnotation(string annotation) { /// /// Список частей /// - private void WithChapters(IEnumerable chapters) { - foreach (var chapter in chapters.Where(c => c.IsValid)) { + private void WithChapters(IEnumerable chapters) + { + foreach (var chapter in chapters.Where(c => c.IsValid)) + { var section = CreateXElement("section"); section.Add(CreateTitle(chapter.Title)); - + var doc = chapter.Content.AsHtmlDoc(); - foreach (var node in doc.DocumentNode.ChildNodes) { + foreach (var node in doc.DocumentNode.ChildNodes) + { ProcessSection(section, node); } - + _body.Add(section); - foreach (var image in chapter.Images) { + foreach (var image in chapter.Images) + { _images.Add(image); } } } - private void WithSeria(Seria seria) { - if (seria != default) { + private void WithSeria(Seria seria) + { + if (seria != default) + { var sequenceElem = CreateXElement("sequence"); sequenceElem.SetAttributeValue("name", seria.Name.CleanInvalidXmlChars()); sequenceElem.SetAttributeValue("number", seria.Number.CleanInvalidXmlChars()); _titleInfo.Add(sequenceElem); + // seria url + var SeriaUrl = seria.Url.ToString(); + if( !string.IsNullOrWhiteSpace(SeriaUrl) && SeriaUrl.Length > 14 ) + { + _customInfo.SetAttributeValue("info-type", "sequence-url"); + _customInfo.Add(SeriaUrl.CleanInvalidXmlChars()); + } } } - private void WithLang(string lang) { - if (!string.IsNullOrWhiteSpace(lang)) { + private void WithLang(string lang) + { + if (!string.IsNullOrWhiteSpace(lang)) + { var langElem = CreateXElement("lang"); langElem.Value = lang; _titleInfo.Add(langElem); } } - private static bool IsTextNode(HtmlNode node) { + private static bool IsTextNode(HtmlNode node) + { return node.Name is "#text" or "span"; } - private void ProcessSection(XElement parent, HtmlNode node, string textNode = "") { - if (node.Name == "br") { + private void ProcessSection(XElement parent, HtmlNode node, string textNode = "") + { + if (node.Name == "br") + { parent.Add(CreateXElement("br")); return; } - - if (node.Name == "a") { + + if (node.Name == "a") + { var href = node.Attributes["href"]?.Value ?? string.Empty; - if (string.IsNullOrWhiteSpace(href)) { - if (string.IsNullOrWhiteSpace(textNode)) { + if (string.IsNullOrWhiteSpace(href)) + { + if (string.IsNullOrWhiteSpace(textNode)) + { parent.Add(node.InnerText); - } else { + } + else + { var tag = CreateXElement(textNode); tag.Value = node.InnerText; parent.Add(tag); } - } else { + } + else + { var a = CreateXElement("a"); a.SetAttributeValue(_xlink + "href", href); a.Value = node.InnerText; - - if (string.IsNullOrWhiteSpace(textNode)) { + + if (string.IsNullOrWhiteSpace(textNode)) + { parent.Add(a); - } else { + } + else + { var tag = CreateXElement(textNode); tag.Add(a); parent.Add(tag); } } - + return; } - - if (node.ChildNodes.Count > 0) { + + if (node.ChildNodes.Count > 0) + { var section = CreateXElement(_map.GetValueOrDefault(node.Name, "p")); - foreach (var child in node.ChildNodes) { + foreach (var child in node.ChildNodes) + { ProcessSection(IsTextNode(node) ? parent : section, child); } - if (!section.IsEmpty) { + if (!section.IsEmpty) + { parent.Add(section); } return; } - - if (node.Name == "img") { - if (node.Attributes["src"] == null) { + + if (node.Name == "img") + { + if (node.Attributes["src"] == null) + { return; } - + var imageElem = CreateXElement("image"); imageElem.SetAttributeValue(_xlink + "href", "#" + node.Attributes["src"].Value); parent.Add(imageElem); @@ -301,50 +360,61 @@ private void ProcessSection(XElement parent, HtmlNode node, string textNode = "" } var nodeText = node.InnerText.HtmlDecode().CleanInvalidXmlChars(); - if (node.InnerText.StartsWith(" ")) { + if (node.InnerText.StartsWith(" ")) + { nodeText = " " + nodeText; } - - if (node.InnerText.EndsWith(" ")) { + + if (node.InnerText.EndsWith(" ")) + { nodeText += " "; } - - if (IsTextNode(node)) { - if (string.IsNullOrWhiteSpace(textNode)) { + + if (IsTextNode(node)) + { + if (string.IsNullOrWhiteSpace(textNode)) + { parent.Add(new XText(nodeText)); - } else { + } + else + { var tag = CreateXElement(textNode); tag.Value = nodeText; parent.Add(tag); } - + return; } - if (_map.TryGetValue(node.Name, out var fb2Tag)) { + if (_map.TryGetValue(node.Name, out var fb2Tag)) + { var tag = CreateXElement(fb2Tag); tag.Value = nodeText; parent.Add(tag); - } else { + } + else + { parent.Add(nodeText); Logger.LogInformation(node.Name); } } - private XElement GetDateElement(DateTime date) { + private XElement GetDateElement(DateTime date) + { var today = date.ToString("yyyy-MM-dd"); - + var dateElem = CreateXElement("date"); dateElem.SetAttributeValue("value", today); dateElem.Value = today; - + return dateElem; } - - protected override async Task BuildInternal(Book book, string fileName) { + + protected override async Task BuildInternal(Book book, string fileName) + { await using var file = File.Create(fileName); - + AddAuthor(book.Author); AddCoAuthors(book.CoAuthors); WithBookUrl(book.Url); @@ -354,6 +424,59 @@ protected override async Task BuildInternal(Book book, string fileName) { WithLang(book.Lang); WithSeria(book.Seria); WithChapters(book.Chapters); + + _documentInfo.Add(GetDateElement(DateTime.Today)); + + var programUsed = CreateXElement("program-used"); + programUsed.Value = "Elib2Ebook"; + _documentInfo.Add(programUsed); + + _description.Add(_titleInfo); + _description.Add(_documentInfo); + + Logger.LogInformation($"SeriaUrl: {_customInfo.Value}"); + + if( !string.IsNullOrEmpty(_customInfo.Value) ) + { + _description.Add(_customInfo); + } + + var xws = new XmlWriterSettings + { + Async = true, + Encoding = new UTF8Encoding(false), + Indent = true, + NewLineHandling = NewLineHandling.Replace, + NewLineChars = "\r\n", + }; + + await using var writer = XmlWriter.Create(file, xws); + var cancellationToken = new CancellationToken(); + + await writer.WriteStartElementAsync(string.Empty, "FictionBook", _ns.NamespaceName); + await writer.WriteAttributeStringAsync("xmlns", "l", null, _xlink.NamespaceName); + + await _description.WriteToAsync(writer, cancellationToken); + await _body.WriteToAsync(writer, cancellationToken); + foreach (var image in _images) + { + await WriteBinary(writer, image); + } + + await writer.WriteEndElementAsync(); + await writer.FlushAsync(); + } + + protected override async Task SplitBuild(Book book, string fileName) + { + AddAuthor(book.Author); + AddCoAuthors(book.CoAuthors); + WithBookUrl(book.Url); + WithTitle(book.Title); + WithAnnotation(book.Annotation); + WithCover(book.Cover); + WithLang(book.Lang); + WithSeria(book.Seria); _documentInfo.Add(GetDateElement(DateTime.Today)); @@ -372,19 +495,91 @@ protected override async Task BuildInternal(Book book, string fileName) { NewLineChars = "\r\n", }; + if (Options.SplitVolumes && Options.SplitChapters) + { + foreach (var volumeNumber in book.Volumes) + { + var volume_name = $"{fileName}.v{volumeNumber}.{Extension}"; + await using var volume_file = File.Create(volume_name); + + var selected_chapters = book.Chapters.Where(c => c.VolumeNumber == volumeNumber); + + foreach (var chapter in selected_chapters) + { + var chapter_name = $"{fileName}.v{chapter.VolumeNumber}.c{chapter.ChapterNumber}.{Extension}"; + + await using var chapter_file = File.Create(chapter_name); + + await BuildPart([chapter], chapter_file, xws); + + Logger.LogInformation($"Книга {chapter_name} успешно сохранена"); + + chapter_file.Dispose(); + } + + await BuildPart(selected_chapters, volume_file, xws); + Logger.LogInformation($"Книга {volume_name} успешно сохранена"); + volume_file.Dispose(); + } + } + else if (Options.SplitVolumes) + { + foreach (var volumeNumber in book.Volumes) + { + var volume_name = $"{fileName}.v{volumeNumber}.{Extension}"; + + await using var volume_file = File.Create(volume_name); + + var selected_chapters = book.Chapters.Where(c => c.VolumeNumber == volumeNumber); + + await BuildPart(selected_chapters, volume_file, xws); + + Logger.LogInformation($"Книга {volume_name} успешно сохранена"); + + volume_file.Dispose(); + } + } + else if (Options.SplitChapters) + { + foreach (var chapter in book.Chapters) + { + var chapter_name = $"{fileName}.v{chapter.VolumeNumber}.c{chapter.ChapterNumber}.{Extension}"; + await using var chapter_file = File.Create(chapter_name); + + await BuildPart([chapter], chapter_file, xws); + + Logger.LogInformation($"Книга {chapter_name} успешно сохранена"); + + chapter_file.Dispose(); + } + } + else + { + await BuildInternal(book, fileName); + } + } + + protected async Task BuildPart(IEnumerable chapters, FileStream file, XmlWriterSettings xws) + { + WithChapters(chapters); + await using var writer = XmlWriter.Create(file, xws); var cancellationToken = new CancellationToken(); - + await writer.WriteStartElementAsync(string.Empty, "FictionBook", _ns.NamespaceName); await writer.WriteAttributeStringAsync("xmlns", "l", null, _xlink.NamespaceName); await _description.WriteToAsync(writer, cancellationToken); await _body.WriteToAsync(writer, cancellationToken); - foreach (var image in _images) { + foreach (var image in _images) + { await WriteBinary(writer, image); } await writer.WriteEndElementAsync(); await writer.FlushAsync(); + + _body.RemoveNodes(); + _images.Clear(); } } \ No newline at end of file diff --git a/Core/Logic/Getters/LibSocial/NewLibSocialGetterBase.cs b/Core/Logic/Getters/LibSocial/NewLibSocialGetterBase.cs index d4896844..bdd7a0b0 100644 --- a/Core/Logic/Getters/LibSocial/NewLibSocialGetterBase.cs +++ b/Core/Logic/Getters/LibSocial/NewLibSocialGetterBase.cs @@ -84,7 +84,13 @@ private static string GetRandom(int length) { } public override async Task Authorize() { - if (!Config.HasCredentials) { + if (string.IsNullOrEmpty(Config.Options.Token) && !Config.HasCredentials ) { + return; + } + + if (!string.IsNullOrEmpty(Config.Options.Token)) { + Config.Logger.LogInformation("Подставлен токен"); + Config.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Config.Options.Token); return; } From ce7c5d80593c81b12246804f71f98c625947ec40 Mon Sep 17 00:00:00 2001 From: RedBuld Date: Thu, 1 Jan 2026 19:28:35 +0300 Subject: [PATCH 02/13] fix split building --- Core/Logic/Builders/BuilderBase.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Core/Logic/Builders/BuilderBase.cs b/Core/Logic/Builders/BuilderBase.cs index 77963238..4a84d492 100644 --- a/Core/Logic/Builders/BuilderBase.cs +++ b/Core/Logic/Builders/BuilderBase.cs @@ -20,6 +20,7 @@ public abstract class BuilderBase(Options options, ILogger logger) { /// Книга /// Имя файла protected abstract Task BuildInternal(Book book, string fileName); + protected virtual async Task SplitBuild(Book book, string fileName) { await BuildInternal(book, fileName); } /// /// Получение имени файла @@ -41,6 +42,11 @@ public abstract class BuilderBase(Options options, ILogger logger) { /// Книга public async Task Build(Book book) { var fileName = GetFileName(book); + + if (book.SupportSplitting && (Options.SplitChapters || Options.SplitVolumes)) + { + fileName = GetTitle(book); + } if (!string.IsNullOrWhiteSpace(Options.SavePath)) { if (!Directory.Exists(Options.SavePath)) { @@ -51,7 +57,11 @@ public async Task Build(Book book) { } Logger.LogInformation($"Начинаю сохранение книги {fileName.CoverQuotes()}"); - await BuildInternal(book, fileName); + if (book.SupportSplitting && (Options.SplitChapters || Options.SplitVolumes)) { + await SplitBuild(book, fileName); + } else { + await BuildInternal(book, fileName); + } if (Options.Cover) { await SaveCover(Options.SavePath, book.Cover, GetTitle(book)); From 05926d4299bd51624d5bc222bbe8a1c968c912c2 Mon Sep 17 00:00:00 2001 From: RedBuld Date: Thu, 1 Jan 2026 19:41:03 +0300 Subject: [PATCH 03/13] fix split builds --- Core/Configs/Options.cs | 3 +++ Core/Logic/Getters/LibSocial/NewLibSocialGetterBase.cs | 6 +++++- Core/Types/Book/Book.cs | 9 +++++++++ Core/Types/Book/Chapter.cs | 10 ++++++++++ 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Core/Configs/Options.cs b/Core/Configs/Options.cs index 56480a40..a0efbe48 100644 --- a/Core/Configs/Options.cs +++ b/Core/Configs/Options.cs @@ -23,6 +23,9 @@ public Options(IEnumerable url) { [Option('p', "password", Required = false, HelpText = "Пароль от системы")] public string Password { get; set; } + + [Option('k', "token", Required = false, HelpText = "Готовый токен авторизации")] + public string Token { get; set; } [Option("proxy", Required = false, HelpText = "Прокси в формате :")] public string Proxy { get; set; } diff --git a/Core/Logic/Getters/LibSocial/NewLibSocialGetterBase.cs b/Core/Logic/Getters/LibSocial/NewLibSocialGetterBase.cs index bdd7a0b0..853fc7fa 100644 --- a/Core/Logic/Getters/LibSocial/NewLibSocialGetterBase.cs +++ b/Core/Logic/Getters/LibSocial/NewLibSocialGetterBase.cs @@ -184,6 +184,8 @@ public override async Task Get(Uri url) { var details = await GetBookDetails(url); var book = new Book(url) { + SupportSplitVolumes = true, + SupportSplitChapters = true, Cover = await GetCover(details), Chapters = await FillChapters(details, bid), Title = string.IsNullOrWhiteSpace(details.Data.RusName) ? details.Data.Name : details.Data.RusName, @@ -272,7 +274,9 @@ private async Task> FillChapters(RanobeLibBookDetails book, Config.Logger.LogInformation($"Загружаю главу {title.CoverQuotes()}"); var chapter = new Chapter { - Title = title + Title = title, + VolumeNumber = socialChapter.Volume, + ChapterNumber = socialChapter.Number, }; var chapterDoc = await GetChapter(book, socialChapter, bid); diff --git a/Core/Types/Book/Book.cs b/Core/Types/Book/Book.cs index 02603f64..aa4af7af 100644 --- a/Core/Types/Book/Book.cs +++ b/Core/Types/Book/Book.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Collections.Generic; using Core.Types.Common; @@ -40,6 +41,9 @@ public class Book : IDisposable { /// public IEnumerable Chapters { get; set; } = new List(); +/// Тома + public IEnumerable Volumes => Chapters.Select(c => c.VolumeNumber).Distinct(); + /// /// Url расположения книги /// @@ -55,6 +59,11 @@ public class Book : IDisposable { /// public string Lang { get; set; } = "ru"; + public bool SupportSplitVolumes { get; set; } = false; + public bool SupportSplitChapters { get; set; } = false; + + public bool SupportSplitting => SupportSplitVolumes || SupportSplitChapters; + public Book(Uri url) { Url = url; } diff --git a/Core/Types/Book/Chapter.cs b/Core/Types/Book/Chapter.cs index cbdab9ab..b8557b20 100644 --- a/Core/Types/Book/Chapter.cs +++ b/Core/Types/Book/Chapter.cs @@ -14,6 +14,16 @@ public class Chapter { /// public string Content { get; set; } + /// + /// Номер тома + /// + public string VolumeNumber { get; set; } + + /// + /// Номер главы + /// + public string ChapterNumber { get; set; } + /// /// Изображения из части /// From 1dbf012c05f2b266ea48a6b5bd30c30df39b7e64 Mon Sep 17 00:00:00 2001 From: RedBuld Date: Sat, 24 Jan 2026 08:00:16 +0300 Subject: [PATCH 04/13] AT cache and alternative litres auth --- Core/Logic/Getters/AuthorTodayGetter.cs | 38 +++++++++ Core/Logic/Getters/LitresGetter.cs | 82 ++++++++++++++----- .../Litres/Response/LitresWebAuthResponse.cs | 27 ++++++ Elib2EbookCli/Program.cs | 2 +- 4 files changed, 129 insertions(+), 20 deletions(-) create mode 100644 Core/Types/Litres/Response/LitresWebAuthResponse.cs diff --git a/Core/Logic/Getters/AuthorTodayGetter.cs b/Core/Logic/Getters/AuthorTodayGetter.cs index 8eb6c4d1..49e79f44 100644 --- a/Core/Logic/Getters/AuthorTodayGetter.cs +++ b/Core/Logic/Getters/AuthorTodayGetter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net; using System.Net.Http; @@ -7,6 +8,7 @@ using System.Net.Http.Json; using System.Security.Cryptography; using System.Text; +using System.Text.Json; using System.Threading.Tasks; using Core.Configs; using Core.Extensions; @@ -69,6 +71,41 @@ public override async Task Authorize() { return; } + // АТ очень не любит много авторизовывать + // поэтому пришлось добавить кеширование токенов + const string directory = "ATCache"; + + if (!Directory.Exists(directory)) { + Directory.CreateDirectory(directory); + } + + var saveCreds = $"{directory}/{Config.Options.Login.RemoveInvalidChars()}"; + if (File.Exists(saveCreds)) { + var timeDiff = File.GetCreationTime(saveCreds) - DateTime.Now; + if ( timeDiff.TotalHours < 24 ) + { + var savedAuth = await File.ReadAllTextAsync(saveCreds).ContinueWith(t => t.Result.Deserialize()); + Config.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", savedAuth.Token); + + try + { + var checkResponse = await Config.Client.SendWithTriesAsync(() => GetDefaultMessage(ApiUrl.MakeRelativeUri("/v1/account/current-user"), _apiUrl)); + var checkUser = await checkResponse.Content.ReadFromJsonAsync(); + UserId = checkUser!.Id.ToString(); + } + catch (System.Exception){} + + if( !string.IsNullOrWhiteSpace(UserId) ) + { + return; + } + } + else + { + File.Delete(saveCreds); + } + } + var response = await Config.Client.SendAsync(GetDefaultMessage(ApiUrl.MakeRelativeUri("/v1/account/login-by-password"), _apiUrl, JsonContent.Create(new { Config.Options.Login, Config.Options.Password }))); var data = await response.Content.ReadFromJsonAsync(); @@ -82,6 +119,7 @@ public override async Task Authorize() { } else { throw new Exception($"Не удалось авторизоваться. {data?.Message}"); } + await File.WriteAllTextAsync(saveCreds, JsonSerializer.Serialize(data)); } public override async Task Get(Uri url) { diff --git a/Core/Logic/Getters/LitresGetter.cs b/Core/Logic/Getters/LitresGetter.cs index 78c3e7b9..4bd3171f 100644 --- a/Core/Logic/Getters/LitresGetter.cs +++ b/Core/Logic/Getters/LitresGetter.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Net.Http.Json; using System.Security.Cryptography; using System.Text; @@ -16,7 +17,6 @@ using Core.Types.Book; using Core.Types.Common; using Core.Types.Litres; -using Core.Types.Litres.Requests; using Core.Types.Litres.Response; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; @@ -29,6 +29,7 @@ public class LitresGetter(BookGetterConfig config) : GetterBase(config) { private const string APP = "13"; protected override Uri SystemUrl => new("https://www.litres.ru"); + protected static Uri ApiUrl => new("https://api.litres.ru"); private static Uri GetShortUri(LitresArt art) => new($"https://catalit.litres.ru/pub/t/{art.Id}.fb3"); @@ -70,10 +71,17 @@ private Uri GetFullUri(LitresArt art, string path, LitresFile file, string host private LitresMe _me; public override async Task Authorize() { - if (!Config.HasCredentials) { + if (string.IsNullOrEmpty(Config.Options.Token) && !Config.HasCredentials ) { Config.Client.DefaultRequestHeaders.Add("Session-Id", _authData.Sid); return; } + + if (!string.IsNullOrEmpty(Config.Options.Token)) { + Config.Logger.LogInformation("Подставлен токен"); + _authData.Sid = Config.Options.Token; + Config.Client.DefaultRequestHeaders.Add("Session-Id", Config.Options.Token); + return; + } // Литрес очень не любит много авторизовывать // поэтому пришлось добавить кеширование токенов @@ -85,24 +93,51 @@ public override async Task Authorize() { var saveCreds = $"{directory}/{Config.Options.Login.RemoveInvalidChars()}"; if (File.Exists(saveCreds)) { - _authData = await File.ReadAllTextAsync(saveCreds).ContinueWith(t => t.Result.Deserialize()); - Config.Client.DefaultRequestHeaders.Add("Session-Id", _authData.Sid); - - var checkResponse = await Config.Client.GetFromJsonAsync>("https://api.litres.ru/foundation/api/users/me/detailed"); - if (checkResponse?.Payload?.Data != default) { - _me = checkResponse.Payload.Data; - return; + var timeDiff = File.GetCreationTime(saveCreds) - DateTime.Now; + if ( timeDiff.TotalHours < 4 ) + { + var _authData = await File.ReadAllTextAsync(saveCreds).ContinueWith(t => t.Result.Deserialize());; + Config.Client.DefaultRequestHeaders.Add("Session-Id", _authData.Sid); + + var checkResponse = await Config.Client.GetFromJsonAsync>(ApiUrl.MakeRelativeUri("/foundation/api/users/me/detailed")); + if (checkResponse?.Payload?.Data != default) { + _me = checkResponse.Payload.Data; + return; + } + } + else + { + File.Delete(saveCreds); } } - var payload = LitresPayload.Create(DateTime.Now, string.Empty, SECRET_KEY, APP); - payload.Requests.Add(new LitresAuthRequest(Config.Options.Login, Config.Options.Password)); - - _authData = await GetResponse(payload); + var auth = new Dictionary { + { "login", Config.Options.Login }, + { "password", Config.Options.Password } + }; + var authPayload = new StringContent( JsonSerializer.Serialize(auth) ); + authPayload.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + Config.Client.DefaultRequestHeaders.Remove("app-id"); + Config.Client.DefaultRequestHeaders.Add("app-id","115"); + + var authResponse = await Config.Client.PostWithTriesAsync(ApiUrl.MakeRelativeUri("/foundation/api/auth/login"), authPayload); + _authData = await authResponse.Content.ReadAsStringAsync().ContinueWith(t => + { + var authData = t.Result.Deserialize(); + var newAuthData = new LitresAuthResponseData(); + newAuthData.Success = authData.Status==200; + newAuthData.Sid = authData.Payload.Data.Sid; + newAuthData.ErrorMessage = authData.Error; + + return newAuthData; + }); if (!_authData.Success) { throw new Exception($"Не удалось авторизоваться. {_authData.ErrorMessage}"); } + + Config.Client.DefaultRequestHeaders.Remove("app-id"); Config.Client.DefaultRequestHeaders.Add("Session-Id", _authData.Sid); var meResponse = await Config.Client.GetFromJsonAsync>("https://api.litres.ru/foundation/api/users/me/detailed"); @@ -116,7 +151,9 @@ public override async Task Authorize() { private async Task GetResponse(LitresPayload payload) { var resp = await Config.Client.PostWithTriesAsync("https://catalit.litres.ru/catalitv2".AsUri(), CreatePayload(payload)); - return await resp.Content.ReadAsStringAsync().ContinueWith(t => t.Result.Deserialize>().Data); + return await resp.Content.ReadAsStringAsync().ContinueWith(t => { + return t.Result.Deserialize>().Data; + }); } private async Task GetResponse(Uri url) { @@ -125,7 +162,9 @@ private async Task GetResponse(Uri url) { return default; } - return await resp.Content.ReadAsStringAsync().ContinueWith(t => t.Result.Deserialize>().Payload.Data); + return await resp.Content.ReadAsStringAsync().ContinueWith(t => { + return t.Result.Deserialize>().Payload.Data; + }); } private static FormUrlEncodedContent CreatePayload(LitresPayload payload) { @@ -139,7 +178,12 @@ private static FormUrlEncodedContent CreatePayload(LitresPayload payload) { public override async Task Get(Uri url) { var bookId = GetBookId(url); - var art = await GetResponse($"https://api.litres.ru/foundation/api/arts/{bookId}".AsUri()); + var art = new LitresArt(); + try + { + art = await GetResponse(ApiUrl.MakeRelativeUri($"/foundation/api/arts/{bookId}")); + } + catch (System.Exception){} var book = new Book(SystemUrl.MakeRelativeUri(bookId)) { Cover = await GetCover(art.Cover), @@ -177,7 +221,7 @@ private async Task FillAdditional(Book book, LitresArt art) { var type = art.ArtType == LitresArtTypeEnum.Audio ? AdditionalTypeEnum.Audio : AdditionalTypeEnum.Books; if (Config.Options.HasAdditionalType(type)) { - var files = await GetResponse($"https://api.litres.ru/foundation/api/arts/{art.Id}/files/grouped".AsUri()); + var files = await GetResponse(ApiUrl.MakeRelativeUri($"/foundation/api/arts/{art.Id}/files/grouped")); foreach (var file in files.SelectMany(f => f.Files)) { using var fileResponse = await GetFileResponse(art, file); @@ -234,7 +278,7 @@ private Seria GetSeria(LitresArt art) { private async Task GetAuthor(LitresArt art) { var person = art.Persons.FirstOrDefault(a => a.Role == "author"); - var author = person == null ? default : await GetResponse>($"https://api.litres.ru/foundation/api/persons/{person.Id}".AsUri()); + var author = person == null ? default : await GetResponse>(ApiUrl.MakeRelativeUri($"/foundation/api/persons/{person.Id}")); return author == default ? new Author("Litres") : new Author(author.FullName, SystemUrl.MakeRelativeUri(author.Url)); @@ -243,7 +287,7 @@ private async Task GetAuthor(LitresArt art) { private async Task> GetCoAuthors(LitresArt art) { var result = new List(); foreach (var person in art.Persons.Where(a => a.Role == "author").Skip(1)) { - var author = person == null ? default : await GetResponse>($"https://api.litres.ru/foundation/api/persons/{person.Id}".AsUri()); + var author = person == null ? default : await GetResponse>(ApiUrl.MakeRelativeUri($"/foundation/api/persons/{person.Id}")); if (author != default) { result.Add(new Author(author.FullName, SystemUrl.MakeRelativeUri(author.Url))); } diff --git a/Core/Types/Litres/Response/LitresWebAuthResponse.cs b/Core/Types/Litres/Response/LitresWebAuthResponse.cs new file mode 100644 index 00000000..7c407e34 --- /dev/null +++ b/Core/Types/Litres/Response/LitresWebAuthResponse.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace Core.Types.Litres.Response; +public class LitresWebAuthPayloadData +{ + [JsonPropertyName("sid")] + public string Sid { get; set; } +} + +public class LitresWebAuthPayload +{ + [JsonPropertyName("data")] + public LitresWebAuthPayloadData Data { get; set; } +} + +public class LitresWebAuthResponse +{ + [JsonPropertyName("status")] + public int Status { get; set; } + + [JsonPropertyName("payload")] + public LitresWebAuthPayload Payload { get; set; } + + [JsonPropertyName("error")] + public string Error { get; set; } = ""; +} + diff --git a/Elib2EbookCli/Program.cs b/Elib2EbookCli/Program.cs index 7e1b7917..9d748af9 100644 --- a/Elib2EbookCli/Program.cs +++ b/Elib2EbookCli/Program.cs @@ -51,7 +51,7 @@ await parserResult await getter.Init(); await getter.Authorize(); } catch (Exception ex) { - logger.LogInformation($"Ошибка авторизации или инициализации. {ex.Message}"); + logger.LogInformation($"Ошибка авторизации или инициализации. {ex.Message} \n {ex.ToString()}"); return; } From e7226c8f40177a399d4ff740da666e3fc793f60c Mon Sep 17 00:00:00 2001 From: RedBuld Date: Sat, 24 Jan 2026 11:11:15 +0300 Subject: [PATCH 05/13] fix liters + boovell content filter --- Core/Logic/Builders/Fb2Builder.cs | 2 -- Core/Logic/Getters/BoovelGetter.cs | 28 +++++++++++++++++++++++++++- Core/Logic/Getters/LitresGetter.cs | 7 +++---- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/Core/Logic/Builders/Fb2Builder.cs b/Core/Logic/Builders/Fb2Builder.cs index f9dc7bb7..446567bf 100644 --- a/Core/Logic/Builders/Fb2Builder.cs +++ b/Core/Logic/Builders/Fb2Builder.cs @@ -433,8 +433,6 @@ protected override async Task BuildInternal(Book book, string fileName) _description.Add(_titleInfo); _description.Add(_documentInfo); - - Logger.LogInformation($"SeriaUrl: {_customInfo.Value}"); if( !string.IsNullOrEmpty(_customInfo.Value) ) { diff --git a/Core/Logic/Getters/BoovelGetter.cs b/Core/Logic/Getters/BoovelGetter.cs index 731a56f9..24764d8a 100644 --- a/Core/Logic/Getters/BoovelGetter.cs +++ b/Core/Logic/Getters/BoovelGetter.cs @@ -92,7 +92,33 @@ private async Task GetChapter(UrlChapter urlChapter) { var output = new MemoryStream(); await cs.CopyToAsync(output); - return Encoding.UTF8.GetString(output.ToArray()).AsHtmlDoc(); + return FilterChapterContent( Encoding.UTF8.GetString(output.ToArray()) ); + } + + private static HtmlDocument FilterChapterContent(string data) + { + var sb = new StringBuilder(); + + var doc = data.AsHtmlDoc(); + + foreach (var node in doc.QuerySelectorAll("> *")) + { + var tag = node.Name; + var value = node.QuerySelector("boovell")?.InnerHtml; + if( tag != "img" && tag != "br" ) + { + if ( node.GetType() != typeof(HtmlTextNode) && node.GetType() != typeof(HtmlCommentNode) ) + { + sb.Append($"<{tag}>{value}"); + } + } + else + { + sb.Append(node?.OuterHtml); + } + } + + return sb.AsHtmlDoc(); } private Task GetCover(HtmlDocument doc, Uri bookUri) { diff --git a/Core/Logic/Getters/LitresGetter.cs b/Core/Logic/Getters/LitresGetter.cs index 4bd3171f..c686a511 100644 --- a/Core/Logic/Getters/LitresGetter.cs +++ b/Core/Logic/Getters/LitresGetter.cs @@ -96,7 +96,7 @@ public override async Task Authorize() { var timeDiff = File.GetCreationTime(saveCreds) - DateTime.Now; if ( timeDiff.TotalHours < 4 ) { - var _authData = await File.ReadAllTextAsync(saveCreds).ContinueWith(t => t.Result.Deserialize());; + _authData = await File.ReadAllTextAsync(saveCreds).ContinueWith(t => t.Result.Deserialize());; Config.Client.DefaultRequestHeaders.Add("Session-Id", _authData.Sid); var checkResponse = await Config.Client.GetFromJsonAsync>(ApiUrl.MakeRelativeUri("/foundation/api/users/me/detailed")); @@ -120,8 +120,9 @@ public override async Task Authorize() { Config.Client.DefaultRequestHeaders.Remove("app-id"); Config.Client.DefaultRequestHeaders.Add("app-id","115"); - var authResponse = await Config.Client.PostWithTriesAsync(ApiUrl.MakeRelativeUri("/foundation/api/auth/login"), authPayload); + Config.Client.DefaultRequestHeaders.Remove("app-id"); + _authData = await authResponse.Content.ReadAsStringAsync().ContinueWith(t => { var authData = t.Result.Deserialize(); @@ -136,8 +137,6 @@ public override async Task Authorize() { if (!_authData.Success) { throw new Exception($"Не удалось авторизоваться. {_authData.ErrorMessage}"); } - - Config.Client.DefaultRequestHeaders.Remove("app-id"); Config.Client.DefaultRequestHeaders.Add("Session-Id", _authData.Sid); var meResponse = await Config.Client.GetFromJsonAsync>("https://api.litres.ru/foundation/api/users/me/detailed"); From d06527794d2e974dccc97902f4be223c55fd3f53 Mon Sep 17 00:00:00 2001 From: RedBuld Date: Sat, 24 Jan 2026 16:27:06 +0300 Subject: [PATCH 06/13] fix boovel cover --- Core/Logic/Getters/BoovelGetter.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Core/Logic/Getters/BoovelGetter.cs b/Core/Logic/Getters/BoovelGetter.cs index 24764d8a..3c2b292d 100644 --- a/Core/Logic/Getters/BoovelGetter.cs +++ b/Core/Logic/Getters/BoovelGetter.cs @@ -2,6 +2,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; @@ -122,7 +125,7 @@ private static HtmlDocument FilterChapterContent(string data) } private Task GetCover(HtmlDocument doc, Uri bookUri) { - var imagePath = doc.QuerySelector("img.wp-post-image")?.Attributes["src"]?.Value; + var imagePath = doc.QuerySelector("a.story__thumbnail-href")?.Attributes["href"]?.Value; return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } } \ No newline at end of file From 0c4cc924bc6142754f0fec7c6403c84ce5b76edc Mon Sep 17 00:00:00 2001 From: RedBuld Date: Thu, 29 Jan 2026 07:58:52 +0300 Subject: [PATCH 07/13] fix cached auth timestamping --- Core/Logic/Getters/AuthorTodayGetter.cs | 2 +- Core/Logic/Getters/LitresGetter.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Core/Logic/Getters/AuthorTodayGetter.cs b/Core/Logic/Getters/AuthorTodayGetter.cs index 49e79f44..04cc2b12 100644 --- a/Core/Logic/Getters/AuthorTodayGetter.cs +++ b/Core/Logic/Getters/AuthorTodayGetter.cs @@ -81,7 +81,7 @@ public override async Task Authorize() { var saveCreds = $"{directory}/{Config.Options.Login.RemoveInvalidChars()}"; if (File.Exists(saveCreds)) { - var timeDiff = File.GetCreationTime(saveCreds) - DateTime.Now; + var timeDiff = DateTime.Now - File.GetLastWriteTime(saveCreds); if ( timeDiff.TotalHours < 24 ) { var savedAuth = await File.ReadAllTextAsync(saveCreds).ContinueWith(t => t.Result.Deserialize()); diff --git a/Core/Logic/Getters/LitresGetter.cs b/Core/Logic/Getters/LitresGetter.cs index c686a511..4cee5056 100644 --- a/Core/Logic/Getters/LitresGetter.cs +++ b/Core/Logic/Getters/LitresGetter.cs @@ -93,7 +93,7 @@ public override async Task Authorize() { var saveCreds = $"{directory}/{Config.Options.Login.RemoveInvalidChars()}"; if (File.Exists(saveCreds)) { - var timeDiff = File.GetCreationTime(saveCreds) - DateTime.Now; + var timeDiff = DateTime.Now - File.GetLastWriteTime(saveCreds); if ( timeDiff.TotalHours < 4 ) { _authData = await File.ReadAllTextAsync(saveCreds).ContinueWith(t => t.Result.Deserialize());; From 0561a0a8756d8f39caec20dca43a3bbe21080736 Mon Sep 17 00:00:00 2001 From: RedBuld Date: Sat, 18 Apr 2026 11:25:46 +0300 Subject: [PATCH 08/13] sync --- Core/Logic/Getters/AuthorTodayGetter.cs | 17 +++++---- Core/Logic/Getters/Litnet/LitnetGetterBase.cs | 36 ++++++++++++++++++- Core/Types/Litnet/LitnetAuthResponse.cs | 3 ++ 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/Core/Logic/Getters/AuthorTodayGetter.cs b/Core/Logic/Getters/AuthorTodayGetter.cs index 04cc2b12..ab27c9ff 100644 --- a/Core/Logic/Getters/AuthorTodayGetter.cs +++ b/Core/Logic/Getters/AuthorTodayGetter.cs @@ -221,7 +221,6 @@ private async Task> FillChapters(AuthorTodayBookDetails boo foreach (var atChapter in await GetChapters(book)) { var title = atChapter.Title.ReplaceNewLine(); - Config.Logger.LogInformation($"Загружаю главу {title.CoverQuotes()}"); var chapter = new Chapter { Title = title @@ -242,13 +241,17 @@ private async Task> FillChapters(AuthorTodayBookDetails boo private async Task> GetChapters(AuthorTodayBookDetails book) { var result = new List(); - foreach (var chunk in book.Chapters.Where(c => !c.IsDraft).OrderBy(c => c.SortOrder).Chunk(100)) { - var ids = string.Join("&", chunk.Select((c, i) => $"ids[{i}]={c.Id}")); - var uri = ApiUrl.MakeRelativeUri($"/v1/work/{book.Id}/chapter/many-texts?{ids}"); + foreach (var chapter in book.Chapters.Where(c => !c.IsDraft).OrderBy(c => c.SortOrder)) { + var uri = ApiUrl.MakeRelativeUri($"/v1/work/{book.Id}/chapter/{chapter.Id}/text"); + Config.Logger.LogInformation($"Загружаю главу {chapter.Title.CoverQuotes()}"); var response = await Config.Client.SendWithTriesAsync(() => GetDefaultMessage(uri, _apiUrl)); - var chapters = await response.Content.ReadFromJsonAsync(); - if (chapters != default) { - result.AddRange(chapters.Where(c => c.Code != "NotFound")); + var rchapter = await response.Content.ReadFromJsonAsync(); + if (rchapter != default) { + Config.Logger.LogInformation($"Загружена глава {rchapter.Title.CoverQuotes()}"); + chapter.Text = rchapter.Text; + chapter.Key = rchapter.Key; + chapter.IsSuccessful = true; + result.Add(chapter); } } diff --git a/Core/Logic/Getters/Litnet/LitnetGetterBase.cs b/Core/Logic/Getters/Litnet/LitnetGetterBase.cs index 2eb0870f..9dee2b76 100644 --- a/Core/Logic/Getters/Litnet/LitnetGetterBase.cs +++ b/Core/Logic/Getters/Litnet/LitnetGetterBase.cs @@ -7,6 +7,7 @@ using System.Net.Http.Json; using System.Security.Cryptography; using System.Text; +using System.Text.Json; using System.Threading.Tasks; using System.Web; using Core.Configs; @@ -28,7 +29,7 @@ public abstract class LitnetGetterBase(BookGetterConfig config) : GetterBase(con protected virtual Uri SiteIp => SystemUrl; - private static readonly string DeviceId = Guid.NewGuid().ToString().ToUpper(); + private static string DeviceId = Guid.NewGuid().ToString().ToUpper(); private const string SECRET = "14a6579a984b3c6abecda6c2dfa83a64"; private string _token { get; set; } @@ -66,15 +67,48 @@ private static string GetSign(string token) { public override async Task Authorize() { var path = Config.HasCredentials ? "user/find-by-login" : "registration/registration-by-device"; + const string directory = "LNCache"; + + if (!Directory.Exists(directory)) { + Directory.CreateDirectory(directory); + } + + var saveCreds = $"{directory}/{Config.Options.Login.RemoveInvalidChars()}"; + if (Config.HasCredentials) { + if (File.Exists(saveCreds)) { + var timeDiff = DateTime.Now - File.GetLastWriteTime(saveCreds); + if ( timeDiff.TotalHours < 2 ) + { + var savedAuth = await File.ReadAllTextAsync(saveCreds).ContinueWith(t => t.Result.Deserialize()); + _token = savedAuth.Token; + DeviceId = savedAuth.DeviceID; + + if( !string.IsNullOrWhiteSpace(_token) ) + { + return; + } + } + else + { + File.Delete(saveCreds); + } + } + } + var url = ApiIp.MakeRelativeUri($"v1/{path}?login={HttpUtility.UrlEncode(Config.Options.Login?.TrimStart('+') ?? string.Empty)}&password={HttpUtility.UrlEncode(Config.Options.Password)}&app=android&device_id={DeviceId}&sign={GetSign(string.Empty)}"); var data = await GetApiData(url); if (!string.IsNullOrWhiteSpace(data?.Token)) { Config.Logger.LogInformation("Успешно авторизовались"); + data.DeviceID = DeviceId; _token = data.Token; } else { throw new Exception($"Не удалось авторизоваться. {data?.Error}"); } + + if (Config.HasCredentials) { + await File.WriteAllTextAsync(saveCreds, JsonSerializer.Serialize(data)); + } } private async Task GetBook(string token, string bookId) { diff --git a/Core/Types/Litnet/LitnetAuthResponse.cs b/Core/Types/Litnet/LitnetAuthResponse.cs index e8dc4483..a5fe8b12 100644 --- a/Core/Types/Litnet/LitnetAuthResponse.cs +++ b/Core/Types/Litnet/LitnetAuthResponse.cs @@ -8,4 +8,7 @@ public class LitnetAuthResponse { [JsonPropertyName("token")] public string Token { get; set; } + + [JsonPropertyName("device_id")] + public string DeviceID { get; set; } } \ No newline at end of file From baab63025b83dbf05ab4894f9b24d6fe1fb6a517 Mon Sep 17 00:00:00 2001 From: RedBuld Date: Sat, 18 Apr 2026 13:51:31 +0300 Subject: [PATCH 09/13] fix --- Core/Logic/Getters/AuthorTodayGetter.cs | 4 ++-- Core/Types/AuthorToday/AuthorTodayChapter.cs | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Core/Logic/Getters/AuthorTodayGetter.cs b/Core/Logic/Getters/AuthorTodayGetter.cs index ab27c9ff..dc39d1c1 100644 --- a/Core/Logic/Getters/AuthorTodayGetter.cs +++ b/Core/Logic/Getters/AuthorTodayGetter.cs @@ -241,12 +241,12 @@ private async Task> FillChapters(AuthorTodayBookDetails boo private async Task> GetChapters(AuthorTodayBookDetails book) { var result = new List(); - foreach (var chapter in book.Chapters.Where(c => !c.IsDraft).OrderBy(c => c.SortOrder)) { + foreach (var chapter in book.Chapters.Where(c => !c.IsDraft && c.isAvailable).OrderBy(c => c.SortOrder)) { var uri = ApiUrl.MakeRelativeUri($"/v1/work/{book.Id}/chapter/{chapter.Id}/text"); Config.Logger.LogInformation($"Загружаю главу {chapter.Title.CoverQuotes()}"); var response = await Config.Client.SendWithTriesAsync(() => GetDefaultMessage(uri, _apiUrl)); var rchapter = await response.Content.ReadFromJsonAsync(); - if (rchapter != default) { + if (rchapter != default && rchapter.Text != "" ) { Config.Logger.LogInformation($"Загружена глава {rchapter.Title.CoverQuotes()}"); chapter.Text = rchapter.Text; chapter.Key = rchapter.Key; diff --git a/Core/Types/AuthorToday/AuthorTodayChapter.cs b/Core/Types/AuthorToday/AuthorTodayChapter.cs index 6b1ac2f3..f7e69637 100644 --- a/Core/Types/AuthorToday/AuthorTodayChapter.cs +++ b/Core/Types/AuthorToday/AuthorTodayChapter.cs @@ -28,6 +28,9 @@ public class AuthorTodayChapter { [JsonPropertyName("IsDraft")] public bool IsDraft { get; set; } + + [JsonPropertyName("isAvailable")] + public bool isAvailable { get; set; } [JsonPropertyName("code")] public string Code { get; set; } From a42d3942f349a7ef4d1a1f528c0098f0264238af Mon Sep 17 00:00:00 2001 From: user <51358775+L34T@users.noreply.github.com> Date: Thu, 18 Jun 2026 18:10:15 +0200 Subject: [PATCH 10/13] .editorconfig and jb cleanup --- .dockerignore | 4 +- .editorconfig | 31 ++ .gitattributes | 5 + .gitignore | 71 ++- Core/Configs/BookGetterConfig.cs | 191 +++++---- Core/Configs/Options.cs | 81 ++-- Core/Core.csproj | 4 +- Core/Exceptions/Elib2EbookAuthException.cs | 18 + Core/Exceptions/Elib2EbookException.cs | 18 + Core/Exceptions/Elib2EbookFormatException.cs | 18 + Core/Exceptions/Elib2EbookParseException.cs | 18 + Core/Extensions/FileProviderExtensions.cs | 18 +- Core/Extensions/HtmlDocumentExtensions.cs | 49 ++- Core/Extensions/HttpClientExtensions.cs | 178 ++++---- Core/Extensions/ImageFormatExtensions.cs | 13 +- Core/Extensions/StreamExtension.cs | 10 +- Core/Extensions/StringBuilderExtensions.cs | 12 +- Core/Extensions/StringExtensions.cs | 96 +++-- Core/Extensions/UriExtension.cs | 64 +-- Core/External/EpubSharp.deps.json | 33 +- Core/External/EpubSharp.dll | Bin 80896 -> 100864 bytes Core/External/EpubSharp.pdb | Bin 34756 -> 44588 bytes Core/Logic/BookNameBuilder.cs | 13 +- Core/Logic/Builders/AdditionaFileBuilder.cs | 55 ++- Core/Logic/Builders/BuilderBase.cs | 83 ++-- Core/Logic/Builders/CbzBuilder.cs | 45 +- Core/Logic/Builders/EpubBuilder.cs | 110 ++--- Core/Logic/Builders/EpubSharpCompat.cs | 249 +++++++++++ Core/Logic/Builders/Fb2Builder.cs | 160 +++---- Core/Logic/Builders/JsonBuilder.cs | 17 +- Core/Logic/Builders/JsonLiteBuilder.cs | 47 +- Core/Logic/Builders/TxtBuilder.cs | 42 +- Core/Logic/FileProvider.cs | 14 +- Core/Logic/Getters/AcomicsGetter.cs | 52 ++- Core/Logic/Getters/AuthorTodayGetter.cs | 241 ++++++----- Core/Logic/Getters/BookInBookGetter.cs | 101 +++-- Core/Logic/Getters/BookRiverGetter.cs | 131 +++--- Core/Logic/Getters/BookinistGetter.cs | 49 ++- .../BooksYandex/BooksYandexBooksGetter.cs | 138 +++--- .../BooksYandex/BooksYandexComicsGetter.cs | 28 +- .../BooksYandex/BooksYandexGetterBase.cs | 99 +++-- Core/Logic/Getters/BookstabGetter.cs | 77 ++-- Core/Logic/Getters/BookstimeGetter.cs | 104 +++-- Core/Logic/Getters/BookurukGetter.cs | 105 +++-- Core/Logic/Getters/BoostyGetter.cs | 85 ++-- Core/Logic/Getters/BoovelGetter.cs | 72 ++-- Core/Logic/Getters/DarkNovelsGetter.cs | 121 +++--- Core/Logic/Getters/DesuGetter.cs | 77 ++-- Core/Logic/Getters/DreameGetter.cs | 74 ++-- Core/Logic/Getters/FanFicusGetter.cs | 110 +++-- Core/Logic/Getters/Fb2Top/Fb2TopGetter.cs | 7 +- Core/Logic/Getters/Fb2Top/Fb2TopGetterBase.cs | 74 ++-- Core/Logic/Getters/Fb2Top/LadyLibGetter.cs | 7 +- Core/Logic/Getters/FicbookGetter.cs | 91 ++-- Core/Logic/Getters/FictionBookGetter.cs | 111 +++-- Core/Logic/Getters/FreedlitGetter.cs | 106 +++-- .../Getters/Freedom/BookHamsterGetter.cs | 5 +- Core/Logic/Getters/Freedom/FreedomGetter.cs | 5 +- .../Getters/Freedom/FreedomGetterBase.cs | 68 +-- Core/Logic/Getters/GetterBase.cs | 300 +++++++------ Core/Logic/Getters/HogwartsNetGetter.cs | 114 ++--- .../Getters/HotNovelPub/EzNovelsGetter.cs | 7 +- .../Getters/HotNovelPub/HotNovelPubGetter.cs | 7 +- .../HotNovelPub/HotNovelPubGetterBase.cs | 81 ++-- .../Getters/HotNovelPub/LaNovelsGetter.cs | 7 +- .../HotNovelPub/LightNovelDailyGetter.cs | 7 +- Core/Logic/Getters/HubBookGetter.cs | 48 ++- Core/Logic/Getters/IGramGetter.cs | 99 +++-- Core/Logic/Getters/JaomixGetter.cs | 72 ++-- .../Getters/LibSocial/HentaiLibGetter.cs | 5 +- .../Logic/Getters/LibSocial/MangaLibGetter.cs | 3 +- .../Getters/LibSocial/MangaLibGetterBase.cs | 13 +- .../Getters/LibSocial/MangaLibOrgGetter.cs | 3 +- .../LibSocial/NewLibSocialGetterBase.cs | 281 ++++++------ .../Getters/LibSocial/RanobeLibGetter.cs | 96 +++-- .../Logic/Getters/LibSocial/SlashLibGetter.cs | 11 +- Core/Logic/Getters/LibboxGetter.cs | 104 +++-- Core/Logic/Getters/LibkingGetter.cs | 104 +++-- Core/Logic/Getters/LibrebookGetter.cs | 101 +++-- Core/Logic/Getters/LibstGetter.cs | 115 ++--- Core/Logic/Getters/LitLifeGetter.cs | 115 ++--- Core/Logic/Getters/LitgorodGetter.cs | 127 +++--- Core/Logic/Getters/LitmarketGetter.cs | 166 +++---- Core/Logic/Getters/LitmirGetter.cs | 139 +++--- Core/Logic/Getters/Litnet/BooknetComGetter.cs | 7 +- Core/Logic/Getters/Litnet/BooknetUaGetter.cs | 7 +- Core/Logic/Getters/Litnet/LitnetGetter.cs | 7 +- Core/Logic/Getters/Litnet/LitnetGetterBase.cs | 245 ++++++----- Core/Logic/Getters/LitresGetter.cs | 405 ++++++++++-------- Core/Logic/Getters/LitsovetGetter.cs | 92 ++-- Core/Logic/Getters/MangaMammyGetter.cs | 69 +-- Core/Logic/Getters/MirKnigGetter.cs | 63 +-- Core/Logic/Getters/MyBookGetter.cs | 161 ++++--- Core/Logic/Getters/NeobookGetter.cs | 94 ++-- Core/Logic/Getters/NovelTranslateGetter.cs | 88 ++-- Core/Logic/Getters/NovelhallGetter.cs | 57 ++- Core/Logic/Getters/Novelxo/NovelxoGetter.cs | 7 +- .../Getters/Novelxo/NovelxoGetterBase.cs | 143 ++++--- Core/Logic/Getters/Novelxo/NovelxoRuGetter.cs | 7 +- Core/Logic/Getters/OnlineKnigiGetter.cs | 51 ++- Core/Logic/Getters/ProdamanGetter.cs | 178 ++++---- Core/Logic/Getters/RanobeGetter.cs | 47 +- Core/Logic/Getters/RanobeHubGetter.cs | 75 ++-- Core/Logic/Getters/RanobeNovelsGetter.cs | 70 +-- .../Logic/Getters/RanobeOvh/MangaOvhGetter.cs | 17 +- .../Logic/Getters/RanobeOvh/NovelOvhGetter.cs | 92 ++-- .../Getters/RanobeOvh/RanobeOvhGetterBase.cs | 64 +-- Core/Logic/Getters/RanobesComGetter.cs | 107 +++-- Core/Logic/Getters/ReadMangaGetter.cs | 75 ++-- Core/Logic/Getters/ReadliGetter.cs | 144 ++++--- Core/Logic/Getters/Renovels/RemangaGetter.cs | 27 +- Core/Logic/Getters/Renovels/RenovelsGetter.cs | 14 +- .../Getters/Renovels/RenovelsGetterBase.cs | 113 +++-- Core/Logic/Getters/RomfantGetter.cs | 83 ++-- Core/Logic/Getters/RoyalRoadGetter.cs | 66 +-- Core/Logic/Getters/Rulate/BllateGetter.cs | 5 +- Core/Logic/Getters/Rulate/ErolateGetter.cs | 7 +- Core/Logic/Getters/Rulate/MlateGetter.cs | 7 +- Core/Logic/Getters/Rulate/RulateGetter.cs | 7 +- Core/Logic/Getters/Rulate/RulateGetterBase.cs | 139 +++--- Core/Logic/Getters/SamlibGetter.cs | 120 +++--- Core/Logic/Getters/StrokiMtsGetter.cs | 212 ++++----- Core/Logic/Getters/TopLiba/BiglibaGetter.cs | 31 +- Core/Logic/Getters/TopLiba/TopLibaGetter.cs | 31 +- .../Getters/TopLiba/TopLibaGetterBase.cs | 93 ++-- Core/Logic/Getters/TwilightRussiaGetter.cs | 67 +-- Core/Logic/Getters/WattpadGetter.cs | 60 +-- Core/Logic/Getters/WuxiaWorldGetter.cs | 78 ++-- Core/Logic/Getters/YounettranslateGetter.cs | 68 +-- Core/Misc/AdditionalTypeEnum.cs | 5 +- Core/Misc/BuilderProvider.cs | 11 +- Core/Misc/GetterProvider.cs | 10 +- Core/Misc/TempFolder/TempFolder.cs | 23 +- Core/Misc/TempFolder/TempFolderFactory.cs | 21 +- .../AuthorToday/AuthorTodayAuthResponse.cs | 15 +- .../AuthorToday/AuthorTodayBookDetails.cs | 72 ++-- Core/Types/AuthorToday/AuthorTodayChapter.cs | 68 ++- Core/Types/AuthorToday/AuthorTodayImage.cs | 8 +- Core/Types/AuthorToday/AuthorTodayUser.cs | 10 +- Core/Types/Book/AdditionalFileCollection.cs | 38 +- Core/Types/Book/Author.cs | 22 +- Core/Types/Book/Book.cs | 49 ++- Core/Types/Book/Chapter.cs | 21 +- Core/Types/Book/Seria.cs | 17 +- .../BookYandex/BooksYandexAudioBitrate.cs | 8 +- .../BookYandex/BooksYandexAudioOffline.cs | 13 +- .../Types/BookYandex/BooksYandexAudioTrack.cs | 8 +- Core/Types/BookYandex/BooksYandexAuthor.cs | 13 +- Core/Types/BookYandex/BooksYandexBook.cs | 55 ++- .../BookYandex/BooksYandexComicContent.cs | 8 +- .../BookYandex/BooksYandexComicMetadata.cs | 8 +- Core/Types/BookYandex/BooksYandexComicPage.cs | 8 +- Core/Types/BookYandex/BooksYandexCover.cs | 13 +- Core/Types/BookYandex/BooksYandexEpisode.cs | 18 +- Core/Types/BookYandex/BooksYandexEpisodes.cs | 8 +- Core/Types/BookYandex/BooksYandexPlaylist.cs | 8 +- Core/Types/BookYandex/BooksYandexResponse.cs | 18 +- Core/Types/BookYandex/BooksYandexUri.cs | 13 +- Core/Types/Bookriver/BookRiverApiResponse.cs | 10 +- Core/Types/Bookriver/BookRiverAuthResponse.cs | 10 +- Core/Types/Bookriver/BookRiverBook.cs | 7 +- Core/Types/Bookriver/BookRiverChapter.cs | 15 +- .../Bookriver/BookRiverChapterContent.cs | 10 +- Core/Types/Bookriver/BookRiverCurrentBook.cs | 10 +- Core/Types/Bookstab/BookstabApiResponse.cs | 15 +- Core/Types/Bookstab/BookstabBook.cs | 28 +- Core/Types/Bookstab/BookstabChapter.cs | 22 +- Core/Types/Bookstab/BookstabUser.cs | 15 +- Core/Types/Bookstime/BookstimeAuthResponse.cs | 7 +- Core/Types/Boosty/BoostyApiResponse.cs | 13 +- Core/Types/Boosty/BoostyExtra.cs | 13 +- Core/Types/Boosty/BoostyPost.cs | 18 +- Core/Types/Common/IdChapter.cs | 4 +- Core/Types/Common/TempFile.cs | 73 ++-- Core/Types/Common/UrlChapter.cs | 4 +- Core/Types/DarkNovels/DarkNovelsAuthData.cs | 10 +- Core/Types/DarkNovels/DarkNovelsAuthToken.cs | 10 +- Core/Types/DarkNovels/DarkNovelsChapter.cs | 22 +- Core/Types/DarkNovels/DarkNovelsData.cs | 22 +- Core/Types/Dreame/DreameApiResponse.cs | 10 +- Core/Types/Dreame/DreameCatalog.cs | 10 +- Core/Types/Dreame/DreameChapter.cs | 22 +- Core/Types/Dreame/DreameNovel.cs | 42 +- Core/Types/Dreame/DreamePager.cs | 10 +- Core/Types/FanFicus/FanFicusApiResponse.cs | 8 +- Core/Types/FanFicus/FanFicusBook.cs | 23 +- Core/Types/FanFicus/FanFicusChapter.cs | 8 +- Core/Types/FanFicus/FanFicusCreator.cs | 13 +- Core/Types/FanFicus/FanFicusImage.cs | 8 +- Core/Types/FanFicus/FanFicusPart.cs | 13 +- Core/Types/FanFicus/FanFicusUser.cs | 8 +- Core/Types/Freedlit/FreedlitApiResponse.cs | 8 +- Core/Types/Freedlit/FreedlitApp.cs | 8 +- Core/Types/Freedlit/FreedlitAuthError.cs | 15 +- Core/Types/Freedlit/FreedlitAuthor.cs | 18 +- Core/Types/Freedlit/FreedlitBook.cs | 33 +- Core/Types/Freedlit/FreedlitChapter.cs | 18 +- Core/Types/Freedlit/FreedlitItemsContent.cs | 8 +- Core/Types/Freedlit/FreedlitProps.cs | 13 +- .../Types/Freedlit/FreedlitSuccessResponse.cs | 8 +- .../HotNovelPub/HotNovelPubApiResponse.cs | 10 +- .../Types/HotNovelPub/HotNovelPubAuthorize.cs | 22 +- Core/Types/HotNovelPub/HotNovelPubBook.cs | 25 +- .../HotNovelPub/HotNovelPubBookResponse.cs | 15 +- Core/Types/HotNovelPub/HotNovelPubChapter.cs | 22 +- Core/Types/Librebook/LibrebookAuthResponse.cs | 13 +- Core/Types/Litexit/LitexitChapter.cs | 22 +- Core/Types/Litexit/LitexitUser.cs | 10 +- Core/Types/Litgorod/LitgorodAuthResponse.cs | 10 +- Core/Types/Litgorod/LitgorodBookPart.cs | 10 +- Core/Types/Litgorod/LitgorodChapter.cs | 7 +- Core/Types/Litmarket/AuthResponse.cs | 10 +- Core/Types/Litmarket/Block.cs | 15 +- Core/Types/Litmarket/Chunk.cs | 10 +- Core/Types/Litmarket/LBook.cs | 10 +- Core/Types/Litmarket/Mod.cs | 28 +- Core/Types/Litmarket/Response.cs | 13 +- Core/Types/Litnet/LitnetAuthResponse.cs | 22 +- Core/Types/Litnet/LitnetBookResponse.cs | 62 ++- Core/Types/Litnet/LitnetChapterResponse.cs | 15 +- Core/Types/Litnet/LitnetContentsResponse.cs | 15 +- Core/Types/Litres/LitresBook.cs | 7 +- Core/Types/Litres/LitresPayload.cs | 52 ++- Core/Types/Litres/LitresTarget.cs | 7 +- Core/Types/Litres/Requests/LitresAuthData.cs | 15 +- .../Litres/Requests/LitresAuthRequest.cs | 10 +- .../Litres/Requests/LitresBrowseArtsData.cs | 22 +- .../Requests/LitresBrowseArtsRequest.cs | 10 +- .../Litres/Requests/LitresRequestBase.cs | 22 +- Core/Types/Litres/Response/LitresArt.cs | 47 +- .../Litres/Response/LitresArtTypeEnum.cs | 5 +- .../Litres/Response/LitresAuthResponseData.cs | 22 +- Core/Types/Litres/Response/LitresFile.cs | 13 +- Core/Types/Litres/Response/LitresFiles.cs | 8 +- Core/Types/Litres/Response/LitresMe.cs | 5 +- .../Response/LitresPartnerSubscriptions.cs | 8 +- Core/Types/Litres/Response/LitresPerson.cs | 25 +- Core/Types/Litres/Response/LitresResponse.cs | 10 +- Core/Types/Litres/Response/LitresSeria.cs | 22 +- .../Litres/Response/LitresStaticResponse.cs | 10 +- .../Litres/Response/LitresSubscription.cs | 18 +- .../Litres/Response/LitresWebAuthResponse.cs | 19 +- Core/Types/Litsovet/LitsovetLoginResponse.cs | 10 +- Core/Types/MangaLib/MangaLibPg.cs | 15 +- Core/Types/MyBook/MyBookAuth.cs | 15 +- Core/Types/MyBook/MyBookAuthor.cs | 15 +- Core/Types/MyBook/MyBookBook.cs | 57 ++- Core/Types/MyBook/MyBookFile.cs | 18 +- Core/Types/MyBook/MyBookMapFile.cs | 8 +- Core/Types/Neobook/NeobookAttachment.cs | 10 +- Core/Types/Neobook/NeobookAuth.cs | 15 +- Core/Types/Neobook/NeobookBook.cs | 10 +- Core/Types/Neobook/NeobookChapter.cs | 8 +- Core/Types/Neobook/NeobookChapterData.cs | 8 +- Core/Types/Neobook/NeobookLogin.cs | 15 +- Core/Types/Neobook/NeobookPostData.cs | 30 +- Core/Types/Neobook/NeobookTocChapter.cs | 15 +- Core/Types/Neobook/NeobookUser.cs | 22 +- Core/Types/Ranobe/RanobeBook.cs | 30 +- Core/Types/Ranobe/RanobeChapter.cs | 10 +- Core/Types/Ranobe/RanobeChapterContent.cs | 10 +- Core/Types/Ranobe/RanobeChapterShort.cs | 15 +- Core/Types/Ranobe/RanobeImage.cs | 13 +- Core/Types/RanobeHub/RanobeHubApiResponse.cs | 10 +- Core/Types/RanobeHub/RanobeHubChapter.cs | 13 +- Core/Types/RanobeHub/RanobeHubVolume.cs | 10 +- .../Types/RanobeNovels/RanobeNovelsChapter.cs | 15 +- Core/Types/RanobeOvh/RanobeOvhBranch.cs | 22 +- Core/Types/RanobeOvh/RanobeOvhChapterFull.cs | 80 ++-- Core/Types/RanobeOvh/RanobeOvhManga.cs | 22 +- Core/Types/RanobeOvh/RanobeOvhMangaName.cs | 10 +- Core/Types/RanobeOvh/RanobeOvhPage.cs | 13 +- Core/Types/RanobeOvh/RanoveOvhTranslator.cs | 15 +- Core/Types/Ranobes/RanobesCookie.cs | 10 +- Core/Types/Renovels/RenovelsApiResponse.cs | 13 +- Core/Types/Renovels/RenovelsAuthResponse.cs | 10 +- Core/Types/Renovels/RenovelsBranch.cs | 15 +- Core/Types/Renovels/RenovelsChapter.cs | 59 ++- Core/Types/Renovels/RenovelsContent.cs | 33 +- Core/Types/Renovels/RenovelsPage.cs | 10 +- Core/Types/Renovels/RenovelsPublisher.cs | 15 +- Core/Types/Renovels/RenovelsTocResponse.cs | 8 +- Core/Types/Rulate/RulateAuthResponse.cs | 10 +- Core/Types/Rulate/RulateChapter.cs | 15 +- Core/Types/SocialLib/LibSocialToken.cs | 8 +- Core/Types/SocialLib/RanobeLibBookDetail.cs | 54 ++- Core/Types/SocialLib/SocialLibBookChapters.cs | 108 +++-- Core/Types/SocialLib/SocialLibChapter.cs | 26 +- Core/Types/SocialLib/SocialLibChapterOld.cs | 24 +- Core/Types/SocialLib/SocialLibChapters.cs | 10 +- Core/Types/SocialLib/User.cs | 10 +- Core/Types/SocialLib/WindowData.cs | 15 +- .../StrokiMts/StrokiMtsApiMultiResponse.cs | 8 +- Core/Types/StrokiMts/StrokiMtsApiResponse.cs | 8 +- Core/Types/StrokiMts/StrokiMtsAuthor.cs | 13 +- Core/Types/StrokiMts/StrokiMtsBookItem.cs | 33 +- Core/Types/StrokiMts/StrokiMtsContentInfo.cs | 8 +- Core/Types/StrokiMts/StrokiMtsFile.cs | 8 +- Core/Types/StrokiMts/StrokiMtsFileUrl.cs | 8 +- Core/Types/StrokiMts/StrokiMtsFiles.cs | 13 +- Core/Types/StrokiMts/StrokiMtsMultiItem.cs | 23 +- Core/Types/Wattpad/WattpadInfo.cs | 28 +- Core/Types/Wattpad/WattpadMeta.cs | 7 +- Core/Types/Wattpad/WattpadPart.cs | 15 +- Core/Types/Wattpad/WattpadUser.cs | 10 +- Core/Types/WuxiaWorld/WuxiaWorldToc.cs | 10 +- Elib2EbookCli/ConsoleLogger.cs | 17 +- Elib2EbookCli/Elib2EbookCli.csproj | 4 +- Elib2EbookCli/Program.cs | 77 ++-- Elib2EbookWeb/Components/App.razor | 24 +- Elib2EbookWeb/Components/BookForm.razor | 95 ++-- .../Components/Layout/MainLayout.razor | 15 +- Elib2EbookWeb/Components/PWA.razor | 4 +- Elib2EbookWeb/Components/Pages/Error.razor | 14 +- Elib2EbookWeb/Components/Pages/Home.razor | 44 +- Elib2EbookWeb/Components/Routes.razor | 7 +- Elib2EbookWeb/Components/ShortBook.razor | 50 ++- Elib2EbookWeb/Components/_Imports.razor | 2 +- Elib2EbookWeb/Elib2EbookWeb.csproj | 2 +- Elib2EbookWeb/Misc/SbWriter.cs | 24 +- Elib2EbookWeb/Program.cs | 10 +- global.json | 6 + scripts/fetch-epubsharp.ps1 | 90 ++++ scripts/fetch-epubsharp.sh | 70 +++ 324 files changed, 7474 insertions(+), 5869 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 Core/Exceptions/Elib2EbookAuthException.cs create mode 100644 Core/Exceptions/Elib2EbookException.cs create mode 100644 Core/Exceptions/Elib2EbookFormatException.cs create mode 100644 Core/Exceptions/Elib2EbookParseException.cs create mode 100644 Core/Logic/Builders/EpubSharpCompat.cs create mode 100644 global.json create mode 100644 scripts/fetch-epubsharp.ps1 create mode 100644 scripts/fetch-epubsharp.sh diff --git a/.dockerignore b/.dockerignore index cd967fc3..2f32bfe4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,4 @@ -**/.dockerignore +**/.dockerignore **/.env **/.git **/.gitignore @@ -22,4 +22,4 @@ **/secrets.dev.yaml **/values.dev.yaml LICENSE -README.md \ No newline at end of file +README.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..4cd39670 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,31 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 +tab_width = 4 + +[*.{json,map}] +indent_size = 2 +tab_width = 2 + +[*.{yml,yaml}] +indent_size = 2 +tab_width = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.cs] +# C# Coding Conventions +csharp_style_pattern_matching_over_as_with_null_check = true:error +csharp_style_prefer_pattern_matching = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +dotnet_style_prefer_isnull_check_over_reference_equality_method = true:suggestion + +# Simplify 'default' expression warnings (IDE0034) +dotnet_diagnostic.IDE0034.severity = warning diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..1c3ca8cb --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto eol=lf +*.dll binary +*.pdb binary +*.png binary +*.ttf binary diff --git a/.gitignore b/.gitignore index 4ae9628d..53bd48ec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,47 +1,28 @@ -**/bin/* -**/obj/* -**_ReSharper* -**/.sonar/* -Code/**/packages/* -**.suo -**.user -**.dbmdl -**.schemaview -DB/**/sql -**.orig -**.rdl.data -**/StyleCopViolations.xml -_references.js -*_*_*.csproj.swp -*_*_*.csproj -/Code/.vs -/Code/.vscode/ -*.nupkg -**/node_modules/** -**/react-widgets/dist/** -**/react-widgets/*.log -/Code/.idea/.idea.IDEV/.idea/.name -/Code/.idea/.idea.IDEV/.idea/contentModel.xml -/Code/.idea/.idea.IDEV/.idea/modules.xml -/Code/.idea/.idea.IDEV/.idea/vcs.xml -/Code/.idea/.idea.IDEV/.idea/workspace.xml -/Code/.idea/.idea.IDEV/riderModule.iml -/Code/.idea/config/applicationhost.config -/Code/.idea/.idea.AllIn/.idea/.name -/Code/.idea/.idea.AllIn/.idea/contentModel.xml -/Code/.idea/.idea.AllIn/.idea/modules.xml -/Code/.idea/.idea.AllIn/.idea/workspace.xml -/Code/.idea/.idea.AllIn/.idea/vcs.xml -/Code/.idea/.idea.AllIn/riderModule.iml -/Code/.idea -Code/\.idea/\.idea\.AllIn/\.idea/ -.idea -*/.idea/* -**/logs/** -dist -.dist -.vs -.vscode/launch.json -.vscode/tasks.json +**/bin/* +**/obj/* +**_ReSharper* +**/.sonar/* +**.suo +**.user +**.dbmdl +**.schemaview +DB/**/sql +**.orig +**.rdl.data +**/StyleCopViolations.xml +_references.js +*_*_*.csproj.swp +*_*_*.csproj +*.nupkg +**/node_modules/** +**/react-widgets/dist/** +**/react-widgets/*.log +**/.idea/ +**/logs/** +dist +.dist +**/.vs/ +.vscode/launch.json +.vscode/tasks.json .dotnet/ diff --git a/Core/Configs/BookGetterConfig.cs b/Core/Configs/BookGetterConfig.cs index 5f24d16d..c1616afc 100644 --- a/Core/Configs/BookGetterConfig.cs +++ b/Core/Configs/BookGetterConfig.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net; using System.Net.Http; using System.Threading; @@ -8,86 +8,64 @@ using FlareSolverrSharp; using Microsoft.Extensions.Logging; -namespace Core.Configs; +namespace Core.Configs; -public class BookGetterConfig : IDisposable { +public class BookGetterConfig : IDisposable +{ public readonly ILogger Logger; - private class RedirectHandler : HttpClientHandler { - private readonly ILogger _logger; - public int Delay { get; set; } - - public RedirectHandler(ILogger logger) { - _logger = logger; - } - - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - try { - var responseMessage = await base.SendAsync(request, cancellationToken); - - if (responseMessage is { - StatusCode: HttpStatusCode.Redirect or HttpStatusCode.PermanentRedirect or HttpStatusCode.MovedPermanently or HttpStatusCode.Moved, Headers.Location: not null - }) { - request = new HttpRequestMessage(request.Method, responseMessage.Headers.Location); - responseMessage = await base.SendAsync(request, cancellationToken); - } - - if (responseMessage.StatusCode == HttpStatusCode.TooManyRequests) { - responseMessage.StatusCode = HttpStatusCode.Forbidden; - } - - return responseMessage; - } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - _logger.LogInformation("Сервер не успевает ответить. Попробуйте увеличить Timeout с помощью параметра -t"); - throw; - } catch (TaskCanceledException ex) { - _logger.LogInformation(ex, "Сервер не успевает ответить. Попробуйте увеличить Timeout с помощью параметра -t"); - throw; - } catch (Exception ex) { - _logger.LogInformation(ex.Message); - throw; - } finally { - if (Delay > 0) { - _logger.LogInformation($"Жду {Delay} секунд(-ы)"); - await Task.Delay(TimeSpan.FromSeconds(Delay), cancellationToken); - } - } - } + public BookGetterConfig(Options options, HttpClient client, CookieContainer cookieContainer, TempFolder tempFolder, + ILogger logger) + { + Logger = logger; + Client = client; + CookieContainer = cookieContainer; + Options = options; + TempFolder = tempFolder; } - + public HttpClient Client { get; } public Options Options { get; } - + public CookieContainer CookieContainer { get; } - - public bool HasCredentials => !string.IsNullOrWhiteSpace(Options.Login) && !string.IsNullOrWhiteSpace(Options.Password); - public TempFolder TempFolder { get; } + public bool HasCredentials => + !string.IsNullOrWhiteSpace(Options.Login) && !string.IsNullOrWhiteSpace(Options.Password); - public BookGetterConfig(Options options, HttpClient client, CookieContainer cookieContainer, TempFolder tempFolder, ILogger logger) { - Logger = logger; - Client = client; - CookieContainer = cookieContainer; - Options = options; - TempFolder = tempFolder; - } + public TempFolder TempFolder { get; } - public void Dispose() { + public void Dispose() + { Client?.Dispose(); TempFolder?.Dispose(); } - - public static BookGetterConfig GetDefault(Options options, ILogger logger) { + + public string GetSetting(string key, string defaultValue = null) + { + return Options.Settings.TryGetValue(key, out var val) ? val : defaultValue; + } + + public bool GetBoolSetting(string key, bool defaultValue = false) + { + if (Options.Settings.TryGetValue(key, out var val) && bool.TryParse(val, out var res)) return res; + return defaultValue; + } + + public static BookGetterConfig GetDefault(Options options, ILogger logger) + { var cookieContainer = new CookieContainer(); var client = GetClient(options, cookieContainer, logger); - return new BookGetterConfig(options, client, cookieContainer, TempFolderFactory.Create(options.TempPath), logger); + return new BookGetterConfig(options, client, cookieContainer, TempFolderFactory.Create(options.TempPath), + logger); } - - private static HttpClient GetClient(Options options, CookieContainer container, ILogger logger) { - var handler = new RedirectHandler(logger) { - AutomaticDecompression = DecompressionMethods.GZip | + + private static HttpClient GetClient(Options options, CookieContainer container, ILogger logger) + { + var handler = new RedirectHandler(logger) + { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli, ServerCertificateCustomValidationCallback = (_, _, _, _) => true, @@ -97,31 +75,94 @@ private static HttpClient GetClient(Options options, CookieContainer container, Delay = options.Delay }; - if (!string.IsNullOrEmpty(options.Proxy)) { + if (!string.IsNullOrEmpty(options.Proxy)) + { handler.Proxy = new WebProxy(options.Proxy.AsUri()); handler.UseProxy = true; } - if (!string.IsNullOrWhiteSpace(options.Flare)) { - var chandler = new ClearanceHandler(options.Flare) { + if (!string.IsNullOrWhiteSpace(options.Flare)) + { + var chandler = new ClearanceHandler(options.Flare) + { MaxTimeout = options.Timeout * 1000, InnerHandler = handler }; - - if (!string.IsNullOrEmpty(options.Proxy)) { - chandler.ProxyUrl = options.Proxy; - } - + + if (!string.IsNullOrEmpty(options.Proxy)) chandler.ProxyUrl = options.Proxy; + var cclient = new HttpClient(chandler); cclient.Timeout = TimeSpan.FromSeconds(options.Timeout); - + return cclient; } var client = new HttpClient(handler); - + client.Timeout = TimeSpan.FromSeconds(options.Timeout); - + return client; } -} \ No newline at end of file + + private class RedirectHandler : HttpClientHandler + { + private readonly ILogger _logger; + + public RedirectHandler(ILogger logger) + { + _logger = logger; + } + + public int Delay { get; set; } + + protected override async Task SendAsync(HttpRequestMessage request, + CancellationToken cancellationToken) + { + try + { + var responseMessage = await base.SendAsync(request, cancellationToken); + + if (responseMessage is + { + StatusCode: HttpStatusCode.Redirect or HttpStatusCode.PermanentRedirect + or HttpStatusCode.MovedPermanently or HttpStatusCode.Moved, + Headers.Location: not null + }) + { + request = new HttpRequestMessage(request.Method, responseMessage.Headers.Location); + responseMessage = await base.SendAsync(request, cancellationToken); + } + + if (responseMessage.StatusCode == HttpStatusCode.TooManyRequests) + responseMessage.StatusCode = HttpStatusCode.Forbidden; + + return responseMessage; + } + catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) + { + _logger.LogInformation( + "Сервер не успевает ответить. Попробуйте увеличить Timeout с помощью параметра -t"); + throw; + } + catch (TaskCanceledException ex) + { + _logger.LogInformation(ex, + "Сервер не успевает ответить. Попробуйте увеличить Timeout с помощью параметра -t"); + throw; + } + catch (Exception ex) + { + _logger.LogInformation(ex.Message); + throw; + } + finally + { + if (Delay > 0) + { + _logger.LogInformation($"Жду {Delay} секунд(-ы)"); + await Task.Delay(TimeSpan.FromSeconds(Delay), cancellationToken); + } + } + } + } +} diff --git a/Core/Configs/Options.cs b/Core/Configs/Options.cs index a0efbe48..ad93bc3c 100644 --- a/Core/Configs/Options.cs +++ b/Core/Configs/Options.cs @@ -1,81 +1,91 @@ +using System; using System.Collections.Generic; using System.Linq; using CommandLine; using Core.Misc; -namespace Core.Configs; +namespace Core.Configs; -public class Options { - public Options() { } +public class Options +{ + private Dictionary _settings; - public Options(IEnumerable url) { + public Options() + { + } + + public Options(IEnumerable url) + { Url = url; } [Option('u', "url", Required = true, HelpText = "Ссылка на книгу", Separator = ',')] public IEnumerable Url { get; set; } - - [Option('f', "format", Required = true, HelpText = "Формат для сохранения книги. Допустимые значения: epub, fb2, cbz, json", Separator = ',')] + + [Option('f', "format", Required = true, + HelpText = "Формат для сохранения книги. Допустимые значения: epub, fb2, cbz, json", Separator = ',')] public IEnumerable Format { get; set; } - + [Option('l', "login", Required = false, HelpText = "Логин от системы")] public string Login { get; set; } - + [Option('p', "password", Required = false, HelpText = "Пароль от системы")] public string Password { get; set; } [Option('k', "token", Required = false, HelpText = "Готовый токен авторизации")] public string Token { get; set; } - + [Option("proxy", Required = false, HelpText = "Прокси в формате :")] public string Proxy { get; set; } - + [Option("flare", Required = false, HelpText = "Адрес flaresolverr")] public string Flare { get; set; } - + [Option('s', "save", Required = false, HelpText = "Директория для сохранения книги")] public string SavePath { get; set; } [Option('c', "cover", Required = false, HelpText = "Сохранить обложку книги в отдельный файл")] public bool Cover { get; set; } - + [Option('t', "timeout", Required = false, HelpText = "Timeout для запросов в секундах", Default = 120)] public int Timeout { get; set; } - + [Option('d', "delay", Required = false, HelpText = "Задержка между запросами в секундах", Default = 0)] public int Delay { get; set; } - + [Option("no-image", Required = false, HelpText = "Не загружать картинки")] public bool NoImage { get; set; } [Option("start", Required = false, HelpText = "Стартовый номер главы")] public int? Start { get; set; } - + [Option("end", Required = false, HelpText = "Конечный номер главы")] public int? End { get; set; } [Option("start-name", Required = false, HelpText = "Стартовое название главы")] public string StartName { get; set; } - + [Option("end-name", Required = false, HelpText = "Конечное название главы")] public string EndName { get; set; } [Option("temp", Required = false, HelpText = "Папка для временного хранения картинок")] public string TempPath { get; set; } - + [Option("save-temp", Required = false, HelpText = "Сохранять временные файлы", Default = false)] public bool SaveTemp { get; set; } - + [Option("no-chapters", Required = false, HelpText = "Не загружать главы", Default = false)] public bool NoChapters { get; set; } - + [Option("additional", Required = false, HelpText = "Сохранить дополнительные файлы", Default = false)] public bool Additional { get; set; } - [Option("additional-types", Required = false, HelpText = "Типы дополнительных файлов. Допустимые значения: books, audio, images", Separator = ',')] + [Option("additional-types", Required = false, + HelpText = "Типы дополнительных файлов. Допустимые значения: books, audio, images", Separator = ',')] public IEnumerable AdditionalTypes { get; set; } - [Option("book-name-pattern", Required = false, HelpText = "Шаблон имени файла.", Default = "{Author.Name} - {Book.Title}")] + [Option("book-name-pattern", Required = false, HelpText = "Шаблон имени файла.", + Default = "{Author.Name} - {Book.Title}")] public string BookNamePattern { get; set; } = "{Author.Name} - {Book.Title}"; [Option("split-volumes", Required = false, HelpText = "Разбивать по томам (если поддерживается)")] @@ -83,8 +93,31 @@ public Options(IEnumerable url) { [Option("split-chapters", Required = false, HelpText = "Разбивать по главам (если поддерживается)")] public bool SplitChapters { get; set; } - - public bool HasAdditionalType(AdditionalTypeEnum type) => Additional && (AdditionalTypes == default || !AdditionalTypes.Any() || AdditionalTypes.Contains(type)); + + [Option('o', "option", Required = false, HelpText = "Дополнительные параметры провайдеров в формате Ключ=Значение")] + public IEnumerable CustomOptions { get; set; } + + public Dictionary Settings => _settings ??= ParseCustomOptions(CustomOptions); public string ResourcesPath => "Patterns"; -} \ No newline at end of file + + private static Dictionary ParseCustomOptions(IEnumerable options) + { + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (options == null) return dict; + foreach (var opt in options) + { + var parts = opt.Split('=', 2); + if (parts.Length == 2) + dict[parts[0].Trim()] = parts[1].Trim(); + else if (parts.Length == 1) dict[parts[0].Trim()] = "true"; + } + + return dict; + } + + public bool HasAdditionalType(AdditionalTypeEnum type) + { + return Additional && (AdditionalTypes is null || !AdditionalTypes.Any() || AdditionalTypes.Contains(type)); + } +} diff --git a/Core/Core.csproj b/Core/Core.csproj index 42e38bb9..c04aca5e 100644 --- a/Core/Core.csproj +++ b/Core/Core.csproj @@ -1,10 +1,10 @@ - net9.0 + net10.0 latestmajor Core - 2.8.1 + 2.8.2 true diff --git a/Core/Exceptions/Elib2EbookAuthException.cs b/Core/Exceptions/Elib2EbookAuthException.cs new file mode 100644 index 00000000..73972b7a --- /dev/null +++ b/Core/Exceptions/Elib2EbookAuthException.cs @@ -0,0 +1,18 @@ +using System; + +namespace Core.Exceptions; + +public class Elib2EbookAuthException : Elib2EbookException +{ + public Elib2EbookAuthException() + { + } + + public Elib2EbookAuthException(string message) : base(message) + { + } + + public Elib2EbookAuthException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/Core/Exceptions/Elib2EbookException.cs b/Core/Exceptions/Elib2EbookException.cs new file mode 100644 index 00000000..c91627ec --- /dev/null +++ b/Core/Exceptions/Elib2EbookException.cs @@ -0,0 +1,18 @@ +using System; + +namespace Core.Exceptions; + +public class Elib2EbookException : Exception +{ + public Elib2EbookException() + { + } + + public Elib2EbookException(string message) : base(message) + { + } + + public Elib2EbookException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/Core/Exceptions/Elib2EbookFormatException.cs b/Core/Exceptions/Elib2EbookFormatException.cs new file mode 100644 index 00000000..f240b015 --- /dev/null +++ b/Core/Exceptions/Elib2EbookFormatException.cs @@ -0,0 +1,18 @@ +using System; + +namespace Core.Exceptions; + +public class Elib2EbookFormatException : Elib2EbookException +{ + public Elib2EbookFormatException() + { + } + + public Elib2EbookFormatException(string message) : base(message) + { + } + + public Elib2EbookFormatException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/Core/Exceptions/Elib2EbookParseException.cs b/Core/Exceptions/Elib2EbookParseException.cs new file mode 100644 index 00000000..eef413a5 --- /dev/null +++ b/Core/Exceptions/Elib2EbookParseException.cs @@ -0,0 +1,18 @@ +using System; + +namespace Core.Exceptions; + +public class Elib2EbookParseException : Elib2EbookException +{ + public Elib2EbookParseException() + { + } + + public Elib2EbookParseException(string message) : base(message) + { + } + + public Elib2EbookParseException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/Core/Extensions/FileProviderExtensions.cs b/Core/Extensions/FileProviderExtensions.cs index 3df7a5ec..3860dfdc 100644 --- a/Core/Extensions/FileProviderExtensions.cs +++ b/Core/Extensions/FileProviderExtensions.cs @@ -6,27 +6,33 @@ namespace Core.Extensions; -public static class FileProviderExtensions { - public static byte[] ReadAllBytes(this IFileInfo fileInfo) { +public static class FileProviderExtensions +{ + public static byte[] ReadAllBytes(this IFileInfo fileInfo) + { using var ms = new MemoryStream(); using var source = fileInfo.CreateReadStream(); source.CopyTo(ms); return ms.ToArray(); } - public static string ReadAllText(this IFileInfo fileInfo) { + public static string ReadAllText(this IFileInfo fileInfo) + { using var source = fileInfo.CreateReadStream(); using var sr = new StreamReader(source); return sr.ReadToEnd(); } - public static string ReadAllText(this IFileProvider fileProvider, string path) { + public static string ReadAllText(this IFileProvider fileProvider, string path) + { var file = fileProvider.GetFileInfo(path); return file.ReadAllText(); } - public static IEnumerable GetFiles(this IFileProvider fileProvider, string directory, string searchPattern) { + public static IEnumerable GetFiles(this IFileProvider fileProvider, string directory, + string searchPattern) + { return fileProvider.GetDirectoryContents(directory) .Where(file => FileSystemName.MatchesSimpleExpression(searchPattern, file.Name)); } -} \ No newline at end of file +} diff --git a/Core/Extensions/HtmlDocumentExtensions.cs b/Core/Extensions/HtmlDocumentExtensions.cs index f8376966..cf7738c3 100644 --- a/Core/Extensions/HtmlDocumentExtensions.cs +++ b/Core/Extensions/HtmlDocumentExtensions.cs @@ -5,10 +5,12 @@ using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; -namespace Core.Extensions; +namespace Core.Extensions; -public static class HtmlDocumentExtensions { - public static string AsString(this HtmlDocument self) { +public static class HtmlDocumentExtensions +{ + public static string AsString(this HtmlDocument self) + { using var sw = new StringWriter(); using var xw = new XmlTextWriter(sw); self.Save(xw); @@ -16,43 +18,46 @@ public static string AsString(this HtmlDocument self) { return sw.ToString(); } - public static string GetTextBySelector(this HtmlDocument doc, string selector) { + public static string GetTextBySelector(this HtmlDocument doc, string selector) + { return doc.DocumentNode.GetTextBySelector(selector); } - - public static string GetTextBySelector(this HtmlNode node, string selector) { + + public static string GetTextBySelector(this HtmlNode node, string selector) + { return node.QuerySelector(selector).GetText(); } - - public static string GetText(this HtmlNode node) { + + public static string GetText(this HtmlNode node) + { return node?.InnerText?.HtmlDecode(); } - public static HtmlDocument RemoveNodes(this HtmlDocument doc, Func predicate) { + public static HtmlDocument RemoveNodes(this HtmlDocument doc, Func predicate) + { doc.DocumentNode.RemoveNodes(predicate); return doc; } - - public static HtmlNode RemoveNodes(this HtmlNode self, Func predicate) { + + public static HtmlNode RemoveNodes(this HtmlNode self, Func predicate) + { var toRemove = self.ChildNodes.Where(predicate).ToList(); - foreach (var node in toRemove) { - node.Remove(); - } + foreach (var node in toRemove) node.Remove(); return self; } - - public static HtmlNode RemoveNodes(this HtmlNode self, string selector) { + + public static HtmlNode RemoveNodes(this HtmlNode self, string selector) + { var toRemove = self.QuerySelectorAll(selector); - foreach (var node in toRemove) { - node.Remove(); - } + foreach (var node in toRemove) node.Remove(); return self; } - - public static HtmlDocument RemoveNodes(this HtmlDocument self, string selector) { + + public static HtmlDocument RemoveNodes(this HtmlDocument self, string selector) + { self.DocumentNode.RemoveNodes(selector); return self; } -} \ No newline at end of file +} diff --git a/Core/Extensions/HttpClientExtensions.cs b/Core/Extensions/HttpClientExtensions.cs index d4daf214..360c129a 100644 --- a/Core/Extensions/HttpClientExtensions.cs +++ b/Core/Extensions/HttpClientExtensions.cs @@ -5,29 +5,34 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; +using Core.Exceptions; using HtmlAgilityPack; -namespace Core.Extensions; +namespace Core.Extensions; -public static class HttpClientExtensions { +public static class HttpClientExtensions +{ private const int MAX_TRY_COUNT = 5; private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(1); - private static TimeSpan GetTimeout(TimeSpan errorTimeout) { + private static TimeSpan GetTimeout(TimeSpan errorTimeout) + { return errorTimeout == default ? DefaultTimeout : errorTimeout; } - public static async Task GetWithTriesAsync(this HttpClient client, Uri url, TimeSpan errorTimeout = default) { + public static async Task GetWithTriesAsync(this HttpClient client, Uri url, + TimeSpan errorTimeout = default) + { Exception lastEx = null; - - for (var i = 0; i < MAX_TRY_COUNT; i++) { - try { + + for (var i = 0; i < MAX_TRY_COUNT; i++) + try + { var response = await client.GetAsync(url); - if (response.StatusCode != HttpStatusCode.OK) { - if (i == MAX_TRY_COUNT - 1) { - return response; - } + if (response.StatusCode != HttpStatusCode.OK) + { + if (i == MAX_TRY_COUNT - 1) return response; response.Dispose(); await Task.Delay(GetTimeout(errorTimeout)); @@ -35,30 +40,31 @@ public static async Task GetWithTriesAsync(this HttpClient } return response; - } catch (Exception ex) { + } + catch (Exception ex) + { lastEx = ex; await Task.Delay(GetTimeout(errorTimeout)); } - } - if (lastEx != null) { - throw lastEx; - } + if (lastEx != null) throw lastEx; return default; } - public static async Task SendWithTriesAsync(this HttpClient client, Func message, TimeSpan errorTimeout = default) { + public static async Task SendWithTriesAsync(this HttpClient client, + Func message, TimeSpan errorTimeout = default) + { Exception lastEx = null; - - for (var i = 0; i < MAX_TRY_COUNT; i++) { - try { + + for (var i = 0; i < MAX_TRY_COUNT; i++) + try + { var response = await client.SendAsync(message()); - if (response.StatusCode != HttpStatusCode.OK) { - if (i == MAX_TRY_COUNT - 1) { - return response; - } + if (response.StatusCode != HttpStatusCode.OK) + { + if (i == MAX_TRY_COUNT - 1) return response; response.Dispose(); await Task.Delay(GetTimeout(errorTimeout)); @@ -66,63 +72,67 @@ public static async Task SendWithTriesAsync(this HttpClient } return response; - } catch (Exception ex) { + } + catch (Exception ex) + { lastEx = ex; await Task.Delay(GetTimeout(errorTimeout)); } - } - if (lastEx != null) { - throw lastEx; - } + if (lastEx != null) throw lastEx; return default; } - - public static async Task PostWithTriesAsync(this HttpClient client, Uri url, HttpContent content, TimeSpan errorTimeout = default) { + + public static async Task PostWithTriesAsync(this HttpClient client, Uri url, + HttpContent content, TimeSpan errorTimeout = default) + { Exception lastEx = null; - - for (var i = 0; i < MAX_TRY_COUNT; i++) { - try { + + for (var i = 0; i < MAX_TRY_COUNT; i++) + try + { var response = await client.PostAsync(url, content); - if (response.StatusCode != HttpStatusCode.OK) { + if (response.StatusCode != HttpStatusCode.OK) + { await Task.Delay(GetTimeout(errorTimeout)); - if (i == MAX_TRY_COUNT - 1) { - return response; - } - + if (i == MAX_TRY_COUNT - 1) return response; + continue; } - + return response; - } catch (Exception ex) { + } + catch (Exception ex) + { lastEx = ex; await Task.Delay(GetTimeout(errorTimeout)); } - } - if (lastEx != null) { - throw lastEx; - } + if (lastEx != null) throw lastEx; return default; } - - public static async Task GetHtmlDocWithTriesAsync(this HttpClient client, Uri url, Encoding encoding = null) { - using var response = await client.GetWithTriesAsync(url); + + public static async Task GetHtmlDocWithTriesAsync(this HttpClient client, Uri url, + Encoding encoding = null) + { + using var response = await client.GetWithTriesAsync(url); return await response.Content.ReadAsStreamAsync().ContinueWith(t => t.Result.AsHtmlDoc(encoding)); } - - public static async Task GetFromJsonWithTriesAsync(this HttpClient client, Uri url, TimeSpan errorTimeout = default) { + + public static async Task GetFromJsonWithTriesAsync(this HttpClient client, Uri url, + TimeSpan errorTimeout = default) + { using var response = await client.GetWithTriesAsync(url, errorTimeout); - if (response == default) { - throw new Exception($"Не удалось получить ответ от {url}"); - } + if (response is null) throw new Elib2EbookParseException($"Не удалось получить ответ от {url}"); - if (response.StatusCode != HttpStatusCode.OK) { + if (response.StatusCode != HttpStatusCode.OK) + { var errorBody = await SafeReadAsStringAsync(response.Content); - throw new Exception($"Ошибка запроса {url}. Код {(int)response.StatusCode} ({response.StatusCode}).{FormatSnippet(errorBody)}"); + throw new Elib2EbookFormatException( + $"Ошибка запроса {url}. Код {(int)response.StatusCode} ({response.StatusCode}).{FormatSnippet(errorBody)}"); } await using var stream = await response.Content.ReadAsStreamAsync(); @@ -130,50 +140,56 @@ public static async Task GetFromJsonWithTriesAsync(this HttpClient client, await stream.CopyToAsync(memoryStream); var payload = Encoding.UTF8.GetString(memoryStream.ToArray()); - if (string.IsNullOrWhiteSpace(payload)) { - throw new Exception($"Ответ {url} пустой"); - } + if (string.IsNullOrWhiteSpace(payload)) throw new Elib2EbookParseException($"Ответ {url} пустой"); - if (LooksLikeHtml(payload)) { - throw new Exception($"Сайт вернул HTML вместо JSON по адресу {url}. Возможно сработала защита (DDoS-Guard). Попробуйте увеличить параметры --delay/--timeout или настроить --flare."); - } + if (LooksLikeHtml(payload)) + throw new Elib2EbookParseException( + $"Сайт вернул HTML вместо JSON по адресу {url}. Возможно сработала защита (DDoS-Guard). Попробуйте увеличить параметры --delay/--timeout или настроить --flare."); - try { - return JsonSerializer.Deserialize(payload, new JsonSerializerOptions(JsonSerializerDefaults.Web)) ?? throw new Exception($"Сайт вернул пустой JSON по адресу {url}"); - } catch (JsonException ex) { - throw new Exception($"Не удалось разобрать JSON ответа {url}. {ex.Message}. {FormatSnippet(payload)}", ex); + try + { + return JsonSerializer.Deserialize(payload, new JsonSerializerOptions(JsonSerializerDefaults.Web)) ?? + throw new Elib2EbookParseException($"Сайт вернул пустой JSON по адресу {url}"); + } + catch (JsonException ex) + { + throw new Elib2EbookFormatException( + $"Не удалось разобрать JSON ответа {url}. {ex.Message}. {FormatSnippet(payload)}", ex); } } - public static async Task PostHtmlDocWithTriesAsync(this HttpClient client, Uri url, HttpContent content, Encoding encoding = null) { + public static async Task PostHtmlDocWithTriesAsync(this HttpClient client, Uri url, + HttpContent content, Encoding encoding = null) + { using var response = await client.PostWithTriesAsync(url, content); return await response.Content.ReadAsStreamAsync().ContinueWith(t => t.Result.AsHtmlDoc(encoding)); } - private static async Task SafeReadAsStringAsync(HttpContent content) { - try { + private static async Task SafeReadAsStringAsync(HttpContent content) + { + try + { return await content.ReadAsStringAsync(); - } catch { + } + catch + { return string.Empty; } } - private static bool LooksLikeHtml(string value) { - return value.TrimStart().StartsWith("<", StringComparison.Ordinal); + private static bool LooksLikeHtml(string value) + { + return value.TrimStart().StartsWith('<'); } - private static string FormatSnippet(string value, int maxLength = 300) { - if (string.IsNullOrWhiteSpace(value)) { - return string.Empty; - } + private static string FormatSnippet(string value, int maxLength = 300) + { + if (string.IsNullOrWhiteSpace(value)) return string.Empty; var trimmed = value.Trim(); - if (LooksLikeHtml(trimmed)) { - return string.Empty; - } - if (trimmed.Length > maxLength) { - trimmed = trimmed[..maxLength] + "…"; - } + if (LooksLikeHtml(trimmed)) return string.Empty; + + if (trimmed.Length > maxLength) trimmed = trimmed[..maxLength] + "…"; return $" Ответ сервера: {trimmed}"; } diff --git a/Core/Extensions/ImageFormatExtensions.cs b/Core/Extensions/ImageFormatExtensions.cs index 75ff446f..eccea29f 100644 --- a/Core/Extensions/ImageFormatExtensions.cs +++ b/Core/Extensions/ImageFormatExtensions.cs @@ -2,11 +2,14 @@ using EpubSharp; using EpubSharp.Format; -namespace Core.Extensions; +namespace Core.Extensions; -public static class ImageFormatExtensions { - public static EpubContentType ToEpubContentType(this ImageFormat self) { - return self switch { +public static class ImageFormatExtensions +{ + public static EpubContentType ToEpubContentType(this ImageFormat self) + { + return self switch + { ImageFormat.Gif => EpubContentType.ImageGif, ImageFormat.Png => EpubContentType.ImagePng, ImageFormat.Jpeg => EpubContentType.ImageJpeg, @@ -14,4 +17,4 @@ public static EpubContentType ToEpubContentType(this ImageFormat self) { _ => throw new ArgumentOutOfRangeException(nameof(self), self, null) }; } -} \ No newline at end of file +} diff --git a/Core/Extensions/StreamExtension.cs b/Core/Extensions/StreamExtension.cs index 6e7f4ada..ff895544 100644 --- a/Core/Extensions/StreamExtension.cs +++ b/Core/Extensions/StreamExtension.cs @@ -2,12 +2,14 @@ using System.Text; using HtmlAgilityPack; -namespace Core.Extensions; +namespace Core.Extensions; -public static class StreamExtension { - public static HtmlDocument AsHtmlDoc(this Stream self, Encoding encoding = null) { +public static class StreamExtension +{ + public static HtmlDocument AsHtmlDoc(this Stream self, Encoding encoding = null) + { var doc = new HtmlDocument(); doc.Load(self, encoding ?? Encoding.UTF8); return doc; } -} \ No newline at end of file +} diff --git a/Core/Extensions/StringBuilderExtensions.cs b/Core/Extensions/StringBuilderExtensions.cs index fd6bee8d..46da9ec7 100644 --- a/Core/Extensions/StringBuilderExtensions.cs +++ b/Core/Extensions/StringBuilderExtensions.cs @@ -1,15 +1,17 @@ using System.Text; using HtmlAgilityPack; -namespace Core.Extensions; +namespace Core.Extensions; -public static class StringBuilderExtensions { +public static class StringBuilderExtensions +{ /// - /// Конвертация строки в Html документ + /// Конвертация строки в Html документ /// /// /// - public static HtmlDocument AsHtmlDoc(this StringBuilder self) { + public static HtmlDocument AsHtmlDoc(this StringBuilder self) + { return self.ToString().AsHtmlDoc(); } -} \ No newline at end of file +} diff --git a/Core/Extensions/StringExtensions.cs b/Core/Extensions/StringExtensions.cs index 5c74f8ab..e3d0bd62 100644 --- a/Core/Extensions/StringExtensions.cs +++ b/Core/Extensions/StringExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Linq; using System.Text; @@ -9,33 +9,37 @@ using AngleSharp.Html.Parser; using HtmlAgilityPack; -namespace Core.Extensions; +namespace Core.Extensions; -public static class StringExtensions { +public static class StringExtensions +{ /// - /// Конвертация строки в Html документ + /// Конвертация строки в Html документ /// /// /// - public static HtmlDocument AsHtmlDoc(this string self) { + public static HtmlDocument AsHtmlDoc(this string self) + { var doc = new HtmlDocument(); doc.LoadHtml(self.HtmlDecode()); return doc; } /// - /// Конвертация строки в Xhtml документ + /// Конвертация строки в Xhtml документ /// /// /// - public static HtmlDocument AsXHtmlDoc(this string self) { + public static HtmlDocument AsXHtmlDoc(this string self) + { HtmlNode.ElementsFlags.Remove("style"); HtmlNode.ElementsFlags.Remove("title"); - var doc = new HtmlDocument { + var doc = new HtmlDocument + { OptionFixNestedTags = true, OptionAutoCloseOnEnd = true, - OptionOutputAsXml = true, + OptionOutputAsXml = true }; doc.LoadHtml(self); @@ -43,91 +47,103 @@ public static HtmlDocument AsXHtmlDoc(this string self) { } /// - /// Удаление из строки запрещенных символов для пути + /// Удаление из строки запрещенных символов для пути /// /// /// - public static string RemoveInvalidChars(this string self) { + public static string RemoveInvalidChars(this string self) + { var sb = new StringBuilder(self); - foreach (var invalidFileNameChar in Path.GetInvalidFileNameChars().Union(['"'])) { + foreach (var invalidFileNameChar in Path.GetInvalidFileNameChars().Union(['"'])) sb.Replace(invalidFileNameChar, ' '); - } return sb.ToString().Trim().Trim('.').Trim(); } /// - /// Обертывание строки кавычками + /// Обертывание строки кавычками /// /// /// - public static string CoverQuotes(this string self) { + public static string CoverQuotes(this string self) + { return "\"" + self + "\""; } - + /// - /// Обрезка строки + /// Обрезка строки /// /// /// /// - public static string Crop(this string self, int lenght) { + public static string Crop(this string self, int lenght) + { return self.Length > lenght ? self[..lenght] : self; } /// - /// HtmlDecode + /// HtmlDecode /// /// /// - public static string HtmlDecode(this string self) { - if (string.IsNullOrWhiteSpace(self)) { - return self; - } - + public static string HtmlDecode(this string self) + { + if (string.IsNullOrWhiteSpace(self)) return self; + var temp = HttpUtility.HtmlDecode(self.Replace(">", "").Replace("<", "")); - while (temp != self) { + while (temp != self) + { self = temp; temp = HttpUtility.HtmlDecode(self.Replace(">", "").Replace("<", "")); } - + return self.Trim(); } - public static string CoverTag(this string self, string tag) { + public static string CoverTag(this string self, string tag) + { return string.IsNullOrEmpty(tag) ? self : $"<{tag}>{self}"; } - public static string CleanInvalidXmlChars(this string self) { - return string.IsNullOrWhiteSpace(self) ? self : Regex.Replace(self, "[\x00-\x08\x0B\x0C\x0E-\x1F\x26]", string.Empty, RegexOptions.Compiled); + public static string CleanInvalidXmlChars(this string self) + { + return string.IsNullOrWhiteSpace(self) + ? self + : Regex.Replace(self, "[\x00-\x08\x0B\x0C\x0E-\x1F\x26]", string.Empty, RegexOptions.Compiled); } - - public static string ReplaceNewLine(this string self) { + + public static string ReplaceNewLine(this string self) + { return string.IsNullOrWhiteSpace(self) ? self : Regex.Replace(self, "\t|\n", " ").CollapseWhitespace().Trim(); } - + /// - /// HtmlEncode + /// HtmlEncode /// /// /// - public static string HtmlEncode(this string self) { + public static string HtmlEncode(this string self) + { return HttpUtility.HtmlEncode(self.Trim()); } - public static string CollapseWhitespace(this string self) { + public static string CollapseWhitespace(this string self) + { return !string.IsNullOrEmpty(self) ? Regex.Replace(self, @"\s+", " ") : self; } - public static T Deserialize(this string self) { + public static T Deserialize(this string self) + { return JsonSerializer.Deserialize(self); } - public static Uri AsUri(this string self) { - return new(self); + public static Uri AsUri(this string self) + { + return new Uri(self); } - public static string PrettyHtml(this string self) { + public static string PrettyHtml(this string self) + { var document = new HtmlParser().ParseDocument(self); using var sw = new StringWriter(); @@ -135,4 +151,4 @@ public static string PrettyHtml(this string self) { return sw.ToString(); } -} \ No newline at end of file +} diff --git a/Core/Extensions/UriExtension.cs b/Core/Extensions/UriExtension.cs index 04b3a184..15c8101f 100644 --- a/Core/Extensions/UriExtension.cs +++ b/Core/Extensions/UriExtension.cs @@ -4,59 +4,75 @@ using System.Text.RegularExpressions; using System.Web; -namespace Core.Extensions; +namespace Core.Extensions; -public static class UriExtension { +public static class UriExtension +{ private static readonly IdnMapping Idn = new(); - - public static string GetFileName(this Uri self) { + + public static string GetFileName(this Uri self) + { return self.Segments.Last().Split(":")[0].TrimEnd('/'); } - public static Uri MakeRelativeUri(this Uri self, string relative) { - return new(self, relative); + public static Uri MakeRelativeUri(this Uri self, string relative) + { + return new Uri(self, relative); } - public static Uri AppendSegment(this Uri self, string segment) { - return new(self.ToString().TrimEnd('/') + "/" + segment.TrimStart('/')); + public static Uri AppendSegment(this Uri self, string segment) + { + return new Uri(self.ToString().TrimEnd('/') + "/" + segment.TrimStart('/')); } - public static string GetSegment(this Uri self, int index) { - var builder = new UriBuilder { + public static string GetSegment(this Uri self, int index) + { + var builder = new UriBuilder + { Scheme = self.Scheme, Host = self.Host, Path = Regex.Replace(self.AbsolutePath, "/+", "/") }; - + return builder.Uri.Segments[index].Trim('/'); } - public static Uri AppendQueryParameter(this Uri self, string name, object value) { + public static Uri AppendQueryParameter(this Uri self, string name, object value) + { var query = HttpUtility.ParseQueryString(self.Query); query.Add(name, value.ToString()); - return new UriBuilder(self) { + return new UriBuilder(self) + { Query = query.ToString()! }.Uri; } - - public static string GetQueryParameter(this Uri self, string name) { + + public static string GetQueryParameter(this Uri self, string name) + { return HttpUtility.ParseQueryString(self.Query)[name]; } - - public static Uri ReplaceHost(this Uri self, string newHost) { - return new UriBuilder(self) { + + public static Uri ReplaceHost(this Uri self, string newHost) + { + return new UriBuilder(self) + { Host = newHost }.Uri; } - public static bool IsSameHost(this Uri self, Uri url) { - return string.Equals(Idn.GetAscii(self.Host).Replace("www.", ""), Idn.GetAscii(url.Host).Replace("www.", ""), StringComparison.InvariantCultureIgnoreCase); + public static bool IsSameHost(this Uri self, Uri url) + { + return string.Equals(Idn.GetAscii(self.Host).Replace("www.", ""), Idn.GetAscii(url.Host).Replace("www.", ""), + StringComparison.InvariantCultureIgnoreCase); } - - public static bool IsSameSubDomain(this Uri self, Uri url) { + + public static bool IsSameSubDomain(this Uri self, Uri url) + { var urlParts = url.Host.Replace("www.", string.Empty).Split("."); var selfParts = self.Host.Replace("www.", string.Empty).Split("."); - return selfParts.Length >= urlParts.Length && string.Equals(Idn.GetAscii(string.Join(".", selfParts[^urlParts.Length..])), Idn.GetAscii(string.Join(".", urlParts)), StringComparison.InvariantCultureIgnoreCase); + return selfParts.Length >= urlParts.Length && string.Equals( + Idn.GetAscii(string.Join(".", selfParts[^urlParts.Length..])), Idn.GetAscii(string.Join(".", urlParts)), + StringComparison.InvariantCultureIgnoreCase); } -} \ No newline at end of file +} diff --git a/Core/External/EpubSharp.deps.json b/Core/External/EpubSharp.deps.json index 71885370..7198bc52 100644 --- a/Core/External/EpubSharp.deps.json +++ b/Core/External/EpubSharp.deps.json @@ -1,46 +1,23 @@ { "runtimeTarget": { - "name": ".NETCoreApp,Version=v6.0", + "name": ".NETCoreApp,Version=v10.0", "signature": "" }, "compilationOptions": {}, "targets": { - ".NETCoreApp,Version=v6.0": { - "EpubSharp/1.1.5.13": { - "dependencies": { - "Microsoft.NETFramework.ReferenceAssemblies": "1.0.2" - }, + ".NETCoreApp,Version=v10.0": { + "EpubSharp/1.1.6-alpha.9": { "runtime": { "EpubSharp.dll": {} } - }, - "Microsoft.NETFramework.ReferenceAssemblies/1.0.2": { - "dependencies": { - "Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.2" - } - }, - "Microsoft.NETFramework.ReferenceAssemblies.net461/1.0.2": {} + } } }, "libraries": { - "EpubSharp/1.1.5.13": { + "EpubSharp/1.1.6-alpha.9": { "type": "project", "serviceable": false, "sha512": "" - }, - "Microsoft.NETFramework.ReferenceAssemblies/1.0.2": { - "type": "package", - "serviceable": true, - "sha512": "sha512-5/cSEVld+px/CuRrbohO/djfg6++eR6zGpy88MgqloXvkj//WXWpFZyu/OpkXPN0u5m+dN/EVwLNYFUxD4h2+A==", - "path": "microsoft.netframework.referenceassemblies/1.0.2", - "hashPath": "microsoft.netframework.referenceassemblies.1.0.2.nupkg.sha512" - }, - "Microsoft.NETFramework.ReferenceAssemblies.net461/1.0.2": { - "type": "package", - "serviceable": true, - "sha512": "sha512-8shzGaD5pZi4npuJYCM3de6pl0zlefcbyTIvXIiCdsTqauZ7lAhdaJVtJSqlhYid9VosFAOygBykoJ1SEOlGWA==", - "path": "microsoft.netframework.referenceassemblies.net461/1.0.2", - "hashPath": "microsoft.netframework.referenceassemblies.net461.1.0.2.nupkg.sha512" } } } \ No newline at end of file diff --git a/Core/External/EpubSharp.dll b/Core/External/EpubSharp.dll index deb7dc1a364002f19b127e4458a0c6d34cff2935..60ec8cba05bfbfeb62ad80ffb4d44f8e36c0e276 100644 GIT binary patch literal 100864 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%0A0v71M$uwNEoCa%mIG-qy8|+>i{%C~+xIFuO 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%0DDZG#D)4yV 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 diff --git a/Core/External/EpubSharp.pdb b/Core/External/EpubSharp.pdb index 1690333282564d6016472e46bf37cae129594eaf..86a5171bd2bbe1114005ec435747ce5ba3dcdf62 100644 GIT binary patch literal 44588 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 literal 34756 zcmaid2V4}#`~KW<97nmMD%ej1Q3M2}*rhjVf{I-!r=q|iMa6p9qp`%^d+)Ku8a37! z6B82?jZqW3CMN1HF`8)df8N<$9MOFLe0Xl=*>~QVcV=g1-FiwPJNMTLa@( z^#`v2w!*|_j*m;d+F75k5xvFG7DU>Y-J$K?o}lZa}yX;cbLZ z5Z1O)F&`T>n~HD=!fz4YLn!{K*e*X63-MR6SfDG=+h5I!fO23Ia0&PUxCJ}}o&e8* ze*tZPn&|;IIwfeiD8v(ibRY}p1DJpUpcEJZR030gnZSHt8L$@E0_*|~0LOqcz!$(J z;0NFq@DO+cJO};-w9QdI;0DwIJb@@65l9EJfIff;C;&=<5kMs{1(*rU2bKYAfi1wU z=2Be_$l);@JHz8v?Ca)M>=y7_a}E0&7#yf!qX3&A4QmDT0i1$0tN`fV!kR4s!b7at zWMB@k2{;OT3s{F*vt~d%FdsM?s+<>V)-tRf>l0QVqp&`^4QRsaGkd@l2mwC92&8d- zg@<}ptEGYY10g^gAPPtX(t#|X4-G43U?#*1fKp%tPzg){W&-npWx!fsOG`(#3pfBA z1I_?nv~*&(fd;LdSYyB+2m#sv@jxoj1?UCj0)v1dz;IwJFb$XuECN;n8-VS=Uf?ir z0yqbJ1zc`L_Byc}h~EVs0lx!(1M1e2%(k@?a{}Cf`hYj!2ebfM1Cc-ikOp)GdINdD zV6N}XN?JR!qrhq4bKo1`DsU5c06Yf%1pWc6+c+}^z!hi!GzR>E5TFeZ)rM?xW{HTW z16e>HzyuTkrN9WF5|{$a1m*+FfVIFDU>9%zI0l>nz5p%(KLEFYhrkoyxm?!2h-=$A zGd@sizcntgr`~z6Gb72mAdUj>5?c7)sU4DG*q7tR@RjtI67PUX#UE9d~ERRdElNndZTI0NKDmzzhuK zat}7TDxDphAjGHfxILSlX3rMM@w0O12k(>oQ!99Ekzj`2@0-NdkdW8w2lz(*;4yk1hrZCpoP}6;ArG0w+1G`9yiVz)7AUaMDi_?4`1k1x{ljMc`DQGi=qIL(EnxZ)}KQ~xMO?^F0I@S%XhY5WcoIE}yI0w;Nez)3zr;3OX@aFUM_ zILSu~oaAE#PI8ODNnR;%l8+NO$;S(vF*FY$#)8zaQOOa!S8n;FSJ=z)5~k;3PjJaFQPu zILVI)oaA&J65Hdbz)5~g;H3YFU@taG*$F{T=}!ur(w`DI$xjQM1y1?VHB3oQ@=F3I z`F8>*`DKBV{EEOy{=L9SepRrS`pXZ3oYG$tIHkWXaFX8;ILUt$ILUt!ILU7coaDCz zPV(CVC;1(Lll-p0Nq$e@B)>1zr!M#dfm8h+3h7DzXMvOcF9Ij|BY~6r*J}CwCdeuM zV}VoprvfMW{|TJrzYCn?e+Zo9&je2LKLt+m=K?4B3xSjTFM*T%Z-JBirNBx4O5h~_ zSKuTkS4HWMB**QWDo%3TuBqZA$8DJ^PIBCasp2HZZI>!ea@=OA;v~0Gy}mxu`O^t< zN^dK0N^d7{lG_WMaFY87oa9XePV%M#C%LaspWZTWgHG81aUY-=s5 zd6>XS9xiZ_w-h+ZTM3-xbPrgxkM8#>oaAi z%l(Ds{}IqvIOR7|;FRAefs=f+z)3zv;3OX_aFSaDPV!2DlYE@ONj_fSB%dH~l1~&k z$tMY%6*$S~37q8f1y1q>0w?)Gfs=fZV2?BSVu4eAmk8-ef2qJpe_6Hk%LO^5 zUm%!vkoBrMDTAACtrYg68JU58vvWYJA%_6`K}6gK%CY-#FHU+MV!`JTU5T- zW0um>e7sE+09zzZ*UIhSon(Fq6(F7gPIa3C5YGf}gm|UQJA>1@k>-1ncL66qQ{i2~ z={lqEEO6?F^W^m1z^Px*-Vo_`2TxMJPTvE3n!tO4UjV1Mn$q_IzYI>-4&uGRyJ}wR z_W@r3o+Rhr7knjnq|Ez)+gQI&pADV>UM$OVz&n8tka;fnbs>EocmuSRQa=-TKY{lL zUnTP$ux|i3{n53S_&{)KU-E5<(|5+GeaV;J2|h?p{}jiF4+d|I_zIa9fJY*}5cm-C zLODI1U*bjJl>a!Ho53mnk-%#Vz0; zL4UX`e}w+MAAGdTb0I&VYFEXn{fUo-yawVPz(G|6v&iX#!4H9tles(iVO4Y$-!1d; zkW*Vvl=)=v5X4)`d{*Ay%|57#n zoxo|nzEX|%q^}_%B$mE9rYe{z%|- z@Ag-LQ-AwS;3R)6@OI!&1fC53JJ!oe{?vYd2%Oe{&(OaWIq??)r}TeS<1YnH{qbLc zQ+lR;oga;1mB7h9wZN%7D}ht}wF0O7bONXLv=un5PwfQm1a2>ID$haS)LxG2;Hvu4 z-msIvdxJX*yb|0^9afb-0lbF5uYtP@d>?pid^J#M&tUL+LVlgW>kFL5O9O$o2R90w z%4;a_3*eptr}@oG;7h=Lg!@fEloxSu+%%08M8`~|*M;FNY4&>Zr`a(~{BZmn=S zH?)=sf_#ZAUyL!Ja5`7`(klNuk92Cja+mgJRcsg}YQFbl#p*z+;rj&WEs}y21%r_W z*JYlv2%lE+_qEB((K3AX81qvz#C0qcscren+>RZ?aeKZi3ttj(6_a$YAg^t9T{<802Hc{df(x-&(He16~)J zBcys+u|!@kI-=t1CN*DON>UZB>h!maOONAIjeEnC^8l1CwFY*-cn-W6{-NUKN>?&z zZ%M^hG-~eaD90%SCFN7zCW$zLZ#hGtVRV(VW?$jBmhHr+4&NoG`Yz`wAK+8XrPvEa zdKK%2BiN_q(x(*VaW((;ycJJr%{@~s-wU?E)rs<~hxE2wYRB#(AA2?iNA$dO13!*$ z=^~{IzeC~5kGS!EQj^cc?)-=c);IJWp%0*2o8QB(!;X^)vJJGFl59|p`~=yXM>+{d zq&_CKMho7y$N+VumQk}mQHm8jBphO?h%0Bx8F73=AKZ$S;8Vlzl1cuqihqMj&G+HZ zLy@P7>A5bggd{&*Er08#1J}KQiWc4l4j~nR`lJq4Uen%cci5JrBBlH!Z+!;zCSL4;5VAXAw`%N zNLMZRV<9vn(2SyD--pr6Vc>BWd>dQ&)>z|~w2E-!$I(|2(iK9*JX%p()#mZLZOIF& z$Kzh@=n7!u@zw1-5jFzI>+|BrtYdr-(k$7O^Zqf^SN#Bg&IcznM;HhMalSi&b|A^q z3+0?8h9e}euNCL36Wbtc%i|{#X||;^pkiH;A`wRMcx6%y!dT#zY5=bO12ID$Q#Dr~ zR|Tm*QTd=%ope4pU*2|BYAa^Yt-+^*?u5pQ+3BpbR!pbs*H+7VCiiP=!^)CZBHkut zCE}w}R>o@CfwUj-sZRef)`rzdH^yn%czoKh_30=O8yT5iZQHl(+qb}+n_W;E$cjwE zVzSG!S)r*cJC7p$^9xKVAp8g`GnZu-L{*fTN;6E^c`Tpy%yP`;!E9hzVF5E2 zu=0`uv`V45qyo928mv?b%FMZmC{{9?$9P-tSYdWiet%PG87D=#!+DrbsY^@+*=4Yd znM?8qLh=F^|K##G?LYvN9#Rm_e}^n%Q}#5|&e*UqB(zsPg_e z4>-hw;_{q={L+CqfVM9x%kQ6$D79iX+7+>Z*~MiD^YSI-Xyj04#&IP?%98w?ayq7* z$5NJEP!NZ{QOcV`ij|nnWv^q@b4ppMsjQ4YT~$;{CwpkVxxAG1Hapsi+COIM6Cw~BM&vbJ> z6zQz;2-#Sb4f=C#YW7g{jMB19d<;x1$}1I;DT1G7scVU9Vk0@)Rs zN;{i7msBvAC4J_Xk#`7x@5`oB_B*>qO`bN)Fmb`wn$L4ObvSMFweze@b}Vg;dez~w zfH|A44v(20=NP=<;O?)jfB9$cm+y~Hn*V)V&V)l9FYDq{#uVB1O5XncXHEwPmFZ9U zzUu7#pmxMk-xq1_P9@LluisR7=6PAd$2kKJcMZHTgjshfMW?nk4>lK=O9z{WYO>5F zgVk{bwy`+znJ~QAK(R<^Tms|Jw8HHXis`WNr`Rm64LpwVJPwlzr+~aMplB~ABSJ7lev%Aet z7G0lw>RfLpQi~m)YvR2Yx^_yDmbu0g-D@15aP-qUPkJrx&-zTZ`nt9E-3~KT9omol z`RB0m>;LLWIfHlVH#EO8hFBK)d}P}pYl3&v=NlTda9?J#y6&k}b|Ll)Pt4ktbIqG; zW|d$(m57>^_Qv0?eG)qV+V8LYoO^9qv*)u(({hHMy`|cI;AGEXk6I3mZ($cZ9CHfI z45f}Vi$s=`WLMxAvT;yMuZ*osH|}rxZ-OSr;oKo#{g&Dj9%$Q4Nh^n#Hq zw%Hfnwo8j$yW`U1Nn7*g{gAxHVVC{T%hTGfv+Y(`@U~t%W74iJt!j5TqRv>~WpADP zV@LH1ba#v17e07K!>%p6b)NT)Q;+;&xkJ6JUT%Ck^U|kRQj9+b?H=*()d>qqKbSI| z?VtUrSFh-4$H#sY(oP%4X8?z2w0Bu{QCX?j&g1rsJ7oN_?L*%m?-`wXp4&5E*5R|$ zTCHA{@XHNz%^3;)*S5WQu*d*anzuPx}d`PUoKudX~Wbo|QJ z+}>|=?ZvZLIPWKt32*9-Z(+RO;?j!W7wza)xxVP$#io5#)Bo|b`SQe(Ur%iDKNC^n zu-%?!IJzQM(=;2`Y;hd$=%aA`^5U?O&A$5M(X%UYz3+{0dHH6SF?Bv&_UNGjQ%#IH zx146OYRa38e?&iA`_+YzjD-znFCNsr)5`b9P0Bs~Fv;%X^rOeSZTiF0keWOEKa}qp z+pWFx-9i7mGjDw#zw(#kbK6Hwi`@9}PhIcaSlIcqC#ef{hP2%N?^d4B$k?FPxzp#J z8uqz;V)l}gHKzS=W5$pxZ5@3BUVYPj-J}Z(Q%PB#UaOs^3H^*CGXE|;5;DK&(Y9w_ z7PMU3$;$uX{l)R!whZ-NZad?EgP$R-_`gbCW}Lh0!Nn(+H4WG7usY}a&Gn=6bhB-* zF0B7x@Z^o-SC;hJX(LSnr4DGgs`JFjCyb9xz-aWcYS*Bu47B99iREn zbU@wzLwS+$=e7=e0()mzk0?#-_{*jl<96kwUg(&-uc05}O#F7@bz_(M&aI|<|8}W|-@K_;YHb+1@$;jpwk4|XdRgw=3jY1TU(^8aR-R-u zuJ7?U@8XQB?z5hqz3U!ZnEg|Z`|}&=%S@xks`L7+?LD3v;6Idm8;y}S%fG9C+M({C z55H?L<$&F`Zj08;`Qn!*I}U8r#jbzWk{aMYlouPDA0OT6WQ_Ov+bN&!`#){Xj-L+n zTo~+gu&!@q!&&!hFMFv;$uG^dNr3Yvj+IH@P5QyO>C8&|W)~c-JbHcfe9W_&!$Kd; zwkq>GIivAk%jeuY6n@<%0Up|$T9aLjtK-*|4xV~$)%Nfa{l?u;Wj3wcq#OLz#m&z~ z>Di4PU;9{@a|WTu6_~P1P1+(;Sy(_|KtO72=V)_@DYCfOudC!awi`-<$kI|1{ML%d zGRZYE(a>;Gy5{7B$;p#DPwr`~lX3Un&ak3uA?;`WG0)W)@!{&3`=|9%yM8gj?n0wF^!S90PgEa1 zQMsv{ReF{6I9J)n#gi1d=E7o3S0(<+p^$(U0fGK40)m5EgoOqK1vbme3k+|W7h(!& z(LXmkFeEsrWy_rG{<$HcAuUZ|{Y}ByIXPj$!J&cNdoCxJJ-QK-oO*xq@ceVlqHbYKR?TDN_=&htxf}nS61pk*taNqyb2!3}~E&eO37H?$5R!>XG z&n+>Rn){apL`xkmub==+(Omtg^~}OpKX1*HE>p{6r)C}fdYN5b``5|4=9iXZ(U)DC zpBq49AwW_b)2Gfu`YuvmpCliT0}*jEPOn*Taz?|k?^cv_o`rY)g$j$gdAZ^AfULBg zSJto(>|go4*2&5*Y7rceUV=_ihDBVdT-c%I*`JW2X-Ia;g`vMh?5(I{zMb!LzPe(D zX#iFSB^9#Tl#yNTvz7P?=FFuI$ItCfTzT}D$XOq3y-`@2Yc46s&ykZoTX=ICRb|w- zc0=qok4js5>*<3FaeujD5+Ls+FS{g9PWjvV%U{x#iTz#M9}n&xn7TM<@_l{i;~z6C zO0g6Rz%;?#68Mc$h3mY(_bAz|wvYetRrJBgS*vHHbnai&R!%Q1)09fCv2L@55_jId zq}PSovx)yu5cXykPThqRJheBduVR>0L7U^=OCU*V( z41LR}w9byH_fI;+FZ#&vTr=ABz&lh^%|-DhtTEAbq+)vo_1Q>v-tYgY>wAmmCoOu< zX`ae+u*W-eGmA~R`Pl{eBTP!``yB514=JyG|JC=Gi|)m&(IkKJY@(~FO1ZGuT!c1B zG3S{I0wRlwuv;OumsI#4m&Z+H$M96p=PnB#oL-fthag9 zLs9?y0p%sU!DSVd&8pwX=Esxw)%yJNEm51s{P40e&*sC|D%f{XdX=MQ&`YwgMW>9u zU!9thy5sop4PTD-@T^K)HS;X~>z50ZGVS`y0VOfJk|)&;KUEYoN9lib-Y`H43(ZBc z9UIj@k7E|R$)xAAzN#b5k{9;b`9(=!nXjBRSD=qfQyCUB@-k4?9WxJK*hd|{)Smm@ z=yxo6M(nMP{#V=d7Imct3f{BkYci`}ot*S7E1E?vo=}$O`B(X3C1rm0fFg5gS$-}b zzGWrZxmfl{s$*{q`kV?Ga=P{1LuZ%8uIgnmZTU6$n|G+jm~zSo$ZGcMb{!|R3u#}S zaM=k~5d;8r_c8 zcAV{I`LIb##|`gL%q+{!9o!l1rF4~^e}A-&TH1Qmw>v%#ITO2m=DClcXuLe|h0v?6aMz2luuZ^G*1k`|rxgY%Wl2zdFrkAys;*<;siG zx{QQ9OYIgO&fHt}E;TayjV8bCHR}=?Z#!`JkDuML>o}$Il+EtDpI`nDWvuIEV+&@O zH;`hzenl&@iszpE_?i}=j_%7}leC4Or%DOfOmKApSmAAIsOY2vBqkH6^H zeBzTyW<^hO{sJ;2H%>O+`;&XusnMo^;ycX6uz+mBXh`?kj62rqP^)9nezRWD1BXF_#tO%N1>V_w&!F;B0M=dhh3i zrX0NLedgu-lt16CDj7S`ZRs^?oZsHx={w;@m!viR?Q0+D===V=b!hO)5=s598@^kq z$fi3Fd)*(@BxYa0k$Ov?y3K#LIyQSK2ie@F6T-KVZu@2dHj~axjN9C(=H&NZoSytz zS1QB%=%cr(+<0DUB&-`{yr+2Evl)%LlCbJ#w zFMmhM6S`|n&K~EYX4@4O_3j#Mc&GAPqhjX1q;;e{WN_Bg_*D}+&bV;v!0tN#p`ArH z9;)V{1yehorHVe9kQIMwOU;;RE4S_W+wwrLIuTb&+~vs2#3nozVR8cDe|-J3tX zbn&%jdVVodzw5LFJLU6f#`yL5b?(!KUi3uSFA zE{Dh|&o(%6nkJ>K#V1^w&-L}**FJ=6g;6GjesM$f*kt#0dKV^5%ne0bA*S2)Nar<0D+?KB! zEz21={sTE}s!8$rr)|-nq;q)s;o`L|cGXBZ^yK3CKKWEvLdhB`hjt1v$g&qL86|iY#8o@&;X~~ zO|Nx3%!|BFW!CGM{^vgn)+Fu;D<11P{Y1E+oBsML8K3U;m`Yk3<<)v>(_=|HQ;(bv z|L|0tn7aCUb$u`ILsUXpXzwM*jy#Uue$%CMOwV?d)+`Ql?)d6Yy`Yd~%84$>f4vwzsD84NdccFj+g)m9xy9`KWnG_y&W$#|sgl6& z@Vs8t%H_G#Y-1qOQJFoZm_bwz%UwNW)-VA544N*BoRqA=kkF5DB zbo;?gKiBN^ENT4T8;81opS?>d4*UC9?<&oAJ9yb#Dxq2P^*PBq<|b|Z)cMA`Gqo$K zr|ycSy}8PJ39fhV6Y4*v4HtGajQlrx(S~(P7JRn&l&qD;@0&`?T*;`)QDGZM;nMJW zn|=u!pFI81&ZpV4+s%*_GEDvDy@P<5{8HS~&K)SW;KVU@56Q^Yk6+ZE?c*J}GG}K+ zf#1mqZ);1XZS;*hLg}q$xBI%F-qz?vzs;|=%qe^I+v#Jmk0YPhVM&@#sfXKM%5M1C ztfz@{!auIl;n*W(90?|JrNm^ub0Ch}2}8ZUqJGucOCg6z)pJK6Y)&1>VgAFb5bY<7J1jtr`=16zA<;p_fBEh-aZKB!3E zojhJ2t6y39wkq8l%A+-?)K+aC>Q>-8cI>=Ue_d&_kJGlf1xH4H)3f8-if?%oi$Bd@ zPxbwM&&sXg-Vf6@C$DStv0MC{w^eDu{+35NYjW#}RNLq6?XRakJRG_6&o#xr+!-*s zN?qEG!M`OM@>FGZ$<3{9B-yHaj9&9r#Er7qtXkP00&xczz38k@bpR&mm>)T4EmbgWhHr7{kcl>~Xv zaAHa_O?+M}4d_x-n%!UVhSIMkeoUE+I~?QEH}B}%S}9syj=YtLw80rI zpRbsU`zk4yMLim}IG^w=VeJ=zYxd}@ZdS`hvU}2%hmV-WhTXmN`I7$U2gGkYIL%&t zy7%|h62En7{?G4k?tbNFBwZ>GjOcizq)xe(Ev*`GvZ)E!e0=S*&3;~EC0VvD<9y3| zEAFH&IlbEZvibayYK0}@zG#WLxT=jLOI<%qUO|qI$vsE2c86#DPPZ+!#@eSk|9_O_ zmVf&iIwbAnuVjpU_sqHz?Ixxk_^8b8han>s*HbxTFb21#v30D>hqF3dIZI!eJaDmo zV9Cej$>Ur?J~FIxS2j5ny<{_er$s&`o815JDV22~Hp_e657{x(hjr@v*NYC-E1vr1 z%)9N8<_e9T@C9@8@7F&ZZSj5 zFEabr&-Jpx#oYXmny#YrO*C4PF;^VZm|8!v>|Haghgxr@%pj$|5T4H z$~EV`b~~?Do_LS)h+Y0_?DKtR6Du>KcZTh@e1Y&@apJKf z=ek7f`uF(U^nE9_|1*oXI#AuSS(RjnkL|5E$CbL2c^69__Ub!zVu(s7FlA?_1$IJU8vVdUw)CPW$5js^=xmS^sY8$~O?g@{fEmX7TXU z+0FVrUahh|Ts<{RF3Qa>&Q|KZU+2tM;^jY2Z`mm)K4$x{ynn6^{X6-;^>EuM&pb@A za%tS%WmIF!@=O0_tjmgDP?*;#qxrXQH9hbACh5yJ@^CrR)1A9}N5c+HRW<1wKdJYH zA-g-*npagczi>&O8$3%{>!*y*`G(@+9d+E6Udc#Z_$tNYmbz4N{^hCs%?;EMGv{2R zah*EtgvZ(?d8s?@O}Rca{By<0rj5^lsxB3cJ0j^=VkykKaZ+(x8$G0lZ-y7Pc0k2!8e)r$`R`1+L`;DDjFX*^)lE-_0XY5-p8Y)bg zKV=wxqD8LX$O6sIn;%4cwClT&jd@G!h{L+io?KO99`xk-N!xTm8>e(Bns!o8JhV zkUaTCP$MVLxcA=D?3`UXxHN!nY4k5J50kZbeP_itNPhG;E}zx-i-c(he#kb)%~CeI zUz;guOU4F{nR}Xw?0o6HRHY{T?wz zZ9DhtaiOBDm-Z#_e0y%v~`~^=rZd>#uP=VBpXhueBV}T)aCgrxTD&j@8*@4 zKl4l28u>{F+X+|eze^*TzL+hWcjw%xgH*9g+ke~lFw!n!{*BRDKl~e=BkOg|E+{v3 zE-$XysI|Hq&TqGGifVX2a@o@4RpEx@)g6~BdnB(%+2*9(dURIe!=Vghv;$Te4*fV^ZSWYAK6AO|Ere%n+5SMvNdl`smsT;ok5Lc z`=I3HR}F5)9r`G*+q`YxjFJ<-_BSkciLF1DrrxRT00Iso}`QjN-ZsJ-dJs1Z~5L&^vuHcy=CVc^hoHqqDH6j zNA6e;6O4J&sgh3H#JmN3J@w^jZF0uStcV4Tn~m62>+in=wQg14vXfMb{Myf@vHofN?j*=ciiK7E zu=j+JiRAJW#_w&jA}~I2=|5*LMov4NE}BPQPL(^;=U*>+Kxd>ZZd>$#56k1Xc6ssS zt38!X-%4J+PW3kb#oPRl=7wd=gAtMY`uw@uF!0!Q`GnJ*Yq^MBUpBlh4uKK$#)(zjeBuiR0!^*hzp>#D8yRa^g2ZF8k3NE*4r0u=5_rO|<) zIym5|)){#5IMCgwr{i8t^*RHSf|>yk1}rS!k>uIxcd^*yGyKYOU~-Hqd-%i zTcWSeEzuZ5xy;zwAJ61?HqvWZBegG{n^WU)sC*mNPCd*8ZH}j+)T8U-;Tl`&E)DH^ z^PB?eNaKKp+202`tcyFkrXuH zL3of9glX{@P(*9kiidZ?JMe=Yc-Hz3ft*E1^^cJ1AL&mA^^w6`7R9sHM@gAQ^Q`sJ zJc%ZO+o?}zM$i0sbcnKN9_q35p_OB9>I!@2W;dGWl+3NuCrj3)NGwHSsjVo9A+%fO;29&_yMl=P!AdC zMsKDaL~*@!pbg?iD|B7rjcr3omO*hZJ&&i-K&s)Ym&Z^IZG{z{iPPahA3mBi7)?3a z3P)i?Q!n&*Jz}K6Ay1K|4QY^|8%pCu>Wn*$(tuIz3`DP_(NM!1R zKN96o9k!hqq2x2CqG4<9w21M{U12mBI>+=(%QQ4T$~=G+s;@WJXqjSxy^ z1!^m5vYIpq95gySs7R$ZP%|%Uyo}4N*Rz2dU2En_mLzF(^;liq5IyUs(fQ#O3Kf=} zD8tP%4!H(VUPiLeTZ5ADe4&~JXz=`?BdHqH$l9o_kB7*pmNn5+w81;QEGN*fxP?xd2pq7+>oziNtYK0^ zR>Po}goe>eGFV%Iw`8McIO=7wD3@3a z27Wle0mt>Mv1JMdD9Xw^Eran4}~P9KKGR-|f9W4V#GqBivtUSKLty|-nrB6Fq&;qwSAa<|r~@m_h} zDz((ina+|o^Rt6y8b&P*oI0#RgIBU>HOz8VqcN~1^sp-0$8u9&llq4ay&rGbh}Blq zqf4+AIT|iZr5Q3D@3e5X{Hn}a2JT{bFn5EKmTnthj-w9@0xgYHD~Y-jh4kGtQ^P`W z9*20cu5=!~WQ&`!SUb$c+*mZcHy)UGuZ%vCJVAnn_!+INVq;LaXsBW^0DEna(TxHp&}4E#F4`ZUtR z7-pH|qc>9WW=e9)R$K&Gwi*Kq>2VQIGt2wvj19x|?s$8HPFo9oetciN7y{kNid+PZ zT1_8_S*bPEj_F8BA7jx>6oZBs!)pRa%0D2`s5Nq8QH^Wxk72mH&=9aJw6R6cR%7bJ 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 j - /// Создание файла + /// Создание файла /// /// Книга /// Имя файла protected abstract Task BuildInternal(Book book, string fileName); - protected virtual async Task SplitBuild(Book book, string fileName) { await BuildInternal(book, fileName); } + + protected virtual async Task SplitBuild(Book book, string fileName) + { + await BuildInternal(book, fileName); + } /// - /// Получение имени файла + /// Получение имени файла /// /// /// - protected virtual string GetFileName(Book book) => $"{GetTitle(book)}.{Extension}"; - + protected virtual string GetFileName(Book book) + { + return $"{GetTitle(book)}.{Extension}"; + } + /// - /// Получение полного названия книги + /// Получение полного названия книги /// /// /// - protected virtual string GetTitle(Book book) => BookNameBuilder.Build(Options.BookNamePattern, book).Crop(100); + protected virtual string GetTitle(Book book) + { + return BookNameBuilder.Build(Options.BookNamePattern, book).Crop(100); + } /// - /// Создание файла + /// Создание файла /// /// Книга - public async Task Build(Book book) { + public async Task Build(Book book) + { var fileName = GetFileName(book); - if (book.SupportSplitting && (Options.SplitChapters || Options.SplitVolumes)) + if (book.SupportSplitting && (Options.SplitChapters || Options.SplitVolumes)) fileName = GetTitle(book); + + if (!string.IsNullOrWhiteSpace(Options.SavePath)) { - fileName = GetTitle(book); - } - - if (!string.IsNullOrWhiteSpace(Options.SavePath)) { - if (!Directory.Exists(Options.SavePath)) { - Directory.CreateDirectory(Options.SavePath); - } + if (!Directory.Exists(Options.SavePath)) Directory.CreateDirectory(Options.SavePath); fileName = Path.Combine(Options.SavePath, fileName); } - + Logger.LogInformation($"Начинаю сохранение книги {fileName.CoverQuotes()}"); - if (book.SupportSplitting && (Options.SplitChapters || Options.SplitVolumes)) { + if (book.SupportSplitting && (Options.SplitChapters || Options.SplitVolumes)) await SplitBuild(book, fileName); - } else { + else await BuildInternal(book, fileName); - } - - if (Options.Cover) { - await SaveCover(Options.SavePath, book.Cover, GetTitle(book)); - } - + + if (Options.Cover) await SaveCover(Options.SavePath, book.Cover, GetTitle(book)); + Logger.LogInformation($"Книга {fileName.CoverQuotes()} успешно сохранена"); } /// - /// Сохранение обложки книни в отдельный файл + /// Сохранение обложки книни в отдельный файл /// /// /// /// - private async Task SaveCover(string directory, TempFile cover, string name) { - if (cover == null) { - return; - } - + private async Task SaveCover(string directory, TempFile cover, string name) + { + if (cover == null) return; + var fileName = $"{name}_cover{cover.Extension}"; - if (!string.IsNullOrWhiteSpace(directory)) { - if (!Directory.Exists(directory)) { - Directory.CreateDirectory(directory); - } + if (!string.IsNullOrWhiteSpace(directory)) + { + if (!Directory.Exists(directory)) Directory.CreateDirectory(directory); fileName = Path.Combine(directory, fileName); } diff --git a/Core/Logic/Builders/CbzBuilder.cs b/Core/Logic/Builders/CbzBuilder.cs index b39c7b60..b83d7442 100644 --- a/Core/Logic/Builders/CbzBuilder.cs +++ b/Core/Logic/Builders/CbzBuilder.cs @@ -14,20 +14,14 @@ public class CbzBuilder(Options options, ILogger logger) : BuilderBase(options, protected override async Task BuildInternal(Book book, string fileName) { - if (File.Exists(fileName)) - { - File.Delete(fileName); - } + if (File.Exists(fileName)) File.Delete(fileName); using var archive = ZipFile.Open(fileName, ZipArchiveMode.Create); var c = 0; foreach (var chapter in book.Chapters) { - if (chapter.Images == default) - { - continue; - } + if (chapter.Images is null) continue; foreach (var image in chapter.Images) { @@ -42,8 +36,7 @@ protected override async Task BuildInternal(Book book, string fileName) protected override async Task SplitBuild(Book book, string fileName) { - if (options.SplitVolumes && options.SplitChapters) - { + if (Options.SplitVolumes && Options.SplitChapters) foreach (var volumeNumber in book.Volumes) { var volume_name = $"{fileName}.v{volumeNumber}.{Extension}"; @@ -51,10 +44,7 @@ protected override async Task SplitBuild(Book book, string fileName) foreach (var chapter in book.Chapters.Where(c => c.VolumeNumber == volumeNumber)) { - if (chapter.Images == default) - { - continue; - } + if (chapter.Images is null) continue; var chapter_name = $"{fileName}.v{chapter.VolumeNumber}.c{chapter.ChapterNumber}.{Extension}"; @@ -73,15 +63,15 @@ protected override async Task SplitBuild(Book book, string fileName) await volume_fileStream.CopyToAsync(volume_entryStream); await chapter_fileStream.CopyToAsync(chapter_entryStream); } + Logger.LogInformation($"Книга {chapter_name} успешно сохранена"); chapter_archive.Dispose(); } + Logger.LogInformation($"Книга {volume_name} успешно сохранена"); volume_archive.Dispose(); } - } - else if (options.SplitVolumes) - { + else if (Options.SplitVolumes) foreach (var volumeNumber in book.Volumes) { var volume_name = $"{fileName}.v{volumeNumber}.{Extension}"; @@ -91,10 +81,7 @@ protected override async Task SplitBuild(Book book, string fileName) var c = 0; foreach (var chapter in book.Chapters.Where(c => c.VolumeNumber == volumeNumber)) { - if (chapter.Images == default) - { - continue; - } + if (chapter.Images is null) continue; foreach (var image in chapter.Images) { var image_name = $"{chapter.VolumeNumber}-{chapter.ChapterNumber}-{++c}{image.Extension}"; @@ -104,21 +91,17 @@ protected override async Task SplitBuild(Book book, string fileName) await fileStream.CopyToAsync(volume_entryStream); } } + Logger.LogInformation($"Книга {volume_name} успешно сохранена"); volume_archive.Dispose(); } - } - else if (options.SplitChapters) - { + else if (Options.SplitChapters) foreach (var chapter in book.Chapters) { var chapter_name = $"{fileName}.v{chapter.VolumeNumber}.c{chapter.ChapterNumber}.{Extension}"; using var chapter_archive = ZipFile.Open(chapter_name, ZipArchiveMode.Create); - if (chapter.Images == default) - { - continue; - } + if (chapter.Images is null) continue; var c = 0; foreach (var image in chapter.Images) { @@ -128,13 +111,11 @@ protected override async Task SplitBuild(Book book, string fileName) await using var fileStream = image.GetStream(); await fileStream.CopyToAsync(chapter_entryStream); } + Logger.LogInformation($"Книга {chapter_name} успешно сохранена"); chapter_archive.Dispose(); } - } else - { await BuildInternal(book, fileName); - } } -} \ No newline at end of file +} diff --git a/Core/Logic/Builders/EpubBuilder.cs b/Core/Logic/Builders/EpubBuilder.cs index 92098435..03f52ae9 100644 --- a/Core/Logic/Builders/EpubBuilder.cs +++ b/Core/Logic/Builders/EpubBuilder.cs @@ -1,9 +1,10 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.Common; @@ -11,95 +12,104 @@ using EpubSharp.Format; using Microsoft.Extensions.Logging; -namespace Core.Logic.Builders; +namespace Core.Logic.Builders; -public class EpubBuilder(Options options, ILogger logger) : BuilderBase(options, logger) { - protected override string Extension => "epub"; - +public class EpubBuilder(Options options, ILogger logger) : BuilderBase(options, logger) +{ private readonly EpubWriter _writer = new(); + protected override string Extension => "epub"; - private List Images { get; set; } = new(); + private List Images { get; } = new(); /// - /// Создание Xhtml документа из кода части + /// Создание Xhtml документа из кода части /// /// Шаблон части /// Заголовок части /// Раскодированный текст /// - private static string ApplyPattern(string pattern, string title, string decodeText) { - return pattern.Replace("{title}", title.CleanInvalidXmlChars().ReplaceNewLine()).Replace("{body}", decodeText.HtmlDecode().CleanInvalidXmlChars()).AsXHtmlDoc().AsString(); + private static string ApplyPattern(string pattern, string title, string decodeText) + { + return pattern.Replace("{title}", title.CleanInvalidXmlChars().ReplaceNewLine()) + .Replace("{body}", decodeText.HtmlDecode().CleanInvalidXmlChars()).AsXHtmlDoc().AsString(); } /// - /// Добавление внешних файлов + /// Добавление внешних файлов /// /// Путь к директории с файлами /// Шаблон поиска файлов /// - private void WithFiles(string directory, string searchPattern) { - foreach (var file in FileProvider.Instance.GetFiles(directory, searchPattern)) { + private void WithFiles(string directory, string searchPattern) + { + foreach (var file in FileProvider.Instance.GetFiles(directory, searchPattern)) + { var fileName = Path.GetFileName(file.Name); - var type = file.Name.EndsWith(".ttf") ? EpubContentType.FontTruetype : - file.Name.EndsWith(".css") ? EpubContentType.Css : throw new Exception($"Неизвестный тип файла {fileName}"); - + var type = file.Name.EndsWith(".ttf") ? EpubContentType.FontTruetype : + file.Name.EndsWith(".css") ? EpubContentType.Css : + throw new Elib2EbookFormatException($"Неизвестный тип файла {fileName}"); + _writer.AddFile(fileName, file.ReadAllBytes(), type); } } - - private static ImageFormat GetImageFormat(string path) { - if (path.EndsWith(".jpg", StringComparison.InvariantCultureIgnoreCase)) { - return ImageFormat.Jpeg; - } - if (path.EndsWith(".gif", StringComparison.InvariantCultureIgnoreCase)) { - return ImageFormat.Gif; - } + private static ImageFormat GetImageFormat(string path) + { + if (path.EndsWith(".jpg", StringComparison.InvariantCultureIgnoreCase)) return ImageFormat.Jpeg; - if (path.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase)) { - return ImageFormat.Png; - } + if (path.EndsWith(".gif", StringComparison.InvariantCultureIgnoreCase)) return ImageFormat.Gif; + + if (path.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase)) return ImageFormat.Png; return path.EndsWith(".svg", StringComparison.InvariantCultureIgnoreCase) ? ImageFormat.Svg : ImageFormat.Jpeg; } - private void Write(Book book) { + private void Write(Book book) + { _writer.AddAuthor(book.Author.Name); - - foreach (var coAuthor in book.CoAuthors) { - _writer.AddAuthor(coAuthor.Name); - } - + + foreach (var coAuthor in book.CoAuthors) _writer.AddAuthor(coAuthor.Name); + _writer.SetTitle(book.Title); - - if (book.Cover != null) { - _writer.SetCover(book.Cover.Content, GetImageFormat(book.Cover.FullName)); - } - - if (!string.IsNullOrWhiteSpace(book.Annotation)) { - _writer.AddDescription(book.Annotation.AsHtmlDoc().DocumentNode.GetText()); - } - - if (book.Seria != default) { + + if (book.Cover != null) _writer.SetCover(book.Cover.Content, GetImageFormat(book.Cover.FullName)); + + if (!string.IsNullOrWhiteSpace(book.Annotation)) + _writer.AddDescription(book.Annotation.AsHtmlDoc().DocumentNode.GetText()); + + if (book.Seria is not null) + { _writer.AddCollection(book.Seria.Name, book.Seria.Number); + + var seriaUrl = book.Seria.Url?.ToString(); + if (!string.IsNullOrWhiteSpace(seriaUrl) && seriaUrl.Length > 14) + EpubSharpCompat.TrySetSeriesUrlIfSupported(_writer, seriaUrl.CleanInvalidXmlChars(), Logger); } var pattern = FileProvider.Instance.ReadAllText($"{Options.ResourcesPath}/ChapterPattern.xhtml"); - foreach (var chapter in book.Chapters.Where(c => c.IsValid)) { - foreach (var image in chapter.Images) { - _writer.AddFile(image.FullName, Array.Empty(), GetImageFormat(image.FullName).ToEpubContentType()); + foreach (var chapter in book.Chapters.Where(c => c.IsValid)) + { + foreach (var image in chapter.Images) + { + _writer.AddFile(image.FullName, Array.Empty(), + GetImageFormat(image.FullName).ToEpubContentType()); Images.Add(image); } - - _writer.AddChapter(chapter.Title.HtmlDecode().ReplaceNewLine().RemoveInvalidChars(), ApplyPattern(pattern, chapter.Title, chapter.Content)); + + _writer.AddChapter(chapter.Title.HtmlDecode().ReplaceNewLine().RemoveInvalidChars(), + ApplyPattern(pattern, chapter.Title, chapter.Content)); } - + WithFiles(Options.ResourcesPath, "*.ttf"); WithFiles(Options.ResourcesPath, "*.css"); + + var ncxWarningXhtml = FileProvider.Instance.ReadAllText($"{Options.ResourcesPath}/NcxWarning.xhtml"); + EpubSharpCompat.TryAddNcxWarningPageIfSupported(_writer, "Предупреждение", ncxWarningXhtml, Logger); } - protected override async Task BuildInternal(Book book, string fileName) { + protected override async Task BuildInternal(Book book, string fileName) + { Write(book); await _writer.Write(fileName, Images.Select(image => new FileMeta(image.FullName, image.FilePath))); } -} \ No newline at end of file +} diff --git a/Core/Logic/Builders/EpubSharpCompat.cs b/Core/Logic/Builders/EpubSharpCompat.cs new file mode 100644 index 00000000..07366596 --- /dev/null +++ b/Core/Logic/Builders/EpubSharpCompat.cs @@ -0,0 +1,249 @@ +using System; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text.Json; +using EpubSharp; +using Microsoft.Extensions.Logging; + +namespace Core.Logic.Builders; + +internal static class EpubSharpCompat +{ + private const string ExpectedPdbMarker = "EpubSharp_Elib2Ebook"; + private const string CacheFileName = "epubsharp-origin.json"; + + private static readonly object CacheLock = new(); + private static OriginCacheEntry _originCache; + + private static readonly Lazy> TrySetSeriesUrlInvoker = + new(CreateTrySetSeriesUrlInvoker); + + private static readonly Lazy> TryAddNcxWarningPageInvoker = + new(CreateTryAddNcxWarningPageInvoker); + + public static bool TrySetSeriesUrlIfSupported(EpubWriter writer, string seriesUrl, ILogger logger) + { + if (writer == null) return false; + if (string.IsNullOrWhiteSpace(seriesUrl)) return false; + + if (!IsEpubSharpElib2EbookBuild(writer.GetType().Assembly, logger)) return false; + + var invoker = TrySetSeriesUrlInvoker.Value; + if (invoker == null) return false; + + try + { + return invoker(writer, seriesUrl); + } + catch (Exception ex) + { + logger?.LogDebug(ex, "EpubSharp TrySetSeriesUrl failed; skipping series url."); + return false; + } + } + + public static bool TryAddNcxWarningPageIfSupported(EpubWriter writer, string title, string xhtml, ILogger logger) + { + if (writer == null) return false; + if (string.IsNullOrWhiteSpace(title)) return false; + if (string.IsNullOrWhiteSpace(xhtml)) return false; + + var invoker = TryAddNcxWarningPageInvoker.Value; + if (invoker == null) return false; + + try + { + return invoker(writer, title, xhtml); + } + catch (Exception ex) + { + logger?.LogDebug(ex, "EpubSharp TryAddNcxWarningPage failed; skipping ncx warning."); + return false; + } + } + + private static Func CreateTrySetSeriesUrlInvoker() + { + var method = typeof(EpubWriter).GetMethod( + "TrySetSeriesUrl", + BindingFlags.Instance | BindingFlags.Public, + null, + [typeof(string)], + null); + + if (method == null) return null; + if (method.ReturnType != typeof(bool)) return null; + + try + { + return method.CreateDelegate>(); + } + catch + { + return null; + } + } + + private static Func CreateTryAddNcxWarningPageInvoker() + { + var method = typeof(EpubWriter).GetMethod( + "TryAddNcxWarningPage", + BindingFlags.Instance | BindingFlags.Public, + null, + [typeof(string), typeof(string)], + null); + + if (method == null) return null; + if (method.ReturnType != typeof(bool)) return null; + + try + { + return method.CreateDelegate>(); + } + catch + { + return null; + } + } + + private static bool IsEpubSharpElib2EbookBuild(Assembly epubSharpAssembly, ILogger logger) + { + var (mvid, size, dllPath) = GetAssemblyFingerprint(epubSharpAssembly); + if (string.IsNullOrWhiteSpace(dllPath)) return false; + + lock (CacheLock) + { + if (_originCache != null && + _originCache.Mvid == mvid && + _originCache.Size == size) + return _originCache.IsEpubSharpElib2Ebook; + + var cachePath = GetCachePath(); + var disk = TryReadCache(cachePath); + if (disk != null && disk.Mvid == mvid && disk.Size == size) + { + _originCache = disk; + return disk.IsEpubSharpElib2Ebook; + } + + var isExpected = ComputeIsExpectedBuild(dllPath); + var entry = new OriginCacheEntry + { + Mvid = mvid, + Size = size, + IsEpubSharpElib2Ebook = isExpected + }; + + _originCache = entry; + TryWriteCache(cachePath, entry, logger); + return isExpected; + } + } + + private static bool ComputeIsExpectedBuild(string dllPath) + { + try + { + var dir = Path.GetDirectoryName(dllPath); + if (string.IsNullOrWhiteSpace(dir)) return false; + + var pdbPath = Path.Combine(dir, "EpubSharp.pdb"); + if (!File.Exists(pdbPath)) return false; + + var bytes = File.ReadAllBytes(pdbPath); + return ContainsAscii(bytes, ExpectedPdbMarker); + } + catch + { + return false; + } + } + + private static (Guid mvid, long size, string path) GetAssemblyFingerprint(Assembly assembly) + { + try + { + var path = assembly.Location; + var size = new FileInfo(path).Length; + var mvid = typeof(EpubWriter).Module.ModuleVersionId; + return (mvid, size, path); + } + catch + { + return (Guid.Empty, 0, string.Empty); + } + } + + private static string GetCachePath() + { + var root = Environment.GetEnvironmentVariable("XDG_CACHE_HOME"); + if (string.IsNullOrWhiteSpace(root)) + root = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cache"); + + return Path.Combine(root, "Elib2Ebook", CacheFileName); + } + + private static OriginCacheEntry TryReadCache(string cachePath) + { + try + { + if (!File.Exists(cachePath)) return null; + var json = File.ReadAllText(cachePath); + return JsonSerializer.Deserialize(json); + } + catch + { + return null; + } + } + + private static void TryWriteCache(string cachePath, OriginCacheEntry entry, ILogger logger) + { + try + { + var dir = Path.GetDirectoryName(cachePath); + if (!string.IsNullOrWhiteSpace(dir)) Directory.CreateDirectory(dir); + + var json = JsonSerializer.Serialize(entry); + File.WriteAllText(cachePath, json); + } + catch (Exception ex) + { + if (logger != null) logger.LogDebug(ex, "Failed to write EpubSharp origin cache."); + } + } + + private static bool ContainsAscii(byte[] haystack, string needle) + { + if (haystack.Length == 0) return false; + if (string.IsNullOrEmpty(needle)) return false; + + var n = needle.Length; + if (n > haystack.Length) return false; + + for (var i = 0; i <= haystack.Length - n; i++) + { + var ok = true; + for (var j = 0; j < n; j++) + if (haystack[i + j] != (byte)needle[j]) + { + ok = false; + break; + } + + if (ok) return true; + } + + return false; + } + + private sealed class OriginCacheEntry + { + public Guid Mvid { get; set; } + public long Size { get; set; } + public bool IsEpubSharpElib2Ebook { get; set; } + } +} diff --git a/Core/Logic/Builders/Fb2Builder.cs b/Core/Logic/Builders/Fb2Builder.cs index 446567bf..122c27cd 100644 --- a/Core/Logic/Builders/Fb2Builder.cs +++ b/Core/Logic/Builders/Fb2Builder.cs @@ -18,38 +18,38 @@ namespace Core.Logic.Builders; public class Fb2Builder : BuilderBase { - protected override string Extension => "fb2"; - - private readonly XNamespace _ns = "http://www.gribuser.ru/xml/fictionbook/2.0"; - private readonly XNamespace _xlink = "http://www.w3.org/1999/xlink"; + private readonly XElement _body; + private readonly XElement _customInfo; private readonly XElement _description; - private readonly XElement _titleInfo; - private readonly XElement _body; private readonly XElement _documentInfo; - private readonly XElement _customInfo; private readonly List _images = new(); - private readonly Dictionary _map = new() { - {"strong", "strong"}, - {"b", "strong"}, - {"i", "emphasis"}, - {"em", "emphasis"}, - {"emphasis", "emphasis"}, - {"del", "strikethrough"}, - {"strikethrough", "strikethrough"}, - {"blockquote", "cite"}, - {"h1", "subtitle"}, - {"h2", "subtitle"}, - {"h3", "subtitle"}, - {"h4", "subtitle"}, - {"h5", "subtitle"}, - {"h6", "subtitle"}, - {"p", "p"}, - {"div", "p"}, - {"u", "u"}, + private readonly Dictionary _map = new() + { + { "strong", "strong" }, + { "b", "strong" }, + { "i", "emphasis" }, + { "em", "emphasis" }, + { "emphasis", "emphasis" }, + { "del", "strikethrough" }, + { "strikethrough", "strikethrough" }, + { "blockquote", "cite" }, + { "h1", "subtitle" }, + { "h2", "subtitle" }, + { "h3", "subtitle" }, + { "h4", "subtitle" }, + { "h5", "subtitle" }, + { "h6", "subtitle" }, + { "p", "p" }, + { "div", "p" }, + { "u", "u" } }; + private readonly XNamespace _ns = "http://www.gribuser.ru/xml/fictionbook/2.0"; + private readonly XElement _titleInfo; + private readonly XNamespace _xlink = "http://www.w3.org/1999/xlink"; + public Fb2Builder(Options options, ILogger logger) : base(options, logger) { _description = CreateXElement("description"); @@ -59,6 +59,8 @@ public Fb2Builder(Options options, ILogger logger) : base(options, logger) _body = CreateXElement("body"); } + protected override string Extension => "fb2"; + private XElement CreateXElement(string name) { return new XElement(_ns + name); @@ -86,7 +88,7 @@ private XElement CreateAuthor(Author author) authorElem.Add(firstName); } - if (author.Url != default) + if (author.Url is not null) { var homePageElem = CreateXElement("home-page"); homePageElem.Value = author.Url.ToString(); @@ -116,7 +118,8 @@ private static async Task WriteBinary(XmlWriter writer, TempFile tempFile) await using var inputFile = tempFile.GetStream(); await writer.WriteStartElementAsync(null, "binary", null); await writer.WriteAttributeStringAsync(null, "id", null, tempFile.FullName); - await writer.WriteAttributeStringAsync(null, "content-type", null, "image/" + tempFile.Extension.TrimStart('.')); + await writer.WriteAttributeStringAsync(null, "content-type", null, + "image/" + tempFile.Extension.TrimStart('.')); using var br = new BinaryReader(inputFile); do @@ -129,7 +132,7 @@ private static async Task WriteBinary(XmlWriter writer, TempFile tempFile) } /// - /// Добавление автора книги + /// Добавление автора книги /// /// Автор /// @@ -141,7 +144,7 @@ private void AddAuthor(Author author) } /// - /// Добавление со-авторов книги + /// Добавление со-авторов книги /// /// Со-авторы /// @@ -156,7 +159,7 @@ private void AddCoAuthors(IEnumerable coAuthors) } /// - /// Указание названия книги + /// Указание названия книги /// /// Название книги /// @@ -170,13 +173,13 @@ private void WithTitle(string title) } /// - /// Добавление обложки книги + /// Добавление обложки книги /// /// Обложка /// private void WithCover(TempFile cover) { - if (cover != default) + if (cover is not null) { var coverPage = CreateXElement("coverpage"); @@ -191,7 +194,7 @@ private void WithCover(TempFile cover) private void WithBookUrl(Uri url) { - if (url != default) + if (url is not null) { var srcUrlElem = CreateXElement("src-url"); srcUrlElem.Value = url.ToString().CleanInvalidXmlChars(); @@ -201,10 +204,7 @@ private void WithBookUrl(Uri url) private void WithAnnotation(string annotation) { - if (!string.IsNullOrWhiteSpace(annotation)) - { - _titleInfo.Add(CreateAnnotation(annotation)); - } + if (!string.IsNullOrWhiteSpace(annotation)) _titleInfo.Add(CreateAnnotation(annotation)); } private XElement CreateAnnotation(string annotation) @@ -213,18 +213,14 @@ private XElement CreateAnnotation(string annotation) var doc = annotation.AsHtmlDoc(); foreach (var node in doc.DocumentNode.ChildNodes) - { if (!string.IsNullOrWhiteSpace(node.InnerText)) - { ProcessSection(a, node, "p"); - } - } return a; } /// - /// Добавление списка частей книги + /// Добавление списка частей книги /// /// Список частей /// @@ -236,23 +232,17 @@ private void WithChapters(IEnumerable chapters) section.Add(CreateTitle(chapter.Title)); var doc = chapter.Content.AsHtmlDoc(); - foreach (var node in doc.DocumentNode.ChildNodes) - { - ProcessSection(section, node); - } + foreach (var node in doc.DocumentNode.ChildNodes) ProcessSection(section, node); _body.Add(section); - foreach (var image in chapter.Images) - { - _images.Add(image); - } + foreach (var image in chapter.Images) _images.Add(image); } } private void WithSeria(Seria seria) { - if (seria != default) + if (seria is not null) { var sequenceElem = CreateXElement("sequence"); sequenceElem.SetAttributeValue("name", seria.Name.CleanInvalidXmlChars()); @@ -260,7 +250,7 @@ private void WithSeria(Seria seria) _titleInfo.Add(sequenceElem); // seria url var SeriaUrl = seria.Url.ToString(); - if( !string.IsNullOrWhiteSpace(SeriaUrl) && SeriaUrl.Length > 14 ) + if (!string.IsNullOrWhiteSpace(SeriaUrl) && SeriaUrl.Length > 14) { _customInfo.SetAttributeValue("info-type", "sequence-url"); _customInfo.Add(SeriaUrl.CleanInvalidXmlChars()); @@ -332,25 +322,16 @@ private void ProcessSection(XElement parent, HtmlNode node, string textNode = "" { var section = CreateXElement(_map.GetValueOrDefault(node.Name, "p")); - foreach (var child in node.ChildNodes) - { - ProcessSection(IsTextNode(node) ? parent : section, child); - } + foreach (var child in node.ChildNodes) ProcessSection(IsTextNode(node) ? parent : section, child); - if (!section.IsEmpty) - { - parent.Add(section); - } + if (!section.IsEmpty) parent.Add(section); return; } if (node.Name == "img") { - if (node.Attributes["src"] == null) - { - return; - } + if (node.Attributes["src"] == null) return; var imageElem = CreateXElement("image"); imageElem.SetAttributeValue(_xlink + "href", "#" + node.Attributes["src"].Value); @@ -360,15 +341,9 @@ private void ProcessSection(XElement parent, HtmlNode node, string textNode = "" } var nodeText = node.InnerText.HtmlDecode().CleanInvalidXmlChars(); - if (node.InnerText.StartsWith(" ")) - { - nodeText = " " + nodeText; - } + if (node.InnerText.StartsWith(" ")) nodeText = " " + nodeText; - if (node.InnerText.EndsWith(" ")) - { - nodeText += " "; - } + if (node.InnerText.EndsWith(" ")) nodeText += " "; if (IsTextNode(node)) { @@ -410,7 +385,6 @@ private XElement GetDateElement(DateTime date) return dateElem; } - protected override async Task BuildInternal(Book book, string fileName) { await using var file = File.Create(fileName); @@ -434,10 +408,7 @@ protected override async Task BuildInternal(Book book, string fileName) _description.Add(_titleInfo); _description.Add(_documentInfo); - if( !string.IsNullOrEmpty(_customInfo.Value) ) - { - _description.Add(_customInfo); - } + if (!string.IsNullOrEmpty(_customInfo.Value)) _description.Add(_customInfo); var xws = new XmlWriterSettings { @@ -445,7 +416,7 @@ protected override async Task BuildInternal(Book book, string fileName) Encoding = new UTF8Encoding(false), Indent = true, NewLineHandling = NewLineHandling.Replace, - NewLineChars = "\r\n", + NewLineChars = "\r\n" }; await using var writer = XmlWriter.Create(file, xws); @@ -456,10 +427,7 @@ protected override async Task BuildInternal(Book book, string fileName) await _description.WriteToAsync(writer, cancellationToken); await _body.WriteToAsync(writer, cancellationToken); - foreach (var image in _images) - { - await WriteBinary(writer, image); - } + foreach (var image in _images) await WriteBinary(writer, image); await writer.WriteEndElementAsync(); await writer.FlushAsync(); @@ -475,26 +443,26 @@ protected override async Task SplitBuild(Book book, string fileName) WithCover(book.Cover); WithLang(book.Lang); WithSeria(book.Seria); - + _documentInfo.Add(GetDateElement(DateTime.Today)); - + var programUsed = CreateXElement("program-used"); programUsed.Value = "Elib2Ebook"; _documentInfo.Add(programUsed); - + _description.Add(_titleInfo); _description.Add(_documentInfo); - - var xws = new XmlWriterSettings { + + var xws = new XmlWriterSettings + { Async = true, Encoding = new UTF8Encoding(false), Indent = true, NewLineHandling = NewLineHandling.Replace, - NewLineChars = "\r\n", + NewLineChars = "\r\n" }; if (Options.SplitVolumes && Options.SplitChapters) - { foreach (var volumeNumber in book.Volumes) { var volume_name = $"{fileName}.v{volumeNumber}.{Extension}"; @@ -519,9 +487,7 @@ protected override async Task SplitBuild(Book book, string fileName) Logger.LogInformation($"Книга {volume_name} успешно сохранена"); volume_file.Dispose(); } - } else if (Options.SplitVolumes) - { foreach (var volumeNumber in book.Volumes) { var volume_name = $"{fileName}.v{volumeNumber}.{Extension}"; @@ -536,9 +502,7 @@ protected override async Task SplitBuild(Book book, string fileName) volume_file.Dispose(); } - } else if (Options.SplitChapters) - { foreach (var chapter in book.Chapters) { var chapter_name = $"{fileName}.v{chapter.VolumeNumber}.c{chapter.ChapterNumber}.{Extension}"; @@ -550,11 +514,8 @@ protected override async Task SplitBuild(Book book, string fileName) chapter_file.Dispose(); } - } else - { await BuildInternal(book, fileName); - } } protected async Task BuildPart(IEnumerable chapters, FileStream file, XmlWriterSettings xws) @@ -569,10 +530,7 @@ protected async Task BuildPart(IEnumerable chapters, FileStream file, X await _description.WriteToAsync(writer, cancellationToken); await _body.WriteToAsync(writer, cancellationToken); - foreach (var image in _images) - { - await WriteBinary(writer, image); - } + foreach (var image in _images) await WriteBinary(writer, image); await writer.WriteEndElementAsync(); await writer.FlushAsync(); @@ -580,4 +538,4 @@ protected async Task BuildPart(IEnumerable chapters, FileStream file, X _body.RemoveNodes(); _images.Clear(); } -} \ No newline at end of file +} diff --git a/Core/Logic/Builders/JsonBuilder.cs b/Core/Logic/Builders/JsonBuilder.cs index 99a97f80..e7e019de 100644 --- a/Core/Logic/Builders/JsonBuilder.cs +++ b/Core/Logic/Builders/JsonBuilder.cs @@ -5,18 +5,21 @@ using Core.Types.Book; using Microsoft.Extensions.Logging; -namespace Core.Logic.Builders; +namespace Core.Logic.Builders; -public class JsonBuilder(Options options, ILogger logger) : BuilderBase(options, logger) { +public class JsonBuilder(Options options, ILogger logger) : BuilderBase(options, logger) +{ protected override string Extension => "json"; - protected override async Task BuildInternal(Book book, string fileName) { + protected override async Task BuildInternal(Book book, string fileName) + { await using var file = File.Create(fileName); - - var jsonSerializerOptions = new JsonSerializerOptions { + + var jsonSerializerOptions = new JsonSerializerOptions + { WriteIndented = true }; - + await JsonSerializer.SerializeAsync(file, book, jsonSerializerOptions); } -} \ No newline at end of file +} diff --git a/Core/Logic/Builders/JsonLiteBuilder.cs b/Core/Logic/Builders/JsonLiteBuilder.cs index 258635f4..e1ba28a5 100644 --- a/Core/Logic/Builders/JsonLiteBuilder.cs +++ b/Core/Logic/Builders/JsonLiteBuilder.cs @@ -10,17 +10,18 @@ namespace Core.Logic.Builders; -public class ShortImageConverter : JsonConverter { - public override TempFile Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { +public class ShortImageConverter : JsonConverter +{ + public override TempFile Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { return JsonSerializer.Deserialize(ref reader, options); } - public override void Write(Utf8JsonWriter writer, TempFile value, JsonSerializerOptions options) { + public override void Write(Utf8JsonWriter writer, TempFile value, JsonSerializerOptions options) + { writer.WriteStartObject(); - - if (value.Url != default) { - writer.WriteString(nameof(value.Url), value.Url.ToString()); - } + + if (value.Url is not null) writer.WriteString(nameof(value.Url), value.Url.ToString()); writer.WriteString(nameof(value.Directory), value.Directory); writer.WriteString(nameof(value.FullName), value.FullName); @@ -30,38 +31,42 @@ public override void Write(Utf8JsonWriter writer, TempFile value, JsonSerializer } } -public class ShortChapterConverter : JsonConverter { - public override Chapter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { +public class ShortChapterConverter : JsonConverter +{ + public override Chapter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { return JsonSerializer.Deserialize(ref reader, options); } - public override void Write(Utf8JsonWriter writer, Chapter value, JsonSerializerOptions options) { + public override void Write(Utf8JsonWriter writer, Chapter value, JsonSerializerOptions options) + { writer.WriteStartObject(); - + writer.WriteString(nameof(value.Title), value.Title); writer.WriteBoolean(nameof(value.IsValid), value.IsValid); - + writer.WriteStartArray(nameof(value.Images)); - foreach (var image in value.Images) { - JsonSerializer.Serialize(writer, image, options); - } - + foreach (var image in value.Images) JsonSerializer.Serialize(writer, image, options); + writer.WriteEndArray(); writer.WriteEndObject(); } } -public class JsonLiteBuilder(Options options, ILogger logger) : BuilderBase(options, logger) { +public class JsonLiteBuilder(Options options, ILogger logger) : BuilderBase(options, logger) +{ protected override string Extension => "json"; - protected override async Task BuildInternal(Book book, string fileName) { + protected override async Task BuildInternal(Book book, string fileName) + { await using var file = File.Create(fileName); - - var jsonSerializerOptions = new JsonSerializerOptions { + + var jsonSerializerOptions = new JsonSerializerOptions + { WriteIndented = true, Converters = { new ShortChapterConverter(), new ShortImageConverter() } }; await JsonSerializer.SerializeAsync(file, book, jsonSerializerOptions); } -} \ No newline at end of file +} diff --git a/Core/Logic/Builders/TxtBuilder.cs b/Core/Logic/Builders/TxtBuilder.cs index e7ae5707..8af828a3 100644 --- a/Core/Logic/Builders/TxtBuilder.cs +++ b/Core/Logic/Builders/TxtBuilder.cs @@ -6,39 +6,37 @@ using Core.Types.Book; using Microsoft.Extensions.Logging; -namespace Core.Logic.Builders; +namespace Core.Logic.Builders; -public class TxtBuilder(Options options, ILogger logger) : BuilderBase(options, logger) { +public class TxtBuilder(Options options, ILogger logger) : BuilderBase(options, logger) +{ protected override string Extension => "txt"; - protected override async Task BuildInternal(Book book, string fileName) { - if (File.Exists(fileName)) { - File.Delete(fileName); - } - + protected override async Task BuildInternal(Book book, string fileName) + { + if (File.Exists(fileName)) File.Delete(fileName); + await using var file = File.CreateText(fileName); - - foreach (var chapter in book.Chapters.Where(c => c.IsValid)) { + + foreach (var chapter in book.Chapters.Where(c => c.IsValid)) + { await file.WriteLineAsync(" " + chapter.Title); await file.WriteLineAsync(); var prettyText = chapter.Content.PrettyHtml().AsHtmlDoc().DocumentNode.GetText(); - using var sr = new StringReader(prettyText); - - while (true) { + using var sr = new StringReader(prettyText); + + while (true) + { var line = await sr.ReadLineAsync(); - if (line == null) { - break; - } - - if (string.IsNullOrWhiteSpace(line)) { - continue; - } - + if (line == null) break; + + if (string.IsNullOrWhiteSpace(line)) continue; + await file.WriteLineAsync(" " + line.Trim()); } - + await file.WriteLineAsync(); } } -} \ No newline at end of file +} diff --git a/Core/Logic/FileProvider.cs b/Core/Logic/FileProvider.cs index 0acf1020..c2b5d608 100644 --- a/Core/Logic/FileProvider.cs +++ b/Core/Logic/FileProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Reflection; @@ -6,20 +6,20 @@ namespace Core.Logic; -public class FileProvider { +public class FileProvider +{ public static readonly IFileProvider Instance; - static FileProvider() { + static FileProvider() + { var cwd = AppDomain.CurrentDomain.BaseDirectory; var providers = new List(); var patternsDirectory = Path.Combine(cwd); - if (Directory.Exists(patternsDirectory)) { - providers.Add(new PhysicalFileProvider(patternsDirectory)); - } + if (Directory.Exists(patternsDirectory)) providers.Add(new PhysicalFileProvider(patternsDirectory)); providers.Add(new ManifestEmbeddedFileProvider(Assembly.GetExecutingAssembly())); Instance = new CompositeFileProvider(providers); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/AcomicsGetter.cs b/Core/Logic/Getters/AcomicsGetter.cs index a98c5cf9..66982b00 100644 --- a/Core/Logic/Getters/AcomicsGetter.cs +++ b/Core/Logic/Getters/AcomicsGetter.cs @@ -11,26 +11,33 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class AcomicsGetter(BookGetterConfig config) : GetterBase(config) { +public class AcomicsGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://acomics.ru"); - protected override string GetId(Uri url) => url.GetSegment(1); + protected override string GetId(Uri url) + { + return url.GetSegment(1); + } - public override async Task Init() { + public override async Task Init() + { await base.Init(); Config.CookieContainer.Add(SystemUrl, new Cookie("ageRestrict", "18")); } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { var id = GetId(url); url = SystemUrl.MakeRelativeUri(id); var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri($"{id}/1")); var title = doc.QuerySelector("meta[property=og:title]").Attributes["content"].Value; - var book = new Book(url) { + var book = new Book(url) + { Chapters = await FillChapters(doc, id, title, url), Title = title, Author = GetAuthor(doc, url), @@ -38,30 +45,33 @@ public override async Task Get(Uri url) { }; book.Cover = book.Chapters.FirstOrDefault()?.Images.FirstOrDefault(); - + return book; } - private static Author GetAuthor(HtmlDocument doc, Uri url) { - var a = doc.QuerySelector("article.authors a.username"); + private static Author GetAuthor(HtmlDocument doc, Uri url) + { + var a = doc.QuerySelector("article.authors,a.username"); return new Author(a.GetText(), url.MakeRelativeUri(a.Attributes["href"].Value)); } - private async Task> FillChapters(HtmlDocument doc, string bookId, string title, Uri url) { - if (Config.Options.NoChapters) { - return []; - } - - var chapter = new Chapter { + private async Task> FillChapters(HtmlDocument doc, string bookId, string title, Uri url) + { + if (Config.Options.NoChapters) return []; + + var chapter = new Chapter + { Title = title }; - - var pages = int.Parse(doc.GetTextBySelector("span.issueNumber").Split("/").Last()); + + var pages = int.Parse(doc.GetTextBySelector("span.issueNumber,span.number-without-name,span.number-with-name") + .Split("/").Last()); var sb = new StringBuilder(); - for (var i = 1; i <= pages; i++) { + for (var i = 1; i <= pages; i++) + { Config.Logger.LogInformation($"Получаю страницу {i}/{pages}"); var response = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri($"{bookId}/{i}")); - var img = response.QuerySelector("#mainImage"); + var img = response.QuerySelector("#mainImage") ?? response.QuerySelector("img.issue"); var src = url.MakeRelativeUri(img.Attributes["src"].Value); sb.Append($""); } @@ -70,6 +80,6 @@ private async Task> FillChapters(HtmlDocument doc, string b chapter.Images = await GetImages(chapterDoc, url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; - return new [] { chapter }; + return new[] { chapter }; } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/AuthorTodayGetter.cs b/Core/Logic/Getters/AuthorTodayGetter.cs index dc39d1c1..656f44d0 100644 --- a/Core/Logic/Getters/AuthorTodayGetter.cs +++ b/Core/Logic/Getters/AuthorTodayGetter.cs @@ -11,6 +11,7 @@ using System.Text.Json; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Misc; using Core.Types.AuthorToday; @@ -20,22 +21,25 @@ namespace Core.Logic.Getters; -public class AuthorTodayGetter(BookGetterConfig config) : GetterBase(config) { +public class AuthorTodayGetter(BookGetterConfig config) : GetterBase(config) +{ + private const string AT_CERT = "rmy8LDkVZR+pSXopxUte7hFbA6I="; + + private bool _bypass; + private bool _singleFetchUsed; protected override Uri SystemUrl => new("https://author.today/"); - + private Uri _apiUrl => new("https://api.author.today/"); - + /// - /// IP сайта author.today + /// IP сайта author.today /// private Uri _systemIp => new("https://185.26.98.195/"); /// - /// IP сайта api.author.today + /// IP сайта api.author.today /// private Uri _apiIp => new("https://185.26.98.195/"); - - private bool _bypass; private Uri ApiUrl => _bypass ? _apiIp : _apiUrl; @@ -43,90 +47,102 @@ public class AuthorTodayGetter(BookGetterConfig config) : GetterBase(config) { private string UserId { get; set; } = string.Empty; - protected override string GetId(Uri url) => url.GetSegment(2); + private string hashedCert => Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(AT_CERT))).ToUpperInvariant(); - private const string AT_CERT = "rmy8LDkVZR+pSXopxUte7hFbA6I="; - - private string hashedCert => Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(AT_CERT))).ToUpper(); + protected override string GetId(Uri url) + { + return url.GetSegment(2); + } - public override async Task Init() { - Config.Client.DefaultRequestHeaders.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); + public override async Task Init() + { + Config.Client.DefaultRequestHeaders.Add("Accept", + "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); Config.Client.DefaultRequestHeaders.Add("Accept-Language", "ru"); Config.Client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate, br"); Config.Client.DefaultRequestHeaders.Add("User-Agent", "okhttp/4.12.0 X_AT_API"); Config.Client.DefaultRequestHeaders.Add("X-AT-Client", "android_1.8.013-GMS"); Config.Client.DefaultRequestHeaders.Add("X-AT-Certificate", hashedCert); - + var response = await Config.Client.GetAsync(_apiUrl); _bypass = response.StatusCode != HttpStatusCode.OK; - Config.Logger.LogInformation(_bypass ? - $"Сайт {_apiUrl} не доступен. Работаю через {_apiIp}" : - $"Сайт {_apiUrl} доступен. Работаю через него"); + Config.Logger.LogInformation(_bypass + ? $"Сайт {_apiUrl} не доступен. Работаю через {_apiIp}" + : $"Сайт {_apiUrl} доступен. Работаю через него"); Config.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "guest"); } - - public override async Task Authorize() { - if (!Config.HasCredentials) { - return; - } - + + public override async Task Authorize() + { + if (!Config.HasCredentials) return; + // АТ очень не любит много авторизовывать // поэтому пришлось добавить кеширование токенов const string directory = "ATCache"; - if (!Directory.Exists(directory)) { - Directory.CreateDirectory(directory); - } + if (!Directory.Exists(directory)) Directory.CreateDirectory(directory); var saveCreds = $"{directory}/{Config.Options.Login.RemoveInvalidChars()}"; - if (File.Exists(saveCreds)) { + if (File.Exists(saveCreds)) + { var timeDiff = DateTime.Now - File.GetLastWriteTime(saveCreds); - if ( timeDiff.TotalHours < 24 ) + if (timeDiff.TotalHours < 24) { - var savedAuth = await File.ReadAllTextAsync(saveCreds).ContinueWith(t => t.Result.Deserialize()); - Config.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", savedAuth.Token); + var savedAuth = await File.ReadAllTextAsync(saveCreds) + .ContinueWith(t => t.Result.Deserialize()); + Config.Client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", savedAuth.Token); try { - var checkResponse = await Config.Client.SendWithTriesAsync(() => GetDefaultMessage(ApiUrl.MakeRelativeUri("/v1/account/current-user"), _apiUrl)); + var checkResponse = await Config.Client.SendWithTriesAsync(() => + GetDefaultMessage(ApiUrl.MakeRelativeUri("/v1/account/current-user"), _apiUrl)); var checkUser = await checkResponse.Content.ReadFromJsonAsync(); UserId = checkUser!.Id.ToString(); } - catch (System.Exception){} - - if( !string.IsNullOrWhiteSpace(UserId) ) + catch (Exception) { - return; } + + if (!string.IsNullOrWhiteSpace(UserId)) return; } else { File.Delete(saveCreds); } } - - var response = await Config.Client.SendAsync(GetDefaultMessage(ApiUrl.MakeRelativeUri("/v1/account/login-by-password"), _apiUrl, JsonContent.Create(new { Config.Options.Login, Config.Options.Password }))); + + var response = await Config.Client.SendAsync(GetDefaultMessage( + ApiUrl.MakeRelativeUri("/v1/account/login-by-password"), _apiUrl, + JsonContent.Create(new { Config.Options.Login, Config.Options.Password }))); var data = await response.Content.ReadFromJsonAsync(); - if (!string.IsNullOrWhiteSpace(data?.Token)) { + if (!string.IsNullOrWhiteSpace(data?.Token)) + { Config.Logger.LogInformation("Успешно авторизовались"); Config.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", data.Token); - response = await Config.Client.SendWithTriesAsync(() => GetDefaultMessage(ApiUrl.MakeRelativeUri("/v1/account/current-user"), _apiUrl)); + response = await Config.Client.SendWithTriesAsync(() => + GetDefaultMessage(ApiUrl.MakeRelativeUri("/v1/account/current-user"), _apiUrl)); var user = await response.Content.ReadFromJsonAsync(); UserId = user!.Id.ToString(); - } else { - throw new Exception($"Не удалось авторизоваться. {data?.Message}"); } + else + { + throw new Elib2EbookAuthException($"Не удалось авторизоваться. {data?.Message}"); + } + await File.WriteAllTextAsync(saveCreds, JsonSerializer.Serialize(data)); } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { url = SystemUrl.MakeRelativeUri($"/work/{GetId(url)}"); var details = await GetBookDetails(GetId(url)); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(details), Chapters = await FillChapters(details), Title = details.Title, @@ -141,92 +157,98 @@ public override async Task Get(Uri url) { return book; } - private async Task FillAdditional(Book book, AuthorTodayBookDetails details) { - if (!Config.Options.HasAdditionalType(AdditionalTypeEnum.Images) || details.GalleryImages == default || details.GalleryImages.Length == 0) { - return; - } + private async Task FillAdditional(Book book, AuthorTodayBookDetails details) + { + if (!Config.Options.HasAdditionalType(AdditionalTypeEnum.Images) || details.GalleryImages == null || + details.GalleryImages.Length == 0) return; Config.Logger.LogInformation("Загружаю дополнительные иллюстрации"); - foreach (var image in details.GalleryImages) { + foreach (var image in details.GalleryImages) book.AdditionalFiles.Add(AdditionalTypeEnum.Images, await SaveImage(SystemUrl.MakeRelativeUri(image.Url))); - } Config.Logger.LogInformation("Дополнительные иллюстрации загружены"); } - private HttpRequestMessage GetDefaultMessage(Uri uri, Uri host, HttpContent content = null) { - var message = new HttpRequestMessage(content == default ? HttpMethod.Get : HttpMethod.Post, uri); + private HttpRequestMessage GetDefaultMessage(Uri uri, Uri host, HttpContent content = null) + { + var message = new HttpRequestMessage(content == null ? HttpMethod.Get : HttpMethod.Post, uri); message.Content = content; message.Version = Config.Client.DefaultRequestVersion; - - foreach (var header in Config.Client.DefaultRequestHeaders) { - message.Headers.Add(header.Key, header.Value); - } + + foreach (var header in Config.Client.DefaultRequestHeaders) message.Headers.Add(header.Key, header.Value); message.Headers.Host = host.Host; return message; } - private async Task GetBookDetails(string bookId) { - var response = await Config.Client.SendWithTriesAsync(() => GetDefaultMessage(ApiUrl.MakeRelativeUri($"/v1/work/{bookId}/details"), _apiUrl)); - if (response.StatusCode != HttpStatusCode.OK) { - throw new Exception("Книга не найдена"); - } + private async Task GetBookDetails(string bookId) + { + var response = await Config.Client.SendWithTriesAsync(() => + GetDefaultMessage(ApiUrl.MakeRelativeUri($"/v1/work/{bookId}/details"), _apiUrl)); + if (response.StatusCode != HttpStatusCode.OK) throw new Elib2EbookParseException("Книга не найдена"); return await response.Content.ReadFromJsonAsync(); } - private Author GetAuthor(AuthorTodayBookDetails book) { + private Author GetAuthor(AuthorTodayBookDetails book) + { return new Author(book.AuthorFio, SystemUrl.MakeRelativeUri($"/u/{book.AuthorUserName}/works")); } - - private IEnumerable GetCoAuthors(AuthorTodayBookDetails book) { + + private IEnumerable GetCoAuthors(AuthorTodayBookDetails book) + { var result = new List(); - if (!string.IsNullOrWhiteSpace(book.CoAuthorFio)) { - result.Add( new Author(book.CoAuthorFio, SystemUrl.MakeRelativeUri($"/u/{book.CoAuthorUserName}/works"))); - } + if (!string.IsNullOrWhiteSpace(book.CoAuthorFio)) + result.Add(new Author(book.CoAuthorFio, SystemUrl.MakeRelativeUri($"/u/{book.CoAuthorUserName}/works"))); return result; } - private Seria GetSeria(AuthorTodayBookDetails book) { - if (!book.SeriesId.HasValue) { - return default; - } + private Seria GetSeria(AuthorTodayBookDetails book) + { + if (!book.SeriesId.HasValue) return null; - return new Seria { + return new Seria + { Name = book.SeriesTitle.Trim(), Number = book.SeriesWorkNumber.HasValue ? book.SeriesWorkNumber.ToString() : string.Empty, Url = SystemUrl.MakeRelativeUri($"/work/series/{book.SeriesId}") }; } - private Task GetCover(AuthorTodayBookDetails book) { - return !string.IsNullOrWhiteSpace(book.CoverUrl) ? SaveImage(book.CoverUrl.AsUri()) : Task.FromResult(default(TempFile)); + private Task GetCover(AuthorTodayBookDetails book) + { + return !string.IsNullOrWhiteSpace(book.CoverUrl) + ? SaveImage(book.CoverUrl.AsUri()) + : Task.FromResult(null); } - protected override HttpRequestMessage GetImageRequestMessage(Uri uri) { - if (uri.IsSameHost(SystemUrl) || uri.IsSameSubDomain(SystemUrl)) { + protected override HttpRequestMessage GetImageRequestMessage(Uri uri) + { + if (uri.IsSameHost(SystemUrl) || uri.IsSameSubDomain(SystemUrl)) return GetDefaultMessage(SiteUrl.MakeRelativeUri(uri.AbsolutePath), uri); - } return base.GetImageRequestMessage(uri); } - private async Task> FillChapters(AuthorTodayBookDetails book) { + private async Task> FillChapters(AuthorTodayBookDetails book) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } - - foreach (var atChapter in await GetChapters(book)) { + if (Config.Options.NoChapters) return result; + + foreach (var atChapter in await GetChapters(book)) + { var title = atChapter.Title.ReplaceNewLine(); - - var chapter = new Chapter { + var prefix = _singleFetchUsed ? "sf " : string.Empty; + Config.Logger.LogInformation($"{prefix}Загружаю главу {title.CoverQuotes()}"); + + var chapter = new Chapter + { Title = title }; - if (atChapter.IsSuccessful) { + if (atChapter.IsSuccessful) + { var chapterDoc = atChapter.Decode(UserId, AT_CERT).AsHtmlDoc(); chapter.Images = await GetImages(chapterDoc, SystemUrl); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -234,27 +256,46 @@ private async Task> FillChapters(AuthorTodayBookDetails boo result.Add(chapter); } - + return result; } - private async Task> GetChapters(AuthorTodayBookDetails book) { + private async Task> GetChapters(AuthorTodayBookDetails book) + { var result = new List(); - - foreach (var chapter in book.Chapters.Where(c => !c.IsDraft && c.isAvailable).OrderBy(c => c.SortOrder)) { - var uri = ApiUrl.MakeRelativeUri($"/v1/work/{book.Id}/chapter/{chapter.Id}/text"); - Config.Logger.LogInformation($"Загружаю главу {chapter.Title.CoverQuotes()}"); - var response = await Config.Client.SendWithTriesAsync(() => GetDefaultMessage(uri, _apiUrl)); - var rchapter = await response.Content.ReadFromJsonAsync(); - if (rchapter != default && rchapter.Text != "" ) { - Config.Logger.LogInformation($"Загружена глава {rchapter.Title.CoverQuotes()}"); - chapter.Text = rchapter.Text; - chapter.Key = rchapter.Key; - chapter.IsSuccessful = true; - result.Add(chapter); + + var fetchMode = Config.GetSetting("AT:FetchMode", "Batch"); + if (fetchMode.Equals("Single", StringComparison.OrdinalIgnoreCase)) + { + _singleFetchUsed = true; + foreach (var chapter in book.Chapters.Where(c => !c.IsDraft && c.isAvailable).OrderBy(c => c.SortOrder)) + { + var uri = ApiUrl.MakeRelativeUri($"/v1/work/{book.Id}/chapter/{chapter.Id}/text"); + Config.Logger.LogInformation($"Загружаю главу {chapter.Title.CoverQuotes()}"); + var response = await Config.Client.SendWithTriesAsync(() => GetDefaultMessage(uri, _apiUrl)); + var rchapter = await response.Content.ReadFromJsonAsync(); + if (rchapter != null && rchapter.Text != "") + { + Config.Logger.LogInformation($"Загружена глава {rchapter.Title.CoverQuotes()}"); + chapter.Text = rchapter.Text; + chapter.Key = rchapter.Key; + chapter.IsSuccessful = true; + result.Add(chapter); + } + } + } + else + { + foreach (var chunk in book.Chapters.Where(c => !c.IsDraft).OrderBy(c => c.SortOrder).Chunk(100)) + { + var ids = string.Join("&", chunk.Select((c, i) => $"ids[{i}]={c.Id}")); + var uri = ApiUrl.MakeRelativeUri($"/v1/work/{book.Id}/chapter/many-texts?{ids}"); + var response = await Config.Client.SendWithTriesAsync(() => GetDefaultMessage(uri, _apiUrl)); + var chapters = await response.Content.ReadFromJsonAsync(); + if (chapters != null) result.AddRange(chapters.Where(c => c.Code != "NotFound")); } } return SliceToc(result, c => c.Title); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/BookInBookGetter.cs b/Core/Logic/Getters/BookInBookGetter.cs index a12a0ed2..3185135a 100644 --- a/Core/Logic/Getters/BookInBookGetter.cs +++ b/Core/Logic/Getters/BookInBookGetter.cs @@ -12,18 +12,24 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class BookInBookGetter(BookGetterConfig config) : GetterBase(config) { +public class BookInBookGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://bookinbook.ru/"); - protected override string GetId(Uri url) => url.GetQueryParameter("id"); + protected override string GetId(Uri url) + { + return url.GetQueryParameter("id"); + } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { url = SystemUrl.MakeRelativeUri($"book?id={GetId(url)}"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - - var book = new Book(url) { + + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, url), Title = doc.GetTextBySelector("span.main-info__name"), @@ -31,20 +37,20 @@ public override async Task Get(Uri url) { Annotation = doc.QuerySelector("div.annotation-form__text")?.InnerHtml, Seria = GetSeria(doc) }; - + return book; } - - private async Task> FillChapters(HtmlDocument doc, Uri uri) { + + private async Task> FillChapters(HtmlDocument doc, Uri uri) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - foreach (var bookChapter in GetToc(doc, uri)) { + foreach (var bookChapter in GetToc(doc, uri)) + { var chapter = new Chapter(); Config.Logger.LogInformation($"Загружаю главу {bookChapter.Title.CoverQuotes()}"); - + var chapterDoc = await GetChapter(bookChapter.Url); chapter.Title = bookChapter.Title; @@ -56,59 +62,64 @@ private async Task> FillChapters(HtmlDocument doc, Uri uri) return result; } - - private async Task GetChapter(Uri url) { + + private async Task GetChapter(Uri url) + { var sb = new StringBuilder(); - - for (var i = 1; ; i++) { - var uri = SystemUrl.MakeRelativeUri($"read?id={url.GetQueryParameter("id")}&chapter={url.GetQueryParameter("chapter")}&page={i}"); + + for (var i = 1;; i++) + { + var uri = SystemUrl.MakeRelativeUri( + $"read?id={url.GetQueryParameter("id")}&chapter={url.GetQueryParameter("chapter")}&page={i}"); using var response = await Config.Client.GetWithTriesAsync(uri, TimeSpan.FromMilliseconds(100)); - if (response == default) { - return sb.AsHtmlDoc(); - } + if (response is null) return sb.AsHtmlDoc(); var doc = await response.Content.ReadAsStreamAsync().ContinueWith(t => t.Result.AsHtmlDoc()); var text = doc.GetTextBySelector("#PAGE_TEXT").Deserialize().HtmlDecode(); using var sr = new StringReader(text); - - while (true) { + + while (true) + { var line = await sr.ReadLineAsync(); - if (line == null) { - break; - } - - if (string.IsNullOrWhiteSpace(line)) { - continue; - } - + if (line == null) break; + + if (string.IsNullOrWhiteSpace(line)) continue; + sb.Append(line.CoverTag("p")); } } } - private IEnumerable GetToc(HtmlDocument doc, Uri url) { - var urlChapters = doc.QuerySelectorAll("a.chapters-form__chapter").Select(a => new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.GetText().ReplaceNewLine())).ToList(); + private IEnumerable GetToc(HtmlDocument doc, Uri url) + { + var urlChapters = doc.QuerySelectorAll("a.chapters-form__chapter").Select(a => + new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.GetText().ReplaceNewLine())).ToList(); return SliceToc(urlChapters, c => c.Title); } - - private static Seria GetSeria(HtmlDocument doc) { + + private static Seria GetSeria(HtmlDocument doc) + { var a = doc.QuerySelector("span.series-info__name"); - if (a != default) { - return new Seria { + if (a is not null) + return new Seria + { Name = a.GetText() }; - } return default; } - - private Task GetCover(HtmlDocument doc, Uri uri) { + + private Task GetCover(HtmlDocument doc, Uri uri) + { var imagePath = doc.QuerySelector("img.book-cover__image")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(uri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } - - private static Author GetAuthor(HtmlDocument doc, Uri uri) { + + private static Author GetAuthor(HtmlDocument doc, Uri uri) + { var a = doc.QuerySelector("a.author"); return new Author(a.GetText(), uri.MakeRelativeUri(a.Attributes["href"].Value)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/BookRiverGetter.cs b/Core/Logic/Getters/BookRiverGetter.cs index dbe1b8af..bf4e3180 100644 --- a/Core/Logic/Getters/BookRiverGetter.cs +++ b/Core/Logic/Getters/BookRiverGetter.cs @@ -7,6 +7,7 @@ using System.Text.Json; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.Bookriver; @@ -15,23 +16,26 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class BookriverGetter(BookGetterConfig config) : GetterBase(config) { +public class BookriverGetter(BookGetterConfig config) : GetterBase(config) +{ + private string _token; protected override Uri SystemUrl => new("https://bookriver.ru/"); private Uri _apiUrl => new($"https://api.{SystemUrl.Host}/"); - private string _token; - - protected override string GetId(Uri url) => url.GetSegment(2); + protected override string GetId(Uri url) + { + return url.GetSegment(2); + } - public override async Task Authorize() { - if (!Config.HasCredentials) { - return; - } + public override async Task Authorize() + { + if (!Config.HasCredentials) return; - var payload = new { + var payload = new + { email = Config.Options.Login, password = Config.Options.Password, rememberMe = 1 @@ -39,21 +43,26 @@ public override async Task Authorize() { using var post = await Config.Client.PostAsJsonAsync(_apiUrl.MakeRelativeUri("/api/v1/auth/login"), payload); var data = await post.Content.ReadFromJsonAsync(); - if (!string.IsNullOrWhiteSpace(data.Token)) { + if (!string.IsNullOrWhiteSpace(data.Token)) + { Config.Logger.LogInformation("Успешно авторизовались"); _token = data.Token; Config.CookieContainer.Add(new Cookie("authToken", _token, "/", SystemUrl.Host)); - } else { - throw new Exception("Не удалось авторизоваться"); + } + else + { + throw new Elib2EbookAuthException("Не удалось авторизоваться"); } } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { var bookId = GetId(url); url = SystemUrl.MakeRelativeUri($"/book/{bookId}"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(url, bookId), Title = doc.GetTextBySelector("h1[itemprop=name]"), @@ -61,79 +70,80 @@ public override async Task Get(Uri url) { Annotation = doc.QuerySelector("span[itemprop=description]")?.InnerHtml, Seria = GetSeria(doc, url) }; - + return book; } - private static Author GetAuthor(HtmlDocument doc, Uri url) { + private static Author GetAuthor(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("span[itemprop=author] a"); return new Author(a.GetText(), url.MakeRelativeUri(a.Attributes["href"].Value)); } - private static Seria GetSeria(HtmlDocument doc, Uri url) { + private static Seria GetSeria(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("a[href^=/series/]"); - if (a == default) { - return default; - } + if (a is null) return default; var span = a.QuerySelector("+ span"); - if (span == default) { - return default; - } + if (span is null) return default; - return new Seria { + return new Seria + { Name = a.GetText()[5..].Trim(), Number = span.GetText().Trim().Trim('#'), Url = url.MakeRelativeUri(a.Attributes["href"].Value) }; } - private async Task> FillChapters(Uri uri, string bookId) { + private async Task> FillChapters(Uri uri, string bookId) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } - + if (Config.Options.NoChapters) return result; + var internalId = await GetInternalBookId(bookId); - - foreach (var bookChapter in await GetToc(internalId)) { + + foreach (var bookChapter in await GetToc(internalId)) + { Config.Logger.LogInformation($"Загружаю главу {bookChapter.Name.CoverQuotes()}"); - var chapter = new Chapter { + var chapter = new Chapter + { Title = bookChapter.Name }; - + var doc = await GetChapter(bookChapter.Id); - if (doc != default) { + if (doc is not null) + { chapter.Images = await GetImages(doc, uri); chapter.Content = doc.DocumentNode.InnerHtml; } - + result.Add(chapter); } return result; } - - protected virtual HttpRequestMessage GetMessage(Uri uri) { + + protected virtual HttpRequestMessage GetMessage(Uri uri) + { var message = new HttpRequestMessage(HttpMethod.Get, uri); message.Version = Config.Client.DefaultRequestVersion; - if (!string.IsNullOrWhiteSpace(_token)) { + if (!string.IsNullOrWhiteSpace(_token)) message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token); - } - foreach (var header in Config.Client.DefaultRequestHeaders) { - message.Headers.Add(header.Key, header.Value); - } - + foreach (var header in Config.Client.DefaultRequestHeaders) message.Headers.Add(header.Key, header.Value); + return message; } - private async Task GetInternalBookId(string bookId) { + private async Task GetInternalBookId(string bookId) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri($"/reader/{bookId}")); return GetNextData(doc, "book").Book.Id.ToString(); } - private static T GetNextData(HtmlDocument doc, string node) { + private static T GetNextData(HtmlDocument doc, string node) + { var json = doc.QuerySelector("#__NEXT_DATA__").InnerText; return JsonDocument.Parse(json) @@ -146,24 +156,31 @@ private static T GetNextData(HtmlDocument doc, string node) { .Deserialize(); } - private async Task GetChapter(long bookChapterId) { - var response = await Config.Client.SendAsync(GetMessage(_apiUrl.MakeRelativeUri($"/api/v1/books/chapter/text/{bookChapterId}"))); - if (response.StatusCode == HttpStatusCode.Forbidden) { - return default; - } - + private async Task GetChapter(long bookChapterId) + { + var response = + await Config.Client.SendAsync( + GetMessage(_apiUrl.MakeRelativeUri($"/api/v1/books/chapter/text/{bookChapterId}"))); + if (response.StatusCode == HttpStatusCode.Forbidden) return default; + var content = await response.Content.ReadAsStringAsync(); return content.Deserialize>().Data.Content.AsHtmlDoc(); } - private async Task> GetToc(string bookId) { - var response = await Config.Client.GetWithTriesAsync(_apiUrl.MakeRelativeUri($"/api/v1/books/chapters/text/published?bookId={bookId}")); + private async Task> GetToc(string bookId) + { + var response = + await Config.Client.GetWithTriesAsync( + _apiUrl.MakeRelativeUri($"/api/v1/books/chapters/text/published?bookId={bookId}")); var content = await response.Content.ReadAsStringAsync(); return SliceToc(content.Deserialize>().Data, c => c.Name); } - private Task GetCover(HtmlDocument doc, Uri uri) { + private Task GetCover(HtmlDocument doc, Uri uri) + { var imagePath = doc.QuerySelector("img[itemprop=contentUrl]")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(uri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/BookinistGetter.cs b/Core/Logic/Getters/BookinistGetter.cs index 77592651..80944538 100644 --- a/Core/Logic/Getters/BookinistGetter.cs +++ b/Core/Logic/Getters/BookinistGetter.cs @@ -10,36 +10,41 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class BookinistGetter(BookGetterConfig config) : GetterBase(config) { +public class BookinistGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://bookinist.pw/"); - public override async Task Get(Uri url) { + + public override async Task Get(Uri url) + { url = SystemUrl.MakeRelativeUri($"/book/{GetId(url)}"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, url), Title = doc.GetTextBySelector("title"), Author = new Author("Bookinist") }; - + return book; } - private async Task> FillChapters(HtmlDocument doc, Uri url) { + private async Task> FillChapters(HtmlDocument doc, Uri url) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - foreach (var bookChapter in GetToc(doc, url)) { + foreach (var bookChapter in GetToc(doc, url)) + { Config.Logger.LogInformation($"Загружаю главу {bookChapter.Title.CoverQuotes()}"); - var chapter = new Chapter { + var chapter = new Chapter + { Title = bookChapter.Title }; - + var chapterDoc = await GetChapter(bookChapter); chapter.Images = await GetImages(chapterDoc, url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -49,19 +54,25 @@ private async Task> FillChapters(HtmlDocument doc, Uri url) return result; } - - private Task GetCover(HtmlDocument doc, Uri uri) { + + private Task GetCover(HtmlDocument doc, Uri uri) + { var imagePath = doc.QuerySelector("meta[property='og:image']")?.Attributes["content"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(uri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } - private Task GetChapter(UrlChapter bookChapter) { + private Task GetChapter(UrlChapter bookChapter) + { return Config.Client.GetHtmlDocWithTriesAsync(bookChapter.Url).ContinueWith(t => t.Result.RemoveNodes("h2")); } - private IEnumerable GetToc(HtmlDocument doc, Uri url) { + private IEnumerable GetToc(HtmlDocument doc, Uri url) + { var bookId = GetId(url); - var result = doc.QuerySelectorAll("ul.menu-toc li a").Skip(1).Select((a, i) => new UrlChapter(SystemUrl.MakeRelativeUri($"/bookpage/{bookId}/{i + 1}"), a.GetText())).ToList(); + var result = doc.QuerySelectorAll("ul.menu-toc li a").Skip(1).Select((a, i) => + new UrlChapter(SystemUrl.MakeRelativeUri($"/bookpage/{bookId}/{i + 1}"), a.GetText())).ToList(); return SliceToc(result, c => c.Title); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/BooksYandex/BooksYandexBooksGetter.cs b/Core/Logic/Getters/BooksYandex/BooksYandexBooksGetter.cs index 52f55fb5..ef44b7c2 100644 --- a/Core/Logic/Getters/BooksYandex/BooksYandexBooksGetter.cs +++ b/Core/Logic/Getters/BooksYandex/BooksYandexBooksGetter.cs @@ -6,6 +6,7 @@ using System.Net.Http.Json; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Misc; using Core.Types.Book; @@ -15,31 +16,30 @@ namespace Core.Logic.Getters.BooksYandex; -public class BooksYandexBooksGetter(BookGetterConfig config) : BooksYandexGetterBase(config) { +public class BooksYandexBooksGetter(BookGetterConfig config) : BooksYandexGetterBase(config) +{ protected override string[] Paths => ["books", "audiobooks", "serials"]; - protected override async Task> FillChapters(Book book, BooksYandexResponse response) { - if (Config.Options.HasAdditionalType(AdditionalTypeEnum.Audio)) { + protected override async Task> FillChapters(Book book, BooksYandexResponse response) + { + if (Config.Options.HasAdditionalType(AdditionalTypeEnum.Audio)) book.AdditionalFiles.Add(AdditionalTypeEnum.Audio, await GetAudio(response)); - } var id = response.Book?.UUID ?? response.AudioBook?.LinkedBooks?.FirstOrDefault(); - if (string.IsNullOrWhiteSpace(id)) { - return []; - } - - if (response.Book?.SourceType == "serial") { - return await GetFromSerial(book, id); - } - + if (string.IsNullOrWhiteSpace(id)) return []; + + if (response.Book?.SourceType == "serial") return await GetFromSerial(book, id); + var requestUri = $"https://api.bookmate.ru/api/v5/books/{id}/content/v4".AsUri(); var epubResponse = await Config.Client.GetAsync(requestUri); - if (epubResponse.StatusCode == HttpStatusCode.OK) { - var epubFile = await TempFile.Create(requestUri, Config.TempFolder.Path, epubResponse.Content.Headers.ContentDisposition.FileName.Trim('\"'), await epubResponse.Content.ReadAsStreamAsync()); + if (epubResponse.StatusCode == HttpStatusCode.OK) + { + var epubFile = await TempFile.Create(requestUri, Config.TempFolder.Path, + epubResponse.Content.Headers.ContentDisposition.FileName.Trim('\"'), + await epubResponse.Content.ReadAsStreamAsync()); - if (epubFile != default && Config.Options.HasAdditionalType(AdditionalTypeEnum.Books)) { + if (epubFile is not null && Config.Options.HasAdditionalType(AdditionalTypeEnum.Books)) book.AdditionalFiles.Add(AdditionalTypeEnum.Books, epubFile); - } return await FillChaptersFromEpub(epubFile); } @@ -47,98 +47,106 @@ protected override async Task> FillChapters(Book book, Book return []; } - private async Task> GetFromSerial(Book book, string id) { + private async Task> GetFromSerial(Book book, string id) + { var episodes = await GetEpisodes(id); var result = new List(); - - foreach (var episode in episodes.Where(e => e.CanBeRead)) { + + foreach (var episode in episodes.Where(e => e.CanBeRead)) + { Config.Logger.LogInformation($"Загружаю эпизод {episode.Title.CoverQuotes()}"); - + var requestUri = $"https://api.bookmate.ru/api/v5/books/{episode.Uuid}/content/v4".AsUri(); var epubResponse = await Config.Client.GetAsync(requestUri); - if (epubResponse.StatusCode != HttpStatusCode.OK) { - continue; - } + if (epubResponse.StatusCode != HttpStatusCode.OK) continue; - var epubFile = await TempFile.Create(requestUri, Config.TempFolder.Path, epubResponse.Content.Headers.ContentDisposition.FileName.Trim('\"'), await epubResponse.Content.ReadAsStreamAsync()); - - if (epubFile != default && Config.Options.HasAdditionalType(AdditionalTypeEnum.Books)) { + var epubFile = await TempFile.Create(requestUri, Config.TempFolder.Path, + epubResponse.Content.Headers.ContentDisposition.FileName.Trim('\"'), + await epubResponse.Content.ReadAsStreamAsync()); + + if (epubFile is not null && Config.Options.HasAdditionalType(AdditionalTypeEnum.Books)) book.AdditionalFiles.Add(AdditionalTypeEnum.Books, epubFile); - } var episodeChapters = await FillChaptersFromEpub(epubFile); - - var chapter = new Chapter { + + var chapter = new Chapter + { Title = episode.Title, Content = string.Join(Environment.NewLine, episodeChapters.Select(c => c.Content)), Images = episodeChapters.SelectMany(c => c.Images) }; - + result.Add(chapter); } return result; } - - private async Task> GetEpisodes(string id) { - try { + + private async Task> GetEpisodes(string id) + { + try + { var result = new List(); - for (var i = 1;; i++) { - var response = await Config.Client.GetFromJsonAsync($"https://api.bookmate.ru/api/v5/books/{id}/episodes".AsUri().AppendQueryParameter("page", i)); - if (response.Episodes.Length == 0) { - break; - } - + for (var i = 1;; i++) + { + var response = await Config.Client.GetFromJsonAsync( + $"https://api.bookmate.ru/api/v5/books/{id}/episodes".AsUri().AppendQueryParameter("page", i)); + if (response.Episodes.Length == 0) break; + result.AddRange(response.Episodes); } - + return result; - } catch (HttpRequestException ex) { - if (ex.StatusCode == HttpStatusCode.Unauthorized) { - throw new Exception("Авторизационный токен невалиден. Требуется обновление"); - } + } + catch (HttpRequestException ex) + { + if (ex.StatusCode == HttpStatusCode.Unauthorized) + throw new Elib2EbookAuthException("Авторизационный токен невалиден. Требуется обновление"); throw; } } - - private async Task> GetAudio(BooksYandexResponse bookResponse) { + + private async Task> GetAudio(BooksYandexResponse bookResponse) + { var result = new List(); var id = bookResponse.AudioBook?.UUID ?? bookResponse.Book?.LinkedAudio?.FirstOrDefault(); - if (string.IsNullOrWhiteSpace(id)) { - return result; - } - + if (string.IsNullOrWhiteSpace(id)) return result; + var playlist = await GetPlayList(id); - - if (playlist.Tracks.Length == 0) { - return result; - } - for (var i = 0; i < playlist.Tracks.Length; i++) { + if (playlist.Tracks.Length == 0) return result; + + for (var i = 0; i < playlist.Tracks.Length; i++) + { var track = playlist.Tracks[i]; var url = track.Offline.Max.Url.Replace(".m3u8", ".m4a"); Config.Logger.LogInformation($"Загружаю аудиоверсию {i + 1}/{playlist.Tracks.Length} {url}"); var response = await Config.Client.GetWithTriesAsync(url.AsUri()); - result.Add(await TempFile.Create(url.AsUri(), Config.TempFolder.Path, $"{i}_{url.AsUri().GetFileName()}", await response.Content.ReadAsStreamAsync())); + result.Add(await TempFile.Create(url.AsUri(), Config.TempFolder.Path, $"{i}_{url.AsUri().GetFileName()}", + await response.Content.ReadAsStreamAsync())); Config.Logger.LogInformation($"Аудиоверсия {i + 1}/{playlist.Tracks.Length} {url} загружена"); } return result; } - - private async Task GetPlayList(string id) { - try { - return await Config.Client.GetFromJsonAsync($"https://api.bookmate.ru/api/v5/audiobooks/{id}/playlists.json".AsUri()); - } catch (HttpRequestException ex) { - if (ex.StatusCode == HttpStatusCode.Unauthorized) { - throw new Exception("Авторизационный токен невалиден. Требуется обновление"); - } + + private async Task GetPlayList(string id) + { + try + { + return await Config.Client.GetFromJsonAsync( + $"https://api.bookmate.ru/api/v5/audiobooks/{id}/playlists.json".AsUri()); + } + catch (HttpRequestException ex) + { + if (ex.StatusCode == HttpStatusCode.Unauthorized) + throw new Elib2EbookAuthException("Авторизационный токен невалиден. Требуется обновление"); throw; } } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/BooksYandex/BooksYandexComicsGetter.cs b/Core/Logic/Getters/BooksYandex/BooksYandexComicsGetter.cs index 50a1b151..d011eb41 100644 --- a/Core/Logic/Getters/BooksYandex/BooksYandexComicsGetter.cs +++ b/Core/Logic/Getters/BooksYandex/BooksYandexComicsGetter.cs @@ -9,24 +9,24 @@ namespace Core.Logic.Getters.BooksYandex; -public class BooksYandexComicsGetter(BookGetterConfig config) : BooksYandexGetterBase(config) { +public class BooksYandexComicsGetter(BookGetterConfig config) : BooksYandexGetterBase(config) +{ protected override string[] Paths => ["comicbooks"]; - protected override async Task> FillChapters(Book book, BooksYandexResponse response) { - if (Config.Options.NoChapters || string.IsNullOrWhiteSpace(response.Comicbook?.UUID)) { - return []; - } - - var metadata = await Config.Client.GetFromJsonAsync($"https://api.bookmate.ru/api/v5/comicbooks/{response.Comicbook.UUID}/metadata.json"); - + protected override async Task> FillChapters(Book book, BooksYandexResponse response) + { + if (Config.Options.NoChapters || string.IsNullOrWhiteSpace(response.Comicbook?.UUID)) return []; + + var metadata = await Config.Client.GetFromJsonAsync( + $"https://api.bookmate.ru/api/v5/comicbooks/{response.Comicbook.UUID}/metadata.json"); + var sb = new StringBuilder(); - - foreach (var page in metadata.Pages) { - sb.Append($""); - } + + foreach (var page in metadata.Pages) sb.Append($""); var doc = sb.AsHtmlDoc(); - var chapter = new Chapter { + var chapter = new Chapter + { Images = await GetImages(doc, SystemUrl), Content = doc.DocumentNode.InnerHtml, Title = response.Comicbook.Title @@ -34,4 +34,4 @@ protected override async Task> FillChapters(Book book, Book return [chapter]; } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/BooksYandex/BooksYandexGetterBase.cs b/Core/Logic/Getters/BooksYandex/BooksYandexGetterBase.cs index e741ac3d..38b9d130 100644 --- a/Core/Logic/Getters/BooksYandex/BooksYandexGetterBase.cs +++ b/Core/Logic/Getters/BooksYandex/BooksYandexGetterBase.cs @@ -6,6 +6,7 @@ using System.Net.Http.Json; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.BookYandex; @@ -13,88 +14,104 @@ namespace Core.Logic.Getters.BooksYandex; -public abstract class BooksYandexGetterBase(BookGetterConfig config) : GetterBase(config) { +public abstract class BooksYandexGetterBase(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://books.yandex.ru/"); - + protected abstract string[] Paths { get; } - - protected override string GetId(Uri url) { + + protected override string GetId(Uri url) + { return url.GetSegment(2); } - public override bool IsSameUrl(Uri url) { - return base.IsSameUrl(url) && Paths.Any(path => string.Equals(url.GetSegment(1), path, StringComparison.InvariantCultureIgnoreCase)); + public override bool IsSameUrl(Uri url) + { + return base.IsSameUrl(url) && Paths.Any(path => + string.Equals(url.GetSegment(1), path, StringComparison.InvariantCultureIgnoreCase)); } - public override Task Authorize() { + public override Task Authorize() + { var token = Config.Options.Login ?? Config.Options.Password; - if (string.IsNullOrWhiteSpace(token)) { - throw new Exception("Не указан авторизационный token."); - } - + if (string.IsNullOrWhiteSpace(token)) throw new Elib2EbookAuthException("Не указан авторизационный token."); + Config.Client.DefaultRequestHeaders.Add("auth-token", token); return Task.CompletedTask; } - - public override async Task Get(Uri url) { + + public override async Task Get(Uri url) + { var id = GetId(url); var path = url.GetSegment(1); url = SystemUrl.MakeRelativeUri($"/{path}/{id}"); - + var bookResponse = await GetBookResponse(path, id); - var details = (BookmateBookBase)bookResponse.Book ?? (BookmateBookBase)bookResponse.AudioBook ?? bookResponse.Comicbook; - - var book = new Book(url) { + var details = (BookmateBookBase)bookResponse.Book ?? + (BookmateBookBase)bookResponse.AudioBook ?? bookResponse.Comicbook; + + var book = new Book(url) + { Cover = await GetCover(details), Title = details.Title, Author = GetAuthor(details), Annotation = details.Annotation, - Lang = details.Language, + Lang = details.Language }; book.Chapters = await FillChapters(book, bookResponse); - + return book; } - protected virtual Task> FillChapters(Book book, BooksYandexResponse response) { + protected virtual Task> FillChapters(Book book, BooksYandexResponse response) + { return Task.FromResult>([]); } - - private async Task GetBookResponse(string path, string id) { - try { + + private async Task GetBookResponse(string path, string id) + { + try + { var response = await Config.Client.GetAsync($"https://api.bookmate.ru/api/v5/{path}/{id}".AsUri()); - if (response.StatusCode != HttpStatusCode.OK) { + if (response.StatusCode != HttpStatusCode.OK) response = await Config.Client.GetAsync($"https://api.bookmate.ru/api/v5/books/{id}".AsUri()); - } var booksYandexResponse = await response.Content.ReadFromJsonAsync(); - - if (booksYandexResponse.AudioBook?.LinkedBooks?.Length > 0) { - var linkedResponse = await Config.Client.GetAsync($"https://api.bookmate.ru/api/v5/books/{booksYandexResponse.AudioBook.LinkedBooks[0]}".AsUri()); - if (linkedResponse.StatusCode == HttpStatusCode.OK) { + + if (booksYandexResponse.AudioBook?.LinkedBooks?.Length > 0) + { + var linkedResponse = await Config.Client.GetAsync( + $"https://api.bookmate.ru/api/v5/books/{booksYandexResponse.AudioBook.LinkedBooks[0]}".AsUri()); + if (linkedResponse.StatusCode == HttpStatusCode.OK) return await linkedResponse.Content.ReadFromJsonAsync(); - } } return booksYandexResponse; - } catch (HttpRequestException ex) { - if (ex.StatusCode == HttpStatusCode.Unauthorized) { - throw new Exception("Авторизационный токен невалиден. Требуется обновление"); - } + } + catch (HttpRequestException ex) + { + if (ex.StatusCode == HttpStatusCode.Unauthorized) + throw new Elib2EbookAuthException("Авторизационный токен невалиден. Требуется обновление"); throw; } } - - private Task GetCover(BookmateBookBase book) { + + private Task GetCover(BookmateBookBase book) + { var url = book.Cover.Large ?? book.Cover.Small; - return !string.IsNullOrWhiteSpace(url) ? SaveImage(SystemUrl.MakeRelativeUri(url)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(url) + ? SaveImage(SystemUrl.MakeRelativeUri(url)) + : Task.FromResult(default(TempFile)); } - - private Author GetAuthor(BookmateBookBase book) { + + private Author GetAuthor(BookmateBookBase book) + { var author = book.GetAuthor(); - return author == default ? new Author("BooksYandex") : new Author(author.Name, SystemUrl.MakeRelativeUri($"/authors/{author.Uuid}")); + return author is null + ? new Author("BooksYandex") + : new Author(author.Name, SystemUrl.MakeRelativeUri($"/authors/{author.Uuid}")); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/BookstabGetter.cs b/Core/Logic/Getters/BookstabGetter.cs index 796b1611..4d42a2d2 100644 --- a/Core/Logic/Getters/BookstabGetter.cs +++ b/Core/Logic/Getters/BookstabGetter.cs @@ -12,72 +12,88 @@ using HtmlAgilityPack; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class BookstabGetter(BookGetterConfig config) : GetterBase(config) { +public class BookstabGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://bookstab.ru/"); private Uri _apiUrl => new($"https://api.{SystemUrl.Host}/"); - protected override string GetId(Uri url) => url.GetSegment(2); + protected override string GetId(Uri url) + { + return url.GetSegment(2); + } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { var bookId = GetId(url); url = SystemUrl.MakeRelativeUri($"/book/{bookId}"); - using var response = await Config.Client.GetWithTriesAsync(_apiUrl.MakeRelativeUri($"/api/reader-get/{bookId}")); + using var response = + await Config.Client.GetWithTriesAsync(_apiUrl.MakeRelativeUri($"/api/reader-get/{bookId}")); var data = await response.Content.ReadFromJsonAsync(); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(data), Chapters = await FillChapters(data, url, bookId), Title = data?.Book.Title, Author = GetAuthor(data), Annotation = GetAnnotation(data?.Book) }; - + return book; } - private Author GetAuthor(BookstabApiResponse book) { + private Author GetAuthor(BookstabApiResponse book) + { return new Author(book.Book.User.Pseudonym, SystemUrl.MakeRelativeUri($"/user/{book.Book.User.Name}")); } - - private static string GetAnnotation(BookstabBook book) { - return string.IsNullOrWhiteSpace(book.Excerpt) ? - string.Empty : - string.Join("", book.Excerpt.Split("\n", StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim().CoverTag("p"))); + + private static string GetAnnotation(BookstabBook book) + { + return string.IsNullOrWhiteSpace(book.Excerpt) + ? string.Empty + : string.Join("", + book.Excerpt.Split("\n", StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim().CoverTag("p"))); } - private async Task> FillChapters(BookstabApiResponse response, Uri uri, string bookId) { + private async Task> FillChapters(BookstabApiResponse response, Uri uri, string bookId) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - foreach (var bookChapter in SliceToc(response.Book.ChaptersShow, c => c.Title)) { - var chapter = new Chapter { + foreach (var bookChapter in SliceToc(response.Book.ChaptersShow, c => c.Title)) + { + var chapter = new Chapter + { Title = bookChapter.Title }; Config.Logger.LogInformation($"Загружаю главу {bookChapter.Title.CoverQuotes()}"); - + var doc = await GetChapter(bookChapter.Id, bookId); - if (doc != default) { + if (doc is not null) + { chapter.Images = await GetImages(doc, uri); chapter.Content = doc.DocumentNode.InnerHtml; } - + result.Add(chapter); } return result; } - private async Task GetChapter(int chapterId, string bookId) { - while (true) { - using var response = await Config.Client.GetAsync(_apiUrl.MakeRelativeUri($"/api/reader-get/{bookId}/{chapterId}")); - if (response.StatusCode == HttpStatusCode.Forbidden) { + private async Task GetChapter(int chapterId, string bookId) + { + while (true) + { + using var response = + await Config.Client.GetAsync(_apiUrl.MakeRelativeUri($"/api/reader-get/{bookId}/{chapterId}")); + if (response.StatusCode == HttpStatusCode.Forbidden) + { Config.Logger.LogInformation("Очень много запросов. Подождем..."); await Task.Delay(TimeSpan.FromSeconds(10)); continue; @@ -88,8 +104,11 @@ private async Task GetChapter(int chapterId, string bookId) { } } - private Task GetCover(BookstabApiResponse response) { + private Task GetCover(BookstabApiResponse response) + { var imagePath = response.Book.Image; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(_apiUrl.MakeRelativeUri($"/storage/{imagePath}")) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(_apiUrl.MakeRelativeUri($"/storage/{imagePath}")) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/BookstimeGetter.cs b/Core/Logic/Getters/BookstimeGetter.cs index ff592d51..541cf787 100644 --- a/Core/Logic/Getters/BookstimeGetter.cs +++ b/Core/Logic/Getters/BookstimeGetter.cs @@ -7,6 +7,7 @@ using System.Text; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.Bookstime; @@ -15,93 +16,103 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class BookstimeGetter(BookGetterConfig config) : GetterBase(config) { +public class BookstimeGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://bookstime.ru/"); - protected override string GetId(Uri url) { + protected override string GetId(Uri url) + { return url.GetSegment(2); } - public override async Task Init() { + public override async Task Init() + { await base.Init(); Config.Client.DefaultRequestHeaders.Add("X-OCTOBER-REQUEST-FLASH", "1"); Config.Client.DefaultRequestHeaders.Add("X-OCTOBER-REQUEST-HANDLER", "bookAccount::onSignin"); Config.Client.DefaultRequestHeaders.Add("X-Requested-With", "XMLHttpRequest"); } - public override async Task Authorize() { - if (!Config.HasCredentials) { - return; - } - + public override async Task Authorize() + { + if (!Config.HasCredentials) return; + using var post = await Config.Client.PostAsync(SystemUrl, GenerateAuthData()); - if (post.StatusCode != HttpStatusCode.OK) { + if (post.StatusCode != HttpStatusCode.OK) + { var content = await post.Content.ReadAsStringAsync(); - throw new Exception($"Не удалось авторизоваться. {content}"); + throw new Elib2EbookAuthException($"Не удалось авторизоваться. {content}"); } - + var response = await post.Content.ReadFromJsonAsync(); - if (string.IsNullOrWhiteSpace(response.XOctoberErrorMessage)) { + if (string.IsNullOrWhiteSpace(response.XOctoberErrorMessage)) Config.Logger.LogInformation("Успешно авторизовались"); - } else { - throw new Exception($"Не удалось авторизоваться. {response.XOctoberErrorMessage}"); - } + else + throw new Elib2EbookAuthException($"Не удалось авторизоваться. {response.XOctoberErrorMessage}"); } - private FormUrlEncodedContent GenerateAuthData() { - var payload = new Dictionary { + private FormUrlEncodedContent GenerateAuthData() + { + var payload = new Dictionary + { { "email", Config.Options.Login }, - { "password", Config.Options.Password }, + { "password", Config.Options.Password } }; return new FormUrlEncodedContent(payload); } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { url = SystemUrl.MakeRelativeUri("/book-card/" + GetId(url)); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, url), Title = doc.GetTextBySelector("h1"), Author = GetAuthor(doc), Annotation = doc.QuerySelector("div[itemprop=description]")?.InnerHtml }; - + return book; } - private Author GetAuthor(HtmlDocument doc) { + private Author GetAuthor(HtmlDocument doc) + { var a = doc.QuerySelector("a.book-card-full__author"); return new Author(a.GetText(), SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)); } - - private IEnumerable GetToc(HtmlDocument doc, Uri url) { + + private IEnumerable GetToc(HtmlDocument doc, Uri url) + { var result = doc .QuerySelectorAll("div.book-card-full-description__content a") .Select(a => new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.GetText().ReplaceNewLine())) .ToList(); - + return SliceToc(result, c => c.Title); } - private async Task> FillChapters(HtmlDocument doc, Uri url) { + private async Task> FillChapters(HtmlDocument doc, Uri url) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - foreach (var urlChapter in GetToc(doc, url)) { + foreach (var urlChapter in GetToc(doc, url)) + { Config.Logger.LogInformation($"Загружаю главу {urlChapter.Title.CoverQuotes()}"); - var chapter = new Chapter { + var chapter = new Chapter + { Title = urlChapter.Title }; var chapterDoc = await GetChapter(urlChapter); - if (chapterDoc != default) { + if (chapterDoc is not null) + { chapter.Images = await GetImages(chapterDoc, urlChapter.Url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; } @@ -112,33 +123,36 @@ private async Task> FillChapters(HtmlDocument doc, Uri url) return result; } - private async Task GetChapter(UrlChapter urlChapter) { + private async Task GetChapter(UrlChapter urlChapter) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(urlChapter.Url); var reader = doc.QuerySelector("div.reader__body"); - if (reader == default) { - return default; - } - + if (reader is null) return default; + var text = new StringBuilder(); text.Append(reader.RemoveNodes("h2.ui-text-head--3-em, iframe").InnerHtml); - + var paging = doc .QuerySelectorAll("a.ui-pagination-item") .Where(a => int.TryParse(a.GetText(), out _)) .Select(a => int.Parse(a.GetText())) .ToList(); - + var pages = paging.Any() ? paging.Max() : 1; - for (var i = 2; i <= pages; i++) { + for (var i = 2; i <= pages; i++) + { doc = await Config.Client.GetHtmlDocWithTriesAsync(urlChapter.Url.AppendSegment(i.ToString())); text.Append(doc.QuerySelector("div.reader__body").RemoveNodes("h2.ui-text-head--3-em, iframe").InnerHtml); } - + return text.AsHtmlDoc(); } - private Task GetCover(HtmlDocument doc, Uri bookUri) { + private Task GetCover(HtmlDocument doc, Uri bookUri) + { var imagePath = doc.QuerySelector("div[itemprop=image] img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(bookUri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/BookurukGetter.cs b/Core/Logic/Getters/BookurukGetter.cs index 58047b6b..cb05b293 100644 --- a/Core/Logic/Getters/BookurukGetter.cs +++ b/Core/Logic/Getters/BookurukGetter.cs @@ -4,6 +4,7 @@ using System.Net.Http; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.Common; @@ -11,119 +12,133 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class BookurukGetter(BookGetterConfig config) : GetterBase(config) { +public class BookurukGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://bookuruk.com/"); - protected override string GetId(Uri url) { + protected override string GetId(Uri url) + { return url.GetSegment(2); } - public override async Task Authorize() { - if (!Config.HasCredentials) { - return; - } + public override async Task Authorize() + { + if (!Config.HasCredentials) return; var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl); var token = doc.QuerySelector("#loginpopup input[name=_csrf-shop]").Attributes["value"].Value; - - doc = await Config.Client.PostHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri("/login"), GenerateAuthData(token)); + + doc = await Config.Client.PostHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri("/login"), + GenerateAuthData(token)); var alert = doc.QuerySelector("#infoMess")?.GetText(); - if (string.IsNullOrWhiteSpace(alert)) { + if (string.IsNullOrWhiteSpace(alert)) Config.Logger.LogInformation("Успешно авторизовались"); - } else { - throw new Exception($"Не удалось авторизоваться. {alert}"); - } + else + throw new Elib2EbookAuthException($"Не удалось авторизоваться. {alert}"); } - private FormUrlEncodedContent GenerateAuthData(string token) { - var payload = new Dictionary { + private FormUrlEncodedContent GenerateAuthData(string token) + { + var payload = new Dictionary + { { "_csrf-shop", token }, { "email", Config.Options.Login }, - { "password", Config.Options.Password }, + { "password", Config.Options.Password } }; return new FormUrlEncodedContent(payload); } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { url = await GetMainUrl(url); url = SystemUrl.MakeRelativeUri($"/book/{GetId(url)}"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - - var book = new Book(url) { + + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, url), Title = doc.GetTextBySelector("h1"), Author = GetAuthor(doc), Annotation = doc.QuerySelector("div.single-prew__desc_anot")?.RemoveNodes(" > b")?.InnerHtml }; - + return book; } - private Author GetAuthor(HtmlDocument doc) { + private Author GetAuthor(HtmlDocument doc) + { var a = doc.QuerySelector("a.bookcard_author + a"); var href = a.Attributes["href"]?.Value; - return string.IsNullOrWhiteSpace(href) ? - new Author(a.GetText()) : - new Author(a.GetText(), SystemUrl.MakeRelativeUri(href)); + return string.IsNullOrWhiteSpace(href) + ? new Author(a.GetText()) + : new Author(a.GetText(), SystemUrl.MakeRelativeUri(href)); } - private async Task GetMainUrl(Uri url) { - if (url.GetSegment(1) == "book") { - return url; - } + private async Task GetMainUrl(Uri url) + { + if (url.GetSegment(1) == "book") return url; var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); return url.MakeRelativeUri(doc.QuerySelector("div.content.aside-right li a").Attributes["href"].Value); } - private async Task> GetToc(HtmlDocument doc, Uri url) { - var internalId = SystemUrl.MakeRelativeUri(doc.QuerySelectorAll("div.content.aside-right li a")[1].Attributes["href"].Value).GetSegment(2); + private async Task> GetToc(HtmlDocument doc, Uri url) + { + var internalId = SystemUrl + .MakeRelativeUri(doc.QuerySelectorAll("div.content.aside-right li a")[1].Attributes["href"].Value) + .GetSegment(2); doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri($"/book/chapters/{internalId}")); - + var result = doc .QuerySelectorAll("a.read-chapters-item") .Select(a => new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.GetText())) .ToList(); - + return SliceToc(result, c => c.Title); } - - private async Task> FillChapters(HtmlDocument doc, Uri uri) { + + private async Task> FillChapters(HtmlDocument doc, Uri uri) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - foreach (var urlChapter in await GetToc(doc, uri)) { - var chapter = new Chapter { + foreach (var urlChapter in await GetToc(doc, uri)) + { + var chapter = new Chapter + { Title = urlChapter.Title }; Config.Logger.LogInformation($"Загружаю главу {urlChapter.Title.CoverQuotes()}"); var chapterDoc = await GetChapter(urlChapter.Url); - if (chapterDoc != default) { + if (chapterDoc is not null) + { chapter.Images = await GetImages(chapterDoc, urlChapter.Url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; } - + result.Add(chapter); } return result; } - private async Task GetChapter(Uri url) { + private async Task GetChapter(Uri url) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); return doc.QuerySelector("div.read-area").InnerHtml.AsHtmlDoc(); } - private Task GetCover(HtmlDocument doc, Uri uri) { + private Task GetCover(HtmlDocument doc, Uri uri) + { var imagePath = doc.QuerySelector("div.single-prew__img img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(uri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/BoostyGetter.cs b/Core/Logic/Getters/BoostyGetter.cs index 57e3a479..473116b9 100644 --- a/Core/Logic/Getters/BoostyGetter.cs +++ b/Core/Logic/Getters/BoostyGetter.cs @@ -13,90 +13,100 @@ namespace Core.Logic.Getters; -public class BoostyGetter(BookGetterConfig config) : GetterBase(config) { +public class BoostyGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://boosty.to/"); private static Uri ApiUrl => new(" https://api.boosty.to/"); - protected override string GetId(Uri url) { + protected override string GetId(Uri url) + { return url.GetSegment(1); } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { var bookId = GetId(url); url = SystemUrl.MakeRelativeUri($"/{bookId}"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(url, bookId), Title = "Exclusive content on Boosty", Author = GetAuthor(doc, url), - Annotation = doc.QuerySelector("div.AboutAuthor_content_HprOc")?.InnerHtml, + Annotation = doc.QuerySelector("div.AboutAuthor_content_HprOc")?.InnerHtml }; - + return book; } - private static Author GetAuthor(HtmlDocument doc, Uri url) { + private static Author GetAuthor(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("h1"); return new Author(a.GetText(), url); } - private async Task> FillChapters(Uri uri, string bookId) { + private async Task> FillChapters(Uri uri, string bookId) + { var result = new List(); - - foreach (var post in await GetToc(bookId)) { + + foreach (var post in await GetToc(bookId)) + { Config.Logger.LogInformation($"Загружаю главу {post.Title.CoverQuotes()}"); - var chapter = new Chapter { + var chapter = new Chapter + { Title = post.Title }; - + var doc = await GetChapter(post, bookId); - if (doc != default) { + if (doc is not null) + { chapter.Images = await GetImages(doc, uri); chapter.Content = doc.DocumentNode.InnerHtml; } - + result.Add(chapter); } return result; } - private async Task GetChapter(BoostyPost post, string bookId) { - if (!post.HasAccess) { - return default; - } - + private async Task GetChapter(BoostyPost post, string bookId) + { + if (!post.HasAccess) return default; + var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri($"/{bookId}/posts/{post.Id}")); doc = doc.QuerySelector("article.Post_content_ETkyT").InnerHtml.AsHtmlDoc().RemoveNodes("h1"); - - foreach (var img in doc.QuerySelectorAll("img")) { - if (img.ParentNode.Name == "a") { + + foreach (var img in doc.QuerySelectorAll("img")) + if (img.ParentNode.Name == "a") + { img.ParentNode.Name = "div"; img.ParentNode.Attributes.RemoveAll(); } - } - foreach (var div in doc.QuerySelectorAll("div.BlockRenderer_markup_Wtipg")) { - if (string.IsNullOrWhiteSpace(div.InnerHtml)) { + foreach (var div in doc.QuerySelectorAll("div.BlockRenderer_markup_Wtipg")) + if (string.IsNullOrWhiteSpace(div.InnerHtml)) div.InnerHtml = "
"; - } - } - + return doc; } - private async Task> GetToc(string bookId) { + private async Task> GetToc(string bookId) + { var result = new List(); - - var response = await Config.Client.GetWithTriesAsync(ApiUrl.MakeRelativeUri($"/v1/blog/{bookId}/post/").AppendQueryParameter("limit", 10)); + + var response = await Config.Client.GetWithTriesAsync(ApiUrl.MakeRelativeUri($"/v1/blog/{bookId}/post/") + .AppendQueryParameter("limit", 10)); var content = await response.Content.ReadFromJsonAsync>(); result.AddRange(content.Data); - while (!content.Extra.IsLast) { - response = await Config.Client.GetWithTriesAsync(ApiUrl.MakeRelativeUri($"/v1/blog/{bookId}/post/").AppendQueryParameter("limit", 10).AppendQueryParameter("offset", content.Extra.Offset)); + while (!content.Extra.IsLast) + { + response = await Config.Client.GetWithTriesAsync(ApiUrl.MakeRelativeUri($"/v1/blog/{bookId}/post/") + .AppendQueryParameter("limit", 10).AppendQueryParameter("offset", content.Extra.Offset)); content = await response.Content.ReadFromJsonAsync>(); result.AddRange(content.Data); } @@ -105,8 +115,11 @@ private async Task> GetToc(string bookId) { return SliceToc(result, c => c.Title); } - private Task GetCover(HtmlDocument doc, Uri uri) { + private Task GetCover(HtmlDocument doc, Uri uri) + { var imagePath = doc.QuerySelector("link[rel=image_src]")?.Attributes["href"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(uri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/BoovelGetter.cs b/Core/Logic/Getters/BoovelGetter.cs index 3c2b292d..02e9ce90 100644 --- a/Core/Logic/Getters/BoovelGetter.cs +++ b/Core/Logic/Getters/BoovelGetter.cs @@ -2,9 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; @@ -18,84 +15,92 @@ namespace Core.Logic.Getters; -public class BoovelGetter(BookGetterConfig config) : GetterBase(config) { - +public class BoovelGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://boovell.ru/"); - protected override string GetId(Uri url) { + protected override string GetId(Uri url) + { return url.GetSegment(2); } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { url = SystemUrl.MakeRelativeUri($"/story/{GetId(url)}"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(doc), Title = doc.GetTextBySelector("h1.story__identity-title"), Author = new Author("Boovell"), Annotation = doc.QuerySelector("section.story__summary")?.InnerHtml }; - + return book; } - private async Task> FillChapters(HtmlDocument doc) { + private async Task> FillChapters(HtmlDocument doc) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } - - foreach (var urlChapter in GetToc(doc)) { + if (Config.Options.NoChapters) return result; + + foreach (var urlChapter in GetToc(doc)) + { Config.Logger.LogInformation($"Загружаю главу {urlChapter.Title.CoverQuotes()}"); - var chapter = new Chapter { + var chapter = new Chapter + { Title = urlChapter.Title }; var chapterDoc = await GetChapter(urlChapter); - if (chapterDoc != default) { + if (chapterDoc is not null) + { chapter.Images = await GetImages(chapterDoc, SystemUrl); chapter.Content = chapterDoc.DocumentNode.InnerHtml; } - + result.Add(chapter); } return result; } - private IEnumerable GetToc(HtmlDocument doc) { + private IEnumerable GetToc(HtmlDocument doc) + { var result = doc .QuerySelectorAll("a.chapter-group__list-item-link[href]") - .Select(a => new UrlChapter(SystemUrl.MakeRelativeUri(a.Attributes["href"].Value), a.GetText().ReplaceNewLine())) + .Select(a => + new UrlChapter(SystemUrl.MakeRelativeUri(a.Attributes["href"].Value), a.GetText().ReplaceNewLine())) .ToList(); return SliceToc(result, c => c.Title); } - private async Task GetChapter(UrlChapter urlChapter) { + private async Task GetChapter(UrlChapter urlChapter) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(urlChapter.Url); var encoded = Convert.FromBase64String(doc.QuerySelector("#chapter-content").InnerText); var secret = doc.QuerySelector("body").Attributes["data-post-id"].Value + "skajldayzonePVPkruto0"; var crypto = SHA256.HashData(Encoding.UTF8.GetBytes(secret).AsSpan(0, Encoding.UTF8.GetByteCount(secret))); using var aes = Aes.Create(); - + const int IV_SHIFT = 16; - aes.Key = crypto; + aes.Key = crypto; aes.IV = encoded[..IV_SHIFT]; - + var decryptor = aes.CreateDecryptor(); using var ms = new MemoryStream(encoded[IV_SHIFT..]); await using var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read); - + var output = new MemoryStream(); await cs.CopyToAsync(output); - return FilterChapterContent( Encoding.UTF8.GetString(output.ToArray()) ); + return FilterChapterContent(Encoding.UTF8.GetString(output.ToArray())); } private static HtmlDocument FilterChapterContent(string data) @@ -108,12 +113,10 @@ private static HtmlDocument FilterChapterContent(string data) { var tag = node.Name; var value = node.QuerySelector("boovell")?.InnerHtml; - if( tag != "img" && tag != "br" ) + if (tag != "img" && tag != "br") { - if ( node.GetType() != typeof(HtmlTextNode) && node.GetType() != typeof(HtmlCommentNode) ) - { + if (node.GetType() != typeof(HtmlTextNode) && node.GetType() != typeof(HtmlCommentNode)) sb.Append($"<{tag}>{value}"); - } } else { @@ -124,8 +127,11 @@ private static HtmlDocument FilterChapterContent(string data) return sb.AsHtmlDoc(); } - private Task GetCover(HtmlDocument doc, Uri bookUri) { + private Task GetCover(HtmlDocument doc, Uri bookUri) + { var imagePath = doc.QuerySelector("a.story__thumbnail-href")?.Attributes["href"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(bookUri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/DarkNovelsGetter.cs b/Core/Logic/Getters/DarkNovelsGetter.cs index 4fa21d9d..665e5bb7 100644 --- a/Core/Logic/Getters/DarkNovelsGetter.cs +++ b/Core/Logic/Getters/DarkNovelsGetter.cs @@ -10,6 +10,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.Common; @@ -20,11 +21,13 @@ namespace Core.Logic.Getters; -public class DarkNovelsGetter : GetterBase { - private static readonly Dictionary _map = new(); +public class DarkNovelsGetter : GetterBase +{ private const string ALPHABET = "аАбБвВгГдДеЕёЁжЖзЗиИйЙкКлЛмМнНоОпПрРсСтТуУфФхХцЦчЧшШщЩъЪыЫьЬэЭюЮяЯ"; + private static readonly Dictionary _map = new(); - public DarkNovelsGetter(BookGetterConfig config) : base(config) { + public DarkNovelsGetter(BookGetterConfig config) : base(config) + { InitMap(); } @@ -32,45 +35,50 @@ public DarkNovelsGetter(BookGetterConfig config) : base(config) { private Uri _apiUrl => new($"https://api.{SystemUrl.Host}/"); - public override Task Init() { + public override Task Init() + { Config.Client.DefaultRequestVersion = HttpVersion.Version20; Config.Client.DefaultRequestHeaders.Add("User-Agent", "novels 1.0.3"); return Task.CompletedTask; } - public override async Task Authorize() { - if (!Config.HasCredentials) { - return; - } + public override async Task Authorize() + { + if (!Config.HasCredentials) return; - var payload = new { + var payload = new + { email = Config.Options.Login, password = Config.Options.Password }; var response = await Config.Client.PostAsJsonAsync(_apiUrl.MakeRelativeUri("/v1/users/login"), payload); var data = await response.Content.ReadFromJsonAsync>(); - if (data!.Status == "success") { + if (data!.Status == "success") + { Config.Client.DefaultRequestHeaders.Add("Token", data.Data.Token.AccessToken); Config.Logger.LogInformation("Успешно авторизовались"); - } else { - throw new Exception($"Не удалось авторизоваться. {data.Message}"); + } + else + { + throw new Elib2EbookAuthException($"Не удалось авторизоваться. {data.Message}"); } } - private static void InitMap() { + private static void InitMap() + { var start = 13338; const int shift = 38; - foreach (var c in ALPHABET) { - for (var i = start; i < start + shift; i++) { - _map[i] = c; - } + foreach (var c in ALPHABET) + { + for (var i = start; i < start + shift; i++) _map[i] = c; start += shift; } } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { url = await GetMainUrl(url); var bookFullId = GetId(url); @@ -79,7 +87,8 @@ public override async Task Get(Uri url) { var uri = SystemUrl.MakeRelativeUri($"/{bookFullId}/"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(uri); - var book = new Book(uri) { + var book = new Book(uri) + { Cover = await GetCover(doc, uri), Chapters = await FillChapters(bookId), Title = doc.GetTextBySelector("h2.display-1"), @@ -90,13 +99,16 @@ public override async Task Get(Uri url) { return book; } - private static Author GetAuthor(HtmlDocument doc) { + private static Author GetAuthor(HtmlDocument doc) + { var match = Regex.Match(doc.ParsedText, "authors:\"(?.*?)\""); return new Author(match.Success ? match.Groups["author"].Value : "DarkNovels"); } - private async Task GetMainUrl(Uri url) { - if (url.GetSegment(1) == "read") { + private async Task GetMainUrl(Uri url) + { + if (url.GetSegment(1) == "read") + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); var id = Regex.Match(doc.DocumentNode.InnerHtml, "slug:\"(?.*?)\""); return SystemUrl.MakeRelativeUri($"/{id.Groups["id"].Value}"); @@ -105,16 +117,17 @@ private async Task GetMainUrl(Uri url) { return url; } - private async Task> FillChapters(string bookId) { + private async Task> FillChapters(string bookId) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - foreach (var darkNovelsChapter in await GetToc(bookId)) { + foreach (var darkNovelsChapter in await GetToc(bookId)) + { Config.Logger.LogInformation($"Загружаю главу {darkNovelsChapter.Title.CoverQuotes()}"); - var chapter = new Chapter { + var chapter = new Chapter + { Title = darkNovelsChapter.Title }; @@ -126,50 +139,56 @@ private async Task> FillChapters(string bookId) { return result; } - private Task GetCover(HtmlDocument doc, Uri bookUri) { + private Task GetCover(HtmlDocument doc, Uri bookUri) + { var imagePath = doc.QuerySelector("div.book-cover-container img")?.Attributes["data-src"]?.Value; - if (string.IsNullOrWhiteSpace(imagePath)) { + if (string.IsNullOrWhiteSpace(imagePath)) + { var match = new Regex("\"image\": \"(?.*?)\"").Match(doc.Text); - if (match.Success) { - imagePath = match.Groups["url"].Value; - } + if (match.Success) imagePath = match.Groups["url"].Value; } - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(bookUri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } - private async Task> GetToc(string bookId) { - return await Config.Client.GetFromJsonAsync>(_apiUrl.MakeRelativeUri($"/v2/toc/{bookId}")) - .ContinueWith(t => SliceToc(t.Result?.Data.Where(c => !c.Title.StartsWith("Volume:")).ToList(), c => c.Title)); + private async Task> GetToc(string bookId) + { + return await Config.Client + .GetFromJsonAsync>(_apiUrl.MakeRelativeUri($"/v2/toc/{bookId}")) + .ContinueWith(t => + SliceToc(t.Result?.Data.Where(c => !c.Title.StartsWith("Volume:")).ToList(), c => c.Title)); } - private async Task FillChapter(string bookId, DarkNovelsChapter darkNovelsChapter, Chapter chapter) { - var data = await Config.Client.PostWithTriesAsync(_apiUrl.MakeRelativeUri("/v2/chapter/"), GetData(bookId, darkNovelsChapter.Id, "html"), TimeSpan.FromMilliseconds(200)); - if (data.StatusCode == HttpStatusCode.BadRequest) { - return; - } - + private async Task FillChapter(string bookId, DarkNovelsChapter darkNovelsChapter, Chapter chapter) + { + var data = await Config.Client.PostWithTriesAsync(_apiUrl.MakeRelativeUri("/v2/chapter/"), + GetData(bookId, darkNovelsChapter.Id, "html"), TimeSpan.FromMilliseconds(200)); + if (data.StatusCode == HttpStatusCode.BadRequest) return; + using var zip = new ZipArchive(await data.Content.ReadAsStreamAsync()); var sb = new StringBuilder(); - foreach (var entry in zip.Entries) { + foreach (var entry in zip.Entries) + { using var sr = new StreamReader(entry.Open()); - foreach (var c in await sr.ReadToEndAsync()) { - sb.Append(_map.GetValueOrDefault(c, c)); - } + foreach (var c in await sr.ReadToEndAsync()) sb.Append(_map.GetValueOrDefault(c, c)); } - + var chapterDoc = sb.AsHtmlDoc().RemoveNodes("h1"); chapter.Images = await GetImages(chapterDoc, SystemUrl); chapter.Content = chapterDoc.DocumentNode.InnerHtml; } - private static MultipartFormDataContent GetData(string bookId, int chapterId, string format) { - return new MultipartFormDataContent { + private static MultipartFormDataContent GetData(string bookId, int chapterId, string format) + { + return new MultipartFormDataContent + { { new StringContent(bookId), "b" }, { new StringContent(format), "f" }, { new StringContent("l"), "t" }, { new StringContent(chapterId.ToString()), "c" } }; } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/DesuGetter.cs b/Core/Logic/Getters/DesuGetter.cs index c96604a6..4958bc03 100644 --- a/Core/Logic/Getters/DesuGetter.cs +++ b/Core/Logic/Getters/DesuGetter.cs @@ -13,96 +13,115 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class DesuGetter(BookGetterConfig config) : GetterBase(config) { +public class DesuGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://desu.me/"); - protected override string GetId(Uri url) { + protected override string GetId(Uri url) + { return url.GetSegment(2); } - public override async Task Init() { + public override async Task Init() + { await base.Init(); Config.Client.DefaultRequestHeaders.Add("Referer", SystemUrl.ToString()); } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { url = SystemUrl.MakeRelativeUri($"/manga/{GetId(url)}"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(doc), Title = GetTitle(doc), Author = GetAuthor(doc), Annotation = doc.QuerySelector("#description div.prgrph")?.InnerHtml }; - + return book; } - private Author GetAuthor(HtmlDocument doc) { + private Author GetAuthor(HtmlDocument doc) + { var a = doc.QuerySelector("ul.translators a"); - return a == default ? new Author("Desu") : new Author(a.GetText(), SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)); + return a is null + ? new Author("Desu") + : new Author(a.GetText(), SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)); } - private static string GetTitle(HtmlDocument doc) { + private static string GetTitle(HtmlDocument doc) + { return doc.GetTextBySelector("h1 span.rus-name") ?? doc.GetTextBySelector("h1 span.name"); } - private async Task> FillChapters(HtmlDocument doc) { + private async Task> FillChapters(HtmlDocument doc) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } - - foreach (var urlChapter in GetToc(doc)) { + if (Config.Options.NoChapters) return result; + + foreach (var urlChapter in GetToc(doc)) + { Config.Logger.LogInformation($"Загружаю главу {urlChapter.Title.CoverQuotes()}"); - var chapter = new Chapter { + var chapter = new Chapter + { Title = urlChapter.Title }; var chapterDoc = await GetChapter(urlChapter); - if (chapterDoc != default) { + if (chapterDoc is not null) + { chapter.Images = await GetImages(chapterDoc, SystemUrl); chapter.Content = chapterDoc.DocumentNode.InnerHtml; } - + result.Add(chapter); } return result; } - private IEnumerable GetToc(HtmlDocument doc) { + private IEnumerable GetToc(HtmlDocument doc) + { var result = doc .QuerySelectorAll("ul.chlist a[href]") - .Select(a => new UrlChapter(SystemUrl.MakeRelativeUri(a.Attributes["href"].Value), a.GetText().ReplaceNewLine())) + .Select(a => + new UrlChapter(SystemUrl.MakeRelativeUri(a.Attributes["href"].Value), a.GetText().ReplaceNewLine())) .Reverse() .ToList(); return SliceToc(result, c => c.Title); } - private async Task GetChapter(UrlChapter urlChapter) { + private async Task GetChapter(UrlChapter urlChapter) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(urlChapter.Url.AppendQueryParameter("mtr", "1")); - var images = Regex.Match(doc.ParsedText, @"Reader\.init(.*?)(?\[\[(.*?)\]\]),", RegexOptions.Singleline).Groups["data"].Value.Replace("'", "\"").Deserialize>(); + var images = Regex.Match(doc.ParsedText, @"Reader\.init(.*?)(?\[\[(.*?)\]\]),", RegexOptions.Singleline) + .Groups["data"].Value.Replace("'", "\"").Deserialize>(); var dir = Regex.Match(doc.ParsedText, "dir: \"(?(.*?))\"").Groups["data"].Value; var sb = new StringBuilder(); - - foreach (var elem in images) { + + foreach (var elem in images) + { var imageUri = SystemUrl.MakeRelativeUri(dir).MakeRelativeUri(elem[0].GetString()); sb.Append($""); } - + return sb.AsHtmlDoc(); } - private Task GetCover(HtmlDocument doc, Uri bookUri) { + private Task GetCover(HtmlDocument doc, Uri bookUri) + { var imagePath = doc.QuerySelector("div.c-poster img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(bookUri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/DreameGetter.cs b/Core/Logic/Getters/DreameGetter.cs index 52881e95..6e81a3a3 100644 --- a/Core/Logic/Getters/DreameGetter.cs +++ b/Core/Logic/Getters/DreameGetter.cs @@ -12,22 +12,28 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class DreameGetter(BookGetterConfig config) : GetterBase(config) { +public class DreameGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://dreame.com/"); - + private Uri _apiUrl => new($"https://wap-api.{SystemUrl.Host}/"); - protected override string GetId(Uri url) => url.GetSegment(2).Split(".")[0].Split("-")[0]; + protected override string GetId(Uri url) + { + return url.GetSegment(2).Split(".")[0].Split("-")[0]; + } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { var bookId = GetId(url); url = $"https://www.dreame.com/story/{bookId}".AsUri(); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); var data = GetNextData(doc, "novel"); - - var book = new Book(url) { + + var book = new Book(url) + { Cover = await GetCover(data.Cover), Chapters = await FillChapters(url, bookId), Title = data.Name, @@ -35,11 +41,12 @@ public override async Task Get(Uri url) { Annotation = data.Description, Lang = data.Language }; - + return book; } - - private static T GetNextData(HtmlDocument doc, string node) { + + private static T GetNextData(HtmlDocument doc, string node) + { var json = doc.QuerySelector("#__NEXT_DATA__").InnerText; return JsonDocument.Parse(json) .RootElement.GetProperty("props") @@ -49,49 +56,58 @@ private static T GetNextData(HtmlDocument doc, string node) { .GetRawText() .Deserialize(); } - - private async Task> FillChapters(Uri url, string bookId) { + + private async Task> FillChapters(Uri url, string bookId) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - foreach (var urlChapter in await GetToc(bookId)) { + foreach (var urlChapter in await GetToc(bookId)) + { Config.Logger.LogInformation($"Загружаю главу {urlChapter.Title.CoverQuotes()}"); - var chapter = new Chapter { + var chapter = new Chapter + { Title = urlChapter.Title }; var chapterDoc = await GetChapter(urlChapter); - if (chapterDoc != default) { + if (chapterDoc is not null) + { chapter.Images = await GetImages(chapterDoc, url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; } - + result.Add(chapter); } return result; } - private async Task GetChapter(DreameChapter urlChapter) { - var response = await Config.Client.GetFromJsonWithTriesAsync>(_apiUrl.MakeRelativeUri($"/novel/getChapter?channel=dreamepmian-173&product=1&osType=2&cid={urlChapter.Id}&systemFlag=android&port=web")); + private async Task GetChapter(DreameChapter urlChapter) + { + var response = await Config.Client.GetFromJsonWithTriesAsync>( + _apiUrl.MakeRelativeUri( + $"/novel/getChapter?channel=dreamepmian-173&product=1&osType=2&cid={urlChapter.Id}&systemFlag=android&port=web")); return Encoding.UTF8.GetString(Convert.FromBase64String(response.Data.Content)).AsHtmlDoc(); } - private async Task> GetToc(string bookId) { - var response = await Config.Client.GetFromJsonWithTriesAsync>(_apiUrl.MakeRelativeUri($"novel/getcatelog?channel=dreamepmian-173&product=1&osType=2&nid={bookId}")); + private async Task> GetToc(string bookId) + { + var response = await Config.Client.GetFromJsonWithTriesAsync>( + _apiUrl.MakeRelativeUri($"novel/getcatelog?channel=dreamepmian-173&product=1&osType=2&nid={bookId}")); return SliceToc(response.Data.Pager.ChapterList, c => c.Title); } - private Author GetAuthor(HtmlDocument doc, DreameNovel data) { + private Author GetAuthor(HtmlDocument doc, DreameNovel data) + { var a = doc.QuerySelector("a a[class*=story_author-name]"); - return a != default ? - new Author(data.AuthorName, SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)) : - new Author(data.AuthorName); + return a is not null + ? new Author(data.AuthorName, SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)) + : new Author(data.AuthorName); } - private Task GetCover(string url) { + private Task GetCover(string url) + { return !string.IsNullOrWhiteSpace(url) ? SaveImage(url.AsUri()) : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/FanFicusGetter.cs b/Core/Logic/Getters/FanFicusGetter.cs index 9fc58eff..dbca0291 100644 --- a/Core/Logic/Getters/FanFicusGetter.cs +++ b/Core/Logic/Getters/FanFicusGetter.cs @@ -6,6 +6,7 @@ using System.Net.Http.Json; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.Common; @@ -15,96 +16,121 @@ namespace Core.Logic.Getters; -public class FanFicusGetter(BookGetterConfig config) : GetterBase(config) { - protected override Uri SystemUrl => new("https://fanficus.com/"); - +public class FanFicusGetter(BookGetterConfig config) : GetterBase(config) +{ private readonly Uri _apiHost = new("https://fanficus-server-mirror-879c30cd977f.herokuapp.com/"); + protected override Uri SystemUrl => new("https://fanficus.com/"); - protected override string GetId(Uri url) { + protected override string GetId(Uri url) + { return url.GetSegment(2); } - public override async Task Authorize() { - if (!Config.HasCredentials) { - return; - } - - var response = await Config.Client.PostAsJsonAsync(_apiHost.MakeRelativeUri("api/v1/user/login"), GenerateAuthData()); - if (response.StatusCode == HttpStatusCode.OK) { + public override async Task Authorize() + { + if (!Config.HasCredentials) return; + + var response = + await Config.Client.PostAsJsonAsync(_apiHost.MakeRelativeUri("api/v1/user/login"), GenerateAuthData()); + if (response.StatusCode == HttpStatusCode.OK) + { var user = await response.Content.ReadFromJsonAsync>(); - Config.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", user.Value.Token); + Config.Client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", user.Value.Token); Config.Logger.LogInformation("Успешно авторизовались"); - } else { - throw new Exception("Не удалось авторизоваться. Неверный пароль или имейл"); + } + else + { + throw new Elib2EbookAuthException("Не удалось авторизоваться. Неверный пароль или имейл"); } } - private object GenerateAuthData() { - return new { - username = Config.Options.Login, - password = Config.Options.Password, + private object GenerateAuthData() + { + return new + { + username = Config.Options.Login, + password = Config.Options.Password }; } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { var bookId = GetId(url); - + url = SystemUrl.MakeRelativeUri($"/post/{bookId}"); - var response = await Config.Client.GetFromJsonWithTriesAsync>(_apiHost.MakeRelativeUri($"api/v1/post/{bookId}")); - - var book = new Book(url) { + var response = + await Config.Client.GetFromJsonWithTriesAsync>( + _apiHost.MakeRelativeUri($"api/v1/post/{bookId}")); + + var book = new Book(url) + { Cover = await GetCover(response.Value, url), Chapters = await FillChapters(bookId), Title = response.Value.Title, Author = GetAuthor(response.Value), Annotation = response.Value.Description }; - + return book; } - private Author GetAuthor(FanFicusBook book) { + private Author GetAuthor(FanFicusBook book) + { var creator = book.Creators.FirstOrDefault(); - return creator == default ? new Author("FanFicus") : new Author(creator.NickName, SystemUrl.MakeRelativeUri($"/user/{creator.Id}")); + return creator is null + ? new Author("FanFicus") + : new Author(creator.NickName, SystemUrl.MakeRelativeUri($"/user/{creator.Id}")); } - private async Task> GetToc(string bookId) { - var response = await Config.Client.GetFromJsonWithTriesAsync>(_apiHost.MakeRelativeUri($"/api/v1/post/{bookId}/post-part")); + private async Task> GetToc(string bookId) + { + var response = + await Config.Client.GetFromJsonWithTriesAsync>( + _apiHost.MakeRelativeUri($"/api/v1/post/{bookId}/post-part")); return SliceToc(response.Value, c => c.Title); } - - private async Task> FillChapters(string bookId) { + + private async Task> FillChapters(string bookId) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - foreach (var fanFicusPart in await GetToc(bookId)) { - var chapter = new Chapter { + foreach (var fanFicusPart in await GetToc(bookId)) + { + var chapter = new Chapter + { Title = fanFicusPart.Title }; Config.Logger.LogInformation($"Загружаю главу {fanFicusPart.Title.CoverQuotes()}"); var chapterDoc = await GetChapter(bookId, fanFicusPart); - if (chapterDoc != default) { + if (chapterDoc is not null) + { chapter.Images = await GetImages(chapterDoc, SystemUrl); chapter.Content = chapterDoc.DocumentNode.InnerHtml; } - + result.Add(chapter); } return result; } - private async Task GetChapter(string bookId, FanFicusPart fanFicusPart) { - var response = await Config.Client.GetFromJsonWithTriesAsync>(_apiHost.MakeRelativeUri($"api/v1/post/{bookId}/post-part/{fanFicusPart.Id}")); + private async Task GetChapter(string bookId, FanFicusPart fanFicusPart) + { + var response = + await Config.Client.GetFromJsonWithTriesAsync>( + _apiHost.MakeRelativeUri($"api/v1/post/{bookId}/post-part/{fanFicusPart.Id}")); return response.Value.Text.AsHtmlDoc(); } - private Task GetCover(FanFicusBook doc, Uri uri) { + private Task GetCover(FanFicusBook doc, Uri uri) + { var imagePath = doc.Images.FirstOrDefault()?.Url; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(uri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/Fb2Top/Fb2TopGetter.cs b/Core/Logic/Getters/Fb2Top/Fb2TopGetter.cs index 6c8e70be..ae6c572f 100644 --- a/Core/Logic/Getters/Fb2Top/Fb2TopGetter.cs +++ b/Core/Logic/Getters/Fb2Top/Fb2TopGetter.cs @@ -1,8 +1,9 @@ using System; using Core.Configs; -namespace Core.Logic.Getters.Fb2Top; +namespace Core.Logic.Getters.Fb2Top; -public class Fb2TopGetter(BookGetterConfig config) : Fb2TopGetterBase(config) { +public class Fb2TopGetter(BookGetterConfig config) : Fb2TopGetterBase(config) +{ protected override Uri SystemUrl => new("https://fb2.top/"); -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/Fb2Top/Fb2TopGetterBase.cs b/Core/Logic/Getters/Fb2Top/Fb2TopGetterBase.cs index 70a41104..f56f8c45 100644 --- a/Core/Logic/Getters/Fb2Top/Fb2TopGetterBase.cs +++ b/Core/Logic/Getters/Fb2Top/Fb2TopGetterBase.cs @@ -10,16 +10,22 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters.Fb2Top; +namespace Core.Logic.Getters.Fb2Top; -public abstract class Fb2TopGetterBase(BookGetterConfig config) : GetterBase(config) { - protected override string GetId(Uri url) => url.GetSegment(1); +public abstract class Fb2TopGetterBase(BookGetterConfig config) : GetterBase(config) +{ + protected override string GetId(Uri url) + { + return url.GetSegment(1); + } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { url = url.MakeRelativeUri(GetId(url)); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - - var book = new Book(url) { + + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, url), Title = doc.GetTextBySelector("h1.book-title"), @@ -27,67 +33,77 @@ public override async Task Get(Uri url) { Annotation = doc.QuerySelector("div.annotation")?.InnerHtml, Seria = GetSeria(doc, url) }; - + return book; } - private async Task> FillChapters(HtmlDocument doc, Uri url) { + private async Task> FillChapters(HtmlDocument doc, Uri url) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - foreach (var urlChapter in GetToc(doc, url)) { + foreach (var urlChapter in GetToc(doc, url)) + { Config.Logger.LogInformation($"Загружаю главу {urlChapter.Title.CoverQuotes()}"); - var chapter = new Chapter { + var chapter = new Chapter + { Title = urlChapter.Title }; var chapterDoc = await GetChapter(urlChapter.Url); - if (chapterDoc != default) { + if (chapterDoc is not null) + { chapter.Images = await GetImages(chapterDoc, urlChapter.Url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; } - + result.Add(chapter); } return result; } - private async Task GetChapter(Uri url) { + private async Task GetChapter(Uri url) + { var doc = await Config.Client.GetStringAsync(url); doc = doc.Replace("", ""); doc = doc.Replace("

", "

"); return doc.AsHtmlDoc().QuerySelector("section").RemoveNodes("h3").InnerHtml.AsHtmlDoc(); } - private IEnumerable GetToc(HtmlDocument doc, Uri url) { - var urlChapters = doc.QuerySelectorAll("div.card-body li a").Select(a => new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.GetText())).ToList(); + private IEnumerable GetToc(HtmlDocument doc, Uri url) + { + var urlChapters = doc.QuerySelectorAll("div.card-body li a") + .Select(a => new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.GetText())).ToList(); return SliceToc(urlChapters, c => c.Title); } - private static Seria GetSeria(HtmlDocument doc, Uri url) { + private static Seria GetSeria(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("div.book-info-body a[href*=/series/]"); - if (a != default) { - return new Seria { + if (a is not null) + return new Seria + { Name = a.GetText(), Url = url.MakeRelativeUri(a.Attributes["href"].Value) }; - } return default; } - private static Author GetAuthor(HtmlDocument doc, Uri url) { + private static Author GetAuthor(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("div.book-info-body a[href*=/authors/]"); - return a != default ? - new Author(a.GetText().ReplaceNewLine(), url.MakeRelativeUri(a.Attributes["href"].Value)) : - new Author("Fb2Top"); + return a is not null + ? new Author(a.GetText().ReplaceNewLine(), url.MakeRelativeUri(a.Attributes["href"].Value)) + : new Author("Fb2Top"); } - private Task GetCover(HtmlDocument doc, Uri url) { + private Task GetCover(HtmlDocument doc, Uri url) + { var imagePath = doc.QuerySelector("img.book-info-poster-img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(url.MakeRelativeUri(imagePath.HtmlDecode())) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(url.MakeRelativeUri(imagePath.HtmlDecode())) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/Fb2Top/LadyLibGetter.cs b/Core/Logic/Getters/Fb2Top/LadyLibGetter.cs index ec5f6453..3e47580d 100644 --- a/Core/Logic/Getters/Fb2Top/LadyLibGetter.cs +++ b/Core/Logic/Getters/Fb2Top/LadyLibGetter.cs @@ -1,8 +1,9 @@ using System; using Core.Configs; -namespace Core.Logic.Getters.Fb2Top; +namespace Core.Logic.Getters.Fb2Top; -public class LadyLibGetter(BookGetterConfig config) : Fb2TopGetterBase(config) { +public class LadyLibGetter(BookGetterConfig config) : Fb2TopGetterBase(config) +{ protected override Uri SystemUrl => new("https://ladylib.top/"); -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/FicbookGetter.cs b/Core/Logic/Getters/FicbookGetter.cs index d85c8d6f..ea1068cf 100644 --- a/Core/Logic/Getters/FicbookGetter.cs +++ b/Core/Logic/Getters/FicbookGetter.cs @@ -11,57 +11,69 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class FicbookGetter(BookGetterConfig config) : GetterBase(config) { +public class FicbookGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://ficbook.net/"); - + protected Uri _appUrl => new("https://fanficlets.xyz/"); - protected override string GetId(Uri url) => url.GetSegment(2); + protected override string GetId(Uri url) + { + return url.GetSegment(2); + } - public override async Task Init() { + public override async Task Init() + { await base.Init(); Config.Client.DefaultRequestHeaders.Remove("User-Agent"); - Config.Client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "Mozilla/5.0 (iPhone); iOS CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1 ||| ficbook (1.3.2) | iOS (26.1) | iPhone 13 | ao2gvvnhkv0t22lo"); + Config.Client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", + "Mozilla/5.0 (iPhone); iOS CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1 ||| ficbook (1.3.2) | iOS (26.1) | iPhone 13 | ao2gvvnhkv0t22lo"); } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { var appUrl = _appUrl.MakeRelativeUri($"/readfic/{GetId(url)}"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(appUrl); var title = doc.GetTextBySelector("h1"); - var book = new Book(SystemUrl.MakeRelativeUri($"/readfic/{GetId(url)}")) { + var book = new Book(SystemUrl.MakeRelativeUri($"/readfic/{GetId(url)}")) + { Cover = await GetCover(doc, appUrl), Chapters = await FillChapters(doc, appUrl, title), Title = title, Author = GetAuthor(doc, appUrl), Annotation = doc.QuerySelector("div[itemprop=description]")?.InnerHtml }; - + return book; } - private static Author GetAuthor(HtmlDocument doc, Uri url) { + private static Author GetAuthor(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("a.creator-username"); return new Author(a.GetText(), url.MakeRelativeUri(a.Attributes["href"].Value)); } - - private Task GetCover(HtmlDocument doc, Uri bookUri) { + + private Task GetCover(HtmlDocument doc, Uri bookUri) + { var imagePath = doc.QuerySelector("fanfic-cover")?.Attributes["src-original"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(bookUri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } - private async Task> FillChapters(HtmlDocument doc, Uri url, string title) { + private async Task> FillChapters(HtmlDocument doc, Uri url, string title) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } - - foreach (var ficbookChapter in GetToc(doc, url, title)) { + if (Config.Options.NoChapters) return result; + + foreach (var ficbookChapter in GetToc(doc, url, title)) + { Config.Logger.LogInformation($"Загружаю главу {ficbookChapter.Title.CoverQuotes()}"); var chapter = new Chapter(); - + var chapterDoc = await GetChapter(ficbookChapter); chapter.Images = await GetImages(chapterDoc, url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -73,44 +85,43 @@ private async Task> FillChapters(HtmlDocument doc, Uri url, return result; } - private async Task GetChapter(UrlChapter urlChapter) { + private async Task GetChapter(UrlChapter urlChapter) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(urlChapter.Url); - + var content = doc.QuerySelector("#content").RemoveNodes("div"); using var sr = new StringReader(content.InnerText.HtmlDecode()); var text = new StringBuilder(); - while (true) { + while (true) + { var line = await sr.ReadLineAsync(); - if (line == null) { - break; - } + if (line == null) break; + + if (string.IsNullOrWhiteSpace(line)) continue; - if (string.IsNullOrWhiteSpace(line)) { - continue; - } - text.Append(line.HtmlEncode().CoverTag("p")); } return text.AsHtmlDoc(); } - private IEnumerable GetToc(HtmlDocument doc, Uri url, string title) { + private IEnumerable GetToc(HtmlDocument doc, Uri url, string title) + { var result = new List(); - + var links = doc.QuerySelectorAll("li.part"); - if (links.Count == 0) { + if (links.Count == 0) result.Add(new UrlChapter(url, title)); - } else { - foreach (var li in links) { + else + foreach (var li in links) + { var a = li.QuerySelector("a.part-link"); - if (a != null) { - result.Add(new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), li.GetTextBySelector("h3"))); - } + if (a != null) + result.Add(new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), + li.GetTextBySelector("h3"))); } - } - + return SliceToc(result, c => c.Title); } } diff --git a/Core/Logic/Getters/FictionBookGetter.cs b/Core/Logic/Getters/FictionBookGetter.cs index a37bfdce..fd9edd13 100644 --- a/Core/Logic/Getters/FictionBookGetter.cs +++ b/Core/Logic/Getters/FictionBookGetter.cs @@ -11,125 +11,142 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class FictionBookGetter(BookGetterConfig config) : GetterBase(config) { +public class FictionBookGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://fictionbook.ru"); - - private Uri GetMainUrl(Uri url) { - if (url.Segments.Last().StartsWith("read_online.html")) { + + private Uri GetMainUrl(Uri url) + { + if (url.Segments.Last().StartsWith("read_online.html")) url = SystemUrl.MakeRelativeUri(string.Join(string.Empty, url.Segments[..^1])); - } return url; } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { url = GetMainUrl(url); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); var name = doc.GetTextBySelector("h1 span[itemprop=name]"); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(url, name), Title = name, Author = GetAuthor(doc), - Annotation = doc.QuerySelector("div.biblio_book__annotation")?.InnerHtml, + Annotation = doc.QuerySelector("div.biblio_book__annotation")?.InnerHtml }; - - return book; + + return book; } - private Author GetAuthor(HtmlDocument doc) { + private Author GetAuthor(HtmlDocument doc) + { var a = doc.QuerySelector("div.info a[data-widget-litres-author][href]"); - return a == default ? - new Author("FictionBook") : - new Author(a.GetText(), SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)); + return a is null + ? new Author("FictionBook") + : new Author(a.GetText(), SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)); } - private Task GetCover(HtmlDocument doc, Uri bookUri) { + private Task GetCover(HtmlDocument doc, Uri bookUri) + { var imagePath = doc.QuerySelector("div.cover_float img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(bookUri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } - private async Task AddChapter(ICollection chapters, Chapter chapter, StringBuilder text) { - if (chapter == null) { - return; - } - + private async Task AddChapter(ICollection chapters, Chapter chapter, StringBuilder text) + { + if (chapter == null) return; + var chapterDoc = text.AsHtmlDoc(); chapter.Images = await GetImages(chapterDoc, SystemUrl.MakeRelativeUri("/br/")); chapter.Content = chapterDoc.DocumentNode.InnerHtml; chapters.Add(chapter); } - private static bool IsChapterHeader(HtmlNode node) { + private static bool IsChapterHeader(HtmlNode node) + { return node.Name == "h2"; } - private static bool IsSingleChapter(IEnumerable nodes) { + private static bool IsSingleChapter(IEnumerable nodes) + { var firstNode = nodes.First(); return !IsChapterHeader(firstNode) || string.IsNullOrWhiteSpace(firstNode.InnerText); } - private static Chapter CreateChapter(string title) { - return new Chapter { + private static Chapter CreateChapter(string title) + { + return new Chapter + { Title = title }; } - private Task GetChapter(Uri url, int page) { - return Config.Client.GetHtmlDocWithTriesAsync(url.AppendSegment("read_online.html").AppendQueryParameter("page", page)); + private Task GetChapter(Uri url, int page) + { + return Config.Client.GetHtmlDocWithTriesAsync(url.AppendSegment("read_online.html") + .AppendQueryParameter("page", page)); } - private async Task GetPages(Uri url) { + private async Task GetPages(Uri url) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(url.AppendSegment("read_online.html")); var pages = doc.QuerySelectorAll("div.reader_pager_bottom > a[href]") .Where(a => int.TryParse(a.GetText(), out _)) .Select(a => int.Parse(a.GetText())) .ToList(); - + return pages.Any() ? pages.Max() : 1; } - private async Task> FillChapters(Uri url, string name) { + private async Task> FillChapters(Uri url, string name) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } - + if (Config.Options.NoChapters) return result; + Chapter chapter = null; var singleChapter = true; var text = new StringBuilder(); var pages = await GetPages(url); - - for (var i = 1; i <= pages; i++) { + + for (var i = 1; i <= pages; i++) + { Config.Logger.LogInformation($"Получаю страницу {i}/{pages}"); var page = await GetChapter(url, i); - var nodes = page.QuerySelector("#onlineread").RemoveNodes("div.biblio_book__wrap").ChildNodes; + var nodes = page.QuerySelector("#onlineread").RemoveNodes("div.biblio_book__wrap").ChildNodes; singleChapter = i == 1 ? IsSingleChapter(nodes) : singleChapter; - foreach (var node in nodes) { - if (singleChapter || !IsChapterHeader(node)) { - if (node.Name == "img" && node.Attributes["src"] != null) { + foreach (var node in nodes) + if (singleChapter || !IsChapterHeader(node)) + { + if (node.Name == "img" && node.Attributes["src"] != null) + { text.Append($""); - } else { - if (!string.IsNullOrWhiteSpace(node.InnerHtml)) { + } + else + { + if (!string.IsNullOrWhiteSpace(node.InnerHtml)) text.Append(node.InnerHtml.HtmlDecode().CoverTag("p")); - } } - } else { + } + else + { await AddChapter(result, chapter, text); text.Clear(); chapter = CreateChapter(node.InnerText.HtmlDecode()); } - } } await AddChapter(result, chapter ?? CreateChapter(name), text); return result; } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/FreedlitGetter.cs b/Core/Logic/Getters/FreedlitGetter.cs index 54fdf24f..7d5b5dbb 100644 --- a/Core/Logic/Getters/FreedlitGetter.cs +++ b/Core/Logic/Getters/FreedlitGetter.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using System.Web; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.Common; @@ -14,50 +15,58 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class FreedlitGetter(BookGetterConfig config) : GetterBase(config) { +public class FreedlitGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://freedlit.space/"); - public override async Task Authorize() { - if (!Config.HasCredentials) { - return; - } + public override async Task Authorize() + { + if (!Config.HasCredentials) return; await Config.Client.GetAsync(SystemUrl.MakeRelativeUri("/login")); SetToken(); using var post = await Config.Client.PostAsJsonAsync(SystemUrl.MakeRelativeUri("/login"), GenerateAuthData()); var doc = await post.Content.ReadAsStreamAsync().ContinueWith(c => c.Result.AsHtmlDoc()); - var errors = doc.QuerySelector("#app").Attributes["data-page"].Value.HtmlDecode().Deserialize>().Props.Errors; - - if (string.IsNullOrWhiteSpace(errors?.Email)) { + var errors = doc.QuerySelector("#app").Attributes["data-page"].Value.HtmlDecode() + .Deserialize>().Props.Errors; + + if (string.IsNullOrWhiteSpace(errors?.Email)) Config.Logger.LogInformation("Успешно авторизовались"); - } else { - throw new Exception($"Не удалось авторизоваться. {errors.Email}"); - } + else + throw new Elib2EbookAuthException($"Не удалось авторизоваться. {errors.Email}"); } - private object GenerateAuthData() { - return new { + private object GenerateAuthData() + { + return new + { email = Config.Options.Login, password = Config.Options.Password, - remember = false, + remember = false }; } - private void SetToken() { + private void SetToken() + { Config.Client.DefaultRequestHeaders.Remove("X-XSRF-TOKEN"); - Config.Client.DefaultRequestHeaders.Add("X-XSRF-TOKEN", HttpUtility.UrlDecode(Config.CookieContainer.GetAllCookies().FirstOrDefault(c => c.Name == "XSRF-TOKEN").Value)); + Config.Client.DefaultRequestHeaders.Add("X-XSRF-TOKEN", + HttpUtility.UrlDecode(Config.CookieContainer.GetAllCookies().FirstOrDefault(c => c.Name == "XSRF-TOKEN") + .Value)); } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { url = SystemUrl.MakeRelativeUri($"/book/{GetId(url)}"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - var freedlitBook = doc.QuerySelector("#app").Attributes["data-page"].Value.HtmlDecode().Deserialize>().Props.Book; + var freedlitBook = doc.QuerySelector("#app").Attributes["data-page"].Value.HtmlDecode() + .Deserialize>().Props.Book; SetToken(); - - var book = new Book(url) { + + var book = new Book(url) + { Cover = await GetCover(freedlitBook), Chapters = await FillChapters(freedlitBook), Title = freedlitBook.Title, @@ -65,36 +74,42 @@ public override async Task Get(Uri url) { Annotation = freedlitBook.Annotation, Lang = freedlitBook.Language }; - + return book; } - private Author GetAuthor(FreedlitBook book) { + private Author GetAuthor(FreedlitBook book) + { return new Author(book.MainAuthor.Name, SystemUrl.MakeRelativeUri($"/p/{book.MainAuthor.UserLink}")); } - - private async Task> GetToc(FreedlitBook book) { - var response = await Config.Client.PostAsJsonAsync(SystemUrl.MakeRelativeUri("/api/bookpage/get-chapters"), new { book_id = book.Id }); - var data = await response.Content.ReadFromJsonAsync>>(); - + + private async Task> GetToc(FreedlitBook book) + { + var response = await Config.Client.PostAsJsonAsync(SystemUrl.MakeRelativeUri("/api/bookpage/get-chapters"), + new { book_id = book.Id }); + var data = + await response.Content.ReadFromJsonAsync>>(); + return SliceToc(data.Success.Items, c => c.Header); } - private async Task> FillChapters(FreedlitBook book) { + private async Task> FillChapters(FreedlitBook book) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - foreach (var freedlitChapter in await GetToc(book)) { - var chapter = new Chapter { + foreach (var freedlitChapter in await GetToc(book)) + { + var chapter = new Chapter + { Title = freedlitChapter.Header }; Config.Logger.LogInformation($"Загружаю главу {freedlitChapter.Header.CoverQuotes()}"); var chapterDoc = await GetChapter(book, freedlitChapter); - if (chapterDoc != default) { + if (chapterDoc is not null) + { chapter.Images = await GetImages(chapterDoc, SystemUrl); chapter.Content = chapterDoc.DocumentNode.InnerHtml; } @@ -105,18 +120,21 @@ private async Task> FillChapters(FreedlitBook book) { return result; } - private async Task GetChapter(FreedlitBook book, FreedlitChapter chapter) { - var response = await Config.Client.PostAsJsonAsync(SystemUrl.MakeRelativeUri("/reader/get-content"), new { book_id = book.Id, chapter_id = chapter.Id }); + private async Task GetChapter(FreedlitBook book, FreedlitChapter chapter) + { + var response = await Config.Client.PostAsJsonAsync(SystemUrl.MakeRelativeUri("/reader/get-content"), + new { book_id = book.Id, chapter_id = chapter.Id }); var fullChapter = await response.Content.ReadFromJsonAsync>(); - if (string.IsNullOrWhiteSpace(fullChapter.Success?.Content)) { - return default; - } - + if (string.IsNullOrWhiteSpace(fullChapter.Success?.Content)) return default; + var doc = fullChapter.Success.Content.AsHtmlDoc(); return doc.QuerySelector("body").InnerHtml.AsHtmlDoc(); } - private Task GetCover(FreedlitBook book) { - return !string.IsNullOrWhiteSpace(book.Cover) ? SaveImage(SystemUrl.MakeRelativeUri($"/storage/{book.Cover}")) : Task.FromResult(default(TempFile)); + private Task GetCover(FreedlitBook book) + { + return !string.IsNullOrWhiteSpace(book.Cover) + ? SaveImage(SystemUrl.MakeRelativeUri($"/storage/{book.Cover}")) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/Freedom/BookHamsterGetter.cs b/Core/Logic/Getters/Freedom/BookHamsterGetter.cs index 7eae0c7a..c208ec3d 100644 --- a/Core/Logic/Getters/Freedom/BookHamsterGetter.cs +++ b/Core/Logic/Getters/Freedom/BookHamsterGetter.cs @@ -3,6 +3,7 @@ namespace Core.Logic.Getters.Freedom; -public class BookHamsterGetter(BookGetterConfig config) : FreedomGetterBase(config) { +public class BookHamsterGetter(BookGetterConfig config) : FreedomGetterBase(config) +{ protected override Uri SystemUrl => new("https://bookhamster.ru/"); -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/Freedom/FreedomGetter.cs b/Core/Logic/Getters/Freedom/FreedomGetter.cs index 7cdd721e..69ade5e7 100644 --- a/Core/Logic/Getters/Freedom/FreedomGetter.cs +++ b/Core/Logic/Getters/Freedom/FreedomGetter.cs @@ -3,6 +3,7 @@ namespace Core.Logic.Getters.Freedom; -public class FreedomGetter(BookGetterConfig config) : FreedomGetterBase(config) { +public class FreedomGetter(BookGetterConfig config) : FreedomGetterBase(config) +{ protected override Uri SystemUrl => new("https://ifreedom.su/"); -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/Freedom/FreedomGetterBase.cs b/Core/Logic/Getters/Freedom/FreedomGetterBase.cs index f01fe9b6..f6b64868 100644 --- a/Core/Logic/Getters/Freedom/FreedomGetterBase.cs +++ b/Core/Logic/Getters/Freedom/FreedomGetterBase.cs @@ -10,83 +10,93 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters.Freedom; +namespace Core.Logic.Getters.Freedom; -public abstract class FreedomGetterBase(BookGetterConfig config) : GetterBase(config) { - public override async Task Get(Uri url) { +public abstract class FreedomGetterBase(BookGetterConfig config) : GetterBase(config) +{ + public override async Task Get(Uri url) + { url = await GetMainUrl(url); url = SystemUrl.MakeRelativeUri($"/ranobe/{GetId(url)}/"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - - var book = new Book(url) { + + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, url), Title = doc.GetTextBySelector("h1.entry-title"), Author = GetAuthor() }; - + return book; } - private async Task GetMainUrl(Uri url) { - if (url.GetSegment(1) == "ranobe") { - return url; - } + private async Task GetMainUrl(Uri url) + { + if (url.GetSegment(1) == "ranobe") return url; var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); return url.MakeRelativeUri(doc.QuerySelector("div.bun2 a").Attributes["href"].Value); } - private IEnumerable GetToc(HtmlDocument doc, Uri url) { + private IEnumerable GetToc(HtmlDocument doc, Uri url) + { var result = doc .QuerySelectorAll("div.li-col1-ranobe > a") .Select(a => new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.GetText())) .Reverse() .ToList(); - + return SliceToc(result, c => c.Title); } - - private async Task> FillChapters(HtmlDocument doc, Uri uri) { + + private async Task> FillChapters(HtmlDocument doc, Uri uri) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - foreach (var urlChapter in GetToc(doc, uri)) { - var chapter = new Chapter { + foreach (var urlChapter in GetToc(doc, uri)) + { + var chapter = new Chapter + { Title = urlChapter.Title }; Config.Logger.LogInformation($"Загружаю главу {urlChapter.Title.CoverQuotes()}"); var chapterDoc = await GetChapter(urlChapter.Url); - if (chapterDoc != default) { + if (chapterDoc is not null) + { chapter.Images = await GetImages(chapterDoc, urlChapter.Url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; } - + result.Add(chapter); } return result; } - private async Task GetChapter(Uri url) { + private async Task GetChapter(Uri url) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); var content = doc.QuerySelector("div.entry-content"); var notice = content.QuerySelector("div.single-notice"); - return notice?.GetText() == "Для чтения купите главу." ? - default : - content.InnerHtml.AsHtmlDoc().RemoveNodes("div[class*=adv]"); + return notice?.GetText() == "Для чтения купите главу." + ? default + : content.InnerHtml.AsHtmlDoc().RemoveNodes("div[class*=adv]"); } - private static Author GetAuthor() { + private static Author GetAuthor() + { return new Author("Ifreedom"); } - private Task GetCover(HtmlDocument doc, Uri uri) { + private Task GetCover(HtmlDocument doc, Uri uri) + { var imagePath = doc.QuerySelector("div.img-ranobe img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(uri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/GetterBase.cs b/Core/Logic/Getters/GetterBase.cs index 26f9e93b..4800331b 100644 --- a/Core/Logic/Getters/GetterBase.cs +++ b/Core/Logic/Getters/GetterBase.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using System.Web; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.Common; @@ -17,87 +18,108 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public abstract class GetterBase(BookGetterConfig config) : IDisposable { +public abstract class GetterBase(BookGetterConfig config) : IDisposable +{ protected readonly BookGetterConfig Config = config; protected abstract Uri SystemUrl { get; } - protected virtual string GetId(Uri url) => url.Segments.Last().Trim('/'); + public virtual void Dispose() + { + Config?.Dispose(); + } + + protected virtual string GetId(Uri url) + { + return url.Segments.Last().Trim('/'); + } - public virtual bool IsSameUrl(Uri url) { + public virtual bool IsSameUrl(Uri url) + { return SystemUrl.IsSameHost(url); } - protected virtual HttpRequestMessage GetImageRequestMessage(Uri uri) { + protected virtual HttpRequestMessage GetImageRequestMessage(Uri uri) + { var message = new HttpRequestMessage(HttpMethod.Get, uri); message.Version = Config.Client.DefaultRequestVersion; - foreach (var header in Config.Client.DefaultRequestHeaders) { + foreach (var header in Config.Client.DefaultRequestHeaders) message.Headers.TryAddWithoutValidation(header.Key, header.Value); - } - + return message; } - + /// - /// Получение изображения + /// Получение изображения /// /// /// - protected async Task SaveImage(Uri uri) { - try { + protected async Task SaveImage(Uri uri) + { + try + { Config.Logger.LogInformation($"Загружаю картинку {uri}"); using var response = await Config.Client.SendWithTriesAsync(() => GetImageRequestMessage(uri)); Config.Logger.LogInformation($"Загружена картинка {response.RequestMessage!.RequestUri}"); - if (response is not { StatusCode: HttpStatusCode.OK }) { + if (response is not { StatusCode: HttpStatusCode.OK }) + { Console.WriteLine(response.StatusCode); return default; } - + await using var stream = await response.Content.ReadAsStreamAsync(); var extension = Path.GetExtension(uri.GetFileName()); - var fullName = Guid.NewGuid() + (string.IsNullOrWhiteSpace(extension) ? ".jpg" : extension); - + var fullName = Guid.NewGuid() + (string.IsNullOrWhiteSpace(extension) ? ".jpg" : extension); + return await TempFile.Create(uri, Config.TempFolder.Path, fullName, stream); - } catch (Exception ex) { + } + catch (Exception ex) + { Console.WriteLine(ex); return default; } } - + /// - /// Загрузка изображений отдельной части + /// Загрузка изображений отдельной части /// /// /// - protected async Task> GetImages(HtmlDocument doc, Uri baseUri) { + protected async Task> GetImages(HtmlDocument doc, Uri baseUri) + { var images = new ConcurrentBag>(); var toRemove = new ConcurrentBag(); var tuples = doc.QuerySelectorAll("img, image").Select((img, i) => Tuple.Create(img, i)); - await Parallel.ForEachAsync(tuples, async (t, _) => { + await Parallel.ForEachAsync(tuples, async (t, _) => + { var img = t.Item1; - - if (Config.Options.NoImage) { + + if (Config.Options.NoImage) + { toRemove.Add(img); return; } - + var path = img.Attributes["src"]?.Value ?? img.Attributes["data-src"]?.Value; - if (string.IsNullOrWhiteSpace(path)) { + if (string.IsNullOrWhiteSpace(path)) + { toRemove.Add(img); return; } - if (!Uri.TryCreate(baseUri, path, out var uri)) { + if (!Uri.TryCreate(baseUri, path, out var uri)) + { toRemove.Add(img); return; } var image = await SaveImage(uri); - if (image == default) { + if (image is null) + { toRemove.Add(img); return; } @@ -107,72 +129,70 @@ await Parallel.ForEachAsync(tuples, async (t, _) => { images.Add(Tuple.Create(image, t.Item2)); }); - foreach (var node in toRemove) { - node.Remove(); - } + foreach (var node in toRemove) node.Remove(); return images.OrderBy(t => t.Item2).Select(t => t.Item1); } - public virtual Task Authorize() { + public virtual Task Authorize() + { return Task.CompletedTask; } - private static int GetIndexByName(ICollection toc, Func selector, string name) { + private static int GetIndexByName(ICollection toc, Func selector, string name) + { var result = toc .Select((chapter, index) => new { chapter, index }) - .FirstOrDefault(c => string.Equals(selector(c.chapter)?.Trim(), name, StringComparison.InvariantCultureIgnoreCase)) + .FirstOrDefault(c => + string.Equals(selector(c.chapter)?.Trim(), name, StringComparison.InvariantCultureIgnoreCase)) ?.index; - if (!result.HasValue) { - throw new Exception($"Глава с названием {name.CoverQuotes()} не найдена"); - } - + if (!result.HasValue) throw new Elib2EbookParseException($"Глава с названием {name.CoverQuotes()} не найдена"); + return result.Value + 1; } - - private static HtmlDocument SliceBook(EpubBook epubBook, EpubChapter epubChapter) { + + private static HtmlDocument SliceBook(EpubBook epubBook, EpubChapter epubChapter) + { var doc = new HtmlDocument(); - var startChapter = epubBook.Resources.Html.First(h => HttpUtility.UrlDecode(h.AbsolutePath) == HttpUtility.UrlDecode(epubChapter.AbsolutePath)); + var startChapter = epubBook.Resources.Html.First(h => + HttpUtility.UrlDecode(h.AbsolutePath) == HttpUtility.UrlDecode(epubChapter.AbsolutePath)); var startIndex = epubBook.Resources.Html.IndexOf(startChapter); - + var chapter = epubBook.Resources.Html[startIndex].TextContent.AsHtmlDoc(); - foreach (var node in chapter.QuerySelector("body").ChildNodes) { - doc.DocumentNode.AppendChild(node); - } - - for (var i = startIndex + 1; i < epubBook.Resources.Html.Count; i++) { + foreach (var node in chapter.QuerySelector("body").ChildNodes) doc.DocumentNode.AppendChild(node); + + for (var i = startIndex + 1; i < epubBook.Resources.Html.Count; i++) + { var chapterContent = epubBook.Resources.Html[i]; - if (chapterContent.AbsolutePath == epubChapter.Next?.AbsolutePath) { - break; - } - + if (chapterContent.AbsolutePath == epubChapter.Next?.AbsolutePath) break; + chapter = chapterContent.TextContent.AsHtmlDoc(); - foreach (var node in chapter.QuerySelector("body").ChildNodes) { - doc.DocumentNode.AppendChild(node); - } + foreach (var node in chapter.QuerySelector("body").ChildNodes) doc.DocumentNode.AppendChild(node); } - + return doc; } - - protected async Task> FillChaptersFromEpub(TempFile shortFile) { + + protected async Task> FillChaptersFromEpub(TempFile shortFile) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; await using var stream = shortFile.GetStream(); var epubBook = EpubReader.Read(stream, true, Encoding.UTF8); - if (epubBook.TableOfContents.Count > 0) { + if (epubBook.TableOfContents.Count > 0) + { var current = epubBook.TableOfContents.First(); - do { + do + { Config.Logger.LogInformation($"Загружаю главу {current.Title.CoverQuotes()}"); - var chapter = new Chapter { + var chapter = new Chapter + { Title = current.Title }; @@ -180,26 +200,27 @@ protected async Task> FillChaptersFromEpub(TempFile shortFi chapter.Images = await GetImages(content, epubBook); chapter.Content = content.DocumentNode.RemoveNodes("h1, h2, h3").InnerHtml; result.Add(chapter); - } while ((current = current.Next) != default); - } else { + } while ((current = current.Next) is not null); + } + else + { Config.Logger.LogInformation($"Загружаю главу {epubBook.Title.CoverQuotes()}"); var sb = new StringBuilder(); - foreach (var textFile in epubBook.SpecialResources.HtmlInReadingOrder) { + foreach (var textFile in epubBook.SpecialResources.HtmlInReadingOrder) + { var textFileDoc = textFile.TextContent.AsHtmlDoc(); var body = textFileDoc.DocumentNode.QuerySelector("body"); - if (body != default) { - textFileDoc = body.InnerHtml.AsHtmlDoc(); - } + if (body is not null) textFileDoc = body.InnerHtml.AsHtmlDoc(); sb.Append(textFileDoc.DocumentNode.InnerHtml); - } - - var chapter = new Chapter { + + var chapter = new Chapter + { Title = epubBook.Title }; - + var content = sb.AsHtmlDoc(); chapter.Images = await GetImages(content, epubBook); chapter.Content = content.DocumentNode.InnerHtml; @@ -208,27 +229,32 @@ protected async Task> FillChaptersFromEpub(TempFile shortFi return result; } - - private async Task> GetImages(HtmlDocument doc, EpubBook book) { + + private async Task> GetImages(HtmlDocument doc, EpubBook book) + { var images = new List(); - foreach (var img in doc.QuerySelectorAll("img")) { + foreach (var img in doc.QuerySelectorAll("img")) + { var path = img.Attributes["src"]?.Value; - if (string.IsNullOrWhiteSpace(path)) { + if (string.IsNullOrWhiteSpace(path)) + { img.Remove(); continue; } var t = book.Resources.Images.FirstOrDefault(i => i.AbsolutePath.EndsWith(path.Trim('.'))); - if (t == default) { + if (t is null) + { img.Remove(); continue; } - if (t.Content == null || t.Content.Length == 0) { + if (t.Content == null || t.Content.Length == 0) + { img.Remove(); continue; } - + var image = await TempFile.Create(null, Config.TempFolder.Path, t.Href, t.Content); img.Attributes["src"].Value = image.FullName; images.Add(image); @@ -237,118 +263,122 @@ private async Task> GetImages(HtmlDocument doc, EpubBook b return images; } - protected IEnumerable SliceToc(ICollection toc, Func selector) { + protected IEnumerable SliceToc(ICollection toc, Func selector) + { var start = Config.Options.Start; var end = Config.Options.End; var startName = (Config.Options.StartName ?? string.Empty).Trim(); var endName = (Config.Options.EndName ?? string.Empty).Trim(); - if (!string.IsNullOrWhiteSpace(startName)) { - start = GetIndexByName(toc, selector, startName); - } - - if (!string.IsNullOrWhiteSpace(endName)) { - end = GetIndexByName(toc, selector, endName); - } + if (!string.IsNullOrWhiteSpace(startName)) start = GetIndexByName(toc, selector, startName); + + if (!string.IsNullOrWhiteSpace(endName)) end = GetIndexByName(toc, selector, endName); + + if (start.HasValue && end.HasValue) + { + var startIndex = start.Value >= 0 ? start.Value : toc.Count + start.Value + 1; + var endIndex = end.Value >= 0 ? end.Value : toc.Count + end.Value; - if (start.HasValue && end.HasValue) { - var startIndex = start.Value >= 0 ? start.Value : toc.Count + start.Value + 1; - var endIndex = end.Value >= 0 ? end.Value : toc.Count + end.Value; - return toc.Skip(startIndex - 1).Take(endIndex - startIndex + 1); } - - if (start.HasValue) { - var startIndex = start.Value >= 0 ? start.Value : toc.Count + start.Value + 1; + + if (start.HasValue) + { + var startIndex = start.Value >= 0 ? start.Value : toc.Count + start.Value + 1; return toc.Skip(startIndex - 1); } - - if (end.HasValue) { - var endIndex = end.Value >= 0 ? end.Value : toc.Count + end.Value; + + if (end.HasValue) + { + var endIndex = end.Value >= 0 ? end.Value : toc.Count + end.Value; return toc.Take(endIndex); } - + return toc; } - private static HtmlDocument GetContent(EpubBook epubBook, EpubChapter epubChapter) { + private static HtmlDocument GetContent(EpubBook epubBook, EpubChapter epubChapter) + { var chapter = new HtmlDocument(); var book = SliceBook(epubBook, epubChapter); - var startNode = string.IsNullOrWhiteSpace(epubChapter.HashLocation) ? book.DocumentNode : book.QuerySelector($"#{epubChapter.HashLocation}"); + var startNode = string.IsNullOrWhiteSpace(epubChapter.HashLocation) + ? book.DocumentNode + : book.QuerySelector($"#{epubChapter.HashLocation}"); var needStop = false; var layer = (startNode.ParentNode ?? startNode).CloneNode(false); - do { + do + { var clone = CloneNode(startNode, epubChapter.Next?.HashLocation, ref needStop); - if (clone != default) { - layer.AppendChild(clone); - } - - do { - if (startNode.NextSibling == default) { - if (startNode.ParentNode == default || startNode.ParentNode.Name == "#document") { + if (clone is not null) layer.AppendChild(clone); + + do + { + if (startNode.NextSibling is null) + { + if (startNode.ParentNode is null || startNode.ParentNode.Name == "#document") + { startNode = default; - } else { + } + else + { var layerClone = layer.CloneNode(true); layer = startNode.ParentNode.CloneNode(false); layer.AppendChild(layerClone); startNode = startNode.ParentNode; } - } else { + } + else + { startNode = startNode.NextSibling; break; } - } while (startNode != default); - } while (startNode != default && !needStop); + } while (startNode is not null); + } while (startNode is not null && !needStop); chapter.DocumentNode.AppendChild(layer); return chapter; } - private static HtmlNode CloneNode(HtmlNode node, string stopId, ref bool needStop) { - if (!string.IsNullOrWhiteSpace(stopId) && node.Name != "#document" && node.Id == stopId) { + private static HtmlNode CloneNode(HtmlNode node, string stopId, ref bool needStop) + { + if (!string.IsNullOrWhiteSpace(stopId) && node.Name != "#document" && node.Id == stopId) + { needStop = true; return default; } - - if (!node.HasChildNodes) { - return node.CloneNode(true); - } + if (!node.HasChildNodes) return node.CloneNode(true); var parent = node.CloneNode(false); - foreach (var child in node.ChildNodes) { + foreach (var child in node.ChildNodes) + { var clone = CloneNode(child, stopId, ref needStop); - if (clone != default) { - parent.ChildNodes.Add(clone); - } + if (clone is not null) parent.ChildNodes.Add(clone); - if (needStop) { - return parent; - } + if (needStop) return parent; } return parent; } public abstract Task Get(Uri url); - - public virtual Task Init() { + + public virtual Task Init() + { Config.Client.DefaultRequestVersion = HttpVersion.Version20; - Config.Client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15"); - Config.Client.DefaultRequestHeaders.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); + Config.Client.DefaultRequestHeaders.Add("User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15"); + Config.Client.DefaultRequestHeaders.Add("Accept", + "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); Config.Client.DefaultRequestHeaders.Add("Accept-Language", "ru"); Config.Client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate, br"); - - return Task.CompletedTask; - } - public virtual void Dispose() { - Config?.Dispose(); + return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/HogwartsNetGetter.cs b/Core/Logic/Getters/HogwartsNetGetter.cs index accbf4e4..c1b4cf97 100644 --- a/Core/Logic/Getters/HogwartsNetGetter.cs +++ b/Core/Logic/Getters/HogwartsNetGetter.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.Common; @@ -12,130 +13,139 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class HogwartsNetGetter(BookGetterConfig config) : GetterBase(config) { +public class HogwartsNetGetter(BookGetterConfig config) : GetterBase(config) +{ + private static readonly Encoding _encoding = Encoding.GetEncoding(1251); protected override Uri SystemUrl => new("https://hogwartsnet.ru/"); - - private static Encoding _encoding = Encoding.GetEncoding(1251); - protected override string GetId(Uri url) => url.GetQueryParameter("fid"); + protected override string GetId(Uri url) + { + return url.GetQueryParameter("fid"); + } - public override async Task Authorize() { - if (!Config.HasCredentials) { - return; - } + public override async Task Authorize() + { + if (!Config.HasCredentials) return; - var payload = new Dictionary { + var payload = new Dictionary + { ["id"] = Config.Options.Login, ["pwd"] = Config.Options.Password }; - var doc = await Config.Client.PostHtmlDocWithTriesAsync("https://hogwartsnet.ru/mfanf/member.php".AsUri(), new FormUrlEncodedContent(payload), _encoding); + var doc = await Config.Client.PostHtmlDocWithTriesAsync("https://hogwartsnet.ru/mfanf/member.php".AsUri(), + new FormUrlEncodedContent(payload), _encoding); var anketa = doc.QuerySelector("tr.top_fanf a[href*=anketa.php]"); - - if (anketa == default) { - throw new Exception("Не удалось авторизоваться. Проверьте правильность ID/пароля"); - } + + if (anketa is null) + throw new Elib2EbookAuthException("Не удалось авторизоваться. Проверьте правильность ID/пароля"); Config.Logger.LogInformation("Успешно авторизовались"); } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { url = $"https://hogwartsnet.ru/mfanf/ffshowfic.php?fid={GetId(url)}".AsUri(); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url, _encoding); - var book = new Book(url) { + var book = new Book(url) + { Chapters = await FillChapters(doc, url), Title = doc.GetTextBySelector("td.fichead a[href*=ffshowfic.php]"), Author = GetAuthor(doc, url), Annotation = doc.QuerySelector("td.fichead div")?.InnerHtml, Seria = GetSeria(doc, url) }; - + return book; } - private async Task> FillChapters(HtmlDocument doc, Uri url) { + private async Task> FillChapters(HtmlDocument doc, Uri url) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - foreach (var urlChapter in GetToc(doc, url)) { + foreach (var urlChapter in GetToc(doc, url)) + { Config.Logger.LogInformation($"Загружаю главу {urlChapter.Title.CoverQuotes()}"); - var chapter = new Chapter { + var chapter = new Chapter + { Title = urlChapter.Title }; var chapterDoc = await GetChapter(urlChapter.Url); - if (chapterDoc != default) { + if (chapterDoc is not null) + { chapter.Images = await GetImages(chapterDoc, urlChapter.Url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; } - + result.Add(chapter); } return result; } - private async Task GetChapter(Uri url) { + private async Task GetChapter(Uri url) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(url, _encoding); const string mature = "Текст фанфика доступен только зарегистрированным пользователям старше 18 лет"; - if (doc.ParsedText.Contains(mature)) { - throw new Exception($"{mature}. Добавьте авторизацию по ID и паролю."); - } + if (doc.ParsedText.Contains(mature)) + throw new Elib2EbookParseException($"{mature}. Добавьте авторизацию по ID и паролю."); var text = new StringBuilder(); - using var sr = new StringReader(doc.QuerySelector("#chap_text").RemoveNodes("center, div[id*=yandex]").InnerText.HtmlDecode()); - while (true) { + using var sr = new StringReader(doc.QuerySelector("#chap_text").RemoveNodes("center, div[id*=yandex]").InnerText + .HtmlDecode()); + while (true) + { var line = await sr.ReadLineAsync(); - if (line == null) { - break; - } + if (line == null) break; + + if (string.IsNullOrWhiteSpace(line)) continue; - if (string.IsNullOrWhiteSpace(line)) { - continue; - } - text.Append(line.HtmlEncode().CoverTag("p")); } return text.AsHtmlDoc(); } - private IEnumerable GetToc(HtmlDocument doc, Uri url) { + private IEnumerable GetToc(HtmlDocument doc, Uri url) + { var result = new List(); - - foreach (var option in doc.QuerySelectorAll("td.fichead_chapters select[name=chapter] option")) { + + foreach (var option in doc.QuerySelectorAll("td.fichead_chapters select[name=chapter] option")) + { var chapterId = option.Attributes["value"].Value; var chapterUrl = url.AppendQueryParameter("chapter", chapterId); var chapterTitle = option.GetText().StartsWith("глава") ? $"Глава {chapterId}" : option.GetText(); - + result.Add(new UrlChapter(chapterUrl, chapterTitle)); } return SliceToc(result, c => c.Title); } - private static Seria GetSeria(HtmlDocument doc, Uri url) { + private static Seria GetSeria(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("td.fichead a[href*=findex.php]"); - if (a != default) { - return new Seria { + if (a is not null) + return new Seria + { Name = a.GetText(), Url = url.MakeRelativeUri(a.Attributes["href"].Value) }; - } return default; } - private static Author GetAuthor(HtmlDocument doc, Uri url) { + private static Author GetAuthor(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("td.fichead a[href*=member.php]"); - return a != default ? - new Author(a.GetText().ReplaceNewLine(), url.MakeRelativeUri(a.Attributes["href"].Value)) : - new Author("HogwartsNet"); + return a is not null + ? new Author(a.GetText().ReplaceNewLine(), url.MakeRelativeUri(a.Attributes["href"].Value)) + : new Author("HogwartsNet"); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/HotNovelPub/EzNovelsGetter.cs b/Core/Logic/Getters/HotNovelPub/EzNovelsGetter.cs index badb19e9..09ec379b 100644 --- a/Core/Logic/Getters/HotNovelPub/EzNovelsGetter.cs +++ b/Core/Logic/Getters/HotNovelPub/EzNovelsGetter.cs @@ -1,9 +1,10 @@ using System; using Core.Configs; -namespace Core.Logic.Getters.HotNovelPub; +namespace Core.Logic.Getters.HotNovelPub; -public class EzNovelsGetter(BookGetterConfig config) : HotNovelPubGetterBase(config) { +public class EzNovelsGetter(BookGetterConfig config) : HotNovelPubGetterBase(config) +{ protected override Uri SystemUrl => new("https://eznovels.com"); protected override string Lang => "ru"; -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/HotNovelPub/HotNovelPubGetter.cs b/Core/Logic/Getters/HotNovelPub/HotNovelPubGetter.cs index 5df959e3..7d37af9b 100644 --- a/Core/Logic/Getters/HotNovelPub/HotNovelPubGetter.cs +++ b/Core/Logic/Getters/HotNovelPub/HotNovelPubGetter.cs @@ -1,9 +1,10 @@ using System; using Core.Configs; -namespace Core.Logic.Getters.HotNovelPub; +namespace Core.Logic.Getters.HotNovelPub; -public class HotNovelPubGetter(BookGetterConfig config) : HotNovelPubGetterBase(config) { +public class HotNovelPubGetter(BookGetterConfig config) : HotNovelPubGetterBase(config) +{ protected override Uri SystemUrl => new("https://hotnovelpub.com/"); protected override string Lang => "en"; -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/HotNovelPub/HotNovelPubGetterBase.cs b/Core/Logic/Getters/HotNovelPub/HotNovelPubGetterBase.cs index 174a2568..acc0fa2a 100644 --- a/Core/Logic/Getters/HotNovelPub/HotNovelPubGetterBase.cs +++ b/Core/Logic/Getters/HotNovelPub/HotNovelPubGetterBase.cs @@ -12,25 +12,29 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters.HotNovelPub; +namespace Core.Logic.Getters.HotNovelPub; -public abstract class HotNovelPubGetterBase(BookGetterConfig config) : GetterBase(config) { +public abstract class HotNovelPubGetterBase(BookGetterConfig config) : GetterBase(config) +{ protected abstract string Lang { get; } private Uri _apiUrl => new($"https://api.{SystemUrl.Host}/"); - public override Task Init() { + public override Task Init() + { base.Init(); Config.Client.DefaultRequestHeaders.Add("lang", Lang); Config.Client.DefaultRequestHeaders.Add("Priority", "u=3, i"); return Task.CompletedTask; } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { url = await GetMainUrl(url); var bookApi = await GetBook(GetId(url)); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(bookApi.Book), Chapters = await FillChapters(bookApi.Chapters), Title = bookApi.Book.Name, @@ -42,27 +46,30 @@ public override async Task Get(Uri url) { return book; } - private async Task GetMainUrl(Uri url) { - if (GetId(url).StartsWith("chapter-")) { + private async Task GetMainUrl(Uri url) + { + if (GetId(url).StartsWith("chapter-")) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); var a = doc.QuerySelectorAll("li.breadcrumb-item")[1].QuerySelector("a[href]"); url = SystemUrl.MakeRelativeUri(a.Attributes["href"].Value); } - + return url; } - private async Task> FillChapters(ICollection toc) { + private async Task> FillChapters(ICollection toc) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } - - foreach (var ezChapter in SliceToc(toc, c => c.Title)) { + if (Config.Options.NoChapters) return result; + + foreach (var ezChapter in SliceToc(toc, c => c.Title)) + { var title = ezChapter.Title.Trim(); Config.Logger.LogInformation($"Загружаю главу {title.CoverQuotes()}"); - - var chapter = new Chapter { + + var chapter = new Chapter + { Title = title }; @@ -72,37 +79,45 @@ private async Task> FillChapters(ICollection GetChapter(HotNovelPubChapter ezChapter) { + private async Task GetChapter(HotNovelPubChapter ezChapter) + { var doc = Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri(ezChapter.Slug)); - var additional = Config.Client.GetFromJsonAsync>(SystemUrl.MakeRelativeUri($"/server/getContent?slug={ezChapter.Slug}")); - + var additional = + Config.Client.GetFromJsonAsync>( + SystemUrl.MakeRelativeUri($"/server/getContent?slug={ezChapter.Slug}")); + const string watermark = ".copy right hot novel pub"; - var sb = new StringBuilder((await doc).QuerySelector("#content-item").InnerHtml.HtmlDecode().CleanInvalidXmlChars().Replace(watermark, string.Empty)); + var sb = new StringBuilder((await doc).QuerySelector("#content-item").InnerHtml.HtmlDecode() + .CleanInvalidXmlChars().Replace(watermark, string.Empty)); - - foreach (var row in (await additional)!.Data) { - foreach (var p in row.Split("\n", StringSplitOptions.RemoveEmptyEntries)) { - sb.Append(p.HtmlDecode().CleanInvalidXmlChars().Replace(watermark, string.Empty).Trim().CoverTag("p")); - } - } + foreach (var row in (await additional)!.Data) + foreach (var p in row.Split("\n", StringSplitOptions.RemoveEmptyEntries)) + sb.Append(p.HtmlDecode().CleanInvalidXmlChars().Replace(watermark, string.Empty).Trim().CoverTag("p")); return sb.AsHtmlDoc(); } - private Author GetAuthor(HotNovelPubBook book) { + private Author GetAuthor(HotNovelPubBook book) + { return new Author(book.Authorize.Name, SystemUrl.MakeRelativeUri(book.Authorize.Slug)); } - private Task GetCover(HotNovelPubBook book) { - return !string.IsNullOrWhiteSpace(book.Image) ? SaveImage(SystemUrl.MakeRelativeUri(book.Image)) : Task.FromResult(default(TempFile)); + private Task GetCover(HotNovelPubBook book) + { + return !string.IsNullOrWhiteSpace(book.Image) + ? SaveImage(SystemUrl.MakeRelativeUri(book.Image)) + : Task.FromResult(default(TempFile)); } - private async Task GetBook(string id) { - var response = await Config.Client.GetFromJsonAsync>(_apiUrl.MakeRelativeUri($"/book/{id}")); + private async Task GetBook(string id) + { + var response = + await Config.Client.GetFromJsonAsync>( + _apiUrl.MakeRelativeUri($"/book/{id}")); return response!.Data; } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/HotNovelPub/LaNovelsGetter.cs b/Core/Logic/Getters/HotNovelPub/LaNovelsGetter.cs index 18c38533..43fa2586 100644 --- a/Core/Logic/Getters/HotNovelPub/LaNovelsGetter.cs +++ b/Core/Logic/Getters/HotNovelPub/LaNovelsGetter.cs @@ -1,9 +1,10 @@ using System; using Core.Configs; -namespace Core.Logic.Getters.HotNovelPub; +namespace Core.Logic.Getters.HotNovelPub; -public class LaNovelsGetter(BookGetterConfig config) : HotNovelPubGetterBase(config) { +public class LaNovelsGetter(BookGetterConfig config) : HotNovelPubGetterBase(config) +{ protected override Uri SystemUrl => new("https://lanovels.com/"); protected override string Lang => "pt"; -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/HotNovelPub/LightNovelDailyGetter.cs b/Core/Logic/Getters/HotNovelPub/LightNovelDailyGetter.cs index 027569de..ff8b8ea3 100644 --- a/Core/Logic/Getters/HotNovelPub/LightNovelDailyGetter.cs +++ b/Core/Logic/Getters/HotNovelPub/LightNovelDailyGetter.cs @@ -1,9 +1,10 @@ using System; using Core.Configs; -namespace Core.Logic.Getters.HotNovelPub; +namespace Core.Logic.Getters.HotNovelPub; -public class LightNovelDailyGetter(BookGetterConfig config) : HotNovelPubGetterBase(config) { +public class LightNovelDailyGetter(BookGetterConfig config) : HotNovelPubGetterBase(config) +{ protected override Uri SystemUrl => new("https://lightnoveldaily.com"); protected override string Lang => "es"; -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/HubBookGetter.cs b/Core/Logic/Getters/HubBookGetter.cs index 7fc09c10..602732da 100644 --- a/Core/Logic/Getters/HubBookGetter.cs +++ b/Core/Logic/Getters/HubBookGetter.cs @@ -10,40 +10,46 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class HubBookGetter(BookGetterConfig config) : GetterBase(config) { +public class HubBookGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://hub-book.com/"); - protected override string GetId(Uri url) => url.GetSegment(2); + protected override string GetId(Uri url) + { + return url.GetSegment(2); + } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { url = SystemUrl.MakeRelativeUri($"/books/{GetId(url)}"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); var author = GetAuthor(doc, url); var title = doc.GetTextBySelector("h1").Replace(author.Name, string.Empty).Trim('-', ' '); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, url, title), Title = title, Author = author, - Annotation = doc.QuerySelector("div.b-book-desc div.more_text")?.InnerHtml, + Annotation = doc.QuerySelector("div.b-book-desc div.more_text")?.InnerHtml }; - + return book; } - private async Task> FillChapters(HtmlDocument doc, Uri url, string title) { - if (Config.Options.NoChapters) { - return []; - } - + private async Task> FillChapters(HtmlDocument doc, Uri url, string title) + { + if (Config.Options.NoChapters) return []; + var pages = int.Parse(doc.GetTextBySelector("span[itemprop=numberOfPages]")); var text = new StringBuilder(); - for (var i = 1; i <= pages; i++) { + for (var i = 1; i <= pages; i++) + { Config.Logger.LogInformation($"Получаю страницу {i}/{pages}"); doc = await Config.Client.GetHtmlDocWithTriesAsync(url.AppendSegment($"/toread/page-{i}")); text.Append(doc.QuerySelector("div.b-reader-text__container").InnerHtml.HtmlDecode()); @@ -51,7 +57,7 @@ private async Task> FillChapters(HtmlDocument doc, Uri url, var chapter = new Chapter(); var chapterDoc = text.AsHtmlDoc(); - + chapter.Title = title; chapter.Images = await GetImages(chapterDoc, url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -59,13 +65,17 @@ private async Task> FillChapters(HtmlDocument doc, Uri url, return new[] { chapter }; } - private Task GetCover(HtmlDocument doc, Uri uri) { + private Task GetCover(HtmlDocument doc, Uri uri) + { var imagePath = doc.QuerySelector("div.b-book-image img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(uri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } - - private static Author GetAuthor(HtmlDocument doc, Uri uri) { + + private static Author GetAuthor(HtmlDocument doc, Uri uri) + { var a = doc.QuerySelector("a.b-book-user__name"); return new Author(a.GetText(), uri.MakeRelativeUri(a.Attributes["href"].Value)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/IGramGetter.cs b/Core/Logic/Getters/IGramGetter.cs index b8f30561..b687e30c 100644 --- a/Core/Logic/Getters/IGramGetter.cs +++ b/Core/Logic/Getters/IGramGetter.cs @@ -6,6 +6,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.Common; @@ -14,33 +15,39 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class IGramGetter(BookGetterConfig config) : GetterBase(config) { +public class IGramGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://i-gram.ru/"); - protected override string GetId(Uri url) => url.GetSegment(2); + protected override string GetId(Uri url) + { + return url.GetSegment(2); + } - public override async Task Authorize() { - if (!Config.HasCredentials) { - return; - } + public override async Task Authorize() + { + if (!Config.HasCredentials) return; var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri("/account/login/")); var token = doc.QuerySelector("input[name=csrfmiddlewaretoken]")?.Attributes["value"]?.Value; - + Config.Client.DefaultRequestHeaders.Add("Referer", SystemUrl.MakeRelativeUri("/account/login/").ToString()); - using var post = await Config.Client.PostAsync(SystemUrl.MakeRelativeUri("/account/login/"), GenerateAuthData(token)); - var checkLogin = await Config.Client.GetFromJsonAsync(SystemUrl.MakeRelativeUri("/api/v2/users/current/")); - if (checkLogin?.Id > 0) { + using var post = + await Config.Client.PostAsync(SystemUrl.MakeRelativeUri("/account/login/"), GenerateAuthData(token)); + var checkLogin = + await Config.Client.GetFromJsonAsync(SystemUrl.MakeRelativeUri("/api/v2/users/current/")); + if (checkLogin?.Id > 0) Config.Logger.LogInformation("Успешно авторизовались"); - } else { - throw new Exception("Не удалось авторизоваться"); - } + else + throw new Elib2EbookAuthException("Не удалось авторизоваться"); } - private FormUrlEncodedContent GenerateAuthData(string token) { - var data = new Dictionary { + private FormUrlEncodedContent GenerateAuthData(string token) + { + var data = new Dictionary + { ["csrfmiddlewaretoken"] = token, ["remember"] = "checked", ["login"] = Config.Options.Login, @@ -50,11 +57,13 @@ private FormUrlEncodedContent GenerateAuthData(string token) { return new FormUrlEncodedContent(data); } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { url = SystemUrl.MakeRelativeUri($"/b/{GetId(url)}"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - - var book = new Book(url) { + + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(url, doc), Title = doc.GetTextBySelector("h1[itemprop=name]"), @@ -62,64 +71,74 @@ public override async Task Get(Uri url) { Annotation = doc.QuerySelector("div[itemprop=description] p")?.InnerHtml, Seria = GetSeria(doc, url) }; - + return book; } - private static Seria GetSeria(HtmlDocument doc, Uri url) { + private static Seria GetSeria(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("div.bk-genre a[href^=/b/cycle/]"); - if (a != default) { - return new Seria { + if (a is not null) + return new Seria + { Name = a.GetText(), Url = url.MakeRelativeUri(a.Attributes["href"].Value) }; - } return default; } - private async Task> FillChapters(Uri uri, HtmlDocument doc) { + private async Task> FillChapters(Uri uri, HtmlDocument doc) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - foreach (var bookChapter in await GetToc(doc)) { + foreach (var bookChapter in await GetToc(doc)) + { Config.Logger.LogInformation($"Загружаю главу {bookChapter.Title.CoverQuotes()}"); - var chapter = new Chapter { + var chapter = new Chapter + { Title = bookChapter.Title }; - - if (!string.IsNullOrWhiteSpace(bookChapter.Text)) { + + if (!string.IsNullOrWhiteSpace(bookChapter.Text)) + { var chapterDoc = bookChapter.Text.AsHtmlDoc(); chapter.Images = await GetImages(chapterDoc, uri); chapter.Content = chapterDoc.DocumentNode.InnerHtml; } - + result.Add(chapter); } return result; } - private async Task> GetToc(HtmlDocument doc) { - var response = await Config.Client.GetAsync(SystemUrl.MakeRelativeUri($"/api/v1/book/{GetInternalId(doc)}/chapters")); + private async Task> GetToc(HtmlDocument doc) + { + var response = + await Config.Client.GetAsync(SystemUrl.MakeRelativeUri($"/api/v1/book/{GetInternalId(doc)}/chapters")); var content = await response.Content.ReadAsStringAsync(); return SliceToc(content.Deserialize>().OrderBy(c => c.Id).ToList(), c => c.Title); } - private static Author GetAuthor(HtmlDocument doc, Uri uri) { + private static Author GetAuthor(HtmlDocument doc, Uri uri) + { var a = doc.QuerySelector("div.bk-author a"); return new Author(a.GetText(), uri.MakeRelativeUri(a.Attributes["href"].Value)); } - private static string GetInternalId(HtmlDocument doc) { + private static string GetInternalId(HtmlDocument doc) + { return Regex.Match(doc.DocumentNode.InnerHtml, @"bookId: '(?\d+)'").Groups["id"].Value; } - private Task GetCover(HtmlDocument doc, Uri uri) { + private Task GetCover(HtmlDocument doc, Uri uri) + { var imagePath = doc.QuerySelector("div.bk-img img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(uri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/JaomixGetter.cs b/Core/Logic/Getters/JaomixGetter.cs index 34d947d3..1f4d304d 100644 --- a/Core/Logic/Getters/JaomixGetter.cs +++ b/Core/Logic/Getters/JaomixGetter.cs @@ -11,34 +11,40 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class JaomixGetter(BookGetterConfig config) : GetterBase(config) { +public class JaomixGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://jaomix.ru/"); - protected override string GetId(Uri url) => url.GetSegment(1); + protected override string GetId(Uri url) + { + return url.GetSegment(1); + } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { url = SystemUrl.MakeRelativeUri($"/{GetId(url)}/"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, url), Title = doc.GetTextBySelector("h1"), Author = new Author("Jaomix") }; - + return book; } - - private async Task> FillChapters(HtmlDocument doc, Uri url) { + + private async Task> FillChapters(HtmlDocument doc, Uri url) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - foreach (var jaomixChapter in GetToc(doc, url)) { + foreach (var jaomixChapter in GetToc(doc, url)) + { Config.Logger.LogInformation($"Загружаю главу {jaomixChapter.Title.CoverQuotes()}"); var chapter = new Chapter(); var chapterDoc = await GetChapter(jaomixChapter.Url); @@ -53,39 +59,49 @@ private async Task> FillChapters(HtmlDocument doc, Uri url) return result; } - private async Task GetChapter(Uri url) { + private async Task GetChapter(Uri url) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - while (doc.QuerySelector("div.themeform div.h-captcha, div.themeform div.but-captcha") != null) { - Config.Logger.LogInformation($"Обнаружена капча. Перейдите по ссылке {url}, введите капчу и нажмите Enter..."); + while (doc.QuerySelector("div.themeform div.h-captcha, div.themeform div.but-captcha") != null) + { + Config.Logger.LogInformation( + $"Обнаружена капча. Перейдите по ссылке {url}, введите капчу и нажмите Enter..."); Console.Read(); doc = await Config.Client.GetHtmlDocWithTriesAsync(url); } - + var sb = new StringBuilder(); - - foreach (var node in doc.QuerySelector("div.themeform").ChildNodes) { - if (node.Name != "br" && node.Name != "script" && !string.IsNullOrWhiteSpace(node.InnerHtml) && node.Attributes["class"]?.Value?.Contains("adblock-service") == null) { + + foreach (var node in doc.QuerySelector("div.themeform").ChildNodes) + if (node.Name != "br" && node.Name != "script" && !string.IsNullOrWhiteSpace(node.InnerHtml) && + node.Attributes["class"]?.Value?.Contains("adblock-service") == null) + { var tag = node.Name == "#text" ? "p" : node.Name; sb.Append(node.InnerHtml.HtmlDecode().CoverTag(tag)); } - } return sb.AsHtmlDoc(); } - private IEnumerable GetToc(HtmlDocument doc, Uri url) { + private IEnumerable GetToc(HtmlDocument doc, Uri url) + { var chapters = ParseChapters(doc, url).ToList(); Config.Logger.LogInformation($"Получено {chapters.Count} глав"); - + return SliceToc(chapters, c => c.Title); } - - private Task GetCover(HtmlDocument doc, Uri bookUri) { + + private Task GetCover(HtmlDocument doc, Uri bookUri) + { var imagePath = doc.QuerySelector("div.img-book img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(bookUri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } - private static IEnumerable ParseChapters(HtmlDocument doc, Uri url) { - return doc.QuerySelectorAll("form.download-chapter .hiddenstab .flex-dow-txt a").Select(a => new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.InnerText.Trim())).Reverse(); + private static IEnumerable ParseChapters(HtmlDocument doc, Uri url) + { + return doc.QuerySelectorAll("form.download-chapter .hiddenstab .flex-dow-txt a").Select(a => + new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.InnerText.Trim())).Reverse(); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/LibSocial/HentaiLibGetter.cs b/Core/Logic/Getters/LibSocial/HentaiLibGetter.cs index bc788384..f9780e4e 100644 --- a/Core/Logic/Getters/LibSocial/HentaiLibGetter.cs +++ b/Core/Logic/Getters/LibSocial/HentaiLibGetter.cs @@ -3,9 +3,10 @@ namespace Core.Logic.Getters.LibSocial; -public class HentaiLibGetter(BookGetterConfig config) : MangaLibGetterBase(config) { +public class HentaiLibGetter(BookGetterConfig config) : MangaLibGetterBase(config) +{ protected override Uri ImagesHost => new("https://img3.imglib.info/"); - + protected override Uri SystemUrl => new("https://hentailib.me/"); protected override int SiteId => 4; diff --git a/Core/Logic/Getters/LibSocial/MangaLibGetter.cs b/Core/Logic/Getters/LibSocial/MangaLibGetter.cs index 0c4c3d1b..d2a09295 100644 --- a/Core/Logic/Getters/LibSocial/MangaLibGetter.cs +++ b/Core/Logic/Getters/LibSocial/MangaLibGetter.cs @@ -3,7 +3,8 @@ namespace Core.Logic.Getters.LibSocial; -public class MangaLibGetter(BookGetterConfig config) : MangaLibGetterBase(config) { +public class MangaLibGetter(BookGetterConfig config) : MangaLibGetterBase(config) +{ protected override Uri ImagesHost => new("https://img33.imgslib.link/"); protected override Uri SystemUrl => new("https://mangalib.me/"); diff --git a/Core/Logic/Getters/LibSocial/MangaLibGetterBase.cs b/Core/Logic/Getters/LibSocial/MangaLibGetterBase.cs index 5a0d056e..514375a0 100644 --- a/Core/Logic/Getters/LibSocial/MangaLibGetterBase.cs +++ b/Core/Logic/Getters/LibSocial/MangaLibGetterBase.cs @@ -4,17 +4,20 @@ using Core.Types.SocialLib; using HtmlAgilityPack; -namespace Core.Logic.Getters.LibSocial; +namespace Core.Logic.Getters.LibSocial; -public abstract class MangaLibGetterBase(BookGetterConfig config) : NewLibSocialGetterBase(config) { - protected override HtmlDocument ResponseToHtmlDoc(SocialLibBookChapter chapterResponse) { +public abstract class MangaLibGetterBase(BookGetterConfig config) : NewLibSocialGetterBase(config) +{ + protected override HtmlDocument ResponseToHtmlDoc(SocialLibBookChapter chapterResponse) + { var sb = new StringBuilder(); - foreach (var page in chapterResponse.Pages) { + foreach (var page in chapterResponse.Pages) + { var url = SystemUrl.MakeRelativeUri(page.Url.TrimStart('/')); sb.Append($""); } return sb.AsHtmlDoc(); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/LibSocial/MangaLibOrgGetter.cs b/Core/Logic/Getters/LibSocial/MangaLibOrgGetter.cs index 0bdd8ce6..c4ebd36c 100644 --- a/Core/Logic/Getters/LibSocial/MangaLibOrgGetter.cs +++ b/Core/Logic/Getters/LibSocial/MangaLibOrgGetter.cs @@ -3,7 +3,8 @@ namespace Core.Logic.Getters.LibSocial; -public class MangaLibOrgGetter(BookGetterConfig config) : MangaLibGetterBase(config) { +public class MangaLibOrgGetter(BookGetterConfig config) : MangaLibGetterBase(config) +{ protected override Uri ImagesHost => new("https://img33.imgslib.link/"); protected override Uri SystemUrl => new("https://mangalib.org/"); diff --git a/Core/Logic/Getters/LibSocial/NewLibSocialGetterBase.cs b/Core/Logic/Getters/LibSocial/NewLibSocialGetterBase.cs index 853fc7fa..b086a4f6 100644 --- a/Core/Logic/Getters/LibSocial/NewLibSocialGetterBase.cs +++ b/Core/Logic/Getters/LibSocial/NewLibSocialGetterBase.cs @@ -9,6 +9,7 @@ using System.Text; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.Common; @@ -19,171 +20,190 @@ namespace Core.Logic.Getters.LibSocial; -public abstract class NewLibSocialGetterBase(BookGetterConfig config) : GetterBase(config) { +public abstract class NewLibSocialGetterBase(BookGetterConfig config) : GetterBase(config) +{ + private const string ALPHABET_BASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + private const string ALPHABET_CHALLENGE = ALPHABET_BASE + "-_"; private static Uri AuthHost => new("https://auth.lib.social/"); private static Uri ApiHost => new("https://api.cdnlibs.org/"); private static Uri BaseImageHost => new("https://cover.imglib.info/"); - - protected virtual Uri ImagesHost => BaseImageHost; - private const string ALPHABET_BASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - private const string ALPHABET_CHALLENGE = ALPHABET_BASE + "-_"; + protected virtual Uri ImagesHost => BaseImageHost; protected abstract int SiteId { get; } - public override async Task Init() { + public override async Task Init() + { await base.Init(); Config.Client.DefaultRequestHeaders.Remove("Site-Id"); Config.Client.DefaultRequestHeaders.Add("Site-Id", SiteId.ToString()); - try { + try + { var timeZoneId = TimeZoneInfo.Local.Id; Config.Client.DefaultRequestHeaders.Remove("Client-Time-Zone"); Config.Client.DefaultRequestHeaders.Add("Client-Time-Zone", timeZoneId); - } catch (TimeZoneNotFoundException) { + } + catch (TimeZoneNotFoundException) + { // Игнорируем: таймзона влияет только на аналитические метрики сайта - } catch (InvalidTimeZoneException) { + } + catch (InvalidTimeZoneException) + { // Игнорируем: таймзона влияет только на аналитические метрики сайта } } - private static string TrimForLog(string value, int maxLength = 300) { - if (string.IsNullOrWhiteSpace(value)) { - return string.Empty; - } + private static string TrimForLog(string value, int maxLength = 300) + { + if (string.IsNullOrWhiteSpace(value)) return string.Empty; var trimmed = value.Trim(); return trimmed.Length <= maxLength ? trimmed : trimmed[..maxLength] + "…"; } - - private static string Challenge(string str) { + + private static string Challenge(string str) + { var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(str).AsSpan(0, Encoding.UTF8.GetByteCount(str))); - var result = string.Empty; - - for (var t = 0; t < bytes.Length; t += 3) { - result += ALPHABET_CHALLENGE[bytes[t] >> 2]; - result += ALPHABET_CHALLENGE[(bytes[t] & 3) << 4 | bytes[t + 1] >> 4]; - result += ALPHABET_CHALLENGE[(bytes[t + 1] & 15) << 2 | (t + 2 < bytes.Length ? bytes[t + 2] : 0) >> 6]; - result += ALPHABET_CHALLENGE[(t + 2 < bytes.Length ? bytes[t + 2] : 0) & 63]; + var sb = new StringBuilder(44); + + for (var t = 0; t < bytes.Length; t += 3) + { + sb.Append(ALPHABET_CHALLENGE[bytes[t] >> 2]); + sb.Append(ALPHABET_CHALLENGE[((bytes[t] & 3) << 4) | (bytes[t + 1] >> 4)]); + sb.Append(ALPHABET_CHALLENGE[ + ((bytes[t + 1] & 15) << 2) | ((t + 2 < bytes.Length ? bytes[t + 2] : 0) >> 6)]); + sb.Append(ALPHABET_CHALLENGE[(t + 2 < bytes.Length ? bytes[t + 2] : 0) & 63]); } - return (bytes.Length % 3) switch { + var result = sb.ToString(); + return (bytes.Length % 3) switch + { 2 => result[..^1], 1 => result[..^2], _ => result }; } - private static string GetRandom(int length) { + private static string GetRandom(int length) + { return Enumerable .Repeat(0, length) .Aggregate(string.Empty, (c, _) => c + ALPHABET_BASE[new Random().Next(ALPHABET_BASE.Length)]); } - public override async Task Authorize() { - if (string.IsNullOrEmpty(Config.Options.Token) && !Config.HasCredentials ) { - return; - } + public override async Task Authorize() + { + if (string.IsNullOrEmpty(Config.Options.Token) && !Config.HasCredentials) return; - if (!string.IsNullOrEmpty(Config.Options.Token)) { + if (!string.IsNullOrEmpty(Config.Options.Token)) + { Config.Logger.LogInformation("Подставлен токен"); - Config.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Config.Options.Token); + Config.Client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", Config.Options.Token); return; } var secret = GetRandom(128); var state = GetRandom(40); var redirectUri = SystemUrl.MakeRelativeUri("/ru/front/auth/oauth/callback"); - + var challenge = Challenge(secret); - var challengeUrl = AuthHost.MakeRelativeUri($"/auth/oauth/authorize?scope=&client_id=1&response_type=code&redirect_uri={redirectUri}&state={state}&code_challenge={challenge}&code_challenge_method=S256&prompt=consent"); - + var challengeUrl = + AuthHost.MakeRelativeUri( + $"/auth/oauth/authorize?scope=&client_id=1&response_type=code&redirect_uri={redirectUri}&state={state}&code_challenge={challenge}&code_challenge_method=S256&prompt=consent"); + var loginForm = await Config.Client.GetHtmlDocWithTriesAsync(challengeUrl); var tokenInput = loginForm.QuerySelector("input[name=_token]"); - if (tokenInput == default) { - throw new Exception("Не удалось получить csrf токен формы авторизации сайта (возможно, заблокировал DDoS-Guard)"); - } + if (tokenInput is null) + throw new Elib2EbookParseException( + "Не удалось получить csrf токен формы авторизации сайта (возможно, заблокировал DDoS-Guard)"); - var payload = new Dictionary { + var payload = new Dictionary + { { "_token", tokenInput.Attributes["value"].Value }, { "login", Config.Options.Login }, - { "password", Config.Options.Password }, + { "password", Config.Options.Password } }; - await Config.Client.PostHtmlDocWithTriesAsync(AuthHost.MakeRelativeUri("/auth/login"), new FormUrlEncodedContent(payload)); + await Config.Client.PostHtmlDocWithTriesAsync(AuthHost.MakeRelativeUri("/auth/login"), + new FormUrlEncodedContent(payload)); var login = await Config.Client.GetHtmlDocWithTriesAsync(challengeUrl); - if (login.QuerySelector(".g-recaptcha") != default) { - throw new Exception("Авторизация заблокирована капчей. Укажите --flare с адресом FlareSolverr"); - } + if (login.QuerySelector(".g-recaptcha") is not null) + throw new Elib2EbookParseException( + "Авторизация заблокирована капчей. Укажите --flare с адресом FlareSolverr"); var error = login.QuerySelector(".form-field__error"); - if (error != default && !string.IsNullOrWhiteSpace(error.InnerText)) { - throw new Exception($"Не удалось авторизоваться. {error.InnerText.Trim()}"); - } + if (error is not null && !string.IsNullOrWhiteSpace(error.InnerText)) + throw new Elib2EbookAuthException($"Не удалось авторизоваться. {error.InnerText.Trim()}"); var postForm = login.QuerySelector("form[method=post]"); - if (postForm == default) { - throw new Exception("Не удалось получить форму подтверждения OAuth на сайте (проверьте логин/пароль)"); - } + if (postForm is null) + throw new Elib2EbookAuthException( + "Не удалось получить форму подтверждения OAuth на сайте (проверьте логин/пароль)"); payload = postForm .QuerySelectorAll("input[type=hidden]") .ToDictionary(input => input.Attributes["name"].Value, input => input.Attributes["value"].Value); - - using var authorize = await Config.Client.PostAsync(AuthHost.MakeRelativeUri(postForm.Attributes["action"].Value), new FormUrlEncodedContent(payload)); - if ((int)authorize.StatusCode == 419) { - throw new Exception("Авторизация отклонена (код 419). Обычно это означает истекший CSRF токен или необходимость пройти капчу. Попробуйте указать --flare"); - } - if (authorize == default || authorize.RequestMessage?.RequestUri == default) { - throw new Exception("Сайт не вернул код авторизации (проверьте логин/пароль)"); - } - - var authCode = authorize.RequestMessage.RequestUri.GetQueryParameter("code"); - if (string.IsNullOrWhiteSpace(authCode)) { - throw new Exception("Сайт не вернул code после авторизации (проверьте логин/пароль)"); - } - - var tokenResponse = await Config.Client.PostAsync(ApiHost.MakeRelativeUri("/api/auth/oauth/token"), JsonContent.Create(new { - grant_type = "authorization_code", - client_id = 1, - redirect_uri = redirectUri, - code_verifier = secret, - code = authCode - })); - if (tokenResponse == default) { - throw new Exception("Не удалось получить ответ от сайта при запросе access token"); - } + using var authorize = await Config.Client.PostAsync( + AuthHost.MakeRelativeUri(postForm.Attributes["action"].Value), new FormUrlEncodedContent(payload)); + if ((int)authorize.StatusCode == 419) + throw new Elib2EbookAuthException( + "Авторизация отклонена (код 419). Обычно это означает истекший CSRF токен или необходимость пройти капчу. Попробуйте указать --flare"); + if (authorize is null || authorize.RequestMessage?.RequestUri is null) + throw new Elib2EbookAuthException("Сайт не вернул код авторизации (проверьте логин/пароль)"); - if (tokenResponse.StatusCode != HttpStatusCode.OK) { + var authCode = authorize.RequestMessage.RequestUri.GetQueryParameter("code"); + if (string.IsNullOrWhiteSpace(authCode)) + throw new Elib2EbookAuthException("Сайт не вернул code после авторизации (проверьте логин/пароль)"); + + var tokenResponse = await Config.Client.PostAsync(ApiHost.MakeRelativeUri("/api/auth/oauth/token"), + JsonContent.Create(new + { + grant_type = "authorization_code", + client_id = 1, + redirect_uri = redirectUri, + code_verifier = secret, + code = authCode + })); + + if (tokenResponse is null) + throw new Elib2EbookAuthException("Не удалось получить ответ от сайта при запросе access token"); + + if (tokenResponse.StatusCode != HttpStatusCode.OK) + { var errorBody = await tokenResponse.Content.ReadAsStringAsync(); var snippet = string.IsNullOrWhiteSpace(errorBody) ? string.Empty : $" Ответ сервера: {TrimForLog(errorBody)}"; - throw new Exception($"Сайт не выдал access token. Код {(int)tokenResponse.StatusCode} ({tokenResponse.StatusCode}).{snippet}"); + throw new Elib2EbookAuthException( + $"Сайт не выдал access token. Код {(int)tokenResponse.StatusCode} ({tokenResponse.StatusCode}).{snippet}"); } var token = await tokenResponse.Content.ReadFromJsonAsync(); - if (token == default || string.IsNullOrWhiteSpace(token.AccessToken)) { - throw new Exception("Сайт не вернул access token. Проверьте корректность логина и пароля"); - } + if (token is null || string.IsNullOrWhiteSpace(token.AccessToken)) + throw new Elib2EbookAuthException("Сайт не вернул access token. Проверьте корректность логина и пароля"); Config.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken); Config.Logger.LogInformation("Успешно авторизовались"); } - protected override string GetId(Uri url) { + protected override string GetId(Uri url) + { var id = url.GetSegment(2); return id is "book" or "read" or "manga" ? url.GetSegment(3) : id; } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { var bid = url.GetQueryParameter("bid"); var details = await GetBookDetails(url); - var book = new Book(url) { + var book = new Book(url) + { SupportSplitVolumes = true, SupportSplitChapters = true, Cover = await GetCover(details), @@ -197,7 +217,8 @@ public override async Task Get(Uri url) { return book; } - private async Task GetBookDetails(Uri url) { + private async Task GetBookDetails(Uri url) + { url = ApiHost .AppendSegment("api/manga/" + GetId(url)) .AppendQueryParameter("fields[]", "background") @@ -207,76 +228,85 @@ private async Task GetBookDetails(Uri url) { .AppendQueryParameter("fields[]", "chap_count"); var response = await Config.Client.GetWithTriesAsync(url); - if (response == default) { - throw new Exception("Не удалось получить ответ от сайта при загрузке информации о книге"); - } + if (response is null) + throw new Elib2EbookParseException("Не удалось получить ответ от сайта при загрузке информации о книге"); - if (response.StatusCode != HttpStatusCode.OK) { - throw new Exception($"Ошибка загрузки информации о книге. Код {(int)response.StatusCode} ({response.StatusCode})."); - } + if (response.StatusCode != HttpStatusCode.OK) + throw new Elib2EbookParseException( + $"Ошибка загрузки информации о книге. Код {(int)response.StatusCode} ({response.StatusCode})."); return await response.Content.ReadFromJsonAsync(); } - private async Task> GetToc(RanobeLibBookDetails book, string bid) { + private async Task> GetToc(RanobeLibBookDetails book, string bid) + { var url = ApiHost.MakeRelativeUri($"/api/manga/{book.Data.SlugUrl}/chapters"); Config.Logger.LogInformation("Загружаю оглавление"); var response = await Config.Client.GetWithTriesAsync(url); - if (response == default) { - throw new Exception("Не удалось получить ответ от сайта при загрузке оглавления"); - } + if (response is null) + throw new Elib2EbookParseException("Не удалось получить ответ от сайта при загрузке оглавления"); - if (response.StatusCode != HttpStatusCode.OK) { - throw new Exception($"Ошибка загрузки оглавления. Код {(int)response.StatusCode} ({response.StatusCode})."); - } + if (response.StatusCode != HttpStatusCode.OK) + throw new Elib2EbookParseException( + $"Ошибка загрузки оглавления. Код {(int)response.StatusCode} ({response.StatusCode})."); - var result = await response.Content.ReadFromJsonAsync().ContinueWith(t => t.Result.Chapters); - if (!string.IsNullOrWhiteSpace(bid)) { + var result = await response.Content.ReadFromJsonAsync() + .ContinueWith(t => t.Result.Chapters); + if (!string.IsNullOrWhiteSpace(bid)) result = result.Where(c => c.Branches.Any(b => b.BranchId.ToString() == bid)).ToList(); - } - + return SliceToc(result, c => c.Name); } - private Author GetAuthor(RanobeLibBookDetails details) { + private Author GetAuthor(RanobeLibBookDetails details) + { var author = details.Data.Authors.FirstOrDefault(); - return author == default ? new Author("Ranobelib") : new Author(author.Name, SystemUrl.MakeRelativeUri($"/ru/people/{author.SlugUrl}")); + return author is null + ? new Author("Ranobelib") + : new Author(author.Name, SystemUrl.MakeRelativeUri($"/ru/people/{author.SlugUrl}")); } - - private IEnumerable GetCoAuthors(RanobeLibBookDetails details) { + + private IEnumerable GetCoAuthors(RanobeLibBookDetails details) + { return details.Data.Authors .Skip(1) - .Select(author => new Author(author.Name, SystemUrl.MakeRelativeUri($"/ru/people/{author.SlugUrl}"))).ToList(); + .Select(author => new Author(author.Name, SystemUrl.MakeRelativeUri($"/ru/people/{author.SlugUrl}"))) + .ToList(); } - - private Task GetCover(RanobeLibBookDetails details) { - return !string.IsNullOrWhiteSpace(details.Data.Cover.Default) ? SaveImage(details.Data.Cover.Default.AsUri()) : Task.FromResult(default(TempFile)); + + private Task GetCover(RanobeLibBookDetails details) + { + return !string.IsNullOrWhiteSpace(details.Data.Cover.Default) + ? SaveImage(details.Data.Cover.Default.AsUri()) + : Task.FromResult(default(TempFile)); } - protected override HttpRequestMessage GetImageRequestMessage(Uri uri) { + protected override HttpRequestMessage GetImageRequestMessage(Uri uri) + { uri = uri.Host == BaseImageHost.Host ? uri : uri.ReplaceHost(ImagesHost.Host); - + var message = base.GetImageRequestMessage(uri); message.Headers.Add("Referer", SystemUrl.ToString()); return message; } - private async Task> FillChapters(RanobeLibBookDetails book, string bid) { + private async Task> FillChapters(RanobeLibBookDetails book, string bid) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } - - foreach (var socialChapter in await GetToc(book, bid)) { + if (Config.Options.NoChapters) return result; + + foreach (var socialChapter in await GetToc(book, bid)) + { var title = socialChapter.Name.ReplaceNewLine(); Config.Logger.LogInformation($"Загружаю главу {title.CoverQuotes()}"); - - var chapter = new Chapter { + + var chapter = new Chapter + { Title = title, VolumeNumber = socialChapter.Volume, - ChapterNumber = socialChapter.Number, + ChapterNumber = socialChapter.Number }; var chapterDoc = await GetChapter(book, socialChapter, bid); @@ -285,17 +315,18 @@ private async Task> FillChapters(RanobeLibBookDetails book, result.Add(chapter); } - + return result; } - private async Task GetChapter(RanobeLibBookDetails book, SocialLibBookChapter chapter, string bid) { - var uri = ApiHost.MakeRelativeUri($"/api/manga/{book.Data.SlugUrl}/chapter?number={chapter.Number}&volume={chapter.Volume}"); - if (!string.IsNullOrWhiteSpace(bid)) { - uri = uri.AppendQueryParameter("branch_id", bid); - } - - var chapterResponse = await Config.Client.GetFromJsonWithTriesAsync(uri, TimeSpan.FromSeconds(30)); + private async Task GetChapter(RanobeLibBookDetails book, SocialLibBookChapter chapter, string bid) + { + var uri = ApiHost.MakeRelativeUri( + $"/api/manga/{book.Data.SlugUrl}/chapter?number={chapter.Number}&volume={chapter.Volume}"); + if (!string.IsNullOrWhiteSpace(bid)) uri = uri.AppendQueryParameter("branch_id", bid); + + var chapterResponse = + await Config.Client.GetFromJsonWithTriesAsync(uri, TimeSpan.FromSeconds(30)); return ResponseToHtmlDoc(chapterResponse.Data); } diff --git a/Core/Logic/Getters/LibSocial/RanobeLibGetter.cs b/Core/Logic/Getters/LibSocial/RanobeLibGetter.cs index a5d5f0e1..e3f75ee5 100644 --- a/Core/Logic/Getters/LibSocial/RanobeLibGetter.cs +++ b/Core/Logic/Getters/LibSocial/RanobeLibGetter.cs @@ -5,81 +5,87 @@ using System.Text.Json; using System.Text.Json.Nodes; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.SocialLib; using HtmlAgilityPack; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters.LibSocial; +namespace Core.Logic.Getters.LibSocial; -public class RanobeLibGetter(BookGetterConfig config) : NewLibSocialGetterBase(config) { - protected override Uri SystemUrl => new("https://ranobelib.me/"); - - protected override int SiteId => 3; - - private static readonly Dictionary RecursiveTag = new() { +public class RanobeLibGetter(BookGetterConfig config) : NewLibSocialGetterBase(config) +{ + private static readonly Dictionary RecursiveTag = new() + { { "paragraph", "p" }, { "orderedList", "ol" }, { "listItem", "li" }, - { "blockquote", "blockquote" }, + { "blockquote", "blockquote" } }; - - private static readonly Dictionary InlineTag = new() { + + private static readonly Dictionary InlineTag = new() + { { "horizontalRule", "
" }, - { "hardBreak", "
" }, + { "hardBreak", "
" } }; - - private static readonly Dictionary MarkTag = new() { + + private static readonly Dictionary MarkTag = new() + { { "italic", "i" }, { "bold", "b" }, - { "underline", "u" }, + { "underline", "u" } }; - private StringBuilder AsHtml(SocialLibBookChapter chapterResponse, IEnumerable contents) { + protected override Uri SystemUrl => new("https://ranobelib.me/"); + + protected override int SiteId => 3; + + private StringBuilder AsHtml(SocialLibBookChapter chapterResponse, IEnumerable contents) + { var sb = new StringBuilder(); - foreach (var content in contents) { - if (RecursiveTag.TryGetValue(content.Type, out var tag)) { - sb.Append(AsHtml(chapterResponse, content.Content).ToString().CoverTag(tag)); + foreach (var content in contents) + { + if (RecursiveTag.TryGetValue(content.Type, out var tag)) + { + sb.Append(AsHtml(chapterResponse, content.Content).ToString().CoverTag(tag)); continue; } - - if (InlineTag.TryGetValue(content.Type, out tag)) { + + if (InlineTag.TryGetValue(content.Type, out tag)) + { sb.Append(tag); continue; } - switch (content.Type) { - case "text": { + switch (content.Type) + { + case "text": + { var text = content.Text.HtmlEncode(); - foreach (var mark in content.Marks) { - if (MarkTag.TryGetValue(mark.Type, out tag)) { + foreach (var mark in content.Marks) + if (MarkTag.TryGetValue(mark.Type, out tag)) text = text.CoverTag(tag); - } else { + else Config.Logger.LogInformation($"Неизвестый тип форматирования {mark.Type}"); - } - } sb.Append(text); continue; } - case "image": { - if (content.Attrs.TryGetValue("images", out var images)) { - foreach (var image in images.Deserialize[]>()) { - if (!image.TryGetValue("image", out var imageId)) { - continue; - } - + case "image": + { + if (content.Attrs.TryGetValue("images", out var images)) + foreach (var image in images.Deserialize[]>()) + { + if (!image.TryGetValue("image", out var imageId)) continue; + var attachment = chapterResponse.Attachments.FirstOrDefault(a => a.Name == imageId); - if (attachment == default) { - continue; - } - + if (attachment is null) continue; + sb.Append($""); } - } - + continue; } default: @@ -90,12 +96,14 @@ private StringBuilder AsHtml(SocialLibBookChapter chapterResponse, IEnumerable e.GetValue().AsHtmlDoc(), JsonObject o => AsHtml(chapterResponse, o.Deserialize().Content).AsHtmlDoc(), - _ => throw new Exception("Неизвестный тип") + _ => throw new Elib2EbookParseException("Неизвестный тип") }; } } diff --git a/Core/Logic/Getters/LibSocial/SlashLibGetter.cs b/Core/Logic/Getters/LibSocial/SlashLibGetter.cs index 93c901a4..ef91938d 100644 --- a/Core/Logic/Getters/LibSocial/SlashLibGetter.cs +++ b/Core/Logic/Getters/LibSocial/SlashLibGetter.cs @@ -1,11 +1,14 @@ using System; using Core.Configs; -namespace Core.Logic.Getters.LibSocial; +namespace Core.Logic.Getters.LibSocial; + +public class SlashLibGetter : MangaLibGetter +{ + public SlashLibGetter(BookGetterConfig config) : base(config) + { + } -public class SlashLibGetter : MangaLibGetter { - public SlashLibGetter(BookGetterConfig config) : base(config) { } - protected override Uri SystemUrl => new("https://v2.slashlib.me/"); protected override int SiteId => 2; diff --git a/Core/Logic/Getters/LibboxGetter.cs b/Core/Logic/Getters/LibboxGetter.cs index 63edb943..457e811d 100644 --- a/Core/Logic/Getters/LibboxGetter.cs +++ b/Core/Logic/Getters/LibboxGetter.cs @@ -11,19 +11,22 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class LibboxGetter(BookGetterConfig config) : GetterBase(config) { +public class LibboxGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://libbox.ru/"); - - public override async Task Get(Uri url) { + + public override async Task Get(Uri url) + { var bookId = GetId(url); url = SystemUrl.MakeRelativeUri($"/book/{bookId}"); - + var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); var title = doc.GetTextBySelector("h1.product__title"); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(bookId, title), Title = title, @@ -32,44 +35,47 @@ public override async Task Get(Uri url) { Seria = GetSeria(doc, url) }; - return book; + return book; } - - private static Seria GetSeria(HtmlDocument doc, Uri url) { + + private static Seria GetSeria(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("div.product__meta a[href*=/book-series/]"); - if (string.IsNullOrWhiteSpace(a.GetText())) { - return default; - } + if (string.IsNullOrWhiteSpace(a.GetText())) return default; - return new Seria { + return new Seria + { Name = a.GetText(), Url = url.MakeRelativeUri(a.Attributes["href"].Value) }; } - private async Task AddChapter(ICollection chapters, Chapter chapter, StringBuilder text) { - if (chapter == null) { - return; - } - + private async Task AddChapter(ICollection chapters, Chapter chapter, StringBuilder text) + { + if (chapter == null) return; + var chapterDoc = text.AsHtmlDoc(); chapter.Images = await GetImages(chapterDoc, SystemUrl); chapter.Content = chapterDoc.DocumentNode.InnerHtml; chapters.Add(chapter); } - private static bool IsSingleChapter(IEnumerable nodes) { + private static bool IsSingleChapter(IEnumerable nodes) + { var firstNode = nodes.First(); return firstNode.Name != "h2" || string.IsNullOrWhiteSpace(firstNode.InnerText); } - private static Chapter CreateChapter(string title) { - return new Chapter { + private static Chapter CreateChapter(string title) + { + return new Chapter + { Title = title }; } - private async Task GetPages(string bookId) { + private async Task GetPages(string bookId) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri($"/books/{bookId}")); return doc .QuerySelectorAll("ul.pagination a.page-numbers") @@ -78,54 +84,64 @@ private async Task GetPages(string bookId) { .Max(); } - private async Task> FillChapters(string bookId, string title) { + private async Task> FillChapters(string bookId, string title) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } - + if (Config.Options.NoChapters) return result; + Chapter chapter = null; var singleChapter = true; var text = new StringBuilder(); var pages = await GetPages(bookId); - - for (var i = 1; i <= pages; i++) { + + for (var i = 1; i <= pages; i++) + { Config.Logger.LogInformation($"Получаю страницу {i}/{pages}"); - var page = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri($"/books/{bookId}?page_book={i}")); + var page = await Config.Client.GetHtmlDocWithTriesAsync( + SystemUrl.MakeRelativeUri($"/books/{bookId}?page_book={i}")); var content = page.QuerySelector("div.entry-content"); var nodes = content.QuerySelectorAll("> h2, > p, > img"); nodes = nodes.Count == 0 ? content.RemoveNodes("script, ins").ChildNodes : nodes; singleChapter = i == 1 ? IsSingleChapter(nodes) : singleChapter; - foreach (var node in nodes) { - if (singleChapter || node.Name != "h2") { - if (node.Name == "img" && node.Attributes["src"] != null) { + foreach (var node in nodes) + if (singleChapter || node.Name != "h2") + { + if (node.Name == "img" && node.Attributes["src"] != null) + { text.Append($""); - } else { - if (!string.IsNullOrWhiteSpace(node.InnerHtml)) { + } + else + { + if (!string.IsNullOrWhiteSpace(node.InnerHtml)) text.Append(node.InnerHtml.HtmlDecode().CoverTag("p")); - } } - } else { + } + else + { await AddChapter(result, chapter, text); text.Clear(); chapter = CreateChapter(node.InnerHtml.HtmlDecode()); } - } } await AddChapter(result, chapter ?? CreateChapter(title), text); return result; } - private static Author GetAuthor(HtmlDocument doc, Uri url) { + private static Author GetAuthor(HtmlDocument doc, Uri url) + { var author = doc.QuerySelector("div.author a"); - return new Author(author.InnerText.HtmlDecode(), url.MakeRelativeUri(author.Attributes["href"]?.Value ?? string.Empty)); + return new Author(author.InnerText.HtmlDecode(), + url.MakeRelativeUri(author.Attributes["href"]?.Value ?? string.Empty)); } - - private Task GetCover(HtmlDocument doc, Uri bookUri) { + + private Task GetCover(HtmlDocument doc, Uri bookUri) + { var imagePath = doc.QuerySelector("div.book-images img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(bookUri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/LibkingGetter.cs b/Core/Logic/Getters/LibkingGetter.cs index b2facd98..fc42b48f 100644 --- a/Core/Logic/Getters/LibkingGetter.cs +++ b/Core/Logic/Getters/LibkingGetter.cs @@ -12,122 +12,134 @@ namespace Core.Logic.Getters; -public class LibkingGetter(BookGetterConfig config) : GetterBase(config) { +public class LibkingGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://libking.ru/"); - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); var name = doc.GetTextBySelector("div[itemprop=name]"); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, url, name), Title = name, Author = GetAuthor(doc), - Annotation = doc.QuerySelector("div.mov-desc-text")?.InnerHtml, + Annotation = doc.QuerySelector("div.mov-desc-text")?.InnerHtml }; return book; } - private Author GetAuthor(HtmlDocument doc) { + private Author GetAuthor(HtmlDocument doc) + { var a = doc.QuerySelector("a[itemprop=author]"); - return a == default ? new Author("LibKing") : new Author(a.GetText(), SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)); + return a is null + ? new Author("LibKing") + : new Author(a.GetText(), SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)); } - private Task GetCover(HtmlDocument doc, Uri bookUri) { + private Task GetCover(HtmlDocument doc, Uri bookUri) + { var imagePath = doc.QuerySelector("div.mov-img img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(bookUri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } - private async Task AddChapter(ICollection chapters, Chapter chapter, StringBuilder text) { + private async Task AddChapter(ICollection chapters, Chapter chapter, StringBuilder text) + { var chapterDoc = text.AsHtmlDoc(); chapter.Images = await GetImages(chapterDoc, SystemUrl); chapter.Content = chapterDoc.DocumentNode.InnerHtml; chapters.Add(chapter); } - - private static Chapter CreateChapter(string title) { - return new Chapter { + + private static Chapter CreateChapter(string title) + { + return new Chapter + { Title = title }; } - private Uri GetFirstPage(HtmlDocument doc, Uri url) { + private Uri GetFirstPage(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("div.pages a.pagenav"); - if (a.GetText() != "1") { - return url; - } + if (a.GetText() != "1") return url; return url.MakeRelativeUri(a.Attributes["href"].Value); } - private Uri GetNextPage(HtmlDocument doc, Uri url) { + private Uri GetNextPage(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("a.load_text"); - if (a == default) { - return default; - } + if (a is null) return default; return url.MakeRelativeUri(a.Attributes["href"].Value); } - private bool IsTitle(HtmlNode node) { + private bool IsTitle(HtmlNode node) + { return node.Name == "div" && node.Attributes["class"]?.Value.Contains("title") == true; } - private HtmlNode GetTitleNode(HtmlDocument doc) { - foreach (var node in doc.DocumentNode.ChildNodes) { - if (node.Name == "#text" && string.IsNullOrWhiteSpace(node.GetText())) { - continue; - } + private HtmlNode GetTitleNode(HtmlDocument doc) + { + foreach (var node in doc.DocumentNode.ChildNodes) + { + if (node.Name == "#text" && string.IsNullOrWhiteSpace(node.GetText())) continue; - if (IsTitle(node)) { - return node; - } + if (IsTitle(node)) return node; return default; } return default; } - - private async Task> FillChapters(HtmlDocument doc, Uri baseUrl, string name) { + + private async Task> FillChapters(HtmlDocument doc, Uri baseUrl, string name) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } - + if (Config.Options.NoChapters) return result; + var fullText = new StringBuilder(); var i = 1; - for (var url = GetFirstPage(doc, baseUrl); url != default;) { + var url = GetFirstPage(doc, baseUrl); + while (url is not null) + { Config.Logger.LogInformation($"Получаю страницу {i}"); doc = await Config.Client.GetHtmlDocWithTriesAsync(url); url = GetNextPage(doc, baseUrl); - fullText.Append(doc.QuerySelector("#book").RemoveNodes(".navigation, .cpab, .cpab1, .load_text, #twentypersent").InnerHtml); + fullText.Append(doc.QuerySelector("#book") + .RemoveNodes(".navigation, .cpab, .cpab1, .load_text, #twentypersent").InnerHtml); i++; } - + var book = fullText.AsHtmlDoc(); var node = GetTitleNode(book); - if (node != default) { - while (node != default) { + if (node is not null) + while (node is not null) + { var title = node.GetText(); var chapterText = new StringBuilder(); - do { + do + { node = node.NextSibling; chapterText.Append(node.OuterHtml); - } while (node.NextSibling != default && !IsTitle(node.NextSibling)); + } while (node.NextSibling is not null && !IsTitle(node.NextSibling)); node = node.NextSibling; await AddChapter(result, CreateChapter(title), chapterText); } - } else { + else await AddChapter(result, CreateChapter(name), fullText); - } - + return result; } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/LibrebookGetter.cs b/Core/Logic/Getters/LibrebookGetter.cs index 9212b100..768dce65 100644 --- a/Core/Logic/Getters/LibrebookGetter.cs +++ b/Core/Logic/Getters/LibrebookGetter.cs @@ -6,6 +6,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.Common; @@ -16,123 +17,141 @@ namespace Core.Logic.Getters; -public class LibrebookGetter(BookGetterConfig config) : GetterBase(config) { +public class LibrebookGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://librebook.me/"); - public override bool IsSameUrl(Uri url) { + public override bool IsSameUrl(Uri url) + { return url.IsSameSubDomain(SystemUrl); } - protected override string GetId(Uri url) { + protected override string GetId(Uri url) + { return url.GetSegment(1); } - public override async Task Init() { + public override async Task Init() + { await base.Init(); Config.Client.DefaultRequestHeaders.Add("Referer", SystemUrl.ToString()); } - public override async Task Authorize() { - if (!Config.HasCredentials) { - return; - } + public override async Task Authorize() + { + if (!Config.HasCredentials) return; - var loginPage = "https://1.grouple.co/internal/auth/login?siteId=6&targetUri=%2Flogin%2FcontinueSso%3FsiteId%3D6%26targetUri%3D%252F".AsUri(); + var loginPage = + "https://1.grouple.co/internal/auth/login?siteId=6&targetUri=%2Flogin%2FcontinueSso%3FsiteId%3D6%26targetUri%3D%252F" + .AsUri(); var doc = await Config.Client.GetHtmlDocWithTriesAsync(loginPage); - + var formUrl = loginPage.MakeRelativeUri(doc.QuerySelector("form")?.Attributes["action"]?.Value); doc = await Config.Client.PostHtmlDocWithTriesAsync(formUrl, GenerateAuthData()); var noty = Regex.Match(doc.ParsedText, @"showNoty\((.*?)\)"); var result = JsonSerializer.Deserialize(noty.Groups[1].Value); - if (result.Type == "success") { + if (result.Type == "success") Config.Logger.LogInformation("Успешно авторизовались"); - } else { - throw new Exception($"Не удалось авторизоваться. {result.Text}"); - } + else + throw new Elib2EbookAuthException($"Не удалось авторизоваться. {result.Text}"); } - private FormUrlEncodedContent GenerateAuthData() { - var payload = new Dictionary { + private FormUrlEncodedContent GenerateAuthData() + { + var payload = new Dictionary + { { "targetUri", "/login/continueSso?siteId=6&targetUri=%2F" }, { "username", Config.Options.Login }, { "password", Config.Options.Password }, { "remember_me", "true" }, { "_remember_me_yes", string.Empty }, - { "remember_me_yes", "on" }, + { "remember_me_yes", "on" } }; return new FormUrlEncodedContent(payload); } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { url = SystemUrl.MakeRelativeUri(GetId(url)).ReplaceHost(url.Host); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); var chapterLink = doc.QuerySelector("a.chapter-link"); - if (chapterLink == default) { - throw new Exception("Книга недоступна для чтения. Возможно, нужна авторизация."); - } - + if (chapterLink is null) + throw new Elib2EbookParseException("Книга недоступна для чтения. Возможно, нужна авторизация."); + var startUrl = url.MakeRelativeUri(chapterLink.Attributes["href"].Value); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(startUrl), Title = doc.GetTextBySelector("h1.names > span.name"), Author = GetAuthor(doc, url), Annotation = doc.QuerySelector("div.manga-description > div")?.InnerHtml }; - + return book; } - private Author GetAuthor(HtmlDocument doc, Uri bookUrl) { + private Author GetAuthor(HtmlDocument doc, Uri bookUrl) + { var a = doc.QuerySelector("span.elem_author a"); - return a == default ? new Author("Librebook") : new Author(a.GetText(), bookUrl.MakeRelativeUri(a.Attributes["href"].Value)); + return a is null + ? new Author("Librebook") + : new Author(a.GetText(), bookUrl.MakeRelativeUri(a.Attributes["href"].Value)); } - private async Task> FillChapters(Uri startUrl) { + private async Task> FillChapters(Uri startUrl) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } - - foreach (var urlChapter in await GetToc(startUrl)) { + if (Config.Options.NoChapters) return result; + + foreach (var urlChapter in await GetToc(startUrl)) + { Config.Logger.LogInformation($"Загружаю главу {urlChapter.Title.CoverQuotes()}"); - var chapter = new Chapter { + var chapter = new Chapter + { Title = urlChapter.Title }; var chapterDoc = await GetChapter(urlChapter); - if (chapterDoc != default) { + if (chapterDoc is not null) + { chapter.Images = await GetImages(chapterDoc, SystemUrl); chapter.Content = chapterDoc.DocumentNode.InnerHtml; } - + result.Add(chapter); } return result; } - private async Task> GetToc(Uri startUrl) { + private async Task> GetToc(Uri startUrl) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(startUrl); var result = doc .QuerySelectorAll("#chapters-list a.chapter-link") - .Select(a => new UrlChapter(SystemUrl.MakeRelativeUri(a.Attributes["href"].Value), a.GetText().ReplaceNewLine())) + .Select(a => + new UrlChapter(SystemUrl.MakeRelativeUri(a.Attributes["href"].Value), a.GetText().ReplaceNewLine())) .ToList(); return SliceToc(result, c => c.Title); } - private async Task GetChapter(UrlChapter urlChapter) { + private async Task GetChapter(UrlChapter urlChapter) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(urlChapter.Url); return doc.QuerySelector("div.b-chapter").InnerHtml.AsHtmlDoc(); } - private Task GetCover(HtmlDocument doc, Uri bookUri) { + private Task GetCover(HtmlDocument doc, Uri bookUri) + { var imagePath = doc.QuerySelector("img.fotorama__img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(bookUri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/LibstGetter.cs b/Core/Logic/Getters/LibstGetter.cs index 61ad4103..aefcf339 100644 --- a/Core/Logic/Getters/LibstGetter.cs +++ b/Core/Logic/Getters/LibstGetter.cs @@ -7,6 +7,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.Common; @@ -14,47 +15,54 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class LibstGetter(BookGetterConfig config) : GetterBase(config) { +public class LibstGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://libst.ru/"); - protected override string GetId(Uri url) => url.GetQueryParameter("BookID") ?? base.GetId(url); + protected override string GetId(Uri url) + { + return url.GetQueryParameter("BookID") ?? base.GetId(url); + } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { var bookId = GetId(url); url = SystemUrl.MakeRelativeUri($"/Detail/BookView/{bookId}"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - - var book = new Book(url) { + + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(doc), Title = doc.GetTextBySelector("h1"), - Author = GetAuthor(doc, url), + Author = GetAuthor(doc, url) }; - + return book; } - public override async Task Authorize() { - if (!Config.HasCredentials) { - return; - } + public override async Task Authorize() + { + if (!Config.HasCredentials) return; var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl); var token = doc.QuerySelector("input[name=__RequestVerificationToken]").Attributes["value"].Value; - doc = await Config.Client.PostHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri("/Account/Login?ReturnUrl=/"), GenerateAuthData(token)); + doc = await Config.Client.PostHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri("/Account/Login?ReturnUrl=/"), + GenerateAuthData(token)); var error = doc.GetTextBySelector("div.validation-summary-errors"); - if (string.IsNullOrEmpty(error)) { + if (string.IsNullOrEmpty(error)) Config.Logger.LogInformation("Авторизация прошла успешно"); - } else { - throw new Exception($"Не удалось авторизоваться. {error}"); - } + else + throw new Elib2EbookAuthException($"Не удалось авторизоваться. {error}"); } - - private FormUrlEncodedContent GenerateAuthData(string token) { - var data = new Dictionary { + + private FormUrlEncodedContent GenerateAuthData(string token) + { + var data = new Dictionary + { ["__RequestVerificationToken"] = token, ["username"] = Config.Options.Login, ["password"] = Config.Options.Password @@ -63,23 +71,22 @@ private FormUrlEncodedContent GenerateAuthData(string token) { return new FormUrlEncodedContent(data); } - private async Task> FillChapters(HtmlDocument doc) { + private async Task> FillChapters(HtmlDocument doc) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - foreach (var bookChapter in GetToc(doc)) { + foreach (var bookChapter in GetToc(doc)) + { Config.Logger.LogInformation($"Загружаю главу {bookChapter.Title.CoverQuotes()}"); - - var chapter = new Chapter { + + var chapter = new Chapter + { Title = bookChapter.Title }; var chapterDoc = await GetChapter(bookChapter); - if (chapterDoc != default) { - chapter.Content = chapterDoc.DocumentNode.InnerHtml; - } + if (chapterDoc is not null) chapter.Content = chapterDoc.DocumentNode.InnerHtml; result.Add(chapter); } @@ -87,18 +94,17 @@ private async Task> FillChapters(HtmlDocument doc) { return result; } - private async Task GetChapter(IdChapter bookChapter) { - if (string.IsNullOrWhiteSpace(bookChapter.Id)) { - return default; - } - + private async Task GetChapter(IdChapter bookChapter) + { + if (string.IsNullOrWhiteSpace(bookChapter.Id)) return default; + var sb = new StringBuilder(); - - for (var i = 1;; i++) { - var response = await Config.Client.GetAsync(SystemUrl.MakeRelativeUri($"/Detail/GetChapterTextForCanvas?ChapID={bookChapter.Id}&k=0&page={i}")); - if (response.StatusCode != HttpStatusCode.OK) { - break; - } + + for (var i = 1;; i++) + { + var response = await Config.Client.GetAsync( + SystemUrl.MakeRelativeUri($"/Detail/GetChapterTextForCanvas?ChapID={bookChapter.Id}&k=0&page={i}")); + if (response.StatusCode != HttpStatusCode.OK) break; var data = await response.Content.ReadAsStringAsync().ContinueWith(t => t.Result.Deserialize()); sb.AppendJoin(string.Empty, data.Select(r => r.Trim().CoverTag("p"))); @@ -107,14 +113,19 @@ private async Task GetChapter(IdChapter bookChapter) { return sb.AsHtmlDoc(); } - private IEnumerable GetToc(HtmlDocument doc) { + private IEnumerable GetToc(HtmlDocument doc) + { var result = new List(); - foreach (var span in doc.QuerySelectorAll("span.mt-action-author")) { + foreach (var span in doc.QuerySelectorAll("span.mt-action-author")) + { var onclick = span.Attributes["onclick"]?.Value; - if (string.IsNullOrWhiteSpace(onclick)) { + if (string.IsNullOrWhiteSpace(onclick)) + { result.Add(new IdChapter(string.Empty, span.GetText())); - } else { + } + else + { var id = Regex.Match(onclick, @"ChapID=(?\d+)").Groups["id"].Value; result.Add(new IdChapter(id, span.GetText())); } @@ -123,13 +134,17 @@ private IEnumerable GetToc(HtmlDocument doc) { return SliceToc(result, c => c.Title); } - private Task GetCover(HtmlDocument doc, Uri uri) { + private Task GetCover(HtmlDocument doc, Uri uri) + { var imagePath = doc.QuerySelector("img.coverBook")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(uri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } - - private static Author GetAuthor(HtmlDocument doc, Uri uri) { + + private static Author GetAuthor(HtmlDocument doc, Uri uri) + { var a = doc.QuerySelector("h2.author_name a"); return new Author(a.GetText(), uri.MakeRelativeUri(a.Attributes["href"].Value)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/LitLifeGetter.cs b/Core/Logic/Getters/LitLifeGetter.cs index 28e9b71c..18271b25 100644 --- a/Core/Logic/Getters/LitLifeGetter.cs +++ b/Core/Logic/Getters/LitLifeGetter.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.Common; @@ -14,52 +15,55 @@ namespace Core.Logic.Getters; -public class LitLifeGetter(BookGetterConfig config) : GetterBase(config) { +public class LitLifeGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://litlife.club/"); - protected override string GetId(Uri url) { + protected override string GetId(Uri url) + { return url.GetSegment(2); } - public override async Task Authorize() { - if (!Config.HasCredentials) { - return; - } + public override async Task Authorize() + { + if (!Config.HasCredentials) return; var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl); var token = doc.QuerySelector("#login_form input[name=_token]").Attributes["value"].Value; - - doc = await Config.Client.PostHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri("/login"), GenerateAuthData(token)); + + doc = await Config.Client.PostHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri("/login"), + GenerateAuthData(token)); var alert = doc.QuerySelector("div.alert-danger"); - if (alert == default) { + if (alert is null) Config.Logger.LogInformation("Успешно авторизовались"); - } else { - throw new Exception($"Не удалось авторизоваться. {alert.GetText()}"); - } + else + throw new Elib2EbookAuthException($"Не удалось авторизоваться. {alert.GetText()}"); } - private FormUrlEncodedContent GenerateAuthData(string token) { - var payload = new Dictionary { + private FormUrlEncodedContent GenerateAuthData(string token) + { + var payload = new Dictionary + { { "_token", token }, { "login", Config.Options.Login }, { "login_password", Config.Options.Password }, - { "remember", "on" }, + { "remember", "on" } }; return new FormUrlEncodedContent(payload); } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { var id = GetId(url); url = SystemUrl.MakeRelativeUri("/books/" + id); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); var noAccess = doc.QuerySelector("p.no_access_text"); - if (noAccess != default) { - throw new Exception("Книна не доступна для чтения."); - } + if (noAccess is not null) throw new Elib2EbookParseException("Книна не доступна для чтения."); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(id), Title = doc.GetTextBySelector("div.card-header h2[itemprop=name]"), @@ -67,51 +71,58 @@ public override async Task Get(Uri url) { Seria = GetSeria(doc), Annotation = doc.QuerySelector("#annotation")?.InnerHtml }; - + return book; } - - private Seria GetSeria(HtmlDocument doc) { + + private Seria GetSeria(HtmlDocument doc) + { var a = doc.QuerySelector("div.card-body a.sequence"); - if (a != default) { - return new Seria { + if (a is not null) + return new Seria + { Name = a.GetText(), Url = SystemUrl.MakeRelativeUri(a.Attributes["href"].Value) }; - } return default; } - private Author GetAuthor(HtmlDocument doc) { + private Author GetAuthor(HtmlDocument doc) + { var a = doc.QuerySelector("div.card-body h3[itemprop=author] a.author"); return new Author(a.GetText(), SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)); } - - private async Task> GetToc(string id) { - var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri($"/books/{id}/sections/list_go_to")); + + private async Task> GetToc(string id) + { + var doc = await Config.Client.GetHtmlDocWithTriesAsync( + SystemUrl.MakeRelativeUri($"/books/{id}/sections/list_go_to")); var result = doc.QuerySelectorAll("ul a") - .Select(a => new UrlChapter(SystemUrl.MakeRelativeUri(a.Attributes["href"].Value), a.RemoveNodes("span").GetText().ReplaceNewLine())) + .Select(a => new UrlChapter(SystemUrl.MakeRelativeUri(a.Attributes["href"].Value), + a.RemoveNodes("span").GetText().ReplaceNewLine())) .ToList(); return SliceToc(result, c => c.Title); } - private async Task> FillChapters(string id) { + private async Task> FillChapters(string id) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } - - foreach (var urlChapter in await GetToc(id)) { + if (Config.Options.NoChapters) return result; + + foreach (var urlChapter in await GetToc(id)) + { Config.Logger.LogInformation($"Загружаю главу {urlChapter.Title.CoverQuotes()}"); - - var chapter = new Chapter { + + var chapter = new Chapter + { Title = urlChapter.Title }; - if (urlChapter.Url != default) { + if (urlChapter.Url is not null) + { var chapterDoc = await GetChapter(urlChapter); chapter.Images = await GetImages(chapterDoc, urlChapter.Url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -123,9 +134,10 @@ private async Task> FillChapters(string id) { return result; } - private async Task GetChapter(UrlChapter urlChapter) { + private async Task GetChapter(UrlChapter urlChapter) + { var result = new StringBuilder(); - + var doc = await Config.Client.GetHtmlDocWithTriesAsync(urlChapter.Url); result.Append(doc.QuerySelector("div.book_text").RemoveNodes("#read_online_ad, h4").InnerHtml); @@ -133,19 +145,22 @@ private async Task GetChapter(UrlChapter urlChapter) { .Where(a => int.TryParse(a.GetText(), out _)) .Select(a => int.Parse(a.GetText())) .ToList(); - - if (pages.Count > 0) { - for (var i = 2; i <= pages.Max(); i++) { + + if (pages.Count > 0) + for (var i = 2; i <= pages.Max(); i++) + { doc = await Config.Client.GetHtmlDocWithTriesAsync(urlChapter.Url.AppendQueryParameter("page", i)); result.Append(doc.QuerySelector("div.book_text").RemoveNodes("#read_online_ad, h4").InnerHtml); } - } - + return result.AsHtmlDoc(); } - private Task GetCover(HtmlDocument doc, Uri bookUri) { + private Task GetCover(HtmlDocument doc, Uri bookUri) + { var imagePath = doc.QuerySelector("div.cover img")?.Attributes["data-src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(bookUri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/LitgorodGetter.cs b/Core/Logic/Getters/LitgorodGetter.cs index dc81d66e..edc18ae2 100644 --- a/Core/Logic/Getters/LitgorodGetter.cs +++ b/Core/Logic/Getters/LitgorodGetter.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using System.Web; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.Common; @@ -14,17 +15,20 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class LitgorodGetter(BookGetterConfig config) : GetterBase(config) { +public class LitgorodGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://litgorod.ru/"); - - public override async Task Get(Uri url) { + + public override async Task Get(Uri url) + { url = SystemUrl.MakeRelativeUri($"/books/view/{GetId(url)}"); Config.Client.DefaultRequestHeaders.Add("Referer", url.ToString()); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, url), Title = doc.GetTextBySelector("div.b-book_item__name h1"), @@ -33,132 +37,137 @@ public override async Task Get(Uri url) { Annotation = doc.QuerySelector("div[data-tab-item=1] > p")?.InnerHtml, Seria = GetSeria(doc, url) }; - + return book; } - public override async Task Init() { + public override async Task Init() + { var response = await Config.Client.GetWithTriesAsync(SystemUrl); var doc = await response.Content.ReadAsStringAsync().ContinueWith(t => t.Result.AsHtmlDoc()); - + var csrf = doc.QuerySelector("[name=csrf-token]")?.Attributes["content"]?.Value; - if (string.IsNullOrWhiteSpace(csrf)) { + if (string.IsNullOrWhiteSpace(csrf)) throw new ArgumentException("Не удалось получить csrf-token", nameof(csrf)); - } - + var cookies = response.Headers.SingleOrDefault(header => header.Key == "Set-Cookie").Value; var xsrfCookie = cookies.FirstOrDefault(c => c.StartsWith("XSRF-TOKEN=")); - if (xsrfCookie == null) { - throw new ArgumentException("Не удалось получить XSRF-TOKEN", nameof(xsrfCookie)); - } - + if (xsrfCookie == null) throw new ArgumentException("Не удалось получить XSRF-TOKEN", nameof(xsrfCookie)); + var xsrf = HttpUtility.UrlDecode(xsrfCookie.Split(";")[0].Replace("XSRF-TOKEN=", "")); - + Config.Client.DefaultRequestHeaders.Add("Accept", "application/json, text/plain, */*"); Config.Client.DefaultRequestHeaders.Add("X-Requested-With", "XMLHttpRequest"); Config.Client.DefaultRequestHeaders.Add("X-CSRF-TOKEN", csrf); Config.Client.DefaultRequestHeaders.Add("X-XSRF-TOKEN", xsrf); } - public override async Task Authorize() { - if (!Config.HasCredentials) { - return; - } + public override async Task Authorize() + { + if (!Config.HasCredentials) return; - var payload = new { + var payload = new + { email = Config.Options.Login, password = Config.Options.Password }; var response = await Config.Client.PostAsJsonAsync(SystemUrl.MakeRelativeUri("login"), payload); var data = await response.Content.ReadFromJsonAsync(); - if (string.IsNullOrWhiteSpace(data?.Message)) { + if (string.IsNullOrWhiteSpace(data?.Message)) Config.Logger.LogInformation("Успешно авторизовались"); - } else { - throw new Exception($"Не удалось авторизоваться. {data.Message}"); - } + else + throw new Elib2EbookAuthException($"Не удалось авторизоваться. {data.Message}"); } - private static Author GetAuthor(HtmlDocument doc, Uri url) { + private static Author GetAuthor(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("div.b-book_item__author a"); return new Author(a.GetText(), url.MakeRelativeUri(a.Attributes["href"].Value)); } - - private static IEnumerable GetCoAuthors(HtmlDocument doc, Uri url) { - return doc.QuerySelectorAll("div.b-book_item__author a").Skip(1).Select(a => new Author(a.GetText(), url.MakeRelativeUri(a.Attributes["href"].Value))).ToList(); + + private static IEnumerable GetCoAuthors(HtmlDocument doc, Uri url) + { + return doc.QuerySelectorAll("div.b-book_item__author a").Skip(1) + .Select(a => new Author(a.GetText(), url.MakeRelativeUri(a.Attributes["href"].Value))).ToList(); } - private static Seria GetSeria(HtmlDocument doc, Uri url) { + private static Seria GetSeria(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("div.b-book_item__cycle a"); - if (a != default) { - return new Seria { + if (a is not null) + return new Seria + { Name = a.GetText(), Url = url.MakeRelativeUri(a.Attributes["href"].Value) }; - } return default; } - - private async Task> FillChapters(HtmlDocument doc, Uri uri) { + + private async Task> FillChapters(HtmlDocument doc, Uri uri) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - foreach (var bookChapter in GetToc(doc, uri)) { - var chapter = new Chapter { + foreach (var bookChapter in GetToc(doc, uri)) + { + var chapter = new Chapter + { Title = bookChapter.Title }; Config.Logger.LogInformation($"Загружаю главу {bookChapter.Title.CoverQuotes()}"); var chapterDoc = await GetChapter(bookChapter.Url); - if (chapterDoc != default) { + if (chapterDoc is not null) + { chapter.Images = await GetImages(chapterDoc, uri); chapter.Content = chapterDoc.DocumentNode.InnerHtml; } - + result.Add(chapter); } return result; } - - private async Task GetChapter(Uri url) { + + private async Task GetChapter(Uri url) + { var response = await Config.Client.GetWithTriesAsync(url); - if (response == default) { - return default; - } + if (response is null) return default; var doc = await response.Content.ReadAsStreamAsync().ContinueWith(t => t.Result.AsHtmlDoc()); var content = doc.QuerySelector("book-reader"); - if (content == default) { - return default; - } + if (content is null) return default; var sb = new StringBuilder(); var chapterContent = doc.QuerySelector("book-reader").Attributes[":current_chapter"].Value; - foreach (var paragraph in HttpUtility.HtmlDecode(chapterContent).Deserialize().Paragraphs) { + foreach (var paragraph in HttpUtility.HtmlDecode(chapterContent).Deserialize().Paragraphs) sb.Append(paragraph.HtmlDecode()); - } return sb.AsHtmlDoc(); } - private IEnumerable GetToc(HtmlDocument doc, Uri url) { + private IEnumerable GetToc(HtmlDocument doc, Uri url) + { var result = doc .QuerySelectorAll("div.b-tab__content ul.list-unstyled a") - .Select(a => new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), string.IsNullOrWhiteSpace(a.GetText()) ? "Без названия" : a.GetText())) + .Select(a => new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), + string.IsNullOrWhiteSpace(a.GetText()) ? "Без названия" : a.GetText())) .ToList(); - + return SliceToc(result, c => c.Title); } - private Task GetCover(HtmlDocument doc, Uri uri) { - var imagePath = doc.QuerySelector("div.b-book_cover a[href]")?.Attributes["href"]?.Value ?? doc.QuerySelector("div.b-book_cover img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + private Task GetCover(HtmlDocument doc, Uri uri) + { + var imagePath = doc.QuerySelector("div.b-book_cover a[href]")?.Attributes["href"]?.Value ?? + doc.QuerySelector("div.b-book_cover img")?.Attributes["src"]?.Value; + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(uri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/LitmarketGetter.cs b/Core/Logic/Getters/LitmarketGetter.cs index 4590ddec..a18dad01 100644 --- a/Core/Logic/Getters/LitmarketGetter.cs +++ b/Core/Logic/Getters/LitmarketGetter.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using System.Web; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.Common; @@ -14,47 +15,48 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class LitmarketGetter(BookGetterConfig config) : GetterBase(config) { +public class LitmarketGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://litmarket.ru"); - public override async Task Init() { + public override async Task Init() + { await base.Init(); - + Config.Client.DefaultRequestHeaders.Add("X-Requested-With", "XMLHttpRequest"); - + var response = await Config.Client.GetWithTriesAsync(SystemUrl); var doc = await response.Content.ReadAsStreamAsync().ContinueWith(t => t.Result.AsHtmlDoc()); var csrf = doc.QuerySelector("[name=csrf-token]")?.Attributes["content"]?.Value; - if (string.IsNullOrWhiteSpace(csrf)) { + if (string.IsNullOrWhiteSpace(csrf)) throw new ArgumentException("Не удалось получить csrf-token", nameof(csrf)); - } - + var cookies = response.Headers.SingleOrDefault(header => header.Key == "Set-Cookie").Value; var xsrfCookie = cookies.FirstOrDefault(c => c.StartsWith("XSRF-TOKEN=")); - if (xsrfCookie == null) { - throw new ArgumentException("Не удалось получить XSRF-TOKEN", nameof(xsrfCookie)); - } - + if (xsrfCookie == null) throw new ArgumentException("Не удалось получить XSRF-TOKEN", nameof(xsrfCookie)); + var xsrf = HttpUtility.UrlDecode(xsrfCookie.Split(";")[0].Replace("XSRF-TOKEN=", "")); - + Config.Client.DefaultRequestHeaders.Add("X-CSRF-TOKEN", csrf); Config.Client.DefaultRequestHeaders.Add("X-XSRF-TOKEN", xsrf); } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { var bookId = GetId(url); url = SystemUrl.MakeRelativeUri($"/books/{bookId}"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); var content = await GetMainData(bookId); - + var blocks = await GetBlocks(content.Book.EbookId); var title = doc.GetTextBySelector("h1.card-title").ReplaceNewLine(); - var book = new Book(url.ReplaceHost(SystemUrl.Host)) { + var book = new Book(url.ReplaceHost(SystemUrl.Host)) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(GetToc(content, title), blocks, url, content.Book.EbookId), Title = title, @@ -62,110 +64,129 @@ public override async Task Get(Uri url) { Annotation = doc.QuerySelector("div.card-description")?.InnerHtml, Seria = GetSeria(doc, url) }; - + return book; } - - private static Seria GetSeria(HtmlDocument doc, Uri url) { + + private static Seria GetSeria(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("div.card-cycle a"); - if (a == default || !a.GetText().Contains('#')) { - return default; - } - + if (a is null || !a.GetText().Contains('#')) return default; + var parts = a.GetText().Split('#', StringSplitOptions.RemoveEmptyEntries); - return new Seria { + return new Seria + { Name = parts[0].HtmlDecode(), Number = parts[1].HtmlDecode(), Url = url.MakeRelativeUri(a.Attributes["href"].Value) }; } - private Author GetAuthor(HtmlDocument doc, Uri url) { + private Author GetAuthor(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("div.card-author a"); - return a == default ? - new Author(doc.GetTextBySelector("div.card-author").Replace("Автор:", "").ReplaceNewLine()): - new Author(a.GetText().ReplaceNewLine(), url.MakeRelativeUri(a.Attributes["href"].Value).ReplaceHost(SystemUrl.Host)); + return a is null + ? new Author(doc.GetTextBySelector("div.card-author").Replace("Автор:", "").ReplaceNewLine()) + : new Author(a.GetText().ReplaceNewLine(), + url.MakeRelativeUri(a.Attributes["href"].Value).ReplaceHost(SystemUrl.Host)); } - + /// - /// Авторизация в системе + /// Авторизация в системе /// /// - public override async Task Authorize() { - if (!Config.HasCredentials) { - return; - } + public override async Task Authorize() + { + if (!Config.HasCredentials) return; - var payload = new { + var payload = new + { email = Config.Options.Login, password = Config.Options.Password }; - + using var post = await Config.Client.PostAsJsonAsync(SystemUrl.MakeRelativeUri("/auth/login"), payload); - try { + try + { var data = await post.Content.ReadFromJsonAsync(); - if (data is { Success: false }) { - throw new Exception("Не удалось авторизоваться"); - } - } catch { - throw new Exception("Не удалось авторизоваться"); + if (data is { Success: false }) throw new Elib2EbookAuthException("Не удалось авторизоваться"); + } + catch + { + throw new Elib2EbookAuthException("Не удалось авторизоваться"); } } - private Task GetCover(HtmlDocument doc, Uri uri) { + private Task GetCover(HtmlDocument doc, Uri uri) + { var imagePath = doc.QuerySelector("div.front img")?.Attributes["data-src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath.AsUri().AbsolutePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(uri.MakeRelativeUri(imagePath.AsUri().AbsolutePath)) + : Task.FromResult(default(TempFile)); } - private List GetToc(Response response, string title) { + private List GetToc(Response response, string title) + { var toc = response.Toc.Deserialize>(); - if (toc?.Count == 0) { - toc = [ - new() { + if (toc?.Count == 0) + toc = + [ + new Block + { Index = 0, - Chunk = new Chunk { - Mods = [ - new Mod { + Chunk = new Chunk + { + Mods = + [ + new Mod + { Text = title } ] } } ]; - } return SliceToc(toc, c => c.Chunk.Mods[0].Text).ToList(); } - private async Task> FillChapters(List toc, Block[] blocks, Uri bookUri, long eBookId) { + private async Task> FillChapters(List toc, Block[] blocks, Uri bookUri, long eBookId) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - for (var i = 0; i < toc.Count; i++) { - var chapterTitle = string.IsNullOrWhiteSpace(toc[i].Chunk.Mods[0].Text.Trim()) ? "Без названия" : toc[i].Chunk.Mods[0].Text.Trim(); + for (var i = 0; i < toc.Count; i++) + { + var chapterTitle = string.IsNullOrWhiteSpace(toc[i].Chunk.Mods[0].Text.Trim()) + ? "Без названия" + : toc[i].Chunk.Mods[0].Text.Trim(); Config.Logger.LogInformation($"Загружаю главу {chapterTitle.CoverQuotes()}"); var sb = new StringBuilder(); var chapter = new Chapter(); - foreach (var block in blocks.Where(b => b.Index >= toc[i].Index && (i == toc.Count -1 || b.Index < toc[i + 1].Index))) { + foreach (var block in blocks.Where(b => + b.Index >= toc[i].Index && (i == toc.Count - 1 || b.Index < toc[i + 1].Index))) + { var p = new StringBuilder(); - - foreach (var mod in block.Chunk.Mods) { - switch (mod.Type) { + + foreach (var mod in block.Chunk.Mods) + switch (mod.Type) + { case "IMAGE": - p.Append($""); + p.Append( + $""); break; case "LINK": - p.Append($"{mod.Mods?.FirstOrDefault()?.Text ?? string.Empty}"); + p.Append( + $"{mod.Mods?.FirstOrDefault()?.Text ?? string.Empty}"); break; default: var text = $"{mod.Text ?? string.Empty}"; - if (mod.Styles?.Length > 0) { - foreach (var style in mod.Styles) { - switch (style) { + if (mod.Styles?.Length > 0) + foreach (var style in mod.Styles) + switch (style) + { case "ITALIC": text = text.CoverTag("em"); break; @@ -179,13 +200,10 @@ private async Task> FillChapters(List toc, Block[] blocks, Config.Logger.LogInformation(style); break; } - } - } p.Append(text); break; } - } sb.Append(p.ToString().CoverTag("p")); } @@ -201,13 +219,15 @@ private async Task> FillChapters(List toc, Block[] blocks, return result; } - private async Task GetMainData(string bookId) { + private async Task GetMainData(string bookId) + { var data = await Config.Client.GetWithTriesAsync(SystemUrl.MakeRelativeUri($"/reader/data/{bookId}")); return await data.Content.ReadFromJsonAsync(); } - private async Task GetBlocks(int eBookId) { + private async Task GetBlocks(int eBookId) + { var resp = await Config.Client.GetWithTriesAsync(SystemUrl.MakeRelativeUri($"/reader/blocks/{eBookId}")); return await resp.Content.ReadFromJsonAsync(); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/LitmirGetter.cs b/Core/Logic/Getters/LitmirGetter.cs index f5b3d27a..88bbb3b4 100644 --- a/Core/Logic/Getters/LitmirGetter.cs +++ b/Core/Logic/Getters/LitmirGetter.cs @@ -11,14 +11,19 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class LitmirGetter(BookGetterConfig config) : GetterBase(config) { +public class LitmirGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://litmir.me/"); - protected override string GetId(Uri url) => url.GetQueryParameter("b"); + protected override string GetId(Uri url) + { + return url.GetQueryParameter("b"); + } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { var bookId = GetId(url); url = SystemUrl.MakeRelativeUri($"/bd/?b={bookId}"); @@ -27,128 +32,146 @@ public override async Task Get(Uri url) { var name = doc.GetTextBySelector("div[itemprop=name]"); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(bookId, pages, name), Title = name, - Author = new Author(doc.QuerySelector("span[itemprop=author] meta")?.Attributes["content"]?.Value ?? "Litmir"), + Author = new Author(doc.QuerySelector("span[itemprop=author] meta")?.Attributes["content"]?.Value ?? + "Litmir"), Annotation = doc.QuerySelector("div[itemprop=description]")?.InnerHtml, Seria = GetSeria(doc, url) }; - - return book; + + return book; } - - private static Seria GetSeria(HtmlDocument doc, Uri url) { + + private static Seria GetSeria(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("td.bd_desc2 a[href^=/books_in_series/]"); - if (a != default) { + if (a is not null) + { var text = a.GetText(); - if (!string.IsNullOrWhiteSpace(text)) { - var seria = new Seria { + if (!string.IsNullOrWhiteSpace(text)) + { + var seria = new Seria + { Url = url.MakeRelativeUri(a.Attributes["href"].Value) }; - - if (text.Contains('#')) { + + if (text.Contains('#')) + { var split = text.Split("#"); - if (int.TryParse(split.Last(), out var number)) { + if (int.TryParse(split.Last(), out var number)) + { seria.Number = number.ToString(); seria.Name = split[0].Trim(); - } else { + } + else + { seria.Name = text; } - } else { + } + else + { seria.Name = text; } - + return seria; } - - } return default; } - - private Task GetCover(HtmlDocument doc, Uri bookUri) { + + private Task GetCover(HtmlDocument doc, Uri bookUri) + { var imagePath = doc.QuerySelector("img[jq=BookCover]")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(bookUri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } - private async Task AddChapter(ICollection chapters, Chapter chapter, StringBuilder text) { - if (chapter == null) { - return; - } - + private async Task AddChapter(ICollection chapters, Chapter chapter, StringBuilder text) + { + if (chapter == null) return; + var chapterDoc = text.AsHtmlDoc(); chapter.Images = await GetImages(chapterDoc, SystemUrl.MakeRelativeUri("/br/")); chapter.Content = chapterDoc.DocumentNode.InnerHtml; chapters.Add(chapter); } - private static bool IsChapterHeader(HtmlNode node) { + private static bool IsChapterHeader(HtmlNode node) + { return node.Name == "a" && node.Attributes["name"]?.Value?.StartsWith("section") == true; } - private static bool IsSingleChapter(IEnumerable nodes) { + private static bool IsSingleChapter(IEnumerable nodes) + { var firstNode = nodes.First(); return !IsChapterHeader(firstNode) || string.IsNullOrWhiteSpace(firstNode.InnerText); } - private static Chapter CreateChapter(string title) { - return new Chapter { + private static Chapter CreateChapter(string title) + { + return new Chapter + { Title = title }; } - private async Task GetChapter(string bookId, int page) { - using var response = await Config.Client.GetWithTriesAsync(SystemUrl.MakeRelativeUri($"/br/?b={bookId}&p={page}")); - if (response == default) { - return default; - } + private async Task GetChapter(string bookId, int page) + { + using var response = + await Config.Client.GetWithTriesAsync(SystemUrl.MakeRelativeUri($"/br/?b={bookId}&p={page}")); + if (response is null) return default; var doc = await response.Content.ReadAsStreamAsync().ContinueWith(t => t.Result.AsHtmlDoc()); - foreach (var node in doc.QuerySelectorAll("p")) { - node.Attributes.RemoveAll(); - } - + foreach (var node in doc.QuerySelectorAll("p")) node.Attributes.RemoveAll(); + return doc; } - private async Task> FillChapters(string bookId, long pages, string name) { + private async Task> FillChapters(string bookId, long pages, string name) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } - + if (Config.Options.NoChapters) return result; + Chapter chapter = null; var singleChapter = true; var text = new StringBuilder(); - - for (var i = 1; i <= pages; i++) { + + for (var i = 1; i <= pages; i++) + { Config.Logger.LogInformation($"Получаю страницу {i}/{pages}"); var page = await GetChapter(bookId, i); var nodes = page.QuerySelector("div.page_text").RemoveNodes("div[id^=adrun], script").ChildNodes; singleChapter = i == 1 ? IsSingleChapter(nodes) : singleChapter; - foreach (var node in nodes) { - if (singleChapter || !IsChapterHeader(node)) { - if (node.Name == "img" && node.Attributes["src"] != null) { + foreach (var node in nodes) + if (singleChapter || !IsChapterHeader(node)) + { + if (node.Name == "img" && node.Attributes["src"] != null) + { text.Append($""); - } else { - if (!string.IsNullOrWhiteSpace(node.InnerHtml)) { + } + else + { + if (!string.IsNullOrWhiteSpace(node.InnerHtml)) text.Append(node.InnerHtml.HtmlDecode().CoverTag("p")); - } } - } else { + } + else + { await AddChapter(result, chapter, text); text.Clear(); chapter = CreateChapter(node.InnerText.HtmlDecode()); } - } } await AddChapter(result, chapter ?? CreateChapter(name), text); return result; } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/Litnet/BooknetComGetter.cs b/Core/Logic/Getters/Litnet/BooknetComGetter.cs index 1f5e78bc..bc6b402f 100644 --- a/Core/Logic/Getters/Litnet/BooknetComGetter.cs +++ b/Core/Logic/Getters/Litnet/BooknetComGetter.cs @@ -1,8 +1,9 @@ using System; using Core.Configs; -namespace Core.Logic.Getters.Litnet; +namespace Core.Logic.Getters.Litnet; -public class BooknetComGetter(BookGetterConfig config) : LitnetGetterBase(config) { +public class BooknetComGetter(BookGetterConfig config) : LitnetGetterBase(config) +{ protected override Uri SystemUrl => new("https://booknet.com/"); -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/Litnet/BooknetUaGetter.cs b/Core/Logic/Getters/Litnet/BooknetUaGetter.cs index 761620b6..a1663c69 100644 --- a/Core/Logic/Getters/Litnet/BooknetUaGetter.cs +++ b/Core/Logic/Getters/Litnet/BooknetUaGetter.cs @@ -1,8 +1,9 @@ using System; using Core.Configs; -namespace Core.Logic.Getters.Litnet; +namespace Core.Logic.Getters.Litnet; -public class BooknetUaGetter(BookGetterConfig config) : LitnetGetterBase(config) { +public class BooknetUaGetter(BookGetterConfig config) : LitnetGetterBase(config) +{ protected override Uri SystemUrl => new("https://booknet.ua/"); -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/Litnet/LitnetGetter.cs b/Core/Logic/Getters/Litnet/LitnetGetter.cs index 66126f79..e6ff292b 100644 --- a/Core/Logic/Getters/Litnet/LitnetGetter.cs +++ b/Core/Logic/Getters/Litnet/LitnetGetter.cs @@ -1,8 +1,9 @@ using System; using Core.Configs; -namespace Core.Logic.Getters.Litnet; +namespace Core.Logic.Getters.Litnet; -public class LitnetGetter(BookGetterConfig config) : LitnetGetterBase(config) { +public class LitnetGetter(BookGetterConfig config) : LitnetGetterBase(config) +{ protected override Uri SystemUrl => new("https://litnet.com/"); -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/Litnet/LitnetGetterBase.cs b/Core/Logic/Getters/Litnet/LitnetGetterBase.cs index 9dee2b76..fd4ba740 100644 --- a/Core/Logic/Getters/Litnet/LitnetGetterBase.cs +++ b/Core/Logic/Getters/Litnet/LitnetGetterBase.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using System.Web; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.Common; @@ -21,70 +22,83 @@ namespace Core.Logic.Getters.Litnet; -public abstract class LitnetGetterBase(BookGetterConfig config) : GetterBase(config) { +public abstract class LitnetGetterBase(BookGetterConfig config) : GetterBase(config) +{ + private const string SECRET = "14a6579a984b3c6abecda6c2dfa83a64"; + + private static string DeviceId = Guid.NewGuid().ToString().ToUpperInvariant(); private Uri ApiUrl => new($"https://api.{SystemUrl.Host}/"); - + //cloudflare :( protected virtual Uri ApiIp => ApiUrl; protected virtual Uri SiteIp => SystemUrl; - private static string DeviceId = Guid.NewGuid().ToString().ToUpper(); - private const string SECRET = "14a6579a984b3c6abecda6c2dfa83a64"; - private string _token { get; set; } - protected override string GetId(Uri url) => base.GetId(url).Split('-').Last().Replace("b", string.Empty); + protected override string GetId(Uri url) + { + return base.GetId(url).Split('-').Last().Replace("b", string.Empty); + } - private static byte[] Decrypt(string text) { + private static byte[] Decrypt(string text) + { using var aes = Aes.Create(); const int IV_SHIFT = 16; - - aes.Key = Encoding.UTF8.GetBytes(SECRET); + + aes.Key = Encoding.UTF8.GetBytes(SECRET); aes.IV = Encoding.UTF8.GetBytes(text)[..IV_SHIFT]; - + var decryptor = aes.CreateDecryptor(); using var ms = new MemoryStream(Convert.FromBase64String(text)); using var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read); - + var output = new MemoryStream(); cs.CopyTo(output); return output.ToArray()[IV_SHIFT..]; } - private static string GetSign(string token) { + private static string GetSign(string token) + { var inputBytes = Encoding.ASCII.GetBytes(DeviceId + SECRET + (token ?? string.Empty)); var hashBytes = MD5.HashData(inputBytes); - return Convert.ToHexString(hashBytes).ToLower(); + return Convert.ToHexString(hashBytes).ToLowerInvariant(); } /// - /// Авторизация в системе + /// Авторизация в системе /// /// - public override async Task Authorize() { + public override async Task Authorize() + { var path = Config.HasCredentials ? "user/find-by-login" : "registration/registration-by-device"; - const string directory = "LNCache"; + var saveCreds = string.Empty; + var useAuthCache = Config.HasCredentials; + if (useAuthCache) + { + const string directory = "LNCache"; + if (!Directory.Exists(directory)) Directory.CreateDirectory(directory); - if (!Directory.Exists(directory)) { - Directory.CreateDirectory(directory); - } + var cacheKey = Config.Options.Login?.RemoveInvalidChars(); + saveCreds = !string.IsNullOrWhiteSpace(cacheKey) ? $"{directory}/{cacheKey}" : string.Empty; + useAuthCache = !string.IsNullOrWhiteSpace(saveCreds); - var saveCreds = $"{directory}/{Config.Options.Login.RemoveInvalidChars()}"; - if (Config.HasCredentials) { - if (File.Exists(saveCreds)) { + if (useAuthCache && File.Exists(saveCreds)) + { var timeDiff = DateTime.Now - File.GetLastWriteTime(saveCreds); - if ( timeDiff.TotalHours < 2 ) + if (timeDiff.TotalHours < 2) { - var savedAuth = await File.ReadAllTextAsync(saveCreds).ContinueWith(t => t.Result.Deserialize()); + var savedAuth = await File.ReadAllTextAsync(saveCreds).ContinueWith(t => + t.Result.Deserialize()); _token = savedAuth.Token; DeviceId = savedAuth.DeviceID; - if( !string.IsNullOrWhiteSpace(_token) ) + if (!string.IsNullOrWhiteSpace(_token)) { + Config.Logger.LogInformation("Litnet: auth cache hit"); return; } } @@ -95,65 +109,77 @@ public override async Task Authorize() { } } - var url = ApiIp.MakeRelativeUri($"v1/{path}?login={HttpUtility.UrlEncode(Config.Options.Login?.TrimStart('+') ?? string.Empty)}&password={HttpUtility.UrlEncode(Config.Options.Password)}&app=android&device_id={DeviceId}&sign={GetSign(string.Empty)}"); + var url = ApiIp.MakeRelativeUri( + $"v1/{path}?login={HttpUtility.UrlEncode(Config.Options.Login?.TrimStart('+') ?? string.Empty)}&password={HttpUtility.UrlEncode(Config.Options.Password)}&app=android&device_id={DeviceId}&sign={GetSign(string.Empty)}"); var data = await GetApiData(url); - if (!string.IsNullOrWhiteSpace(data?.Token)) { + if (!string.IsNullOrWhiteSpace(data?.Token)) + { Config.Logger.LogInformation("Успешно авторизовались"); - data.DeviceID = DeviceId; _token = data.Token; - } else { - throw new Exception($"Не удалось авторизоваться. {data?.Error}"); } + else + { + throw new Elib2EbookAuthException($"Не удалось авторизоваться. {data?.Error}"); + } + + if (useAuthCache) + { + if (data is not null) data.DeviceID = DeviceId; - if (Config.HasCredentials) { await File.WriteAllTextAsync(saveCreds, JsonSerializer.Serialize(data)); } } - private async Task GetBook(string token, string bookId) { - var url = ApiIp.MakeRelativeUri($"/v1/book/get/{bookId}?app=android&device_id={DeviceId}&user_token={token}&sign={GetSign(token)}"); + private async Task GetBook(string token, string bookId) + { + var url = ApiIp.MakeRelativeUri( + $"/v1/book/get/{bookId}?app=android&device_id={DeviceId}&user_token={token}&sign={GetSign(token)}"); var data = await GetApiData(url); - if (!Config.HasCredentials && data!.AdultOnly) { - throw new Exception("Произведение 18+. Необходимо добавить логин и пароль."); - } - + if (!Config.HasCredentials && data!.AdultOnly) + throw new Elib2EbookAuthException("Произведение 18+. Необходимо добавить логин и пароль."); + return data; } - private Task GetBookContents(string token, string bookId) { - var url = ApiIp.MakeRelativeUri($"/v1/book/contents?bookId={bookId}&app=android&device_id={DeviceId}&user_token={token}&sign={GetSign(token)}"); + private Task GetBookContents(string token, string bookId) + { + var url = ApiIp.MakeRelativeUri( + $"/v1/book/contents?bookId={bookId}&app=android&device_id={DeviceId}&user_token={token}&sign={GetSign(token)}"); return GetApiData(url); } - private async Task> GetToc(string token, IEnumerable contents) { + private async Task> GetToc(string token, + IEnumerable contents) + { var chapters = string.Join("&", contents.Select(t => $"chapter_ids[]={t.Id}")); - var url = ApiIp.MakeRelativeUri($"/v1/book/get-chapters-texts/?{chapters}&app=android&device_id={DeviceId}&sign={GetSign(token)}&user_token={token}"); + var url = ApiIp.MakeRelativeUri( + $"/v1/book/get-chapters-texts/?{chapters}&app=android&device_id={DeviceId}&sign={GetSign(token)}&user_token={token}"); var data = await GetApiData(url); return SliceToc(data, c => c.Text); } - private async Task GetApiData(Uri uri) { + private async Task GetApiData(Uri uri) + { var response = await Config.Client.SendAsync(GetDefaultMessage(uri, ApiUrl)); - if (response.StatusCode == HttpStatusCode.Forbidden) { - throw new Exception("Получен бан. Попробуйте позже."); - } + if (response.StatusCode == HttpStatusCode.Forbidden) + throw new Elib2EbookParseException("Получен бан. Попробуйте позже."); + + if (response.StatusCode == HttpStatusCode.NotFound) return default; - if (response.StatusCode == HttpStatusCode.NotFound) { - return default; - } - var data = await response.Content.ReadFromJsonAsync(); return data; } - - public override async Task Get(Uri url) { + + public override async Task Get(Uri url) + { var bookId = GetId(url); var litnetBook = await GetBook(_token, bookId); var uri = SystemUrl.MakeRelativeUri(litnetBook.Url.AsUri().AbsolutePath); - var book = new Book(uri) { + var book = new Book(uri) + { Cover = await GetCover(litnetBook), Chapters = await FillChapters(_token, litnetBook, bookId), Title = litnetBook.Title.Trim(), @@ -163,98 +189,108 @@ public override async Task Get(Uri url) { Seria = await GetSeria(uri, litnetBook), Lang = litnetBook.Lang }; - + return book; } - private async Task GetSeria(Uri url, LitnetBookResponse book) { - try { - var doc = await Config.Client.SendAsync(GetDefaultMessage(url.ReplaceHost(SiteIp.Host), SystemUrl)).ContinueWith(t => t.Result.Content.ReadAsStream().AsHtmlDoc()); + private async Task GetSeria(Uri url, LitnetBookResponse book) + { + try + { + var doc = await Config.Client.SendAsync(GetDefaultMessage(url.ReplaceHost(SiteIp.Host), SystemUrl)) + .ContinueWith(t => t.Result.Content.ReadAsStream().AsHtmlDoc()); var a = doc.QuerySelector("div.book-view-info-coll a[href*='sort=cycles']"); - if (a != default) { - return new Seria { + if (a is not null) + return new Seria + { Name = a.GetText(), Url = url.MakeRelativeUri(a.Attributes["href"].Value), Number = book.CyclePriority is > 0 ? book.CyclePriority.Value.ToString() : string.Empty }; - } - } catch (Exception ex) { + } + catch (Exception ex) + { Config.Logger.LogInformation(ex.ToString()); } return default; } - private Author GetAuthor(LitnetBookResponse book) { - return new Author((book.AuthorName ?? SystemUrl.Host).Trim(), SystemUrl.MakeRelativeUri($"/ru/{book.AuthorId}")); + private Author GetAuthor(LitnetBookResponse book) + { + return new Author((book.AuthorName ?? SystemUrl.Host).Trim(), + SystemUrl.MakeRelativeUri($"/ru/{book.AuthorId}")); } - - private IEnumerable GetCoAuthors(LitnetBookResponse book) { + + private IEnumerable GetCoAuthors(LitnetBookResponse book) + { var result = new List(); - if (!string.IsNullOrWhiteSpace(book.CoAuthorName) && book.CoAuthorId.HasValue) { - result.Add(new Author((book.CoAuthorName ?? SystemUrl.Host).Trim(), SystemUrl.MakeRelativeUri($"/ru/{book.CoAuthorId.Value}"))); - } + if (!string.IsNullOrWhiteSpace(book.CoAuthorName) && book.CoAuthorId.HasValue) + result.Add(new Author((book.CoAuthorName ?? SystemUrl.Host).Trim(), + SystemUrl.MakeRelativeUri($"/ru/{book.CoAuthorId.Value}"))); return result; } - private static string GetAnnotation(LitnetBookResponse book) { - return string.IsNullOrWhiteSpace(book.Annotation) ? - string.Empty : - string.Join("", book.Annotation.Split("\n", StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim().CoverTag("p"))); + private static string GetAnnotation(LitnetBookResponse book) + { + return string.IsNullOrWhiteSpace(book.Annotation) + ? string.Empty + : string.Join("", + book.Annotation.Split("\n", StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim().CoverTag("p"))); } - - private Task GetCover(LitnetBookResponse book) { - return !string.IsNullOrWhiteSpace(book.Cover) ? SaveImage(book.Cover.AsUri()) : Task.FromResult(default(TempFile)); + + private Task GetCover(LitnetBookResponse book) + { + return !string.IsNullOrWhiteSpace(book.Cover) + ? SaveImage(book.Cover.AsUri()) + : Task.FromResult(default(TempFile)); } - - protected override HttpRequestMessage GetImageRequestMessage(Uri uri) { - if (uri.IsSameHost(SystemUrl) || uri.IsSameSubDomain(SystemUrl)) { + + protected override HttpRequestMessage GetImageRequestMessage(Uri uri) + { + if (uri.IsSameHost(SystemUrl) || uri.IsSameSubDomain(SystemUrl)) return GetDefaultMessage(SystemUrl.MakeRelativeUri(uri.AbsolutePath), uri); - } return base.GetImageRequestMessage(uri); } - - private HttpRequestMessage GetDefaultMessage(Uri uri, Uri host, HttpContent content = null) { - var message = new HttpRequestMessage(content == default ? HttpMethod.Get : HttpMethod.Post, uri); + + private HttpRequestMessage GetDefaultMessage(Uri uri, Uri host, HttpContent content = null) + { + var message = new HttpRequestMessage(content is null ? HttpMethod.Get : HttpMethod.Post, uri); message.Content = content; message.Version = Config.Client.DefaultRequestVersion; - - foreach (var header in Config.Client.DefaultRequestHeaders) { - message.Headers.Add(header.Key, header.Value); - } + + foreach (var header in Config.Client.DefaultRequestHeaders) message.Headers.Add(header.Key, header.Value); message.Headers.Host = host.Host; return message; } - private async Task> FillChapters(string token, LitnetBookResponse book, string bookId) { + private async Task> FillChapters(string token, LitnetBookResponse book, string bookId) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } - + if (Config.Options.NoChapters) return result; + var contents = await GetBookContents(token, bookId); - if (contents == default || contents.Length == 0) { - return result; - } - + if (contents is null || contents.Length == 0) return result; + var chapters = await GetToc(token, contents); var map = chapters.ToDictionary(t => t.Id); - - foreach (var content in contents) { - if (!map.TryGetValue(content.Id, out var litnetChapter)) { - litnetChapter = new LitnetChapterResponse(); - } + + foreach (var content in contents) + { + if (!map.TryGetValue(content.Id, out var litnetChapter)) litnetChapter = new LitnetChapterResponse(); Config.Logger.LogInformation($"Загружаю главу {content.Title.Trim().CoverQuotes()}"); - var chapter = new Chapter { + var chapter = new Chapter + { Title = (content.Title ?? book.Title).Trim() }; - - if (!string.IsNullOrWhiteSpace(litnetChapter.Text)) { + + if (!string.IsNullOrWhiteSpace(litnetChapter.Text)) + { var chapterDoc = GetChapterDoc(Encoding.UTF8.GetString(Decrypt(litnetChapter.Text))); chapter.Images = await GetImages(chapterDoc, SystemUrl); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -266,7 +302,8 @@ private async Task> FillChapters(string token, LitnetBookResponse return result; } - private static HtmlDocument GetChapterDoc(string text) { + private static HtmlDocument GetChapterDoc(string text) + { return text.Deserialize().Aggregate(new StringBuilder(), (sb, row) => sb.Append(row)).AsHtmlDoc(); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/LitresGetter.cs b/Core/Logic/Getters/LitresGetter.cs index 4cee5056..3a386fc9 100644 --- a/Core/Logic/Getters/LitresGetter.cs +++ b/Core/Logic/Getters/LitresGetter.cs @@ -12,6 +12,7 @@ using System.Text.Json; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Misc; using Core.Types.Book; @@ -22,85 +23,100 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class LitresGetter(BookGetterConfig config) : GetterBase(config) { +public class LitresGetter(BookGetterConfig config) : GetterBase(config) +{ private const string SECRET_KEY = "AsAAfdV000-1kksn6591x:[}A{}<> new("https://www.litres.ru"); protected static Uri ApiUrl => new("https://api.litres.ru"); - - private static Uri GetShortUri(LitresArt art) => new($"https://catalit.litres.ru/pub/t/{art.Id}.fb3"); - private static long GetCurrentMilli() { - var jan1970 = new DateTime(1970, 1, 1, 0, 0, 0,DateTimeKind.Utc); + private static Uri GetShortUri(LitresArt art) + { + return new Uri($"https://catalit.litres.ru/pub/t/{art.Id}.fb3"); + } + + private static long GetCurrentMilli() + { + var jan1970 = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); var javaSpan = DateTime.UtcNow - jan1970; return (long)javaSpan.TotalMilliseconds / 1000; } - private Uri GetFullUri(LitresArt art, string path, LitresFile file, string host = null) { + private Uri GetFullUri(LitresArt art, string path, LitresFile file, string host = null) + { var ts = GetCurrentMilli(); var inputBytes = Encoding.ASCII.GetBytes($"{ts}:{art.Id}:{SECRET_KEY}"); var hashBytes = MD5.HashData(inputBytes); - + // Текстовая книга - if (art.ArtType != LitresArtTypeEnum.Audio) { - var result = new Uri($"https://{host ?? "catalit.litres.ru"}/pages/{path}?art={art.Id}&sid={_authData.Sid}&uilang=ru&libapp={APP}×tamp={ts}&md5={Convert.ToHexString(hashBytes).ToLower()}"); - if (file == default) { - return result.AppendQueryParameter("type", "fb3"); - } - + if (art.ArtType != LitresArtTypeEnum.Audio) + { + var result = + new Uri( + $"https://{host ?? "catalit.litres.ru"}/pages/{path}?art={art.Id}&sid={_authData.Sid}&uilang=ru&libapp={APP}×tamp={ts}&md5={Convert.ToHexString(hashBytes).ToLowerInvariant()}"); + if (file is null) return result.AppendQueryParameter("type", "fb3"); + result = result.AppendQueryParameter("file", file.Id); - if (!string.IsNullOrWhiteSpace(file.Extension)) { + if (!string.IsNullOrWhiteSpace(file.Extension)) result = result.AppendQueryParameter("type", file.Extension); - } return result; } - + // Аудиокнига - return new Uri($"https://{host ?? "ios.litres.ru"}/pages/{path}/{art.Id}/{file.Id}.mp3?sid={_authData.Sid}&uilang=ru&libapp={APP}×tamp={ts}&md5={Convert.ToHexString(hashBytes).ToLower()}"); + return new Uri( + $"https://{host ?? "ios.litres.ru"}/pages/{path}/{art.Id}/{file.Id}.mp3?sid={_authData.Sid}&uilang=ru&libapp={APP}×tamp={ts}&md5={Convert.ToHexString(hashBytes).ToLowerInvariant()}"); } - private LitresAuthResponseData _authData = new(){ - Sid = "6ufp4b2wbx1acc4k1f0wdo571762c0fn" - }; - - private LitresMe _me; - - public override async Task Authorize() { - if (string.IsNullOrEmpty(Config.Options.Token) && !Config.HasCredentials ) { + public override async Task Authorize() + { + if (string.IsNullOrEmpty(Config.Options.Token) && !Config.HasCredentials) + { Config.Client.DefaultRequestHeaders.Add("Session-Id", _authData.Sid); return; } - if (!string.IsNullOrEmpty(Config.Options.Token)) { + if (!string.IsNullOrEmpty(Config.Options.Token)) + { Config.Logger.LogInformation("Подставлен токен"); _authData.Sid = Config.Options.Token; Config.Client.DefaultRequestHeaders.Add("Session-Id", Config.Options.Token); return; } - + // Литрес очень не любит много авторизовывать // поэтому пришлось добавить кеширование токенов const string directory = "LitresCache"; - if (!Directory.Exists(directory)) { - Directory.CreateDirectory(directory); - } + if (!Directory.Exists(directory)) Directory.CreateDirectory(directory); var saveCreds = $"{directory}/{Config.Options.Login.RemoveInvalidChars()}"; - if (File.Exists(saveCreds)) { + if (File.Exists(saveCreds)) + { var timeDiff = DateTime.Now - File.GetLastWriteTime(saveCreds); - if ( timeDiff.TotalHours < 4 ) + if (timeDiff.TotalHours < 4) { - _authData = await File.ReadAllTextAsync(saveCreds).ContinueWith(t => t.Result.Deserialize());; + _authData = await File.ReadAllTextAsync(saveCreds) + .ContinueWith(t => t.Result.Deserialize()); + ; Config.Client.DefaultRequestHeaders.Add("Session-Id", _authData.Sid); - - var checkResponse = await Config.Client.GetFromJsonAsync>(ApiUrl.MakeRelativeUri("/foundation/api/users/me/detailed")); - if (checkResponse?.Payload?.Data != default) { + + var checkResponse = + await Config.Client.GetFromJsonAsync>( + ApiUrl.MakeRelativeUri("/foundation/api/users/me/detailed")); + if (checkResponse?.Payload?.Data is not null) + { _me = checkResponse.Payload.Data; return; } @@ -111,70 +127,77 @@ public override async Task Authorize() { } } - var auth = new Dictionary { + var auth = new Dictionary + { { "login", Config.Options.Login }, { "password", Config.Options.Password } }; - var authPayload = new StringContent( JsonSerializer.Serialize(auth) ); + var authPayload = new StringContent(JsonSerializer.Serialize(auth)); authPayload.Headers.ContentType = new MediaTypeHeaderValue("application/json"); Config.Client.DefaultRequestHeaders.Remove("app-id"); - Config.Client.DefaultRequestHeaders.Add("app-id","115"); - var authResponse = await Config.Client.PostWithTriesAsync(ApiUrl.MakeRelativeUri("/foundation/api/auth/login"), authPayload); + Config.Client.DefaultRequestHeaders.Add("app-id", "115"); + var authResponse = + await Config.Client.PostWithTriesAsync(ApiUrl.MakeRelativeUri("/foundation/api/auth/login"), authPayload); Config.Client.DefaultRequestHeaders.Remove("app-id"); - _authData = await authResponse.Content.ReadAsStringAsync().ContinueWith(t => + _authData = await authResponse.Content.ReadAsStringAsync().ContinueWith(t => { var authData = t.Result.Deserialize(); var newAuthData = new LitresAuthResponseData(); - newAuthData.Success = authData.Status==200; + newAuthData.Success = authData.Status == 200; newAuthData.Sid = authData.Payload.Data.Sid; newAuthData.ErrorMessage = authData.Error; return newAuthData; }); - if (!_authData.Success) { - throw new Exception($"Не удалось авторизоваться. {_authData.ErrorMessage}"); - } - + if (!_authData.Success) + throw new Elib2EbookAuthException($"Не удалось авторизоваться. {_authData.ErrorMessage}"); + Config.Client.DefaultRequestHeaders.Add("Session-Id", _authData.Sid); - var meResponse = await Config.Client.GetFromJsonAsync>("https://api.litres.ru/foundation/api/users/me/detailed"); - if (meResponse?.Payload?.Data == default) { - return; - } + var meResponse = + await Config.Client.GetFromJsonAsync>( + "https://api.litres.ru/foundation/api/users/me/detailed"); + if (meResponse?.Payload?.Data is null) return; _me = meResponse.Payload.Data; await File.WriteAllTextAsync(saveCreds, JsonSerializer.Serialize(_authData)); } - private async Task GetResponse(LitresPayload payload) { - var resp = await Config.Client.PostWithTriesAsync("https://catalit.litres.ru/catalitv2".AsUri(), CreatePayload(payload)); - return await resp.Content.ReadAsStringAsync().ContinueWith(t => { + private async Task GetResponse(LitresPayload payload) + { + var resp = await Config.Client.PostWithTriesAsync("https://catalit.litres.ru/catalitv2".AsUri(), + CreatePayload(payload)); + return await resp.Content.ReadAsStringAsync().ContinueWith(t => + { return t.Result.Deserialize>().Data; }); } - - private async Task GetResponse(Uri url) { + + private async Task GetResponse(Uri url) + { var resp = await Config.Client.GetWithTriesAsync(url); - if (resp == default) { - return default; - } - - return await resp.Content.ReadAsStringAsync().ContinueWith(t => { + if (resp is null) return default; + + return await resp.Content.ReadAsStringAsync().ContinueWith(t => + { return t.Result.Deserialize>().Payload.Data; }); } - private static FormUrlEncodedContent CreatePayload(LitresPayload payload) { - var d = new Dictionary { + private static FormUrlEncodedContent CreatePayload(LitresPayload payload) + { + var d = new Dictionary + { ["jdata"] = JsonSerializer.Serialize(payload) }; return new FormUrlEncodedContent(d); } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { var bookId = GetBookId(url); var art = new LitresArt(); @@ -182,9 +205,12 @@ public override async Task Get(Uri url) { { art = await GetResponse(ApiUrl.MakeRelativeUri($"/foundation/api/arts/{bookId}")); } - catch (System.Exception){} - - var book = new Book(SystemUrl.MakeRelativeUri(bookId)) { + catch (Exception) + { + } + + var book = new Book(SystemUrl.MakeRelativeUri(bookId)) + { Cover = await GetCover(art.Cover), Title = art.Title, Author = await GetAuthor(art), @@ -192,128 +218,149 @@ public override async Task Get(Uri url) { Annotation = art.Annotation, Seria = GetSeria(art) }; - + await FillAdditional(book, art); - foreach (var linked in art.LinkedArts ?? []) { - await FillAdditional(book, linked); - } - + foreach (var linked in art.LinkedArts ?? []) await FillAdditional(book, linked); + var fb3File = book.AdditionalFiles.Get(AdditionalTypeEnum.Books).FirstOrDefault(f => f.Extension == ".fb3"); - if (fb3File == default) { + if (fb3File is null) Config.Logger.LogInformation("Нет файла fb3. Сформировать файл книги невозможно"); - } else { - try { + else + try + { await using var stream = fb3File.GetStream(); var litresBook = await GetBook(stream); book.Chapters = await FillChapters(litresBook, art.Title); - } catch (Exception) { + } + catch (Exception) + { Config.Logger.LogInformation($"Не удалось обработать оригинальный файл {fb3File.FullName}"); } - } return book; } - private async Task FillAdditional(Book book, LitresArt art) { - if (_authData != default) { + private async Task FillAdditional(Book book, LitresArt art) + { + if (_authData is not null) + { var type = art.ArtType == LitresArtTypeEnum.Audio ? AdditionalTypeEnum.Audio : AdditionalTypeEnum.Books; - - if (Config.Options.HasAdditionalType(type)) { - var files = await GetResponse(ApiUrl.MakeRelativeUri($"/foundation/api/arts/{art.Id}/files/grouped")); - foreach (var file in files.SelectMany(f => f.Files)) { + if (Config.Options.HasAdditionalType(type)) + { + var files = await GetResponse( + ApiUrl.MakeRelativeUri($"/foundation/api/arts/{art.Id}/files/grouped")); + + foreach (var file in files.SelectMany(f => f.Files)) + { using var fileResponse = await GetFileResponse(art, file); - if (fileResponse != default) { + if (fileResponse is not null) + { var tempFile = await CreateTempFile(fileResponse); - book.AdditionalFiles.Add(art.ArtType != LitresArtTypeEnum.Audio ? AdditionalTypeEnum.Books : AdditionalTypeEnum.Audio, tempFile); + book.AdditionalFiles.Add( + art.ArtType != LitresArtTypeEnum.Audio + ? AdditionalTypeEnum.Books + : AdditionalTypeEnum.Audio, tempFile); } } - } else if (art.ArtType != LitresArtTypeEnum.Audio) { + } + else if (art.ArtType != LitresArtTypeEnum.Audio) + { using var fileResponse = await GetFileResponse(art, null); - if (fileResponse != default) { + if (fileResponse is not null) + { var tempFile = await CreateTempFile(fileResponse); book.AdditionalFiles.Add(AdditionalTypeEnum.Books, tempFile); - } } } - if (_authData == default || (book.AdditionalFiles.Get(AdditionalTypeEnum.Books).Count == 0 && art.ArtType != LitresArtTypeEnum.Audio)) { + if (_authData is null || (book.AdditionalFiles.Get(AdditionalTypeEnum.Books).Count == 0 && + art.ArtType != LitresArtTypeEnum.Audio)) + { var fileResponse = await GetShortBook(art); - if (fileResponse != default) { + if (fileResponse is not null) book.AdditionalFiles.Add(AdditionalTypeEnum.Books, await CreateTempFile(fileResponse)); - } } } - private async Task CreateTempFile(HttpResponseMessage response) { - return await TempFile.Create(response.RequestMessage.RequestUri, Config.TempFolder.Path, response.Content.Headers.ContentDisposition?.FileName?.Trim('\"') ?? response.RequestMessage.RequestUri.GetFileName(), await response.Content.ReadAsStreamAsync()); + private async Task CreateTempFile(HttpResponseMessage response) + { + return await TempFile.Create(response.RequestMessage.RequestUri, Config.TempFolder.Path, + response.Content.Headers.ContentDisposition?.FileName?.Trim('\"') ?? + response.RequestMessage.RequestUri.GetFileName(), await response.Content.ReadAsStreamAsync()); } - private static string GetBookId(Uri url) { + private static string GetBookId(Uri url) + { var art = url.GetQueryParameter("art"); - if (!string.IsNullOrWhiteSpace(art)) { - return art; - } - + if (!string.IsNullOrWhiteSpace(art)) return art; + art = url.GetSegment(url.Segments.Length - 1).Split("-").Last(); - if (long.TryParse(art, out _)) { - return art; - } + if (long.TryParse(art, out _)) return art; - throw new Exception("Не удалось определить artId"); + throw new Elib2EbookParseException("Не удалось определить artId"); } - private Seria GetSeria(LitresArt art) { + private Seria GetSeria(LitresArt art) + { return art .Sequences - .Select(s => new Seria { - Name = s.Name, - Url = SystemUrl.MakeRelativeUri("serii-knig/").AppendQueryParameter("id", s.Id), + .Select(s => new Seria + { + Name = s.Name, + Url = SystemUrl.MakeRelativeUri("serii-knig/").AppendQueryParameter("id", s.Id), Number = s.SequenceNumber }).FirstOrDefault(); } - - private async Task GetAuthor(LitresArt art) { + + private async Task GetAuthor(LitresArt art) + { var person = art.Persons.FirstOrDefault(a => a.Role == "author"); - var author = person == null ? default : await GetResponse>(ApiUrl.MakeRelativeUri($"/foundation/api/persons/{person.Id}")); - return author == default ? - new Author("Litres") : - new Author(author.FullName, SystemUrl.MakeRelativeUri(author.Url)); + var author = person == null + ? default + : await GetResponse>(ApiUrl.MakeRelativeUri($"/foundation/api/persons/{person.Id}")); + return author is null + ? new Author("Litres") + : new Author(author.FullName, SystemUrl.MakeRelativeUri(author.Url)); } - - private async Task> GetCoAuthors(LitresArt art) { + + private async Task> GetCoAuthors(LitresArt art) + { var result = new List(); - foreach (var person in art.Persons.Where(a => a.Role == "author").Skip(1)) { - var author = person == null ? default : await GetResponse>(ApiUrl.MakeRelativeUri($"/foundation/api/persons/{person.Id}")); - if (author != default) { - result.Add(new Author(author.FullName, SystemUrl.MakeRelativeUri(author.Url))); - } + foreach (var person in art.Persons.Where(a => a.Role == "author").Skip(1)) + { + var author = person == null + ? default + : await GetResponse>(ApiUrl.MakeRelativeUri($"/foundation/api/persons/{person.Id}")); + if (author is not null) result.Add(new Author(author.FullName, SystemUrl.MakeRelativeUri(author.Url))); } return result; } - - private Task GetCover(string imagePath) { - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(SystemUrl.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + + private Task GetCover(string imagePath) + { + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(SystemUrl.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } - private async Task> FillChapters(LitresBook book, string title) { + private async Task> FillChapters(LitresBook book, string title) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } - - foreach (var section in book.Content.QuerySelectorAll("section")) { - if (section.QuerySelector("> section") != null) { - continue; - } - - var chapter = new Chapter { + if (Config.Options.NoChapters) return result; + + foreach (var section in book.Content.QuerySelectorAll("section")) + { + if (section.QuerySelector("> section") != null) continue; + + var chapter = new Chapter + { Title = (section.GetTextBySelector("title") ?? title).ReplaceNewLine() }; - + section.RemoveNodes("title, note, clipped"); chapter.Images = await GetImages(section, book); chapter.Content = section.InnerHtml; @@ -323,21 +370,26 @@ private async Task> FillChapters(LitresBook book, string ti return result; } - private async Task> GetImages(HtmlNode doc, LitresBook book) { + private async Task> GetImages(HtmlNode doc, LitresBook book) + { var images = new List(); - foreach (var img in doc.QuerySelectorAll("img")) { + foreach (var img in doc.QuerySelectorAll("img")) + { var path = img.Attributes["src"]?.Value; - if (string.IsNullOrWhiteSpace(path)) { + if (string.IsNullOrWhiteSpace(path)) + { img.Remove(); continue; } - - if (!book.Targets.TryGetValue(path, out var t)) { + + if (!book.Targets.TryGetValue(path, out var t)) + { img.Remove(); continue; } - - if (t?.Content == null || t.Content.Length == 0) { + + if (t?.Content == null || t.Content.Length == 0) + { img.Remove(); continue; } @@ -351,71 +403,72 @@ private async Task> GetImages(HtmlNode doc, LitresBook boo return images; } - private async Task GetFileResponse(LitresArt art, LitresFile file) { - var paths = new []{"download_book_j", "download_book_subscr", "download_my_book_j"}; + private async Task GetFileResponse(LitresArt art, LitresFile file) + { + var paths = new[] { "download_book_j", "download_book_subscr", "download_my_book_j" }; var hosts = new List { null, SystemUrl.Host }; - if (_me?.PartnerSubscriptions?.Subscriptions != default) { - hosts.AddRange(_me.PartnerSubscriptions.Subscriptions.Where(s => s.IsActive && s.Type == (art.ArtType == LitresArtTypeEnum.Audio ? "audio" : "text")).Select(s => s.Host).Distinct()); - } - - if (_authData != null) { - foreach (var host in hosts) { - foreach (var path in paths) { - var uri = GetFullUri(art, path, file, host); - var response = await Config.Client.GetAsync(uri); - if (response.StatusCode == HttpStatusCode.OK && response.Headers.AcceptRanges.Count != 0) { - Config.Logger.LogInformation($"Дополнительный файл доступен по ссылке {uri}"); - return response; - } + if (_me?.PartnerSubscriptions?.Subscriptions is not null) + hosts.AddRange(_me.PartnerSubscriptions.Subscriptions + .Where(s => s.IsActive && s.Type == (art.ArtType == LitresArtTypeEnum.Audio ? "audio" : "text")) + .Select(s => s.Host).Distinct()); + + if (_authData != null) + foreach (var host in hosts) + foreach (var path in paths) + { + var uri = GetFullUri(art, path, file, host); + var response = await Config.Client.GetAsync(uri); + if (response.StatusCode == HttpStatusCode.OK && response.Headers.AcceptRanges.Count != 0) + { + Config.Logger.LogInformation($"Дополнительный файл доступен по ссылке {uri}"); + return response; } } - } return default; } - private async Task GetShortBook(LitresArt art) { + private async Task GetShortBook(LitresArt art) + { var shortUri = GetShortUri(art); Config.Logger.LogInformation($"Дополнительный файл доступен по ссылке {shortUri}"); return await Config.Client.GetAsync(shortUri); } - private async Task GetBook(Stream stream) { + private async Task GetBook(Stream stream) + { var result = new LitresBook(); - + var map = new Dictionary(); using var zip = new ZipArchive(stream); - foreach (var entry in zip.Entries) { + foreach (var entry in zip.Entries) + { using var ms = new MemoryStream(); await entry.Open().CopyToAsync(ms); map[entry.FullName.Replace("fb3/", string.Empty)] = ms.ToArray(); } - foreach (var (key, value) in map) { - if (key.EndsWith("body.xml")) { + foreach (var (key, value) in map) + if (key.EndsWith("body.xml")) result.Content = Encoding.UTF8.GetString(value).AsXHtmlDoc(); - } else if (key.EndsWith("body.xml.rels")) { - foreach (var r in Encoding.UTF8.GetString(value).AsXHtmlDoc().QuerySelectorAll("Relationship")) { + else if (key.EndsWith("body.xml.rels")) + foreach (var r in Encoding.UTF8.GetString(value).AsXHtmlDoc().QuerySelectorAll("Relationship")) + { var id = r.Attributes["Id"]?.Value; var target = r.Attributes["Target"]?.Value; - if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(target)) { - continue; - } + if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(target)) continue; - if (!map.TryGetValue(target, out var t)) { - continue; - } - - result.Targets[id] = new LitresTarget { + if (!map.TryGetValue(target, out var t)) continue; + + result.Targets[id] = new LitresTarget + { Id = id, Target = target, Content = t }; } - } - } return result; } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/LitsovetGetter.cs b/Core/Logic/Getters/LitsovetGetter.cs index 90009993..aa4433a8 100644 --- a/Core/Logic/Getters/LitsovetGetter.cs +++ b/Core/Logic/Getters/LitsovetGetter.cs @@ -5,6 +5,7 @@ using System.Net.Http.Json; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.Common; @@ -13,83 +14,96 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class LitsovetGetter(BookGetterConfig config) : GetterBase(config) { +public class LitsovetGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://litsovet.ru/"); - protected override string GetId(Uri url) => url.GetSegment(2); - public override async Task Authorize() { - if (!Config.HasCredentials) { - return; - } - - using var post = await Config.Client.PostAsync(SystemUrl.MakeRelativeUri("/ajax/login_signup_remind"), GenerateAuthData()); + protected override string GetId(Uri url) + { + return url.GetSegment(2); + } + + public override async Task Authorize() + { + if (!Config.HasCredentials) return; + + using var post = await Config.Client.PostAsync(SystemUrl.MakeRelativeUri("/ajax/login_signup_remind"), + GenerateAuthData()); var response = await post.Content.ReadFromJsonAsync(); - if (response.Ok == 1) { + if (response.Ok == 1) Config.Logger.LogInformation("Успешно авторизовались"); - } else { - throw new Exception("Не удалось авторизоваться."); - } + else + throw new Elib2EbookAuthException("Не удалось авторизоваться."); } - private FormUrlEncodedContent GenerateAuthData() { - var payload = new Dictionary { + private FormUrlEncodedContent GenerateAuthData() + { + var payload = new Dictionary + { { "action", "login" }, { "email", Config.Options.Login }, - { "password", Config.Options.Password }, + { "password", Config.Options.Password } }; return new FormUrlEncodedContent(payload); } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { url = SystemUrl.MakeRelativeUri("/books/" + GetId(url)); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(url), Title = doc.GetTextBySelector("h1"), Author = GetAuthor(doc), Annotation = doc.QuerySelector("div.book-informate div.item-descr")?.InnerHtml }; - + return book; } - private Author GetAuthor(HtmlDocument doc) { + private Author GetAuthor(HtmlDocument doc) + { var a = doc.QuerySelector("div.p-book-author a"); return new Author(a.QuerySelector("span").GetText(), SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)); } - - private async Task> GetToc(Uri url) { + + private async Task> GetToc(Uri url) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(url.AppendSegment("/read/")); - + var result = new List(); - foreach (var a in doc.QuerySelectorAll("div.book-navi-list li a")) { + foreach (var a in doc.QuerySelectorAll("div.book-navi-list li a")) + { var href = a.Attributes["onclick"]?.Value?.Split(" ").FirstOrDefault(s => s.Contains("books"))?.Trim('\''); result.Add(string.IsNullOrEmpty(href) ? new UrlChapter(null, a.GetText().ReplaceNewLine()) : new UrlChapter(url.MakeRelativeUri(href), a.GetText().ReplaceNewLine())); } - + return SliceToc(result, c => c.Title); } - private async Task> FillChapters(Uri url) { + private async Task> FillChapters(Uri url) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } - - foreach (var urlChapter in await GetToc(url)) { + if (Config.Options.NoChapters) return result; + + foreach (var urlChapter in await GetToc(url)) + { Config.Logger.LogInformation($"Загружаю главу {urlChapter.Title.CoverQuotes()}"); - var chapter = new Chapter { + var chapter = new Chapter + { Title = urlChapter.Title }; - if (urlChapter.Url != default) { + if (urlChapter.Url is not null) + { var chapterDoc = await GetChapter(urlChapter); chapter.Images = await GetImages(chapterDoc, urlChapter.Url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -101,13 +115,17 @@ private async Task> FillChapters(Uri url) { return result; } - private async Task GetChapter(UrlChapter urlChapter) { + private async Task GetChapter(UrlChapter urlChapter) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(urlChapter.Url); return doc.QuerySelector("div.reader").InnerHtml.AsHtmlDoc(); } - private Task GetCover(HtmlDocument doc, Uri bookUri) { + private Task GetCover(HtmlDocument doc, Uri bookUri) + { var imagePath = doc.QuerySelector("div.p-book-cover img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(bookUri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/MangaMammyGetter.cs b/Core/Logic/Getters/MangaMammyGetter.cs index 3e15834f..468444d8 100644 --- a/Core/Logic/Getters/MangaMammyGetter.cs +++ b/Core/Logic/Getters/MangaMammyGetter.cs @@ -13,67 +13,80 @@ namespace Core.Logic.Getters; -public class MangaMammyGetter(BookGetterConfig config) : GetterBase(config) { +public class MangaMammyGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://mangamammy.ru/"); - - protected override string GetId(Uri url) => url.GetSegment(2); - - public override async Task Get(Uri url) { + + protected override string GetId(Uri url) + { + return url.GetSegment(2); + } + + public override async Task Get(Uri url) + { url = SystemUrl.MakeRelativeUri("/manga/" + GetId(url)); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, url), Title = doc.GetTextBySelector("h1"), Author = GetAuthor(doc, url), Annotation = doc.QuerySelector("div.manga-excerpt")?.InnerHtml }; - + return book; } - private static Author GetAuthor(HtmlDocument doc, Uri url) { + private static Author GetAuthor(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("div.author-content a"); - return a == default ? new Author("MangaMammy") : new Author(a.GetText(), url.MakeRelativeUri(a.Attributes["href"].Value)); + return a is null + ? new Author("MangaMammy") + : new Author(a.GetText(), url.MakeRelativeUri(a.Attributes["href"].Value)); } - private async Task> FillChapters(HtmlDocument doc, Uri url) { + private async Task> FillChapters(HtmlDocument doc, Uri url) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } - - foreach (var urlChapter in GetToc(doc, url)) { + if (Config.Options.NoChapters) return result; + + foreach (var urlChapter in GetToc(doc, url)) + { Config.Logger.LogInformation($"Загружаю главу {urlChapter.Title.CoverQuotes()}"); - var chapter = new Chapter { + var chapter = new Chapter + { Title = urlChapter.Title }; var chapterDoc = await GetChapter(urlChapter); - if (chapterDoc != default) { + if (chapterDoc is not null) + { chapter.Images = await GetImages(chapterDoc, url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; } - + result.Add(chapter); } return result; } - private IEnumerable GetToc(HtmlDocument doc, Uri url) { + private IEnumerable GetToc(HtmlDocument doc, Uri url) + { var result = doc .QuerySelectorAll("ul.sub-chap-list li.wp-manga-chapter > a") .Select(a => new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.GetText().ReplaceNewLine())) .Reverse() .ToList(); - + return SliceToc(result, c => c.Title); } - private async Task GetChapter(UrlChapter urlChapter) { + private async Task GetChapter(UrlChapter urlChapter) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(urlChapter.Url); var images = doc @@ -83,16 +96,16 @@ private async Task GetChapter(UrlChapter urlChapter) { var sb = new StringBuilder(); - foreach (var image in images) - { - sb.Append($""); - } + foreach (var image in images) sb.Append($""); return sb.AsHtmlDoc(); } - private Task GetCover(HtmlDocument doc, Uri bookUri) { + private Task GetCover(HtmlDocument doc, Uri bookUri) + { var imagePath = doc.QuerySelector("div.summary_image img")?.Attributes["data-lazy-src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(bookUri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/MirKnigGetter.cs b/Core/Logic/Getters/MirKnigGetter.cs index d7fa66cd..59ed7693 100644 --- a/Core/Logic/Getters/MirKnigGetter.cs +++ b/Core/Logic/Getters/MirKnigGetter.cs @@ -12,75 +12,84 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class MirKnigGetter(BookGetterConfig config) : GetterBase(config) { +public class MirKnigGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://mir-knig.com/"); - protected override string GetId(Uri url) => base.GetId(url).Split('_').Last().Split('-').First(); + protected override string GetId(Uri url) + { + return base.GetId(url).Split('_').Last().Split('-').First(); + } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { var bookId = GetId(url); url = SystemUrl.MakeRelativeUri($"/view_{bookId}"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); var title = doc.GetTextBySelector("h1.heading"); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(bookId, title), Title = title, Author = GetAuthor(doc, url), Annotation = doc.QuerySelector("div.desc")?.InnerHtml }; - + return book; } - private async Task> FillChapters(string bookId, string title) { - if (Config.Options.NoChapters) { - return []; - } - + private async Task> FillChapters(string bookId, string title) + { + if (Config.Options.NoChapters) return []; + var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri($"/read_{bookId}-1")); var pages = int.Parse(doc.QuerySelectorAll("select.allp option").Last().Attributes["value"].Value); var sb = new StringBuilder(); - for (var i = 1; i <= pages; i++) { + for (var i = 1; i <= pages; i++) + { Config.Logger.LogInformation($"Получаю страницу {i}/{pages}"); doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri($"/read_{bookId}-{i}")); sb.Append(doc.QuerySelector("div.text-block").InnerHtml.HtmlDecode()); } doc = sb.AsHtmlDoc(); - var chapter = new Chapter { + var chapter = new Chapter + { Title = title, Images = await GetImages(doc, SystemUrl), Content = doc.DocumentNode.InnerHtml }; - return new []{chapter}; + return new[] { chapter }; } - private static Author GetAuthor(HtmlDocument doc, Uri url) { + private static Author GetAuthor(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("a.owner"); return new Author(a.GetText(), url.MakeRelativeUri(a.Attributes["href"].Value)); } - - private Task GetCover(HtmlDocument doc, Uri bookUri) { + + private Task GetCover(HtmlDocument doc, Uri bookUri) + { var thumb = doc.QuerySelector("div.cover div.thumb"); var imagePath = string.Empty; - if (thumb != default) { + if (thumb is not null) + { var style = thumb.Attributes["style"]?.Value; - if (!string.IsNullOrWhiteSpace(style)) { + if (!string.IsNullOrWhiteSpace(style)) + { var match = Regex.Match(style, @"url\((?.*?)\)"); - if (match.Success) { - imagePath = match.Groups["url"].Value; - } + if (match.Success) imagePath = match.Groups["url"].Value; } } - - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(bookUri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } - - -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/MyBookGetter.cs b/Core/Logic/Getters/MyBookGetter.cs index 8c1d472a..3bfd0ef3 100644 --- a/Core/Logic/Getters/MyBookGetter.cs +++ b/Core/Logic/Getters/MyBookGetter.cs @@ -10,6 +10,7 @@ using System.Text.Json.Nodes; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Misc; using Core.Types.Book; @@ -20,26 +21,31 @@ using Microsoft.Extensions.Logging; using OAuth; -namespace Core.Logic.Getters; - -public class MyBookGetter(BookGetterConfig config) : GetterBase(config) { - protected override Uri SystemUrl => new("https://mybook.ru/"); +namespace Core.Logic.Getters; +public class MyBookGetter(BookGetterConfig config) : GetterBase(config) +{ private const string CONSUMER_KEY = "830968793b2a44c688400319f4a77231"; private const string CONSUMER_SECRET = "HhaxXsMryrLz49R4"; private const string IDENTIFIER = "47b17a4c8231a28f06c6c836d0b5f6f2134cb74d"; private HttpClient _apiClient; - private MyBookAuth _token = new() { + + private MyBookAuth _token = new() + { Secret = "fYFPndYhOTW6YxIJ", Token = "f8d32ef906664ed3a1525b7298aac461" }; - private Uri GetMainUrl(Uri uri) { + protected override Uri SystemUrl => new("https://mybook.ru/"); + + private Uri GetMainUrl(Uri uri) + { return SystemUrl.MakeRelativeUri($"{uri.GetSegment(1)}/{uri.GetSegment(2)}/{uri.GetSegment(3)}"); } - public override Task Init() { + public override Task Init() + { _apiClient = new HttpClient(); _apiClient.Timeout = Config.Client.Timeout; _apiClient.DefaultRequestHeaders.Add("User-Agent", "MyBook/6.6.0 (iPhone; iOS 16.0.3; Scale/3.00)"); @@ -47,8 +53,10 @@ public override Task Init() { return Task.CompletedTask; } - private void SetAuthHeader(string method, Uri uri) { - var client = new OAuthRequest { + private void SetAuthHeader(string method, Uri uri) + { + var client = new OAuthRequest + { Method = method, SignatureMethod = OAuthSignatureMethod.HmacSha1, ConsumerKey = CONSUMER_KEY, @@ -58,36 +66,41 @@ private void SetAuthHeader(string method, Uri uri) { Token = _token.Token, TokenSecret = _token.Secret }; - - _apiClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("OAuth", client.GetAuthorizationHeader().Replace("OAuth", string.Empty).Trim()); + + _apiClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("OAuth", + client.GetAuthorizationHeader().Replace("OAuth", string.Empty).Trim()); } - public override async Task Authorize() { - if (!Config.HasCredentials) { - return; - } - + public override async Task Authorize() + { + if (!Config.HasCredentials) return; + var authUrl = SystemUrl.MakeRelativeUri("/api/auth/"); SetAuthHeader("POST", authUrl); - var payload = new { + var payload = new + { identifier = IDENTIFIER, email = Config.Options.Login, password = Config.Options.Password }; - + using var response = await _apiClient.PostAsJsonAsync(authUrl, payload); - if (response.StatusCode == HttpStatusCode.OK) { + if (response.StatusCode == HttpStatusCode.OK) + { Config.Logger.LogInformation("Успешно авторизовались"); - } else { + } + else + { var message = await response.Content.ReadAsStringAsync(); - throw new Exception($"Не удалось авторизоваться. {message}"); + throw new Elib2EbookAuthException($"Не удалось авторизоваться. {message}"); } - + _token = await response.Content.ReadFromJsonAsync(); } - - private static T GetNextData(HtmlDocument doc, string node) { + + private static T GetNextData(HtmlDocument doc, string node) + { var json = doc.QuerySelector("#__NEXT_DATA__").InnerText; return JsonDocument.Parse(json) .RootElement.GetProperty("props") @@ -98,74 +111,78 @@ private static T GetNextData(HtmlDocument doc, string node) { .Deserialize(); } - private async Task GetDetails(Uri url) { + private async Task GetDetails(Uri url) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); var details = GetNextData(doc, "book"); - if (details.Type == "audio") { - var bookId = details.Connected?.Id ?? details.MapFiles.FirstOrDefault(b => b.Book != default)?.Book; - if (bookId.HasValue) { - return await GetDetailsApi(bookId.Value); - } + if (details.Type == "audio") + { + var bookId = details.Connected?.Id ?? details.MapFiles.FirstOrDefault(b => b.Book != 0)?.Book; + if (bookId.HasValue) return await GetDetailsApi(bookId.Value); } return details; } - private async Task GetDetailsApi(long bookId) { + private async Task GetDetailsApi(long bookId) + { var apiUrl = SystemUrl.MakeRelativeUri($"/api/v1/books/{bookId}/"); SetAuthHeader("GET", apiUrl); return await _apiClient.GetFromJsonAsync(apiUrl); } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { url = GetMainUrl(url); var details = await GetDetails(url); - - var book = new Book(url) { + + var book = new Book(url) + { Cover = await GetCover(details), Title = details.Name, Author = GetAuthor(details), - Annotation = details.Annotation, + Annotation = details.Annotation }; - + var bookUrl = SystemUrl.MakeRelativeUri(details.BookFile); - + SetAuthHeader("GET", bookUrl); - if (details.Type != "audio") { + if (details.Type != "audio") + { using var response = await _apiClient.GetAsync(bookUrl); - if (response.StatusCode != HttpStatusCode.OK) { - throw new Exception("Не удалось получить книгу"); - } + if (response.StatusCode != HttpStatusCode.OK) + throw new Elib2EbookParseException("Не удалось получить книгу"); - var origBook = await TempFile.Create(bookUrl, Config.TempFolder.Path, bookUrl.GetFileName(), await response.Content.ReadAsStreamAsync()); + var origBook = await TempFile.Create(bookUrl, Config.TempFolder.Path, bookUrl.GetFileName(), + await response.Content.ReadAsStreamAsync()); - if (Config.Options.HasAdditionalType(AdditionalTypeEnum.Books)) { + if (Config.Options.HasAdditionalType(AdditionalTypeEnum.Books)) book.AdditionalFiles.Add(AdditionalTypeEnum.Books, origBook); - } book.Chapters = await FillChaptersFromEpub(origBook); } - if (Config.Options.HasAdditionalType(AdditionalTypeEnum.Audio)) { - if (details.Type == "audio") { - book.AdditionalFiles.Add(AdditionalTypeEnum.Audio, await GetAudio(details.Id)); - } - - if (details.Connected is { Type: "audio" }) { + if (Config.Options.HasAdditionalType(AdditionalTypeEnum.Audio)) + { + if (details.Type == "audio") book.AdditionalFiles.Add(AdditionalTypeEnum.Audio, await GetAudio(details.Id)); + + if (details.Connected is { Type: "audio" }) book.AdditionalFiles.Add(AdditionalTypeEnum.Audio, await GetAudio(details.Connected.Id)); - } } - + return book; } - private async Task> GetAudio(long bookId) { + private async Task> GetAudio(long bookId) + { var result = new List(); var details = await GetDetailsApi(bookId); - var files = details.Files.Where(node => node is JsonObject).Select(node => node.Deserialize()).ToList(); + var files = details.Files.Where(node => node is JsonObject).Select(node => node.Deserialize()) + .ToList(); - for (var i = 0; i < files.Count; i++) { + for (var i = 0; i < files.Count; i++) + { var file = files[i]; var url = SystemUrl.MakeRelativeUri(file.Url); @@ -174,30 +191,36 @@ private async Task> GetAudio(long bookId) { var name = file.Title; var ext = Path.GetExtension(url.GetFileName()); - + using var response = await _apiClient.GetAsync(url); - result.Add(await TempFile.Create(url, Config.TempFolder.Path, $"{file.Order}_{name}{ext}", await response.Content.ReadAsStreamAsync())); + result.Add(await TempFile.Create(url, Config.TempFolder.Path, $"{file.Order}_{name}{ext}", + await response.Content.ReadAsStreamAsync())); Config.Logger.LogInformation($"Аудиофайл файл {i + 1}/{files.Count} {url} загружен"); } - + return result; } - private Task GetCover(MyBookBook book) { - return !string.IsNullOrWhiteSpace(book.Cover) ? SaveImage($"https://i3.mybook.io/p/x378/{book.Cover.TrimStart('/')}".AsUri()) : Task.FromResult(default(TempFile)); + private Task GetCover(MyBookBook book) + { + return !string.IsNullOrWhiteSpace(book.Cover) + ? SaveImage($"https://i3.mybook.io/p/x378/{book.Cover.TrimStart('/')}".AsUri()) + : Task.FromResult(default(TempFile)); } - - private Author GetAuthor(MyBookBook book) { - if (book.Authors.Length == 0) { - return new Author("MyBook"); - } + + private Author GetAuthor(MyBookBook book) + { + if (book.Authors.Length == 0) return new Author("MyBook"); var author = book.Authors[0]; - return string.IsNullOrWhiteSpace(author.Url) ? new Author(author.Name) : new Author(author.Name, SystemUrl.MakeRelativeUri(author.Url)); + return string.IsNullOrWhiteSpace(author.Url) + ? new Author(author.Name) + : new Author(author.Name, SystemUrl.MakeRelativeUri(author.Url)); } - - public override void Dispose() { + + public override void Dispose() + { base.Dispose(); _apiClient?.Dispose(); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/NeobookGetter.cs b/Core/Logic/Getters/NeobookGetter.cs index 11e39e14..07630e4b 100644 --- a/Core/Logic/Getters/NeobookGetter.cs +++ b/Core/Logic/Getters/NeobookGetter.cs @@ -6,6 +6,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.Common; @@ -15,33 +16,34 @@ namespace Core.Logic.Getters; -public class NeobookGetter(BookGetterConfig config) : GetterBase(config) { +public class NeobookGetter(BookGetterConfig config) : GetterBase(config) +{ + private readonly Uri _apiUrl = new("https://api.neobook.org/"); protected override Uri SystemUrl => new("http://neobook.org/"); - private Uri _apiUrl = new("https://api.neobook.org/"); - - protected override string GetId(Uri url) { + protected override string GetId(Uri url) + { return url.GetSegment(1) == "book" ? url.GetSegment(2) : url.GetQueryParameter("book"); } - public override async Task Authorize() { - if (!Config.HasCredentials) { - return; - } + public override async Task Authorize() + { + if (!Config.HasCredentials) return; var response = await Config.Client.PostWithTriesAsync(_apiUrl, GenerateAuthData()); var data = await response.Content.ReadFromJsonAsync(); - if (!string.IsNullOrWhiteSpace(data?.Error)) { - throw new Exception($"Не удалось авторизоваться. {data.Error}"); - } + if (!string.IsNullOrWhiteSpace(data?.Error)) + throw new Elib2EbookAuthException($"Не удалось авторизоваться. {data.Error}"); Config.Logger.LogInformation("Успешно авторизовались"); Config.CookieContainer.Add(SystemUrl, new Cookie("utoken", data?.Login.Utoken)); Config.CookieContainer.Add(SystemUrl, new Cookie("uid", data?.Login.Uid)); } - private MultipartFormDataContent GenerateAuthData() { - return new() { + private MultipartFormDataContent GenerateAuthData() + { + return new MultipartFormDataContent + { { new StringContent("3.1"), "version" }, { new StringContent("0"), "uid" }, { new StringContent(string.Empty), "utoken" }, @@ -49,17 +51,19 @@ private MultipartFormDataContent GenerateAuthData() { { new StringContent("login_by_email"), "action" }, { new StringContent(Config.Options.Login), "email" }, { new StringContent(Config.Options.Password), "password" }, - { new StringContent("3"), "platform_id" }, + { new StringContent("3"), "platform_id" } }; } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { var bookId = GetId(url); url = SystemUrl.MakeRelativeUri("/book/" + bookId + "/"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); var data = GetPostData(doc); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(data), Chapters = await FillChapters(data, bookId), Title = data.Title, @@ -70,29 +74,34 @@ public override async Task Get(Uri url) { return book; } - private static NeobookPostData GetPostData(HtmlDocument doc) { + private static NeobookPostData GetPostData(HtmlDocument doc) + { var data = Regex.Match(doc.ParsedText, "var postData = (?.*?);\n").Groups["data"].Value; return data.Deserialize(); } - private Author GetAuthor(NeobookPostData data) { - return new Author(data.User.LastName + " " + data.User.FirstName, SystemUrl.MakeRelativeUri(data.User.UserName)); + private Author GetAuthor(NeobookPostData data) + { + return new Author(data.User.LastName + " " + data.User.FirstName, + SystemUrl.MakeRelativeUri(data.User.UserName)); } - private async Task> FillChapters(NeobookPostData data, string bookId) { + private async Task> FillChapters(NeobookPostData data, string bookId) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - foreach (var neobookChapter in SliceToc(data.Chapters, c => c.Title)) { + foreach (var neobookChapter in SliceToc(data.Chapters, c => c.Title)) + { Config.Logger.LogInformation($"Загружаю главу {neobookChapter.Title.CoverQuotes()}"); - var chapter = new Chapter { + var chapter = new Chapter + { Title = neobookChapter.Title }; - + var chapterDoc = await GetChapter(neobookChapter, bookId); - if (chapterDoc != default) { + if (chapterDoc is not null) + { chapter.Images = await GetImages(chapterDoc, SystemUrl); chapter.Content = chapterDoc.DocumentNode.InnerHtml; } @@ -103,28 +112,27 @@ private async Task> FillChapters(NeobookPostData data, stri return result; } - private async Task GetChapter(NeobookTocChapter neobookTocChapter, string bookId) { - var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri("reader/").AppendQueryParameter("book", bookId).AppendQueryParameter("chapter", neobookTocChapter.Token)); + private async Task GetChapter(NeobookTocChapter neobookTocChapter, string bookId) + { + var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri("reader/") + .AppendQueryParameter("book", bookId).AppendQueryParameter("chapter", neobookTocChapter.Token)); var data = Regex.Match(doc.ParsedText, "var data = (?.*?);\n").Groups["data"].Value; - if (string.IsNullOrWhiteSpace(data)) { - return default; - } - + if (string.IsNullOrWhiteSpace(data)) return default; + var neobookBook = data.Deserialize(); - if (neobookBook.ActiveChapterIndex > neobookBook.Chapters.Length) { - return default; - } + if (neobookBook.ActiveChapterIndex > neobookBook.Chapters.Length) return default; var chapter = neobookBook.Chapters[neobookBook.ActiveChapterIndex]; - if (string.IsNullOrWhiteSpace(chapter.Data?.Html)) { - return default; - } - + if (string.IsNullOrWhiteSpace(chapter.Data?.Html)) return default; + return chapter.Data.Html.AsHtmlDoc(); } - private Task GetCover(NeobookPostData data) { + private Task GetCover(NeobookPostData data) + { var imagePath = data.Attachment?.Cover?.GetValueOrDefault("l"); - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(imagePath.AsUri()) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(imagePath.AsUri()) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/NovelTranslateGetter.cs b/Core/Logic/Getters/NovelTranslateGetter.cs index b2260d34..eecb811a 100644 --- a/Core/Logic/Getters/NovelTranslateGetter.cs +++ b/Core/Logic/Getters/NovelTranslateGetter.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.Common; @@ -9,33 +10,38 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class NovelTranslateGetter(BookGetterConfig config) : GetterBase(config) { +public class NovelTranslateGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://noveltranslate.com/"); - protected override string GetId(Uri url) { - for (var i = 0; i < url.Segments.Length; i++) { - if (url.Segments[i].StartsWith("novel")) { + protected override string GetId(Uri url) + { + for (var i = 0; i < url.Segments.Length; i++) + if (url.Segments[i].StartsWith("novel")) return url.Segments[i + 1].Trim('/'); - } - } - throw new Exception($"Не могу определить ID книги {url}"); + throw new Elib2EbookParseException($"Не могу определить ID книги {url}"); } - private static string GetLang(Uri url) { + private static string GetLang(Uri url) + { var lang = url.Segments[1].Trim('/'); return lang == "novel" ? string.Empty : lang; } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { var lang = GetLang(url); - url = string.IsNullOrWhiteSpace(lang) ? SystemUrl.MakeRelativeUri($"/novel/{GetId(url)}/") : SystemUrl.MakeRelativeUri($"/{lang}/novel/{GetId(url)}/"); - + url = string.IsNullOrWhiteSpace(lang) + ? SystemUrl.MakeRelativeUri($"/novel/{GetId(url)}/") + : SystemUrl.MakeRelativeUri($"/{lang}/novel/{GetId(url)}/"); + var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, lang, url), Title = doc.GetTextBySelector("h1"), @@ -43,23 +49,24 @@ public override async Task Get(Uri url) { Annotation = GetAnnotation(doc), Lang = lang }; - + return book; } - private async Task> FillChapters(HtmlDocument doc, string lang, Uri url) { + private async Task> FillChapters(HtmlDocument doc, string lang, Uri url) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - foreach (var urlChapter in await GetToc(doc, lang, url)) { + foreach (var urlChapter in await GetToc(doc, lang, url)) + { Config.Logger.LogInformation($"Загружаю главу {urlChapter.Title.CoverQuotes()}"); - var chapter = new Chapter { + var chapter = new Chapter + { Title = urlChapter.Title }; - + var chapterDoc = await GetChapter(urlChapter); chapter.Images = await GetImages(chapterDoc, SystemUrl); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -70,22 +77,26 @@ private async Task> FillChapters(HtmlDocument doc, string l return result; } - private async Task GetChapter(UrlChapter urlChapter) { + private async Task GetChapter(UrlChapter urlChapter) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(urlChapter.Url); - return doc.QuerySelector("div.reading-content div.text-left").RemoveNodes("div.novel-before-content, div.tptn_counter, div.novel-after-content").InnerHtml.AsHtmlDoc(); + return doc.QuerySelector("div.reading-content div.text-left") + .RemoveNodes("div.novel-before-content, div.tptn_counter, div.novel-after-content").InnerHtml.AsHtmlDoc(); } - private async Task> GetToc(HtmlDocument doc, string lang, Uri url) { - var lastChapter = url.MakeRelativeUri(doc.QuerySelector("div.listing-chapters_wrap li.wp-manga-chapter a").Attributes["href"].Value); + private async Task> GetToc(HtmlDocument doc, string lang, Uri url) + { + var lastChapter = url.MakeRelativeUri(doc.QuerySelector("div.listing-chapters_wrap li.wp-manga-chapter a") + .Attributes["href"].Value); var chapterDoc = await Config.Client.GetHtmlDocWithTriesAsync(lastChapter); var chapters = new List(); - foreach (var option in chapterDoc.QuerySelector("select.selectpicker_chapter").QuerySelectorAll("option")) { + foreach (var option in chapterDoc.QuerySelector("select.selectpicker_chapter").QuerySelectorAll("option")) + { var name = option.GetText(); var chapterUri = option.Attributes["data-redirect"].Value.AsUri(); - if (!string.IsNullOrWhiteSpace(lang) && !chapterUri.LocalPath.Trim('/').StartsWith(lang)) { + if (!string.IsNullOrWhiteSpace(lang) && !chapterUri.LocalPath.Trim('/').StartsWith(lang)) chapterUri = url.MakeRelativeUri($"/{lang}/{chapterUri.LocalPath.Trim('/')}"); - } chapters.Add(new UrlChapter(chapterUri, name)); } @@ -94,17 +105,24 @@ private async Task> GetToc(HtmlDocument doc, string lang return SliceToc(chapters, c => c.Title); } - private static string GetAnnotation(HtmlDocument doc) { + private static string GetAnnotation(HtmlDocument doc) + { return doc.QuerySelector("div.description-summary div.summary__content > p")?.InnerHtml; } - private Task GetCover(HtmlDocument doc, Uri uri) { + private Task GetCover(HtmlDocument doc, Uri uri) + { var imagePath = doc.QuerySelector("div.summary_image img")?.Attributes["data-lazy-src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(uri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } - - private static Author GetAuthor(HtmlDocument doc, Uri uri) { + + private static Author GetAuthor(HtmlDocument doc, Uri uri) + { var a = doc.QuerySelector("div.summary-content div.author-content a"); - return a != default ? new Author(a.GetText(), uri.MakeRelativeUri(a.Attributes["href"].Value)) : new Author("NovelTranslate"); + return a is not null + ? new Author(a.GetText(), uri.MakeRelativeUri(a.Attributes["href"].Value)) + : new Author("NovelTranslate"); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/NovelhallGetter.cs b/Core/Logic/Getters/NovelhallGetter.cs index 08f8c591..7891edf4 100644 --- a/Core/Logic/Getters/NovelhallGetter.cs +++ b/Core/Logic/Getters/NovelhallGetter.cs @@ -12,18 +12,22 @@ namespace Core.Logic.Getters; -public class NovelhallGetter(BookGetterConfig config) : GetterBase(config) { +public class NovelhallGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://www.novelhall.com/"); - protected override string GetId(Uri url) { + protected override string GetId(Uri url) + { return url.GetSegment(1); } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { url = SystemUrl.MakeRelativeUri(GetId(url)); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, url), Title = doc.GetTextBySelector("h1"), @@ -31,23 +35,24 @@ public override async Task Get(Uri url) { Annotation = GetAnnotation(doc), Lang = "en" }; - + return book; } - private async Task> FillChapters(HtmlDocument doc, Uri url) { + private async Task> FillChapters(HtmlDocument doc, Uri url) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - foreach (var urlChapter in GetToc(doc, url)) { + foreach (var urlChapter in GetToc(doc, url)) + { Config.Logger.LogInformation($"Загружаю главу {urlChapter.Title.CoverQuotes()}"); - var chapter = new Chapter { + var chapter = new Chapter + { Title = urlChapter.Title }; - + var chapterDoc = await GetChapter(urlChapter); chapter.Images = await GetImages(chapterDoc, SystemUrl); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -58,27 +63,35 @@ private async Task> FillChapters(HtmlDocument doc, Uri url) return result; } - private async Task GetChapter(UrlChapter urlChapter) { + private async Task GetChapter(UrlChapter urlChapter) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(urlChapter.Url); return doc.QuerySelector("div.entry-content").InnerHtml.AsHtmlDoc(); } - private IEnumerable GetToc(HtmlDocument doc, Uri url) { - var chapters = doc.QuerySelectorAll("#morelist ul li a[href]").Select(a => new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.GetText())).ToList(); + private IEnumerable GetToc(HtmlDocument doc, Uri url) + { + var chapters = doc.QuerySelectorAll("#morelist ul li a[href]") + .Select(a => new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.GetText())).ToList(); return SliceToc(chapters, c => c.Title); } - private static string GetAnnotation(HtmlDocument doc) { + private static string GetAnnotation(HtmlDocument doc) + { return doc.QuerySelector("div.intro span.js-close-wrap").RemoveNodes(".blue")?.InnerHtml; } - private Task GetCover(HtmlDocument doc, Uri uri) { + private Task GetCover(HtmlDocument doc, Uri uri) + { var imagePath = doc.QuerySelector("div.book-img img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(uri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } - - private static Author GetAuthor(HtmlDocument doc) { + + private static Author GetAuthor(HtmlDocument doc) + { var a = doc.QuerySelector("meta[property='books:author']"); - return a != default ? new Author(a.Attributes["content"].Value) : new Author("NovelHall"); + return a is not null ? new Author(a.Attributes["content"].Value) : new Author("NovelHall"); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/Novelxo/NovelxoGetter.cs b/Core/Logic/Getters/Novelxo/NovelxoGetter.cs index 93dfd1d1..0dc04a3a 100644 --- a/Core/Logic/Getters/Novelxo/NovelxoGetter.cs +++ b/Core/Logic/Getters/Novelxo/NovelxoGetter.cs @@ -1,8 +1,9 @@ using System; using Core.Configs; -namespace Core.Logic.Getters.Novelxo; +namespace Core.Logic.Getters.Novelxo; -public class NovelxoGetter(BookGetterConfig config) : NovelxoGetterBase(config) { +public class NovelxoGetter(BookGetterConfig config) : NovelxoGetterBase(config) +{ protected override Uri SystemUrl => new("https://novelxo.com"); -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/Novelxo/NovelxoGetterBase.cs b/Core/Logic/Getters/Novelxo/NovelxoGetterBase.cs index 954e77c5..28057473 100644 --- a/Core/Logic/Getters/Novelxo/NovelxoGetterBase.cs +++ b/Core/Logic/Getters/Novelxo/NovelxoGetterBase.cs @@ -15,136 +15,154 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters.Novelxo; +namespace Core.Logic.Getters.Novelxo; -public abstract class NovelxoGetterBase(BookGetterConfig config) : GetterBase(config) { +public abstract class NovelxoGetterBase(BookGetterConfig config) : GetterBase(config) +{ private static readonly byte[] Key = StringToByteArray("61626326312a7e235e325e2373305e3d295e5e3725623334"); private static readonly byte[] IV = StringToByteArray("31323334353637383930383533373237"); - protected override string GetId(Uri url) => url.GetSegment(1); + protected override string GetId(Uri url) + { + return url.GetSegment(1); + } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { url = await GetMainUrl(url); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - - var book = new Book(url) { + + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, url), Title = doc.GetTextBySelector("h1"), Author = GetAuthor(doc, url), Annotation = GetAnnotation(doc) }; - + return book; } - private async Task GetMainUrl(Uri url) { - if (url.Segments.Length == 2) { - return SystemUrl.MakeRelativeUri(GetId(url)); - } + private async Task GetMainUrl(Uri url) + { + if (url.Segments.Length == 2) return SystemUrl.MakeRelativeUri(GetId(url)); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); return url.MakeRelativeUri(doc.QuerySelector("div.readerheader-wg a").Attributes["href"].Value); } - private async Task> FillChapters(HtmlDocument doc, Uri url) { + private async Task> FillChapters(HtmlDocument doc, Uri url) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } - + if (Config.Options.NoChapters) return result; + var start = url.MakeRelativeUri(doc.QuerySelector("div.actions a.read").Attributes["href"].Value); - - while (true) { + + while (true) + { var sb = new StringBuilder(); doc = await Config.Client.GetHtmlDocWithTriesAsync(start); var container = doc.QuerySelector("div.readerbody-wg"); - foreach (var node in container.QuerySelectorAll("p, div.ctp")) { - if (node.Name == "p") { + foreach (var node in container.QuerySelectorAll("p, div.ctp")) + if (node.Name == "p") + { sb.Append(node.InnerHtml.HtmlDecode().CoverTag(node.Name)); - } else if (node.Name == "div" && node.Attributes["class"]?.Value?.Contains("ctp") == true) { - if (node.Attributes["class"].Value.Contains("protected")) { + } + else if (node.Name == "div" && node.Attributes["class"]?.Value?.Contains("ctp") == true) + { + if (node.Attributes["class"].Value.Contains("protected")) sb.Append(await GetProtected(doc, node)); - } else { - sb.Append(Encoding.UTF8.GetString(Transform(Convert.FromBase64String(node.InnerText), Key, IV)).HtmlDecode()); - } + else + sb.Append(Encoding.UTF8.GetString(Transform(Convert.FromBase64String(node.InnerText), Key, IV)) + .HtmlDecode()); } - } var chapterDoc = sb.AsHtmlDoc(); - var chapter = new Chapter { + var chapter = new Chapter + { Title = doc.GetTextBySelector("h2.chapter-title"), Images = await GetImages(chapterDoc, start), Content = chapterDoc.DocumentNode.InnerHtml }; result.Add(chapter); - + Config.Logger.LogInformation($"Загружена глава {chapter.Title.CoverQuotes()}"); var next = doc.QuerySelector("div.readernav-wg a.next"); - if (next == default) { - return result; - } + if (next is null) return result; start = start.MakeRelativeUri(next.Attributes["href"].Value); } } - - private async Task GetProtected(HtmlDocument doc, HtmlNode node) { + + private async Task GetProtected(HtmlDocument doc, HtmlNode node) + { var id = node.Attributes["data-id"].Value; var header = Regex.Match(doc.ParsedText, @"http.setRequestHeader\((?.*?), '(?.*?)'"); - - var headerName = Regex.Match(doc.ParsedText, $"{header.Groups["name"].Value} = \'(?.*?)\'").Groups["id"].Value; + + var headerName = Regex.Match(doc.ParsedText, $"{header.Groups["name"].Value} = \'(?.*?)\'").Groups["id"] + .Value; var headerValue = header.Groups["value"].Value; var key = StringToByteArray(Regex.Match(doc.ParsedText, "signKey = \'(?.*?)\'").Groups["id"].Value); var encode = Transform(Encoding.UTF8.GetBytes(id), key, IV); - var url = new Uri($"https://a.novelxo.com/v1/chapters/{id}/ctp?width=1140&sign=932870{Atob(Convert.ToBase64String(encode))}182906"); + var url = new Uri( + $"https://a.novelxo.com/v1/chapters/{id}/ctp?width=1140&sign=932870{Atob(Convert.ToBase64String(encode))}182906"); var message = new HttpRequestMessage(HttpMethod.Get, url); message.Headers.Add(headerName, headerValue); message.Headers.Add("referer", SystemUrl.ToString()); var response = await Config.Client.SendAsync(message); var readAsStringAsync = await response.Content.ReadAsStringAsync(); - var result = Encoding.UTF8.GetString(Transform(Convert.FromBase64String(readAsStringAsync.Trim('\"')), Key, IV)).HtmlDecode(); + var result = Encoding.UTF8.GetString(Transform(Convert.FromBase64String(readAsStringAsync.Trim('\"')), Key, IV)) + .HtmlDecode(); return result; } - private static string Atob(string toEncode) { + private static string Atob(string toEncode) + { return Convert.ToBase64String(Encoding.GetEncoding(28591).GetBytes(toEncode)); } - private static string GetAnnotation(HtmlDocument doc) { + private static string GetAnnotation(HtmlDocument doc) + { var html = doc.QuerySelector("#bookinfo")?.InnerHtml; - return string.IsNullOrWhiteSpace(html) ? - string.Empty : - html.AsHtmlDoc().RemoveNodes("script, ins").DocumentNode.InnerHtml; + return string.IsNullOrWhiteSpace(html) + ? string.Empty + : html.AsHtmlDoc().RemoveNodes("script, ins").DocumentNode.InnerHtml; } - - private Task GetCover(HtmlDocument doc, Uri uri) { + + private Task GetCover(HtmlDocument doc, Uri uri) + { var imagePath = doc.QuerySelector("div.cover img")?.Attributes["data-src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(uri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } - - private static Author GetAuthor(HtmlDocument doc, Uri url) { + + private static Author GetAuthor(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("tr.authors a"); - return a == default ? - new Author("Novelxo") : - new Author(a.GetText(), url.MakeRelativeUri(a.Attributes["href"].Value)); + return a is null + ? new Author("Novelxo") + : new Author(a.GetText(), url.MakeRelativeUri(a.Attributes["href"].Value)); } - private static byte[] StringToByteArray(string hex) { + private static byte[] StringToByteArray(string hex) + { return Enumerable.Range(0, hex.Length) .Where(x => x % 2 == 0) .Select(x => Convert.ToByte(hex.Substring(x, 2), 16)) .ToArray(); } - private static void AesCtrTransform(byte[] key, byte[] salt, Stream inputStream, Stream outputStream) { + private static void AesCtrTransform(byte[] key, byte[] salt, Stream inputStream, Stream outputStream) + { var aes = Aes.Create(); aes.Mode = CipherMode.ECB; aes.Padding = PaddingMode.None; @@ -157,21 +175,19 @@ private static void AesCtrTransform(byte[] key, byte[] salt, Stream inputStream, var counterEncryptor = aes.CreateEncryptor(key, zeroIv); int b; - while ((b = inputStream.ReadByte()) != -1) { - if (xorMask.Count == 0) { + while ((b = inputStream.ReadByte()) != -1) + { + if (xorMask.Count == 0) + { var counterModeBlock = new byte[blockSize]; counterEncryptor.TransformBlock(counter, 0, counter.Length, counterModeBlock, 0); - for (var i = counter.Length - 1; i >= 0; i--) { - if (++counter[i] != 0) { + for (var i = counter.Length - 1; i >= 0; i--) + if (++counter[i] != 0) break; - } - } - foreach (var b2 in counterModeBlock) { - xorMask.Enqueue(b2); - } + foreach (var b2 in counterModeBlock) xorMask.Enqueue(b2); } var mask = xorMask.Dequeue(); @@ -179,7 +195,8 @@ private static void AesCtrTransform(byte[] key, byte[] salt, Stream inputStream, } } - private static byte[] Transform(byte[] str, byte[] key, byte[] iv) { + private static byte[] Transform(byte[] str, byte[] key, byte[] iv) + { using var outputEncryptedStream = new MemoryStream(str); using var outputDecryptedStream = new MemoryStream(); AesCtrTransform(key, iv, outputEncryptedStream, outputDecryptedStream); @@ -187,4 +204,4 @@ private static byte[] Transform(byte[] str, byte[] key, byte[] iv) { outputDecryptedStream.Position = 0; return outputDecryptedStream.ToArray(); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/Novelxo/NovelxoRuGetter.cs b/Core/Logic/Getters/Novelxo/NovelxoRuGetter.cs index 22b6a3a8..968a40c6 100644 --- a/Core/Logic/Getters/Novelxo/NovelxoRuGetter.cs +++ b/Core/Logic/Getters/Novelxo/NovelxoRuGetter.cs @@ -1,8 +1,9 @@ using System; using Core.Configs; -namespace Core.Logic.Getters.Novelxo; +namespace Core.Logic.Getters.Novelxo; -public class NovelxoRuGetter(BookGetterConfig config) : NovelxoGetterBase(config) { +public class NovelxoRuGetter(BookGetterConfig config) : NovelxoGetterBase(config) +{ protected override Uri SystemUrl => new("https://ru.novelxo.com"); -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/OnlineKnigiGetter.cs b/Core/Logic/Getters/OnlineKnigiGetter.cs index cb40e662..57591609 100644 --- a/Core/Logic/Getters/OnlineKnigiGetter.cs +++ b/Core/Logic/Getters/OnlineKnigiGetter.cs @@ -10,40 +10,46 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class OnlineKnigiGetter(BookGetterConfig config) : GetterBase(config) { +public class OnlineKnigiGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://online-knigi.com.ua/"); - protected override string GetId(Uri url) => url.GetSegment(2); + protected override string GetId(Uri url) + { + return url.GetSegment(2); + } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { var bookId = GetId(url); url = SystemUrl.MakeRelativeUri($"/kniga/{bookId}"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); var title = doc.GetTextBySelector("h1"); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc), Chapters = await FillChapters(bookId, title), Title = title, Author = GetAuthor(doc, url), - Annotation = doc.QuerySelector("div.book_description")?.InnerHtml, + Annotation = doc.QuerySelector("div.book_description")?.InnerHtml }; - + return book; } - private async Task> FillChapters(string bookId, string title) { - if (Config.Options.NoChapters) { - return []; - } - + private async Task> FillChapters(string bookId, string title) + { + if (Config.Options.NoChapters) return []; + var chapter = new Chapter(); var sb = new StringBuilder(); var pages = await GetPages(bookId); - for (var i = 1; i <= pages; i++) { + for (var i = 1; i <= pages; i++) + { Config.Logger.LogInformation($"Получаю страницу {i}/{pages}"); var uri = SystemUrl.MakeRelativeUri($"/page/{bookId}?page={i}"); @@ -59,18 +65,23 @@ private async Task> FillChapters(string bookId, string titl return new[] { chapter }; } - private async Task GetPages(string bookId) { + private async Task GetPages(string bookId) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri($"/page/{bookId}")); - return int.Parse(doc.QuerySelector("li.last a").Attributes["href"].Value.AsUri().GetQueryParameter("page")); + return int.Parse(doc.QuerySelector("li.last a").Attributes["href"].Value.AsUri().GetQueryParameter("page")); } - private Task GetCover(HtmlDocument doc) { + private Task GetCover(HtmlDocument doc) + { var imagePath = doc.QuerySelector("meta[property=og:image]")?.Attributes["content"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(SystemUrl.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(SystemUrl.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } - - private static Author GetAuthor(HtmlDocument doc, Uri uri) { + + private static Author GetAuthor(HtmlDocument doc, Uri uri) + { var a = doc.QuerySelector("div[itemprop=author] a"); return new Author(a.GetText(), uri.MakeRelativeUri(a.Attributes["href"].Value)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/ProdamanGetter.cs b/Core/Logic/Getters/ProdamanGetter.cs index 3b6d837a..ea671399 100644 --- a/Core/Logic/Getters/ProdamanGetter.cs +++ b/Core/Logic/Getters/ProdamanGetter.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.Common; @@ -14,49 +15,51 @@ namespace Core.Logic.Getters; -public class ProdamanGetter : GetterBase { - public ProdamanGetter(BookGetterConfig config) : base(config) { +public class ProdamanGetter : GetterBase +{ + private static readonly Dictionary Map = new(); + + public ProdamanGetter(BookGetterConfig config) : base(config) + { InitMap(); } protected override Uri SystemUrl => new("https://prodaman.ru/"); - - private static readonly Dictionary Map = new(); - private static void Append(string str, int start) { - for (var i = 0; i < str.Length; i++) { - Map[start + i] = str[i]; - } + private static void Append(string str, int start) + { + for (var i = 0; i < str.Length; i++) Map[start + i] = str[i]; } - + /// - /// Авторизация в системе + /// Авторизация в системе /// /// - public override async Task Authorize(){ - if (!Config.HasCredentials) { - return; - } + public override async Task Authorize() + { + if (!Config.HasCredentials) return; var doc = await Config.Client.PostHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri("/login"), GetAuthData()); - if (!string.IsNullOrWhiteSpace(doc.GetTextBySelector("p.error"))) { - throw new Exception("Не удалось авторизоваться"); - } + if (!string.IsNullOrWhiteSpace(doc.GetTextBySelector("p.error"))) + throw new Elib2EbookAuthException("Не удалось авторизоваться"); } - - private FormUrlEncodedContent GetAuthData() { - var data = new Dictionary { + + private FormUrlEncodedContent GetAuthData() + { + var data = new Dictionary + { ["email"] = Config.Options.Login, ["pass"] = Config.Options.Password, - ["remember"] = "on", + ["remember"] = "on" }; return new FormUrlEncodedContent(data); } - + // MAAAAAAGIC!!! - private static void InitMap() { + private static void InitMap() + { Append("ЕКЦДНСИПТЬЧЖФ", 0x02CB); Append("ЗЭЯГЛЙОЮУХРМЩ", 0x02DE); Append("е", 0x02EC); @@ -117,12 +120,14 @@ private static void InitMap() { Append("нх,ьтзщядвусцшкмеэр.бл", 0x25D0); } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { url = SystemUrl.MakeRelativeUri(url.AbsolutePath); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); var title = doc.GetTextBySelector("h1"); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapter(url, title), Title = title, @@ -134,129 +139,140 @@ public override async Task Get(Uri url) { return book; } - private static Author GetAuthor(HtmlDocument doc, Uri url) { + private static Author GetAuthor(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("a[data-widget-feisovet-author]"); - if (a == default) { - return new Author("Prodaman"); - } + if (a is null) return new Author("Prodaman"); return new Author(a.GetText(), url.MakeRelativeUri(a.Attributes["href"].Value)); } - private static Seria GetSeria(HtmlDocument doc, Uri url) { + private static Seria GetSeria(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("p.blog-info a[href*=/series/]"); - if (a != default) { - return new Seria { + if (a is not null) + return new Seria + { Name = a.GetText(), Url = url.MakeRelativeUri(a.Attributes["href"].Value) }; - } return default; } - private async Task GetPages(Uri url) { + private async Task GetPages(Uri url) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(url.AppendQueryParameter("nav", "ok")); var pages = doc.QuerySelectorAll("div.pageList a") .Where(a => int.TryParse(a.InnerText, out _)) .Select(a => int.Parse(a.InnerText)) .ToList(); - + return pages.Count > 0 ? pages.Max() : 1; } - - private static bool IsHeaderStart(IEnumerable nodes) { + + private static bool IsHeaderStart(IEnumerable nodes) + { var firstNode = nodes.First(); return firstNode.Name == "h3" && !string.IsNullOrWhiteSpace(firstNode.InnerText); } - - private static Chapter CreateChapter(string title) { - return new Chapter { + + private static Chapter CreateChapter(string title) + { + return new Chapter + { Title = title }; } - - private async Task AddChapter(ICollection chapters, Chapter chapter, StringBuilder text, Uri url) { - if (chapter == null) { - return; - } - + + private async Task AddChapter(ICollection chapters, Chapter chapter, StringBuilder text, Uri url) + { + if (chapter == null) return; + var chapterDoc = text.ToString().AsHtmlDoc(); chapter.Images = await GetImages(chapterDoc, url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; chapters.Add(chapter); } - private static string Decode(string encode) { + private static string Decode(string encode) + { return encode.Aggregate(new StringBuilder(), (sb, c) => sb.Append(Map.GetValueOrDefault(c, c))).ToString(); } - private async Task> FillChapter(Uri url, string title) { + private async Task> FillChapter(Uri url, string title) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } - + if (Config.Options.NoChapters) return result; + Chapter chapter = null; var text = new StringBuilder(); var pages = await GetPages(url); var firstBr = true; - for (var i = 1; i <= pages; i++) { + for (var i = 1; i <= pages; i++) + { Config.Logger.LogInformation($"Получаю страницу {i}/{pages}"); var appendSegment = url.AppendQueryParameter("page", i); var page = await Config.Client.GetHtmlDocWithTriesAsync(appendSegment); var content = page.QuerySelector("div.blog-text").RemoveNodes("a[href*=snoska], div[id*=snoska]"); var nodes = content.ChildNodes; - if (i == 1 && !IsHeaderStart(nodes)) { - chapter = CreateChapter(title); - } - - foreach (var node in nodes) { - if (node.Name != "h3" || (node.Name == "h3" && node.GetText() == "***")) { - if (node.Name == "br") { - if (firstBr) { + if (i == 1 && !IsHeaderStart(nodes)) chapter = CreateChapter(title); + + foreach (var node in nodes) + if (node.Name != "h3" || (node.Name == "h3" && node.GetText() == "***")) + { + if (node.Name == "br") + { + if (firstBr) + { firstBr = false; text.Append("

"); - } else { + } + else + { text.Append("

"); } - + continue; } - - if (node.Name == "img" && node.Attributes["src"] != null) { + + if (node.Name == "img" && node.Attributes["src"] != null) + { text.Append($""); - } else if (!string.IsNullOrWhiteSpace(node.InnerHtml)) { + } + else if (!string.IsNullOrWhiteSpace(node.InnerHtml)) + { var pText = Decode(node.InnerHtml.HtmlDecode()); - - if (node.InnerHtml.StartsWith(" ")) { - pText = " " + pText; - } - if (node.InnerHtml.EndsWith(" ")) { - pText += " "; - } - + if (node.InnerHtml.StartsWith(" ")) pText = " " + pText; + + if (node.InnerHtml.EndsWith(" ")) pText += " "; + text.Append(pText.CoverTag(node.Name == "#text" ? string.Empty : node.Name)); } - } else { + } + else + { text.Append("

"); await AddChapter(result, chapter, text, url); text.Clear(); chapter = CreateChapter(Decode(node.InnerText.HtmlDecode())); } - } } - + text.Append("

"); await AddChapter(result, chapter ?? CreateChapter(title), text, url); return result; } - private Task GetCover(HtmlDocument doc, Uri url) { - var imagePath = doc.QuerySelector("div[itemprop=aggregateRating] img[itemprop=image]")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(url.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + private Task GetCover(HtmlDocument doc, Uri url) + { + var imagePath = doc.QuerySelector("div[itemprop=aggregateRating] img[itemprop=image]")?.Attributes["src"] + ?.Value; + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(url.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/RanobeGetter.cs b/Core/Logic/Getters/RanobeGetter.cs index 6ae5ec4b..3f8fe142 100644 --- a/Core/Logic/Getters/RanobeGetter.cs +++ b/Core/Logic/Getters/RanobeGetter.cs @@ -12,37 +12,43 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class RanobeGetter(BookGetterConfig config) : GetterBase(config) { +public class RanobeGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://ранобэ.рф/"); - - protected override string GetId(Uri url) => url.Segments.Length == 2 ? base.GetId(url) : url.GetSegment(1); - public override async Task Get(Uri url) { + protected override string GetId(Uri url) + { + return url.Segments.Length == 2 ? base.GetId(url) : url.GetSegment(1); + } + + public override async Task Get(Uri url) + { url = SystemUrl.MakeRelativeUri(GetId(url)); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); var ranobeBook = GetNextData(doc, "book"); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(ranobeBook, url), Chapters = await FillChapters(ranobeBook, url), Title = ranobeBook.Title, Author = new Author(string.IsNullOrWhiteSpace(ranobeBook.Author) ? "Ranobe" : ranobeBook.Author) }; - + return book; } - private async Task> FillChapters(RanobeBook ranobeBook, Uri url) { + private async Task> FillChapters(RanobeBook ranobeBook, Uri url) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; ranobeBook.Chapters.Reverse(); - foreach (var ranobeChapter in SliceToc(ranobeBook.Chapters, c => c.Title)) { + foreach (var ranobeChapter in SliceToc(ranobeBook.Chapters, c => c.Title)) + { Config.Logger.LogInformation($"Загружаю главу {ranobeChapter.Title.CoverQuotes()}"); var chapter = new Chapter(); var doc = await GetChapter(url, ranobeChapter.Url); @@ -56,12 +62,14 @@ private async Task> FillChapters(RanobeBook ranobeBook, Uri return result; } - private async Task GetChapter(Uri mainUrl, string url) { + private async Task GetChapter(Uri mainUrl, string url) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(mainUrl.MakeRelativeUri(url)); return GetNextData(doc, "chapter").Content.Text.AsHtmlDoc(); } - private static T GetNextData(HtmlDocument doc, string node) { + private static T GetNextData(HtmlDocument doc, string node) + { var json = doc.QuerySelector("#__NEXT_DATA__").InnerText; return JsonDocument.Parse(json) .RootElement.GetProperty("props") @@ -70,9 +78,12 @@ private static T GetNextData(HtmlDocument doc, string node) { .GetRawText() .Deserialize(); } - - private Task GetCover(RanobeBook book, Uri bookUri) { + + private Task GetCover(RanobeBook book, Uri bookUri) + { var imagePath = book.Image?.Url ?? book.Images.MaxBy(t => t.Height)?.Url; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(bookUri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/RanobeHubGetter.cs b/Core/Logic/Getters/RanobeHubGetter.cs index 4372df6c..d92b4fb5 100644 --- a/Core/Logic/Getters/RanobeHubGetter.cs +++ b/Core/Logic/Getters/RanobeHubGetter.cs @@ -12,35 +12,38 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class RanobeHubGetter(BookGetterConfig config) : GetterBase(config) { +public class RanobeHubGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://ranobehub.org/"); - - public override async Task Get(Uri url) { + + public override async Task Get(Uri url) + { url = SystemUrl.MakeRelativeUri($"/ranobe/{GetId(url)}"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, url), Title = doc.GetTextBySelector("h1.header"), Author = new Author("RanobeHub") }; - + return book; } - private async Task> FillChapters(HtmlDocument doc, Uri url) { + private async Task> FillChapters(HtmlDocument doc, Uri url) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - foreach (var ranobeChapter in await GetToc(doc)) { + foreach (var ranobeChapter in await GetToc(doc)) + { Config.Logger.LogInformation($"Загружаю главу {ranobeChapter.Name}"); var chapter = new Chapter(); - + var chapterDoc = await GetChapter(ranobeChapter.Url); chapter.Images = await GetImages(chapterDoc, url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -52,40 +55,48 @@ private async Task> FillChapters(HtmlDocument doc, Uri url) return result; } - private async Task GetChapter(string url) { + private async Task GetChapter(string url) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(url.AsUri()); - while (doc.QuerySelector("div[data-callback=correctCaptcha]") != null) { - Config.Logger.LogInformation($"Обнаружена капча. Перейдите по ссылке {url}, введите капчу и нажмите Enter..."); + while (doc.QuerySelector("div[data-callback=correctCaptcha]") != null) + { + Config.Logger.LogInformation( + $"Обнаружена капча. Перейдите по ссылке {url}, введите капчу и нажмите Enter..."); Console.Read(); doc = await Config.Client.GetHtmlDocWithTriesAsync(url.AsUri()); } - - var result = doc.QuerySelector("div.container[data-container]").RemoveNodes("div.title-wrapper, div.ads-desktop, div.tablet, div.chapter-hoticons").InnerHtml.AsHtmlDoc(); - - foreach (var img in result.QuerySelectorAll("img")) { + + var result = doc.QuerySelector("div.container[data-container]") + .RemoveNodes("div.title-wrapper, div.ads-desktop, div.tablet, div.chapter-hoticons").InnerHtml.AsHtmlDoc(); + + foreach (var img in result.QuerySelectorAll("img")) + { var id = img.Attributes["data-media-id"]?.Value; - if (string.IsNullOrWhiteSpace(id)) { - continue; - } + if (string.IsNullOrWhiteSpace(id)) continue; - if (img.Attributes.Contains("src")) { + if (img.Attributes.Contains("src")) img.Attributes["src"].Value = $"/api/media/{id}"; - } else { + else img.Attributes.Add("src", $"/api/media/{id}"); - } } - + return result; } - private async Task> GetToc(HtmlDocument doc) { + private async Task> GetToc(HtmlDocument doc) + { var internalId = doc.QuerySelector("html[data-id]").Attributes["data-id"].Value; - var response = await Config.Client.GetFromJsonAsync(SystemUrl.MakeRelativeUri($"/api/ranobe/{internalId}/contents")); + var response = + await Config.Client.GetFromJsonAsync( + SystemUrl.MakeRelativeUri($"/api/ranobe/{internalId}/contents")); return SliceToc(response?.Volumes.SelectMany(t => t.Chapters).ToList(), c => c.Name); } - - private Task GetCover(HtmlDocument doc, Uri bookUri) { + + private Task GetCover(HtmlDocument doc, Uri bookUri) + { var imagePath = doc.QuerySelector("div.poster-slider img")?.Attributes["data-src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(bookUri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/RanobeNovelsGetter.cs b/Core/Logic/Getters/RanobeNovelsGetter.cs index 1ad4324f..1b203eb7 100644 --- a/Core/Logic/Getters/RanobeNovelsGetter.cs +++ b/Core/Logic/Getters/RanobeNovelsGetter.cs @@ -15,39 +15,43 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class RanobeNovelsGetter(BookGetterConfig config) : GetterBase(config) { +public class RanobeNovelsGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://ranobe-novels.ru/"); - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { var bookId = GetId(url); url = SystemUrl.MakeRelativeUri($"/ranobe/{bookId}/"); var doc = await GetSafety(url); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, url), Title = doc.GetTextBySelector("h1"), Author = new Author("ranobe-novels") }; - + return book; } - - private async Task> FillChapters(HtmlDocument doc, Uri url) { + + private async Task> FillChapters(HtmlDocument doc, Uri url) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; var toc = await GetToc(doc); - if (toc.Count == 0) { + if (toc.Count == 0) + { doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri($"/{GetId(url)}")); toc = await GetToc(doc); } - - foreach (var ranobeChapter in toc) { + + foreach (var ranobeChapter in toc) + { Config.Logger.LogInformation($"Загружаю главу {ranobeChapter.Title.CoverQuotes()}"); var chapter = new Chapter(); var chapterDoc = await GetChapter(ranobeChapter.Url); @@ -61,46 +65,58 @@ private async Task> FillChapters(HtmlDocument doc, Uri url) return result; } - private async Task GetChapter(Uri url) { + private async Task GetChapter(Uri url) + { var doc = await GetSafety(url); return doc.QuerySelector("div.js-full-content").RemoveNodes("> :not(p)").InnerHtml.AsHtmlDoc(); } - private async Task> GetToc(HtmlDocument doc) { + private async Task> GetToc(HtmlDocument doc) + { var catId = Regex.Match(doc.ParsedText, "data-cat=\"(?\\d+)\"").Groups["catId"].Value; var result = new List(); - - foreach (var span in doc.QuerySelectorAll("ul.myUL span.caret[data-id]")) { + + foreach (var span in doc.QuerySelectorAll("ul.myUL span.caret[data-id]")) + { var offset = (int.Parse(span.Attributes["data-id"].Value) - 1) * 100; - var payload = new FormUrlEncodedContent(new Dictionary { + var payload = new FormUrlEncodedContent(new Dictionary + { ["cat_id"] = catId, ["offset"] = offset.ToString() }); - var post = await Config.Client.PostWithTriesAsync(SystemUrl.MakeRelativeUri("/wp-content/themes/ranobe-novels/template-parts/post/menu-query.php"), payload); + var post = await Config.Client.PostWithTriesAsync( + SystemUrl.MakeRelativeUri("/wp-content/themes/ranobe-novels/template-parts/post/menu-query.php"), + payload); var toc = await post.Content.ReadFromJsonAsync(); - result.AddRange(toc.Select((a, i) => new UrlChapter(SystemUrl.MakeRelativeUri(a.PostName), $"Глава {offset + i + 1}"))); + result.AddRange(toc.Select((a, i) => + new UrlChapter(SystemUrl.MakeRelativeUri(a.PostName), $"Глава {offset + i + 1}"))); } return SliceToc(result, c => c.Title).ToList(); } - private Task GetCover(HtmlDocument doc, Uri uri) { + private Task GetCover(HtmlDocument doc, Uri uri) + { var imagePath = doc.QuerySelector("meta[property=og:image]")?.Attributes["content"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(uri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } - private async Task GetSafety(Uri url) { + private async Task GetSafety(Uri url) + { var response = await Config.Client.GetWithTriesAsync(url); await Task.Delay(TimeSpan.FromSeconds(1)); - - while (response == default || response.StatusCode == HttpStatusCode.ServiceUnavailable) { + + while (response is null || response.StatusCode == HttpStatusCode.ServiceUnavailable) + { Config.Logger.LogInformation("Получен бан от системы. Жду..."); var errorTimeout = TimeSpan.FromSeconds(30); response = await Config.Client.GetWithTriesAsync(url, errorTimeout); await Task.Delay(errorTimeout); } - + return await response.Content.ReadAsStreamAsync().ContinueWith(t => t.Result.AsHtmlDoc()); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/RanobeOvh/MangaOvhGetter.cs b/Core/Logic/Getters/RanobeOvh/MangaOvhGetter.cs index a35d2ced..4e489730 100644 --- a/Core/Logic/Getters/RanobeOvh/MangaOvhGetter.cs +++ b/Core/Logic/Getters/RanobeOvh/MangaOvhGetter.cs @@ -7,19 +7,20 @@ using Core.Types.RanobeOvh; using HtmlAgilityPack; -namespace Core.Logic.Getters.RanobeOvh; +namespace Core.Logic.Getters.RanobeOvh; -public class MangaOvhGetter(BookGetterConfig config) : RanobeOvhGetterBase(config) { +public class MangaOvhGetter(BookGetterConfig config) : RanobeOvhGetterBase(config) +{ protected override Uri SystemUrl => new("https://manga.ovh/"); - protected override async Task GetChapter(RanobeOvhChapterShort ranobeOvhChapterFull) { - var data = await Config.Client.GetFromJsonAsync($"https://api.{SystemUrl.Host}/chapter/{ranobeOvhChapterFull.Id}"); + protected override async Task GetChapter(RanobeOvhChapterShort ranobeOvhChapterFull) + { + var data = await Config.Client.GetFromJsonAsync( + $"https://api.{SystemUrl.Host}/chapter/{ranobeOvhChapterFull.Id}"); var sb = new StringBuilder(); - foreach (var page in data.Pages) { - sb.Append($""); - } + foreach (var page in data.Pages) sb.Append($""); return sb.AsHtmlDoc(); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/RanobeOvh/NovelOvhGetter.cs b/Core/Logic/Getters/RanobeOvh/NovelOvhGetter.cs index 1253d37e..4bb392fc 100644 --- a/Core/Logic/Getters/RanobeOvh/NovelOvhGetter.cs +++ b/Core/Logic/Getters/RanobeOvh/NovelOvhGetter.cs @@ -7,75 +7,84 @@ using System.Text.Json.Nodes; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.RanobeOvh; using HtmlAgilityPack; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters.RanobeOvh; +namespace Core.Logic.Getters.RanobeOvh; -public class NovelOvhGetter(BookGetterConfig config) : RanobeOvhGetterBase(config) { - protected override Uri SystemUrl => new("https://novel.ovh/"); - - private static readonly Dictionary RecursiveTag = new() { +public class NovelOvhGetter(BookGetterConfig config) : RanobeOvhGetterBase(config) +{ + private static readonly Dictionary RecursiveTag = new() + { { "paragraph", "p" }, { "orderedList", "ol" }, { "listItem", "li" }, - { "blockquote", "blockquote" }, + { "blockquote", "blockquote" } }; - - private static readonly Dictionary InlineTag = new() { + + private static readonly Dictionary InlineTag = new() + { { "horizontalRule", "
" }, - { "hardBreak", "
" }, + { "hardBreak", "
" } }; - - private static readonly Dictionary MarkTag = new() { + + private static readonly Dictionary MarkTag = new() + { { "italic", "i" }, { "bold", "b" }, - { "underline", "u" }, + { "underline", "u" } }; - private StringBuilder AsHtml(RanobeOvhChapterFull chapterFullResponse, IEnumerable contents) { + protected override Uri SystemUrl => new("https://novel.ovh/"); + + private StringBuilder AsHtml(RanobeOvhChapterFull chapterFullResponse, + IEnumerable contents) + { var sb = new StringBuilder(); - foreach (var content in contents) { - if (RecursiveTag.TryGetValue(content.Type, out var tag)) { - sb.Append(AsHtml(chapterFullResponse, content.Content).ToString().CoverTag(tag)); + foreach (var content in contents) + { + if (RecursiveTag.TryGetValue(content.Type, out var tag)) + { + sb.Append(AsHtml(chapterFullResponse, content.Content).ToString().CoverTag(tag)); continue; } - - if (InlineTag.TryGetValue(content.Type, out tag)) { + + if (InlineTag.TryGetValue(content.Type, out tag)) + { sb.Append(tag); continue; } - switch (content.Type) { - case "text": { + switch (content.Type) + { + case "text": + { var text = content.Text.HtmlEncode(); - foreach (var mark in content.Marks) { - if (MarkTag.TryGetValue(mark.Type, out tag)) { + foreach (var mark in content.Marks) + if (MarkTag.TryGetValue(mark.Type, out tag)) text = text.CoverTag(tag); - } else { + else Config.Logger.LogInformation($"Неизвестый тип форматирования {mark.Type}"); - } - } sb.Append(text); continue; } - case "image": { - if (content.Attrs.TryGetValue("pages", out var pages)) { - foreach (var imageId in pages.Deserialize()) { + case "image": + { + if (content.Attrs.TryGetValue("pages", out var pages)) + foreach (var imageId in pages.Deserialize()) + { var attachment = chapterFullResponse.Pages.FirstOrDefault(a => a.Id == imageId); - if (attachment == default) { - continue; - } - + if (attachment is null) continue; + sb.Append($""); } - } - + continue; } default: @@ -86,13 +95,16 @@ private StringBuilder AsHtml(RanobeOvhChapterFull chapterFullResponse, IEnumerab return sb; } - - protected override async Task GetChapter(RanobeOvhChapterShort ranobeOvhChapterFull) { - var data = await Config.Client.GetFromJsonAsync($"https://api.{SystemUrl.Host}/v2/chapters/{ranobeOvhChapterFull.Id}"); - return data.Content switch { + + protected override async Task GetChapter(RanobeOvhChapterShort ranobeOvhChapterFull) + { + var data = await Config.Client.GetFromJsonAsync( + $"https://api.{SystemUrl.Host}/v2/chapters/{ranobeOvhChapterFull.Id}"); + return data.Content switch + { JsonValue e => e.GetValue().AsHtmlDoc(), JsonObject o => AsHtml(data, o.Deserialize().Content).AsHtmlDoc(), - _ => throw new Exception("Неизвестный тип") + _ => throw new Elib2EbookParseException("Неизвестный тип") }; } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/RanobeOvh/RanobeOvhGetterBase.cs b/Core/Logic/Getters/RanobeOvh/RanobeOvhGetterBase.cs index 862e09db..14f9e32e 100644 --- a/Core/Logic/Getters/RanobeOvh/RanobeOvhGetterBase.cs +++ b/Core/Logic/Getters/RanobeOvh/RanobeOvhGetterBase.cs @@ -13,24 +13,28 @@ using HtmlAgilityPack; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters.RanobeOvh; +namespace Core.Logic.Getters.RanobeOvh; -public abstract class RanobeOvhGetterBase(BookGetterConfig config) : GetterBase(config) { - protected override string GetId(Uri url) { +public abstract class RanobeOvhGetterBase(BookGetterConfig config) : GetterBase(config) +{ + protected override string GetId(Uri url) + { return url.GetSegment(2); } protected abstract Task GetChapter(RanobeOvhChapterShort ranobeOvhChapterFull); - - public override async Task Get(Uri url) { + + public override async Task Get(Uri url) + { url = SystemUrl.MakeRelativeUri($"/content/{GetId(url)}"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - + var manga = GetNextData(doc, "book"); var chapters = GetNextData(doc, "chapters"); var branch = GetBranch(doc); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(manga, url), Chapters = await FillChapters(branch, chapters, url), Title = manga.Name.Ru, @@ -40,20 +44,22 @@ public override async Task Get(Uri url) { return book; } - - protected override HttpRequestMessage GetImageRequestMessage(Uri uri) { + + protected override HttpRequestMessage GetImageRequestMessage(Uri uri) + { var message = base.GetImageRequestMessage(uri); message.Headers.Referrer = SystemUrl; return message; } - private async Task> FillChapters(RanobeOvhBranch branch, RanobeOvhChapterShort[] chapters, Uri url) { + private async Task> FillChapters(RanobeOvhBranch branch, RanobeOvhChapterShort[] chapters, + Uri url) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - foreach (var ranobeOvhChapter in GetToc(branch, chapters)) { + foreach (var ranobeOvhChapter in GetToc(branch, chapters)) + { var chapter = new Chapter(); Config.Logger.LogInformation($"Загружаю главу {ranobeOvhChapter.FullName.CoverQuotes()}"); @@ -68,8 +74,10 @@ private async Task> FillChapters(RanobeOvhBranch branch, Ra return result; } - private T GetNextData(HtmlDocument doc, string node) { - var json = Regex.Match(doc.ParsedText, "__remixContext = (?.*?);", RegexOptions.Singleline).Groups["data"].Value; + private T GetNextData(HtmlDocument doc, string node) + { + var json = Regex.Match(doc.ParsedText, "__remixContext = (?.*?);", RegexOptions.Singleline) + .Groups["data"].Value; return JsonDocument.Parse(json) .RootElement.GetProperty("state") .GetProperty("loaderData") @@ -79,25 +87,29 @@ private T GetNextData(HtmlDocument doc, string node) { .Deserialize(); } - private IEnumerable GetToc(RanobeOvhBranch branch, RanobeOvhChapterShort[] chapters) { + private IEnumerable GetToc(RanobeOvhBranch branch, RanobeOvhChapterShort[] chapters) + { return SliceToc(chapters.Where(c => c.BranchId == branch.Id).Reverse().ToList(), c => c.FullName); } - private RanobeOvhBranch GetBranch(HtmlDocument doc) { + private RanobeOvhBranch GetBranch(HtmlDocument doc) + { var branches = GetNextData(doc, "branches"); return branches.MaxBy(c => c.ChaptersCount); } - private Author GetAuthor(RanobeOvhBranch branch) { - if (branch.Translators == null) { - return new Author("RanobeOvh"); - } - + private Author GetAuthor(RanobeOvhBranch branch) + { + if (branch.Translators == null) return new Author("RanobeOvh"); + var translator = branch.Translators[0]; return new Author(translator.Name, SystemUrl.MakeRelativeUri($"/translator/{translator.Slug}")); } - private Task GetCover(RanobeOvhManga manga, Uri uri) { - return !string.IsNullOrWhiteSpace(manga.Poster) ? SaveImage(uri.MakeRelativeUri(manga.Poster)) : Task.FromResult(default(TempFile)); + private Task GetCover(RanobeOvhManga manga, Uri uri) + { + return !string.IsNullOrWhiteSpace(manga.Poster) + ? SaveImage(uri.MakeRelativeUri(manga.Poster)) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/RanobesComGetter.cs b/Core/Logic/Getters/RanobesComGetter.cs index 233a883d..2596a876 100644 --- a/Core/Logic/Getters/RanobesComGetter.cs +++ b/Core/Logic/Getters/RanobesComGetter.cs @@ -16,32 +16,39 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class RanobesComGetter(BookGetterConfig config) : GetterBase(config) { +public class RanobesComGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://ranobes.com/"); - protected override string GetId(Uri url) => base.GetId(url).Split(".")[0]; + protected override string GetId(Uri url) + { + return base.GetId(url).Split(".")[0]; + } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { url = await GetMainUrl(url); url = SystemUrl.MakeRelativeUri($"/ranobe/{GetId(url)}.html"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); await Antibot(doc, url); - doc = await Config.Client.GetHtmlDocWithTriesAsync(url); + doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, url), Title = doc.QuerySelector("h1.title").FirstChild.InnerText.Trim().HtmlDecode(), Author = new Author(doc.GetTextBySelector("span[itemprop=creator]") ?? "Ranobes"), Annotation = doc.QuerySelector("div[itemprop=description]")?.RemoveNodes("style")?.InnerHtml }; - + return book; } - private async Task Antibot(HtmlDocument doc, Uri referrer) { + private async Task Antibot(HtmlDocument doc, Uri referrer) + { var h1 = Regex.Match(doc.ParsedText, "var h1 = \'(?.*?)\'").Groups["id"].Value; var h2 = Regex.Match(doc.ParsedText, "var h2 = \'(?.*?)\'").Groups["id"].Value; var date = Regex.Match(doc.ParsedText, "var date = \'(?.*?)\'").Groups["id"].Value; @@ -51,7 +58,8 @@ private async Task Antibot(HtmlDocument doc, Uri referrer) { var v = Regex.Match(doc.ParsedText, "var v = \'(?.*?)\'").Groups["id"].Value; var antibot = Regex.Match(doc.ParsedText, "antibot_(?.*?)=").Groups["id"].Value; - var data = new Dictionary { + var data = new Dictionary + { { "hdc", "0" }, { "scheme", "https" }, { "a", "0" }, @@ -70,18 +78,21 @@ private async Task Antibot(HtmlDocument doc, Uri referrer) { { "co", "24" }, { "pi", "24" }, { "ref", "ranobes.com" }, - { "xxx", string.Empty }, + { "xxx", string.Empty } }; - + Config.Client.DefaultRequestHeaders.Add("Referer", referrer.ToString()); - var post = await Config.Client.PostAsync(SystemUrl.MakeRelativeUri("antibot8/ab.php"), new FormUrlEncodedContent(data)); + var post = await Config.Client.PostAsync(SystemUrl.MakeRelativeUri("antibot8/ab.php"), + new FormUrlEncodedContent(data)); var cookie = await post.Content.ReadFromJsonAsync(); Config.CookieContainer.Add(SystemUrl, new Cookie($"antibot_{antibot}", cookie.Cookie + "-" + date)); Config.Client.DefaultRequestHeaders.Remove("Referer"); } - private async Task GetMainUrl(Uri url) { - if (url.GetSegment(1) == "chapters" || !url.Segments.Last().EndsWith(".html")) { + private async Task GetMainUrl(Uri url) + { + if (url.GetSegment(1) == "chapters" || !url.Segments.Last().EndsWith(".html")) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri(url.AbsolutePath)); return url.MakeRelativeUri(doc.QuerySelector("a[rel=up]").Attributes["href"].Value); } @@ -89,13 +100,13 @@ private async Task GetMainUrl(Uri url) { return url; } - private async Task> FillChapters(HtmlDocument doc, Uri url) { + private async Task> FillChapters(HtmlDocument doc, Uri url) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - foreach (var ranobeChapter in await GetToc(GetTocLink(doc, url))) { + foreach (var ranobeChapter in await GetToc(GetTocLink(doc, url))) + { Config.Logger.LogInformation($"Загружаю главу {ranobeChapter.Title.CoverQuotes()}"); var chapter = new Chapter(); var chapterDoc = await GetChapter(ranobeChapter); @@ -109,57 +120,69 @@ private async Task> FillChapters(HtmlDocument doc, Uri url) return result; } - private async Task GetChapter(UrlChapter chapter) { + private async Task GetChapter(UrlChapter chapter) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(chapter.Url); var sb = new StringBuilder(); - foreach (var node in doc.QuerySelectorAll("#arrticle > :not(.splitnewsnavigation)")) { + foreach (var node in doc.QuerySelectorAll("#arrticle > :not(.splitnewsnavigation)")) + { var tag = node.Name == "#text" ? "p" : node.Name; - if (tag == "img") { + if (tag == "img") + { sb.Append(node.OuterHtml.Trim().CoverTag(tag)); - } else { - if (node.InnerHtml?.Contains("window.yaContextCb") == false) { - sb.Append(node.InnerHtml.CoverTag(tag)); - } + } + else + { + if (node.InnerHtml?.Contains("window.yaContextCb") == false) sb.Append(node.InnerHtml.CoverTag(tag)); } } - + return sb.AsHtmlDoc().RemoveNodes(t => t.Name is "script" or "br" || t.Id?.Contains("yandex_rtb") == true); } - private Task GetCover(HtmlDocument doc, Uri bookUri) { + private Task GetCover(HtmlDocument doc, Uri bookUri) + { var imagePath = doc.QuerySelector("div.poster img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(bookUri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } - private Uri GetTocLink(HtmlDocument doc, Uri uri) { - var relativeUri = doc.QuerySelector("div.r-fullstory-btns a[title~=оглавление]").Attributes["href"].Value.HtmlDecode(); - if (!relativeUri.Contains("chapters")) { + private Uri GetTocLink(HtmlDocument doc, Uri uri) + { + var relativeUri = doc.QuerySelector("div.r-fullstory-btns a[title~=оглавление]").Attributes["href"].Value + .HtmlDecode(); + if (!relativeUri.Contains("chapters")) relativeUri = $"/chapters/{string.Join("-", GetId(uri).Split(".")[0].Split("-").Skip(1))}"; - } - + return uri.MakeRelativeUri(relativeUri.AsUri().AbsolutePath.Trim('/')); } - - private async Task> GetToc(Uri tocUri) { + + private async Task> GetToc(Uri tocUri) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(tocUri); var lastA = doc.QuerySelector("div.pages a:last-child")?.InnerText; var pages = string.IsNullOrWhiteSpace(lastA) ? 1 : int.Parse(lastA); - + Config.Logger.LogInformation("Получаю оглавление"); var result = new List(); - for (var i = 1; i <= pages; i++) { + for (var i = 1; i <= pages; i++) + { doc = await Config.Client.GetHtmlDocWithTriesAsync(tocUri.AppendSegment($"/page/{i}")); var chapters = doc .QuerySelectorAll("#dle-content > .cat_block.cat_line a[title]") - .Select(a => new UrlChapter(a.Attributes["href"].Value.AsUri(), string.IsNullOrWhiteSpace(a.Attributes["title"].Value) ? "Без названия" : a.Attributes["title"].Value)) + .Select(a => new UrlChapter(a.Attributes["href"].Value.AsUri(), + string.IsNullOrWhiteSpace(a.Attributes["title"].Value) + ? "Без названия" + : a.Attributes["title"].Value)) .ToList(); - + result.AddRange(chapters); } - + Config.Logger.LogInformation($"Получено {result.Count} глав"); result.Reverse(); return SliceToc(result, c => c.Title); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/ReadMangaGetter.cs b/Core/Logic/Getters/ReadMangaGetter.cs index 0e0223a5..9d8caa8f 100644 --- a/Core/Logic/Getters/ReadMangaGetter.cs +++ b/Core/Logic/Getters/ReadMangaGetter.cs @@ -13,53 +13,63 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class ReadMangaGetter(BookGetterConfig config) : GetterBase(config) { +public class ReadMangaGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://readmanga.live/"); - protected override string GetId(Uri url) => url.GetSegment(1); - public override async Task Get(Uri url) { + protected override string GetId(Uri url) + { + return url.GetSegment(1); + } + + public override async Task Get(Uri url) + { url = SystemUrl.MakeRelativeUri(GetId(url)); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, url), Title = doc.GetTextBySelector("span.name"), Author = new Author("ReadManga"), Annotation = doc.QuerySelector("div.manga-description").InnerHtml }; - + return book; } - private async Task> FillChapters(HtmlDocument doc, Uri url) { + private async Task> FillChapters(HtmlDocument doc, Uri url) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } - - foreach (var urlChapter in GetToc(doc, url)) { + if (Config.Options.NoChapters) return result; + + foreach (var urlChapter in GetToc(doc, url)) + { Config.Logger.LogInformation($"Загружаю главу {urlChapter.Title.CoverQuotes()}"); - var chapter = new Chapter { + var chapter = new Chapter + { Title = urlChapter.Title }; var chapterDoc = await GetChapter(urlChapter); - if (chapterDoc != default) { + if (chapterDoc is not null) + { chapter.Images = await GetImages(chapterDoc, url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; } - + result.Add(chapter); } return result; } - private IEnumerable GetToc(HtmlDocument doc, Uri url) { + private IEnumerable GetToc(HtmlDocument doc, Uri url) + { var result = doc .QuerySelectorAll("td.item-title a.chapter-link") .Select(a => new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.GetText().ReplaceNewLine())) @@ -68,30 +78,33 @@ private IEnumerable GetToc(HtmlDocument doc, Uri url) { return SliceToc(result, c => c.Title); } - private async Task GetChapter(UrlChapter urlChapter) { + private async Task GetChapter(UrlChapter urlChapter) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(urlChapter.Url.AppendQueryParameter("mtr", "1")); - if (doc.QuerySelector("div.buy-button") != default) { - return default; - } - - var json = Regex.Match(doc.ParsedText, @"readerInit(.*?)(?\[\[(.*?)]]),").Groups["data"].Value.Replace("'", "\"").Deserialize>(); + if (doc.QuerySelector("div.buy-button") is not null) return default; + + var json = Regex.Match(doc.ParsedText, @"readerInit(.*?)(?\[\[(.*?)]]),").Groups["data"].Value + .Replace("'", "\"").Deserialize>(); var sb = new StringBuilder(); - - foreach (var elem in json) { + + foreach (var elem in json) + { var imageUri = elem[0].GetString().AsUri().MakeRelativeUri(elem[2].GetString()); // Это не костыль. На сайте также - if (imageUri.Host == "one-way.work") { - imageUri = imageUri.MakeRelativeUri(imageUri.AbsolutePath); - } + if (imageUri.Host == "one-way.work") imageUri = imageUri.MakeRelativeUri(imageUri.AbsolutePath); + sb.Append($""); } - + return sb.AsHtmlDoc(); } - private Task GetCover(HtmlDocument doc, Uri bookUri) { + private Task GetCover(HtmlDocument doc, Uri bookUri) + { var imagePath = doc.QuerySelector("div.picture-fotorama img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(bookUri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/ReadliGetter.cs b/Core/Logic/Getters/ReadliGetter.cs index 7cb0fe65..fe4d8d10 100644 --- a/Core/Logic/Getters/ReadliGetter.cs +++ b/Core/Logic/Getters/ReadliGetter.cs @@ -11,20 +11,22 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class ReadliGetter(BookGetterConfig config) : GetterBase(config) { - protected override Uri SystemUrl => new("https://readli.net"); - +public class ReadliGetter(BookGetterConfig config) : GetterBase(config) +{ // cloudflare :( private readonly Uri _host = new("https://213.227.140.81/"); + protected override Uri SystemUrl => new("https://readli.net"); - public override async Task Init() { + public override async Task Init() + { await base.Init(); Config.Client.DefaultRequestHeaders.Add("Host", SystemUrl.Host); } - - public override async Task Get(Uri url) { + + public override async Task Get(Uri url) + { url = await GetMainUrl(url); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); @@ -32,9 +34,10 @@ public override async Task Get(Uri url) { var imageDiv = doc.QuerySelector("div.book-image"); var href = url.MakeRelativeUri(imageDiv.QuerySelector("a").Attributes["href"].Value); var bookId = GetBookId(href); - + var title = doc.GetTextBySelector("h1.main-info__title"); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(imageDiv, url), Chapters = await FillChapters(bookId, pages, title), Title = title, @@ -43,46 +46,52 @@ public override async Task Get(Uri url) { Seria = GetSeria(doc, url) }; - return book; + return book; } - - private static Author GetAuthor(HtmlDocument doc, Uri url) { + + private static Author GetAuthor(HtmlDocument doc, Uri url) + { var author = doc.QuerySelector("a.main-info__link"); - return new Author(author.InnerText.HtmlDecode(), url.MakeRelativeUri(author.Attributes["href"]?.Value ?? string.Empty)); + return new Author(author.InnerText.HtmlDecode(), + url.MakeRelativeUri(author.Attributes["href"]?.Value ?? string.Empty)); } - - private static Seria GetSeria(HtmlDocument doc, Uri url) { + + private static Seria GetSeria(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("div.book-info a.book-info__link[href^=/serie/]"); - if (string.IsNullOrWhiteSpace(a.GetText())) { - return default; - } + if (string.IsNullOrWhiteSpace(a.GetText())) return default; var text = a.GetText(); var uri = url.MakeRelativeUri(a.Attributes["href"].Value); - if (!text.Contains('#')) { - return new Seria { + if (!text.Contains('#')) + return new Seria + { Name = text, Url = uri }; - } var parts = text.Split('#', StringSplitOptions.RemoveEmptyEntries); - return new Seria { + return new Seria + { Name = parts[0].HtmlDecode(), Number = parts[1].HtmlDecode(), Url = uri }; - } - private static string GetBookId(Uri uri) { - return uri.GetQueryParameter("b") ?? throw new InvalidOperationException("Не удалось определить идентификатор книги"); + private static string GetBookId(Uri uri) + { + return uri.GetQueryParameter("b") ?? + throw new InvalidOperationException("Не удалось определить идентификатор книги"); } - private async Task GetMainUrl(Uri url) { - if (GetId(url).StartsWith("chitat-online", StringComparison.InvariantCultureIgnoreCase)) { - var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri($"/chitat-online/?b={GetBookId(url)}").ReplaceHost(_host.Host)); + private async Task GetMainUrl(Uri url) + { + if (GetId(url).StartsWith("chitat-online", StringComparison.InvariantCultureIgnoreCase)) + { + var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl + .MakeRelativeUri($"/chitat-online/?b={GetBookId(url)}").ReplaceHost(_host.Host)); var href = doc.QuerySelector("h1 a").Attributes["href"].Value; return url.MakeRelativeUri(href).ReplaceHost(_host.Host); } @@ -90,83 +99,94 @@ private async Task GetMainUrl(Uri url) { return url.ReplaceHost(_host.Host); } - private Task GetCover(HtmlNode doc, Uri bookUri) { + private Task GetCover(HtmlNode doc, Uri bookUri) + { var imagePath = doc.QuerySelector("img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath).ReplaceHost(_host.Host)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(bookUri.MakeRelativeUri(imagePath).ReplaceHost(_host.Host)) + : Task.FromResult(default(TempFile)); } - private async Task AddChapter(ICollection chapters, Chapter chapter, StringBuilder text) { - if (chapter == null) { - return; - } - + private async Task AddChapter(ICollection chapters, Chapter chapter, StringBuilder text) + { + if (chapter == null) return; + var chapterDoc = text.AsHtmlDoc(); - chapter.Images = await GetImages(chapterDoc, SystemUrl.MakeRelativeUri("/chitat-online/").ReplaceHost(_host.Host)); + chapter.Images = + await GetImages(chapterDoc, SystemUrl.MakeRelativeUri("/chitat-online/").ReplaceHost(_host.Host)); chapter.Content = chapterDoc.DocumentNode.InnerHtml; chapters.Add(chapter); } - private static bool IsSingleChapter(IEnumerable nodes) { + private static bool IsSingleChapter(IEnumerable nodes) + { var firstNode = nodes.First(); return firstNode.Name != "h3" || string.IsNullOrWhiteSpace(firstNode.InnerText); } - private static Chapter CreateChapter(string title) { - return new Chapter { + private static Chapter CreateChapter(string title) + { + return new Chapter + { Title = title }; } - private async Task GetChapter(string bookId, int page) { - var response = await Config.Client.GetWithTriesAsync(SystemUrl.MakeRelativeUri($"/chitat-online/?b={bookId}&pg={page}").ReplaceHost(_host.Host)); - if (response == default) { - return default; - } + private async Task GetChapter(string bookId, int page) + { + var response = await Config.Client.GetWithTriesAsync(SystemUrl + .MakeRelativeUri($"/chitat-online/?b={bookId}&pg={page}").ReplaceHost(_host.Host)); + if (response is null) return default; return await response.Content.ReadAsStreamAsync().ContinueWith(t => t.Result.AsHtmlDoc()); } - private async Task> FillChapters(string bookId, long pages, string name) { + private async Task> FillChapters(string bookId, long pages, string name) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } - + if (Config.Options.NoChapters) return result; + Chapter chapter = null; var singleChapter = true; var text = new StringBuilder(); - - for (var i = 1; i <= pages; i++) { + + for (var i = 1; i <= pages; i++) + { Config.Logger.LogInformation($"Получаю страницу {i}/{pages}"); var page = await GetChapter(bookId, i); - if (page == default) { + if (page is null) + { Config.Logger.LogInformation("Неожиданный конец"); break; } - + var content = page.QuerySelector("div.reading__text").RemoveNodes("script, ins, div[id*=adfox]"); var nodes = content.QuerySelectorAll("> h3, > p, > img"); nodes = nodes.Count == 0 ? content.ChildNodes : nodes; singleChapter = i == 1 ? IsSingleChapter(nodes) : singleChapter; - foreach (var node in nodes) { - if (singleChapter || node.Name != "h3") { - if (node.Name == "img" && node.Attributes["src"] != null) { + foreach (var node in nodes) + if (singleChapter || node.Name != "h3") + { + if (node.Name == "img" && node.Attributes["src"] != null) + { text.Append($""); - } else { - if (!string.IsNullOrWhiteSpace(node.InnerHtml)) { + } + else + { + if (!string.IsNullOrWhiteSpace(node.InnerHtml)) text.Append(node.InnerHtml.HtmlDecode().CoverTag("p")); - } } - } else { + } + else + { await AddChapter(result, chapter, text); text.Clear(); chapter = CreateChapter(node.InnerHtml.HtmlDecode()); } - } } await AddChapter(result, chapter ?? CreateChapter(name), text); return result; } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/Renovels/RemangaGetter.cs b/Core/Logic/Getters/Renovels/RemangaGetter.cs index 3a769b22..c22562d5 100644 --- a/Core/Logic/Getters/Renovels/RemangaGetter.cs +++ b/Core/Logic/Getters/Renovels/RemangaGetter.cs @@ -7,31 +7,32 @@ using Core.Types.Renovels; using HtmlAgilityPack; -namespace Core.Logic.Getters.Renovels; +namespace Core.Logic.Getters.Renovels; -public class RemangaGetter(BookGetterConfig config) : RenovelsGetterBase(config) { +public class RemangaGetter(BookGetterConfig config) : RenovelsGetterBase(config) +{ protected override Uri SystemUrl => new("https://remanga.org/"); - + protected override string Segment => "manga"; - - protected override HtmlDocument GetChapterAsHtml(RenovelsChapter response) { + + protected override HtmlDocument GetChapterAsHtml(RenovelsChapter response) + { var sb = new StringBuilder(); - foreach (var obj in response.Pages) { - switch (obj) { + foreach (var obj in response.Pages) + switch (obj) + { case JsonObject: sb.Append($""); break; - case JsonArray pages: { - foreach (var page in pages) { - sb.Append($""); - } + case JsonArray pages: + { + foreach (var page in pages) sb.Append($""); break; } } - } return sb.AsHtmlDoc(); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/Renovels/RenovelsGetter.cs b/Core/Logic/Getters/Renovels/RenovelsGetter.cs index 6d24b89d..cc7e97aa 100644 --- a/Core/Logic/Getters/Renovels/RenovelsGetter.cs +++ b/Core/Logic/Getters/Renovels/RenovelsGetter.cs @@ -4,14 +4,16 @@ using Core.Types.Renovels; using HtmlAgilityPack; -namespace Core.Logic.Getters.Renovels; +namespace Core.Logic.Getters.Renovels; -public class RenovelsGetter(BookGetterConfig config) : RenovelsGetterBase(config) { +public class RenovelsGetter(BookGetterConfig config) : RenovelsGetterBase(config) +{ protected override Uri SystemUrl => new("https://renovels.org/"); - + protected override string Segment => "novel"; - - protected override HtmlDocument GetChapterAsHtml(RenovelsChapter response) { + + protected override HtmlDocument GetChapterAsHtml(RenovelsChapter response) + { return response.Content.AsHtmlDoc(); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/Renovels/RenovelsGetterBase.cs b/Core/Logic/Getters/Renovels/RenovelsGetterBase.cs index e5006bcd..32b24ee4 100644 --- a/Core/Logic/Getters/Renovels/RenovelsGetterBase.cs +++ b/Core/Logic/Getters/Renovels/RenovelsGetterBase.cs @@ -7,6 +7,7 @@ using System.Text.Json.Nodes; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.Common; @@ -14,89 +15,99 @@ using HtmlAgilityPack; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters.Renovels; +namespace Core.Logic.Getters.Renovels; -public abstract class RenovelsGetterBase(BookGetterConfig config) : GetterBase(config) { +public abstract class RenovelsGetterBase(BookGetterConfig config) : GetterBase(config) +{ private Uri _apiUrl => new($"https://api.{SystemUrl.Host}/"); - + protected abstract string Segment { get; } - + protected abstract HtmlDocument GetChapterAsHtml(RenovelsChapter response); - protected override string GetId(Uri url) => url.GetSegment(2); + protected override string GetId(Uri url) + { + return url.GetSegment(2); + } - public override Task Init() { + public override Task Init() + { Config.Client.DefaultRequestHeaders.Add("User-Agent", "remanga/1.1.6 CFNetwork/1408.0.4 Darwin/22.5.0"); Config.Client.DefaultRequestHeaders.Add("Referer", SystemUrl.ToString()); return Task.CompletedTask; } - public override async Task Authorize() { - if (!Config.HasCredentials) { - return; - } - - var payload = new { + public override async Task Authorize() + { + if (!Config.HasCredentials) return; + + var payload = new + { user = Config.Options.Login, password = Config.Options.Password }; - var response = await Config.Client.PostAsJsonAsync("https://api.renovels.org/api/users/login/".AsUri(), payload); + var response = + await Config.Client.PostAsJsonAsync("https://api.renovels.org/api/users/login/".AsUri(), payload); var data = await response.Content.ReadFromJsonAsync>(); - if (string.IsNullOrWhiteSpace(data.Message)) { + if (string.IsNullOrWhiteSpace(data.Message)) + { var auth = data.Content.Deserialize(); Config.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", auth.Token); Config.Logger.LogInformation("Успешно авторизовались"); - } else { - throw new Exception($"Не удалось авторизоваться. {data.Message}"); + } + else + { + throw new Elib2EbookAuthException($"Не удалось авторизоваться. {data.Message}"); } } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { var bookId = GetId(url); var content = await GetContent(bookId); - var book = new Book(SystemUrl.MakeRelativeUri($"/{Segment}/{bookId}")) { + var book = new Book(SystemUrl.MakeRelativeUri($"/{Segment}/{bookId}")) + { Cover = await GetCover(content, url), Chapters = await FillChapters(content, url), Title = content.MainName ?? content.SecondaryName ?? content.AnotherName, Author = GetAuthor(content), Annotation = content.Description }; - + return book; } - - private Author GetAuthor(RenovelsContent content) { - if (content.Branches.Length == 0) { - return new Author("Renovels"); - } - - if (content.Branches[0].Publishers.Length == 0) { - return new Author("Renovels"); - } + + private Author GetAuthor(RenovelsContent content) + { + if (content.Branches.Length == 0) return new Author("Renovels"); + + if (content.Branches[0].Publishers.Length == 0) return new Author("Renovels"); var author = content.Branches[0].Publishers[0]; return new Author(author.Name, SystemUrl.MakeRelativeUri($"/team/{author.Dir}")); } - private async Task GetContent(string bookId) { - var response = await Config.Client.GetFromJsonAsync(_apiUrl.MakeRelativeUri($"/api/v2/titles/{bookId}/")); + private async Task GetContent(string bookId) + { + var response = + await Config.Client.GetFromJsonAsync(_apiUrl.MakeRelativeUri($"/api/v2/titles/{bookId}/")); return response; } - private async Task> FillChapters(RenovelsContent content, Uri url) { + private async Task> FillChapters(RenovelsContent content, Uri url) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } - - foreach (var ranobeChapter in await GetToc(content)) { + if (Config.Options.NoChapters) return result; + + foreach (var ranobeChapter in await GetToc(content)) + { Config.Logger.LogInformation($"Загружаю главу {ranobeChapter.Title.CoverQuotes()}"); - + var chapter = new Chapter(); var doc = await GetChapter(ranobeChapter); - + chapter.Images = await GetImages(doc, url); chapter.Content = doc.DocumentNode.InnerHtml; chapter.Title = ranobeChapter.Title; @@ -107,29 +118,37 @@ private async Task> FillChapters(RenovelsContent content, U return result; } - private async Task GetChapter(RenovelsChapter ranobeChapter) { + private async Task GetChapter(RenovelsChapter ranobeChapter) + { var makeRelativeUri = _apiUrl.MakeRelativeUri($"/api/v2/titles/chapters/{ranobeChapter.Id}/"); var response = await Config.Client.GetFromJsonAsync(makeRelativeUri); return GetChapterAsHtml(response); } - private Task GetCover(RenovelsContent book, Uri bookUri) { + private Task GetCover(RenovelsContent book, Uri bookUri) + { var imagePath = book.Cover.GetValueOrDefault("high", null) ?? book.Cover.FirstOrDefault().Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(bookUri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } - private async Task> GetToc(RenovelsContent content) { + private async Task> GetToc(RenovelsContent content) + { var result = new List(); - - for (var i = 1;; i++) { - var uri = _apiUrl.MakeRelativeUri($"/api/v2/titles/chapters/?branch_id={content.Branches[0].Id}&ordering=-index&user_data=1&count=40&page={i}"); + + for (var i = 1;; i++) + { + var uri = _apiUrl.MakeRelativeUri( + $"/api/v2/titles/chapters/?branch_id={content.Branches[0].Id}&ordering=-index&user_data=1&count=40&page={i}"); var response = await Config.Client.GetFromJsonAsync(uri); result.AddRange(response.Results); - if (response.Results.Length < 40) { + if (response.Results.Length < 40) + { result.Reverse(); return SliceToc(result.Where(c => !c.IsPaid || c.IsBought == true).ToList(), c => c.Name); } } } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/RomfantGetter.cs b/Core/Logic/Getters/RomfantGetter.cs index 7d29d05d..c0651a5f 100644 --- a/Core/Logic/Getters/RomfantGetter.cs +++ b/Core/Logic/Getters/RomfantGetter.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.Common; @@ -12,79 +13,83 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class RomfantGetter(BookGetterConfig config) : GetterBase(config) { +public class RomfantGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://romfant.ru"); /// - /// Авторизация в системе + /// Авторизация в системе /// /// - public override async Task Authorize() { - if (!Config.HasCredentials) { - return; - } - + public override async Task Authorize() + { + if (!Config.HasCredentials) return; + using var post = await Config.Client.PostAsync(SystemUrl.MakeRelativeUri("ajax/"), GenerateAuthData()); var response = await post.Content.ReadAsStringAsync(); - if (response == "ok") { + if (response == "ok") Config.Logger.LogInformation("Успешно авторизовались"); - } else { - throw new Exception($"Не удалось авторизоваться. {response.AsHtmlDoc().DocumentNode.GetText()}"); - } + else + throw new Elib2EbookAuthException( + $"Не удалось авторизоваться. {response.AsHtmlDoc().DocumentNode.GetText()}"); } - private MultipartFormDataContent GenerateAuthData() { - return new() { - {new StringContent("auth"), "action"}, - {new StringContent(Config.Options.Login), "login"}, - {new StringContent(Config.Options.Password), "pass"}, + private MultipartFormDataContent GenerateAuthData() + { + return new MultipartFormDataContent + { + { new StringContent("auth"), "action" }, + { new StringContent(Config.Options.Login), "login" }, + { new StringContent(Config.Options.Password), "pass" } }; } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { var bookId = GetId(url); url = SystemUrl.MakeRelativeUri($"/book/{bookId}/"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); var title = doc.GetTextBySelector("h1"); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc), Chapters = await FillChapters(bookId, title), Title = title, Author = new Author("Витамина Мятная"), - Annotation = doc.QuerySelector("div.subscribe div.text-center p")?.InnerHtml.HtmlDecode(), + Annotation = doc.QuerySelector("div.subscribe div.text-center p")?.InnerHtml.HtmlDecode() }; - + return book; } - private async Task> FillChapters(string bookId, string title) { - if (Config.Options.NoChapters) { - return []; - } - + private async Task> FillChapters(string bookId, string title) + { + if (Config.Options.NoChapters) return []; + var chapter = new Chapter(); var sb = new StringBuilder(); var pages = await GetPages(bookId); - for (var i = 1; i <= pages; i++) { + for (var i = 1; i <= pages; i++) + { Config.Logger.LogInformation($"Получаю страницу {i}/{pages}"); var uri = SystemUrl.MakeRelativeUri($"/read/{bookId}/?page={i}"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(uri); - if (doc.DocumentNode.InnerText.Contains("Чтобы продолжить чтение, пожалуйста, оплатите доступ.")) { + if (doc.DocumentNode.InnerText.Contains("Чтобы продолжить чтение, пожалуйста, оплатите доступ.")) + { Config.Logger.LogInformation("Платный контент"); break; } - foreach (var p in doc.QuerySelectorAll("div.book p")) { + foreach (var p in doc.QuerySelectorAll("div.book p")) + { var content = p.GetText(); - if (!string.IsNullOrWhiteSpace(content)) { - sb.AppendLine(content.CoverTag("p")); - } + if (!string.IsNullOrWhiteSpace(content)) sb.AppendLine(content.CoverTag("p")); } } @@ -94,18 +99,22 @@ private async Task> FillChapters(string bookId, string titl return new[] { chapter }; } - private async Task GetPages(string bookId) { + private async Task GetPages(string bookId) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri($"/read/{bookId}/")); var pages = doc.QuerySelectorAll("div.navigation > a[href]") .Where(a => int.TryParse(a.GetText(), out _)) .Select(a => int.Parse(a.GetText())) .ToList(); - + return pages.Any() ? pages.Max() : 1; } - private Task GetCover(HtmlDocument doc) { + private Task GetCover(HtmlDocument doc) + { var imagePath = doc.QuerySelector("div.subscribe img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(SystemUrl.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(SystemUrl.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/RoyalRoadGetter.cs b/Core/Logic/Getters/RoyalRoadGetter.cs index 6e9a49ee..59df50c3 100644 --- a/Core/Logic/Getters/RoyalRoadGetter.cs +++ b/Core/Logic/Getters/RoyalRoadGetter.cs @@ -10,40 +10,46 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class RoyalRoadGetter(BookGetterConfig config) : GetterBase(config) { +public class RoyalRoadGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://royalroad.com/"); - protected override string GetId(Uri url) => url.GetSegment(2); + protected override string GetId(Uri url) + { + return url.GetSegment(2); + } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { url = SystemUrl.MakeRelativeUri($"/fiction/{GetId(url)}"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, url), Title = doc.GetTextBySelector("h1"), Author = GetAuthor(doc, url), - Annotation = doc.QuerySelector("div.description div.hidden-content")?.InnerHtml, + Annotation = doc.QuerySelector("div.description div.hidden-content")?.InnerHtml }; - + return book; } - - private async Task> FillChapters(HtmlDocument doc, Uri url) { + + private async Task> FillChapters(HtmlDocument doc, Uri url) + { var result = new List(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - foreach (var bookChapter in GetToc(doc, url)) { + foreach (var bookChapter in GetToc(doc, url)) + { var chapter = new Chapter(); Config.Logger.LogInformation($"Загружаю главу {bookChapter.Title.CoverQuotes()}"); - + var chapterDoc = await GetChapter(bookChapter.Url); - + chapter.Title = bookChapter.Title; chapter.Images = await GetImages(chapterDoc, url); chapter.Content = chapterDoc.DocumentNode.InnerHtml.HtmlDecode(); @@ -54,36 +60,40 @@ private async Task> FillChapters(HtmlDocument doc, Uri url) return result; } - private async Task GetChapter(Uri url) { + private async Task GetChapter(Uri url) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); var content = doc.QuerySelector("div.chapter-content"); - foreach (var node in content.QuerySelectorAll("p, span")) { - node.Attributes.RemoveAll(); - } + foreach (var node in content.QuerySelectorAll("p, span")) node.Attributes.RemoveAll(); return content.InnerHtml.AsHtmlDoc(); } - private IEnumerable GetToc(HtmlDocument doc, Uri url) { + private IEnumerable GetToc(HtmlDocument doc, Uri url) + { var result = doc .QuerySelectorAll("tr.chapter-row") .Select(r => r.QuerySelector("a[href^=/fiction/]")) .Select(a => new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.GetText())) .ToList(); - + return SliceToc(result, c => c.Title); } - private Author GetAuthor(HtmlDocument doc, Uri url) { + private Author GetAuthor(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("div.fic-header h4 a[href*=profile]"); - return a == default ? - new Author("Royalroad") : - new Author(a.GetText(), SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)); + return a is null + ? new Author("Royalroad") + : new Author(a.GetText(), SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)); } - private Task GetCover(HtmlDocument doc, Uri uri) { + private Task GetCover(HtmlDocument doc, Uri uri) + { var imagePath = doc.QuerySelector("div.cover-art-container img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(uri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/Rulate/BllateGetter.cs b/Core/Logic/Getters/Rulate/BllateGetter.cs index 70797e58..105a6cbe 100644 --- a/Core/Logic/Getters/Rulate/BllateGetter.cs +++ b/Core/Logic/Getters/Rulate/BllateGetter.cs @@ -3,7 +3,8 @@ namespace Core.Logic.Getters.Rulate; -public class BllateGetter(BookGetterConfig config) : RulateGetterBase(config) { +public class BllateGetter(BookGetterConfig config) : RulateGetterBase(config) +{ protected override Uri SystemUrl => new("https://bllate.org"); protected override string Mature { get; } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/Rulate/ErolateGetter.cs b/Core/Logic/Getters/Rulate/ErolateGetter.cs index bd2321d9..b514e8ac 100644 --- a/Core/Logic/Getters/Rulate/ErolateGetter.cs +++ b/Core/Logic/Getters/Rulate/ErolateGetter.cs @@ -1,9 +1,10 @@ using System; using Core.Configs; -namespace Core.Logic.Getters.Rulate; +namespace Core.Logic.Getters.Rulate; -public class ErolateGetter(BookGetterConfig config) : RulateGetterBase(config) { +public class ErolateGetter(BookGetterConfig config) : RulateGetterBase(config) +{ protected override Uri SystemUrl => new("https://erolate.com/"); protected override string Mature => "7da3ee594b38fc5355692d978fe8f5adbeb3d17di%3A1%3B"; -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/Rulate/MlateGetter.cs b/Core/Logic/Getters/Rulate/MlateGetter.cs index da495bfc..31949b0d 100644 --- a/Core/Logic/Getters/Rulate/MlateGetter.cs +++ b/Core/Logic/Getters/Rulate/MlateGetter.cs @@ -1,9 +1,10 @@ using System; using Core.Configs; -namespace Core.Logic.Getters.Rulate; +namespace Core.Logic.Getters.Rulate; -public class MlateGetter(BookGetterConfig config) : RulateGetterBase(config) { +public class MlateGetter(BookGetterConfig config) : RulateGetterBase(config) +{ protected override Uri SystemUrl => new("https://mlate.ru/"); protected override string Mature => "c3a2ed4b199a1a15f5a5483504c7a75a7030dc4bi%3A1%3B"; -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/Rulate/RulateGetter.cs b/Core/Logic/Getters/Rulate/RulateGetter.cs index b8e174f6..ce4aeafc 100644 --- a/Core/Logic/Getters/Rulate/RulateGetter.cs +++ b/Core/Logic/Getters/Rulate/RulateGetter.cs @@ -1,9 +1,10 @@ using System; using Core.Configs; -namespace Core.Logic.Getters.Rulate; +namespace Core.Logic.Getters.Rulate; -public class RulateGetter(BookGetterConfig config) : RulateGetterBase(config) { +public class RulateGetter(BookGetterConfig config) : RulateGetterBase(config) +{ protected override Uri SystemUrl => new("https://tl.rulate.ru"); protected override string Mature => "c3a2ed4b199a1a15f5a5483504c7a75a7030dc4bi%3A1%3B"; -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/Rulate/RulateGetterBase.cs b/Core/Logic/Getters/Rulate/RulateGetterBase.cs index 6d38c501..64afc49d 100644 --- a/Core/Logic/Getters/Rulate/RulateGetterBase.cs +++ b/Core/Logic/Getters/Rulate/RulateGetterBase.cs @@ -6,6 +6,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.Common; @@ -14,109 +15,128 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters.Rulate; +namespace Core.Logic.Getters.Rulate; -public abstract class RulateGetterBase(BookGetterConfig config) : GetterBase(config) { - protected override string GetId(Uri url) => url.Segments.Length == 3 ? base.GetId(url) : url.GetSegment(2); - +public abstract class RulateGetterBase(BookGetterConfig config) : GetterBase(config) +{ protected abstract string Mature { get; } - - public override async Task Init() { + + protected override string GetId(Uri url) + { + return url.Segments.Length == 3 ? base.GetId(url) : url.GetSegment(2); + } + + public override async Task Init() + { await base.Init(); Config.Client.DefaultRequestHeaders.Add("X-Requested-With", "XMLHttpRequest"); Config.CookieContainer.Add(SystemUrl, new Cookie("mature", Mature)); } - public override async Task Authorize() { - if (!Config.HasCredentials) { - return; - } - + public override async Task Authorize() + { + if (!Config.HasCredentials) return; + var doc = await Config.Client.PostHtmlDocWithTriesAsync(SystemUrl, GetAuthData()); var alertBlock = doc.QuerySelector("div.alert.in.alert-block"); - - if (alertBlock == default) { + + if (alertBlock is null) + { RulateAuthResponse error = null; - try { + try + { error = doc.ParsedText.Deserialize(); - } catch { } - - if (!string.IsNullOrWhiteSpace(error?.Error)) { - throw new Exception($"Не удалось авторизоваться. {error.Error}"); } - + catch + { + // Ignore deserialization errors if the response is not valid JSON + } + + if (!string.IsNullOrWhiteSpace(error?.Error)) + throw new Elib2EbookAuthException($"Не удалось авторизоваться. {error.Error}"); + Config.Logger.LogInformation("Успешно авторизовались"); - } else { - throw new Exception($"Не удалось авторизоваться. {alertBlock.GetText()}"); + } + else + { + throw new Elib2EbookAuthException($"Не удалось авторизоваться. {alertBlock.GetText()}"); } } - - private FormUrlEncodedContent GetAuthData() { - var data = new Dictionary { + + private FormUrlEncodedContent GetAuthData() + { + var data = new Dictionary + { ["login[login]"] = Config.Options.Login, - ["login[pass]"] = Config.Options.Password, + ["login[pass]"] = Config.Options.Password }; return new FormUrlEncodedContent(data); } - public override async Task Get(Uri url) { + public override async Task Get(Uri url) + { var bookId = GetId(url); url = SystemUrl.MakeRelativeUri($"/book/{bookId}"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, url, bookId), Title = GetTitle(doc), Author = GetAuthor(doc), Annotation = doc.QuerySelector("#Info div.btn-toolbar + div")?.InnerHtml }; - + return book; } - private static string GetTitle(HtmlDocument doc) { + private static string GetTitle(HtmlDocument doc) + { var match = Regex.Match(doc.ParsedText, "t_title: '(?.*?)',"); return match.Success ? match.Groups["title"].Value : doc.GetTextBySelector("h1"); } - private Author GetAuthor(HtmlDocument doc) { + private Author GetAuthor(HtmlDocument doc) + { var def = new Author("rulate"); - foreach (var p in doc.QuerySelectorAll("#Info p")) { + foreach (var p in doc.QuerySelectorAll("#Info p")) + { var strong = p.QuerySelector("strong"); - if (strong != null && strong.InnerText.Contains("Автор")) { + if (strong != null && strong.InnerText.Contains("Автор")) + { var em = p.QuerySelector("em"); - if (em == default) { - return def; - } - - var a = em.QuerySelector("a[href]"); - return a == default ? - new Author(em.GetText()) : - new Author(a.GetText(), SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)); + if (em is null) return def; + var a = em.QuerySelector("a[href]"); + return a is null + ? new Author(em.GetText()) + : new Author(a.GetText(), SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)); } } return def; } - - private Task<TempFile> GetCover(HtmlDocument doc, Uri bookUri) { + + private Task<TempFile> GetCover(HtmlDocument doc, Uri bookUri) + { var imagePath = doc.QuerySelector("div.slick img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(bookUri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } - - private async Task<List<Chapter>> FillChapters(HtmlDocument doc, Uri bookUri, string bookId) { + + private async Task<List<Chapter>> FillChapters(HtmlDocument doc, Uri bookUri, string bookId) + { var result = new List<Chapter>(); - if (Config.Options.NoChapters) { - return result; - } - - foreach (var (id, name) in GetToc(doc)) { + if (Config.Options.NoChapters) return result; + + foreach (var (id, name) in GetToc(doc)) + { Config.Logger.LogInformation($"Загружаю главу {name.CoverQuotes()}"); var chapter = new Chapter(); - + var chapterDoc = await GetChapter(bookId, id); chapter.Images = await GetImages(chapterDoc, bookUri); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -128,16 +148,21 @@ private async Task<List<Chapter>> FillChapters(HtmlDocument doc, Uri bookUri, st return result; } - private async Task<HtmlDocument> GetChapter(string bookId, string chapterId) { - var s = await Config.Client.GetFromJsonWithTriesAsync<RulateChapter>(SystemUrl.MakeRelativeUri($"/book/{bookId}/{chapterId}/readyajax")); - return (s.CanRead ? s.Content.AsHtmlDoc().QuerySelector("div.content-text")?.InnerHtml ?? string.Empty : string.Empty).AsHtmlDoc(); + private async Task<HtmlDocument> GetChapter(string bookId, string chapterId) + { + var s = await Config.Client.GetFromJsonWithTriesAsync<RulateChapter>( + SystemUrl.MakeRelativeUri($"/book/{bookId}/{chapterId}/readyajax")); + return (s.CanRead + ? s.Content.AsHtmlDoc().QuerySelector("div.content-text")?.InnerHtml ?? string.Empty + : string.Empty).AsHtmlDoc(); } - private IEnumerable<IdChapter> GetToc(HtmlDocument doc) { + private IEnumerable<IdChapter> GetToc(HtmlDocument doc) + { var result = doc.QuerySelectorAll("#Chapters tr[data-id]") .Select(chapter => new IdChapter(chapter.Attributes["data-id"].Value, chapter.GetTextBySelector("td.t"))) .ToList(); - + return SliceToc(result, c => c.Title); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/SamlibGetter.cs b/Core/Logic/Getters/SamlibGetter.cs index f87971ed..8b6f3dbe 100644 --- a/Core/Logic/Getters/SamlibGetter.cs +++ b/Core/Logic/Getters/SamlibGetter.cs @@ -11,9 +11,10 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class SamlibGetter(BookGetterConfig config) : GetterBase(config) { +public class SamlibGetter(BookGetterConfig config) : GetterBase(config) +{ private const string START_BOOK_PATTERN = "Собственно произведение"; private const string ABOUT_BLOCK_PATTERN = "Блок описания произведения"; private const string END_BOOK_PATTERN = "-----------------------------------------------"; @@ -21,46 +22,46 @@ public class SamlibGetter(BookGetterConfig config) : GetterBase(config) { private const string START_LINK_BLOCK_PATTERN = "Блок ссылок на произведения"; private const string END_LINK_BLOCK_PATTERN = "Подножие"; - private static Encoding _encoding = Encoding.GetEncoding(1251); + private static readonly Encoding _encoding = Encoding.GetEncoding(1251); protected override Uri SystemUrl => new("http://samlib.ru/"); - public override async Task<Book> Get(Uri url) { + + public override async Task<Book> Get(Uri url) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(url, _encoding); var title = doc.GetTextBySelector("h2, h3 font"); - var book = new Book(url) { + var book = new Book(url) + { Cover = null, Chapters = await FillChapters(doc, url, title), Title = title, Author = GetAuthor(doc, url) }; - + return book; } - - private static Author GetAuthor(HtmlDocument doc, Uri url) { + + private static Author GetAuthor(HtmlDocument doc, Uri url) + { var def = new Author("Samlib"); - + var h3 = doc.QuerySelector("h3"); - if (h3 == default) { - return def; - } + if (h3 is null) return def; var a = h3.QuerySelector("a[href]"); - if (a == default) { - return def; - } + if (a is null) return def; return new Author(h3.RemoveNodes("small").GetText().Trim(':'), url.MakeRelativeUri(a.Attributes["href"].Value)); } - private async Task<IEnumerable<Chapter>> FillChapters(HtmlDocument doc, Uri url, string title) { + private async Task<IEnumerable<Chapter>> FillChapters(HtmlDocument doc, Uri url, string title) + { var result = new List<Chapter>(); - if (Config.Options.NoChapters) { - return result; - } - - foreach (var urlChapter in GetToc(doc, url, title)) { + if (Config.Options.NoChapters) return result; + + foreach (var urlChapter in GetToc(doc, url, title)) + { Config.Logger.LogInformation($"Загружаю главу {urlChapter.Title.CoverQuotes()}"); result.Add(await GetChapter(urlChapter)); } @@ -68,86 +69,77 @@ private async Task<IEnumerable<Chapter>> FillChapters(HtmlDocument doc, Uri url, return result; } - private IEnumerable<UrlChapter> GetToc(HtmlDocument doc, Uri url, string title) { + private IEnumerable<UrlChapter> GetToc(HtmlDocument doc, Uri url, string title) + { var content = GetStringBetween(doc.Text, START_LINK_BLOCK_PATTERN, END_LINK_BLOCK_PATTERN); - if (string.IsNullOrWhiteSpace(content)) { + if (string.IsNullOrWhiteSpace(content)) yield return new UrlChapter(url, title); - } else { - foreach (var a in content.AsHtmlDoc().QuerySelectorAll("li > a")) { + else + foreach (var a in content.AsHtmlDoc().QuerySelectorAll("li > a")) yield return new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.GetText()); - } - } } - private static string GetStringBetween(string origin, string startPattern, string endPattern) { + private static string GetStringBetween(string origin, string startPattern, string endPattern) + { var start = origin.IndexOf(startPattern, StringComparison.InvariantCultureIgnoreCase); - if (start == -1) { - return string.Empty; - } - + if (start == -1) return string.Empty; + start = origin.IndexOf(">", start, StringComparison.InvariantCultureIgnoreCase) + 1; var stop = origin.IndexOf(endPattern, start, StringComparison.InvariantCultureIgnoreCase); - for (var i = stop;; i--) { - if (origin[i] == '<') { + for (var i = stop;; i--) + if (origin[i] == '<') + { stop = i - 1; break; } - } return origin[start..stop]; } - - private static string GetBookContent(string origin) { + + private static string GetBookContent(string origin) + { var start = origin.IndexOf(START_BOOK_PATTERN, StringComparison.InvariantCultureIgnoreCase); - if (start == -1) { - return string.Empty; - } + if (start == -1) return string.Empty; var about = origin.LastIndexOf(ABOUT_BLOCK_PATTERN, StringComparison.OrdinalIgnoreCase); - if (about == -1) { - return string.Empty; - } - + if (about == -1) return string.Empty; + start = origin.IndexOf(">", start, StringComparison.InvariantCultureIgnoreCase) + 1; var stop = origin.LastIndexOf(END_BOOK_PATTERN, about, StringComparison.InvariantCultureIgnoreCase); - - for (var i = stop;; i--) { - if (origin[i] == '<') { + + for (var i = stop;; i--) + if (origin[i] == '<') + { stop = i - 1; break; } - } return origin[start..stop].HtmlDecode(); } - private async Task<Chapter> GetChapter(UrlChapter urlChapter) { + private async Task<Chapter> GetChapter(UrlChapter urlChapter) + { var chapter = new Chapter(); var doc = await Config.Client.GetHtmlDocWithTriesAsync(urlChapter.Url, _encoding); doc.LoadHtml(GetBookContent(doc.Text)); - + var sr = new StringReader(doc.DocumentNode.InnerHtml.HtmlDecode()); var text = new StringBuilder(); - while (true) { + while (true) + { var line = await sr.ReadLineAsync(); - if (line == null) { - break; - } + if (line == null) break; + + if (string.IsNullOrWhiteSpace(line)) continue; - if (string.IsNullOrWhiteSpace(line)) { - continue; - } - var htmlDoc = line.AsHtmlDoc(); - foreach (var node in htmlDoc.DocumentNode.ChildNodes) { - if (!string.IsNullOrWhiteSpace(node.InnerText) || node.QuerySelector("img") != null) { + foreach (var node in htmlDoc.DocumentNode.ChildNodes) + if (!string.IsNullOrWhiteSpace(node.InnerText) || node.QuerySelector("img") != null) text.Append(node.InnerHtml.HtmlDecode().CoverTag("p")); - } - } } - + var chapterDoc = text.AsHtmlDoc(); chapter.Images = await GetImages(chapterDoc, urlChapter.Url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -155,4 +147,4 @@ private async Task<Chapter> GetChapter(UrlChapter urlChapter) { return chapter; } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/StrokiMtsGetter.cs b/Core/Logic/Getters/StrokiMtsGetter.cs index 80e0bd0e..d21137d5 100644 --- a/Core/Logic/Getters/StrokiMtsGetter.cs +++ b/Core/Logic/Getters/StrokiMtsGetter.cs @@ -9,6 +9,7 @@ using System.Text; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Misc; using Core.Types.Book; @@ -18,198 +19,213 @@ namespace Core.Logic.Getters; -public class StrokiMtsGetter(BookGetterConfig config) : GetterBase(config) { +public class StrokiMtsGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://stroki.mts.ru/"); - protected override string GetId(Uri url) { + protected override string GetId(Uri url) + { return url.GetSegment(2).Split("-").Last(); } - public override Task Authorize() { + public override Task Authorize() + { var token = Config.Options.Login ?? Config.Options.Password; - if (string.IsNullOrWhiteSpace(token)) { - throw new Exception("Не указан авторизационный token."); - } - + if (string.IsNullOrWhiteSpace(token)) throw new Elib2EbookAuthException("Не указан авторизационный token."); + Config.Client.DefaultRequestHeaders.Add("access-token", token); return Task.CompletedTask; } - - public override async Task<Book> Get(Uri url) { + + public override async Task<Book> Get(Uri url) + { var id = GetId(url); - + var details = await GetBook(id); - var detailBook = details?.Items.FirstOrDefault(d => d.TextBook != default)?.TextBook ?? - details?.Items.FirstOrDefault(d => d.AudioBook != default)?.AudioBook ?? - details?.Items.FirstOrDefault(d => d.ComicBook != default)?.ComicBook ?? - details?.Items.FirstOrDefault(d => d.PressBook != default)?.PressBook ?? + var detailBook = details?.Items.FirstOrDefault(d => d.TextBook is not null)?.TextBook ?? + details?.Items.FirstOrDefault(d => d.AudioBook is not null)?.AudioBook ?? + details?.Items.FirstOrDefault(d => d.ComicBook is not null)?.ComicBook ?? + details?.Items.FirstOrDefault(d => d.PressBook is not null)?.PressBook ?? await GetSerial(id); - if (detailBook == default) { - throw new Exception("Не удалось получить книгу"); - } - - var book = new Book(url) { + if (detailBook is null) throw new Elib2EbookParseException("Не удалось получить книгу"); + + var book = new Book(url) + { Cover = await GetCover(detailBook), Title = detailBook.Title, Author = GetAuthor(detailBook), CoAuthors = GetCoAuthors(detailBook), - Annotation = detailBook.Annotation, + Annotation = detailBook.Annotation }; - - if (Config.Options.NoChapters) { - return book; - } - + + if (Config.Options.NoChapters) return book; + var origBooks = await GetOriginalFiles(id); - if (origBooks.Count == 1) { - if (detailBook.ContentInfo.FileType == "EPUB") { + if (origBooks.Count == 1) + { + if (detailBook.ContentInfo.FileType == "EPUB") book.Chapters = await FillChaptersFromEpub(origBooks[0]); - } else { + else Config.Logger.LogInformation("Эта книга не в формате epub. Получить главы невозможно"); - } - } else { - if (detailBook.ContentInfo.FileType == "JPG") { - book.Chapters = FillChaptersFromImages(detailBook, origBooks); - } } - - if (Config.Options.HasAdditionalType(AdditionalTypeEnum.Books)) { - foreach (var origBook in origBooks) { - book.AdditionalFiles.Add(AdditionalTypeEnum.Books, origBook); - } + else + { + if (detailBook.ContentInfo.FileType == "JPG") book.Chapters = FillChaptersFromImages(detailBook, origBooks); } - if (Config.Options.HasAdditionalType(AdditionalTypeEnum.Audio)) { + if (Config.Options.HasAdditionalType(AdditionalTypeEnum.Books)) + foreach (var origBook in origBooks) + book.AdditionalFiles.Add(AdditionalTypeEnum.Books, origBook); + + if (Config.Options.HasAdditionalType(AdditionalTypeEnum.Audio)) book.AdditionalFiles.Add(AdditionalTypeEnum.Audio, await GetAudio(details)); - } - + return book; } - private static IEnumerable<Chapter> FillChaptersFromImages(StrokiMtsBookItem book, IEnumerable<TempFile> images) { + private static IEnumerable<Chapter> FillChaptersFromImages(StrokiMtsBookItem book, IEnumerable<TempFile> images) + { var sb = new StringBuilder(); - foreach (var image in images) { - sb.Append($"<img src=\"{image.FullName}\" />"); - } - - var chapter = new Chapter { + foreach (var image in images) sb.Append($"<img src=\"{image.FullName}\" />"); + + var chapter = new Chapter + { Images = images, Content = sb.ToString(), - Title = book.Title, + Title = book.Title }; - - return new List<Chapter>{chapter}; + + return new List<Chapter> { chapter }; } - private async Task<List<TempFile>> GetOriginalFiles(string bookId) { + private async Task<List<TempFile>> GetOriginalFiles(string bookId) + { var fileMetas = await GetFileMeta(bookId); var result = new ConcurrentBag<Tuple<TempFile, int>>(); - + var tuples = fileMetas.Select((file, i) => Tuple.Create(file, i)); - await Parallel.ForEachAsync(tuples, async (t, _) => { + await Parallel.ForEachAsync(tuples, async (t, _) => + { var fileUrl = await GetFileUrl(t.Item1); var response = await Config.Client.GetWithTriesAsync(fileUrl.Url.AsUri()); Config.Logger.LogInformation($"Оригинальный файл доступен по ссылке {fileUrl.Url.AsUri()}"); - var tempFile = await TempFile.Create(fileUrl.Url.AsUri(), Config.TempFolder.Path, fileUrl.Url.AsUri().GetFileName(), await response.Content.ReadAsStreamAsync(_)); - + var tempFile = await TempFile.Create(fileUrl.Url.AsUri(), Config.TempFolder.Path, + fileUrl.Url.AsUri().GetFileName(), await response.Content.ReadAsStreamAsync(_)); + result.Add(Tuple.Create(tempFile, t.Item2)); }); - return result.OrderBy(t => t.Item2).Select(t => t.Item1).ToList(); } - private async Task<List<TempFile>> GetAudio(StrokiMtsApiMultiResponse details) { + private async Task<List<TempFile>> GetAudio(StrokiMtsApiMultiResponse details) + { var result = new List<TempFile>(); - var detail = details?.Items.FirstOrDefault(d => d.AudioBook != default); - if (detail == default) { - return result; - } + var detail = details?.Items.FirstOrDefault(d => d.AudioBook is not null); + if (detail is null) return result; var audio = detail.AudioBook; var fileMetas = await GetFileMeta(audio.Id.ToString()); - for (var i = 0; i < fileMetas.Length; i++) { + for (var i = 0; i < fileMetas.Length; i++) + { var meta = fileMetas[i]; var fileUrl = await GetFileUrl(meta); Config.Logger.LogInformation($"Загружаю аудиоверсию {i + 1}/{fileMetas.Length} {fileUrl.Url}"); var response = await Config.Client.GetWithTriesAsync(fileUrl.Url.AsUri()); - result.Add(await TempFile.Create(fileUrl.Url.AsUri(), Config.TempFolder.Path, $"{i}_{fileUrl.Url.AsUri().GetFileName()}", await response.Content.ReadAsStreamAsync())); + result.Add(await TempFile.Create(fileUrl.Url.AsUri(), Config.TempFolder.Path, + $"{i}_{fileUrl.Url.AsUri().GetFileName()}", await response.Content.ReadAsStreamAsync())); Config.Logger.LogInformation($"Аудиоверсия {i + 1}/{fileMetas.Length} {fileUrl.Url} загружена"); } return result; } - - private Task<TempFile> GetCover(StrokiMtsBookItem book) { + + private Task<TempFile> GetCover(StrokiMtsBookItem book) + { var url = book.ImageUrl.TryGetValue("extraLarge", out var item) ? item : book.ImageUrl.FirstOrDefault().Value; - return !string.IsNullOrWhiteSpace(url) ? SaveImage(SystemUrl.MakeRelativeUri(url)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(url) + ? SaveImage(SystemUrl.MakeRelativeUri(url)) + : Task.FromResult(default(TempFile)); } - - private Author GetAuthor(StrokiMtsBookItem book) { + + private Author GetAuthor(StrokiMtsBookItem book) + { var author = book.Authors.FirstOrDefault(); - return author == default ? new Author("Stroki") : new Author(author.Name, SystemUrl.MakeRelativeUri(author.FriendlyUrl)); + return author is null + ? new Author("Stroki") + : new Author(author.Name, SystemUrl.MakeRelativeUri(author.FriendlyUrl)); } - - private IEnumerable<Author> GetCoAuthors(StrokiMtsBookItem book) { - return book.Authors.Skip(1).Select(author => new Author(author.Name, SystemUrl.MakeRelativeUri(author.FriendlyUrl))).ToList(); + + private IEnumerable<Author> GetCoAuthors(StrokiMtsBookItem book) + { + return book.Authors.Skip(1) + .Select(author => new Author(author.Name, SystemUrl.MakeRelativeUri(author.FriendlyUrl))).ToList(); } - private async Task<StrokiMtsFile[]> GetFileMeta(string id) { - var json = await SendAsync<StrokiMtsApiResponse<StrokiMtsFiles>>(() => GetMessage(SystemUrl.MakeRelativeUri("/api/books/files").AppendQueryParameter("bookId", id), "5.0", "5.0")); + private async Task<StrokiMtsFile[]> GetFileMeta(string id) + { + var json = await SendAsync<StrokiMtsApiResponse<StrokiMtsFiles>>(() => + GetMessage(SystemUrl.MakeRelativeUri("/api/books/files").AppendQueryParameter("bookId", id), "5.0", "5.0")); return json.Data.Full?.Length > 0 ? json.Data.Full : [json.Data.Preview]; } - - private async Task<StrokiMtsFileUrl> GetFileUrl(StrokiMtsFile file) { - var json = await SendAsync<StrokiMtsApiResponse<StrokiMtsFileUrl>>(() => GetMessage(SystemUrl.MakeRelativeUri($"api/books/files/data/link/{file.FileId}"), "5.0", "5.0")); + + private async Task<StrokiMtsFileUrl> GetFileUrl(StrokiMtsFile file) + { + var json = await SendAsync<StrokiMtsApiResponse<StrokiMtsFileUrl>>(() => + GetMessage(SystemUrl.MakeRelativeUri($"api/books/files/data/link/{file.FileId}"), "5.0", "5.0")); return json.Data; } - private async Task<StrokiMtsApiMultiResponse> GetBook(string id) { - var json = await SendAsync<StrokiMtsApiResponse<StrokiMtsApiMultiResponse>>(() => GetMessage(SystemUrl.MakeRelativeUri($"/api/books/multi/{id}"), "5.53", "7.0")); + private async Task<StrokiMtsApiMultiResponse> GetBook(string id) + { + var json = await SendAsync<StrokiMtsApiResponse<StrokiMtsApiMultiResponse>>(() => + GetMessage(SystemUrl.MakeRelativeUri($"/api/books/multi/{id}"), "5.53", "7.0")); return json.Data; } - - private async Task<StrokiMtsBookItem> GetSerial(string id) { - var json = await SendAsync<StrokiMtsApiResponse<StrokiMtsBookItem>>(() => GetMessage(SystemUrl.MakeRelativeUri($"/api/serials/episodes/{id} "), "5.53", "7.0")); + + private async Task<StrokiMtsBookItem> GetSerial(string id) + { + var json = await SendAsync<StrokiMtsApiResponse<StrokiMtsBookItem>>(() => + GetMessage(SystemUrl.MakeRelativeUri($"/api/serials/episodes/{id} "), "5.53", "7.0")); return json.Data; } - private async Task<T> SendAsync<T>(Func<HttpRequestMessage> message) { + private async Task<T> SendAsync<T>(Func<HttpRequestMessage> message) + { var response = await Config.Client.SendWithTriesAsync(message); - if (response.StatusCode != HttpStatusCode.OK) { - throw new Exception(await response.Content.ReadAsStringAsync()); - } - + if (response.StatusCode != HttpStatusCode.OK) + throw new Elib2EbookParseException(await response.Content.ReadAsStringAsync()); + return await response.Content.ReadFromJsonAsync<T>(); } - - protected virtual HttpRequestMessage GetMessage(Uri uri, string appVersion, string apiVerions) { + + protected virtual HttpRequestMessage GetMessage(Uri uri, string appVersion, string apiVerions) + { var message = new HttpRequestMessage(HttpMethod.Get, uri); - foreach (var header in Config.Client.DefaultRequestHeaders) { - message.Headers.Add(header.Key, header.Value); - } - + foreach (var header in Config.Client.DefaultRequestHeaders) message.Headers.Add(header.Key, header.Value); + message.Headers.Add("install-guid", Guid.NewGuid().ToString()); message.Headers.Add("app-version", appVersion); message.Headers.Add("language", "ru"); message.Headers.Add("platform", "ios"); message.Headers.Add("api-version", apiVerions); - + message.Headers.Add("signature", GetSignature(uri)); - + return message; } - private static string GetSignature(Uri url) { + private static string GetSignature(Uri url) + { var inputBytes = Encoding.UTF8.GetBytes(url.ToString().Replace("https://", "http://") + "meg@$h!t"); var hashBytes = MD5.HashData(inputBytes); - return Convert.ToHexString(hashBytes).ToLower(); + return Convert.ToHexString(hashBytes).ToLowerInvariant(); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/TopLiba/BiglibaGetter.cs b/Core/Logic/Getters/TopLiba/BiglibaGetter.cs index d99d06f9..060fd60d 100644 --- a/Core/Logic/Getters/TopLiba/BiglibaGetter.cs +++ b/Core/Logic/Getters/TopLiba/BiglibaGetter.cs @@ -8,26 +8,32 @@ using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; -namespace Core.Logic.Getters.TopLiba; +namespace Core.Logic.Getters.TopLiba; -public class BiglibaGetter(BookGetterConfig config) : TopLibaGetterBase(config) { +public class BiglibaGetter(BookGetterConfig config) : TopLibaGetterBase(config) +{ protected override Uri SystemUrl => new("https://bigliba.com/"); - protected override Seria GetSeria(HtmlDocument doc, Uri url) { + protected override Seria GetSeria(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("div.book-series a"); - if (a != default) { + if (a is not null) + { var text = a.GetText(); - - if (text.Contains('#')) { + + if (text.Contains('#')) + { var parts = text.Split(':').Last().Split("(#"); - return new Seria { + return new Seria + { Name = parts[0].Trim(), Number = parts[1].Trim(')').Trim(), Url = url.MakeRelativeUri(a.Attributes["href"].Value) }; } - return new Seria { + return new Seria + { Name = text, Url = url.MakeRelativeUri(a.Attributes["href"].Value) }; @@ -36,8 +42,11 @@ protected override Seria GetSeria(HtmlDocument doc, Uri url) { return default; } - protected override Task<TempFile> GetCover(HtmlDocument doc, Uri uri) { + protected override Task<TempFile> GetCover(HtmlDocument doc, Uri uri) + { var imagePath = doc.QuerySelector("img[itemprop=image]")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(uri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/TopLiba/TopLibaGetter.cs b/Core/Logic/Getters/TopLiba/TopLibaGetter.cs index 97d00494..a2579f17 100644 --- a/Core/Logic/Getters/TopLiba/TopLibaGetter.cs +++ b/Core/Logic/Getters/TopLiba/TopLibaGetter.cs @@ -7,26 +7,30 @@ using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; -namespace Core.Logic.Getters.TopLiba; +namespace Core.Logic.Getters.TopLiba; -public class TopLibaGetter(BookGetterConfig config) : TopLibaGetterBase(config) { +public class TopLibaGetter(BookGetterConfig config) : TopLibaGetterBase(config) +{ protected override Uri SystemUrl => new("https://topliba.com/"); - protected override Seria GetSeria(HtmlDocument doc, Uri url) { + protected override Seria GetSeria(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("div.book-series a"); - if (a != default) { + if (a is not null) + { var text = a.GetText(); var number = a.NextSibling; - - if (number != default && number.GetText().Contains('#')) { - return new Seria { + + if (number is not null && number.GetText().Contains('#')) + return new Seria + { Name = text, Number = number.GetText().Replace("(", "").Replace(")", "").Replace("#", ""), Url = url.MakeRelativeUri(a.Attributes["href"].Value) }; - } - return new Seria { + return new Seria + { Name = text, Url = url.MakeRelativeUri(a.Attributes["href"].Value) }; @@ -35,8 +39,11 @@ protected override Seria GetSeria(HtmlDocument doc, Uri url) { return default; } - protected override Task<TempFile> GetCover(HtmlDocument doc, Uri uri) { + protected override Task<TempFile> GetCover(HtmlDocument doc, Uri uri) + { var imagePath = doc.QuerySelector("img[itemprop=contentUrl]")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(uri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/TopLiba/TopLibaGetterBase.cs b/Core/Logic/Getters/TopLiba/TopLibaGetterBase.cs index bbc18843..e2601c0b 100644 --- a/Core/Logic/Getters/TopLiba/TopLibaGetterBase.cs +++ b/Core/Logic/Getters/TopLiba/TopLibaGetterBase.cs @@ -5,6 +5,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Core.Configs; +using Core.Exceptions; using Core.Extensions; using Core.Types.Book; using Core.Types.Common; @@ -12,22 +13,28 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters.TopLiba; +namespace Core.Logic.Getters.TopLiba; -public abstract class TopLibaGetterBase(BookGetterConfig config) : GetterBase(config) { - protected override string GetId(Uri url) => url.GetSegment(2); +public abstract class TopLibaGetterBase(BookGetterConfig config) : GetterBase(config) +{ + protected override string GetId(Uri url) + { + return url.GetSegment(2); + } protected abstract Seria GetSeria(HtmlDocument doc, Uri url); protected abstract Task<TempFile> GetCover(HtmlDocument doc, Uri uri); - - public override async Task<Book> Get(Uri url) { + + public override async Task<Book> Get(Uri url) + { var bookId = GetId(url); url = SystemUrl.MakeRelativeUri($"/books/{bookId}"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); var title = doc.GetTextBySelector("h1[itemprop=name]"); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(url, bookId, GetToken(doc), title), Title = title, @@ -35,30 +42,30 @@ public override async Task<Book> Get(Uri url) { Annotation = doc.QuerySelector("div.description")?.InnerHtml, Seria = GetSeria(doc, url) }; - + return book; } - public override async Task Authorize() { - if (!Config.HasCredentials) { - return; - } + public override async Task Authorize() + { + if (!Config.HasCredentials) return; var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri("/login")); var token = doc.QuerySelector("meta[name=_token]").Attributes["content"].Value; - + var response = await Config.Client.PostWithTriesAsync(SystemUrl.MakeRelativeUri("/login"), GetAuthData(token)); doc = await response.Content.ReadAsStreamAsync().ContinueWith(t => t.Result.AsHtmlDoc()); var helpBlock = doc.QuerySelector("input[type=email] + span.help-block"); - if (helpBlock == default) { + if (helpBlock is null) Config.Logger.LogInformation("Успешно авторизовались"); - } else { - throw new Exception($"Не удалось авторизоваться. {helpBlock.GetText()}"); - } + else + throw new Elib2EbookAuthException($"Не удалось авторизоваться. {helpBlock.GetText()}"); } - - private FormUrlEncodedContent GetAuthData(string token) { - var data = new Dictionary<string, string> { + + private FormUrlEncodedContent GetAuthData(string token) + { + var data = new Dictionary<string, string> + { ["email"] = Config.Options.Login, ["password"] = Config.Options.Password, ["_token"] = token @@ -67,21 +74,23 @@ private FormUrlEncodedContent GetAuthData(string token) { return new FormUrlEncodedContent(data); } - private static Author GetAuthor(HtmlDocument doc, Uri url) { + private static Author GetAuthor(HtmlDocument doc, Uri url) + { var a = doc.QuerySelector("h2[itemprop=author] a"); return new Author(a.GetText(), url.MakeRelativeUri(a.Attributes["href"].Value)); } - private async Task<IEnumerable<Chapter>> FillChapters(Uri uri, string bookId, string token, string title) { + private async Task<IEnumerable<Chapter>> FillChapters(Uri uri, string bookId, string token, string title) + { var result = new List<Chapter>(); - if (Config.Options.NoChapters) { - return result; - } - - foreach (var id in await GetToc(bookId)) { + if (Config.Options.NoChapters) return result; + + foreach (var id in await GetToc(bookId)) + { var chapter = new Chapter(); var content = await GetChapter(bookId, id, token); - if (content.StartsWith("{\"status\":\"error\"")) { + if (content.StartsWith("{\"status\":\"error\"")) + { Config.Logger.LogInformation($"Часть {id} заблокирована"); continue; } @@ -92,8 +101,7 @@ private async Task<IEnumerable<Chapter>> FillChapters(Uri uri, string bookId, st doc.RemoveNodes("h1"); chapter.Images = await GetImages(doc, uri); chapter.Content = doc.DocumentNode.InnerHtml; - - + Config.Logger.LogInformation($"Загружаю главу {chapter.Title.CoverQuotes()}"); result.Add(chapter); @@ -102,27 +110,34 @@ private async Task<IEnumerable<Chapter>> FillChapters(Uri uri, string bookId, st return result; } - private async Task<string> GetChapter(string bookId, string id, string token) { - var data = await Config.Client.PostWithTriesAsync(SystemUrl.MakeRelativeUri($"/reader/{bookId}/chapter"), GetData(id, token)); + private async Task<string> GetChapter(string bookId, string id, string token) + { + var data = await Config.Client.PostWithTriesAsync(SystemUrl.MakeRelativeUri($"/reader/{bookId}/chapter"), + GetData(id, token)); return await data.Content.ReadAsStringAsync(); } - - private static FormUrlEncodedContent GetData(string chapterId, string token) { - var data = new Dictionary<string, string> { + + private static FormUrlEncodedContent GetData(string chapterId, string token) + { + var data = new Dictionary<string, string> + { ["chapter"] = chapterId, - ["_token"] = token, + ["_token"] = token }; return new FormUrlEncodedContent(data); } - private async Task<IEnumerable<string>> GetToc(string bookId) { + private async Task<IEnumerable<string>> GetToc(string bookId) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri($"/reader/{bookId}")); - var result = new Regex("capters: \\[(?<chapters>.*?)\\]").Match(doc.Text).Groups["chapters"].Value.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim('\"')).ToList(); + var result = new Regex("capters: \\[(?<chapters>.*?)\\]").Match(doc.Text).Groups["chapters"].Value + .Split(',', StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim('\"')).ToList(); return SliceToc(result, c => c); } - private static string GetToken(HtmlDocument doc) { + private static string GetToken(HtmlDocument doc) + { return doc.QuerySelector("meta[name=_token]").Attributes["content"].Value; } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/TwilightRussiaGetter.cs b/Core/Logic/Getters/TwilightRussiaGetter.cs index b776382d..49854c9c 100644 --- a/Core/Logic/Getters/TwilightRussiaGetter.cs +++ b/Core/Logic/Getters/TwilightRussiaGetter.cs @@ -9,29 +9,34 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class TwilightRussiaGetter(BookGetterConfig config) : GetterBase(config) { +public class TwilightRussiaGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://twilightrussia.ru/"); - - public override async Task<Book> Get(Uri url) { + + public override async Task<Book> Get(Uri url) + { url = await GetMainUrl(url); url = SystemUrl.MakeRelativeUri($"/forum/{GetId(url)}"); - + var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - var book = new Book(url) { + var book = new Book(url) + { Cover = null, Chapters = await FillChapters(doc, url), Title = doc.GetTextBySelector("a.forumBarA"), Author = new Author(doc.GetTextBySelector("span[class^='forum_nik']")) }; - + return book; } - private async Task<Uri> GetMainUrl(Uri url) { - if (url.ToString().Contains("/publ/")) { + private async Task<Uri> GetMainUrl(Uri url) + { + if (url.ToString().Contains("/publ/")) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); var a = doc.QuerySelector("#msgd ~ a[href]"); return a.Attributes["href"].Value.AsUri(); @@ -40,20 +45,18 @@ private async Task<Uri> GetMainUrl(Uri url) { return url; } - private async Task<IEnumerable<Chapter>> FillChapters(HtmlDocument doc, Uri url) { - if (Config.Options.NoChapters) { - return []; - } - + private async Task<IEnumerable<Chapter>> FillChapters(HtmlDocument doc, Uri url) + { + if (Config.Options.NoChapters) return []; + var post = doc.QuerySelectorAll("td.content-block-forum"); - + var result = new List<Chapter>(); - foreach (var a in post.QuerySelectorAll("a[href*='/publ/']")) { + foreach (var a in post.QuerySelectorAll("a[href*='/publ/']")) + { var title = a.GetText(); - if (string.IsNullOrWhiteSpace(title)) { - continue; - } - + if (string.IsNullOrWhiteSpace(title)) continue; + Config.Logger.LogInformation($"Загружаю главу {title}"); var href = url.MakeRelativeUri(a.Attributes["href"].Value); result.Add(await GetChapter(href, title)); @@ -61,27 +64,27 @@ private async Task<IEnumerable<Chapter>> FillChapters(HtmlDocument doc, Uri url) return result; } - - private async Task<Chapter> GetChapter(Uri url, string title) { + + private async Task<Chapter> GetChapter(Uri url, string title) + { var chapter = new Chapter(); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - + var text = new StringBuilder(); - foreach (var node in doc.QuerySelector("#msgd").ChildNodes) { + foreach (var node in doc.QuerySelector("#msgd").ChildNodes) + { var img = node.QuerySelector("img[src]"); - if (img != null) { + if (img != null) + { text.Append($"<img src='{img.Attributes["src"].Value}'/>"); continue; } - + var nodeText = node.GetText(); - if (!string.IsNullOrWhiteSpace(nodeText)) { - text.Append(nodeText.CoverTag("p")); - } + if (!string.IsNullOrWhiteSpace(nodeText)) text.Append(nodeText.CoverTag("p")); } - - + var chapterDoc = text.AsHtmlDoc(); chapter.Images = await GetImages(chapterDoc, url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -89,4 +92,4 @@ private async Task<Chapter> GetChapter(Uri url, string title) { return chapter; } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/WattpadGetter.cs b/Core/Logic/Getters/WattpadGetter.cs index dd2b2a4a..bf0cbbc4 100644 --- a/Core/Logic/Getters/WattpadGetter.cs +++ b/Core/Logic/Getters/WattpadGetter.cs @@ -11,48 +11,54 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class WattpadGetter(BookGetterConfig config) : GetterBase(config) { +public class WattpadGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://wattpad.com/"); - protected override string GetId(Uri url) => base.GetId(url).Split('-')[0]; + protected override string GetId(Uri url) + { + return base.GetId(url).Split('-')[0]; + } - private async Task<string> GetStoryId(Uri url) { - if (url.ToString().Contains("/story/")) { - return GetId(url); - } + private async Task<string> GetStoryId(Uri url) + { + if (url.ToString().Contains("/story/")) return GetId(url); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); return doc.QuerySelector("button[data-story-id]").Attributes["data-story-id"].Value; } - public override async Task<Book> Get(Uri url) { + public override async Task<Book> Get(Uri url) + { var storyId = await GetStoryId(url); url = SystemUrl.MakeRelativeUri(storyId); - var wattpadInfo = await Config.Client.GetFromJsonAsync<WattpadInfo>(SystemUrl.MakeRelativeUri($"/api/v3/stories/{storyId}")); + var wattpadInfo = + await Config.Client.GetFromJsonAsync<WattpadInfo>(SystemUrl.MakeRelativeUri($"/api/v3/stories/{storyId}")); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(wattpadInfo), Chapters = await FillChapters(wattpadInfo), Title = wattpadInfo.Title, Author = new Author(wattpadInfo.User.Name, SystemUrl.MakeRelativeUri($"/user/{wattpadInfo.User.Name}")), Annotation = wattpadInfo.Description }; - + return book; } - private async Task<IEnumerable<Chapter>> FillChapters(WattpadInfo wattpadInfo) { + private async Task<IEnumerable<Chapter>> FillChapters(WattpadInfo wattpadInfo) + { var result = new List<Chapter>(); - if (Config.Options.NoChapters) { - return result; - } - - foreach (var group in SliceToc(wattpadInfo.Parts, c => c.FullName)) { + if (Config.Options.NoChapters) return result; + + foreach (var group in SliceToc(wattpadInfo.Parts, c => c.FullName)) + { Config.Logger.LogInformation($"Загружаю главу {group.FullName.CoverQuotes()}"); var chapter = new Chapter(); - + var chapterDoc = await GetChapter(group); chapter.Images = await GetImages(chapterDoc, group.Url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -64,16 +70,18 @@ private async Task<IEnumerable<Chapter>> FillChapters(WattpadInfo wattpadInfo) { return result; } - private async Task<HtmlDocument> GetChapter(WattpadPart part) { + private async Task<HtmlDocument> GetChapter(WattpadPart part) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(part.Url); - foreach (var node in doc.QuerySelectorAll("p")) { - node.Attributes.RemoveAll(); - } - + foreach (var node in doc.QuerySelectorAll("p")) node.Attributes.RemoveAll(); + return doc; } - private Task<TempFile> GetCover(WattpadInfo wattpadInfo) { - return !string.IsNullOrWhiteSpace(wattpadInfo.Cover) ? SaveImage(wattpadInfo.Cover.AsUri()) : Task.FromResult(default(TempFile)); + private Task<TempFile> GetCover(WattpadInfo wattpadInfo) + { + return !string.IsNullOrWhiteSpace(wattpadInfo.Cover) + ? SaveImage(wattpadInfo.Cover.AsUri()) + : Task.FromResult(default(TempFile)); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/WuxiaWorldGetter.cs b/Core/Logic/Getters/WuxiaWorldGetter.cs index dd4b7e57..1cd5725a 100644 --- a/Core/Logic/Getters/WuxiaWorldGetter.cs +++ b/Core/Logic/Getters/WuxiaWorldGetter.cs @@ -15,63 +15,73 @@ using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Core.Logic.Getters; -public class WuxiaWorldGetter(BookGetterConfig config) : GetterBase(config) { +public class WuxiaWorldGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://wuxiaworld.ru/"); - - private async Task<Uri> GetMainUrl(Uri url) { - if (url.GetSegment(1) == "category") { + + private async Task<Uri> GetMainUrl(Uri url) + { + if (url.GetSegment(1) == "category") + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); url = doc.QuerySelector("h2[itemprop=name] a").Attributes["href"].Value.AsUri(); } return url; } - - public override async Task<Book> Get(Uri url) { + + public override async Task<Book> Get(Uri url) + { url = await GetMainUrl(url); var doc = await GetSafety(url); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, url), Title = doc.GetTextBySelector("h1"), - Author = new Author("WuxiaWorld"), + Author = new Author("WuxiaWorld") }; - + return book; } - private async Task<IEnumerable<UrlChapter>> GetToc(HtmlDocument doc, Uri url) { + private async Task<IEnumerable<UrlChapter>> GetToc(HtmlDocument doc, Uri url) + { var catId = Regex.Match(doc.ParsedText, "data-cat=\"(?<catId>\\d+)\"").Groups["catId"].Value; var result = new List<UrlChapter>(); - - foreach (var span in doc.QuerySelectorAll("ul.myUL span.caret[data-id]")) { + + foreach (var span in doc.QuerySelectorAll("ul.myUL span.caret[data-id]")) + { var offset = (int.Parse(span.Attributes["data-id"].Value) - 1) * 100; - var payload = new FormUrlEncodedContent(new Dictionary<string, string> { + var payload = new FormUrlEncodedContent(new Dictionary<string, string> + { ["cat_id"] = catId, ["offset"] = offset.ToString() }); - var post = await Config.Client.PostWithTriesAsync(SystemUrl.MakeRelativeUri("/wp-content/themes/Wuxia/template-parts/post/menu-query.php"), payload); + var post = await Config.Client.PostWithTriesAsync( + SystemUrl.MakeRelativeUri("/wp-content/themes/Wuxia/template-parts/post/menu-query.php"), payload); var toc = await post.Content.ReadFromJsonAsync<WuxiaWorldToc[]>(); - result.AddRange(toc.Select((a, i) => new UrlChapter(url.MakeRelativeUri(a.PostName), $"Глава {offset + i + 1}"))); + result.AddRange(toc.Select((a, i) => + new UrlChapter(url.MakeRelativeUri(a.PostName), $"Глава {offset + i + 1}"))); } return SliceToc(result, c => c.Title); } - private async Task<IEnumerable<Chapter>> FillChapters(HtmlDocument doc, Uri url) { + private async Task<IEnumerable<Chapter>> FillChapters(HtmlDocument doc, Uri url) + { var result = new List<Chapter>(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - foreach (var bookChapter in await GetToc(doc, url)) { + foreach (var bookChapter in await GetToc(doc, url)) + { var chapter = new Chapter(); Config.Logger.LogInformation($"Загружаю главу {bookChapter.Title.CoverQuotes()}"); - + var chapterDoc = await GetChapter(bookChapter.Url); chapter.Title = bookChapter.Title; chapter.Images = await GetImages(chapterDoc, url); @@ -83,27 +93,33 @@ private async Task<IEnumerable<Chapter>> FillChapters(HtmlDocument doc, Uri url) return result; } - private async Task<HtmlDocument> GetChapter(Uri url) { + private async Task<HtmlDocument> GetChapter(Uri url) + { var doc = await GetSafety(url); return doc.QuerySelector("div.js-full-content").RemoveNodes("> :not(p)").InnerHtml.AsHtmlDoc(); } - private Task<TempFile> GetCover(HtmlDocument doc, Uri uri) { + private Task<TempFile> GetCover(HtmlDocument doc, Uri uri) + { var imagePath = doc.QuerySelector("meta[property=og:image]")?.Attributes["content"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) + ? SaveImage(uri.MakeRelativeUri(imagePath)) + : Task.FromResult(default(TempFile)); } - - private async Task<HtmlDocument> GetSafety(Uri url) { + + private async Task<HtmlDocument> GetSafety(Uri url) + { var response = await Config.Client.GetWithTriesAsync(url); await Task.Delay(TimeSpan.FromSeconds(1)); - - while (response == default || response.StatusCode == HttpStatusCode.ServiceUnavailable) { + + while (response is null || response.StatusCode == HttpStatusCode.ServiceUnavailable) + { Config.Logger.LogInformation("Получен бан от системы. Жду..."); var errorTimeout = TimeSpan.FromSeconds(30); response = await Config.Client.GetWithTriesAsync(url, errorTimeout); await Task.Delay(errorTimeout); } - + return await response.Content.ReadAsStringAsync().ContinueWith(t => t.Result.AsHtmlDoc()); } -} \ No newline at end of file +} diff --git a/Core/Logic/Getters/YounettranslateGetter.cs b/Core/Logic/Getters/YounettranslateGetter.cs index d7d640ae..25891bac 100644 --- a/Core/Logic/Getters/YounettranslateGetter.cs +++ b/Core/Logic/Getters/YounettranslateGetter.cs @@ -12,18 +12,22 @@ namespace Core.Logic.Getters; -public class YounettranslateGetter(BookGetterConfig config) : GetterBase(config) { +public class YounettranslateGetter(BookGetterConfig config) : GetterBase(config) +{ protected override Uri SystemUrl => new("https://younettranslate.com/"); - protected override string GetId(Uri url) { + protected override string GetId(Uri url) + { return url.GetSegment(2); } - public override async Task<Book> Get(Uri url) { + public override async Task<Book> Get(Uri url) + { url = SystemUrl.MakeRelativeUri($"/projects/{GetId(url)}"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - var book = new Book(url) { + var book = new Book(url) + { Cover = await GetCover(url), Chapters = await FillChapters(url), Title = GetTitle(doc), @@ -34,36 +38,40 @@ public override async Task<Book> Get(Uri url) { return book; } - private Author GetAuthor(HtmlDocument doc) { - foreach (var div in doc.QuerySelectorAll("div.col-md-3")) { + private Author GetAuthor(HtmlDocument doc) + { + foreach (var div in doc.QuerySelectorAll("div.col-md-3")) + { var label = div.QuerySelector("p.small-label"); - if (label != default && label.GetText().StartsWith("Автор")) { + if (label is not null && label.GetText().StartsWith("Автор")) return new Author(label.GetTextBySelector("+ p")); - } } return new Author("Younettranslate"); } - private static string GetTitle(HtmlDocument doc) { + private static string GetTitle(HtmlDocument doc) + { return doc.GetTextBySelector("h1"); } - private async Task<IEnumerable<Chapter>> FillChapters(Uri url) { + private async Task<IEnumerable<Chapter>> FillChapters(Uri url) + { var result = new List<Chapter>(); - if (Config.Options.NoChapters) { - return result; - } + if (Config.Options.NoChapters) return result; - foreach (var urlChapter in await GetToc(url)) { + foreach (var urlChapter in await GetToc(url)) + { Config.Logger.LogInformation($"Загружаю главу {urlChapter.Title.CoverQuotes()}"); - var chapter = new Chapter { + var chapter = new Chapter + { Title = urlChapter.Title }; var chapterDoc = await GetChapter(urlChapter); - if (chapterDoc != default) { + if (chapterDoc is not null) + { chapter.Images = await GetImages(chapterDoc, SystemUrl); chapter.Content = chapterDoc.DocumentNode.InnerHtml; } @@ -74,31 +82,35 @@ private async Task<IEnumerable<Chapter>> FillChapters(Uri url) { return result; } - private async Task<IEnumerable<UrlChapter>> GetToc(Uri url) { + private async Task<IEnumerable<UrlChapter>> GetToc(Uri url) + { var result = new List<UrlChapter>(); - for (var i = 1;; i++) { + for (var i = 1;; i++) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(url.AppendQueryParameter("page", i)); - var links = doc.QuerySelectorAll("table.catposts-table td.hidden-sm a[href]").Select(a => new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.GetText())).ToList(); - if (links.Count == 0) { - break; - } - + var links = doc.QuerySelectorAll("table.catposts-table td.hidden-sm a[href]").Select(a => + new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.GetText())).ToList(); + if (links.Count == 0) break; + result.AddRange(links); } return SliceToc(result, c => c.Title); } - private async Task<HtmlDocument> GetChapter(UrlChapter urlChapter) { + private async Task<HtmlDocument> GetChapter(UrlChapter urlChapter) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync(urlChapter.Url); - return doc.QuerySelector("div.postdata-content").RemoveNodes(n => n.InnerText.Contains("ЗАПРЕЩЕНО ПУБЛИКОВАТЬ")).InnerHtml.AsHtmlDoc(); + return doc.QuerySelector("div.postdata-content").RemoveNodes(n => n.InnerText.Contains("ЗАПРЕЩЕНО ПУБЛИКОВАТЬ")) + .InnerHtml.AsHtmlDoc(); } - private async Task<TempFile> GetCover(Uri bookUri) { + private async Task<TempFile> GetCover(Uri bookUri) + { var doc = await Config.Client.GetHtmlDocWithTriesAsync("https://younettranslate.com/projects/".AsUri()); - + var imagePath = doc.QuerySelector($"div.category-item a[href='{bookUri}'] img")?.Attributes["src"]?.Value; return !string.IsNullOrWhiteSpace(imagePath) ? await SaveImage(bookUri.MakeRelativeUri(imagePath)) : default; } -} \ No newline at end of file +} diff --git a/Core/Misc/AdditionalTypeEnum.cs b/Core/Misc/AdditionalTypeEnum.cs index eb39e763..0b0aeb2d 100644 --- a/Core/Misc/AdditionalTypeEnum.cs +++ b/Core/Misc/AdditionalTypeEnum.cs @@ -3,8 +3,9 @@ namespace Core.Misc; [Flags] -public enum AdditionalTypeEnum { +public enum AdditionalTypeEnum +{ Books = 1, Audio = 2, Images = 4 -} \ No newline at end of file +} diff --git a/Core/Misc/BuilderProvider.cs b/Core/Misc/BuilderProvider.cs index 1ac97517..6d5bc51f 100644 --- a/Core/Misc/BuilderProvider.cs +++ b/Core/Misc/BuilderProvider.cs @@ -5,9 +5,12 @@ namespace Core.Misc; -public static class BuilderProvider { - public static BuilderBase Get(string format, Options options, ILogger logger) { - return format.Trim().ToLower() switch { +public static class BuilderProvider +{ + public static BuilderBase Get(string format, Options options, ILogger logger) + { + return format.Trim().ToLowerInvariant() switch + { "fb2" => new Fb2Builder(options, logger), "epub" => new EpubBuilder(options, logger), "json" => new JsonBuilder(options, logger), @@ -17,4 +20,4 @@ public static BuilderBase Get(string format, Options options, ILogger logger) { _ => throw new ArgumentException("Неизвестный формат", nameof(format)) }; } -} \ No newline at end of file +} diff --git a/Core/Misc/GetterProvider.cs b/Core/Misc/GetterProvider.cs index 5bd6610c..f3bc546e 100644 --- a/Core/Misc/GetterProvider.cs +++ b/Core/Misc/GetterProvider.cs @@ -6,12 +6,14 @@ namespace Core.Misc; -public static class GetterProvider { - public static GetterBase Get(BookGetterConfig config, Uri url) { +public static class GetterProvider +{ + public static GetterBase Get(BookGetterConfig config, Uri url) + { return Assembly.GetAssembly(typeof(GetterBase))!.GetTypes() .Where(myType => myType.IsClass && !myType.IsAbstract && myType.IsSubclassOf(typeof(GetterBase))) - .Select(type => (GetterBase) Activator.CreateInstance(type, config)) + .Select(type => (GetterBase)Activator.CreateInstance(type, config)) .FirstOrDefault(g => g!.IsSameUrl(url)) ?? throw new ArgumentException($"Сайт {url.Host} не поддерживается"); } -} \ No newline at end of file +} diff --git a/Core/Misc/TempFolder/TempFolder.cs b/Core/Misc/TempFolder/TempFolder.cs index 788515fd..fc4a5a41 100644 --- a/Core/Misc/TempFolder/TempFolder.cs +++ b/Core/Misc/TempFolder/TempFolder.cs @@ -2,25 +2,26 @@ using System.IO; using System.Linq; -namespace Core.Misc.TempFolder; +namespace Core.Misc.TempFolder; -public class TempFolder : IDisposable { +public class TempFolder : IDisposable +{ /// <summary> - /// Temporary's folder path + /// Temporary's folder path /// </summary> public readonly string Path; /// <summary> - /// Create new instance of temporary folder that will be deleted after dispose + /// Create new instance of temporary folder that will be deleted after dispose /// </summary> /// <param name="path">Temporary folder</param> - public TempFolder(string path) { + public TempFolder(string path) + { Path = path; } - - public void Dispose() { - if (Directory.Exists(Path) && !Directory.EnumerateFileSystemEntries(Path).Any()) { - Directory.Delete(Path, true); - } + + public void Dispose() + { + if (Directory.Exists(Path) && !Directory.EnumerateFileSystemEntries(Path).Any()) Directory.Delete(Path, true); } -} \ No newline at end of file +} diff --git a/Core/Misc/TempFolder/TempFolderFactory.cs b/Core/Misc/TempFolder/TempFolderFactory.cs index b1e85fc6..1ce8fae6 100644 --- a/Core/Misc/TempFolder/TempFolderFactory.cs +++ b/Core/Misc/TempFolder/TempFolderFactory.cs @@ -1,20 +1,21 @@ #nullable enable using System.IO; -namespace Core.Misc.TempFolder; +namespace Core.Misc.TempFolder; -public class TempFolderFactory { +public class TempFolderFactory +{ /// <summary> - /// Create temporary folder by path + /// Create temporary folder by path /// </summary> /// <param name="path">Temporary folder path</param> /// <returns>New instance of temporary folder</returns> - public static TempFolder Create(string? path) { - path ??= Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), $"{Path.GetRandomFileName()}_e2e_temp")).FullName; - if (!Directory.Exists(path)) { - Directory.CreateDirectory(path); - } - + public static TempFolder Create(string? path) + { + path ??= Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), $"{Path.GetRandomFileName()}_e2e_temp")) + .FullName; + if (!Directory.Exists(path)) Directory.CreateDirectory(path); + return new TempFolder(path); } -} \ No newline at end of file +} diff --git a/Core/Types/AuthorToday/AuthorTodayAuthResponse.cs b/Core/Types/AuthorToday/AuthorTodayAuthResponse.cs index 72d4398e..5be99357 100644 --- a/Core/Types/AuthorToday/AuthorTodayAuthResponse.cs +++ b/Core/Types/AuthorToday/AuthorTodayAuthResponse.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.AuthorToday; +namespace Core.Types.AuthorToday; -public class AuthorTodayAuthResponse { - [JsonPropertyName("message")] - public string Message { get; set; } - - [JsonPropertyName("token")] - public string Token { get; set; } -} \ No newline at end of file +public class AuthorTodayAuthResponse +{ + [JsonPropertyName("message")] public string Message { get; set; } + + [JsonPropertyName("token")] public string Token { get; set; } +} diff --git a/Core/Types/AuthorToday/AuthorTodayBookDetails.cs b/Core/Types/AuthorToday/AuthorTodayBookDetails.cs index f9dfdeec..03b27897 100644 --- a/Core/Types/AuthorToday/AuthorTodayBookDetails.cs +++ b/Core/Types/AuthorToday/AuthorTodayBookDetails.cs @@ -1,44 +1,32 @@ using System.Text.Json.Serialization; -namespace Core.Types.AuthorToday; - -public class AuthorTodayBookDetails { - [JsonPropertyName("annotation")] - public string Annotation { get; set; } - - [JsonPropertyName("seriesId")] - public long? SeriesId { get; set; } - - [JsonPropertyName("seriesWorkNumber")] - public long? SeriesWorkNumber { get; set; } - - [JsonPropertyName("seriesTitle")] - public string SeriesTitle { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("authorFIO")] - public string AuthorFio { get; set; } - - [JsonPropertyName("coAuthorFIO")] - public string CoAuthorFio { get; set; } - - [JsonPropertyName("coverUrl")] - public string CoverUrl { get; set; } - - [JsonPropertyName("chapters")] - public AuthorTodayChapter[] Chapters { get; set; } - - [JsonPropertyName("id")] - public long Id { get; set; } - - [JsonPropertyName("authorUserName")] - public string AuthorUserName { get; set; } - - [JsonPropertyName("CoAuthorUserName")] - public string CoAuthorUserName { get; set; } - - [JsonPropertyName("galleryImages")] - public AuthorTodayImage[] GalleryImages { get; set; } -} \ No newline at end of file +namespace Core.Types.AuthorToday; + +public class AuthorTodayBookDetails +{ + [JsonPropertyName("annotation")] public string Annotation { get; set; } + + [JsonPropertyName("seriesId")] public long? SeriesId { get; set; } + + [JsonPropertyName("seriesWorkNumber")] public long? SeriesWorkNumber { get; set; } + + [JsonPropertyName("seriesTitle")] public string SeriesTitle { get; set; } + + [JsonPropertyName("title")] public string Title { get; set; } + + [JsonPropertyName("authorFIO")] public string AuthorFio { get; set; } + + [JsonPropertyName("coAuthorFIO")] public string CoAuthorFio { get; set; } + + [JsonPropertyName("coverUrl")] public string CoverUrl { get; set; } + + [JsonPropertyName("chapters")] public AuthorTodayChapter[] Chapters { get; set; } + + [JsonPropertyName("id")] public long Id { get; set; } + + [JsonPropertyName("authorUserName")] public string AuthorUserName { get; set; } + + [JsonPropertyName("CoAuthorUserName")] public string CoAuthorUserName { get; set; } + + [JsonPropertyName("galleryImages")] public AuthorTodayImage[] GalleryImages { get; set; } +} diff --git a/Core/Types/AuthorToday/AuthorTodayChapter.cs b/Core/Types/AuthorToday/AuthorTodayChapter.cs index f7e69637..4dbfb669 100644 --- a/Core/Types/AuthorToday/AuthorTodayChapter.cs +++ b/Core/Types/AuthorToday/AuthorTodayChapter.cs @@ -5,54 +5,48 @@ using System.Text; using System.Text.Json.Serialization; -namespace Core.Types.AuthorToday; - -public class AuthorTodayChapter { - [JsonPropertyName("id")] - public long Id { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("sortOrder")] - public long SortOrder { get; set; } - - [JsonPropertyName("text")] - public string Text { get; set; } - - [JsonPropertyName("key")] - public string Key { get; set; } - - [JsonPropertyName("IsSuccessful")] - public bool IsSuccessful { get; set; } - - [JsonPropertyName("IsDraft")] - public bool IsDraft { get; set; } - - [JsonPropertyName("isAvailable")] - public bool isAvailable { get; set; } - - [JsonPropertyName("code")] - public string Code { get; set; } - - public string Decode(string userId, string cert) { - var secret = $"{string.Concat(Key.Reverse())}:{(string.IsNullOrWhiteSpace(userId) ? "Guest" : userId)}:{"FjPg]{2+$8JRvv~("}:{cert}"; +namespace Core.Types.AuthorToday; + +public class AuthorTodayChapter +{ + [JsonPropertyName("id")] public long Id { get; set; } + + [JsonPropertyName("title")] public string Title { get; set; } + + [JsonPropertyName("sortOrder")] public long SortOrder { get; set; } + + [JsonPropertyName("text")] public string Text { get; set; } + + [JsonPropertyName("key")] public string Key { get; set; } + + [JsonPropertyName("IsSuccessful")] public bool IsSuccessful { get; set; } + + [JsonPropertyName("IsDraft")] public bool IsDraft { get; set; } + + [JsonPropertyName("isAvailable")] public bool isAvailable { get; set; } + + [JsonPropertyName("code")] public string Code { get; set; } + + public string Decode(string userId, string cert) + { + var secret = + $"{string.Concat(Key.Reverse())}:{(string.IsNullOrWhiteSpace(userId) ? "Guest" : userId)}:{"FjPg]{2+$8JRvv~("}:{cert}"; var hashSecret = Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(secret))); - + using var aes = Aes.Create(); const int IV_SHIFT = 16; var aesKey = Encoding.UTF8.GetBytes(hashSecret)[..IV_SHIFT]; - - aes.Key = aesKey; + + aes.Key = aesKey; aes.IV = aesKey; using var ms = new MemoryStream(Convert.FromBase64String(Text)); using var cs = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Read); - + var output = new MemoryStream(); cs.CopyTo(output); return Encoding.UTF8.GetString(output.ToArray()); } -} \ No newline at end of file +} diff --git a/Core/Types/AuthorToday/AuthorTodayImage.cs b/Core/Types/AuthorToday/AuthorTodayImage.cs index e9c16f4b..a2c27ece 100644 --- a/Core/Types/AuthorToday/AuthorTodayImage.cs +++ b/Core/Types/AuthorToday/AuthorTodayImage.cs @@ -2,7 +2,7 @@ namespace Core.Types.AuthorToday; -public class AuthorTodayImage { - [JsonPropertyName("url")] - public string Url { get; set; } -} \ No newline at end of file +public class AuthorTodayImage +{ + [JsonPropertyName("url")] public string Url { get; set; } +} diff --git a/Core/Types/AuthorToday/AuthorTodayUser.cs b/Core/Types/AuthorToday/AuthorTodayUser.cs index 9d062ee8..0c88c898 100644 --- a/Core/Types/AuthorToday/AuthorTodayUser.cs +++ b/Core/Types/AuthorToday/AuthorTodayUser.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.AuthorToday; +namespace Core.Types.AuthorToday; -public class AuthorTodayUser { - [JsonPropertyName("id")] - public long Id { get; set; } -} \ No newline at end of file +public class AuthorTodayUser +{ + [JsonPropertyName("id")] public long Id { get; set; } +} diff --git a/Core/Types/Book/AdditionalFileCollection.cs b/Core/Types/Book/AdditionalFileCollection.cs index 5c2b64c8..153a26dc 100644 --- a/Core/Types/Book/AdditionalFileCollection.cs +++ b/Core/Types/Book/AdditionalFileCollection.cs @@ -6,31 +6,33 @@ namespace Core.Types.Book; -public class AdditionalFileCollection : IDisposable { +public class AdditionalFileCollection : IDisposable +{ public Dictionary<AdditionalTypeEnum, List<TempFile>> Collection { get; set; } = new(); - public void Add(AdditionalTypeEnum type, TempFile file) { - if (!Collection.TryGetValue(type, out var files)) { - files = new(); + public void Dispose() + { + foreach (var file in Collection.SelectMany(pair => pair.Value)) file.Dispose(); + } + + public void Add(AdditionalTypeEnum type, TempFile file) + { + if (!Collection.TryGetValue(type, out var files)) + { + files = new List<TempFile>(); Collection[type] = files; } - + files.Add(file); } - - public void Add(AdditionalTypeEnum type, IEnumerable<TempFile> files) { - foreach (var file in files) { - Add(type, file); - } - } - public List<TempFile> Get(AdditionalTypeEnum type) { - return Collection.TryGetValue(type, out var result) ? result : []; + public void Add(AdditionalTypeEnum type, IEnumerable<TempFile> files) + { + foreach (var file in files) Add(type, file); } - public void Dispose() { - foreach (var file in Collection.SelectMany(pair => pair.Value)) { - file.Dispose(); - } + public List<TempFile> Get(AdditionalTypeEnum type) + { + return Collection.TryGetValue(type, out var result) ? result : []; } -} \ No newline at end of file +} diff --git a/Core/Types/Book/Author.cs b/Core/Types/Book/Author.cs index 5f2bf87f..b81d6b5b 100644 --- a/Core/Types/Book/Author.cs +++ b/Core/Types/Book/Author.cs @@ -1,20 +1,22 @@ using System; -namespace Core.Types.Book; +namespace Core.Types.Book; + +public class Author +{ + public Author(string name, Uri url = null) + { + Name = name; + Url = url; + } -public class Author { /// <summary> - /// Имя автора + /// Имя автора /// </summary> public string Name { get; set; } /// <summary> - /// Url профиля автора + /// Url профиля автора /// </summary> public Uri Url { get; set; } - - public Author(string name, Uri url = null) { - Name = name; - Url = url; - } -} \ No newline at end of file +} diff --git a/Core/Types/Book/Book.cs b/Core/Types/Book/Book.cs index aa4af7af..c0982c0f 100644 --- a/Core/Types/Book/Book.cs +++ b/Core/Types/Book/Book.cs @@ -1,61 +1,67 @@ using System; -using System.Linq; using System.Collections.Generic; +using System.Linq; using Core.Types.Common; -namespace Core.Types.Book; +namespace Core.Types.Book; + +public class Book : IDisposable +{ + public Book(Uri url) + { + Url = url; + } -public class Book : IDisposable { /// <summary> - /// Название книги + /// Название книги /// </summary> public string Title { get; set; } - + /// <summary> - /// Автор книги + /// Автор книги /// </summary> public Author Author { get; set; } /// <summary> - /// Автор книги + /// Автор книги /// </summary> public IEnumerable<Author> CoAuthors { get; set; } = new List<Author>(); /// <summary> - /// Описание книги + /// Описание книги /// </summary> public string Annotation { get; set; } - + /// <summary> - /// Обложка + /// Обложка /// </summary> public TempFile Cover { get; set; } - + /// <summary> - /// Серия + /// Серия /// </summary> public Seria Seria { get; set; } /// <summary> - /// Части + /// Части /// </summary> public IEnumerable<Chapter> Chapters { get; set; } = new List<Chapter>(); -/// Тома + /// Тома public IEnumerable<string> Volumes => Chapters.Select(c => c.VolumeNumber).Distinct(); /// <summary> - /// Url расположения книги + /// Url расположения книги /// </summary> public Uri Url { get; set; } /// <summary> - /// Дополнительные файлы + /// Дополнительные файлы /// </summary> public AdditionalFileCollection AdditionalFiles { get; set; } = new(); /// <summary> - /// Язык книги + /// Язык книги /// </summary> public string Lang { get; set; } = "ru"; @@ -64,12 +70,9 @@ public class Book : IDisposable { public bool SupportSplitting => SupportSplitVolumes || SupportSplitChapters; - public Book(Uri url) { - Url = url; - } - - public void Dispose() { + public void Dispose() + { Cover?.Dispose(); AdditionalFiles?.Dispose(); } -} \ No newline at end of file +} diff --git a/Core/Types/Book/Chapter.cs b/Core/Types/Book/Chapter.cs index b8557b20..aa941d1a 100644 --- a/Core/Types/Book/Chapter.cs +++ b/Core/Types/Book/Chapter.cs @@ -1,36 +1,37 @@ using System.Collections.Generic; using Core.Types.Common; -namespace Core.Types.Book; +namespace Core.Types.Book; -public class Chapter { +public class Chapter +{ /// <summary> - /// Название части + /// Название части /// </summary> public string Title { get; set; } - + /// <summary> - /// Контент части + /// Контент части /// </summary> public string Content { get; set; } /// <summary> - /// Номер тома + /// Номер тома /// </summary> public string VolumeNumber { get; set; } /// <summary> - /// Номер главы + /// Номер главы /// </summary> public string ChapterNumber { get; set; } /// <summary> - /// Изображения из части + /// Изображения из части /// </summary> public IEnumerable<TempFile> Images { get; set; } = new List<TempFile>(); /// <summary> - /// Валидна ли часть + /// Валидна ли часть /// </summary> public bool IsValid => !string.IsNullOrWhiteSpace(Content); -} \ No newline at end of file +} diff --git a/Core/Types/Book/Seria.cs b/Core/Types/Book/Seria.cs index dbb23f30..7d7e2154 100644 --- a/Core/Types/Book/Seria.cs +++ b/Core/Types/Book/Seria.cs @@ -1,20 +1,21 @@ using System; -namespace Core.Types.Book; +namespace Core.Types.Book; -public class Seria { +public class Seria +{ /// <summary> - /// Название серии + /// Название серии /// </summary> public string Name { get; set; } - + /// <summary> - /// Номер в серии + /// Номер в серии /// </summary> public string Number { get; set; } - + /// <summary> - /// Ссылка на книги серии + /// Ссылка на книги серии /// </summary> public Uri Url { get; set; } -} \ No newline at end of file +} diff --git a/Core/Types/BookYandex/BooksYandexAudioBitrate.cs b/Core/Types/BookYandex/BooksYandexAudioBitrate.cs index d161f9d7..0dd88efc 100644 --- a/Core/Types/BookYandex/BooksYandexAudioBitrate.cs +++ b/Core/Types/BookYandex/BooksYandexAudioBitrate.cs @@ -2,7 +2,7 @@ namespace Core.Types.BookYandex; -public class BooksYandexAudioBitrate { - [JsonPropertyName("url")] - public string Url { get; set; } -} \ No newline at end of file +public class BooksYandexAudioBitrate +{ + [JsonPropertyName("url")] public string Url { get; set; } +} diff --git a/Core/Types/BookYandex/BooksYandexAudioOffline.cs b/Core/Types/BookYandex/BooksYandexAudioOffline.cs index ffaed28d..1d08394b 100644 --- a/Core/Types/BookYandex/BooksYandexAudioOffline.cs +++ b/Core/Types/BookYandex/BooksYandexAudioOffline.cs @@ -2,10 +2,9 @@ namespace Core.Types.BookYandex; -public class BooksYandexAudioOffline { - [JsonPropertyName("max_bit_rate")] - public BooksYandexAudioBitrate Max { get; set; } - - [JsonPropertyName("min_bit_rate")] - public BooksYandexAudioBitrate Min { get; set; } -} \ No newline at end of file +public class BooksYandexAudioOffline +{ + [JsonPropertyName("max_bit_rate")] public BooksYandexAudioBitrate Max { get; set; } + + [JsonPropertyName("min_bit_rate")] public BooksYandexAudioBitrate Min { get; set; } +} diff --git a/Core/Types/BookYandex/BooksYandexAudioTrack.cs b/Core/Types/BookYandex/BooksYandexAudioTrack.cs index 83bcf1cd..9c3bf6de 100644 --- a/Core/Types/BookYandex/BooksYandexAudioTrack.cs +++ b/Core/Types/BookYandex/BooksYandexAudioTrack.cs @@ -2,7 +2,7 @@ namespace Core.Types.BookYandex; -public class BooksYandexAudioTrack { - [JsonPropertyName("offline")] - public BooksYandexAudioOffline Offline { get; set; } -} \ No newline at end of file +public class BooksYandexAudioTrack +{ + [JsonPropertyName("offline")] public BooksYandexAudioOffline Offline { get; set; } +} diff --git a/Core/Types/BookYandex/BooksYandexAuthor.cs b/Core/Types/BookYandex/BooksYandexAuthor.cs index bdd955b1..fdb745d9 100644 --- a/Core/Types/BookYandex/BooksYandexAuthor.cs +++ b/Core/Types/BookYandex/BooksYandexAuthor.cs @@ -2,10 +2,9 @@ namespace Core.Types.BookYandex; -public class BooksYandexAuthor { - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("uuid")] - public string Uuid { get; set; } -} \ No newline at end of file +public class BooksYandexAuthor +{ + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("uuid")] public string Uuid { get; set; } +} diff --git a/Core/Types/BookYandex/BooksYandexBook.cs b/Core/Types/BookYandex/BooksYandexBook.cs index 3621b97b..e48d2617 100644 --- a/Core/Types/BookYandex/BooksYandexBook.cs +++ b/Core/Types/BookYandex/BooksYandexBook.cs @@ -3,48 +3,45 @@ namespace Core.Types.BookYandex; -public abstract class BookmateBookBase { - [JsonPropertyName("uuid")] - public string UUID { get; set; } - - [JsonPropertyName("annotation")] - public string Annotation { get; set; } - - [JsonPropertyName("cover")] - public BooksYandexCover Cover { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("language")] - public string Language { get; set; } - +public abstract class BookmateBookBase +{ + [JsonPropertyName("uuid")] public string UUID { get; set; } + + [JsonPropertyName("annotation")] public string Annotation { get; set; } + + [JsonPropertyName("cover")] public BooksYandexCover Cover { get; set; } + + [JsonPropertyName("title")] public string Title { get; set; } + + [JsonPropertyName("language")] public string Language { get; set; } + [JsonPropertyName("linked_book_uuids")] public string[] LinkedBooks { get; set; } - + [JsonPropertyName("linked_audiobook_uuids")] public string[] LinkedAudio { get; set; } - - [JsonPropertyName("source_type")] - public string SourceType { get; set; } + + [JsonPropertyName("source_type")] public string SourceType { get; set; } public abstract BooksYandexAuthor GetAuthor(); } -public class BooksYandexBook : BookmateBookBase { - [JsonPropertyName("authors_objects")] - public BooksYandexAuthor[] AuthorsObjects { get; set; } +public class BooksYandexBook : BookmateBookBase +{ + [JsonPropertyName("authors_objects")] public BooksYandexAuthor[] AuthorsObjects { get; set; } - public override BooksYandexAuthor GetAuthor() { + public override BooksYandexAuthor GetAuthor() + { return AuthorsObjects?.FirstOrDefault(); } } -public class BookmateComic : BookmateBookBase { - [JsonPropertyName("authors")] - public BooksYandexAuthor[] Authors { get; set; } +public class BookmateComic : BookmateBookBase +{ + [JsonPropertyName("authors")] public BooksYandexAuthor[] Authors { get; set; } - public override BooksYandexAuthor GetAuthor() { + public override BooksYandexAuthor GetAuthor() + { return Authors?.FirstOrDefault(); } -} \ No newline at end of file +} diff --git a/Core/Types/BookYandex/BooksYandexComicContent.cs b/Core/Types/BookYandex/BooksYandexComicContent.cs index 324342a5..cfb5126d 100644 --- a/Core/Types/BookYandex/BooksYandexComicContent.cs +++ b/Core/Types/BookYandex/BooksYandexComicContent.cs @@ -2,7 +2,7 @@ namespace Core.Types.BookYandex; -public class BooksYandexComicContent { - [JsonPropertyName("uris")] - public BooksYandexUri Uri { get; set; } -} \ No newline at end of file +public class BooksYandexComicContent +{ + [JsonPropertyName("uris")] public BooksYandexUri Uri { get; set; } +} diff --git a/Core/Types/BookYandex/BooksYandexComicMetadata.cs b/Core/Types/BookYandex/BooksYandexComicMetadata.cs index fd4c9f8d..ca5ee961 100644 --- a/Core/Types/BookYandex/BooksYandexComicMetadata.cs +++ b/Core/Types/BookYandex/BooksYandexComicMetadata.cs @@ -2,7 +2,7 @@ namespace Core.Types.BookYandex; -public class BooksYandexComicMetadata { - [JsonPropertyName("pages")] - public BooksYandexComicPage[] Pages { get; set; } -} \ No newline at end of file +public class BooksYandexComicMetadata +{ + [JsonPropertyName("pages")] public BooksYandexComicPage[] Pages { get; set; } +} diff --git a/Core/Types/BookYandex/BooksYandexComicPage.cs b/Core/Types/BookYandex/BooksYandexComicPage.cs index b31ff5a3..797c0c78 100644 --- a/Core/Types/BookYandex/BooksYandexComicPage.cs +++ b/Core/Types/BookYandex/BooksYandexComicPage.cs @@ -2,7 +2,7 @@ namespace Core.Types.BookYandex; -public class BooksYandexComicPage { - [JsonPropertyName("content")] - public BooksYandexComicContent Content { get; set; } -} \ No newline at end of file +public class BooksYandexComicPage +{ + [JsonPropertyName("content")] public BooksYandexComicContent Content { get; set; } +} diff --git a/Core/Types/BookYandex/BooksYandexCover.cs b/Core/Types/BookYandex/BooksYandexCover.cs index 35f928e0..5cf57455 100644 --- a/Core/Types/BookYandex/BooksYandexCover.cs +++ b/Core/Types/BookYandex/BooksYandexCover.cs @@ -2,10 +2,9 @@ namespace Core.Types.BookYandex; -public class BooksYandexCover { - [JsonPropertyName("large")] - public string Large { get; set; } - - [JsonPropertyName("small")] - public string Small { get; set; } -} \ No newline at end of file +public class BooksYandexCover +{ + [JsonPropertyName("large")] public string Large { get; set; } + + [JsonPropertyName("small")] public string Small { get; set; } +} diff --git a/Core/Types/BookYandex/BooksYandexEpisode.cs b/Core/Types/BookYandex/BooksYandexEpisode.cs index 4b9b5264..e56ff4d3 100644 --- a/Core/Types/BookYandex/BooksYandexEpisode.cs +++ b/Core/Types/BookYandex/BooksYandexEpisode.cs @@ -2,13 +2,11 @@ namespace Core.Types.BookYandex; -public class BooksYandexEpisode { - [JsonPropertyName("uuid")] - public string Uuid { get; set; } - - [JsonPropertyName("can_be_read")] - public bool CanBeRead { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } -} \ No newline at end of file +public class BooksYandexEpisode +{ + [JsonPropertyName("uuid")] public string Uuid { get; set; } + + [JsonPropertyName("can_be_read")] public bool CanBeRead { get; set; } + + [JsonPropertyName("title")] public string Title { get; set; } +} diff --git a/Core/Types/BookYandex/BooksYandexEpisodes.cs b/Core/Types/BookYandex/BooksYandexEpisodes.cs index d153a2d8..5457bbeb 100644 --- a/Core/Types/BookYandex/BooksYandexEpisodes.cs +++ b/Core/Types/BookYandex/BooksYandexEpisodes.cs @@ -2,7 +2,7 @@ namespace Core.Types.BookYandex; -public class BooksYandexEpisodes { - [JsonPropertyName("episodes")] - public BooksYandexEpisode[] Episodes { get; set; } -} \ No newline at end of file +public class BooksYandexEpisodes +{ + [JsonPropertyName("episodes")] public BooksYandexEpisode[] Episodes { get; set; } +} diff --git a/Core/Types/BookYandex/BooksYandexPlaylist.cs b/Core/Types/BookYandex/BooksYandexPlaylist.cs index 0057ffa1..b8c1416c 100644 --- a/Core/Types/BookYandex/BooksYandexPlaylist.cs +++ b/Core/Types/BookYandex/BooksYandexPlaylist.cs @@ -2,7 +2,7 @@ namespace Core.Types.BookYandex; -public class BooksYandexPlaylist { - [JsonPropertyName("tracks")] - public BooksYandexAudioTrack[] Tracks { get; set; } -} \ No newline at end of file +public class BooksYandexPlaylist +{ + [JsonPropertyName("tracks")] public BooksYandexAudioTrack[] Tracks { get; set; } +} diff --git a/Core/Types/BookYandex/BooksYandexResponse.cs b/Core/Types/BookYandex/BooksYandexResponse.cs index 8e9cc5d9..4e0f14b3 100644 --- a/Core/Types/BookYandex/BooksYandexResponse.cs +++ b/Core/Types/BookYandex/BooksYandexResponse.cs @@ -2,13 +2,11 @@ namespace Core.Types.BookYandex; -public class BooksYandexResponse { - [JsonPropertyName("book")] - public BooksYandexBook Book { get; set; } - - [JsonPropertyName("audiobook")] - public BooksYandexBook AudioBook { get; set; } - - [JsonPropertyName("comicbook")] - public BookmateComic Comicbook { get; set; } -} \ No newline at end of file +public class BooksYandexResponse +{ + [JsonPropertyName("book")] public BooksYandexBook Book { get; set; } + + [JsonPropertyName("audiobook")] public BooksYandexBook AudioBook { get; set; } + + [JsonPropertyName("comicbook")] public BookmateComic Comicbook { get; set; } +} diff --git a/Core/Types/BookYandex/BooksYandexUri.cs b/Core/Types/BookYandex/BooksYandexUri.cs index c8a9e1e0..5ca243d3 100644 --- a/Core/Types/BookYandex/BooksYandexUri.cs +++ b/Core/Types/BookYandex/BooksYandexUri.cs @@ -2,10 +2,9 @@ namespace Core.Types.BookYandex; -public class BooksYandexUri { - [JsonPropertyName("Image")] - public string Image { get; set; } - - [JsonPropertyName("path")] - public string Path { get; set; } -} \ No newline at end of file +public class BooksYandexUri +{ + [JsonPropertyName("Image")] public string Image { get; set; } + + [JsonPropertyName("path")] public string Path { get; set; } +} diff --git a/Core/Types/Bookriver/BookRiverApiResponse.cs b/Core/Types/Bookriver/BookRiverApiResponse.cs index 1b1e1274..7ab44b25 100644 --- a/Core/Types/Bookriver/BookRiverApiResponse.cs +++ b/Core/Types/Bookriver/BookRiverApiResponse.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Bookriver; +namespace Core.Types.Bookriver; -public class BookRiverApiResponse<T> { - [JsonPropertyName("data")] - public T Data { get; set; } -} \ No newline at end of file +public class BookRiverApiResponse<T> +{ + [JsonPropertyName("data")] public T Data { get; set; } +} diff --git a/Core/Types/Bookriver/BookRiverAuthResponse.cs b/Core/Types/Bookriver/BookRiverAuthResponse.cs index ef18db2c..3e0b02e7 100644 --- a/Core/Types/Bookriver/BookRiverAuthResponse.cs +++ b/Core/Types/Bookriver/BookRiverAuthResponse.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Bookriver; +namespace Core.Types.Bookriver; -public class BookRiverAuthResponse { - [JsonPropertyName("token")] - public string Token { get; set; } -} \ No newline at end of file +public class BookRiverAuthResponse +{ + [JsonPropertyName("token")] public string Token { get; set; } +} diff --git a/Core/Types/Bookriver/BookRiverBook.cs b/Core/Types/Bookriver/BookRiverBook.cs index 9ec931f5..2b7af786 100644 --- a/Core/Types/Bookriver/BookRiverBook.cs +++ b/Core/Types/Bookriver/BookRiverBook.cs @@ -1,8 +1,9 @@ using System.Text.Json.Serialization; -namespace Core.Types.Bookriver; +namespace Core.Types.Bookriver; -public class BookRiverBook { +public class BookRiverBook +{ [JsonPropertyName("currentGraphqlBook")] public BookRiverCurrentBook Book { get; set; } -} \ No newline at end of file +} diff --git a/Core/Types/Bookriver/BookRiverChapter.cs b/Core/Types/Bookriver/BookRiverChapter.cs index 6beb9ab4..735617ff 100644 --- a/Core/Types/Bookriver/BookRiverChapter.cs +++ b/Core/Types/Bookriver/BookRiverChapter.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.Bookriver; +namespace Core.Types.Bookriver; -public class BookRiverChapter { - [JsonPropertyName("id")] - public long Id { get; set; } - - [JsonPropertyName("name")] - public string Name { get; set; } -} \ No newline at end of file +public class BookRiverChapter +{ + [JsonPropertyName("id")] public long Id { get; set; } + + [JsonPropertyName("name")] public string Name { get; set; } +} diff --git a/Core/Types/Bookriver/BookRiverChapterContent.cs b/Core/Types/Bookriver/BookRiverChapterContent.cs index 4d08a0fc..350aae05 100644 --- a/Core/Types/Bookriver/BookRiverChapterContent.cs +++ b/Core/Types/Bookriver/BookRiverChapterContent.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Bookriver; +namespace Core.Types.Bookriver; -public class BookRiverChapterContent { - [JsonPropertyName("content")] - public string Content { get; set; } -} \ No newline at end of file +public class BookRiverChapterContent +{ + [JsonPropertyName("content")] public string Content { get; set; } +} diff --git a/Core/Types/Bookriver/BookRiverCurrentBook.cs b/Core/Types/Bookriver/BookRiverCurrentBook.cs index a648bf81..5c5d27ba 100644 --- a/Core/Types/Bookriver/BookRiverCurrentBook.cs +++ b/Core/Types/Bookriver/BookRiverCurrentBook.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Bookriver; +namespace Core.Types.Bookriver; -public class BookRiverCurrentBook { - [JsonPropertyName("id")] - public long Id { get; set; } -} \ No newline at end of file +public class BookRiverCurrentBook +{ + [JsonPropertyName("id")] public long Id { get; set; } +} diff --git a/Core/Types/Bookstab/BookstabApiResponse.cs b/Core/Types/Bookstab/BookstabApiResponse.cs index 33589e81..d6c77728 100644 --- a/Core/Types/Bookstab/BookstabApiResponse.cs +++ b/Core/Types/Bookstab/BookstabApiResponse.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.Bookstab; +namespace Core.Types.Bookstab; -public class BookstabApiResponse { - [JsonPropertyName("book")] - public BookstabBook Book { get; set; } - - [JsonPropertyName("chapter")] - public BookstabChapter Chapter { get; set; } -} \ No newline at end of file +public class BookstabApiResponse +{ + [JsonPropertyName("book")] public BookstabBook Book { get; set; } + + [JsonPropertyName("chapter")] public BookstabChapter Chapter { get; set; } +} diff --git a/Core/Types/Bookstab/BookstabBook.cs b/Core/Types/Bookstab/BookstabBook.cs index 79240ff4..d01cc902 100644 --- a/Core/Types/Bookstab/BookstabBook.cs +++ b/Core/Types/Bookstab/BookstabBook.cs @@ -1,20 +1,16 @@ using System.Text.Json.Serialization; -namespace Core.Types.Bookstab; +namespace Core.Types.Bookstab; -public class BookstabBook { - [JsonPropertyName("image")] - public string Image { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("excerpt")] - public string Excerpt { get; set; } +public class BookstabBook +{ + [JsonPropertyName("image")] public string Image { get; set; } - [JsonPropertyName("user")] - public BooksnabUser User { get; set; } - - [JsonPropertyName("chapters_show")] - public BookstabChapter[] ChaptersShow { get; set; } -} \ No newline at end of file + [JsonPropertyName("title")] public string Title { get; set; } + + [JsonPropertyName("excerpt")] public string Excerpt { get; set; } + + [JsonPropertyName("user")] public BooksnabUser User { get; set; } + + [JsonPropertyName("chapters_show")] public BookstabChapter[] ChaptersShow { get; set; } +} diff --git a/Core/Types/Bookstab/BookstabChapter.cs b/Core/Types/Bookstab/BookstabChapter.cs index 01e10f4a..3d455b5f 100644 --- a/Core/Types/Bookstab/BookstabChapter.cs +++ b/Core/Types/Bookstab/BookstabChapter.cs @@ -1,14 +1,12 @@ using System.Text.Json.Serialization; -namespace Core.Types.Bookstab; - -public class BookstabChapter { - [JsonPropertyName("id")] - public int Id { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("body")] - public string Body { get; set; } -} \ No newline at end of file +namespace Core.Types.Bookstab; + +public class BookstabChapter +{ + [JsonPropertyName("id")] public int Id { get; set; } + + [JsonPropertyName("title")] public string Title { get; set; } + + [JsonPropertyName("body")] public string Body { get; set; } +} diff --git a/Core/Types/Bookstab/BookstabUser.cs b/Core/Types/Bookstab/BookstabUser.cs index 6f38050e..ef62bd8c 100644 --- a/Core/Types/Bookstab/BookstabUser.cs +++ b/Core/Types/Bookstab/BookstabUser.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.Bookstab; +namespace Core.Types.Bookstab; -public class BooksnabUser { - [JsonPropertyName("pseudonym")] - public string Pseudonym { get; set; } - - [JsonPropertyName("name")] - public string Name { get; set; } -} \ No newline at end of file +public class BooksnabUser +{ + [JsonPropertyName("pseudonym")] public string Pseudonym { get; set; } + + [JsonPropertyName("name")] public string Name { get; set; } +} diff --git a/Core/Types/Bookstime/BookstimeAuthResponse.cs b/Core/Types/Bookstime/BookstimeAuthResponse.cs index 3366465e..305d02e6 100644 --- a/Core/Types/Bookstime/BookstimeAuthResponse.cs +++ b/Core/Types/Bookstime/BookstimeAuthResponse.cs @@ -1,8 +1,9 @@ using System.Text.Json.Serialization; -namespace Core.Types.Bookstime; +namespace Core.Types.Bookstime; -public class BookstimeAuthResponse { +public class BookstimeAuthResponse +{ [JsonPropertyName("X_OCTOBER_ERROR_MESSAGE")] public string XOctoberErrorMessage { get; set; } -} \ No newline at end of file +} diff --git a/Core/Types/Boosty/BoostyApiResponse.cs b/Core/Types/Boosty/BoostyApiResponse.cs index 410418a8..db5f6ba3 100644 --- a/Core/Types/Boosty/BoostyApiResponse.cs +++ b/Core/Types/Boosty/BoostyApiResponse.cs @@ -2,10 +2,9 @@ namespace Core.Types.Boosty; -public class BoostyApiResponse<T> { - [JsonPropertyName("data")] - public T Data { get; set; } - - [JsonPropertyName("extra")] - public BoostyExtra Extra { get; set; } -} \ No newline at end of file +public class BoostyApiResponse<T> +{ + [JsonPropertyName("data")] public T Data { get; set; } + + [JsonPropertyName("extra")] public BoostyExtra Extra { get; set; } +} diff --git a/Core/Types/Boosty/BoostyExtra.cs b/Core/Types/Boosty/BoostyExtra.cs index 478980cb..d33cee9c 100644 --- a/Core/Types/Boosty/BoostyExtra.cs +++ b/Core/Types/Boosty/BoostyExtra.cs @@ -2,10 +2,9 @@ namespace Core.Types.Boosty; -public class BoostyExtra { - [JsonPropertyName("isLast")] - public bool IsLast { get; set; } - - [JsonPropertyName("offset")] - public string Offset { get; set; } -} \ No newline at end of file +public class BoostyExtra +{ + [JsonPropertyName("isLast")] public bool IsLast { get; set; } + + [JsonPropertyName("offset")] public string Offset { get; set; } +} diff --git a/Core/Types/Boosty/BoostyPost.cs b/Core/Types/Boosty/BoostyPost.cs index b88317e1..df2b8518 100644 --- a/Core/Types/Boosty/BoostyPost.cs +++ b/Core/Types/Boosty/BoostyPost.cs @@ -3,13 +3,11 @@ namespace Core.Types.Boosty; -public class BoostyPost { - [JsonPropertyName("id")] - public Guid Id { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("hasAccess")] - public bool HasAccess { get; set; } -} \ No newline at end of file +public class BoostyPost +{ + [JsonPropertyName("id")] public Guid Id { get; set; } + + [JsonPropertyName("title")] public string Title { get; set; } + + [JsonPropertyName("hasAccess")] public bool HasAccess { get; set; } +} diff --git a/Core/Types/Common/IdChapter.cs b/Core/Types/Common/IdChapter.cs index 1d8e774e..82a703ac 100644 --- a/Core/Types/Common/IdChapter.cs +++ b/Core/Types/Common/IdChapter.cs @@ -1,3 +1,3 @@ -namespace Core.Types.Common; +namespace Core.Types.Common; -public record IdChapter(string Id, string Title); \ No newline at end of file +public record IdChapter(string Id, string Title); diff --git a/Core/Types/Common/TempFile.cs b/Core/Types/Common/TempFile.cs index b094991e..b8bd02f6 100644 --- a/Core/Types/Common/TempFile.cs +++ b/Core/Types/Common/TempFile.cs @@ -2,51 +2,61 @@ using System.IO; using System.Threading.Tasks; -namespace Core.Types.Common; +namespace Core.Types.Common; -public class TempFile : IDisposable { +public class TempFile : IDisposable +{ + public string Extension; public string Name; - public string Extension; - + private TempFile(Uri url, string directory, string name, string extension) + { + Directory = directory; + Url = url; + Name = name; + Extension = extension; + } + public Uri Url { get; set; } - + public string Directory { get; set; } - public byte[] Content => File.Exists(FilePath) ? File.ReadAllBytes(FilePath) : []; + public byte[] Content => File.Exists(FilePath) ? File.ReadAllBytes(FilePath) : []; public string FullName => $"{Name}{Extension}"; - + public string FilePath => Path.Combine(Directory, FullName); public long Size => Content.Length; - - private TempFile(Uri url, string directory, string name, string extension) { - Directory = directory; - Url = url; - Name = name; - Extension = extension; + + public void Dispose() + { + if (File.Exists(FilePath)) File.Delete(FilePath); } - - public static async Task<TempFile> Create(Uri url, string directory, string fullName, byte[] content) { - return await Create(url, directory, Path.GetFileNameWithoutExtension(fullName), Path.GetExtension(fullName), content); + + public static async Task<TempFile> Create(Uri url, string directory, string fullName, byte[] content) + { + return await Create(url, directory, Path.GetFileNameWithoutExtension(fullName), Path.GetExtension(fullName), + content); } - - public static async Task<TempFile> Create(Uri url, string directory, string fullName, Stream stream) { - return await Create(url, directory, Path.GetFileNameWithoutExtension(fullName), Path.GetExtension(fullName), stream); + + public static async Task<TempFile> Create(Uri url, string directory, string fullName, Stream stream) + { + return await Create(url, directory, Path.GetFileNameWithoutExtension(fullName), Path.GetExtension(fullName), + stream); } - public static async Task<TempFile> Create(Uri url, string directory, string name, string extension, byte[] content) { + public static async Task<TempFile> Create(Uri url, string directory, string name, string extension, byte[] content) + { var image = new TempFile(url, directory, name, extension); await File.WriteAllBytesAsync(image.FilePath, content); return image; } - - public static async Task<TempFile> Create(Uri url, string directory, string name, string extension, Stream stream) { - if (stream.Length == 0) { - return default; - } - + + public static async Task<TempFile> Create(Uri url, string directory, string name, string extension, Stream stream) + { + if (stream.Length == 0) return default; + var tempFile = new TempFile(url, directory, name, extension); await using var file = File.OpenWrite(tempFile.FilePath); @@ -55,13 +65,8 @@ public static async Task<TempFile> Create(Uri url, string directory, string name return tempFile; } - public Stream GetStream() { + public Stream GetStream() + { return File.OpenRead(FilePath); } - - public void Dispose() { - if (File.Exists(FilePath)) { - File.Delete(FilePath); - } - } -} \ No newline at end of file +} diff --git a/Core/Types/Common/UrlChapter.cs b/Core/Types/Common/UrlChapter.cs index 30577a03..8b0a4f25 100644 --- a/Core/Types/Common/UrlChapter.cs +++ b/Core/Types/Common/UrlChapter.cs @@ -1,5 +1,5 @@ using System; -namespace Core.Types.Common; +namespace Core.Types.Common; -public record UrlChapter(Uri Url, string Title); \ No newline at end of file +public record UrlChapter(Uri Url, string Title); diff --git a/Core/Types/DarkNovels/DarkNovelsAuthData.cs b/Core/Types/DarkNovels/DarkNovelsAuthData.cs index 7ebdca90..fb11e03a 100644 --- a/Core/Types/DarkNovels/DarkNovelsAuthData.cs +++ b/Core/Types/DarkNovels/DarkNovelsAuthData.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.DarkNovels; +namespace Core.Types.DarkNovels; -public class DarkNovelsAuthResponse { - [JsonPropertyName("token")] - public DarkNovelsAuthToken Token { get; set; } -} \ No newline at end of file +public class DarkNovelsAuthResponse +{ + [JsonPropertyName("token")] public DarkNovelsAuthToken Token { get; set; } +} diff --git a/Core/Types/DarkNovels/DarkNovelsAuthToken.cs b/Core/Types/DarkNovels/DarkNovelsAuthToken.cs index b573227d..f43ac4ca 100644 --- a/Core/Types/DarkNovels/DarkNovelsAuthToken.cs +++ b/Core/Types/DarkNovels/DarkNovelsAuthToken.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.DarkNovels; +namespace Core.Types.DarkNovels; -public class DarkNovelsAuthToken { - [JsonPropertyName("accessToken")] - public string AccessToken { get; set; } -} \ No newline at end of file +public class DarkNovelsAuthToken +{ + [JsonPropertyName("accessToken")] public string AccessToken { get; set; } +} diff --git a/Core/Types/DarkNovels/DarkNovelsChapter.cs b/Core/Types/DarkNovels/DarkNovelsChapter.cs index abf2df1e..ebd9af05 100644 --- a/Core/Types/DarkNovels/DarkNovelsChapter.cs +++ b/Core/Types/DarkNovels/DarkNovelsChapter.cs @@ -1,14 +1,12 @@ using System.Text.Json.Serialization; -namespace Core.Types.DarkNovels; - -public class DarkNovelsChapter { - [JsonPropertyName("id")] - public int Id { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("payed")] - public int Payed { get; set; } -} \ No newline at end of file +namespace Core.Types.DarkNovels; + +public class DarkNovelsChapter +{ + [JsonPropertyName("id")] public int Id { get; set; } + + [JsonPropertyName("title")] public string Title { get; set; } + + [JsonPropertyName("payed")] public int Payed { get; set; } +} diff --git a/Core/Types/DarkNovels/DarkNovelsData.cs b/Core/Types/DarkNovels/DarkNovelsData.cs index 0e118a65..dc2e59eb 100644 --- a/Core/Types/DarkNovels/DarkNovelsData.cs +++ b/Core/Types/DarkNovels/DarkNovelsData.cs @@ -1,14 +1,12 @@ using System.Text.Json.Serialization; -namespace Core.Types.DarkNovels; - -public class DarkNovelsData<T> { - [JsonPropertyName("status")] - public string Status { get; set; } - - [JsonPropertyName("message")] - public string Message { get; set; } - - [JsonPropertyName("data")] - public T Data { get; set; } -} \ No newline at end of file +namespace Core.Types.DarkNovels; + +public class DarkNovelsData<T> +{ + [JsonPropertyName("status")] public string Status { get; set; } + + [JsonPropertyName("message")] public string Message { get; set; } + + [JsonPropertyName("data")] public T Data { get; set; } +} diff --git a/Core/Types/Dreame/DreameApiResponse.cs b/Core/Types/Dreame/DreameApiResponse.cs index e2d5803b..92a787b1 100644 --- a/Core/Types/Dreame/DreameApiResponse.cs +++ b/Core/Types/Dreame/DreameApiResponse.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Dreame; +namespace Core.Types.Dreame; -public class DreameApiResponse<T> { - [JsonPropertyName("data")] - public T Data { get; set; } -} \ No newline at end of file +public class DreameApiResponse<T> +{ + [JsonPropertyName("data")] public T Data { get; set; } +} diff --git a/Core/Types/Dreame/DreameCatalog.cs b/Core/Types/Dreame/DreameCatalog.cs index 523a2541..c0f73a84 100644 --- a/Core/Types/Dreame/DreameCatalog.cs +++ b/Core/Types/Dreame/DreameCatalog.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Dreame; +namespace Core.Types.Dreame; -public class DreameCatalog { - [JsonPropertyName("pager")] - public DreamePager Pager { get; set; } -} \ No newline at end of file +public class DreameCatalog +{ + [JsonPropertyName("pager")] public DreamePager Pager { get; set; } +} diff --git a/Core/Types/Dreame/DreameChapter.cs b/Core/Types/Dreame/DreameChapter.cs index a052ac89..06b06dd3 100644 --- a/Core/Types/Dreame/DreameChapter.cs +++ b/Core/Types/Dreame/DreameChapter.cs @@ -1,14 +1,12 @@ using System.Text.Json.Serialization; -namespace Core.Types.Dreame; - -public class DreameChapter { - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("id")] - public string Id { get; set; } - - [JsonPropertyName("content")] - public string Content { get; set; } -} \ No newline at end of file +namespace Core.Types.Dreame; + +public class DreameChapter +{ + [JsonPropertyName("title")] public string Title { get; set; } + + [JsonPropertyName("id")] public string Id { get; set; } + + [JsonPropertyName("content")] public string Content { get; set; } +} diff --git a/Core/Types/Dreame/DreameNovel.cs b/Core/Types/Dreame/DreameNovel.cs index c57d2625..3062b18e 100644 --- a/Core/Types/Dreame/DreameNovel.cs +++ b/Core/Types/Dreame/DreameNovel.cs @@ -1,26 +1,20 @@ using System.Text.Json.Serialization; -namespace Core.Types.Dreame; - -public class DreameNovel { - [JsonPropertyName("id")] - public long Id { get; set; } - - [JsonPropertyName("author_id")] - public string AuthorId { get; set; } - - [JsonPropertyName("author_name")] - public string AuthorName { get; set; } - - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("descr")] - public string Description { get; set; } - - [JsonPropertyName("language")] - public string Language { get; set; } - - [JsonPropertyName("cover_url")] - public string Cover { get; set; } -} \ No newline at end of file +namespace Core.Types.Dreame; + +public class DreameNovel +{ + [JsonPropertyName("id")] public long Id { get; set; } + + [JsonPropertyName("author_id")] public string AuthorId { get; set; } + + [JsonPropertyName("author_name")] public string AuthorName { get; set; } + + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("descr")] public string Description { get; set; } + + [JsonPropertyName("language")] public string Language { get; set; } + + [JsonPropertyName("cover_url")] public string Cover { get; set; } +} diff --git a/Core/Types/Dreame/DreamePager.cs b/Core/Types/Dreame/DreamePager.cs index 26ed24a2..dfcfccd1 100644 --- a/Core/Types/Dreame/DreamePager.cs +++ b/Core/Types/Dreame/DreamePager.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; using System.Text.Json.Serialization; -namespace Core.Types.Dreame; +namespace Core.Types.Dreame; -public class DreamePager { - [JsonPropertyName("chap_list")] - public List<DreameChapter> ChapterList { get; set; } -} \ No newline at end of file +public class DreamePager +{ + [JsonPropertyName("chap_list")] public List<DreameChapter> ChapterList { get; set; } +} diff --git a/Core/Types/FanFicus/FanFicusApiResponse.cs b/Core/Types/FanFicus/FanFicusApiResponse.cs index e83bf349..e668d16c 100644 --- a/Core/Types/FanFicus/FanFicusApiResponse.cs +++ b/Core/Types/FanFicus/FanFicusApiResponse.cs @@ -2,7 +2,7 @@ namespace Core.Types.FanFicus; -public class FanFicusApiResponse<T> { - [JsonPropertyName("value")] - public T Value { get; set; } -} \ No newline at end of file +public class FanFicusApiResponse<T> +{ + [JsonPropertyName("value")] public T Value { get; set; } +} diff --git a/Core/Types/FanFicus/FanFicusBook.cs b/Core/Types/FanFicus/FanFicusBook.cs index fc8d9aa9..d2da3f03 100644 --- a/Core/Types/FanFicus/FanFicusBook.cs +++ b/Core/Types/FanFicus/FanFicusBook.cs @@ -2,16 +2,13 @@ namespace Core.Types.FanFicus; -public class FanFicusBook { - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("description")] - public string Description { get; set; } - - [JsonPropertyName("creatorId")] - public FanFicusCreator[] Creators { get; set; } - - [JsonPropertyName("images")] - public FanFicusImage[] Images { get; set; } -} \ No newline at end of file +public class FanFicusBook +{ + [JsonPropertyName("title")] public string Title { get; set; } + + [JsonPropertyName("description")] public string Description { get; set; } + + [JsonPropertyName("creatorId")] public FanFicusCreator[] Creators { get; set; } + + [JsonPropertyName("images")] public FanFicusImage[] Images { get; set; } +} diff --git a/Core/Types/FanFicus/FanFicusChapter.cs b/Core/Types/FanFicus/FanFicusChapter.cs index d071ac34..ca2c2f78 100644 --- a/Core/Types/FanFicus/FanFicusChapter.cs +++ b/Core/Types/FanFicus/FanFicusChapter.cs @@ -2,7 +2,7 @@ namespace Core.Types.FanFicus; -public class FanFicusChapter { - [JsonPropertyName("text")] - public string Text { get; set; } -} \ No newline at end of file +public class FanFicusChapter +{ + [JsonPropertyName("text")] public string Text { get; set; } +} diff --git a/Core/Types/FanFicus/FanFicusCreator.cs b/Core/Types/FanFicus/FanFicusCreator.cs index dc46acfc..8ea2962c 100644 --- a/Core/Types/FanFicus/FanFicusCreator.cs +++ b/Core/Types/FanFicus/FanFicusCreator.cs @@ -2,10 +2,9 @@ namespace Core.Types.FanFicus; -public class FanFicusCreator { - [JsonPropertyName("_id")] - public string Id { get; set; } - - [JsonPropertyName("nickName")] - public string NickName { get; set; } -} \ No newline at end of file +public class FanFicusCreator +{ + [JsonPropertyName("_id")] public string Id { get; set; } + + [JsonPropertyName("nickName")] public string NickName { get; set; } +} diff --git a/Core/Types/FanFicus/FanFicusImage.cs b/Core/Types/FanFicus/FanFicusImage.cs index 096d1f5c..b42d04e2 100644 --- a/Core/Types/FanFicus/FanFicusImage.cs +++ b/Core/Types/FanFicus/FanFicusImage.cs @@ -2,7 +2,7 @@ namespace Core.Types.FanFicus; -public class FanFicusImage { - [JsonPropertyName("url")] - public string Url { get; set; } -} \ No newline at end of file +public class FanFicusImage +{ + [JsonPropertyName("url")] public string Url { get; set; } +} diff --git a/Core/Types/FanFicus/FanFicusPart.cs b/Core/Types/FanFicus/FanFicusPart.cs index eb4cf839..e553d17f 100644 --- a/Core/Types/FanFicus/FanFicusPart.cs +++ b/Core/Types/FanFicus/FanFicusPart.cs @@ -2,10 +2,9 @@ namespace Core.Types.FanFicus; -public class FanFicusPart { - [JsonPropertyName("_id")] - public string Id { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } -} \ No newline at end of file +public class FanFicusPart +{ + [JsonPropertyName("_id")] public string Id { get; set; } + + [JsonPropertyName("title")] public string Title { get; set; } +} diff --git a/Core/Types/FanFicus/FanFicusUser.cs b/Core/Types/FanFicus/FanFicusUser.cs index 2491d58c..8eff5e7e 100644 --- a/Core/Types/FanFicus/FanFicusUser.cs +++ b/Core/Types/FanFicus/FanFicusUser.cs @@ -2,7 +2,7 @@ namespace Core.Types.FanFicus; -public class FanFicusUser { - [JsonPropertyName("token")] - public string Token { get; set; } -} \ No newline at end of file +public class FanFicusUser +{ + [JsonPropertyName("token")] public string Token { get; set; } +} diff --git a/Core/Types/Freedlit/FreedlitApiResponse.cs b/Core/Types/Freedlit/FreedlitApiResponse.cs index 6ef791ac..4c849db6 100644 --- a/Core/Types/Freedlit/FreedlitApiResponse.cs +++ b/Core/Types/Freedlit/FreedlitApiResponse.cs @@ -2,7 +2,7 @@ namespace Core.Types.Freedlit; -public class FreedlitApiResponse<T> { - [JsonPropertyName("success")] - public T Success { get; set; } -} \ No newline at end of file +public class FreedlitApiResponse<T> +{ + [JsonPropertyName("success")] public T Success { get; set; } +} diff --git a/Core/Types/Freedlit/FreedlitApp.cs b/Core/Types/Freedlit/FreedlitApp.cs index d698b42b..4ad1add7 100644 --- a/Core/Types/Freedlit/FreedlitApp.cs +++ b/Core/Types/Freedlit/FreedlitApp.cs @@ -2,7 +2,7 @@ namespace Core.Types.Freedlit; -public class FreedlitApp<TErr> { - [JsonPropertyName("props")] - public FreedlitProps<TErr> Props { get; set; } -} \ No newline at end of file +public class FreedlitApp<TErr> +{ + [JsonPropertyName("props")] public FreedlitProps<TErr> Props { get; set; } +} diff --git a/Core/Types/Freedlit/FreedlitAuthError.cs b/Core/Types/Freedlit/FreedlitAuthError.cs index 9685a0ee..62d91adc 100644 --- a/Core/Types/Freedlit/FreedlitAuthError.cs +++ b/Core/Types/Freedlit/FreedlitAuthError.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.Freedlit; +namespace Core.Types.Freedlit; -public class FreedlitAuthError { - [JsonPropertyName("email")] - public string Email { get; set; } - - [JsonPropertyName("password")] - public string[] Password { get; set; } -} \ No newline at end of file +public class FreedlitAuthError +{ + [JsonPropertyName("email")] public string Email { get; set; } + + [JsonPropertyName("password")] public string[] Password { get; set; } +} diff --git a/Core/Types/Freedlit/FreedlitAuthor.cs b/Core/Types/Freedlit/FreedlitAuthor.cs index 436245a8..a5316187 100644 --- a/Core/Types/Freedlit/FreedlitAuthor.cs +++ b/Core/Types/Freedlit/FreedlitAuthor.cs @@ -2,13 +2,11 @@ namespace Core.Types.Freedlit; -public class FreedlitAuthor { - [JsonPropertyName("id")] - public long Id { get; set; } - - [JsonPropertyName("user_link")] - public string UserLink { get; set; } - - [JsonPropertyName("name")] - public string Name { get; set; } -} \ No newline at end of file +public class FreedlitAuthor +{ + [JsonPropertyName("id")] public long Id { get; set; } + + [JsonPropertyName("user_link")] public string UserLink { get; set; } + + [JsonPropertyName("name")] public string Name { get; set; } +} diff --git a/Core/Types/Freedlit/FreedlitBook.cs b/Core/Types/Freedlit/FreedlitBook.cs index 9debc8f7..bef06ad7 100644 --- a/Core/Types/Freedlit/FreedlitBook.cs +++ b/Core/Types/Freedlit/FreedlitBook.cs @@ -2,22 +2,17 @@ namespace Core.Types.Freedlit; -public class FreedlitBook { - [JsonPropertyName("id")] - public long Id { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("cover")] - public string Cover { get; set; } - - [JsonPropertyName("annotation")] - public string Annotation { get; set; } - - [JsonPropertyName("language")] - public string Language { get; set; } - - [JsonPropertyName("main_author")] - public FreedlitAuthor MainAuthor { get; set; } -} \ No newline at end of file +public class FreedlitBook +{ + [JsonPropertyName("id")] public long Id { get; set; } + + [JsonPropertyName("title")] public string Title { get; set; } + + [JsonPropertyName("cover")] public string Cover { get; set; } + + [JsonPropertyName("annotation")] public string Annotation { get; set; } + + [JsonPropertyName("language")] public string Language { get; set; } + + [JsonPropertyName("main_author")] public FreedlitAuthor MainAuthor { get; set; } +} diff --git a/Core/Types/Freedlit/FreedlitChapter.cs b/Core/Types/Freedlit/FreedlitChapter.cs index bd5dfb98..63c1de48 100644 --- a/Core/Types/Freedlit/FreedlitChapter.cs +++ b/Core/Types/Freedlit/FreedlitChapter.cs @@ -2,13 +2,11 @@ namespace Core.Types.Freedlit; -public class FreedlitChapter { - [JsonPropertyName("id")] - public long Id { get; set; } - - [JsonPropertyName("header")] - public string Header { get; set; } - - [JsonPropertyName("content")] - public string Content { get; set; } -} \ No newline at end of file +public class FreedlitChapter +{ + [JsonPropertyName("id")] public long Id { get; set; } + + [JsonPropertyName("header")] public string Header { get; set; } + + [JsonPropertyName("content")] public string Content { get; set; } +} diff --git a/Core/Types/Freedlit/FreedlitItemsContent.cs b/Core/Types/Freedlit/FreedlitItemsContent.cs index 29249e6b..df1dfc1b 100644 --- a/Core/Types/Freedlit/FreedlitItemsContent.cs +++ b/Core/Types/Freedlit/FreedlitItemsContent.cs @@ -3,7 +3,7 @@ namespace Core.Types.Freedlit; -public class FreedlitItemsContent<T> { - [JsonPropertyName("items")] - public List<T> Items { get; set; } -} \ No newline at end of file +public class FreedlitItemsContent<T> +{ + [JsonPropertyName("items")] public List<T> Items { get; set; } +} diff --git a/Core/Types/Freedlit/FreedlitProps.cs b/Core/Types/Freedlit/FreedlitProps.cs index 6f8ca76e..d9a84d5e 100644 --- a/Core/Types/Freedlit/FreedlitProps.cs +++ b/Core/Types/Freedlit/FreedlitProps.cs @@ -2,10 +2,9 @@ namespace Core.Types.Freedlit; -public class FreedlitProps<TErr> { - [JsonPropertyName("book")] - public FreedlitBook Book { get; set; } - - [JsonPropertyName("errors")] - public TErr Errors { get; set; } -} \ No newline at end of file +public class FreedlitProps<TErr> +{ + [JsonPropertyName("book")] public FreedlitBook Book { get; set; } + + [JsonPropertyName("errors")] public TErr Errors { get; set; } +} diff --git a/Core/Types/Freedlit/FreedlitSuccessResponse.cs b/Core/Types/Freedlit/FreedlitSuccessResponse.cs index 14944614..a6a458e4 100644 --- a/Core/Types/Freedlit/FreedlitSuccessResponse.cs +++ b/Core/Types/Freedlit/FreedlitSuccessResponse.cs @@ -3,7 +3,7 @@ namespace Core.Types.Freedlit; -public class FreedlitSuccessResponse<T> { - [JsonPropertyName("items")] - public List<T> Items { get; set; } -} \ No newline at end of file +public class FreedlitSuccessResponse<T> +{ + [JsonPropertyName("items")] public List<T> Items { get; set; } +} diff --git a/Core/Types/HotNovelPub/HotNovelPubApiResponse.cs b/Core/Types/HotNovelPub/HotNovelPubApiResponse.cs index 55cca7ab..1c14bb4e 100644 --- a/Core/Types/HotNovelPub/HotNovelPubApiResponse.cs +++ b/Core/Types/HotNovelPub/HotNovelPubApiResponse.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.HotNovelPub; +namespace Core.Types.HotNovelPub; -public class HotNovelPubApiResponse<T> { - [JsonPropertyName("data")] - public T Data { get; set; } -} \ No newline at end of file +public class HotNovelPubApiResponse<T> +{ + [JsonPropertyName("data")] public T Data { get; set; } +} diff --git a/Core/Types/HotNovelPub/HotNovelPubAuthorize.cs b/Core/Types/HotNovelPub/HotNovelPubAuthorize.cs index 219a9faf..19df3a4b 100644 --- a/Core/Types/HotNovelPub/HotNovelPubAuthorize.cs +++ b/Core/Types/HotNovelPub/HotNovelPubAuthorize.cs @@ -1,14 +1,12 @@ using System.Text.Json.Serialization; -namespace Core.Types.HotNovelPub; - -public class HotNovelPubAuthorize { - [JsonPropertyName("id")] - public int Id { get; set; } - - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("slug")] - public string Slug { get; set; } -} \ No newline at end of file +namespace Core.Types.HotNovelPub; + +public class HotNovelPubAuthorize +{ + [JsonPropertyName("id")] public int Id { get; set; } + + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("slug")] public string Slug { get; set; } +} diff --git a/Core/Types/HotNovelPub/HotNovelPubBook.cs b/Core/Types/HotNovelPub/HotNovelPubBook.cs index 76c3682f..f26d2765 100644 --- a/Core/Types/HotNovelPub/HotNovelPubBook.cs +++ b/Core/Types/HotNovelPub/HotNovelPubBook.cs @@ -1,17 +1,14 @@ using System.Text.Json.Serialization; -namespace Core.Types.HotNovelPub; +namespace Core.Types.HotNovelPub; -public class HotNovelPubBook { - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("description")] - public string Description { get; set; } - - [JsonPropertyName("authorize")] - public HotNovelPubAuthorize Authorize { get; set; } - - [JsonPropertyName("image")] - public string Image { get; set; } -} \ No newline at end of file +public class HotNovelPubBook +{ + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("description")] public string Description { get; set; } + + [JsonPropertyName("authorize")] public HotNovelPubAuthorize Authorize { get; set; } + + [JsonPropertyName("image")] public string Image { get; set; } +} diff --git a/Core/Types/HotNovelPub/HotNovelPubBookResponse.cs b/Core/Types/HotNovelPub/HotNovelPubBookResponse.cs index 421ac832..f543efe0 100644 --- a/Core/Types/HotNovelPub/HotNovelPubBookResponse.cs +++ b/Core/Types/HotNovelPub/HotNovelPubBookResponse.cs @@ -1,12 +1,11 @@ using System.Collections.Generic; using System.Text.Json.Serialization; -namespace Core.Types.HotNovelPub; +namespace Core.Types.HotNovelPub; -public class HotNovelPubBookResponse { - [JsonPropertyName("book")] - public HotNovelPubBook Book { get; set; } - - [JsonPropertyName("chapters")] - public List<HotNovelPubChapter> Chapters { get; set; } -} \ No newline at end of file +public class HotNovelPubBookResponse +{ + [JsonPropertyName("book")] public HotNovelPubBook Book { get; set; } + + [JsonPropertyName("chapters")] public List<HotNovelPubChapter> Chapters { get; set; } +} diff --git a/Core/Types/HotNovelPub/HotNovelPubChapter.cs b/Core/Types/HotNovelPub/HotNovelPubChapter.cs index 15739fc7..211e4bc9 100644 --- a/Core/Types/HotNovelPub/HotNovelPubChapter.cs +++ b/Core/Types/HotNovelPub/HotNovelPubChapter.cs @@ -1,14 +1,12 @@ using System.Text.Json.Serialization; -namespace Core.Types.HotNovelPub; - -public class HotNovelPubChapter { - [JsonPropertyName("id")] - public int Id { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("slug")] - public string Slug { get; set; } -} \ No newline at end of file +namespace Core.Types.HotNovelPub; + +public class HotNovelPubChapter +{ + [JsonPropertyName("id")] public int Id { get; set; } + + [JsonPropertyName("title")] public string Title { get; set; } + + [JsonPropertyName("slug")] public string Slug { get; set; } +} diff --git a/Core/Types/Librebook/LibrebookAuthResponse.cs b/Core/Types/Librebook/LibrebookAuthResponse.cs index c4269177..f4984ede 100644 --- a/Core/Types/Librebook/LibrebookAuthResponse.cs +++ b/Core/Types/Librebook/LibrebookAuthResponse.cs @@ -2,10 +2,9 @@ namespace Core.Types.Librebook; -public class LibrebookAuthResponse { - [JsonPropertyName("type")] - public string Type { get; set; } - - [JsonPropertyName("text")] - public string Text { get; set; } -} \ No newline at end of file +public class LibrebookAuthResponse +{ + [JsonPropertyName("type")] public string Type { get; set; } + + [JsonPropertyName("text")] public string Text { get; set; } +} diff --git a/Core/Types/Litexit/LitexitChapter.cs b/Core/Types/Litexit/LitexitChapter.cs index c8a0c325..9d38d3b3 100644 --- a/Core/Types/Litexit/LitexitChapter.cs +++ b/Core/Types/Litexit/LitexitChapter.cs @@ -1,14 +1,12 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litexit; - -public class LitexitChapter { - [JsonPropertyName("id")] - public int Id { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("text")] - public string Text { get; set; } -} \ No newline at end of file +namespace Core.Types.Litexit; + +public class LitexitChapter +{ + [JsonPropertyName("id")] public int Id { get; set; } + + [JsonPropertyName("title")] public string Title { get; set; } + + [JsonPropertyName("text")] public string Text { get; set; } +} diff --git a/Core/Types/Litexit/LitexitUser.cs b/Core/Types/Litexit/LitexitUser.cs index a1a08bf0..b4a4706f 100644 --- a/Core/Types/Litexit/LitexitUser.cs +++ b/Core/Types/Litexit/LitexitUser.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litexit; +namespace Core.Types.Litexit; -public class LitexitUser { - [JsonPropertyName("id")] - public int Id { get; set; } -} \ No newline at end of file +public class LitexitUser +{ + [JsonPropertyName("id")] public int Id { get; set; } +} diff --git a/Core/Types/Litgorod/LitgorodAuthResponse.cs b/Core/Types/Litgorod/LitgorodAuthResponse.cs index 916b0cbc..9be9dabc 100644 --- a/Core/Types/Litgorod/LitgorodAuthResponse.cs +++ b/Core/Types/Litgorod/LitgorodAuthResponse.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litgorod; +namespace Core.Types.Litgorod; -public class LitgorodAuthResponse { - [JsonPropertyName("message")] - public string Message { get; set; } -} \ No newline at end of file +public class LitgorodAuthResponse +{ + [JsonPropertyName("message")] public string Message { get; set; } +} diff --git a/Core/Types/Litgorod/LitgorodBookPart.cs b/Core/Types/Litgorod/LitgorodBookPart.cs index 748fb176..873c82a1 100644 --- a/Core/Types/Litgorod/LitgorodBookPart.cs +++ b/Core/Types/Litgorod/LitgorodBookPart.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litgorod; +namespace Core.Types.Litgorod; -public class LitgorodBookPart { - [JsonPropertyName("text")] - public string Text { get; set; } -} \ No newline at end of file +public class LitgorodBookPart +{ + [JsonPropertyName("text")] public string Text { get; set; } +} diff --git a/Core/Types/Litgorod/LitgorodChapter.cs b/Core/Types/Litgorod/LitgorodChapter.cs index 2cf935c0..18cedd46 100644 --- a/Core/Types/Litgorod/LitgorodChapter.cs +++ b/Core/Types/Litgorod/LitgorodChapter.cs @@ -1,8 +1,9 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litgorod; +namespace Core.Types.Litgorod; -public class LitgorodChapter { +public class LitgorodChapter +{ [JsonPropertyName("explodedParagraphs")] public string[] Paragraphs { get; set; } -} \ No newline at end of file +} diff --git a/Core/Types/Litmarket/AuthResponse.cs b/Core/Types/Litmarket/AuthResponse.cs index f3d073aa..ad28974b 100644 --- a/Core/Types/Litmarket/AuthResponse.cs +++ b/Core/Types/Litmarket/AuthResponse.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litmarket; +namespace Core.Types.Litmarket; -public class AuthResponse { - [JsonPropertyName("success")] - public bool Success { get; set; } -} \ No newline at end of file +public class AuthResponse +{ + [JsonPropertyName("success")] public bool Success { get; set; } +} diff --git a/Core/Types/Litmarket/Block.cs b/Core/Types/Litmarket/Block.cs index 4fd6f666..0d07077e 100644 --- a/Core/Types/Litmarket/Block.cs +++ b/Core/Types/Litmarket/Block.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litmarket; +namespace Core.Types.Litmarket; -public class Block { - [JsonPropertyName("chunk")] - public Chunk Chunk { get; set; } - - [JsonPropertyName("index")] - public int Index { get; set; } -} \ No newline at end of file +public class Block +{ + [JsonPropertyName("chunk")] public Chunk Chunk { get; set; } + + [JsonPropertyName("index")] public int Index { get; set; } +} diff --git a/Core/Types/Litmarket/Chunk.cs b/Core/Types/Litmarket/Chunk.cs index 1ce3645a..0b825cbd 100644 --- a/Core/Types/Litmarket/Chunk.cs +++ b/Core/Types/Litmarket/Chunk.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litmarket; +namespace Core.Types.Litmarket; -public class Chunk { - [JsonPropertyName("mods")] - public Mod[] Mods { get; set; } -} \ No newline at end of file +public class Chunk +{ + [JsonPropertyName("mods")] public Mod[] Mods { get; set; } +} diff --git a/Core/Types/Litmarket/LBook.cs b/Core/Types/Litmarket/LBook.cs index 0112f07c..689f5f57 100644 --- a/Core/Types/Litmarket/LBook.cs +++ b/Core/Types/Litmarket/LBook.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litmarket; +namespace Core.Types.Litmarket; -public class LBook { - [JsonPropertyName("ebookId")] - public int EbookId { get; set; } -} \ No newline at end of file +public class LBook +{ + [JsonPropertyName("ebookId")] public int EbookId { get; set; } +} diff --git a/Core/Types/Litmarket/Mod.cs b/Core/Types/Litmarket/Mod.cs index 84a5d596..29572b1b 100644 --- a/Core/Types/Litmarket/Mod.cs +++ b/Core/Types/Litmarket/Mod.cs @@ -1,21 +1,17 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Core.Types.Litmarket; +namespace Core.Types.Litmarket; -public class Mod { - [JsonPropertyName("type")] - public string Type { get; set; } - - [JsonPropertyName("styles")] - public string[] Styles { get; set; } +public class Mod +{ + [JsonPropertyName("type")] public string Type { get; set; } - [JsonPropertyName("text")] - public string Text { get; set; } - - [JsonPropertyName("data")] - public JsonElement Data { get; set; } - - [JsonPropertyName("mods")] - public Mod[] Mods { get; set; } -} \ No newline at end of file + [JsonPropertyName("styles")] public string[] Styles { get; set; } + + [JsonPropertyName("text")] public string Text { get; set; } + + [JsonPropertyName("data")] public JsonElement Data { get; set; } + + [JsonPropertyName("mods")] public Mod[] Mods { get; set; } +} diff --git a/Core/Types/Litmarket/Response.cs b/Core/Types/Litmarket/Response.cs index 52a8d3f2..fe518b08 100644 --- a/Core/Types/Litmarket/Response.cs +++ b/Core/Types/Litmarket/Response.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litmarket; +namespace Core.Types.Litmarket; -public class Response { - [JsonPropertyName("book")] - public LBook Book { get; set; } +public class Response +{ + [JsonPropertyName("book")] public LBook Book { get; set; } - [JsonPropertyName("tableOfContent")] - public string Toc { get; set; } -} \ No newline at end of file + [JsonPropertyName("tableOfContent")] public string Toc { get; set; } +} diff --git a/Core/Types/Litnet/LitnetAuthResponse.cs b/Core/Types/Litnet/LitnetAuthResponse.cs index a5fe8b12..f6ce395a 100644 --- a/Core/Types/Litnet/LitnetAuthResponse.cs +++ b/Core/Types/Litnet/LitnetAuthResponse.cs @@ -1,14 +1,12 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litnet; - -public class LitnetAuthResponse { - [JsonPropertyName("error")] - public string Error { get; set; } - - [JsonPropertyName("token")] - public string Token { get; set; } - - [JsonPropertyName("device_id")] - public string DeviceID { get; set; } -} \ No newline at end of file +namespace Core.Types.Litnet; + +public class LitnetAuthResponse +{ + [JsonPropertyName("error")] public string Error { get; set; } + + [JsonPropertyName("token")] public string Token { get; set; } + + [JsonPropertyName("device_id")] public string DeviceID { get; set; } +} diff --git a/Core/Types/Litnet/LitnetBookResponse.cs b/Core/Types/Litnet/LitnetBookResponse.cs index eb330bef..b341a2f5 100644 --- a/Core/Types/Litnet/LitnetBookResponse.cs +++ b/Core/Types/Litnet/LitnetBookResponse.cs @@ -1,38 +1,28 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litnet; - -public class LitnetBookResponse { - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("author_name")] - public string AuthorName { get; set; } - - [JsonPropertyName("cover")] - public string Cover { get; set; } - - [JsonPropertyName("annotation")] - public string Annotation { get; set; } - - [JsonPropertyName("url")] - public string Url { get; set; } - - [JsonPropertyName("author_id")] - public long AuthorId { get; set; } - - [JsonPropertyName("lang")] - public string Lang { get; set; } - - [JsonPropertyName("cycle_priority")] - public int? CyclePriority { get; set; } - - [JsonPropertyName("adult_only")] - public bool AdultOnly { get; set; } - - [JsonPropertyName("co_author_name")] - public string CoAuthorName { get; set; } - - [JsonPropertyName("co_author")] - public long? CoAuthorId { get; set; } -} \ No newline at end of file +namespace Core.Types.Litnet; + +public class LitnetBookResponse +{ + [JsonPropertyName("title")] public string Title { get; set; } + + [JsonPropertyName("author_name")] public string AuthorName { get; set; } + + [JsonPropertyName("cover")] public string Cover { get; set; } + + [JsonPropertyName("annotation")] public string Annotation { get; set; } + + [JsonPropertyName("url")] public string Url { get; set; } + + [JsonPropertyName("author_id")] public long AuthorId { get; set; } + + [JsonPropertyName("lang")] public string Lang { get; set; } + + [JsonPropertyName("cycle_priority")] public int? CyclePriority { get; set; } + + [JsonPropertyName("adult_only")] public bool AdultOnly { get; set; } + + [JsonPropertyName("co_author_name")] public string CoAuthorName { get; set; } + + [JsonPropertyName("co_author")] public long? CoAuthorId { get; set; } +} diff --git a/Core/Types/Litnet/LitnetChapterResponse.cs b/Core/Types/Litnet/LitnetChapterResponse.cs index b81f3a04..d88052db 100644 --- a/Core/Types/Litnet/LitnetChapterResponse.cs +++ b/Core/Types/Litnet/LitnetChapterResponse.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litnet; +namespace Core.Types.Litnet; -public class LitnetChapterResponse { - [JsonPropertyName("id")] - public long Id { get; set; } - - [JsonPropertyName("text")] - public string Text { get; set; } -} \ No newline at end of file +public class LitnetChapterResponse +{ + [JsonPropertyName("id")] public long Id { get; set; } + + [JsonPropertyName("text")] public string Text { get; set; } +} diff --git a/Core/Types/Litnet/LitnetContentsResponse.cs b/Core/Types/Litnet/LitnetContentsResponse.cs index 37b2d269..d7e381f8 100644 --- a/Core/Types/Litnet/LitnetContentsResponse.cs +++ b/Core/Types/Litnet/LitnetContentsResponse.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litnet; +namespace Core.Types.Litnet; -public class LitnetContentsResponse { - [JsonPropertyName("id")] - public long Id { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } -} \ No newline at end of file +public class LitnetContentsResponse +{ + [JsonPropertyName("id")] public long Id { get; set; } + + [JsonPropertyName("title")] public string Title { get; set; } +} diff --git a/Core/Types/Litres/LitresBook.cs b/Core/Types/Litres/LitresBook.cs index d3261202..b04d420c 100644 --- a/Core/Types/Litres/LitresBook.cs +++ b/Core/Types/Litres/LitresBook.cs @@ -1,10 +1,11 @@ using System.Collections.Generic; using HtmlAgilityPack; -namespace Core.Types.Litres; +namespace Core.Types.Litres; -public class LitresBook { +public class LitresBook +{ public HtmlDocument Content; public Dictionary<string, LitresTarget> Targets = new(); -} \ No newline at end of file +} diff --git a/Core/Types/Litres/LitresPayload.cs b/Core/Types/Litres/LitresPayload.cs index 6b87383b..c0e6fa3f 100644 --- a/Core/Types/Litres/LitresPayload.cs +++ b/Core/Types/Litres/LitresPayload.cs @@ -5,43 +5,41 @@ using System.Text; using System.Text.Json.Serialization; -namespace Core.Types.Litres; - -public class LitresPayload { - [JsonPropertyName("sha")] - public string Sha { get; set; } - - [JsonPropertyName("sid")] - public string Sid { get; set; } - - [JsonPropertyName("uilang")] - public string UiLang { get; set; } = "rus"; - - [JsonPropertyName("time")] - public string Time { get; set; } - - [JsonPropertyName("mobile_app")] - public string App { get; set; } - - [JsonPropertyName("requests")] - public List<object> Requests { get; set; } = new(); - - public static LitresPayload Create(DateTime date, string sid, string secret, string app) { - var result = new LitresPayload { +namespace Core.Types.Litres; + +public class LitresPayload +{ + [JsonPropertyName("sha")] public string Sha { get; set; } + + [JsonPropertyName("sid")] public string Sid { get; set; } + + [JsonPropertyName("uilang")] public string UiLang { get; set; } = "rus"; + + [JsonPropertyName("time")] public string Time { get; set; } + + [JsonPropertyName("mobile_app")] public string App { get; set; } + + [JsonPropertyName("requests")] public List<object> Requests { get; set; } = new(); + + public static LitresPayload Create(DateTime date, string sid, string secret, string app) + { + var result = new LitresPayload + { Time = date.ToString("yyyy-MM-ddTHH:mm:sszzzz"), App = app, Sid = sid }; - + result.Sha = Sha256(result.Time + secret); return result; } - - private static string Sha256(string str) { + + private static string Sha256(string str) + { var crypt = SHA256.Create(); var crypto = crypt.ComputeHash(Encoding.UTF8.GetBytes(str), 0, Encoding.UTF8.GetByteCount(str)); return crypto.Aggregate(new StringBuilder(), (sb, b) => sb.Append(b.ToString("x2"))).ToString(); } -} \ No newline at end of file +} diff --git a/Core/Types/Litres/LitresTarget.cs b/Core/Types/Litres/LitresTarget.cs index b33ee445..1bae77ad 100644 --- a/Core/Types/Litres/LitresTarget.cs +++ b/Core/Types/Litres/LitresTarget.cs @@ -1,7 +1,8 @@ -namespace Core.Types.Litres; +namespace Core.Types.Litres; -public class LitresTarget { +public class LitresTarget +{ public string Id { get; set; } public string Target { get; set; } public byte[] Content { get; set; } -} \ No newline at end of file +} diff --git a/Core/Types/Litres/Requests/LitresAuthData.cs b/Core/Types/Litres/Requests/LitresAuthData.cs index 24b96eeb..e19fb8eb 100644 --- a/Core/Types/Litres/Requests/LitresAuthData.cs +++ b/Core/Types/Litres/Requests/LitresAuthData.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litres.Requests; +namespace Core.Types.Litres.Requests; -public class LitresAuthData { - [JsonPropertyName("login")] - public string Login { get; set; } - - [JsonPropertyName("pwd")] - public string Password { get; set; } -} \ No newline at end of file +public class LitresAuthData +{ + [JsonPropertyName("login")] public string Login { get; set; } + + [JsonPropertyName("pwd")] public string Password { get; set; } +} diff --git a/Core/Types/Litres/Requests/LitresAuthRequest.cs b/Core/Types/Litres/Requests/LitresAuthRequest.cs index 5c71d4a4..2011fe80 100644 --- a/Core/Types/Litres/Requests/LitresAuthRequest.cs +++ b/Core/Types/Litres/Requests/LitresAuthRequest.cs @@ -1,9 +1,11 @@ -namespace Core.Types.Litres.Requests; +namespace Core.Types.Litres.Requests; -public class LitresAuthRequest : LitresRequestBase<LitresAuthData> { - public LitresAuthRequest(string login, string password) { +public class LitresAuthRequest : LitresRequestBase<LitresAuthData> +{ + public LitresAuthRequest(string login, string password) + { Func = "w_create_sid"; Param.Login = login; Param.Password = password; } -} \ No newline at end of file +} diff --git a/Core/Types/Litres/Requests/LitresBrowseArtsData.cs b/Core/Types/Litres/Requests/LitresBrowseArtsData.cs index 49fe6d72..e4540326 100644 --- a/Core/Types/Litres/Requests/LitresBrowseArtsData.cs +++ b/Core/Types/Litres/Requests/LitresBrowseArtsData.cs @@ -1,14 +1,12 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litres.Requests; - -public class LitresBrowseArtsData { - [JsonPropertyName("currency")] - public string Currency { get; set; } - - [JsonPropertyName("anno")] - public string Anno { get; set; } - - [JsonPropertyName("id")] - public string[] Id { get; set; } -} \ No newline at end of file +namespace Core.Types.Litres.Requests; + +public class LitresBrowseArtsData +{ + [JsonPropertyName("currency")] public string Currency { get; set; } + + [JsonPropertyName("anno")] public string Anno { get; set; } + + [JsonPropertyName("id")] public string[] Id { get; set; } +} diff --git a/Core/Types/Litres/Requests/LitresBrowseArtsRequest.cs b/Core/Types/Litres/Requests/LitresBrowseArtsRequest.cs index ac812e17..2827d6ff 100644 --- a/Core/Types/Litres/Requests/LitresBrowseArtsRequest.cs +++ b/Core/Types/Litres/Requests/LitresBrowseArtsRequest.cs @@ -1,10 +1,12 @@ -namespace Core.Types.Litres.Requests; +namespace Core.Types.Litres.Requests; -public class LitresBrowseArtsRequest : LitresRequestBase<LitresBrowseArtsData> { - public LitresBrowseArtsRequest(string[] id) { +public class LitresBrowseArtsRequest : LitresRequestBase<LitresBrowseArtsData> +{ + public LitresBrowseArtsRequest(string[] id) + { Func = "r_browse_arts"; Param.Currency = "RUB"; Param.Anno = "1"; Param.Id = id; } -} \ No newline at end of file +} diff --git a/Core/Types/Litres/Requests/LitresRequestBase.cs b/Core/Types/Litres/Requests/LitresRequestBase.cs index fe157b38..81ec054c 100644 --- a/Core/Types/Litres/Requests/LitresRequestBase.cs +++ b/Core/Types/Litres/Requests/LitresRequestBase.cs @@ -1,14 +1,12 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litres.Requests; - -public abstract class LitresRequestBase<T> where T : new() { - [JsonPropertyName("id")] - public string Id { get; set; } = "data"; - - [JsonPropertyName("func")] - public string Func { get; set; } - - [JsonPropertyName("param")] - public T Param { get; } = new(); -} \ No newline at end of file +namespace Core.Types.Litres.Requests; + +public abstract class LitresRequestBase<T> where T : new() +{ + [JsonPropertyName("id")] public string Id { get; set; } = "data"; + + [JsonPropertyName("func")] public string Func { get; set; } + + [JsonPropertyName("param")] public T Param { get; } = new(); +} diff --git a/Core/Types/Litres/Response/LitresArt.cs b/Core/Types/Litres/Response/LitresArt.cs index fec94f86..d9c51e47 100644 --- a/Core/Types/Litres/Response/LitresArt.cs +++ b/Core/Types/Litres/Response/LitresArt.cs @@ -1,29 +1,22 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litres.Response; - -public class LitresArt { - [JsonPropertyName("id")] - public long Id { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("html_annotation")] - public string Annotation { get; set; } - - [JsonPropertyName("persons")] - public LitresPerson<long>[] Persons { get; set; } = []; - - [JsonPropertyName("cover_url")] - public string Cover { get; set; } - - [JsonPropertyName("series")] - public LitresSeria[] Sequences { get; set; } = []; - - [JsonPropertyName("linked_arts")] - public LitresArt[] LinkedArts { get; set; } - - [JsonPropertyName("art_type")] - public LitresArtTypeEnum ArtType { get; set; } -} \ No newline at end of file +namespace Core.Types.Litres.Response; + +public class LitresArt +{ + [JsonPropertyName("id")] public long Id { get; set; } + + [JsonPropertyName("title")] public string Title { get; set; } + + [JsonPropertyName("html_annotation")] public string Annotation { get; set; } + + [JsonPropertyName("persons")] public LitresPerson<long>[] Persons { get; set; } = []; + + [JsonPropertyName("cover_url")] public string Cover { get; set; } + + [JsonPropertyName("series")] public LitresSeria[] Sequences { get; set; } = []; + + [JsonPropertyName("linked_arts")] public LitresArt[] LinkedArts { get; set; } + + [JsonPropertyName("art_type")] public LitresArtTypeEnum ArtType { get; set; } +} diff --git a/Core/Types/Litres/Response/LitresArtTypeEnum.cs b/Core/Types/Litres/Response/LitresArtTypeEnum.cs index 8605135a..ea5ddd0b 100644 --- a/Core/Types/Litres/Response/LitresArtTypeEnum.cs +++ b/Core/Types/Litres/Response/LitresArtTypeEnum.cs @@ -1,7 +1,8 @@ namespace Core.Types.Litres.Response; -public enum LitresArtTypeEnum { +public enum LitresArtTypeEnum +{ Text = 0, Audio = 1, Pdf = 4 -} \ No newline at end of file +} diff --git a/Core/Types/Litres/Response/LitresAuthResponseData.cs b/Core/Types/Litres/Response/LitresAuthResponseData.cs index c6f212a1..77c9ef5f 100644 --- a/Core/Types/Litres/Response/LitresAuthResponseData.cs +++ b/Core/Types/Litres/Response/LitresAuthResponseData.cs @@ -1,14 +1,12 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litres.Response; - -public class LitresAuthResponseData { - [JsonPropertyName("success")] - public bool Success { get; set; } - - [JsonPropertyName("sid")] - public string Sid { get; set; } - - [JsonPropertyName("error_message")] - public string ErrorMessage { get; set; } -} \ No newline at end of file +namespace Core.Types.Litres.Response; + +public class LitresAuthResponseData +{ + [JsonPropertyName("success")] public bool Success { get; set; } + + [JsonPropertyName("sid")] public string Sid { get; set; } + + [JsonPropertyName("error_message")] public string ErrorMessage { get; set; } +} diff --git a/Core/Types/Litres/Response/LitresFile.cs b/Core/Types/Litres/Response/LitresFile.cs index 981a68c2..7f0c54c5 100644 --- a/Core/Types/Litres/Response/LitresFile.cs +++ b/Core/Types/Litres/Response/LitresFile.cs @@ -2,10 +2,9 @@ namespace Core.Types.Litres.Response; -public class LitresFile { - [JsonPropertyName("extension")] - public string Extension { get; set; } - - [JsonPropertyName("id")] - public long Id { get; set; } -} \ No newline at end of file +public class LitresFile +{ + [JsonPropertyName("extension")] public string Extension { get; set; } + + [JsonPropertyName("id")] public long Id { get; set; } +} diff --git a/Core/Types/Litres/Response/LitresFiles.cs b/Core/Types/Litres/Response/LitresFiles.cs index 703c82fc..97b99f60 100644 --- a/Core/Types/Litres/Response/LitresFiles.cs +++ b/Core/Types/Litres/Response/LitresFiles.cs @@ -2,7 +2,7 @@ namespace Core.Types.Litres.Response; -public class LitresFiles { - [JsonPropertyName("files")] - public LitresFile[] Files { get; set; } -} \ No newline at end of file +public class LitresFiles +{ + [JsonPropertyName("files")] public LitresFile[] Files { get; set; } +} diff --git a/Core/Types/Litres/Response/LitresMe.cs b/Core/Types/Litres/Response/LitresMe.cs index d4379d42..427f07ef 100644 --- a/Core/Types/Litres/Response/LitresMe.cs +++ b/Core/Types/Litres/Response/LitresMe.cs @@ -2,7 +2,8 @@ namespace Core.Types.Litres.Response; -public class LitresMe { +public class LitresMe +{ [JsonPropertyName("partner_subscriptions")] public LitresPartnerSubscriptions PartnerSubscriptions { get; set; } -} \ No newline at end of file +} diff --git a/Core/Types/Litres/Response/LitresPartnerSubscriptions.cs b/Core/Types/Litres/Response/LitresPartnerSubscriptions.cs index b959fbb3..1d665297 100644 --- a/Core/Types/Litres/Response/LitresPartnerSubscriptions.cs +++ b/Core/Types/Litres/Response/LitresPartnerSubscriptions.cs @@ -2,7 +2,7 @@ namespace Core.Types.Litres.Response; -public class LitresPartnerSubscriptions { - [JsonPropertyName("subscriptions")] - public LitresSubscription[] Subscriptions { get; set; } -} \ No newline at end of file +public class LitresPartnerSubscriptions +{ + [JsonPropertyName("subscriptions")] public LitresSubscription[] Subscriptions { get; set; } +} diff --git a/Core/Types/Litres/Response/LitresPerson.cs b/Core/Types/Litres/Response/LitresPerson.cs index 87d69808..48aa9fdd 100644 --- a/Core/Types/Litres/Response/LitresPerson.cs +++ b/Core/Types/Litres/Response/LitresPerson.cs @@ -1,17 +1,14 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litres.Response; +namespace Core.Types.Litres.Response; -public class LitresPerson<TId> { - [JsonPropertyName("id")] - public TId Id { get; set; } - - [JsonPropertyName("full_name")] - public string FullName { get; set; } - - [JsonPropertyName("role")] - public string Role { get; set; } - - [JsonPropertyName("url")] - public string Url { get; set; } -} \ No newline at end of file +public class LitresPerson<TId> +{ + [JsonPropertyName("id")] public TId Id { get; set; } + + [JsonPropertyName("full_name")] public string FullName { get; set; } + + [JsonPropertyName("role")] public string Role { get; set; } + + [JsonPropertyName("url")] public string Url { get; set; } +} diff --git a/Core/Types/Litres/Response/LitresResponse.cs b/Core/Types/Litres/Response/LitresResponse.cs index 91f222a0..e837d3ad 100644 --- a/Core/Types/Litres/Response/LitresResponse.cs +++ b/Core/Types/Litres/Response/LitresResponse.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litres.Response; +namespace Core.Types.Litres.Response; -public class LitresResponse<T> { - [JsonPropertyName("data")] - public T Data { get; set; } -} \ No newline at end of file +public class LitresResponse<T> +{ + [JsonPropertyName("data")] public T Data { get; set; } +} diff --git a/Core/Types/Litres/Response/LitresSeria.cs b/Core/Types/Litres/Response/LitresSeria.cs index 948d61ae..81e1a82c 100644 --- a/Core/Types/Litres/Response/LitresSeria.cs +++ b/Core/Types/Litres/Response/LitresSeria.cs @@ -1,14 +1,12 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litres.Response; - -public class LitresSeria { - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("id")] - public long Id { get; set; } - - [JsonPropertyName("sequence_number")] - public string SequenceNumber { get; set; } -} \ No newline at end of file +namespace Core.Types.Litres.Response; + +public class LitresSeria +{ + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("id")] public long Id { get; set; } + + [JsonPropertyName("sequence_number")] public string SequenceNumber { get; set; } +} diff --git a/Core/Types/Litres/Response/LitresStaticResponse.cs b/Core/Types/Litres/Response/LitresStaticResponse.cs index 0a5d2038..bebc55b9 100644 --- a/Core/Types/Litres/Response/LitresStaticResponse.cs +++ b/Core/Types/Litres/Response/LitresStaticResponse.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litres.Response; +namespace Core.Types.Litres.Response; -public class LitresStaticResponse<T> { - [JsonPropertyName("payload")] - public LitresResponse<T> Payload { get; set; } -} \ No newline at end of file +public class LitresStaticResponse<T> +{ + [JsonPropertyName("payload")] public LitresResponse<T> Payload { get; set; } +} diff --git a/Core/Types/Litres/Response/LitresSubscription.cs b/Core/Types/Litres/Response/LitresSubscription.cs index f662f9d0..c1a4894a 100644 --- a/Core/Types/Litres/Response/LitresSubscription.cs +++ b/Core/Types/Litres/Response/LitresSubscription.cs @@ -2,13 +2,11 @@ namespace Core.Types.Litres.Response; -public class LitresSubscription { - [JsonPropertyName("is_active")] - public bool IsActive { get; set; } - - [JsonPropertyName("host")] - public string Host { get; set; } - - [JsonPropertyName("type")] - public string Type { get; set; } -} \ No newline at end of file +public class LitresSubscription +{ + [JsonPropertyName("is_active")] public bool IsActive { get; set; } + + [JsonPropertyName("host")] public string Host { get; set; } + + [JsonPropertyName("type")] public string Type { get; set; } +} diff --git a/Core/Types/Litres/Response/LitresWebAuthResponse.cs b/Core/Types/Litres/Response/LitresWebAuthResponse.cs index 7c407e34..60c2194c 100644 --- a/Core/Types/Litres/Response/LitresWebAuthResponse.cs +++ b/Core/Types/Litres/Response/LitresWebAuthResponse.cs @@ -1,27 +1,22 @@ using System.Text.Json.Serialization; namespace Core.Types.Litres.Response; + public class LitresWebAuthPayloadData { - [JsonPropertyName("sid")] - public string Sid { get; set; } + [JsonPropertyName("sid")] public string Sid { get; set; } } public class LitresWebAuthPayload { - [JsonPropertyName("data")] - public LitresWebAuthPayloadData Data { get; set; } + [JsonPropertyName("data")] public LitresWebAuthPayloadData Data { get; set; } } public class LitresWebAuthResponse { - [JsonPropertyName("status")] - public int Status { get; set; } - - [JsonPropertyName("payload")] - public LitresWebAuthPayload Payload { get; set; } + [JsonPropertyName("status")] public int Status { get; set; } - [JsonPropertyName("error")] - public string Error { get; set; } = ""; -} + [JsonPropertyName("payload")] public LitresWebAuthPayload Payload { get; set; } + [JsonPropertyName("error")] public string Error { get; set; } = ""; +} diff --git a/Core/Types/Litsovet/LitsovetLoginResponse.cs b/Core/Types/Litsovet/LitsovetLoginResponse.cs index a9a61917..9ce32185 100644 --- a/Core/Types/Litsovet/LitsovetLoginResponse.cs +++ b/Core/Types/Litsovet/LitsovetLoginResponse.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litsovet; +namespace Core.Types.Litsovet; -public class LitsovetLoginResponse { - [JsonPropertyName("ok")] - public int Ok { get; set; } -} \ No newline at end of file +public class LitsovetLoginResponse +{ + [JsonPropertyName("ok")] public int Ok { get; set; } +} diff --git a/Core/Types/MangaLib/MangaLibPg.cs b/Core/Types/MangaLib/MangaLibPg.cs index 66c5f9fd..7bf9f03c 100644 --- a/Core/Types/MangaLib/MangaLibPg.cs +++ b/Core/Types/MangaLib/MangaLibPg.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.MangaLib; +namespace Core.Types.MangaLib; -public class MangaLibPg { - [JsonPropertyName("p")] - public int P { get; set; } - - [JsonPropertyName("u")] - public string U { get; set; } -} \ No newline at end of file +public class MangaLibPg +{ + [JsonPropertyName("p")] public int P { get; set; } + + [JsonPropertyName("u")] public string U { get; set; } +} diff --git a/Core/Types/MyBook/MyBookAuth.cs b/Core/Types/MyBook/MyBookAuth.cs index 8dc38bdb..6c193e58 100644 --- a/Core/Types/MyBook/MyBookAuth.cs +++ b/Core/Types/MyBook/MyBookAuth.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.MyBook; +namespace Core.Types.MyBook; -public class MyBookAuth { - [JsonPropertyName("token")] - public string Token { get; set; } - - [JsonPropertyName("secret")] - public string Secret { get; set; } -} \ No newline at end of file +public class MyBookAuth +{ + [JsonPropertyName("token")] public string Token { get; set; } + + [JsonPropertyName("secret")] public string Secret { get; set; } +} diff --git a/Core/Types/MyBook/MyBookAuthor.cs b/Core/Types/MyBook/MyBookAuthor.cs index 0f1abdd1..8fcae065 100644 --- a/Core/Types/MyBook/MyBookAuthor.cs +++ b/Core/Types/MyBook/MyBookAuthor.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.MyBook; +namespace Core.Types.MyBook; -public class MyBookAuthor { - [JsonPropertyName("cover_name")] - public string Name { get; set; } - - [JsonPropertyName("absolute_url")] - public string Url { get; set; } -} \ No newline at end of file +public class MyBookAuthor +{ + [JsonPropertyName("cover_name")] public string Name { get; set; } + + [JsonPropertyName("absolute_url")] public string Url { get; set; } +} diff --git a/Core/Types/MyBook/MyBookBook.cs b/Core/Types/MyBook/MyBookBook.cs index 5304e2fd..edd5eb80 100644 --- a/Core/Types/MyBook/MyBookBook.cs +++ b/Core/Types/MyBook/MyBookBook.cs @@ -1,36 +1,27 @@ using System.Text.Json.Nodes; using System.Text.Json.Serialization; -namespace Core.Types.MyBook; - -public class MyBookBook { - [JsonPropertyName("id")] - public long Id { get; set; } - - [JsonPropertyName("annotation")] - public string Annotation { get; set; } - - [JsonPropertyName("bookfile")] - public string BookFile { get; set; } - - [JsonPropertyName("default_cover")] - public string Cover { get; set; } - - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("authors")] - public MyBookAuthor[] Authors { get; set; } - - [JsonPropertyName("type")] - public string Type { get; set; } - - [JsonPropertyName("mapfiles")] - public MyBookMapFile[] MapFiles { get; set; } - - [JsonPropertyName("connected_book")] - public MyBookBook Connected { get; set; } - - [JsonPropertyName("bookfiles")] - public JsonNode[] Files { get; set; } -} \ No newline at end of file +namespace Core.Types.MyBook; + +public class MyBookBook +{ + [JsonPropertyName("id")] public long Id { get; set; } + + [JsonPropertyName("annotation")] public string Annotation { get; set; } + + [JsonPropertyName("bookfile")] public string BookFile { get; set; } + + [JsonPropertyName("default_cover")] public string Cover { get; set; } + + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("authors")] public MyBookAuthor[] Authors { get; set; } + + [JsonPropertyName("type")] public string Type { get; set; } + + [JsonPropertyName("mapfiles")] public MyBookMapFile[] MapFiles { get; set; } + + [JsonPropertyName("connected_book")] public MyBookBook Connected { get; set; } + + [JsonPropertyName("bookfiles")] public JsonNode[] Files { get; set; } +} diff --git a/Core/Types/MyBook/MyBookFile.cs b/Core/Types/MyBook/MyBookFile.cs index e6ed4f9d..ddc72f88 100644 --- a/Core/Types/MyBook/MyBookFile.cs +++ b/Core/Types/MyBook/MyBookFile.cs @@ -2,13 +2,11 @@ namespace Core.Types.MyBook; -public class MyBookFile { - [JsonPropertyName("url")] - public string Url { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("order")] - public int Order { get; set; } -} \ No newline at end of file +public class MyBookFile +{ + [JsonPropertyName("url")] public string Url { get; set; } + + [JsonPropertyName("title")] public string Title { get; set; } + + [JsonPropertyName("order")] public int Order { get; set; } +} diff --git a/Core/Types/MyBook/MyBookMapFile.cs b/Core/Types/MyBook/MyBookMapFile.cs index 6929b795..2c4c6656 100644 --- a/Core/Types/MyBook/MyBookMapFile.cs +++ b/Core/Types/MyBook/MyBookMapFile.cs @@ -2,7 +2,7 @@ namespace Core.Types.MyBook; -public class MyBookMapFile { - [JsonPropertyName("book")] - public long Book { get; set; } -} \ No newline at end of file +public class MyBookMapFile +{ + [JsonPropertyName("book")] public long Book { get; set; } +} diff --git a/Core/Types/Neobook/NeobookAttachment.cs b/Core/Types/Neobook/NeobookAttachment.cs index bc3be366..8cbcc332 100644 --- a/Core/Types/Neobook/NeobookAttachment.cs +++ b/Core/Types/Neobook/NeobookAttachment.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; using System.Text.Json.Serialization; -namespace Core.Types.Neobook; +namespace Core.Types.Neobook; -public class NeobookAttachment { - [JsonPropertyName("image")] - public Dictionary<string, string> Cover { get; set; } -} \ No newline at end of file +public class NeobookAttachment +{ + [JsonPropertyName("image")] public Dictionary<string, string> Cover { get; set; } +} diff --git a/Core/Types/Neobook/NeobookAuth.cs b/Core/Types/Neobook/NeobookAuth.cs index a80c5ce0..aa425870 100644 --- a/Core/Types/Neobook/NeobookAuth.cs +++ b/Core/Types/Neobook/NeobookAuth.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.Neobook; +namespace Core.Types.Neobook; -public class NeobookAuth { - [JsonPropertyName("error")] - public string Error { get; set; } - - [JsonPropertyName("login")] - public NeobookLogin Login { get; set; } -} \ No newline at end of file +public class NeobookAuth +{ + [JsonPropertyName("error")] public string Error { get; set; } + + [JsonPropertyName("login")] public NeobookLogin Login { get; set; } +} diff --git a/Core/Types/Neobook/NeobookBook.cs b/Core/Types/Neobook/NeobookBook.cs index 258124e5..dbe5a164 100644 --- a/Core/Types/Neobook/NeobookBook.cs +++ b/Core/Types/Neobook/NeobookBook.cs @@ -2,10 +2,10 @@ namespace Core.Types.Neobook; -public class NeobookBook { - [JsonPropertyName("chapters")] - public NeobookChapter[] Chapters { get; set; } - +public class NeobookBook +{ + [JsonPropertyName("chapters")] public NeobookChapter[] Chapters { get; set; } + [JsonPropertyName("active_chapter_index")] public int ActiveChapterIndex { get; set; } -} \ No newline at end of file +} diff --git a/Core/Types/Neobook/NeobookChapter.cs b/Core/Types/Neobook/NeobookChapter.cs index 5123e58a..9e391174 100644 --- a/Core/Types/Neobook/NeobookChapter.cs +++ b/Core/Types/Neobook/NeobookChapter.cs @@ -2,7 +2,7 @@ namespace Core.Types.Neobook; -public class NeobookChapter { - [JsonPropertyName("data")] - public NeobookChapterData Data { get; set; } -} \ No newline at end of file +public class NeobookChapter +{ + [JsonPropertyName("data")] public NeobookChapterData Data { get; set; } +} diff --git a/Core/Types/Neobook/NeobookChapterData.cs b/Core/Types/Neobook/NeobookChapterData.cs index d31bb149..85e39cdf 100644 --- a/Core/Types/Neobook/NeobookChapterData.cs +++ b/Core/Types/Neobook/NeobookChapterData.cs @@ -2,7 +2,7 @@ namespace Core.Types.Neobook; -public class NeobookChapterData { - [JsonPropertyName("html")] - public string Html { get; set; } -} \ No newline at end of file +public class NeobookChapterData +{ + [JsonPropertyName("html")] public string Html { get; set; } +} diff --git a/Core/Types/Neobook/NeobookLogin.cs b/Core/Types/Neobook/NeobookLogin.cs index 14d612f1..1d2eaffb 100644 --- a/Core/Types/Neobook/NeobookLogin.cs +++ b/Core/Types/Neobook/NeobookLogin.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.Neobook; +namespace Core.Types.Neobook; -public class NeobookLogin { - [JsonPropertyName("utoken")] - public string Utoken { get; set; } - - [JsonPropertyName("uid")] - public string Uid { get; set; } -} \ No newline at end of file +public class NeobookLogin +{ + [JsonPropertyName("utoken")] public string Utoken { get; set; } + + [JsonPropertyName("uid")] public string Uid { get; set; } +} diff --git a/Core/Types/Neobook/NeobookPostData.cs b/Core/Types/Neobook/NeobookPostData.cs index 9fc029f3..fb6c85f3 100644 --- a/Core/Types/Neobook/NeobookPostData.cs +++ b/Core/Types/Neobook/NeobookPostData.cs @@ -1,20 +1,16 @@ using System.Text.Json.Serialization; -namespace Core.Types.Neobook; +namespace Core.Types.Neobook; -public class NeobookPostData { - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("text")] - public string Text { get; set; } - - [JsonPropertyName("user")] - public NeobookUser User { get; set; } - - [JsonPropertyName("chapters")] - public NeobookTocChapter[] Chapters { get; set; } - - [JsonPropertyName("attachment")] - public NeobookAttachment Attachment { get; set; } -} \ No newline at end of file +public class NeobookPostData +{ + [JsonPropertyName("title")] public string Title { get; set; } + + [JsonPropertyName("text")] public string Text { get; set; } + + [JsonPropertyName("user")] public NeobookUser User { get; set; } + + [JsonPropertyName("chapters")] public NeobookTocChapter[] Chapters { get; set; } + + [JsonPropertyName("attachment")] public NeobookAttachment Attachment { get; set; } +} diff --git a/Core/Types/Neobook/NeobookTocChapter.cs b/Core/Types/Neobook/NeobookTocChapter.cs index 1e3a8ae8..e1540f65 100644 --- a/Core/Types/Neobook/NeobookTocChapter.cs +++ b/Core/Types/Neobook/NeobookTocChapter.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.Neobook; +namespace Core.Types.Neobook; -public class NeobookTocChapter { - [JsonPropertyName("token")] - public string Token { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } -} \ No newline at end of file +public class NeobookTocChapter +{ + [JsonPropertyName("token")] public string Token { get; set; } + + [JsonPropertyName("title")] public string Title { get; set; } +} diff --git a/Core/Types/Neobook/NeobookUser.cs b/Core/Types/Neobook/NeobookUser.cs index 15084228..3aacfef7 100644 --- a/Core/Types/Neobook/NeobookUser.cs +++ b/Core/Types/Neobook/NeobookUser.cs @@ -1,14 +1,12 @@ using System.Text.Json.Serialization; -namespace Core.Types.Neobook; - -public class NeobookUser { - [JsonPropertyName("username")] - public string UserName { get; set; } - - [JsonPropertyName("firstname")] - public string FirstName { get; set; } - - [JsonPropertyName("lastname")] - public string LastName { get; set; } -} \ No newline at end of file +namespace Core.Types.Neobook; + +public class NeobookUser +{ + [JsonPropertyName("username")] public string UserName { get; set; } + + [JsonPropertyName("firstname")] public string FirstName { get; set; } + + [JsonPropertyName("lastname")] public string LastName { get; set; } +} diff --git a/Core/Types/Ranobe/RanobeBook.cs b/Core/Types/Ranobe/RanobeBook.cs index 2a282490..a4946dad 100644 --- a/Core/Types/Ranobe/RanobeBook.cs +++ b/Core/Types/Ranobe/RanobeBook.cs @@ -1,21 +1,17 @@ using System.Collections.Generic; using System.Text.Json.Serialization; -namespace Core.Types.Ranobe; +namespace Core.Types.Ranobe; -public class RanobeBook { - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("author")] - public string Author { get; set; } - - [JsonPropertyName("verticalImages")] - public RanobeImage[] Images { get; set; } - - [JsonPropertyName("verticalImage")] - public RanobeImage Image { get; set; } - - [JsonPropertyName("chapters")] - public List<RanobeChapterShort> Chapters { get; set; } -} \ No newline at end of file +public class RanobeBook +{ + [JsonPropertyName("title")] public string Title { get; set; } + + [JsonPropertyName("author")] public string Author { get; set; } + + [JsonPropertyName("verticalImages")] public RanobeImage[] Images { get; set; } + + [JsonPropertyName("verticalImage")] public RanobeImage Image { get; set; } + + [JsonPropertyName("chapters")] public List<RanobeChapterShort> Chapters { get; set; } +} diff --git a/Core/Types/Ranobe/RanobeChapter.cs b/Core/Types/Ranobe/RanobeChapter.cs index 45622675..a2eb7fd4 100644 --- a/Core/Types/Ranobe/RanobeChapter.cs +++ b/Core/Types/Ranobe/RanobeChapter.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Ranobe; +namespace Core.Types.Ranobe; -public class RanobeChapter { - [JsonPropertyName("content")] - public RanobeChapterContent Content { get; set; } -} \ No newline at end of file +public class RanobeChapter +{ + [JsonPropertyName("content")] public RanobeChapterContent Content { get; set; } +} diff --git a/Core/Types/Ranobe/RanobeChapterContent.cs b/Core/Types/Ranobe/RanobeChapterContent.cs index 6ed1465f..ae84c90c 100644 --- a/Core/Types/Ranobe/RanobeChapterContent.cs +++ b/Core/Types/Ranobe/RanobeChapterContent.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Ranobe; +namespace Core.Types.Ranobe; -public class RanobeChapterContent { - [JsonPropertyName("text")] - public string Text { get; set; } -} \ No newline at end of file +public class RanobeChapterContent +{ + [JsonPropertyName("text")] public string Text { get; set; } +} diff --git a/Core/Types/Ranobe/RanobeChapterShort.cs b/Core/Types/Ranobe/RanobeChapterShort.cs index 0e6e650a..eec594f9 100644 --- a/Core/Types/Ranobe/RanobeChapterShort.cs +++ b/Core/Types/Ranobe/RanobeChapterShort.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.Ranobe; +namespace Core.Types.Ranobe; -public class RanobeChapterShort { - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("url")] - public string Url { get; set; } -} \ No newline at end of file +public class RanobeChapterShort +{ + [JsonPropertyName("title")] public string Title { get; set; } + + [JsonPropertyName("url")] public string Url { get; set; } +} diff --git a/Core/Types/Ranobe/RanobeImage.cs b/Core/Types/Ranobe/RanobeImage.cs index 86918b93..6f1e00c7 100644 --- a/Core/Types/Ranobe/RanobeImage.cs +++ b/Core/Types/Ranobe/RanobeImage.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.Ranobe; +namespace Core.Types.Ranobe; -public class RanobeImage { - [JsonPropertyName("height")] - public int Height { get; set; } +public class RanobeImage +{ + [JsonPropertyName("height")] public int Height { get; set; } - [JsonPropertyName("url")] - public string Url { get; set; } -} \ No newline at end of file + [JsonPropertyName("url")] public string Url { get; set; } +} diff --git a/Core/Types/RanobeHub/RanobeHubApiResponse.cs b/Core/Types/RanobeHub/RanobeHubApiResponse.cs index 9bae700e..7f834b20 100644 --- a/Core/Types/RanobeHub/RanobeHubApiResponse.cs +++ b/Core/Types/RanobeHub/RanobeHubApiResponse.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.RanobeHub; +namespace Core.Types.RanobeHub; -public class RanobeHubApiResponse { - [JsonPropertyName("volumes")] - public RanobeHubVolume[] Volumes { get; set; } -} \ No newline at end of file +public class RanobeHubApiResponse +{ + [JsonPropertyName("volumes")] public RanobeHubVolume[] Volumes { get; set; } +} diff --git a/Core/Types/RanobeHub/RanobeHubChapter.cs b/Core/Types/RanobeHub/RanobeHubChapter.cs index f173eb87..c92d9904 100644 --- a/Core/Types/RanobeHub/RanobeHubChapter.cs +++ b/Core/Types/RanobeHub/RanobeHubChapter.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.RanobeHub; +namespace Core.Types.RanobeHub; -public class RanobeHubChapter { - [JsonPropertyName("name")] - public string Name { get; set; } +public class RanobeHubChapter +{ + [JsonPropertyName("name")] public string Name { get; set; } - [JsonPropertyName("url")] - public string Url { get; set; } -} \ No newline at end of file + [JsonPropertyName("url")] public string Url { get; set; } +} diff --git a/Core/Types/RanobeHub/RanobeHubVolume.cs b/Core/Types/RanobeHub/RanobeHubVolume.cs index 3f73b73d..d2131c9c 100644 --- a/Core/Types/RanobeHub/RanobeHubVolume.cs +++ b/Core/Types/RanobeHub/RanobeHubVolume.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.RanobeHub; +namespace Core.Types.RanobeHub; -public class RanobeHubVolume { - [JsonPropertyName("chapters")] - public RanobeHubChapter[] Chapters { get; set; } -} \ No newline at end of file +public class RanobeHubVolume +{ + [JsonPropertyName("chapters")] public RanobeHubChapter[] Chapters { get; set; } +} diff --git a/Core/Types/RanobeNovels/RanobeNovelsChapter.cs b/Core/Types/RanobeNovels/RanobeNovelsChapter.cs index 4a3f32c0..f69cb4a5 100644 --- a/Core/Types/RanobeNovels/RanobeNovelsChapter.cs +++ b/Core/Types/RanobeNovels/RanobeNovelsChapter.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.RanobeNovels; +namespace Core.Types.RanobeNovels; -public class RanobeNovelsChapter { - [JsonPropertyName("post_name")] - public string Name { get; set; } - - [JsonPropertyName("post_title")] - public string Title { get; set; } -} \ No newline at end of file +public class RanobeNovelsChapter +{ + [JsonPropertyName("post_name")] public string Name { get; set; } + + [JsonPropertyName("post_title")] public string Title { get; set; } +} diff --git a/Core/Types/RanobeOvh/RanobeOvhBranch.cs b/Core/Types/RanobeOvh/RanobeOvhBranch.cs index 2e5293fc..f9c1a255 100644 --- a/Core/Types/RanobeOvh/RanobeOvhBranch.cs +++ b/Core/Types/RanobeOvh/RanobeOvhBranch.cs @@ -1,14 +1,12 @@ using System.Text.Json.Serialization; -namespace Core.Types.RanobeOvh; - -public class RanobeOvhBranch { - [JsonPropertyName("id")] - public string Id { get; set; } - - [JsonPropertyName("translators")] - public RanoveOvhTranslator[] Translators { get; set; } - - [JsonPropertyName("chaptersCount")] - public long ChaptersCount { get; set; } -} \ No newline at end of file +namespace Core.Types.RanobeOvh; + +public class RanobeOvhBranch +{ + [JsonPropertyName("id")] public string Id { get; set; } + + [JsonPropertyName("translators")] public RanoveOvhTranslator[] Translators { get; set; } + + [JsonPropertyName("chaptersCount")] public long ChaptersCount { get; set; } +} diff --git a/Core/Types/RanobeOvh/RanobeOvhChapterFull.cs b/Core/Types/RanobeOvh/RanobeOvhChapterFull.cs index b9090b69..3e4fd86a 100644 --- a/Core/Types/RanobeOvh/RanobeOvhChapterFull.cs +++ b/Core/Types/RanobeOvh/RanobeOvhChapterFull.cs @@ -2,63 +2,53 @@ using System.Text.Json.Nodes; using System.Text.Json.Serialization; -namespace Core.Types.RanobeOvh; - -public class RanobeOvhChapterFull { - [JsonPropertyName("pages")] - public RanobeOvhPage[] Pages { get; set; } - - [JsonPropertyName("content")] - public JsonNode Content { get; set; } +namespace Core.Types.RanobeOvh; + +public class RanobeOvhChapterFull +{ + [JsonPropertyName("pages")] public RanobeOvhPage[] Pages { get; set; } + + [JsonPropertyName("content")] public JsonNode Content { get; set; } } +public class RanobeOvhChapterShort +{ + [JsonPropertyName("id")] public string Id { get; set; } + + [JsonPropertyName("number")] public decimal Number { get; set; } + + [JsonPropertyName("volume")] public int? Volume { get; set; } + + [JsonPropertyName("title")] public string Title { get; set; } + + [JsonPropertyName("branchId")] public string BranchId { get; set; } + + public string FullName + { + get + { + if (!Volume.HasValue) return Title; -public class RanobeOvhChapterShort { - [JsonPropertyName("id")] - public string Id { get; set; } - - [JsonPropertyName("number")] - public decimal Number { get; set; } - - [JsonPropertyName("volume")] - public int? Volume { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("branchId")] - public string BranchId { get; set; } - - public string FullName { - get { - if (!Volume.HasValue) { - return Title; - } - var shortName = $"Том {Volume}. Глава {(int)Number}"; return string.IsNullOrWhiteSpace(Title) ? shortName : Title; } } } -public class RanobeOvhChapterContent { - [JsonPropertyName("type")] - public string Type { get; set; } - - [JsonPropertyName("text")] - public string Text { get; set; } +public class RanobeOvhChapterContent +{ + [JsonPropertyName("type")] public string Type { get; set; } + + [JsonPropertyName("text")] public string Text { get; set; } - [JsonPropertyName("marks")] - public RanobeOvhChapterMark[] Marks { get; set; } = []; + [JsonPropertyName("marks")] public RanobeOvhChapterMark[] Marks { get; set; } = []; - [JsonPropertyName("content")] - public RanobeOvhChapterContent[] Content { get; set; } = []; + [JsonPropertyName("content")] public RanobeOvhChapterContent[] Content { get; set; } = []; - [JsonPropertyName("attrs")] - public Dictionary<string, JsonNode> Attrs { get; set; } = new(); + [JsonPropertyName("attrs")] public Dictionary<string, JsonNode> Attrs { get; set; } = new(); } -public class RanobeOvhChapterMark { - [JsonPropertyName("type")] - public string Type { get; set; } +public class RanobeOvhChapterMark +{ + [JsonPropertyName("type")] public string Type { get; set; } } diff --git a/Core/Types/RanobeOvh/RanobeOvhManga.cs b/Core/Types/RanobeOvh/RanobeOvhManga.cs index 69c07496..dc8013ef 100644 --- a/Core/Types/RanobeOvh/RanobeOvhManga.cs +++ b/Core/Types/RanobeOvh/RanobeOvhManga.cs @@ -1,14 +1,12 @@ using System.Text.Json.Serialization; -namespace Core.Types.RanobeOvh; - -public class RanobeOvhManga { - [JsonPropertyName("name")] - public RanobeOvhMangaName Name { get; set; } - - [JsonPropertyName("poster")] - public string Poster { get; set; } - - [JsonPropertyName("description")] - public string Description { get; set; } -} \ No newline at end of file +namespace Core.Types.RanobeOvh; + +public class RanobeOvhManga +{ + [JsonPropertyName("name")] public RanobeOvhMangaName Name { get; set; } + + [JsonPropertyName("poster")] public string Poster { get; set; } + + [JsonPropertyName("description")] public string Description { get; set; } +} diff --git a/Core/Types/RanobeOvh/RanobeOvhMangaName.cs b/Core/Types/RanobeOvh/RanobeOvhMangaName.cs index b23a4f5e..6e3f66f6 100644 --- a/Core/Types/RanobeOvh/RanobeOvhMangaName.cs +++ b/Core/Types/RanobeOvh/RanobeOvhMangaName.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.RanobeOvh; +namespace Core.Types.RanobeOvh; -public class RanobeOvhMangaName { - [JsonPropertyName("ru")] - public string Ru { get; set; } -} \ No newline at end of file +public class RanobeOvhMangaName +{ + [JsonPropertyName("ru")] public string Ru { get; set; } +} diff --git a/Core/Types/RanobeOvh/RanobeOvhPage.cs b/Core/Types/RanobeOvh/RanobeOvhPage.cs index f743be1a..4bf91bad 100644 --- a/Core/Types/RanobeOvh/RanobeOvhPage.cs +++ b/Core/Types/RanobeOvh/RanobeOvhPage.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.RanobeOvh; +namespace Core.Types.RanobeOvh; -public class RanobeOvhPage { - [JsonPropertyName("id")] - public string Id { get; set; } +public class RanobeOvhPage +{ + [JsonPropertyName("id")] public string Id { get; set; } - [JsonPropertyName("image")] - public string Image { get; set; } -} \ No newline at end of file + [JsonPropertyName("image")] public string Image { get; set; } +} diff --git a/Core/Types/RanobeOvh/RanoveOvhTranslator.cs b/Core/Types/RanobeOvh/RanoveOvhTranslator.cs index 1fb9c9ac..4f2c853b 100644 --- a/Core/Types/RanobeOvh/RanoveOvhTranslator.cs +++ b/Core/Types/RanobeOvh/RanoveOvhTranslator.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.RanobeOvh; +namespace Core.Types.RanobeOvh; -public class RanoveOvhTranslator { - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("slug")] - public string Slug { get; set; } -} \ No newline at end of file +public class RanoveOvhTranslator +{ + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("slug")] public string Slug { get; set; } +} diff --git a/Core/Types/Ranobes/RanobesCookie.cs b/Core/Types/Ranobes/RanobesCookie.cs index 1749fa06..1603d3e8 100644 --- a/Core/Types/Ranobes/RanobesCookie.cs +++ b/Core/Types/Ranobes/RanobesCookie.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Ranobes; +namespace Core.Types.Ranobes; -public class RanobesCookie { - [JsonPropertyName("cookie")] - public string Cookie { get; set; } -} \ No newline at end of file +public class RanobesCookie +{ + [JsonPropertyName("cookie")] public string Cookie { get; set; } +} diff --git a/Core/Types/Renovels/RenovelsApiResponse.cs b/Core/Types/Renovels/RenovelsApiResponse.cs index bd9be845..5053e6ec 100644 --- a/Core/Types/Renovels/RenovelsApiResponse.cs +++ b/Core/Types/Renovels/RenovelsApiResponse.cs @@ -2,10 +2,9 @@ namespace Core.Types.Renovels; -public class RenovelsApiResponse<T> { - [JsonPropertyName("msg")] - public string Message { get; set; } - - [JsonPropertyName("content")] - public T Content { get; set; } -} \ No newline at end of file +public class RenovelsApiResponse<T> +{ + [JsonPropertyName("msg")] public string Message { get; set; } + + [JsonPropertyName("content")] public T Content { get; set; } +} diff --git a/Core/Types/Renovels/RenovelsAuthResponse.cs b/Core/Types/Renovels/RenovelsAuthResponse.cs index 6b9a31b5..e5157d59 100644 --- a/Core/Types/Renovels/RenovelsAuthResponse.cs +++ b/Core/Types/Renovels/RenovelsAuthResponse.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Renovels; +namespace Core.Types.Renovels; -public class RenovelsAuthResponse { - [JsonPropertyName("access_token")] - public string Token { get; set; } -} \ No newline at end of file +public class RenovelsAuthResponse +{ + [JsonPropertyName("access_token")] public string Token { get; set; } +} diff --git a/Core/Types/Renovels/RenovelsBranch.cs b/Core/Types/Renovels/RenovelsBranch.cs index b600b99b..350b6c3f 100644 --- a/Core/Types/Renovels/RenovelsBranch.cs +++ b/Core/Types/Renovels/RenovelsBranch.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.Renovels; +namespace Core.Types.Renovels; -public class RenovelsBranch { - [JsonPropertyName("id")] - public int Id { get; set; } - - [JsonPropertyName("publishers")] - public RenovelsPublisher[] Publishers { get; set; } -} \ No newline at end of file +public class RenovelsBranch +{ + [JsonPropertyName("id")] public int Id { get; set; } + + [JsonPropertyName("publishers")] public RenovelsPublisher[] Publishers { get; set; } +} diff --git a/Core/Types/Renovels/RenovelsChapter.cs b/Core/Types/Renovels/RenovelsChapter.cs index 00979bab..a0edf90c 100644 --- a/Core/Types/Renovels/RenovelsChapter.cs +++ b/Core/Types/Renovels/RenovelsChapter.cs @@ -1,41 +1,34 @@ using System.Text.Json.Nodes; using System.Text.Json.Serialization; -namespace Core.Types.Renovels; - -public class RenovelsChapter { - [JsonPropertyName("id")] - public int Id { get; set; } - - [JsonPropertyName("chapter")] - public string Chapter { get; set; } - - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("tome")] - public int Tome { get; set; } - - [JsonPropertyName("content")] - public string Content { get; set; } - - [JsonPropertyName("is_paid")] - public bool IsPaid { get; set; } - - [JsonPropertyName("is_bought")] - public bool? IsBought { get; set; } - - [JsonPropertyName("pages")] - public JsonArray Pages { get; set; } - - public string Title { - get { +namespace Core.Types.Renovels; + +public class RenovelsChapter +{ + [JsonPropertyName("id")] public int Id { get; set; } + + [JsonPropertyName("chapter")] public string Chapter { get; set; } + + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("tome")] public int Tome { get; set; } + + [JsonPropertyName("content")] public string Content { get; set; } + + [JsonPropertyName("is_paid")] public bool IsPaid { get; set; } + + [JsonPropertyName("is_bought")] public bool? IsBought { get; set; } + + [JsonPropertyName("pages")] public JsonArray Pages { get; set; } + + public string Title + { + get + { var name = $"Том {Tome}. Глава {Chapter}"; - if (string.IsNullOrWhiteSpace(Name)) { - return name; - } + if (string.IsNullOrWhiteSpace(Name)) return name; return name + $" - {Name}"; } } -} \ No newline at end of file +} diff --git a/Core/Types/Renovels/RenovelsContent.cs b/Core/Types/Renovels/RenovelsContent.cs index acbf6a39..4b49dbb0 100644 --- a/Core/Types/Renovels/RenovelsContent.cs +++ b/Core/Types/Renovels/RenovelsContent.cs @@ -1,24 +1,19 @@ using System.Collections.Generic; using System.Text.Json.Serialization; -namespace Core.Types.Renovels; +namespace Core.Types.Renovels; -public class RenovelsContent { - [JsonPropertyName("cover")] - public Dictionary<string, string> Cover { get; set; } - - [JsonPropertyName("main_name")] - public string MainName { get; set; } - - [JsonPropertyName("secondary_name")] - public string SecondaryName { get; set; } - - [JsonPropertyName("another_name")] - public string AnotherName { get; set; } - - [JsonPropertyName("description")] - public string Description { get; set; } +public class RenovelsContent +{ + [JsonPropertyName("cover")] public Dictionary<string, string> Cover { get; set; } - [JsonPropertyName("branches")] - public RenovelsBranch[] Branches { get; set; } -} \ No newline at end of file + [JsonPropertyName("main_name")] public string MainName { get; set; } + + [JsonPropertyName("secondary_name")] public string SecondaryName { get; set; } + + [JsonPropertyName("another_name")] public string AnotherName { get; set; } + + [JsonPropertyName("description")] public string Description { get; set; } + + [JsonPropertyName("branches")] public RenovelsBranch[] Branches { get; set; } +} diff --git a/Core/Types/Renovels/RenovelsPage.cs b/Core/Types/Renovels/RenovelsPage.cs index a0271496..eaa0c498 100644 --- a/Core/Types/Renovels/RenovelsPage.cs +++ b/Core/Types/Renovels/RenovelsPage.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Renovels; +namespace Core.Types.Renovels; -public class RenovelsPage { - [JsonPropertyName("link")] - public string Link { get; set; } -} \ No newline at end of file +public class RenovelsPage +{ + [JsonPropertyName("link")] public string Link { get; set; } +} diff --git a/Core/Types/Renovels/RenovelsPublisher.cs b/Core/Types/Renovels/RenovelsPublisher.cs index 431930df..dbb794f5 100644 --- a/Core/Types/Renovels/RenovelsPublisher.cs +++ b/Core/Types/Renovels/RenovelsPublisher.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.Renovels; +namespace Core.Types.Renovels; -public class RenovelsPublisher { - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("dir")] - public string Dir { get; set; } -} \ No newline at end of file +public class RenovelsPublisher +{ + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("dir")] public string Dir { get; set; } +} diff --git a/Core/Types/Renovels/RenovelsTocResponse.cs b/Core/Types/Renovels/RenovelsTocResponse.cs index ca3d56ed..d3b194f7 100644 --- a/Core/Types/Renovels/RenovelsTocResponse.cs +++ b/Core/Types/Renovels/RenovelsTocResponse.cs @@ -2,7 +2,7 @@ namespace Core.Types.Renovels; -public class RenovelsTocResponse { - [JsonPropertyName("results")] - public RenovelsChapter[] Results { get; set; } -} \ No newline at end of file +public class RenovelsTocResponse +{ + [JsonPropertyName("results")] public RenovelsChapter[] Results { get; set; } +} diff --git a/Core/Types/Rulate/RulateAuthResponse.cs b/Core/Types/Rulate/RulateAuthResponse.cs index 69191ae6..8d1c48c0 100644 --- a/Core/Types/Rulate/RulateAuthResponse.cs +++ b/Core/Types/Rulate/RulateAuthResponse.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Rulate; +namespace Core.Types.Rulate; -public class RulateAuthResponse { - [JsonPropertyName("error")] - public string Error { get; set; } -} \ No newline at end of file +public class RulateAuthResponse +{ + [JsonPropertyName("error")] public string Error { get; set; } +} diff --git a/Core/Types/Rulate/RulateChapter.cs b/Core/Types/Rulate/RulateChapter.cs index ed4bc160..ce64e866 100644 --- a/Core/Types/Rulate/RulateChapter.cs +++ b/Core/Types/Rulate/RulateChapter.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.Rulate; +namespace Core.Types.Rulate; -public class RulateChapter { - [JsonPropertyName("can_read")] - public bool CanRead { get; set; } - - [JsonPropertyName("content")] - public string Content { get; set; } -} \ No newline at end of file +public class RulateChapter +{ + [JsonPropertyName("can_read")] public bool CanRead { get; set; } + + [JsonPropertyName("content")] public string Content { get; set; } +} diff --git a/Core/Types/SocialLib/LibSocialToken.cs b/Core/Types/SocialLib/LibSocialToken.cs index 1b43c11f..01b67789 100644 --- a/Core/Types/SocialLib/LibSocialToken.cs +++ b/Core/Types/SocialLib/LibSocialToken.cs @@ -2,7 +2,7 @@ namespace Core.Types.SocialLib; -public class LibSocialToken { - [JsonPropertyName("access_token")] - public string AccessToken { get; set; } -} \ No newline at end of file +public class LibSocialToken +{ + [JsonPropertyName("access_token")] public string AccessToken { get; set; } +} diff --git a/Core/Types/SocialLib/RanobeLibBookDetail.cs b/Core/Types/SocialLib/RanobeLibBookDetail.cs index c2ea44ef..504a54a0 100644 --- a/Core/Types/SocialLib/RanobeLibBookDetail.cs +++ b/Core/Types/SocialLib/RanobeLibBookDetail.cs @@ -3,47 +3,41 @@ namespace Core.Types.SocialLib; -public class RanobeLibBookDetails { - [JsonPropertyName("data")] - public RLBDData Data { get; set; } +public class RanobeLibBookDetails +{ + [JsonPropertyName("data")] public RLBDData Data { get; set; } } -public class RLBDData { - public RLBDData(int? id) { +public class RLBDData +{ + public RLBDData(int? id) + { Id = id; } - [JsonPropertyName("id")] - public int? Id { get; set; } + [JsonPropertyName("id")] public int? Id { get; set; } - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("rus_name")] - public string RusName { get; set; } + [JsonPropertyName("name")] public string Name { get; set; } - [JsonPropertyName("slug_url")] - public string SlugUrl { get; set; } + [JsonPropertyName("rus_name")] public string RusName { get; set; } - [JsonPropertyName("cover")] - public RLBDCover Cover { get; set; } + [JsonPropertyName("slug_url")] public string SlugUrl { get; set; } - [JsonPropertyName("authors")] - public List<RLBDAuthor> Authors { get; set; } - - [JsonPropertyName("summary")] - public string Summary { get; set; } + [JsonPropertyName("cover")] public RLBDCover Cover { get; set; } + + [JsonPropertyName("authors")] public List<RLBDAuthor> Authors { get; set; } + + [JsonPropertyName("summary")] public string Summary { get; set; } } -public class RLBDCover { - [JsonPropertyName("default")] - public string Default { get; set; } +public class RLBDCover +{ + [JsonPropertyName("default")] public string Default { get; set; } } -public class RLBDAuthor { - [JsonPropertyName("name")] - public string Name { get; set; } +public class RLBDAuthor +{ + [JsonPropertyName("name")] public string Name { get; set; } - [JsonPropertyName("slug_url")] - public string SlugUrl { get; set; } -} \ No newline at end of file + [JsonPropertyName("slug_url")] public string SlugUrl { get; set; } +} diff --git a/Core/Types/SocialLib/SocialLibBookChapters.cs b/Core/Types/SocialLib/SocialLibBookChapters.cs index e91c2085..37394572 100644 --- a/Core/Types/SocialLib/SocialLibBookChapters.cs +++ b/Core/Types/SocialLib/SocialLibBookChapters.cs @@ -4,87 +4,77 @@ namespace Core.Types.SocialLib; -public class SocialLibBookChapters { - [JsonPropertyName("data")] - public List<SocialLibBookChapter> Chapters { get; set; } +public class SocialLibBookChapters +{ + [JsonPropertyName("data")] public List<SocialLibBookChapter> Chapters { get; set; } } -public class SocialLibBookChapterResponse { - [JsonPropertyName("data")] - public SocialLibBookChapter Data { get; set; } +public class SocialLibBookChapterResponse +{ + [JsonPropertyName("data")] public SocialLibBookChapter Data { get; set; } } -public class SocialLibChapterBranch { - [JsonPropertyName("branch_id")] - public long? BranchId { get; set; } +public class SocialLibChapterBranch +{ + [JsonPropertyName("branch_id")] public long? BranchId { get; set; } } -public class SocialLibChapterAttachment { - [JsonPropertyName("url")] - public string Url { get; set; } - - [JsonPropertyName("name")] - public string Name { get; set; } +public class SocialLibChapterAttachment +{ + [JsonPropertyName("url")] public string Url { get; set; } + + [JsonPropertyName("name")] public string Name { get; set; } } -public class SocialLibBookChapter { - [JsonPropertyName("volume")] - public string Volume { get; set; } - - [JsonPropertyName("number")] - public string Number { get; set; } - - [JsonPropertyName("branches")] - public List<SocialLibChapterBranch> Branches { get; set; } - - [JsonPropertyName("attachments")] - public SocialLibChapterAttachment[] Attachments { get; set; } = []; - - [JsonPropertyName("pages")] - public SocialLibPage[] Pages { get; set; } = []; - - [JsonPropertyName("content")] - public JsonNode Content { get; set; } - +public class SocialLibBookChapter +{ + [JsonPropertyName("volume")] public string Volume { get; set; } + + [JsonPropertyName("number")] public string Number { get; set; } + + [JsonPropertyName("branches")] public List<SocialLibChapterBranch> Branches { get; set; } + + [JsonPropertyName("attachments")] public SocialLibChapterAttachment[] Attachments { get; set; } = []; + + [JsonPropertyName("pages")] public SocialLibPage[] Pages { get; set; } = []; + + [JsonPropertyName("content")] public JsonNode Content { get; set; } + private string RawName { get; set; } [JsonPropertyName("name")] - public string Name { - get { + public string Name + { + get + { var name = $"Том {Volume} Глава {Number}"; - if (!string.IsNullOrWhiteSpace(RawName)) { - name += $" - {RawName}"; - } + if (!string.IsNullOrWhiteSpace(RawName)) name += $" - {RawName}"; return name; } - set { RawName = value; } + set => RawName = value; } } -public class SocialLibChapterContent { - [JsonPropertyName("type")] - public string Type { get; set; } - - [JsonPropertyName("text")] - public string Text { get; set; } +public class SocialLibChapterContent +{ + [JsonPropertyName("type")] public string Type { get; set; } - [JsonPropertyName("marks")] - public SocialLibChapterMark[] Marks { get; set; } = []; + [JsonPropertyName("text")] public string Text { get; set; } - [JsonPropertyName("content")] - public SocialLibChapterContent[] Content { get; set; } = []; + [JsonPropertyName("marks")] public SocialLibChapterMark[] Marks { get; set; } = []; - [JsonPropertyName("attrs")] - public Dictionary<string, JsonNode> Attrs { get; set; } = new(); + [JsonPropertyName("content")] public SocialLibChapterContent[] Content { get; set; } = []; + + [JsonPropertyName("attrs")] public Dictionary<string, JsonNode> Attrs { get; set; } = new(); } -public class SocialLibChapterMark { - [JsonPropertyName("type")] - public string Type { get; set; } +public class SocialLibChapterMark +{ + [JsonPropertyName("type")] public string Type { get; set; } } -public class SocialLibPage { - [JsonPropertyName("url")] - public string Url { get; set; } -} \ No newline at end of file +public class SocialLibPage +{ + [JsonPropertyName("url")] public string Url { get; set; } +} diff --git a/Core/Types/SocialLib/SocialLibChapter.cs b/Core/Types/SocialLib/SocialLibChapter.cs index 2ec35780..7473fe0a 100644 --- a/Core/Types/SocialLib/SocialLibChapter.cs +++ b/Core/Types/SocialLib/SocialLibChapter.cs @@ -1,22 +1,18 @@ using System.Text.Json.Serialization; -namespace Core.Types.SocialLib; +namespace Core.Types.SocialLib; -public class SocialLibChapter { - [JsonPropertyName("chapter_number")] - public string ChapterNumber { get; set; } +public class SocialLibChapter +{ + [JsonPropertyName("chapter_number")] public string ChapterNumber { get; set; } - [JsonPropertyName("chapter_volume")] - public int ChapterVolume { get; set; } + [JsonPropertyName("chapter_volume")] public int ChapterVolume { get; set; } - [JsonPropertyName("chapter_name")] - public string ChapterName { get; set; } - - [JsonPropertyName("chapter_slug")] - public string ChapterSlug { get; set; } - - [JsonPropertyName("branch_id")] - public int? BranchId { get; set; } + [JsonPropertyName("chapter_name")] public string ChapterName { get; set; } + + [JsonPropertyName("chapter_slug")] public string ChapterSlug { get; set; } + + [JsonPropertyName("branch_id")] public int? BranchId { get; set; } public string Name => $"Том {ChapterVolume} Глава {ChapterNumber} {ChapterName}".Trim(); -} \ No newline at end of file +} diff --git a/Core/Types/SocialLib/SocialLibChapterOld.cs b/Core/Types/SocialLib/SocialLibChapterOld.cs index b8b4ade2..039797af 100644 --- a/Core/Types/SocialLib/SocialLibChapterOld.cs +++ b/Core/Types/SocialLib/SocialLibChapterOld.cs @@ -1,21 +1,19 @@ using System.Text.Json.Serialization; -namespace Core.Types.SocialLib; +namespace Core.Types.SocialLib; -public class SocialLibChapterOld { - [JsonPropertyName("number")] - public string ChapterNumber { get; set; } +public class SocialLibChapterOld +{ + [JsonPropertyName("number")] public string ChapterNumber { get; set; } - [JsonPropertyName("volume")] - public string ChapterVolume { get; set; } + [JsonPropertyName("volume")] public string ChapterVolume { get; set; } - [JsonPropertyName("name")] - public string ChapterName { get; set; } - - [JsonPropertyName("slug")] - public string ChapterSlug { get; set; } + [JsonPropertyName("name")] public string ChapterName { get; set; } - public string GetName() { + [JsonPropertyName("slug")] public string ChapterSlug { get; set; } + + public string GetName() + { return $"Том {ChapterVolume} Глава {ChapterNumber} {ChapterName}".Trim(); } -} \ No newline at end of file +} diff --git a/Core/Types/SocialLib/SocialLibChapters.cs b/Core/Types/SocialLib/SocialLibChapters.cs index afb78616..238c6f6a 100644 --- a/Core/Types/SocialLib/SocialLibChapters.cs +++ b/Core/Types/SocialLib/SocialLibChapters.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; using System.Text.Json.Serialization; -namespace Core.Types.SocialLib; +namespace Core.Types.SocialLib; -public class SocialLibChapters { - [JsonPropertyName("list")] - public List<SocialLibChapter> List { get; set; } -} \ No newline at end of file +public class SocialLibChapters +{ + [JsonPropertyName("list")] public List<SocialLibChapter> List { get; set; } +} diff --git a/Core/Types/SocialLib/User.cs b/Core/Types/SocialLib/User.cs index eba2bd1d..5cc19a89 100644 --- a/Core/Types/SocialLib/User.cs +++ b/Core/Types/SocialLib/User.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.SocialLib; +namespace Core.Types.SocialLib; -public class User { - [JsonPropertyName("id")] - public long Id { get; set; } -} \ No newline at end of file +public class User +{ + [JsonPropertyName("id")] public long Id { get; set; } +} diff --git a/Core/Types/SocialLib/WindowData.cs b/Core/Types/SocialLib/WindowData.cs index f4fb0ce1..a6cc2dff 100644 --- a/Core/Types/SocialLib/WindowData.cs +++ b/Core/Types/SocialLib/WindowData.cs @@ -1,11 +1,10 @@ using System.Text.Json.Serialization; -namespace Core.Types.SocialLib; +namespace Core.Types.SocialLib; -public class WindowData { - [JsonPropertyName("chapters")] - public SocialLibChapters Chapters { get; set; } - - [JsonPropertyName("user")] - public User User { get; set; } -} \ No newline at end of file +public class WindowData +{ + [JsonPropertyName("chapters")] public SocialLibChapters Chapters { get; set; } + + [JsonPropertyName("user")] public User User { get; set; } +} diff --git a/Core/Types/StrokiMts/StrokiMtsApiMultiResponse.cs b/Core/Types/StrokiMts/StrokiMtsApiMultiResponse.cs index ab52c4fe..5c146992 100644 --- a/Core/Types/StrokiMts/StrokiMtsApiMultiResponse.cs +++ b/Core/Types/StrokiMts/StrokiMtsApiMultiResponse.cs @@ -3,7 +3,7 @@ namespace Core.Types.StrokiMts; -public class StrokiMtsApiMultiResponse { - [JsonPropertyName("items")] - public List<StrokiMtsMultiItem> Items { get; set; } -} \ No newline at end of file +public class StrokiMtsApiMultiResponse +{ + [JsonPropertyName("items")] public List<StrokiMtsMultiItem> Items { get; set; } +} diff --git a/Core/Types/StrokiMts/StrokiMtsApiResponse.cs b/Core/Types/StrokiMts/StrokiMtsApiResponse.cs index ed72493b..219c82fd 100644 --- a/Core/Types/StrokiMts/StrokiMtsApiResponse.cs +++ b/Core/Types/StrokiMts/StrokiMtsApiResponse.cs @@ -2,7 +2,7 @@ namespace Core.Types.StrokiMts; -public class StrokiMtsApiResponse<T> { - [JsonPropertyName("data")] - public T Data { get; set; } -} \ No newline at end of file +public class StrokiMtsApiResponse<T> +{ + [JsonPropertyName("data")] public T Data { get; set; } +} diff --git a/Core/Types/StrokiMts/StrokiMtsAuthor.cs b/Core/Types/StrokiMts/StrokiMtsAuthor.cs index 6ed3d3b6..3e91280d 100644 --- a/Core/Types/StrokiMts/StrokiMtsAuthor.cs +++ b/Core/Types/StrokiMts/StrokiMtsAuthor.cs @@ -2,10 +2,9 @@ namespace Core.Types.StrokiMts; -public class StrokiMtsAuthor { - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("friendlyUrl")] - public string FriendlyUrl { get; set; } -} \ No newline at end of file +public class StrokiMtsAuthor +{ + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("friendlyUrl")] public string FriendlyUrl { get; set; } +} diff --git a/Core/Types/StrokiMts/StrokiMtsBookItem.cs b/Core/Types/StrokiMts/StrokiMtsBookItem.cs index f3b5a107..8294daa8 100644 --- a/Core/Types/StrokiMts/StrokiMtsBookItem.cs +++ b/Core/Types/StrokiMts/StrokiMtsBookItem.cs @@ -3,22 +3,17 @@ namespace Core.Types.StrokiMts; -public class StrokiMtsBookItem { - [JsonPropertyName("id")] - public long Id { get; set; } - - [JsonPropertyName("annotation")] - public string Annotation { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("imageUrl")] - public Dictionary<string, string> ImageUrl { get; set; } - - [JsonPropertyName("authors")] - public List<StrokiMtsAuthor> Authors { get; set; } - - [JsonPropertyName("contentInfo")] - public StrokiMtsContentInfo ContentInfo { get; set; } -} \ No newline at end of file +public class StrokiMtsBookItem +{ + [JsonPropertyName("id")] public long Id { get; set; } + + [JsonPropertyName("annotation")] public string Annotation { get; set; } + + [JsonPropertyName("title")] public string Title { get; set; } + + [JsonPropertyName("imageUrl")] public Dictionary<string, string> ImageUrl { get; set; } + + [JsonPropertyName("authors")] public List<StrokiMtsAuthor> Authors { get; set; } + + [JsonPropertyName("contentInfo")] public StrokiMtsContentInfo ContentInfo { get; set; } +} diff --git a/Core/Types/StrokiMts/StrokiMtsContentInfo.cs b/Core/Types/StrokiMts/StrokiMtsContentInfo.cs index a8995103..b838d12f 100644 --- a/Core/Types/StrokiMts/StrokiMtsContentInfo.cs +++ b/Core/Types/StrokiMts/StrokiMtsContentInfo.cs @@ -2,7 +2,7 @@ namespace Core.Types.StrokiMts; -public class StrokiMtsContentInfo { - [JsonPropertyName("fileType")] - public string FileType { get; set; } -} \ No newline at end of file +public class StrokiMtsContentInfo +{ + [JsonPropertyName("fileType")] public string FileType { get; set; } +} diff --git a/Core/Types/StrokiMts/StrokiMtsFile.cs b/Core/Types/StrokiMts/StrokiMtsFile.cs index 717b912f..f2da4ce2 100644 --- a/Core/Types/StrokiMts/StrokiMtsFile.cs +++ b/Core/Types/StrokiMts/StrokiMtsFile.cs @@ -2,7 +2,7 @@ namespace Core.Types.StrokiMts; -public class StrokiMtsFile { - [JsonPropertyName("fileId")] - public long FileId { get; set; } -} \ No newline at end of file +public class StrokiMtsFile +{ + [JsonPropertyName("fileId")] public long FileId { get; set; } +} diff --git a/Core/Types/StrokiMts/StrokiMtsFileUrl.cs b/Core/Types/StrokiMts/StrokiMtsFileUrl.cs index 26956eda..b111fca1 100644 --- a/Core/Types/StrokiMts/StrokiMtsFileUrl.cs +++ b/Core/Types/StrokiMts/StrokiMtsFileUrl.cs @@ -2,7 +2,7 @@ namespace Core.Types.StrokiMts; -public class StrokiMtsFileUrl { - [JsonPropertyName("url")] - public string Url { get; set; } -} \ No newline at end of file +public class StrokiMtsFileUrl +{ + [JsonPropertyName("url")] public string Url { get; set; } +} diff --git a/Core/Types/StrokiMts/StrokiMtsFiles.cs b/Core/Types/StrokiMts/StrokiMtsFiles.cs index 639a42c2..01fa6236 100644 --- a/Core/Types/StrokiMts/StrokiMtsFiles.cs +++ b/Core/Types/StrokiMts/StrokiMtsFiles.cs @@ -2,10 +2,9 @@ namespace Core.Types.StrokiMts; -public class StrokiMtsFiles { - [JsonPropertyName("preview")] - public StrokiMtsFile Preview { get; set; } - - [JsonPropertyName("full")] - public StrokiMtsFile[] Full { get; set; } -} \ No newline at end of file +public class StrokiMtsFiles +{ + [JsonPropertyName("preview")] public StrokiMtsFile Preview { get; set; } + + [JsonPropertyName("full")] public StrokiMtsFile[] Full { get; set; } +} diff --git a/Core/Types/StrokiMts/StrokiMtsMultiItem.cs b/Core/Types/StrokiMts/StrokiMtsMultiItem.cs index 43822a27..5d2502a8 100644 --- a/Core/Types/StrokiMts/StrokiMtsMultiItem.cs +++ b/Core/Types/StrokiMts/StrokiMtsMultiItem.cs @@ -2,16 +2,13 @@ namespace Core.Types.StrokiMts; -public class StrokiMtsMultiItem { - [JsonPropertyName("textBook")] - public StrokiMtsBookItem TextBook { get; set; } - - [JsonPropertyName("audioBook")] - public StrokiMtsBookItem AudioBook { get; set; } - - [JsonPropertyName("comics")] - public StrokiMtsBookItem ComicBook { get; set; } - - [JsonPropertyName("pressa")] - public StrokiMtsBookItem PressBook { get; set; } -} \ No newline at end of file +public class StrokiMtsMultiItem +{ + [JsonPropertyName("textBook")] public StrokiMtsBookItem TextBook { get; set; } + + [JsonPropertyName("audioBook")] public StrokiMtsBookItem AudioBook { get; set; } + + [JsonPropertyName("comics")] public StrokiMtsBookItem ComicBook { get; set; } + + [JsonPropertyName("pressa")] public StrokiMtsBookItem PressBook { get; set; } +} diff --git a/Core/Types/Wattpad/WattpadInfo.cs b/Core/Types/Wattpad/WattpadInfo.cs index 328fe19f..1ae50bf8 100644 --- a/Core/Types/Wattpad/WattpadInfo.cs +++ b/Core/Types/Wattpad/WattpadInfo.cs @@ -1,20 +1,16 @@ using System.Text.Json.Serialization; -namespace Core.Types.Wattpad; +namespace Core.Types.Wattpad; -public class WattpadInfo { - [JsonPropertyName("user")] - public WattpadUser User { get; set; } - - [JsonPropertyName("cover")] - public string Cover { get; set; } - - [JsonPropertyName("parts")] - public WattpadPart[] Parts { get; set; } +public class WattpadInfo +{ + [JsonPropertyName("user")] public WattpadUser User { get; set; } - [JsonPropertyName("description")] - public string Description { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } -} \ No newline at end of file + [JsonPropertyName("cover")] public string Cover { get; set; } + + [JsonPropertyName("parts")] public WattpadPart[] Parts { get; set; } + + [JsonPropertyName("description")] public string Description { get; set; } + + [JsonPropertyName("title")] public string Title { get; set; } +} diff --git a/Core/Types/Wattpad/WattpadMeta.cs b/Core/Types/Wattpad/WattpadMeta.cs index f948dc7d..13f106f6 100644 --- a/Core/Types/Wattpad/WattpadMeta.cs +++ b/Core/Types/Wattpad/WattpadMeta.cs @@ -1,5 +1,6 @@ -namespace Core.Types.Wattpad; +namespace Core.Types.Wattpad; -public class WattpadMeta { +public class WattpadMeta +{ public string StoryId { get; set; } -} \ No newline at end of file +} diff --git a/Core/Types/Wattpad/WattpadPart.cs b/Core/Types/Wattpad/WattpadPart.cs index e9995f32..1143d8e7 100644 --- a/Core/Types/Wattpad/WattpadPart.cs +++ b/Core/Types/Wattpad/WattpadPart.cs @@ -2,16 +2,15 @@ using System.Text.Json.Serialization; using Core.Extensions; -namespace Core.Types.Wattpad; +namespace Core.Types.Wattpad; -public class WattpadPart { - [JsonPropertyName("id")] - public long Id { get; set; } - - [JsonPropertyName("title")] - public string Title { private get; set; } +public class WattpadPart +{ + [JsonPropertyName("id")] public long Id { get; set; } + + [JsonPropertyName("title")] public string Title { private get; set; } public string FullName => Title.Replace("\r", " ").CollapseWhitespace().Trim(); public Uri Url => new($"https://www.wattpad.com/apiv2/storytext?id={Id}"); -} \ No newline at end of file +} diff --git a/Core/Types/Wattpad/WattpadUser.cs b/Core/Types/Wattpad/WattpadUser.cs index 5743a991..f5990de3 100644 --- a/Core/Types/Wattpad/WattpadUser.cs +++ b/Core/Types/Wattpad/WattpadUser.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Wattpad; +namespace Core.Types.Wattpad; -public class WattpadUser { - [JsonPropertyName("name")] - public string Name { get; set; } -} \ No newline at end of file +public class WattpadUser +{ + [JsonPropertyName("name")] public string Name { get; set; } +} diff --git a/Core/Types/WuxiaWorld/WuxiaWorldToc.cs b/Core/Types/WuxiaWorld/WuxiaWorldToc.cs index a8e7f56a..c814037c 100644 --- a/Core/Types/WuxiaWorld/WuxiaWorldToc.cs +++ b/Core/Types/WuxiaWorld/WuxiaWorldToc.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.WuxiaWorld; +namespace Core.Types.WuxiaWorld; -public class WuxiaWorldToc { - [JsonPropertyName("post_name")] - public string PostName { get; set; } -} \ No newline at end of file +public class WuxiaWorldToc +{ + [JsonPropertyName("post_name")] public string PostName { get; set; } +} diff --git a/Elib2EbookCli/ConsoleLogger.cs b/Elib2EbookCli/ConsoleLogger.cs index 40511993..ad792d67 100644 --- a/Elib2EbookCli/ConsoleLogger.cs +++ b/Elib2EbookCli/ConsoleLogger.cs @@ -2,14 +2,21 @@ namespace Elib2EbookCli; -public class ConsoleLogger : ILogger { - public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) { +public class ConsoleLogger : ILogger +{ + public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, + Func<TState, Exception?, string> formatter) + { Console.WriteLine($"{formatter(state, exception)}"); } - bool ILogger.IsEnabled(LogLevel logLevel) { + bool ILogger.IsEnabled(LogLevel logLevel) + { return true; } - public IDisposable? BeginScope<TState>(TState state) where TState : notnull => default!; -} \ No newline at end of file + public IDisposable? BeginScope<TState>(TState state) where TState : notnull + { + return default!; + } +} diff --git a/Elib2EbookCli/Elib2EbookCli.csproj b/Elib2EbookCli/Elib2EbookCli.csproj index c1d7ae4a..d8a2dce9 100644 --- a/Elib2EbookCli/Elib2EbookCli.csproj +++ b/Elib2EbookCli/Elib2EbookCli.csproj @@ -1,8 +1,8 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> diff --git a/Elib2EbookCli/Program.cs b/Elib2EbookCli/Program.cs index 9d748af9..70912c76 100644 --- a/Elib2EbookCli/Program.cs +++ b/Elib2EbookCli/Program.cs @@ -1,4 +1,4 @@ -using System.Reflection; +using System.Reflection; using System.Text; using CommandLine; using CommandLine.Text; @@ -10,69 +10,78 @@ namespace Elib2EbookCli; -internal static class Program { - - private static async Task Main(string[] args) { +internal static class Program +{ + private static async Task Main(string[] args) + { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); Console.OutputEncoding = Encoding.UTF8; - + var logger = new ConsoleLogger(); - var parserResult = new Parser(c => { - c.CaseInsensitiveEnumValues = true; - }).ParseArguments<Options>(args); - - + var parserResult = new Parser(c => { c.CaseInsensitiveEnumValues = true; }).ParseArguments<Options>(args); + await parserResult - .WithNotParsed(errs => { + .WithNotParsed(errs => + { var title = AppDomain.CurrentDomain.FriendlyName; - var coreAssembly = typeof(BookGetterConfig).Assembly; + var coreAssembly = typeof(BookGetterConfig).Assembly; var version = coreAssembly.GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version ?? string.Empty; var heading = new HeadingInfo(title, version); - if (errs.IsVersion()) { + if (errs.IsVersion()) + { logger.LogInformation(heading); - } else { - var helpText = HelpText.AutoBuild(parserResult, h => { + } + else + { + var helpText = HelpText.AutoBuild(parserResult, h => + { h.Heading = heading; h.Copyright = string.Empty; return h; }); - + logger.LogInformation(helpText); } }) - .WithParsedAsync(async options => { - using var getterConfig = BookGetterConfig.GetDefault(options, logger); + .WithParsedAsync(async options => + { + using var getterConfig = BookGetterConfig.GetDefault(options, logger); using var getter = GetterProvider.Get(getterConfig, options.Url.First().AsUri()); - try { + try + { await getter.Init(); await getter.Authorize(); - } catch (Exception ex) { - logger.LogInformation($"Ошибка авторизации или инициализации. {ex.Message} \n {ex.ToString()}"); + } + catch (Exception ex) + { + logger.LogInformation($"Ошибка авторизации или инициализации. {ex.Message} \n {ex}"); return; } - foreach (var url in options.Url) { + foreach (var url in options.Url) + { logger.LogInformation($"Начинаю генерацию книги {url.CoverQuotes()}"); - try { + try + { var book = await getter.Get(url.AsUri()); - foreach (var format in options.Format) { + foreach (var format in options.Format) await BuilderProvider.Get(format, options, logger).Build(book); - } - if (options.Additional) { - await new AdditionaFileBuilder(options, logger).Build(book); - } + if (options.Additional) await new AdditionaFileBuilder(options, logger).Build(book); - if (!options.SaveTemp) { - book.Dispose(); - } - } catch (TaskCanceledException) { - logger.LogInformation("Сервер не успевает ответить. Попробуйте увеличить Timeout с помощью параметра -t"); - } catch (Exception ex) { + if (!options.SaveTemp) book.Dispose(); + } + catch (TaskCanceledException) + { + logger.LogInformation( + "Сервер не успевает ответить. Попробуйте увеличить Timeout с помощью параметра -t"); + } + catch (Exception ex) + { logger.LogInformation($"Генерация книги {url} завершилась с ошибкой. {ex}"); } } diff --git a/Elib2EbookWeb/Components/App.razor b/Elib2EbookWeb/Components/App.razor index 6bd64bf6..cf995804 100644 --- a/Elib2EbookWeb/Components/App.razor +++ b/Elib2EbookWeb/Components/App.razor @@ -2,17 +2,17 @@ <html lang="en"> <head> - <meta charset="utf-8"/> - <meta name="viewport" content="width=device-width, initial-scale=1.0"/> - <base href="/"/> - <link rel="manifest" href="manifest.json" /> - <link rel="stylesheet" href="bootstrap/bootstrap.min.css"/> - <link rel="stylesheet" href="app.css"/> - <link rel="stylesheet" href="Elib2EbookWeb.styles.css"/> - <link rel="icon" type="image/png" href="favicon.png"/> - <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" /> - <link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" /> - <HeadOutlet/> + <meta charset="utf-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1.0"/> + <base href="/"/> + <link rel="manifest" href="manifest.json"/> + <link rel="stylesheet" href="bootstrap/bootstrap.min.css"/> + <link rel="stylesheet" href="app.css"/> + <link rel="stylesheet" href="Elib2EbookWeb.styles.css"/> + <link rel="icon" type="image/png" href="favicon.png"/> + <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet"/> + <link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet"/> + <HeadOutlet/> </head> <body> @@ -37,4 +37,4 @@ </body> -</html> \ No newline at end of file +</html> diff --git a/Elib2EbookWeb/Components/BookForm.razor b/Elib2EbookWeb/Components/BookForm.razor index a438ee70..2c7f20d3 100644 --- a/Elib2EbookWeb/Components/BookForm.razor +++ b/Elib2EbookWeb/Components/BookForm.razor @@ -11,23 +11,25 @@ <MudStack> <MudToggleGroup T="string" SelectionMode="SelectionMode.MultiSelection" Color="Color.Tertiary" CheckMark @bind-Values="_options.Format"> <MudToggleItem Value="@("epub")" UnselectedIcon="@Icons.Material.Filled.CheckBoxOutlineBlank" SelectedIcon="@Icons.Material.Filled.CheckBox"/> - <MudToggleItem Value="@("fb2")" UnselectedIcon="@Icons.Material.Filled.CheckBoxOutlineBlank" SelectedIcon="@Icons.Material.Filled.CheckBox" /> - <MudToggleItem Value="@("cbz")" UnselectedIcon="@Icons.Material.Filled.CheckBoxOutlineBlank" SelectedIcon="@Icons.Material.Filled.CheckBox" /> - <MudToggleItem Value="@("txt")" UnselectedIcon="@Icons.Material.Filled.CheckBoxOutlineBlank" SelectedIcon="@Icons.Material.Filled.CheckBox" /> + <MudToggleItem Value="@("fb2")" UnselectedIcon="@Icons.Material.Filled.CheckBoxOutlineBlank" SelectedIcon="@Icons.Material.Filled.CheckBox"/> + <MudToggleItem Value="@("cbz")" UnselectedIcon="@Icons.Material.Filled.CheckBoxOutlineBlank" SelectedIcon="@Icons.Material.Filled.CheckBox"/> + <MudToggleItem Value="@("txt")" UnselectedIcon="@Icons.Material.Filled.CheckBoxOutlineBlank" SelectedIcon="@Icons.Material.Filled.CheckBox"/> </MudToggleGroup> </MudStack> - <MudTextField @bind-Value="@_options.Login" Label="Логин" Variant="Variant.Outlined"/> - <MudTextField @bind-Value="@_options.Password" Label="Пароль" Variant="Variant.Outlined" InputType="@_passwordInput" Adornment="Adornment.End" AdornmentIcon="@_passwordInputIcon" OnAdornmentClick="ShowPassword" AdornmentAriaLabel="Показать пароль" /> - - - @if (_startEndCSwitcher) { + <MudTextField @bind-Value="@_options.Password" Label="Пароль" Variant="Variant.Outlined" InputType="@_passwordInput" Adornment="Adornment.End" AdornmentIcon="@_passwordInputIcon" OnAdornmentClick="ShowPassword" AdornmentAriaLabel="Показать пароль"/> + <MudTextField T="string" @bind-Value="_customOptionsText" Label="Дополнительные параметры (Ключ=Значение, каждый с новой строки)" Variant="Variant.Outlined" Lines="3"/> + + @if (_startEndCSwitcher) + { <div class="d-flex gap-x-4"> <MudTextField T="string" Label="Глава от" Variant="Variant.Outlined" @bind-Value="@_options.StartName"/> <MudTextField T="string" Label="Глава до" Variant="Variant.Outlined" @bind-Value="@_options.EndName"/> </div> - } else { + } + else + { <div HtmlTag="div" Class="d-flex gap-x-4"> <MudTextField T="int?" Label="Глава от" Variant="Variant.Outlined" @bind-Value="@_options.Start"/> <MudTextField T="int?" Label="Глава до" Variant="Variant.Outlined" @bind-Value="@_options.End"/> @@ -42,17 +44,24 @@ </MudPaper> <MudPaper Class="pa-4 mt-4"> - @if (_onGenerate) { + @if (_onGenerate) + { <MudButton Variant="Variant.Filled" Color="Color.Info" DropShadow="false" Disabled="true" FullWidth="true" Size="Size.Large"> - @if (_onGenerate) { - @if (!string.IsNullOrWhiteSpace(_log)) { + @if (_onGenerate) + { + @if (!string.IsNullOrWhiteSpace(_log)) + { <MudText Class="text-truncate">@_log</MudText> - } else { + } + else + { <MudProgressCircular Class="ms-n1" Size="Size.Small" Indeterminate="true"/> } } </MudButton> - } else { + } + else + { <MudButtonGroup Color="Color.Info" Variant="Variant.Filled" Size="Size.Large" FullWidth="true"> <MudButton OnClick="WithImages" Disabled="_onGenerate" FullWidth="true"> <MudText>Скачать</MudText> @@ -66,68 +75,86 @@ @code { private readonly Options _options = new(); + private string _customOptionsText = string.Empty; private ActionLogger? _logger; private bool _onGenerate; private string? _log; private bool _startEndCSwitcher; - + private bool _isShow; private InputType _passwordInput = InputType.Password; private string _passwordInputIcon = Icons.Material.Filled.VisibilityOff; - [Parameter] - public required Func<Options, ILogger, Task> OnClick { get; set; } + [Parameter] public required Func<Options, ILogger, Task> OnClick { get; set; } - void ShowPassword() { - if (_isShow) { + void ShowPassword() + { + if (_isShow) + { _isShow = false; _passwordInputIcon = Icons.Material.Filled.VisibilityOff; _passwordInput = InputType.Password; - } else { + } + else + { _isShow = true; _passwordInputIcon = Icons.Material.Filled.Visibility; _passwordInput = InputType.Text; } } - private async Task WithImages() { + private async Task WithImages() + { _options.NoImage = false; await Download(); } - - private async Task NoImages() { + + private async Task NoImages() + { _options.NoImage = true; await Download(); } - private async Task Download() { - if (_options.Url == null || !_options.Url.Any() || string.IsNullOrWhiteSpace(_options.Url.First())) { + private async Task Download() + { + if (_options.Url == null || !_options.Url.Any() || string.IsNullOrWhiteSpace(_options.Url.First())) + { Snackbar.Add("Укажите ссылку на книгу", Severity.Error); return; } - - if (_options.Format == null || !_options.Format.Any()) { + + if (_options.Format == null || !_options.Format.Any()) + { Snackbar.Add("Выберите формат", Severity.Error); return; } - + + _options.CustomOptions = string.IsNullOrWhiteSpace(_customOptionsText) + ? Array.Empty<string>() + : _customOptionsText.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Trim()) + .Where(line => !string.IsNullOrWhiteSpace(line)) + .ToList(); + _onGenerate = true; - _logger = new ActionLogger(str => { + _logger = new ActionLogger(str => + { _log = str.Crop(300); InvokeAsync(StateHasChanged); }); - + await OnClick(_options, _logger); _onGenerate = false; } - private void OnSwitch(bool obj) { + private void OnSwitch(bool obj) + { _startEndCSwitcher = obj; - + _options.Start = default; _options.End = default; _options.StartName = default; _options.EndName = default; } - -} \ No newline at end of file + +} diff --git a/Elib2EbookWeb/Components/Layout/MainLayout.razor b/Elib2EbookWeb/Components/Layout/MainLayout.razor index 52b2136f..2e0f862d 100644 --- a/Elib2EbookWeb/Components/Layout/MainLayout.razor +++ b/Elib2EbookWeb/Components/Layout/MainLayout.razor @@ -1,20 +1,19 @@ @inherits LayoutComponentBase -<MudThemeProvider @rendermode="InteractiveServer" /> -<MudPopoverProvider @rendermode="InteractiveServer" /> -<MudSnackbarProvider @rendermode="InteractiveServer" /> - +<MudThemeProvider @rendermode="InteractiveServer"/> +<MudPopoverProvider @rendermode="InteractiveServer"/> +<MudSnackbarProvider @rendermode="InteractiveServer"/> <div class="page"> <main> <MudAppBar Color="Color.Primary" Fixed="false"> <MudText Typo="Typo.h5" Class="ml-3">Elib2Ebook</MudText> - <MudSpacer /> - <PWA /> + <MudSpacer/> + <PWA/> <MudSpacer/> <MudIconButton Icon="@Icons.Custom.Brands.GitHub" Color="Color.Inherit" Href="https://github.com/OnlyFart/Elib2Ebook" Target="_blank"/> </MudAppBar> - + <article class="content px-4"> @Body </article> @@ -25,4 +24,4 @@ An unhandled error has occurred. <a href="" class="reload">Reload</a> <a class="dismiss">🗙</a> -</div> \ No newline at end of file +</div> diff --git a/Elib2EbookWeb/Components/PWA.razor b/Elib2EbookWeb/Components/PWA.razor index 9ee3d191..a074810d 100644 --- a/Elib2EbookWeb/Components/PWA.razor +++ b/Elib2EbookWeb/Components/PWA.razor @@ -1,4 +1,4 @@ -<MudButton Variant="Variant.Filled" Color="Color.Tertiary" DropShadow="false" Size="Size.Small" UserAttributes="@(new Dictionary<string, object?> { {"hidden", "" }, {"data-pwa", ""} })">Установить приложение</MudButton> +<MudButton Variant="Variant.Filled" Color="Color.Tertiary" DropShadow="false" Size="Size.Small" UserAttributes="@(new Dictionary<string, object?> { { "hidden", "" }, { "data-pwa", "" } })">Установить приложение</MudButton> <script> let installPrompt = null; @@ -19,4 +19,4 @@ installPrompt = null; installButton.setAttribute("hidden", ""); }); -</script> \ No newline at end of file +</script> diff --git a/Elib2EbookWeb/Components/Pages/Error.razor b/Elib2EbookWeb/Components/Pages/Error.razor index 86dea8a6..2199bfd7 100644 --- a/Elib2EbookWeb/Components/Pages/Error.razor +++ b/Elib2EbookWeb/Components/Pages/Error.razor @@ -6,7 +6,8 @@ <h1 class="text-danger">Error.</h1> <h2 class="text-danger">An error occurred while processing your request.</h2> -@if (ShowRequestId) { +@if (ShowRequestId) +{ <p> <strong>Request ID:</strong> <code>@RequestId</code> </p> @@ -25,11 +26,14 @@ @code{ - [CascadingParameter] - private HttpContext? HttpContext { get; set; } + [CascadingParameter] private HttpContext? HttpContext { get; set; } private string? RequestId { get; set; } private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); - protected override void OnInitialized() => RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; -} \ No newline at end of file + protected override void OnInitialized() + { + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; + } + +} diff --git a/Elib2EbookWeb/Components/Pages/Home.razor b/Elib2EbookWeb/Components/Pages/Home.razor index 6ec407ca..dac4fb24 100644 --- a/Elib2EbookWeb/Components/Pages/Home.razor +++ b/Elib2EbookWeb/Components/Pages/Home.razor @@ -16,7 +16,8 @@ </MudItem> </MudGrid> -@if (_shortBook != default) { +@if (_shortBook != default) +{ <MudGrid Justify="Justify.Center" class="mt-2"> <MudItem xs="12" sm="12" md="10" lg="6" xl="4"> <ShortBook Book="_shortBook"/> @@ -24,25 +25,28 @@ </MudGrid> } - @code { private Book? _shortBook; - - private async Task OnClick(Options options, ILogger logger) { + + private async Task OnClick(Options options, ILogger logger) + { using var tempSave = TempFolderFactory.Create("Books_" + Guid.NewGuid()); options.Timeout = 10; options.SavePath = tempSave.Path; - try { + try + { using var getterConfig = BookGetterConfig.GetDefault(options, logger); using var getter = GetterProvider.Get(getterConfig, options.Url.First().AsUri()); - + await getter.Init(); await getter.Authorize(); - foreach (var url in options.Url) { - try { + foreach (var url in options.Url) + { + try + { getterConfig.Options.NoChapters = true; _shortBook = await getter.Get(url.AsUri()); @@ -50,29 +54,39 @@ getterConfig.Options.NoChapters = false; using var book = await getter.Get(url.AsUri()); - foreach (var format in options.Format) { + foreach (var format in options.Format) + { await BuilderProvider.Get(format, options, logger).Build(book); } - + logger.LogInformation("Ожидайте. Сейчас начнется скачивание"); - foreach (var fileName in Directory.GetFiles(tempSave.Path)) { + foreach (var fileName in Directory.GetFiles(tempSave.Path)) + { await using var file = File.OpenRead(fileName); using var streamRef = new DotNetStreamReference(file); await JsRuntime.InvokeVoidAsync("downloadFileFromStream", Path.GetFileName(file.Name), streamRef); } Snackbar.Add($"Генерация книги {GetBookName(url, _shortBook).CoverQuotes()} завершена", Severity.Success); - } catch (Exception ex) { + } + catch (Exception ex) + { logger.LogInformation($"Генерация книги {GetBookName(url, _shortBook).CoverQuotes()} завершилась с ошибкой. {ex}"); Snackbar.Add($"Генерация книги {GetBookName(url, _shortBook).CoverQuotes()} завершилась с ошибкой. {ex.Message}", Severity.Error); } } - } catch (Exception ex) { + } + catch (Exception ex) + { logger.LogInformation(ex.Message); Snackbar.Add(ex.Message, Severity.Error); } } - private string GetBookName(string url, Book? book) => book?.Title ?? url; -} \ No newline at end of file + private string GetBookName(string url, Book? book) + { + return book?.Title ?? url; + } + +} diff --git a/Elib2EbookWeb/Components/Routes.razor b/Elib2EbookWeb/Components/Routes.razor index ae94e9e9..14eab61b 100644 --- a/Elib2EbookWeb/Components/Routes.razor +++ b/Elib2EbookWeb/Components/Routes.razor @@ -1,6 +1,7 @@ -<Router AppAssembly="typeof(Program).Assembly"> +@using Elib2EbookWeb.Components.Layout +<Router AppAssembly="typeof(Program).Assembly"> <Found Context="routeData"> - <RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)"/> + <RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)"/> <FocusOnNavigate RouteData="routeData" Selector="h1"/> </Found> -</Router> \ No newline at end of file +</Router> diff --git a/Elib2EbookWeb/Components/ShortBook.razor b/Elib2EbookWeb/Components/ShortBook.razor index d0362ba3..9c01d33d 100644 --- a/Elib2EbookWeb/Components/ShortBook.razor +++ b/Elib2EbookWeb/Components/ShortBook.razor @@ -1,5 +1,4 @@ @using Core.Types.Book - <MudCard> <MudGrid Justify="Justify.Center"> <MudItem sm="5" md="5" xl="5"> @@ -7,25 +6,36 @@ </MudItem> <MudItem sm="7" md="7" xl="7"> <MudCardContent> - <MudLink Href="@Book.Url.ToString()" Target="_blank"><MudText Typo="Typo.h5">@Book.Title</MudText></MudLink> - - @if (Book.Author != default) { - @if (Book.Author.Url == default) { + <MudLink Href="@Book.Url.ToString()" Target="_blank"> + <MudText Typo="Typo.h5">@Book.Title</MudText> + </MudLink> + + @if (Book.Author != default) + { + @if (Book.Author.Url == default) + { <MudText Typo="Typo.subtitle2">Автор: @Book.Author.Name</MudText> - } else { + } + else + { <MudText Typo="Typo.subtitle2">Автор: <MudLink Href="@Book.Author.Url.ToString()" Target="_blank">@Book.Author.Name</MudLink></MudText> } } - @if (Book.Seria != default) { - @if (Book.Seria?.Url == default) { + @if (Book.Seria != default) + { + @if (Book.Seria?.Url == default) + { <MudText Typo="Typo.subtitle2">Серия: @Seria</MudText> - } else { + } + else + { <MudText Typo="Typo.subtitle2">Серия: <MudLink Href="@Book.Seria.Url.ToString()" Target="_blank">@Seria</MudLink></MudText> } } - @if (!string.IsNullOrWhiteSpace(Book.Annotation)) { + @if (!string.IsNullOrWhiteSpace(Book.Annotation)) + { <MudElement HtmlTag="pre" Style="white-space: pre-line">@((MarkupString)Book.Annotation)</MudElement> } </MudCardContent> @@ -34,8 +44,7 @@ </MudCard> @code { - [Parameter] - public required Book Book { get; set; } + [Parameter] public required Book Book { get; set; } private string? _cover; @@ -43,21 +52,26 @@ private string Seria => Book.Seria?.Name + (string.IsNullOrWhiteSpace(Book.Seria?.Number) ? string.Empty : $" #{Book.Seria.Number}"); - protected override Task OnInitializedAsync() { + protected override Task OnInitializedAsync() + { InitCover(); return base.OnInitializedAsync(); } - protected override Task OnParametersSetAsync() { - if (Book.Cover?.FilePath != _coverPath) { + protected override Task OnParametersSetAsync() + { + if (Book.Cover?.FilePath != _coverPath) + { InitCover(); } - + return base.OnParametersSetAsync(); } - private void InitCover() { + private void InitCover() + { _cover = Book.Cover != default && Book.Cover.Content.Length > 0 ? $"data:image/png;base64, {Convert.ToBase64String(Book.Cover.Content)}" : _cover; _coverPath = Book.Cover?.FilePath; } -} \ No newline at end of file + +} diff --git a/Elib2EbookWeb/Components/_Imports.razor b/Elib2EbookWeb/Components/_Imports.razor index ab21d4f2..a4fed926 100644 --- a/Elib2EbookWeb/Components/_Imports.razor +++ b/Elib2EbookWeb/Components/_Imports.razor @@ -8,4 +8,4 @@ @using Microsoft.JSInterop @using Elib2EbookWeb @using Elib2EbookWeb.Components -@using MudBlazor \ No newline at end of file +@using MudBlazor diff --git a/Elib2EbookWeb/Elib2EbookWeb.csproj b/Elib2EbookWeb/Elib2EbookWeb.csproj index 11ea8951..07cf4f34 100644 --- a/Elib2EbookWeb/Elib2EbookWeb.csproj +++ b/Elib2EbookWeb/Elib2EbookWeb.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> diff --git a/Elib2EbookWeb/Misc/SbWriter.cs b/Elib2EbookWeb/Misc/SbWriter.cs index ad8eb55a..595bec78 100644 --- a/Elib2EbookWeb/Misc/SbWriter.cs +++ b/Elib2EbookWeb/Misc/SbWriter.cs @@ -1,17 +1,27 @@ namespace Elib2EbookWeb.Misc; -public class ActionLogger : ILogger { +public class ActionLogger : ILogger +{ private readonly Action<string?> _action; - public ActionLogger(Action<string?> action) { + public ActionLogger(Action<string?> action) + { _action = action; } - - public IDisposable? BeginScope<TState>(TState state) where TState : notnull => default!; - public bool IsEnabled(LogLevel logLevel) => true; + public IDisposable? BeginScope<TState>(TState state) where TState : notnull + { + return default!; + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } - public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) { + public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, + Func<TState, Exception?, string> formatter) + { _action($"{formatter(state, exception)}"); } -} \ No newline at end of file +} diff --git a/Elib2EbookWeb/Program.cs b/Elib2EbookWeb/Program.cs index 500706e3..28850e2a 100644 --- a/Elib2EbookWeb/Program.cs +++ b/Elib2EbookWeb/Program.cs @@ -6,7 +6,8 @@ // Add services to the container. builder.Services.AddRazorComponents().AddInteractiveServerComponents(); -builder.Services.AddMudServices(config => { +builder.Services.AddMudServices(config => +{ config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomCenter; config.SnackbarConfiguration.PreventDuplicates = false; @@ -21,8 +22,9 @@ var app = builder.Build(); // Configure the HTTP request pipeline. -if (!app.Environment.IsDevelopment()) { - app.UseExceptionHandler("/Error", createScopeForErrors: true); +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error", true); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } @@ -35,4 +37,4 @@ app.MapRazorComponents<App>() .AddInteractiveServerRenderMode(); -app.Run(); \ No newline at end of file +app.Run(); diff --git a/global.json b/global.json new file mode 100644 index 00000000..512142d2 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature" + } +} diff --git a/scripts/fetch-epubsharp.ps1 b/scripts/fetch-epubsharp.ps1 new file mode 100644 index 00000000..aa63219a --- /dev/null +++ b/scripts/fetch-epubsharp.ps1 @@ -0,0 +1,90 @@ +param( + [string] $Repo = "L34T/EpubSharp_Elib2Ebook", + [string] $Tag = "", + [string] $OutDir = "Core/External", + [switch] $Force +) + +$ErrorActionPreference = "Stop" + +function Get-LatestRelease { + param([string] $repo) + + $headers = @{ + "Accept" = "application/vnd.github+json" + "User-Agent" = "Elib2Ebook-fetch-epubsharp" + } + + return Invoke-RestMethod -Headers $headers -Uri ("https://api.github.com/repos/{0}/releases/latest" -f $repo) +} + +function Get-ReleaseByTag { + param([string] $repo, [string] $tag) + + $headers = @{ + "Accept" = "application/vnd.github+json" + "User-Agent" = "Elib2Ebook-fetch-epubsharp" + } + + return Invoke-RestMethod -Headers $headers -Uri ("https://api.github.com/repos/{0}/releases/tags/{1}" -f $repo, $tag) +} + +function Ensure-Dir { + param([string] $path) + if (-not (Test-Path -LiteralPath $path)) { + New-Item -ItemType Directory -Path $path | Out-Null + } +} + +function Download-Asset { + param( + [string] $url, + [string] $path, + [switch] $force + ) + + if (-not $force -and (Test-Path -LiteralPath $path) -and ((Get-Item -LiteralPath $path).Length -gt 0)) { + Write-Host ("Skip (exists): {0}" -f $path) + return + } + + Write-Host ("Download: {0}" -f $path) + Invoke-WebRequest -Uri $url -OutFile $path -UseBasicParsing | Out-Null +} + +if ([string]::IsNullOrWhiteSpace($Tag)) { + $release = Get-LatestRelease -repo $Repo +} +else { + $release = Get-ReleaseByTag -repo $Repo -tag $Tag +} + +if ($null -eq $release -or [string]::IsNullOrWhiteSpace($release.tag_name)) { + throw "Unable to resolve release for repo '$Repo' tag '$Tag'." +} + +$resolvedTag = $release.tag_name +Write-Host ("Using release tag: {0}" -f $resolvedTag) + +Ensure-Dir -path $OutDir + +$required = @{ + "EpubSharp-net10.dll" = "EpubSharp.dll" + "EpubSharp-net10.pdb" = "EpubSharp.pdb" + "EpubSharp-net10.deps.json" = "EpubSharp.deps.json" +} + +foreach ($item in $required.GetEnumerator()) { + $assetName = $item.Key + $destName = $item.Value + $asset = $release.assets | Where-Object { $_.name -eq $assetName } | Select-Object -First 1 + if ($null -eq $asset -or [string]::IsNullOrWhiteSpace($asset.browser_download_url)) { + throw "Missing asset '$assetName' in release '$resolvedTag' for repo '$Repo'." + } + + $dest = Join-Path $OutDir $destName + Download-Asset -url $asset.browser_download_url -path $dest -force:$Force +} + +Write-Host "Done." + diff --git a/scripts/fetch-epubsharp.sh b/scripts/fetch-epubsharp.sh new file mode 100644 index 00000000..0222625c --- /dev/null +++ b/scripts/fetch-epubsharp.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO="${EPUBSHARP_REPO:-L34T/EpubSharp_Elib2Ebook}" +TAG="${EPUBSHARP_TAG:-}" +OUTDIR="${EPUBSHARP_OUTDIR:-Core/External}" +FORCE="${EPUBSHARP_FORCE:-0}" + +api() { + local url="$1" + curl -fsSL \ + -H "Accept: application/vnd.github+json" \ + -H "User-Agent: Elib2Ebook-fetch-epubsharp" \ + "$url" +} + +if [[ -z "${TAG}" ]]; then + release_json="$(api "https://api.github.com/repos/${REPO}/releases/latest")" +else + release_json="$(api "https://api.github.com/repos/${REPO}/releases/tags/${TAG}")" +fi + +resolved_tag="$( + python3 -c 'import json,sys; obj=json.load(sys.stdin); print(obj.get("tag_name",""))' <<<"${release_json}" +)" + +if [[ -z "${resolved_tag}" ]]; then + echo "Failed to resolve release tag for repo '${REPO}'." >&2 + exit 1 +fi + +echo "Using release tag: ${resolved_tag}" +mkdir -p "${OUTDIR}" + +download_url_for() { + local asset_name="$1" + python3 -c ' +import json,sys +name=sys.argv[1] +obj=json.load(sys.stdin) +for a in obj.get("assets",[]): + if a.get("name")==name: + print(a.get("browser_download_url","")) + sys.exit(0) +sys.exit(1) +' "${asset_name}" <<<"${release_json}" +} + +download() { + local url="$1" + local dest="$2" + if [[ "${FORCE}" != "1" && -s "${dest}" ]]; then + echo "Skip (exists): ${dest}" + return 0 + fi + echo "Download: ${dest}" + curl -fsSL -o "${dest}" "${url}" +} + +for suffix in dll pdb deps.json; do + asset_name="EpubSharp-net10.${suffix}" + url="$(download_url_for "${asset_name}")" + if [[ -z "${url}" ]]; then + echo "Missing asset '${asset_name}' in release '${resolved_tag}' for repo '${REPO}'." >&2 + exit 1 + fi + download "${url}" "${OUTDIR}/EpubSharp.${suffix}" +done + +echo "Done." From a7c52fba3c3f20e57489743acb654f8c6d6a9aa1 Mon Sep 17 00:00:00 2001 From: L34T <51358775+l34t@users.noreply.github.com> Date: Sat, 20 Jun 2026 22:56:47 +0200 Subject: [PATCH 11/13] more clenup --- .codacy.yaml | 3 + Core/Configs/AppSecrets.cs | 30 +++++ Core/Logic/Builders/Fb2Builder.cs | 87 +++++++------ Core/Logic/Getters/AuthorTodayGetter.cs | 115 ++++++++++++------ Core/Logic/Getters/BoovelGetter.cs | 4 +- .../LibSocial/NewLibSocialGetterBase.cs | 32 ++++- Core/Logic/Getters/Litnet/LitnetGetterBase.cs | 8 +- Core/Logic/Getters/LitresGetter.cs | 5 +- Core/Logic/Getters/MyBookGetter.cs | 10 +- .../Getters/Novelxo/NovelxoGetterBase.cs | 4 +- Core/Logic/Getters/ProdamanGetter.cs | 65 +++++----- Core/Logic/Getters/Renovels/RemangaGetter.cs | 4 + Core/Logic/Getters/Rulate/ErolateGetter.cs | 2 +- Core/Logic/Getters/Rulate/MlateGetter.cs | 2 +- Core/Logic/Getters/Rulate/RulateGetter.cs | 2 +- Core/Logic/Getters/StrokiMtsGetter.cs | 22 +++- Core/Types/AuthorToday/AuthorTodayChapter.cs | 4 +- .../{Dockerfile => Dockerfile.disabled} | 0 .../{Dockerfile => Dockerfile.disabled} | 0 19 files changed, 266 insertions(+), 133 deletions(-) create mode 100644 .codacy.yaml create mode 100644 Core/Configs/AppSecrets.cs rename Elib2EbookCli/{Dockerfile => Dockerfile.disabled} (100%) rename Elib2EbookWeb/{Dockerfile => Dockerfile.disabled} (100%) diff --git a/.codacy.yaml b/.codacy.yaml new file mode 100644 index 00000000..519f8653 --- /dev/null +++ b/.codacy.yaml @@ -0,0 +1,3 @@ +--- +exclude_paths: + - "Core/Configs/AppSecrets.cs" diff --git a/Core/Configs/AppSecrets.cs b/Core/Configs/AppSecrets.cs new file mode 100644 index 00000000..c6e227aa --- /dev/null +++ b/Core/Configs/AppSecrets.cs @@ -0,0 +1,30 @@ +namespace Core.Configs; + +public static class AppSecrets +{ + public const string MyBookConsumerKey = "830968793b2a44c688400319f4a77231"; + public const string MyBookConsumerSecret = "HhaxXsMryrLz49R4"; + public const string MyBookIdentifier = "47b17a4c8231a28f06c6c836d0b5f6f2134cb74d"; + public const string MyBookDefaultAuthSecret = "fYFPndYhOTW6YxIJ"; + public const string MyBookDefaultAuthToken = "f8d32ef906664ed3a1525b7298aac461"; + + public const string LitnetSecret = "14a6579a984b3c6abecda6c2dfa83a64"; + + public const string LitresSecretKey = "AsAAfdV000-1kksn6591x:[}A{}<><DO#Brn`BnB6E`^s\"ivP:RY'4|v\"h/r^]"; + + public const string BoovelSalt = "skajldayzonePVPkruto0"; + + public const string AuthorTodaySalt = "FjPg]{2+$8JRvv~("; + + public const int AesIvShift = 16; + + public const string NovelxoKeyHex = "61626326312a7e235e325e2373305e3d295e5e3725623334"; + public const string NovelxoIvHex = "31323334353637383930383533373237"; + + public const string LitresDefaultSid = "6ufp4b2wbx1acc4k1f0wdo571762c0fn"; + + public const string AuthorTodayCert = "rmy8LDkVZR+pSXopxUte7hFbA6I="; + + public const string ErolateMature = "7da3ee594b38fc5355692d978fe8f5adbeb3d17di%3A1%3B"; + public const string RulateMature = "c3a2ed4b199a1a15f5a5483504c7a75a7030dc4bi%3A1%3B"; +} diff --git a/Core/Logic/Builders/Fb2Builder.cs b/Core/Logic/Builders/Fb2Builder.cs index 122c27cd..f488d71b 100644 --- a/Core/Logic/Builders/Fb2Builder.cs +++ b/Core/Logic/Builders/Fb2Builder.cs @@ -283,38 +283,7 @@ private void ProcessSection(XElement parent, HtmlNode node, string textNode = "" if (node.Name == "a") { - var href = node.Attributes["href"]?.Value ?? string.Empty; - if (string.IsNullOrWhiteSpace(href)) - { - if (string.IsNullOrWhiteSpace(textNode)) - { - parent.Add(node.InnerText); - } - else - { - var tag = CreateXElement(textNode); - tag.Value = node.InnerText; - parent.Add(tag); - } - } - else - { - var a = CreateXElement("a"); - a.SetAttributeValue(_xlink + "href", href); - a.Value = node.InnerText; - - if (string.IsNullOrWhiteSpace(textNode)) - { - parent.Add(a); - } - else - { - var tag = CreateXElement(textNode); - tag.Add(a); - parent.Add(tag); - } - } - + ProcessLinkNode(parent, node, textNode); return; } @@ -331,15 +300,59 @@ private void ProcessSection(XElement parent, HtmlNode node, string textNode = "" if (node.Name == "img") { - if (node.Attributes["src"] == null) return; + ProcessImageNode(parent, node); + return; + } - var imageElem = CreateXElement("image"); - imageElem.SetAttributeValue(_xlink + "href", "#" + node.Attributes["src"].Value); - parent.Add(imageElem); + ProcessTextOrMappedNode(parent, node, textNode); + } - return; + private void ProcessLinkNode(XElement parent, HtmlNode node, string textNode) + { + var href = node.Attributes["href"]?.Value ?? string.Empty; + if (string.IsNullOrWhiteSpace(href)) + { + if (string.IsNullOrWhiteSpace(textNode)) + { + parent.Add(node.InnerText); + } + else + { + var tag = CreateXElement(textNode); + tag.Value = node.InnerText; + parent.Add(tag); + } + } + else + { + var a = CreateXElement("a"); + a.SetAttributeValue(_xlink + "href", href); + a.Value = node.InnerText; + + if (string.IsNullOrWhiteSpace(textNode)) + { + parent.Add(a); + } + else + { + var tag = CreateXElement(textNode); + tag.Add(a); + parent.Add(tag); + } } + } + + private void ProcessImageNode(XElement parent, HtmlNode node) + { + if (node.Attributes["src"] == null) return; + var imageElem = CreateXElement("image"); + imageElem.SetAttributeValue(_xlink + "href", "#" + node.Attributes["src"].Value); + parent.Add(imageElem); + } + + private void ProcessTextOrMappedNode(XElement parent, HtmlNode node, string textNode) + { var nodeText = node.InnerText.HtmlDecode().CleanInvalidXmlChars(); if (node.InnerText.StartsWith(" ")) nodeText = " " + nodeText; diff --git a/Core/Logic/Getters/AuthorTodayGetter.cs b/Core/Logic/Getters/AuthorTodayGetter.cs index 656f44d0..80f26999 100644 --- a/Core/Logic/Getters/AuthorTodayGetter.cs +++ b/Core/Logic/Getters/AuthorTodayGetter.cs @@ -23,7 +23,7 @@ namespace Core.Logic.Getters; public class AuthorTodayGetter(BookGetterConfig config) : GetterBase(config) { - private const string AT_CERT = "rmy8LDkVZR+pSXopxUte7hFbA6I="; + private const string AT_CERT = AppSecrets.AuthorTodayCert; private bool _bypass; private bool _singleFetchUsed; @@ -77,63 +77,102 @@ public override async Task Authorize() { if (!Config.HasCredentials) return; - // АТ очень не любит много авторизовывать - // поэтому пришлось добавить кеширование токенов const string directory = "ATCache"; - if (!Directory.Exists(directory)) Directory.CreateDirectory(directory); - var saveCreds = $"{directory}/{Config.Options.Login.RemoveInvalidChars()}"; - if (File.Exists(saveCreds)) + var cachePath = $"{directory}/{Config.Options.Login.RemoveInvalidChars()}"; + + if (await TryAuthorizeFromCacheAsync(cachePath)) return; + + await AuthorizeByPasswordAsync(cachePath); + } + + private async Task<string> GetUserIdAsync() + { + try { - var timeDiff = DateTime.Now - File.GetLastWriteTime(saveCreds); - if (timeDiff.TotalHours < 24) - { - var savedAuth = await File.ReadAllTextAsync(saveCreds) - .ContinueWith(t => t.Result.Deserialize<AuthorTodayAuthResponse>()); - Config.Client.DefaultRequestHeaders.Authorization = - new AuthenticationHeaderValue("Bearer", savedAuth.Token); + var response = await Config.Client.SendWithTriesAsync(() => + GetDefaultMessage(ApiUrl.MakeRelativeUri("/v1/account/current-user"), _apiUrl)); - try - { - var checkResponse = await Config.Client.SendWithTriesAsync(() => - GetDefaultMessage(ApiUrl.MakeRelativeUri("/v1/account/current-user"), _apiUrl)); - var checkUser = await checkResponse.Content.ReadFromJsonAsync<AuthorTodayUser>(); - UserId = checkUser!.Id.ToString(); - } - catch (Exception) - { - } + if (response.StatusCode != HttpStatusCode.OK) return null; + + var user = await response.Content.ReadFromJsonAsync<AuthorTodayUser>(); + return user?.Id.ToString(); + } + catch (Exception ex) + { + Config.Logger.LogDebug(ex, "AuthorToday: не удалось получить идентификатор пользователя."); + return null; + } + } - if (!string.IsNullOrWhiteSpace(UserId)) return; + private async Task<bool> TryAuthorizeFromCacheAsync(string cachePath) + { + if (!File.Exists(cachePath)) return false; + + try + { + if (DateTime.Now - File.GetLastWriteTime(cachePath) >= TimeSpan.FromHours(24)) + { + File.Delete(cachePath); + return false; } - else + + var json = await File.ReadAllTextAsync(cachePath); + var savedAuth = json.Deserialize<AuthorTodayAuthResponse>(); + if (string.IsNullOrWhiteSpace(savedAuth?.Token)) return false; + + Config.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", savedAuth.Token); + + UserId = await GetUserIdAsync(); + if (!string.IsNullOrWhiteSpace(UserId)) { - File.Delete(saveCreds); + Config.Logger.LogInformation("AuthorToday: успешно авторизовались с помощью кэша"); + return true; } } + catch (Exception ex) + { + Config.Logger.LogDebug(ex, "AuthorToday: не удалось авторизоваться через кэш."); + try { File.Delete(cachePath); } catch { /* ignore */ } + } + Config.Client.DefaultRequestHeaders.Authorization = null; + return false; + } + + private async Task AuthorizeByPasswordAsync(string cachePath) + { var response = await Config.Client.SendAsync(GetDefaultMessage( ApiUrl.MakeRelativeUri("/v1/account/login-by-password"), _apiUrl, JsonContent.Create(new { Config.Options.Login, Config.Options.Password }))); - var data = await response.Content.ReadFromJsonAsync<AuthorTodayAuthResponse>(); - if (!string.IsNullOrWhiteSpace(data?.Token)) + if (response.StatusCode != HttpStatusCode.OK) { - Config.Logger.LogInformation("Успешно авторизовались"); - Config.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", data.Token); + var err = await response.Content.ReadFromJsonAsync<AuthorTodayAuthResponse>(); + throw new Elib2EbookAuthException($"Не удалось авторизоваться. {err?.Message}"); + } - response = await Config.Client.SendWithTriesAsync(() => - GetDefaultMessage(ApiUrl.MakeRelativeUri("/v1/account/current-user"), _apiUrl)); - var user = await response.Content.ReadFromJsonAsync<AuthorTodayUser>(); - UserId = user!.Id.ToString(); + var data = await response.Content.ReadFromJsonAsync<AuthorTodayAuthResponse>(); + if (string.IsNullOrWhiteSpace(data?.Token)) + throw new Elib2EbookAuthException("Сайт не вернул token после успешного запроса авторизации."); + + Config.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", data.Token); + + UserId = await GetUserIdAsync(); + if (string.IsNullOrWhiteSpace(UserId)) + throw new Elib2EbookAuthException("Не удалось получить идентификатор пользователя после авторизации."); + + Config.Logger.LogInformation("Успешно авторизовались"); + + try + { + await File.WriteAllTextAsync(cachePath, JsonSerializer.Serialize(data)); } - else + catch (Exception ex) { - throw new Elib2EbookAuthException($"Не удалось авторизоваться. {data?.Message}"); + Config.Logger.LogDebug(ex, "AuthorToday: не удалось записать кэш авторизации."); } - - await File.WriteAllTextAsync(saveCreds, JsonSerializer.Serialize(data)); } public override async Task<Book> Get(Uri url) diff --git a/Core/Logic/Getters/BoovelGetter.cs b/Core/Logic/Getters/BoovelGetter.cs index 02e9ce90..35fe67b3 100644 --- a/Core/Logic/Getters/BoovelGetter.cs +++ b/Core/Logic/Getters/BoovelGetter.cs @@ -83,12 +83,12 @@ private async Task<HtmlDocument> GetChapter(UrlChapter urlChapter) { var doc = await Config.Client.GetHtmlDocWithTriesAsync(urlChapter.Url); var encoded = Convert.FromBase64String(doc.QuerySelector("#chapter-content").InnerText); - var secret = doc.QuerySelector("body").Attributes["data-post-id"].Value + "skajldayzonePVPkruto0"; + var secret = doc.QuerySelector("body").Attributes["data-post-id"].Value + AppSecrets.BoovelSalt; var crypto = SHA256.HashData(Encoding.UTF8.GetBytes(secret).AsSpan(0, Encoding.UTF8.GetByteCount(secret))); using var aes = Aes.Create(); - const int IV_SHIFT = 16; + const int IV_SHIFT = AppSecrets.AesIvShift; aes.Key = crypto; aes.IV = encoded[..IV_SHIFT]; diff --git a/Core/Logic/Getters/LibSocial/NewLibSocialGetterBase.cs b/Core/Logic/Getters/LibSocial/NewLibSocialGetterBase.cs index b086a4f6..3e6739fd 100644 --- a/Core/Logic/Getters/LibSocial/NewLibSocialGetterBase.cs +++ b/Core/Logic/Getters/LibSocial/NewLibSocialGetterBase.cs @@ -92,7 +92,7 @@ private static string GetRandom(int length) { return Enumerable .Repeat(0, length) - .Aggregate(string.Empty, (c, _) => c + ALPHABET_BASE[new Random().Next(ALPHABET_BASE.Length)]); + .Aggregate(string.Empty, (c, _) => c + ALPHABET_BASE[RandomNumberGenerator.GetInt32(ALPHABET_BASE.Length)]); } public override async Task Authorize() @@ -116,15 +116,29 @@ public override async Task Authorize() AuthHost.MakeRelativeUri( $"/auth/oauth/authorize?scope=&client_id=1&response_type=code&redirect_uri={redirectUri}&state={state}&code_challenge={challenge}&code_challenge_method=S256&prompt=consent"); + var csrfToken = await FetchCsrfTokenAsync(challengeUrl); + var postForm = await SubmitCredentialsAsync(challengeUrl, csrfToken); + var authCode = await ProcessAuthorizationConsentAsync(postForm); + await ExchangeTokenAsync(redirectUri, secret, authCode); + + Config.Logger.LogInformation("Успешно авторизовались"); + } + + private async Task<string> FetchCsrfTokenAsync(Uri challengeUrl) + { var loginForm = await Config.Client.GetHtmlDocWithTriesAsync(challengeUrl); var tokenInput = loginForm.QuerySelector("input[name=_token]"); if (tokenInput is null) throw new Elib2EbookParseException( "Не удалось получить csrf токен формы авторизации сайта (возможно, заблокировал DDoS-Guard)"); + return tokenInput.Attributes["value"].Value; + } + private async Task<HtmlNode> SubmitCredentialsAsync(Uri challengeUrl, string csrfToken) + { var payload = new Dictionary<string, string> { - { "_token", tokenInput.Attributes["value"].Value }, + { "_token", csrfToken }, { "login", Config.Options.Login }, { "password", Config.Options.Password } }; @@ -135,6 +149,7 @@ await Config.Client.PostHtmlDocWithTriesAsync(AuthHost.MakeRelativeUri("/auth/lo if (login.QuerySelector(".g-recaptcha") is not null) throw new Elib2EbookParseException( "Авторизация заблокирована капчей. Укажите --flare с адресом FlareSolverr"); + var error = login.QuerySelector(".form-field__error"); if (error is not null && !string.IsNullOrWhiteSpace(error.InnerText)) throw new Elib2EbookAuthException($"Не удалось авторизоваться. {error.InnerText.Trim()}"); @@ -144,7 +159,12 @@ await Config.Client.PostHtmlDocWithTriesAsync(AuthHost.MakeRelativeUri("/auth/lo throw new Elib2EbookAuthException( "Не удалось получить форму подтверждения OAuth на сайте (проверьте логин/пароль)"); - payload = postForm + return postForm; + } + + private async Task<string> ProcessAuthorizationConsentAsync(HtmlNode postForm) + { + var payload = postForm .QuerySelectorAll("input[type=hidden]") .ToDictionary(input => input.Attributes["name"].Value, input => input.Attributes["value"].Value); @@ -160,6 +180,11 @@ await Config.Client.PostHtmlDocWithTriesAsync(AuthHost.MakeRelativeUri("/auth/lo if (string.IsNullOrWhiteSpace(authCode)) throw new Elib2EbookAuthException("Сайт не вернул code после авторизации (проверьте логин/пароль)"); + return authCode; + } + + private async Task ExchangeTokenAsync(Uri redirectUri, string secret, string authCode) + { var tokenResponse = await Config.Client.PostAsync(ApiHost.MakeRelativeUri("/api/auth/oauth/token"), JsonContent.Create(new { @@ -188,7 +213,6 @@ await Config.Client.PostHtmlDocWithTriesAsync(AuthHost.MakeRelativeUri("/auth/lo throw new Elib2EbookAuthException("Сайт не вернул access token. Проверьте корректность логина и пароля"); Config.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken); - Config.Logger.LogInformation("Успешно авторизовались"); } protected override string GetId(Uri url) diff --git a/Core/Logic/Getters/Litnet/LitnetGetterBase.cs b/Core/Logic/Getters/Litnet/LitnetGetterBase.cs index fd4ba740..b6f161f9 100644 --- a/Core/Logic/Getters/Litnet/LitnetGetterBase.cs +++ b/Core/Logic/Getters/Litnet/LitnetGetterBase.cs @@ -24,9 +24,9 @@ namespace Core.Logic.Getters.Litnet; public abstract class LitnetGetterBase(BookGetterConfig config) : GetterBase(config) { - private const string SECRET = "14a6579a984b3c6abecda6c2dfa83a64"; + private const string SECRET = AppSecrets.LitnetSecret; - private static string DeviceId = Guid.NewGuid().ToString().ToUpperInvariant(); + private string DeviceId = Guid.NewGuid().ToString().ToUpperInvariant(); private Uri ApiUrl => new($"https://api.{SystemUrl.Host}/"); //cloudflare :( @@ -44,7 +44,7 @@ protected override string GetId(Uri url) private static byte[] Decrypt(string text) { using var aes = Aes.Create(); - const int IV_SHIFT = 16; + const int IV_SHIFT = AppSecrets.AesIvShift; aes.Key = Encoding.UTF8.GetBytes(SECRET); aes.IV = Encoding.UTF8.GetBytes(text)[..IV_SHIFT]; @@ -59,7 +59,7 @@ private static byte[] Decrypt(string text) return output.ToArray()[IV_SHIFT..]; } - private static string GetSign(string token) + private string GetSign(string token) { var inputBytes = Encoding.ASCII.GetBytes(DeviceId + SECRET + (token ?? string.Empty)); var hashBytes = MD5.HashData(inputBytes); diff --git a/Core/Logic/Getters/LitresGetter.cs b/Core/Logic/Getters/LitresGetter.cs index 3a386fc9..98284288 100644 --- a/Core/Logic/Getters/LitresGetter.cs +++ b/Core/Logic/Getters/LitresGetter.cs @@ -27,12 +27,12 @@ namespace Core.Logic.Getters; public class LitresGetter(BookGetterConfig config) : GetterBase(config) { - private const string SECRET_KEY = "AsAAfdV000-1kksn6591x:[}A{}<><DO#Brn`BnB6E`^s\"ivP:RY'4|v\"h/r^]"; + private const string SECRET_KEY = AppSecrets.LitresSecretKey; private const string APP = "13"; private LitresAuthResponseData _authData = new() { - Sid = "6ufp4b2wbx1acc4k1f0wdo571762c0fn" + Sid = AppSecrets.LitresDefaultSid }; private LitresMe _me; @@ -207,6 +207,7 @@ public override async Task<Book> Get(Uri url) } catch (Exception) { + Config.Logger.LogWarning("Litres: Не удалось получить метаданные книги из API."); } var book = new Book(SystemUrl.MakeRelativeUri(bookId)) diff --git a/Core/Logic/Getters/MyBookGetter.cs b/Core/Logic/Getters/MyBookGetter.cs index 3bfd0ef3..571c7309 100644 --- a/Core/Logic/Getters/MyBookGetter.cs +++ b/Core/Logic/Getters/MyBookGetter.cs @@ -25,16 +25,16 @@ namespace Core.Logic.Getters; public class MyBookGetter(BookGetterConfig config) : GetterBase(config) { - private const string CONSUMER_KEY = "830968793b2a44c688400319f4a77231"; - private const string CONSUMER_SECRET = "HhaxXsMryrLz49R4"; - private const string IDENTIFIER = "47b17a4c8231a28f06c6c836d0b5f6f2134cb74d"; + private const string CONSUMER_KEY = AppSecrets.MyBookConsumerKey; + private const string CONSUMER_SECRET = AppSecrets.MyBookConsumerSecret; + private const string IDENTIFIER = AppSecrets.MyBookIdentifier; private HttpClient _apiClient; private MyBookAuth _token = new() { - Secret = "fYFPndYhOTW6YxIJ", - Token = "f8d32ef906664ed3a1525b7298aac461" + Secret = AppSecrets.MyBookDefaultAuthSecret, + Token = AppSecrets.MyBookDefaultAuthToken }; protected override Uri SystemUrl => new("https://mybook.ru/"); diff --git a/Core/Logic/Getters/Novelxo/NovelxoGetterBase.cs b/Core/Logic/Getters/Novelxo/NovelxoGetterBase.cs index 28057473..d0df631c 100644 --- a/Core/Logic/Getters/Novelxo/NovelxoGetterBase.cs +++ b/Core/Logic/Getters/Novelxo/NovelxoGetterBase.cs @@ -19,8 +19,8 @@ namespace Core.Logic.Getters.Novelxo; public abstract class NovelxoGetterBase(BookGetterConfig config) : GetterBase(config) { - private static readonly byte[] Key = StringToByteArray("61626326312a7e235e325e2373305e3d295e5e3725623334"); - private static readonly byte[] IV = StringToByteArray("31323334353637383930383533373237"); + private static readonly byte[] Key = StringToByteArray(AppSecrets.NovelxoKeyHex); + private static readonly byte[] IV = StringToByteArray(AppSecrets.NovelxoIvHex); protected override string GetId(Uri url) { diff --git a/Core/Logic/Getters/ProdamanGetter.cs b/Core/Logic/Getters/ProdamanGetter.cs index ea671399..202194e6 100644 --- a/Core/Logic/Getters/ProdamanGetter.cs +++ b/Core/Logic/Getters/ProdamanGetter.cs @@ -221,37 +221,10 @@ private async Task<IEnumerable<Chapter>> FillChapter(Uri url, string title) if (i == 1 && !IsHeaderStart(nodes)) chapter = CreateChapter(title); foreach (var node in nodes) + { if (node.Name != "h3" || (node.Name == "h3" && node.GetText() == "***")) { - if (node.Name == "br") - { - if (firstBr) - { - firstBr = false; - text.Append("<p>"); - } - else - { - text.Append("</p><p>"); - } - - continue; - } - - if (node.Name == "img" && node.Attributes["src"] != null) - { - text.Append($"<img src='{node.Attributes["src"].Value}'/>"); - } - else if (!string.IsNullOrWhiteSpace(node.InnerHtml)) - { - var pText = Decode(node.InnerHtml.HtmlDecode()); - - if (node.InnerHtml.StartsWith(" ")) pText = " " + pText; - - if (node.InnerHtml.EndsWith(" ")) pText += " "; - - text.Append(pText.CoverTag(node.Name == "#text" ? string.Empty : node.Name)); - } + ProcessContentNode(text, node, ref firstBr); } else { @@ -260,6 +233,7 @@ private async Task<IEnumerable<Chapter>> FillChapter(Uri url, string title) text.Clear(); chapter = CreateChapter(Decode(node.InnerText.HtmlDecode())); } + } } text.Append("</p>"); @@ -267,6 +241,39 @@ private async Task<IEnumerable<Chapter>> FillChapter(Uri url, string title) return result; } + private void ProcessContentNode(StringBuilder text, HtmlNode node, ref bool firstBr) + { + if (node.Name == "br") + { + if (firstBr) + { + firstBr = false; + text.Append("<p>"); + } + else + { + text.Append("</p><p>"); + } + + return; + } + + if (node.Name == "img" && node.Attributes["src"] != null) + { + text.Append($"<img src='{node.Attributes["src"].Value}'/>"); + } + else if (!string.IsNullOrWhiteSpace(node.InnerHtml)) + { + var pText = Decode(node.InnerHtml.HtmlDecode()); + + if (node.InnerHtml.StartsWith(" ")) pText = " " + pText; + + if (node.InnerHtml.EndsWith(" ")) pText += " "; + + text.Append(pText.CoverTag(node.Name == "#text" ? string.Empty : node.Name)); + } + } + private Task<TempFile> GetCover(HtmlDocument doc, Uri url) { var imagePath = doc.QuerySelector("div[itemprop=aggregateRating] img[itemprop=image]")?.Attributes["src"] diff --git a/Core/Logic/Getters/Renovels/RemangaGetter.cs b/Core/Logic/Getters/Renovels/RemangaGetter.cs index c22562d5..48ba6bb2 100644 --- a/Core/Logic/Getters/Renovels/RemangaGetter.cs +++ b/Core/Logic/Getters/Renovels/RemangaGetter.cs @@ -20,6 +20,7 @@ protected override HtmlDocument GetChapterAsHtml(RenovelsChapter response) var sb = new StringBuilder(); foreach (var obj in response.Pages) + { switch (obj) { case JsonObject: @@ -31,7 +32,10 @@ protected override HtmlDocument GetChapterAsHtml(RenovelsChapter response) break; } + default: + break; } + } return sb.AsHtmlDoc(); } diff --git a/Core/Logic/Getters/Rulate/ErolateGetter.cs b/Core/Logic/Getters/Rulate/ErolateGetter.cs index b514e8ac..1461164f 100644 --- a/Core/Logic/Getters/Rulate/ErolateGetter.cs +++ b/Core/Logic/Getters/Rulate/ErolateGetter.cs @@ -6,5 +6,5 @@ namespace Core.Logic.Getters.Rulate; public class ErolateGetter(BookGetterConfig config) : RulateGetterBase(config) { protected override Uri SystemUrl => new("https://erolate.com/"); - protected override string Mature => "7da3ee594b38fc5355692d978fe8f5adbeb3d17di%3A1%3B"; + protected override string Mature => AppSecrets.ErolateMature; } diff --git a/Core/Logic/Getters/Rulate/MlateGetter.cs b/Core/Logic/Getters/Rulate/MlateGetter.cs index 31949b0d..07880a68 100644 --- a/Core/Logic/Getters/Rulate/MlateGetter.cs +++ b/Core/Logic/Getters/Rulate/MlateGetter.cs @@ -6,5 +6,5 @@ namespace Core.Logic.Getters.Rulate; public class MlateGetter(BookGetterConfig config) : RulateGetterBase(config) { protected override Uri SystemUrl => new("https://mlate.ru/"); - protected override string Mature => "c3a2ed4b199a1a15f5a5483504c7a75a7030dc4bi%3A1%3B"; + protected override string Mature => AppSecrets.RulateMature; } diff --git a/Core/Logic/Getters/Rulate/RulateGetter.cs b/Core/Logic/Getters/Rulate/RulateGetter.cs index ce4aeafc..ee499a49 100644 --- a/Core/Logic/Getters/Rulate/RulateGetter.cs +++ b/Core/Logic/Getters/Rulate/RulateGetter.cs @@ -6,5 +6,5 @@ namespace Core.Logic.Getters.Rulate; public class RulateGetter(BookGetterConfig config) : RulateGetterBase(config) { protected override Uri SystemUrl => new("https://tl.rulate.ru"); - protected override string Mature => "c3a2ed4b199a1a15f5a5483504c7a75a7030dc4bi%3A1%3B"; + protected override string Mature => AppSecrets.RulateMature; } diff --git a/Core/Logic/Getters/StrokiMtsGetter.cs b/Core/Logic/Getters/StrokiMtsGetter.cs index d21137d5..7ba7257e 100644 --- a/Core/Logic/Getters/StrokiMtsGetter.cs +++ b/Core/Logic/Getters/StrokiMtsGetter.cs @@ -42,11 +42,7 @@ public override async Task<Book> Get(Uri url) var id = GetId(url); var details = await GetBook(id); - var detailBook = details?.Items.FirstOrDefault(d => d.TextBook is not null)?.TextBook ?? - details?.Items.FirstOrDefault(d => d.AudioBook is not null)?.AudioBook ?? - details?.Items.FirstOrDefault(d => d.ComicBook is not null)?.ComicBook ?? - details?.Items.FirstOrDefault(d => d.PressBook is not null)?.PressBook ?? - await GetSerial(id); + var detailBook = await FindDetailBook(details, id); if (detailBook is null) throw new Elib2EbookParseException("Не удалось получить книгу"); @@ -84,6 +80,22 @@ public override async Task<Book> Get(Uri url) return book; } + private async Task<StrokiMtsBookItem> FindDetailBook(StrokiMtsApiMultiResponse details, string id) + { + if (details?.Items is not null) + { + foreach (var item in details.Items) + { + if (item.TextBook is not null) return item.TextBook; + if (item.AudioBook is not null) return item.AudioBook; + if (item.ComicBook is not null) return item.ComicBook; + if (item.PressBook is not null) return item.PressBook; + } + } + + return await GetSerial(id); + } + private static IEnumerable<Chapter> FillChaptersFromImages(StrokiMtsBookItem book, IEnumerable<TempFile> images) { var sb = new StringBuilder(); diff --git a/Core/Types/AuthorToday/AuthorTodayChapter.cs b/Core/Types/AuthorToday/AuthorTodayChapter.cs index 4dbfb669..33fb74c4 100644 --- a/Core/Types/AuthorToday/AuthorTodayChapter.cs +++ b/Core/Types/AuthorToday/AuthorTodayChapter.cs @@ -30,11 +30,11 @@ public class AuthorTodayChapter public string Decode(string userId, string cert) { var secret = - $"{string.Concat(Key.Reverse())}:{(string.IsNullOrWhiteSpace(userId) ? "Guest" : userId)}:{"FjPg]{2+$8JRvv~("}:{cert}"; + $"{string.Concat(Key.Reverse())}:{(string.IsNullOrWhiteSpace(userId) ? "Guest" : userId)}:{Core.Configs.AppSecrets.AuthorTodaySalt}:{cert}"; var hashSecret = Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(secret))); using var aes = Aes.Create(); - const int IV_SHIFT = 16; + const int IV_SHIFT = Core.Configs.AppSecrets.AesIvShift; var aesKey = Encoding.UTF8.GetBytes(hashSecret)[..IV_SHIFT]; diff --git a/Elib2EbookCli/Dockerfile b/Elib2EbookCli/Dockerfile.disabled similarity index 100% rename from Elib2EbookCli/Dockerfile rename to Elib2EbookCli/Dockerfile.disabled diff --git a/Elib2EbookWeb/Dockerfile b/Elib2EbookWeb/Dockerfile.disabled similarity index 100% rename from Elib2EbookWeb/Dockerfile rename to Elib2EbookWeb/Dockerfile.disabled From b7f79f35b04c430f3d151dd1861cff5444691bce Mon Sep 17 00:00:00 2001 From: L34T <51358775+l34t@users.noreply.github.com> Date: Sat, 20 Jun 2026 23:25:50 +0200 Subject: [PATCH 12/13] clenup 3 --- .../{dotnet.yml => dotnet.yml.disabled} | 0 Core/Extensions/HttpClientExtensions.cs | 68 ++++--------------- .../LibSocial/NewLibSocialGetterBase.cs | 10 +-- Core/Logic/Getters/LitresGetter.cs | 1 - Core/Types/Litres/LitresPayload.cs | 6 +- 5 files changed, 21 insertions(+), 64 deletions(-) rename .github/workflows/{dotnet.yml => dotnet.yml.disabled} (100%) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml.disabled similarity index 100% rename from .github/workflows/dotnet.yml rename to .github/workflows/dotnet.yml.disabled diff --git a/Core/Extensions/HttpClientExtensions.cs b/Core/Extensions/HttpClientExtensions.cs index 360c129a..c997c9b6 100644 --- a/Core/Extensions/HttpClientExtensions.cs +++ b/Core/Extensions/HttpClientExtensions.cs @@ -20,15 +20,16 @@ private static TimeSpan GetTimeout(TimeSpan errorTimeout) return errorTimeout == default ? DefaultTimeout : errorTimeout; } - public static async Task<HttpResponseMessage> GetWithTriesAsync(this HttpClient client, Uri url, - TimeSpan errorTimeout = default) + private static async Task<HttpResponseMessage> SendWithTriesAsync( + Func<Task<HttpResponseMessage>> requestFunc, + TimeSpan errorTimeout) { Exception lastEx = null; for (var i = 0; i < MAX_TRY_COUNT; i++) try { - var response = await client.GetAsync(url); + var response = await requestFunc(); if (response.StatusCode != HttpStatusCode.OK) { @@ -52,67 +53,22 @@ public static async Task<HttpResponseMessage> GetWithTriesAsync(this HttpClient return default; } + public static async Task<HttpResponseMessage> GetWithTriesAsync(this HttpClient client, Uri url, + TimeSpan errorTimeout = default) + { + return await SendWithTriesAsync(() => client.GetAsync(url), errorTimeout); + } + public static async Task<HttpResponseMessage> SendWithTriesAsync(this HttpClient client, Func<HttpRequestMessage> message, TimeSpan errorTimeout = default) { - Exception lastEx = null; - - for (var i = 0; i < MAX_TRY_COUNT; i++) - try - { - var response = await client.SendAsync(message()); - - if (response.StatusCode != HttpStatusCode.OK) - { - if (i == MAX_TRY_COUNT - 1) return response; - - response.Dispose(); - await Task.Delay(GetTimeout(errorTimeout)); - continue; - } - - return response; - } - catch (Exception ex) - { - lastEx = ex; - await Task.Delay(GetTimeout(errorTimeout)); - } - - if (lastEx != null) throw lastEx; - - return default; + return await SendWithTriesAsync(() => client.SendAsync(message()), errorTimeout); } public static async Task<HttpResponseMessage> PostWithTriesAsync(this HttpClient client, Uri url, HttpContent content, TimeSpan errorTimeout = default) { - Exception lastEx = null; - - for (var i = 0; i < MAX_TRY_COUNT; i++) - try - { - var response = await client.PostAsync(url, content); - - if (response.StatusCode != HttpStatusCode.OK) - { - await Task.Delay(GetTimeout(errorTimeout)); - if (i == MAX_TRY_COUNT - 1) return response; - - continue; - } - - return response; - } - catch (Exception ex) - { - lastEx = ex; - await Task.Delay(GetTimeout(errorTimeout)); - } - - if (lastEx != null) throw lastEx; - - return default; + return await SendWithTriesAsync(() => client.PostAsync(url, content), errorTimeout); } public static async Task<HtmlDocument> GetHtmlDocWithTriesAsync(this HttpClient client, Uri url, diff --git a/Core/Logic/Getters/LibSocial/NewLibSocialGetterBase.cs b/Core/Logic/Getters/LibSocial/NewLibSocialGetterBase.cs index 3e6739fd..2e44844d 100644 --- a/Core/Logic/Getters/LibSocial/NewLibSocialGetterBase.cs +++ b/Core/Logic/Getters/LibSocial/NewLibSocialGetterBase.cs @@ -67,7 +67,11 @@ private static string TrimForLog(string value, int maxLength = 300) private static string Challenge(string str) { - var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(str).AsSpan(0, Encoding.UTF8.GetByteCount(str))); + Span<byte> bytesToHash = stackalloc byte[Encoding.UTF8.GetMaxByteCount(str.Length)]; + int byteCount = Encoding.UTF8.GetBytes(str, bytesToHash); + Span<byte> bytes = stackalloc byte[32]; + SHA256.HashData(bytesToHash[..byteCount], bytes); + var sb = new StringBuilder(44); for (var t = 0; t < bytes.Length; t += 3) @@ -90,9 +94,7 @@ private static string Challenge(string str) private static string GetRandom(int length) { - return Enumerable - .Repeat(0, length) - .Aggregate(string.Empty, (c, _) => c + ALPHABET_BASE[RandomNumberGenerator.GetInt32(ALPHABET_BASE.Length)]); + return RandomNumberGenerator.GetString(ALPHABET_BASE, length); } public override async Task Authorize() diff --git a/Core/Logic/Getters/LitresGetter.cs b/Core/Logic/Getters/LitresGetter.cs index 98284288..448122bc 100644 --- a/Core/Logic/Getters/LitresGetter.cs +++ b/Core/Logic/Getters/LitresGetter.cs @@ -109,7 +109,6 @@ public override async Task Authorize() { _authData = await File.ReadAllTextAsync(saveCreds) .ContinueWith(t => t.Result.Deserialize<LitresAuthResponseData>()); - ; Config.Client.DefaultRequestHeaders.Add("Session-Id", _authData.Sid); var checkResponse = diff --git a/Core/Types/Litres/LitresPayload.cs b/Core/Types/Litres/LitresPayload.cs index c0e6fa3f..21492a00 100644 --- a/Core/Types/Litres/LitresPayload.cs +++ b/Core/Types/Litres/LitresPayload.cs @@ -37,9 +37,9 @@ public static LitresPayload Create(DateTime date, string sid, string secret, str private static string Sha256(string str) { - var crypt = SHA256.Create(); - var crypto = crypt.ComputeHash(Encoding.UTF8.GetBytes(str), 0, Encoding.UTF8.GetByteCount(str)); + var bytes = Encoding.UTF8.GetBytes(str); + var crypto = SHA256.HashData(bytes); - return crypto.Aggregate(new StringBuilder(), (sb, b) => sb.Append(b.ToString("x2"))).ToString(); + return Convert.ToHexString(crypto).ToLowerInvariant(); } } From 6603d59b1d129d768adf0b75f8b3c05410a6c897 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 21:26:41 +0000 Subject: [PATCH 13/13] Bump the nuget group with 1 update Bumps AngleSharp from 1.3.0 to 1.5.0 --- updated-dependencies: - dependency-name: AngleSharp dependency-version: 1.5.0 dependency-type: direct:production dependency-group: nuget ... Signed-off-by: dependabot[bot] <support@github.com> --- Core/Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/Core.csproj b/Core/Core.csproj index c04aca5e..a0a7f3a8 100644 --- a/Core/Core.csproj +++ b/Core/Core.csproj @@ -9,7 +9,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="AngleSharp" Version="1.3.0" /> + <PackageReference Include="AngleSharp" Version="1.5.0" /> <PackageReference Include="CommandLineParser" Version="2.9.1" /> <PackageReference Include="FlareSolverrSharp" Version="3.0.7" /> <PackageReference Include="HtmlAgilityPack" Version="1.12.4" />