diff --git a/ExampleUsingPDFFileGenerator/Assets/coverTestImage.png b/ExampleUsingPDFFileGenerator/Assets/coverTestImage.png deleted file mode 100644 index 5c1b1e0..0000000 Binary files a/ExampleUsingPDFFileGenerator/Assets/coverTestImage.png and /dev/null differ diff --git a/ExampleUsingPDFFileGenerator/Assets/longTestImage.png b/ExampleUsingPDFFileGenerator/Assets/longTestImage.png deleted file mode 100644 index 71bdf23..0000000 Binary files a/ExampleUsingPDFFileGenerator/Assets/longTestImage.png and /dev/null differ diff --git a/ExampleUsingPDFFileGenerator/Assets/shortTestImage.png b/ExampleUsingPDFFileGenerator/Assets/shortTestImage.png deleted file mode 100644 index 575ffa4..0000000 Binary files a/ExampleUsingPDFFileGenerator/Assets/shortTestImage.png and /dev/null differ diff --git a/ExampleUsingPDFFileGenerator/Assets/testHtml.html b/ExampleUsingPDFFileGenerator/Assets/testHtml.html deleted file mode 100644 index 453cc94..0000000 --- a/ExampleUsingPDFFileGenerator/Assets/testHtml.html +++ /dev/null @@ -1,203 +0,0 @@ -
-
- - - -
-

Lorem

-

h1

-

h2

-

h3

-

h4

-
h5
-
-
-
-
-
h6
-

- Lorem   ipsum dolor
sit, amet consectetur adipisicing elit. Illo aperiam perferendis - soluta nam ducimus ipsa
alias animi asperiores quisquam aut ex minus, - cum possimus accusamus corporis Test link - consequatur ipsam praesentium. -

- - - - - - - - - - - - - - - - - -
1234
56323423
78
-
-
-
-
lots of div
-
-
-
- -
-
-
-
-
-
-
-

- - - - -

- - - - -

-

- - - - -

s
s - - - - asd -

- - - - - - - - - - - - - - - - -
First NameLast NameEmail Address
HillaryNyakunditables@mail.com
LaryMakdeveloper@mail.com
-

-

-

-

lots of p

-

-

-

- test b - test b -
-
first line
- second line -

p line

-
- -
-

p inside div

-
- -

-

div inside p
-

- -

Lorem ipsum dolor sit.

-

- Лорем ипсум долор сит амет, хинц феугаит албуциус не пер, вис еу темпор номинати маиестатис. Еу ерос оптион яуи. Ерат миним ехерци хас еа, нец легимус детерруиссет ат. Еирмод тхеопхрастус те хис, еи яуо видит аугуе, дицтас цонсецтетуер при ад. - Бруте сенсибус вис ат, нам путант форенсибус ид, мел еа порро толлит перфецто. Яуот цлита ут нец. Ан иус алиа цонсецтетуер, те хис тале неморе. Цу сусципит апеириан торяуатос цум. Яуи ут адхуц аппареат цомпрехенсам, граецис еррорибус еи вел. -

-

Lorem ipsum dolor sit.

-

- Lorem ipsum dolor sit amet consectetur adipisicing elit. At esse natus eum est - beatae voluptatum obcaecati officia dolorum non ex! -

-

Lorem ipsum dolor sit.

-
-

- Lorem ipsum dolor sit amet consectetur adipisicing elit. At esse natus eum est - beatae voluptatum obcaecati officia dolorum non ex! -

-

- Lorem ipsum dolor sit amet consectetur adipisicing elit. Laboriosam suscipit - consequuntur ratione. Autem, debitis omnis obcaecati quod voluptatibus ea non - delectus magni amet dolor sunt eos vel suscipit. Minus vel facere nihil - deleniti, doloremque velit, iste vitae id dolor dolorum quisquam consequuntur - illum aliquid provident consectetur quia voluptatum omnis aliquam dignissimos - odit sed, totam enim. -
In porro molestiae, dolor animi, quisquam, quos beatae - quod ab esse deserunt velit. Quas accusantium nam laboriosam quos aliquid - eligendi quidem facere aut commodi! Facilis, reprehenderit porro! Optio - reiciendis, et nam distinctio harum inventore voluptas ratione praesentium - mollitia, reprehenderit id repudiandae assumenda esse atque fugiat. -

-

Lorem ipsum dolor sit amet.

-

- Lorem ipsum dolor sit amet consectetur adipisicing elit. Laboriosam suscipit - consequuntur ratione. Autem, debitis omnis obcaecati quod voluptatibus ea non - delectus magni amet dolor sunt eos vel suscipit. Minus vel facere nihil - deleniti, doloremque velit, iste vitae id dolor dolorum quisquam consequuntur - illum aliquid provident consectetur quia voluptatum omnis aliquam dignissimos - odit sed, totam enim. -

-

Lorem ipsum dolor sit amet.

-

- Lorem ipsum dolor sit amet consectetur adipisicing elit. Laboriosam suscipit - consequuntur ratione. Autem, debitis omnis obcaecati quod voluptatibus ea non - delectus magni amet dolor sunt eos vel suscipit. Minus vel facere nihil - deleniti, doloremque velit, iste vitae id dolor dolorum quisquam consequuntur - illum aliquid provident consectetur quia voluptatum omnis aliquam dignissimos - odit sed, totam enim. -

-

Lorem ipsum dolor sit amet.

-

- Lorem ipsum dolor sit amet consectetur adipisicing elit. Laboriosam suscipit - consequuntur ratione. Autem, debitis omnis obcaecati quod voluptatibus ea non - delectus magni amet dolor sunt eos vel suscipit. Minus vel facere nihil - deleniti, doloremque velit, iste vitae id dolor dolorum quisquam consequuntur - illum aliquid provident consectetur quia voluptatum omnis aliquam dignissimos - odit sed, totam enim. -

-

—————————————————————

-
- Test link 2 -

● Lorem ipsum dolor sit amet

-

● Lorem ipsum dolor sit amet

-

Lorem ipsum dolor sit amet. 😊

- - -
\ No newline at end of file diff --git a/ExampleUsingPDFFileGenerator/Assets/testImage.png b/ExampleUsingPDFFileGenerator/Assets/testImage.png deleted file mode 100644 index 6cde232..0000000 Binary files a/ExampleUsingPDFFileGenerator/Assets/testImage.png and /dev/null differ diff --git a/ExampleUsingPDFFileGenerator/ExampleUsingPDFFileGenerator.csproj b/ExampleUsingPDFFileGenerator/ExampleUsingPDFFileGenerator.csproj deleted file mode 100644 index 3299033..0000000 --- a/ExampleUsingPDFFileGenerator/ExampleUsingPDFFileGenerator.csproj +++ /dev/null @@ -1,41 +0,0 @@ - - - - Exe - net6.0 - enable - enable - - - - - - - - - True - True - Resources.resx - - - - - - ResXFileCodeGenerator - Resources.Designer.cs - - - - - - Always - - - Always - - - Always - - - - diff --git a/ExampleUsingPDFFileGenerator/Program.cs b/ExampleUsingPDFFileGenerator/Program.cs deleted file mode 100644 index 3b52612..0000000 --- a/ExampleUsingPDFFileGenerator/Program.cs +++ /dev/null @@ -1,65 +0,0 @@ -using ExampleUsingPDFFileGenerator.Properties; -using NovelParserBLL.FileGenerators.PDF; -using NovelParserBLL.Models; - -var pdfGenerator = new PDFFileGenerator(); - -var testGroupName = "test group"; - -var novel = new Novel() -{ - Name = "Test novel name", - Description = "Test novel description", - Author = "no author", - URL = "none", - Cover = GetTestCover(), - ChaptersByGroup = new Dictionary>() - { - { - testGroupName, - new SortedList() - { - {1, GetTestChapter() }, - } - } - } -}; - -var pdfGeneratorParams = new PDFGenerationParams("testFile.pdf", novel, testGroupName, "all", PDFType.FixPageSize); - -pdfGenerator.ShowInPreviewer(pdfGeneratorParams); - -Chapter GetTestChapter() -{ - var chapter = new Chapter() - { - Content = Resources.testHtml, - Images = new List() - { - //GetTestLongImage(), - GetTestShortImage() - } - }; - - return chapter; -} - -ImageInfo GetTestCover() -{ - return GetTestImage("coverTestImage.png"); -} - -ImageInfo GetTestShortImage() -{ - return GetTestImage("shortTestImage.png"); -} - -ImageInfo GetTestLongImage() -{ - return GetTestImage("longTestImage.png"); -} - -ImageInfo GetTestImage(string fileName) -{ - return new ImageInfo($"Assets/{fileName}", fileName, ""); -} \ No newline at end of file diff --git a/ExampleUsingPDFFileGenerator/Properties/Resources.Designer.cs b/ExampleUsingPDFFileGenerator/Properties/Resources.Designer.cs deleted file mode 100644 index 8d4cb5e..0000000 --- a/ExampleUsingPDFFileGenerator/Properties/Resources.Designer.cs +++ /dev/null @@ -1,82 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace ExampleUsingPDFFileGenerator.Properties { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ExampleUsingPDFFileGenerator.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to <div class="reader-container container container_center"> - /// <div class="article-image"> - /// <img class="lazyload" - /// data-background="" - /// src="shortTestImage.png" /> - /// </div> - /// <p>Lorem</p> - /// <p> - /// <s>Lorem ipsum</s> dolor sit, <b>amet</b> consectetur <strike>adipisicing</strike> elit. Illo aperiam perferendis - /// soluta nam <small>ducimus</small> ipsa alias animi asperiores quisquam aut ex minus, - /// cum <u>possimus</u> accusamus corporis? <a href="h [rest of string was truncated]";. - /// - internal static string testHtml { - get { - return ResourceManager.GetString("testHtml", resourceCulture); - } - } - } -} diff --git a/ExampleUsingPDFFileGenerator/Properties/Resources.resx b/ExampleUsingPDFFileGenerator/Properties/Resources.resx deleted file mode 100644 index 41796db..0000000 --- a/ExampleUsingPDFFileGenerator/Properties/Resources.resx +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - - ..\assets\testhtml.html;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 - - \ No newline at end of file diff --git a/HTMLQuestPDF/HTMLQuestPDF.csproj b/HTMLQuestPDF/HTMLQuestPDF.csproj index 863c2c2..32b2c8e 100644 --- a/HTMLQuestPDF/HTMLQuestPDF.csproj +++ b/HTMLQuestPDF/HTMLQuestPDF.csproj @@ -1,7 +1,7 @@ - net6.0 + net6.0-windows enable enable diff --git a/NovelParser.sln b/NovelParser.sln index ae25caf..80c09ca 100644 --- a/NovelParser.sln +++ b/NovelParser.sln @@ -9,9 +9,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NovelParserWPF", "NovelPars EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NovelParserBLLTests", "NovelParserBLLTests\NovelParserBLLTests.csproj", "{77CC6381-193E-4EEB-8CAC-9C2CB5B1AB63}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExampleUsingPDFFileGenerator", "ExampleUsingPDFFileGenerator\ExampleUsingPDFFileGenerator.csproj", "{9859CE98-B647-41A1-86DA-91505E03F62B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HTMLQuestPDF", "HTMLQuestPDF\HTMLQuestPDF.csproj", "{4A990109-E829-46A3-8559-F8DE7771BE4D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HTMLQuestPDF", "HTMLQuestPDF\HTMLQuestPDF.csproj", "{4A990109-E829-46A3-8559-F8DE7771BE4D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -31,10 +29,6 @@ Global {77CC6381-193E-4EEB-8CAC-9C2CB5B1AB63}.Debug|Any CPU.Build.0 = Debug|Any CPU {77CC6381-193E-4EEB-8CAC-9C2CB5B1AB63}.Release|Any CPU.ActiveCfg = Release|Any CPU {77CC6381-193E-4EEB-8CAC-9C2CB5B1AB63}.Release|Any CPU.Build.0 = Release|Any CPU - {9859CE98-B647-41A1-86DA-91505E03F62B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9859CE98-B647-41A1-86DA-91505E03F62B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9859CE98-B647-41A1-86DA-91505E03F62B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9859CE98-B647-41A1-86DA-91505E03F62B}.Release|Any CPU.Build.0 = Release|Any CPU {4A990109-E829-46A3-8559-F8DE7771BE4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4A990109-E829-46A3-8559-F8DE7771BE4D}.Debug|Any CPU.Build.0 = Debug|Any CPU {4A990109-E829-46A3-8559-F8DE7771BE4D}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/NovelParserBLL/Comparer/ChaptersComparer.cs b/NovelParserBLL/Comparer/ChaptersComparer.cs new file mode 100644 index 0000000..82448b7 --- /dev/null +++ b/NovelParserBLL/Comparer/ChaptersComparer.cs @@ -0,0 +1,29 @@ +using System.Globalization; +using NovelParserBLL.Models; + +namespace NovelParserBLL.Comparer; + +public class ChaptersComparer : IComparer +{ + public int Compare(Chapter? x, Chapter? y) + { + if (x == y) return 0; + if (y is null) return 1; + if (x?.Number is null) return -1; + if (y.Number is null) return 1; + + if (x.Volume > y.Volume) return 1; + if (x.Volume < y.Volume) return -1; + + var xVal = ToFloat(x.Number); + var yVal = ToFloat(y.Number); + return xVal.CompareTo(yVal); + } + + private static float ToFloat(string value) + { + value = value.Replace(",","."); + return float.TryParse(value, NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var result) + ? result : 0f; + } +} diff --git a/NovelParserBLL/Comparer/ChaptersEqualityComparer.cs b/NovelParserBLL/Comparer/ChaptersEqualityComparer.cs new file mode 100644 index 0000000..4654c35 --- /dev/null +++ b/NovelParserBLL/Comparer/ChaptersEqualityComparer.cs @@ -0,0 +1,19 @@ +using NovelParserBLL.Models; + +namespace NovelParserBLL.Comparer; + +public class ChaptersEqualityComparer : IEqualityComparer +{ + public bool Equals(Chapter? x, Chapter? y) + { + if (ReferenceEquals(x, y)) return true; + if (x is null || y is null) return false; + if (x.GetType() != y.GetType()) return false; + return x.Number == y.Number && x.Volume == y.Volume; + } + + public int GetHashCode(Chapter obj) + { + return HashCode.Combine(obj.Number, obj.Volume); + } +} diff --git a/NovelParserBLL/Extensions/ListChaptersExtension.cs b/NovelParserBLL/Extensions/ListChaptersExtension.cs new file mode 100644 index 0000000..77f2cec --- /dev/null +++ b/NovelParserBLL/Extensions/ListChaptersExtension.cs @@ -0,0 +1,24 @@ +using NovelParserBLL.Comparer; +using NovelParserBLL.Models; + +namespace NovelParserBLL.Extensions; + +public static class ListChaptersExtension +{ + public static List ForLoad(this List chapters, bool includeImages) + { + return chapters.Where(ch => + string.IsNullOrEmpty(ch.Content) || (!ch.ImagesLoaded || !ch.Images.Exists()) && includeImages) + .ToList(); + } + + public static List SortChapters(this List chapters) + { + return chapters.OrderBy(c => c, new ChaptersComparer()).ToList(); + } + + public static int VolumesCount(this List chapters) + { + return chapters.Select(c => c.Volume).ToHashSet().Count; + } +} \ No newline at end of file diff --git a/NovelParserBLL/Extensions/MarkupFormattableExtension.cs b/NovelParserBLL/Extensions/MarkupFormattableExtension.cs new file mode 100644 index 0000000..51f5af6 --- /dev/null +++ b/NovelParserBLL/Extensions/MarkupFormattableExtension.cs @@ -0,0 +1,14 @@ +using AngleSharp.Xhtml; +using AngleSharp; + +namespace NovelParserBLL.Extensions; + +public static class MarkupFormattableExtension +{ + public static async Task ToXhtmlAsync(this IMarkupFormattable html) + { + await using var sw = new StringWriter(); + await Task.Run(()=>html.ToHtml(sw, new XhtmlMarkupFormatter(true))); + return sw.ToString(); + } +} diff --git a/NovelParserBLL/Extensions/NovelExtensions.cs b/NovelParserBLL/Extensions/NovelExtensions.cs new file mode 100644 index 0000000..ab72c20 --- /dev/null +++ b/NovelParserBLL/Extensions/NovelExtensions.cs @@ -0,0 +1,39 @@ +using NovelParserBLL.Comparer; +using NovelParserBLL.Models; + +namespace NovelParserBLL.Extensions; + +public static class NovelExtensions +{ + public static void Merge(this Novel first, Novel second) + { + first.URL ??= second.URL; + first.Name ??= second.Name; + first.Author ??= second.Author; + first.Description ??= second.Description; + first.Cover ??= second.Cover; + + if (first.ChaptersByGroup != null && second.ChaptersByGroup != null) + { + foreach (var team in second.ChaptersByGroup) + { + if (first.ChaptersByGroup.TryGetValue(team.Key, out var chapters)) + { + foreach (var item in team.Value + .Where(item => !chapters.Contains(item, new ChaptersEqualityComparer()))) + { + chapters.Add(item); + } + } + else + { + first.ChaptersByGroup.Add(team.Key, team.Value); + } + } + } + else + { + first.ChaptersByGroup ??= second.ChaptersByGroup; + } + } +} diff --git a/NovelParserBLL/Extensions/SortedListChaptersExtension.cs b/NovelParserBLL/Extensions/SortedListChaptersExtension.cs deleted file mode 100644 index 9033926..0000000 --- a/NovelParserBLL/Extensions/SortedListChaptersExtension.cs +++ /dev/null @@ -1,12 +0,0 @@ -using NovelParserBLL.Models; - -namespace NovelParserBLL.Extensions -{ - internal static class SortedListChaptersExtension - { - public static List ForLoad(this SortedList chapters, bool includeImages) - { - return chapters.Select(v => v.Value).Where(ch => string.IsNullOrEmpty(ch.Content) || (!ch.ImagesLoaded || !ch.Images.Exists()) && includeImages).ToList(); - } - } -} \ No newline at end of file diff --git a/NovelParserBLL/Extensions/StringExtension.cs b/NovelParserBLL/Extensions/StringExtension.cs index b974f77..baccabc 100644 --- a/NovelParserBLL/Extensions/StringExtension.cs +++ b/NovelParserBLL/Extensions/StringExtension.cs @@ -8,5 +8,34 @@ public static string RemoveWhiteSpaces(this string str) { return Regex.Replace(str, @"\s+", ""); } + + /// + /// Deletes substring between markers (include markers) from string + /// Markers must be unique + /// + /// + /// + /// + /// + /// + public static string DeleteSubstring(this string str, string startMarker, string endMarker) + { + var startIndex = str.IndexOf(startMarker, 0, StringComparison.OrdinalIgnoreCase); + var endIndex = str.IndexOf(endMarker, 0, StringComparison.OrdinalIgnoreCase); + + if (startIndex == -1 || endIndex == -1) + throw new ArgumentException("Could not find markers."); + + if (startIndex >= endIndex) + throw new ArgumentException("Markers positioning error."); + + var substringLen = endIndex - startIndex + endMarker.Length; + return str.Remove(startIndex, substringLen); + } + + public static string DeleteSubstring(this string str, string subString) + { + return str.Replace(subString, ""); + } } } \ No newline at end of file diff --git a/NovelParserBLL/FileGenerators/EPUB/EPUBGenerationParams.cs b/NovelParserBLL/FileGenerators/EPUB/EPUBGenerationParams.cs index 78f7b6b..dc8feff 100644 --- a/NovelParserBLL/FileGenerators/EPUB/EPUBGenerationParams.cs +++ b/NovelParserBLL/FileGenerators/EPUB/EPUBGenerationParams.cs @@ -1,12 +1,11 @@ using NovelParserBLL.Models; -namespace NovelParserBLL.FileGenerators.EPUB +namespace NovelParserBLL.FileGenerators.EPUB; + +public class EPUBGenerationParams : GenerationParams { - public class EPUBGenerationParams : GenerationParams + public EPUBGenerationParams(string filePath, Novel novel, string group, string pattern) : + base(filePath, novel, group, pattern) { - public EPUBGenerationParams(string filePath, Novel novel, string group, string pattern) : - base(filePath, novel, group, pattern) - { - } } } \ No newline at end of file diff --git a/NovelParserBLL/FileGenerators/EPUB/EpubDocument.cs b/NovelParserBLL/FileGenerators/EPUB/EpubDocument.cs new file mode 100644 index 0000000..b9facf6 --- /dev/null +++ b/NovelParserBLL/FileGenerators/EPUB/EpubDocument.cs @@ -0,0 +1,67 @@ +using net.vieapps.Components.Utility.Epub; +using NovelParserBLL.Utilities; + +namespace NovelParserBLL.FileGenerators.EPUB; + +public class EpubDocument +{ + private readonly Document _epubDocument; + private readonly string _fullPathToEpub; + private readonly string _imagesDir; + private int _pagePlayOrder; + public EpubDocument(string fullPathToEpubFile, string imagesDirXhtml) + { + _imagesDir = imagesDirXhtml; + _pagePlayOrder = 1; + _epubDocument = new Document(); + _fullPathToEpub = fullPathToEpubFile; + _epubDocument.AddMetaItem("dc:language", "en"); + } + + public void AddAuthor(string? author) + { + if (!string.IsNullOrEmpty(author)) + _epubDocument.AddAuthor(author); + } + + public void AddTitle(string? title) + { + if (!string.IsNullOrEmpty(title)) + _epubDocument.AddTitle(title); + } + + public void AddDescription(string? description) + { + if (!string.IsNullOrEmpty(description)) + _epubDocument.AddDescription(description); + } + + public Task AddImageAsync(string imageName, byte[] imageBinary) + { + return Task.Run(() => + { + var path = HtmlPathHelper.Combine(_imagesDir, imageName); + var tag = _epubDocument.AddImageData(path, imageBinary); + _epubDocument.AddMetaItem(imageName, tag); + }); + } + + /// + /// Добавить страницу в контейнер. Страницы добавляются в порядке вызовов метода + /// + /// Имя файла в контейнере (без расширения) + /// Метка в оглавлении + /// Содержимое xhtml файла страницы + public void AddPage(string pageName, string label, string content) + { + var pageXhtmlName = $"{pageName}.xhtml"; + _epubDocument.AddXhtmlData(pageXhtmlName, content); + _epubDocument.AddNavPoint(label, pageXhtmlName, _pagePlayOrder); + _pagePlayOrder++; + } + + public Task GenerateDocumentAsync() + { + return Task.Run(() => { _epubDocument.Generate(_fullPathToEpub); }); + } +} diff --git a/NovelParserBLL/FileGenerators/EPUB/EpubFileGenerator.cs b/NovelParserBLL/FileGenerators/EPUB/EpubFileGenerator.cs index 8fb527e..7299b0e 100644 --- a/NovelParserBLL/FileGenerators/EPUB/EpubFileGenerator.cs +++ b/NovelParserBLL/FileGenerators/EPUB/EpubFileGenerator.cs @@ -1,41 +1,96 @@ -using EpubSharp; +using NovelParserBLL.Extensions; +using NovelParserBLL.FileGenerators.EPUB.PageModels; +using NovelParserBLL.Models; -namespace NovelParserBLL.FileGenerators.EPUB +namespace NovelParserBLL.FileGenerators.EPUB; + +internal class EpubFileGenerator : IFileGenerator { - internal class EpubFileGenerator : IFileGenerator + private const string ImagesXhtmlDir = "Images"; + private readonly ChapterPage _emptyChapterPage; + + public EpubFileGenerator() + { + _emptyChapterPage = new ChapterPage(new ChapterPageTemplate()); + } + + public async Task Generate(EPUBGenerationParams generationParams) + { + var novel = generationParams.Novel + ?? throw new ApplicationException("Novel object not set."); + var chapters = generationParams.Chapters; + + var epubDoc = new EpubDocument(generationParams.FilePath, ImagesXhtmlDir); + + novel.Name = !string.IsNullOrWhiteSpace(novel.Name) ? novel.Name : "(без названия)"; + novel.Author = !string.IsNullOrWhiteSpace(novel.Author) ? novel.Name : "(нет автора)"; + novel.Description = !string.IsNullOrWhiteSpace(novel.Description) ? novel.Name : "(нет описания)"; + + AddNovelInfo(epubDoc, novel); + await AddCoverPage(epubDoc, novel.Name, novel.Cover); + await AddChapters(epubDoc, chapters); + await epubDoc.GenerateDocumentAsync(); + } + + private static void AddNovelInfo(EpubDocument epubDoc, Novel novel) + { + epubDoc.AddAuthor(novel.Author); + epubDoc.AddTitle(novel.Name); + epubDoc.AddDescription(novel.Description); + } + + private static async Task AddCoverPage(EpubDocument epubDoc, string novelName, ImageInfo? cover) + { + var coverPageBuilder = new CoverPageModel(new CoverPageTemplate(), ImagesXhtmlDir) + { + Title = novelName + }; + + if (cover != null && !string.IsNullOrEmpty(cover.Name)) + { + var coverBinary = await cover.GetByteArray(); + coverPageBuilder.CoverImageName = cover.Name; + if (coverBinary != null) + await epubDoc.AddImageAsync(cover.Name, coverBinary); + } + + var coverPage = coverPageBuilder.GetContent(); + epubDoc.AddPage("Cover", "Постер", coverPage); + } + + private async Task AddChapters(EpubDocument epubDoc, List chapters) { - public Task Generate(EPUBGenerationParams generationParams) + var volumes = chapters.VolumesCount(); + + foreach (var chapter in chapters.SortChapters()) { - return Task.Run(() => + var chapNum = (volumes <= 1 ? "" : $"Том {chapter.Volume}, ") + + $"Глава {chapter.Number}"; + + var page = new ChapterPageModel(_emptyChapterPage, ImagesXhtmlDir) { - var novel = generationParams.Novel; - - EpubWriter writer = new EpubWriter(); - - writer.AddAuthor(novel.Author); - - if (novel.Cover?.TryGetByteArray(out byte[]? cover) ?? false) - { - writer.SetCover(cover, ImageFormat.Png); - } - - writer.SetTitle(novel.Name); - - foreach (var chapter in generationParams.Chapters) - { - var title = string.IsNullOrEmpty(chapter.Value.Name) ? $"Глава {chapter.Value.Number}" : chapter.Value.Name; - var content = $"

{title}

" + chapter.Value.Content; - writer.AddChapter(title, content); - foreach (var item in chapter.Value.Images) - { - if (item.TryGetByteArray(out byte[]? img)) - { - writer.AddFile(item.Name, img, EpubSharp.Format.EpubContentType.ImagePng); - } - } - } - writer.Write(generationParams.FilePath); - }); + ChapterNumber = chapNum, + ChapterTitle = chapter.Name ?? string.Empty, + Content = chapter.Content ?? string.Empty, + Images = chapter.Images + }; + var pageContent = await page.GetContent(); + var pageName = $"chapter_v{chapter.Volume}_{chapter.Number}"; + var label = page.ChapterNumber + + (string.IsNullOrEmpty(page.ChapterTitle) ? "" : $" - {page.ChapterTitle}"); + + epubDoc.AddPage(pageName, label, pageContent); + await AddPageImages(epubDoc, page); + } + } + + private static async Task AddPageImages(EpubDocument doc, ChapterPageModel page) + { + foreach (var image in page.Images) + { + var binary = await page.GetImageBinary(image); + if (binary != null) + await doc.AddImageAsync(image.Name, binary); } } } \ No newline at end of file diff --git a/NovelParserBLL/FileGenerators/EPUB/PageModels/BaseTemplate.cs b/NovelParserBLL/FileGenerators/EPUB/PageModels/BaseTemplate.cs new file mode 100644 index 0000000..5f7cdac --- /dev/null +++ b/NovelParserBLL/FileGenerators/EPUB/PageModels/BaseTemplate.cs @@ -0,0 +1,13 @@ +namespace NovelParserBLL.FileGenerators.EPUB.PageModels; + +public abstract class BaseTemplate +{ + private readonly Lazy lazyTemplate; + + protected BaseTemplate(string templateResource) + { + lazyTemplate = new Lazy(() => + ResourceReader.GetResourceData(templateResource).Trim()); + } + public string Template => lazyTemplate.Value; +} diff --git a/NovelParserBLL/FileGenerators/EPUB/PageModels/ChapterPage.cs b/NovelParserBLL/FileGenerators/EPUB/PageModels/ChapterPage.cs new file mode 100644 index 0000000..b227950 --- /dev/null +++ b/NovelParserBLL/FileGenerators/EPUB/PageModels/ChapterPage.cs @@ -0,0 +1,26 @@ +using NovelParserBLL.FileGenerators.EPUB.PageModels.Interfaces; + +namespace NovelParserBLL.FileGenerators.EPUB.PageModels; + +public class ChapterPage +{ + private readonly IChapterPageTemplate _template; + public ChapterPage(IChapterPageTemplate template) + { + _template = template; + Title = string.Empty; + Name = string.Empty; + Content = string.Empty; + } + public string Title { get; set; } + public string Name { get; set; } + public string Content { get; set; } + + public string GetPage() + { + return _template.Template.Replace(_template.TitlePlaceholder, Title) + .Replace(_template.ContentPlaceholder, Content) + .Replace(_template.NamePlaceholder, Name) + .Trim(); + } +} diff --git a/NovelParserBLL/FileGenerators/EPUB/PageModels/ChapterPageModel.cs b/NovelParserBLL/FileGenerators/EPUB/PageModels/ChapterPageModel.cs new file mode 100644 index 0000000..f343c64 --- /dev/null +++ b/NovelParserBLL/FileGenerators/EPUB/PageModels/ChapterPageModel.cs @@ -0,0 +1,98 @@ +using AngleSharp; +using AngleSharp.Dom; +using AngleSharp.Html.Dom; +using AngleSharp.Html.Parser; +using NovelParserBLL.Extensions; +using NovelParserBLL.Models; +using NovelParserBLL.Utilities; + +namespace NovelParserBLL.FileGenerators.EPUB.PageModels; + +public class ChapterPageModel +{ + private readonly ChapterPage _page; + private readonly string _imagesXhtmlDir; + private readonly Dictionary _imagesBinary; + + public ChapterPageModel(ChapterPage emptyPage, string imagesXhtmlDir) + { + _page = emptyPage; + _imagesXhtmlDir = imagesXhtmlDir; + Content = string.Empty; + ChapterNumber = string.Empty; + ChapterTitle = string.Empty; + Images = new List(); + _imagesBinary = new Dictionary(); + } + + public string Content { get; set; } + public string ChapterNumber { get; set; } + public string ChapterTitle { get; set;} + public List Images { get; set; } + + public async Task GetContent() + { + _page.Name = ChapterNumber; + _page.Title = ChapterTitle; + + var parser = new HtmlParser(); + var doc = await parser.ParseDocumentAsync(Content); + + if (doc.Body == null) + return _page.GetPage(); + + await FixImagesLinksGetBinaryData(doc); + _page.Content = await ContentToXhtmlAsync(doc.Body); + + return _page.GetPage(); + } + + public Task GetImageBinary(ImageInfo image) + { + return Task.Run(async () => + { + if (!Images.Contains(image)) return null; + if (_imagesBinary.ContainsKey(image)) return _imagesBinary[image]; + + var binary = await image.GetByteArray(); + _imagesBinary.Add(image, binary); + return binary; + }); + } + + private async Task FixImagesLinksGetBinaryData(IParentNode html) + { + var images = html.QuerySelectorAll("img") + .Cast(); + + foreach (var image in images) + { + var imageName = Path.GetFileName(image.Source) ?? string.Empty; + var imageInfo = Images.Find(i => + string.Equals(i.Name, imageName, StringComparison.OrdinalIgnoreCase) && i.Exists); + + image.Source = string.Empty; + + if (!string.IsNullOrEmpty(imageName) && imageInfo != null) + { + var imageBinary = await GetImageBinary(imageInfo); + if (imageBinary != null) + { + image.Source = HtmlPathHelper.Combine(_imagesXhtmlDir, imageName); + } + } + if (string.IsNullOrEmpty(image.Source)) + image.Remove(); + } + } + + private static async Task ContentToXhtmlAsync(IMarkupFormattable content) + { + var result = (await content.ToXhtmlAsync()) + .Replace("", "", StringComparison.OrdinalIgnoreCase) + .Replace("", "", StringComparison.OrdinalIgnoreCase) + .Trim(); + + return result; + } +} diff --git a/NovelParserBLL/FileGenerators/EPUB/PageModels/ChapterPageTemplate.cs b/NovelParserBLL/FileGenerators/EPUB/PageModels/ChapterPageTemplate.cs new file mode 100644 index 0000000..532b757 --- /dev/null +++ b/NovelParserBLL/FileGenerators/EPUB/PageModels/ChapterPageTemplate.cs @@ -0,0 +1,16 @@ +using NovelParserBLL.FileGenerators.EPUB.PageModels.Interfaces; + +namespace NovelParserBLL.FileGenerators.EPUB.PageModels; + +public class ChapterPageTemplate : BaseTemplate, IChapterPageTemplate +{ + public ChapterPageTemplate() : base("PageTemplate.xhtml") + { + TitlePlaceholder = "{title}"; + NamePlaceholder = "{number}"; + ContentPlaceholder = "{content}"; + } + public string TitlePlaceholder { get; } + public string NamePlaceholder { get; } + public string ContentPlaceholder { get; } +} diff --git a/NovelParserBLL/FileGenerators/EPUB/PageModels/CoverPageModel.cs b/NovelParserBLL/FileGenerators/EPUB/PageModels/CoverPageModel.cs new file mode 100644 index 0000000..62332fa --- /dev/null +++ b/NovelParserBLL/FileGenerators/EPUB/PageModels/CoverPageModel.cs @@ -0,0 +1,43 @@ +using NovelParserBLL.Extensions; +using NovelParserBLL.FileGenerators.EPUB.PageModels.Interfaces; +using NovelParserBLL.Utilities; + +namespace NovelParserBLL.FileGenerators.EPUB.PageModels; + +public class CoverPageModel +{ + private readonly ICoverPageTemplate template; + private readonly string _imagesXhtmlDir; + public CoverPageModel(ICoverPageTemplate coverPageTemplate, string imagesXhtmlDir) + { + template = coverPageTemplate; + _imagesXhtmlDir = imagesXhtmlDir; + Title = string.Empty; + CoverImageName = string.Empty; + } + public string Title { get; set; } + public string CoverImageName { get; set; } + + public string GetContent() + { + var coverPage = template.Template; + coverPage = coverPage.Replace(template.TitlePlaceholder, Title); + + if (string.IsNullOrWhiteSpace(CoverImageName)) + { + coverPage = coverPage + .DeleteSubstring(template.ImageTagLimiters.Start, template.ImageTagLimiters.End); + } + else + { + var imagePath = HtmlPathHelper.Combine(_imagesXhtmlDir, CoverImageName); + coverPage = coverPage.DeleteSubstring(template.ImageTagLimiters.Start) + .DeleteSubstring(template.ImageTagLimiters.End) + .Replace(template.ImagePathPlaceholder, imagePath); + } + + return coverPage; + } + + +} diff --git a/NovelParserBLL/FileGenerators/EPUB/PageModels/CoverPageTemplate.cs b/NovelParserBLL/FileGenerators/EPUB/PageModels/CoverPageTemplate.cs new file mode 100644 index 0000000..c9611e3 --- /dev/null +++ b/NovelParserBLL/FileGenerators/EPUB/PageModels/CoverPageTemplate.cs @@ -0,0 +1,17 @@ +using NovelParserBLL.FileGenerators.EPUB.PageModels.Interfaces; + +namespace NovelParserBLL.FileGenerators.EPUB.PageModels; + +public class CoverPageTemplate : BaseTemplate, ICoverPageTemplate +{ + public CoverPageTemplate() : base("CoverTemplate.xhtml") + { + TitlePlaceholder = "{title}"; + ImageTagLimiters = new Limiters("{start}", "{end}"); + ImagePathPlaceholder = "{cover}"; + } + + public string TitlePlaceholder { get; } + public string ImagePathPlaceholder { get; } + public Limiters ImageTagLimiters { get; } +} diff --git a/NovelParserBLL/FileGenerators/EPUB/PageModels/Interfaces/IChapterPageTemplate.cs b/NovelParserBLL/FileGenerators/EPUB/PageModels/Interfaces/IChapterPageTemplate.cs new file mode 100644 index 0000000..0f81d04 --- /dev/null +++ b/NovelParserBLL/FileGenerators/EPUB/PageModels/Interfaces/IChapterPageTemplate.cs @@ -0,0 +1,9 @@ +namespace NovelParserBLL.FileGenerators.EPUB.PageModels.Interfaces; + +public interface IChapterPageTemplate +{ + public string Template { get; } + public string TitlePlaceholder { get; } + public string NamePlaceholder { get; } + public string ContentPlaceholder { get; } +} diff --git a/NovelParserBLL/FileGenerators/EPUB/PageModels/Interfaces/ICoverPageTemplate.cs b/NovelParserBLL/FileGenerators/EPUB/PageModels/Interfaces/ICoverPageTemplate.cs new file mode 100644 index 0000000..803fbba --- /dev/null +++ b/NovelParserBLL/FileGenerators/EPUB/PageModels/Interfaces/ICoverPageTemplate.cs @@ -0,0 +1,10 @@ +namespace NovelParserBLL.FileGenerators.EPUB.PageModels.Interfaces; + +public record Limiters(string Start, string End); +public interface ICoverPageTemplate +{ + public string Template { get; } + public string TitlePlaceholder { get; } + public string ImagePathPlaceholder { get; } + public Limiters ImageTagLimiters { get; } +} diff --git a/NovelParserBLL/FileGenerators/GenerationParams.cs b/NovelParserBLL/FileGenerators/GenerationParams.cs index 6b83ec0..513f2eb 100644 --- a/NovelParserBLL/FileGenerators/GenerationParams.cs +++ b/NovelParserBLL/FileGenerators/GenerationParams.cs @@ -23,6 +23,6 @@ protected GenerationParams(string filePath, Novel novel, string group, string pa Pattern = pattern; } - public SortedList Chapters => Novel[Group, Pattern]; + public List Chapters => Novel[Group, Pattern]; } } \ No newline at end of file diff --git a/NovelParserBLL/FileGenerators/PDF/PdfFileGenerator.cs b/NovelParserBLL/FileGenerators/PDF/PdfFileGenerator.cs index 1e4a72b..d27c8a2 100644 --- a/NovelParserBLL/FileGenerators/PDF/PdfFileGenerator.cs +++ b/NovelParserBLL/FileGenerators/PDF/PdfFileGenerator.cs @@ -1,60 +1,63 @@ -using HTMLQuestPDF; -using HTMLQuestPDF.Extensions; +using HTMLQuestPDF.Extensions; +using NovelParserBLL.Extensions; using NovelParserBLL.Models; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Previewer; -namespace NovelParserBLL.FileGenerators.PDF +using Document = QuestPDF.Fluent.Document; + +namespace NovelParserBLL.FileGenerators.PDF; + +public class PDFFileGenerator : IFileGenerator { - public class PDFFileGenerator : IFileGenerator + public Task Generate(PDFGenerationParams generationParams) { - public Task Generate(PDFGenerationParams generationParams) + return Task.Run(() => { - return Task.Run(() => - { - GetDocumnet(generationParams).GeneratePdf(generationParams.FilePath); - }); - } + GetDocument(generationParams).GeneratePdf(generationParams.FilePath); + }); + } - public void ShowInPreviewer(PDFGenerationParams generationParams) - { - var document = GetDocumnet(generationParams); - document.ShowInPreviewer(); - document.GeneratePdf(generationParams.FilePath); - } + public void ShowInPreviewer(PDFGenerationParams generationParams) + { + var document = GetDocument(generationParams); + document.ShowInPreviewer(); + document.GeneratePdf(generationParams.FilePath); + } + //todo переделать + private Document GetDocument(PDFGenerationParams generationParams) + { + var novel = generationParams.Novel; - private Document GetDocumnet(PDFGenerationParams generationParams) + var chaptersWithCover = new List(generationParams.Chapters); + if (novel.Cover != null) { - var novel = generationParams.Novel; - - var chaptersWithCover = new SortedList(generationParams.Chapters); - if (novel.Cover != null) + chaptersWithCover.Add(new Chapter { - chaptersWithCover.Add(-1, new Chapter() + Name = "Cover", + Volume = 0, + Number = "0", + Content = $"
", + Images = new List { - Name = "Cover", - Content = $"
", - Images = new List() - { - novel.Cover - } - }); - } + novel.Cover + } + }); + } - QuestPDF.Settings.CheckIfAllTextGlyphsAreAvailable = false; - return Document.Create(container => + QuestPDF.Settings.CheckIfAllTextGlyphsAreAvailable = false; + return Document.Create(container => + { + foreach (var item in chaptersWithCover.SortChapters()) { - foreach (var item in chaptersWithCover.Values) + string GetImagePath(string src) { - var getImagePath = (string src) => - { - return item.Images.Find(i => i.Name.Equals(src))?.FullPath; - }; - - container.HTMLPage(item.Content ?? "", getImagePath, PageSizes.A4, 1, 0.5f, QuestPDF.Infrastructure.Unit.Centimetre); + return item.Images.Find(i => i.Name.Equals(src))?.FullPath ?? string.Empty; } - }); - } + + container.HTMLPage(item.Content ?? "", GetImagePath, PageSizes.A4, 1, 0.5f, QuestPDF.Infrastructure.Unit.Centimetre); + } + }); } } \ No newline at end of file diff --git a/NovelParserBLL/Models/Chapter.cs b/NovelParserBLL/Models/Chapter.cs index 8c48557..ab29407 100644 --- a/NovelParserBLL/Models/Chapter.cs +++ b/NovelParserBLL/Models/Chapter.cs @@ -1,12 +1,12 @@ -namespace NovelParserBLL.Models +namespace NovelParserBLL.Models; + +public class Chapter { - public class Chapter - { - public string? Name { get; set; } - public string? Url { get; set; } - public string? Number { get; set; } - public string? Content { get; set; } - public List Images { get; set; } = new List(); - public bool ImagesLoaded { get; set; } - } + public string? Name { get; set; } + public string? Url { get; set; } + public string? Number { get; set; } + public int Volume { get; set; } + public string? Content { get; set; } + public List Images { get; set; } = new (); + public bool ImagesLoaded { get; set; } } \ No newline at end of file diff --git a/NovelParserBLL/Models/ImageInfo.cs b/NovelParserBLL/Models/ImageInfo.cs index 87d5f90..c9147a4 100644 --- a/NovelParserBLL/Models/ImageInfo.cs +++ b/NovelParserBLL/Models/ImageInfo.cs @@ -1,38 +1,45 @@ using Newtonsoft.Json; -namespace NovelParserBLL.Models +namespace NovelParserBLL.Models; + +public class ImageInfo { - public class ImageInfo + public ImageInfo(string directory, string url) + { + var ext = Path.GetExtension(url); + Name = $"{Guid.NewGuid()}{ext}"; + FullPath = Path.Combine(directory, Name); + URL = url; + } + + [JsonConstructor] + public ImageInfo(string fullPath, string name, string url) + { + FullPath = fullPath; + Name = name; + URL = url; + } + + [JsonIgnore] + public bool Exists => !string.IsNullOrEmpty(FullPath) && File.Exists(FullPath); + + [JsonIgnore] + public string NameFromURL => Path.GetFileName(URL); + + public string FullPath { get; } + public string Name { get; } + public string URL { get; } + + public bool TryGetByteArray(out byte[]? img) { - public ImageInfo(string directory, string url) - { - Name = $"{Guid.NewGuid()}.image"; - FullPath = Path.Combine(directory, Name); - URL = url; - } - - [JsonConstructor] - public ImageInfo(string fullPath, string name, string url) - { - FullPath = fullPath; - Name = name; - URL = url; - } - - [JsonIgnore] - public bool Exists => !string.IsNullOrEmpty(FullPath) && File.Exists(FullPath); - - [JsonIgnore] - public string NameFromURL => URL.Substring(URL.LastIndexOf('/') + 1); - - public string FullPath { get; } - public string Name { get; } - public string URL { get; } - - public bool TryGetByteArray(out byte[]? img) - { - img = Exists ? File.ReadAllBytes(FullPath) : null; - return img != null; - } + img = Exists ? File.ReadAllBytes(FullPath) : null; + return img != null; } + + public Task GetByteArray() + { + return Task.Run(() => TryGetByteArray(out var bites) ? bites : null); + } + + public override int GetHashCode() => HashCode.Combine(FullPath, Name, URL); } \ No newline at end of file diff --git a/NovelParserBLL/Models/Novel.cs b/NovelParserBLL/Models/Novel.cs index 63a5476..9f8a2a7 100644 --- a/NovelParserBLL/Models/Novel.cs +++ b/NovelParserBLL/Models/Novel.cs @@ -8,7 +8,7 @@ namespace NovelParserBLL.Models public class Novel { public string? Author { get; set; } - public Dictionary>? ChaptersByGroup { get; set; } + public Dictionary>? ChaptersByGroup { get; set; } public ImageInfo? Cover { get; set; } public string? Description { get; set; } public string? Name { get; set; } @@ -17,96 +17,55 @@ public class Novel [JsonIgnore] public string DownloadFolderName => Path.Combine(Resources.CacheFolder, FileHelper.RemoveInvalidFilePathCharacters(URL ?? "")); - public SortedList this[string? group, string? pattern] + //todo Переработать с учетом томов + public List this[string? group, string? pattern] { get { + var result = new List(); + if (string.IsNullOrEmpty(group) || string.IsNullOrEmpty(pattern) || - (!ChaptersByGroup?.ContainsKey(group) ?? false)) return new SortedList(); + (!ChaptersByGroup?.ContainsKey(group) ?? false)) + return result; - var chapters = this.ChaptersByGroup![group]; + var chapters = ChaptersByGroup?[group]; + if (chapters == null) + return result; - var result = new SortedList(chapters?.Count ?? 0); + return chapters.SortChapters(); + //var lastChapterIndex = chapters.Count - 1; + //var parts = pattern.RemoveWhiteSpaces().ToLower().Split(','); - if (chapters == null) return result; + //foreach (var part in parts) + //{ + // if (part.Equals("all")) return chapters; - var parts = pattern.RemoveWhiteSpaces().ToLower().Split(','); + // if (part.Contains('-')) + // { + // var nums = part.Split('-'); + // var containsNum1 = int.TryParse(nums[0], out var num1); + // var containsNum2 = int.TryParse(nums[1], out var num2); - var addRange = (int start, int end) => - { - if (end < start) (start, end) = (end, start); - start = Math.Max(1, start); - end = Math.Min(chapters.Last().Key, end); - for (int i = start; i <= end; i++) - { - if (!result.ContainsKey(i) && chapters.TryGetValue(i, out Chapter? ch)) - { - result.Add(i, ch); - } - } - }; + // var start = containsNum1 ? num1 : 1; + // var end = containsNum2 ? num2 : lastChapterIndex; - foreach (var part in parts) - { - if (part.Equals("all")) - { - return chapters; - } - if (part.Contains('-')) - { - var nums = part.Split('-'); + // if (end < start) (start, end) = (end, start); - bool containsNum1 = int.TryParse(nums[0], out int num1); - bool containsNum2 = int.TryParse(nums[1], out int num2); + // start = Math.Max(1, start); + // end = Math.Min(lastChapterIndex, end); - addRange(containsNum1 ? num1 : 1, containsNum2 ? num2 : chapters.Last().Key); - } - else if (int.TryParse(part, out int num)) - { - var index = Math.Min(chapters.Last().Key, num); - if (chapters.TryGetValue(index, out Chapter? ch)) - { - result.Add(index, ch); - } - } - } + // var list = chapters.Where(ch => ch.Id >= start && ch.Id <= end); + // result.AddRange(list); + // } + // else if (int.TryParse(part, out var num)) + // { + // var index = Math.Min(lastChapterIndex, num); + // result.Add(chapters[index]); + // } + //} - return result; - } - } - - public void Merge(Novel secondNovelInfo) - { - this.URL = this.URL ?? secondNovelInfo.URL; - this.Name = this.Name ?? secondNovelInfo.Name; - this.Author = this.Author ?? secondNovelInfo.Author; - this.Description = this.Description ?? secondNovelInfo.Description; - this.Cover = this.Cover ?? secondNovelInfo.Cover; - - if (this.ChaptersByGroup != null && secondNovelInfo.ChaptersByGroup != null) - { - foreach (var team in secondNovelInfo.ChaptersByGroup) - { - if (this.ChaptersByGroup.TryGetValue(team.Key, out SortedList? chapters)) - { - foreach (var item in team.Value) - { - if (!chapters.TryGetValue(item.Key, out Chapter? chapter)) - { - chapters.Add(item.Key, item.Value); - } - } - } - else - { - this.ChaptersByGroup.Add(team.Key, team.Value); - } - } - } - else - { - this.ChaptersByGroup = this.ChaptersByGroup ?? secondNovelInfo.ChaptersByGroup; + //return result.OrderBy(c => c, new ChaptersComparer()).ToList(); } } } diff --git a/NovelParserBLL/NovelParserBLL.csproj b/NovelParserBLL/NovelParserBLL.csproj index cd052b3..9b1cc06 100644 --- a/NovelParserBLL/NovelParserBLL.csproj +++ b/NovelParserBLL/NovelParserBLL.csproj @@ -1,7 +1,7 @@ - net6.0 + net6.0-windows enable enable true @@ -17,20 +17,22 @@ - + + + + + + + - - - - - - - - + + + + diff --git a/NovelParserBLL/Parsers/DTO/ComicCurrent.cs b/NovelParserBLL/Parsers/DTO/ComicCurrent.cs new file mode 100644 index 0000000..25bda3b --- /dev/null +++ b/NovelParserBLL/Parsers/DTO/ComicCurrent.cs @@ -0,0 +1,13 @@ +namespace NovelParserBLL.Parsers.DTO; + +#nullable disable +public class ComicCurrent +{ + public int id { get; set; } + public int volume { get; set; } + public string number { get; set; } + public int index { get; set; } + public object status { get; set; } + public int price { get; set; } +} +#nullable restore \ No newline at end of file diff --git a/NovelParserBLL/Parsers/DTO/ComicImg.cs b/NovelParserBLL/Parsers/DTO/ComicImg.cs new file mode 100644 index 0000000..35d47b1 --- /dev/null +++ b/NovelParserBLL/Parsers/DTO/ComicImg.cs @@ -0,0 +1,10 @@ +namespace NovelParserBLL.Parsers.DTO; + +#nullable disable +public class ComicImg +{ + public string url { get; set; } + public string server { get; set; } + public bool supportWebp { get; set; } +} +#nullable restore \ No newline at end of file diff --git a/NovelParserBLL/Parsers/DTO/ComicMedia.cs b/NovelParserBLL/Parsers/DTO/ComicMedia.cs new file mode 100644 index 0000000..f3242d1 --- /dev/null +++ b/NovelParserBLL/Parsers/DTO/ComicMedia.cs @@ -0,0 +1,12 @@ +namespace NovelParserBLL.Parsers.DTO; + +#nullable disable +public class ComicMedia +{ + public int id { get; set; } + public string slug { get; set; } + public int type { get; set; } + public int caution { get; set; } + public int close_view { get; set; } +} +#nullable restore \ No newline at end of file diff --git a/NovelParserBLL/Parsers/DTO/ComicMediaInfo.cs b/NovelParserBLL/Parsers/DTO/ComicMediaInfo.cs new file mode 100644 index 0000000..fb48f39 --- /dev/null +++ b/NovelParserBLL/Parsers/DTO/ComicMediaInfo.cs @@ -0,0 +1,15 @@ +namespace NovelParserBLL.Parsers.DTO; + +#nullable disable +public class ComicMediaInfo +{ + public int page { get; set; } + public ComicMedia media { get; set; } + public object bookmark { get; set; } + public ComicCurrent current { get; set; } + public object next { get; set; } + public object prev { get; set; } + public ComicImg img { get; set; } + public ComicServers servers { get; set; } +} +#nullable restore \ No newline at end of file diff --git a/NovelParserBLL/Parsers/DTO/ComicPage.cs b/NovelParserBLL/Parsers/DTO/ComicPage.cs new file mode 100644 index 0000000..31f614c --- /dev/null +++ b/NovelParserBLL/Parsers/DTO/ComicPage.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace NovelParserBLL.Parsers.DTO; + +#nullable disable +public class ComicPage +{ + [JsonProperty("p")] + public int Page { get; set; } + [JsonProperty("u")] + public string Url { get; set; } +} +#nullable restore \ No newline at end of file diff --git a/NovelParserBLL/Parsers/DTO/ComicServers.cs b/NovelParserBLL/Parsers/DTO/ComicServers.cs new file mode 100644 index 0000000..5526b1e --- /dev/null +++ b/NovelParserBLL/Parsers/DTO/ComicServers.cs @@ -0,0 +1,11 @@ +namespace NovelParserBLL.Parsers.DTO; + +#nullable disable +public class ComicServers +{ + public string main { get; set; } + public string secondary { get; set; } + public string compress { get; set; } + public string fourth { get; set; } +} +#nullable restore \ No newline at end of file diff --git a/NovelParserBLL/Parsers/DTO/SiteBranch.cs b/NovelParserBLL/Parsers/DTO/SiteBranch.cs new file mode 100644 index 0000000..17ed9f6 --- /dev/null +++ b/NovelParserBLL/Parsers/DTO/SiteBranch.cs @@ -0,0 +1,9 @@ +namespace NovelParserBLL.Parsers.DTO; + +#nullable disable +public class SiteBranch +{ + public string id { get; set; } + public string name { get; set; } +} +#nullable restore \ No newline at end of file diff --git a/NovelParserBLL/Parsers/DTO/SiteChapterInfo.cs b/NovelParserBLL/Parsers/DTO/SiteChapterInfo.cs new file mode 100644 index 0000000..4012ff2 --- /dev/null +++ b/NovelParserBLL/Parsers/DTO/SiteChapterInfo.cs @@ -0,0 +1,21 @@ +namespace NovelParserBLL.Parsers.DTO; + +#nullable disable +public class SiteChapterInfo +{ + public int chapter_id { get; set; } + public string chapter_slug { get; set; } + public string chapter_name { get; set; } + public string chapter_number { get; set; } + public int chapter_volume { get; set; } + public int chapter_moderated { get; set; } + public int chapter_user_id { get; set; } + public string chapter_expired_at { get; set; } + public int chapter_scanlator_id { get; set; } + public string chapter_created_at { get; set; } + public object status { get; set; } + public int price { get; set; } + public string branch_id { get; set; } + public string username { get; set; } +} +#nullable restore \ No newline at end of file diff --git a/NovelParserBLL/Parsers/DTO/SiteChaptersInfo.cs b/NovelParserBLL/Parsers/DTO/SiteChaptersInfo.cs new file mode 100644 index 0000000..083df38 --- /dev/null +++ b/NovelParserBLL/Parsers/DTO/SiteChaptersInfo.cs @@ -0,0 +1,10 @@ +namespace NovelParserBLL.Parsers.DTO; + +#nullable disable +public class SiteChaptersInfo +{ + public SiteChapterInfo[] list { get; set; } + public SiteTeam[] teams { get; set; } + public SiteBranch[] branches { get; set; } +} +#nullable restore \ No newline at end of file diff --git a/NovelParserBLL/Parsers/DTO/SiteMangaInfo.cs b/NovelParserBLL/Parsers/DTO/SiteMangaInfo.cs new file mode 100644 index 0000000..669d733 --- /dev/null +++ b/NovelParserBLL/Parsers/DTO/SiteMangaInfo.cs @@ -0,0 +1,15 @@ +namespace NovelParserBLL.Parsers.DTO; + +#nullable disable +public class SiteMangaInfo +{ + public int id { get; set; } + public string name { get; set; } + public string rusName { get; set; } + public string engName { get; set; } + public string slug { get; set; } + public int status { get; set; } + public int chapters_count { get; set; } + public string[] altNames { get; set; } +} +#nullable restore \ No newline at end of file diff --git a/NovelParserBLL/Parsers/DTO/SiteNovelInfo.cs b/NovelParserBLL/Parsers/DTO/SiteNovelInfo.cs new file mode 100644 index 0000000..a33a5b1 --- /dev/null +++ b/NovelParserBLL/Parsers/DTO/SiteNovelInfo.cs @@ -0,0 +1,13 @@ +namespace NovelParserBLL.Parsers.DTO; + +#nullable disable +public class SiteNovelInfo +{ + public bool hasStickyPermission { get; set; } + public object bookmark { get; set; } + public bool auth { get; set; } + public string comments_version { get; set; } + public SiteMangaInfo manga { get; set; } + public SiteChaptersInfo chapters { get; set; } +} +#nullable restore \ No newline at end of file diff --git a/NovelParserBLL/Parsers/DTO/SitePivot.cs b/NovelParserBLL/Parsers/DTO/SitePivot.cs new file mode 100644 index 0000000..f55aed8 --- /dev/null +++ b/NovelParserBLL/Parsers/DTO/SitePivot.cs @@ -0,0 +1,7 @@ +namespace NovelParserBLL.Parsers.DTO; + +public class SitePivot +{ + public int manga_id { get; set; } + public int team_id { get; set; } +} diff --git a/NovelParserBLL/Parsers/DTO/SiteTeam.cs b/NovelParserBLL/Parsers/DTO/SiteTeam.cs new file mode 100644 index 0000000..13a168f --- /dev/null +++ b/NovelParserBLL/Parsers/DTO/SiteTeam.cs @@ -0,0 +1,16 @@ +namespace NovelParserBLL.Parsers.DTO; + +#nullable disable +public class SiteTeam +{ + public string name { get; set; } + public string alt_name { get; set; } + public string cover { get; set; } + public string slug { get; set; } + public int id { get; set; } + public object branch_id { get; set; } + public int sale { get; set; } + public string href { get; set; } + public SitePivot pivot { get; set; } +} +#nullable restore \ No newline at end of file diff --git a/NovelParserBLL/Parsers/INovelParser.cs b/NovelParserBLL/Parsers/INovelParser.cs index c94e15a..f529243 100644 --- a/NovelParserBLL/Parsers/INovelParser.cs +++ b/NovelParserBLL/Parsers/INovelParser.cs @@ -1,17 +1,12 @@ using NovelParserBLL.Models; -namespace NovelParserBLL.Parsers -{ - internal interface INovelParser - { - public ParserInfo ParserInfo { get; } - - Task LoadChapters(Novel novel, string group, string pattern, bool includeImages, CancellationToken cancellationToken); - - Task ParseCommonInfo(Novel novel, CancellationToken cancellationToken); +namespace NovelParserBLL.Parsers; - bool ValidateUrl(string url); - - string PrepareUrl(string url); - } +internal interface INovelParser +{ + public ParserInfo ParserInfo { get; } + Task LoadChapters(Novel novel, string group, string pattern, bool includeImages, CancellationToken token); + Task ParseCommonInfo(Novel novel, CancellationToken token); + bool ValidateUrl(string url); + string PrepareUrl(string url); } \ No newline at end of file diff --git a/NovelParserBLL/Parsers/Kemono/KemonoParser.cs b/NovelParserBLL/Parsers/Kemono/KemonoParser.cs index 0aa14ea..cd17e06 100644 --- a/NovelParserBLL/Parsers/Kemono/KemonoParser.cs +++ b/NovelParserBLL/Parsers/Kemono/KemonoParser.cs @@ -1,217 +1,205 @@ -using AngleSharp; +// ReSharper disable StringLiteralTypo +// ReSharper disable IdentifierTypo + using AngleSharp.Dom; using AngleSharp.Html.Parser; -using AngleSharp.Io; using NovelParserBLL.Extensions; using NovelParserBLL.Models; using NovelParserBLL.Properties; using NovelParserBLL.Services; using Sayaka.Common; using System.Text.RegularExpressions; +using AngleSharp.Html.Dom; +using NovelParserBLL.Services.Interfaces; -namespace NovelParserBLL.Parsers.kemono -{ - internal class KemonoParser : INovelParser - { - private readonly HttpClient httpClient = new HttpClient(); - private readonly HtmlParser parser = new HtmlParser(); - private readonly SetProgress setProgress; - private readonly string urlPattern = @"https:\/\/kemono\.party\/[a-zA-Z]+\/user\/\d*"; - private IBrowsingContext context = BrowsingContext.New(Configuration.Default.WithDefaultLoader()); - private DefaultHttpRequester requester = new DefaultHttpRequester(); +namespace NovelParserBLL.Parsers.kemono; - public KemonoParser(SetProgress setProgress) - { - this.setProgress = setProgress; - } +internal class KemonoParser : INovelParser +{ + private readonly IWebClient _webClient; + private readonly HtmlParser parser = new(); + private readonly SetProgress setProgress; + private readonly string urlPattern = @"https:\/\/kemono\.party\/[a-zA-Z]+\/user\/\d*"; + + public KemonoParser(SetProgress setProgress, IWebClient webClient) + { + this.setProgress = setProgress; + _webClient = webClient; + } - public ParserInfo ParserInfo => new ParserInfo("https://kemono.party/", "Kemono", ""); + public ParserInfo ParserInfo => new ("https://kemono.party/", "Kemono", ""); - public Task LoadChapters(Novel novel, string group, string pattern, bool includeImages, CancellationToken cancellationToken) - { - return Task.Run(async () => - { - int i = 1; - var nonLoadedChapters = novel[group, pattern].ForLoad(includeImages); - - setProgress(nonLoadedChapters.Count, 0, Resources.ProgressStatusParsing); - foreach (var chapter in nonLoadedChapters) - { - var doc = await context.OpenAsync(chapter.Url!); - - var attempt = 1; - while (doc.StatusCode != System.Net.HttpStatusCode.OK && attempt < 4) - { - doc = await context.OpenAsync(chapter.Url!); - if (cancellationToken.IsCancellationRequested) return; - RefreshContext(); - await Task.Delay(1000 * attempt++); - } - - chapter.Content = doc.QuerySelector(".post__body")?.InnerHtml ?? ""; - if (string.IsNullOrEmpty(chapter.Content)) chapter.Name += " "; - - await UpdateImages(chapter, novel.DownloadFolderName); - UpdateLinks(chapter); - setProgress(nonLoadedChapters.Count, i++, Resources.ProgressStatusParsing); - - if (cancellationToken.IsCancellationRequested) return; - } - }); - } + public async Task LoadChapters(Novel novel, string group, string pattern, bool includeImages, CancellationToken token) + { + var i = 1; + var nonLoadedChapters = novel[group, pattern].ForLoad(includeImages); - public async Task ParseCommonInfo(Novel novel, CancellationToken cancellationToken) + setProgress(nonLoadedChapters.Count, 0, Resources.ProgressStatusParsing); + foreach (var chapter in nonLoadedChapters.Where(chapter => chapter.Url != null)) { - setProgress(0, 0, Resources.ProgressStatusLoading); - if (novel == null || string.IsNullOrEmpty(novel.URL)) throw new ArgumentNullException(nameof(novel)); + var content = await _webClient.GetStringContentAsync(chapter.Url!, token: token); + if (string.IsNullOrWhiteSpace(content)) continue; + + var doc = await parser.ParseDocumentAsync(content, token); + + chapter.Content = doc.QuerySelector(".post__body")?.InnerHtml ?? ""; + if (string.IsNullOrEmpty(chapter.Content)) chapter.Name += " "; - var doc = await context.OpenAsync(novel.URL); + await UpdateImages(chapter, novel.DownloadFolderName); + UpdateLinks(chapter); - var chapterCount = GetCountChapters(doc); + setProgress(nonLoadedChapters.Count, i++, Resources.ProgressStatusParsing); - if (chapterCount != novel.ChaptersByGroup?.FirstOrDefault().Value.Count) - { - novel.ChaptersByGroup = new Dictionary>() { - { "none", await GetChapters(novel!.URL, chapterCount, cancellationToken) } - }; - } + if (token.IsCancellationRequested) return; + } + } - novel.Author = GetName(doc); - novel.Name = $"{novel.Author}'s Kemono"; + public async Task ParseCommonInfo(Novel novel, CancellationToken token) + { + setProgress(0, 0, Resources.ProgressStatusLoading); + if (novel == null || string.IsNullOrEmpty(novel.URL)) + throw new ArgumentNullException(nameof(novel)); - if (!(novel.Cover?.Exists ?? false)) - { - novel.Cover = await GetImg(GetCoverUrl(doc), novel.DownloadFolderName); - } + var doc = await GetHtmlDoc(novel.URL, token); + if (doc == null) return novel; - return novel; - } + var chapterCount = GetCountChapters(doc); - public string PrepareUrl(string url) + if (chapterCount != novel.ChaptersByGroup?.FirstOrDefault().Value.Count) { - return Regex.Match(url, urlPattern).Value; + novel.ChaptersByGroup = new Dictionary> { + { "none", await GetChapters(novel.URL, chapterCount, token) } + }; } - public void RefreshContext() - { - requester.Headers["User-Agent"] = $"--user-agent={ProviderFakeUserAgent.Random}"; - var config = Configuration.Default.With(requester).WithDefaultLoader(); - context = BrowsingContext.New(config); - } + novel.Author = GetName(doc); + novel.Name = $"{novel.Author}'s Kemono"; - public bool ValidateUrl(string url) + if (!(novel.Cover?.Exists ?? false)) { - return !string.IsNullOrEmpty(PrepareUrl(url)); + novel.Cover = await GetImg(GetCoverUrl(doc), novel.DownloadFolderName); } - private async Task> GetChapters(string url, int count, CancellationToken cancellationToken) - { - var chapters = new SortedList(); + return novel; + } - int j = count; + public string PrepareUrl(string url) + { + return Regex.Match(url, urlPattern).Value; + } - for (int i = 0; i < count; i += 25) - { - var page = await context.OpenAsync(url + $"?o={i}"); - foreach (var item in page.QuerySelectorAll(".post-card__heading > a")) - { - var chapterUrl = ParserInfo.SiteDomen + item.GetAttribute("href"); - var title = item.TextContent; - chapters.Add(j, new Chapter() { Name = title, Url = chapterUrl, Content = "", Number = j.ToString() }); - j--; - } - - setProgress(count, i, Resources.ProgressStatusLoading); - if (cancellationToken.IsCancellationRequested) return chapters; - } + public bool ValidateUrl(string url) + { + return !string.IsNullOrEmpty(PrepareUrl(url)); + } - return chapters; - } + private async Task GetHtmlDoc(string url, CancellationToken token = default) + { + var agent = ProviderFakeUserAgent.Random; + var content = await _webClient.GetStringContentAsync(url, agent, token); + if (string.IsNullOrWhiteSpace(content)) + return null; + + return await parser.ParseDocumentAsync(content); + } - private int GetCountChapters(IDocument doc) + private async Task> GetChapters(string url, int count, CancellationToken token) + { + var chapters = new List(); + + var j = count; + + for (var i = 0; i < count; i += 25) { - var paginator = doc.QuerySelector(".paginator > small")?.TextContent ?? "of 0"; - return int.Parse(paginator.Substring(paginator.IndexOf("of") + 2).Trim()); + var page = await GetHtmlDoc(url + $"?o={i}", token); + if (page == null) continue; + + foreach (var item in page.QuerySelectorAll(".post-card__heading > a")) + { + var chapterUrl = ParserInfo.SiteDomain + item.GetAttribute("href"); + var title = item.TextContent; + chapters.Add(new Chapter { Name = title, Url = chapterUrl, Content = "", Number = j.ToString() }); + j--; + } + + setProgress(count, i, Resources.ProgressStatusLoading); + if (token.IsCancellationRequested) return chapters; } - private string GetCoverUrl(IDocument doc) => ParserInfo.SiteDomen + doc.QuerySelector(".image-link .fancy-image__image")?.GetAttribute("src"); + return chapters; + } - private async Task GetImg(string url, string folder) - { - var result = new ImageInfo(folder, url); + private int GetCountChapters(IDocument doc) + { + var paginator = doc.QuerySelector(".paginator > small")?.TextContent ?? "of 0"; + return int.Parse(paginator[(paginator.IndexOf("of", StringComparison.Ordinal) + 2)..].Trim()); + } - var attempt = 1; + private string GetCoverUrl(IDocument doc) => ParserInfo.SiteDomain + doc.QuerySelector(".image-link .fancy-image__image")?.GetAttribute("src"); - while (attempt < 4) - { - try - { - var bytes = await httpClient.GetByteArrayAsync(url); - - Directory.CreateDirectory(Path.GetDirectoryName(result.FullPath)!); - using var stream = File.Create(result.FullPath); - stream.Write(bytes); - break; - } - catch - { - RefreshContext(); - attempt++; - } - } + private async Task GetImg(string url, string folder) + { + var result = new ImageInfo(folder, url); + var binary = await _webClient.GetBinaryContentAsync(url); + if (!binary.Any()) return result; - } - private string GetName(IDocument doc) => doc.QuerySelector(".user-header__profile")?.TextContent.Trim() ?? ""; + Directory.CreateDirectory(Path.GetDirectoryName(result.FullPath)!); + + await using var stream = File.Create(result.FullPath); + stream.Write(binary); + await stream.FlushAsync(); + + return result; + } + + private static string GetName(IParentNode doc) => + doc.QuerySelector(".user-header__profile")?.TextContent.Trim() ?? ""; - private async Task UpdateImages(Chapter chapter, string folder) + private async Task UpdateImages(Chapter chapter, string folder) + { + if (chapter.Content != null && chapter.Content.Contains("img")) { - if (chapter.Content != null && chapter.Content.Contains("img")) - { - var doc = parser.ParseDocument(chapter.Content); + var doc = parser.ParseDocument(chapter.Content); - foreach (var img in doc.QuerySelectorAll("img")) - { - string? url = img.GetAttribute("src"); + foreach (var img in doc.QuerySelectorAll("img")) + { + var url = img.GetAttribute("src"); - if (!string.IsNullOrEmpty(url)) - { - var image = await GetImg(ParserInfo.SiteDomen + img.GetAttribute("src"), folder); - chapter.Images.Add(image); - img.SetAttribute("src", image.Name); - } - } + if (string.IsNullOrEmpty(url)) continue; - chapter.Content = doc.Body?.InnerHtml ?? ""; + var image = await GetImg(ParserInfo.SiteDomain + img.GetAttribute("src"), folder); + chapter.Images.Add(image); + img.SetAttribute("src", image.Name); } - chapter.ImagesLoaded = chapter.Images.Exists(); + chapter.Content = doc.Body?.InnerHtml ?? ""; } - private void UpdateLinks(Chapter chapter) + chapter.ImagesLoaded = chapter.Images.Exists(); + } + + private void UpdateLinks(Chapter chapter) + { + if (chapter.Content == null || !chapter.Content.Contains("fileThumb")) + return; + + var doc = parser.ParseDocument(chapter.Content); + + foreach (var a in doc.QuerySelectorAll(".fileThumb")) { - if (chapter.Content != null && chapter.Content.Contains("fileThumb")) + var href = a.GetAttribute("href"); + if (string.IsNullOrEmpty(href)) continue; + + if (href.StartsWith("/data")) { - var doc = parser.ParseDocument(chapter.Content); - - foreach (var a in doc.QuerySelectorAll(".fileThumb")) - { - string? href = a.GetAttribute("href"); - - if (!string.IsNullOrEmpty(href)) - { - if (href.StartsWith("/data")) - { - href = ParserInfo.SiteDomen + href; - } - a.SetAttribute("href", href); - } - } - - chapter.Content = doc.Body?.InnerHtml ?? ""; + href = ParserInfo.SiteDomain + href; } + a.SetAttribute("href", href); } + + chapter.Content = doc.Body?.InnerHtml ?? ""; } } \ No newline at end of file diff --git a/NovelParserBLL/Parsers/ParserInfo.cs b/NovelParserBLL/Parsers/ParserInfo.cs index ca5ecb5..2a63a8c 100644 --- a/NovelParserBLL/Parsers/ParserInfo.cs +++ b/NovelParserBLL/Parsers/ParserInfo.cs @@ -2,13 +2,12 @@ { public class ParserInfo { - public string SiteDomen { get; } + public string SiteDomain { get; } public string AuthPage { get; } public string SiteName { get; } - - public ParserInfo(string siteDomen, string siteName, string authPage) + public ParserInfo(string siteDomain, string siteName, string authPage) { - SiteDomen = siteDomen; + SiteDomain = siteDomain; SiteName = siteName; AuthPage = authPage; } diff --git a/NovelParserBLL/Parsers/libme/BaseLibMeParser.cs b/NovelParserBLL/Parsers/libme/BaseLibMeParser.cs index 2211dc5..f43251e 100644 --- a/NovelParserBLL/Parsers/libme/BaseLibMeParser.cs +++ b/NovelParserBLL/Parsers/libme/BaseLibMeParser.cs @@ -1,60 +1,197 @@ -using Newtonsoft.Json; +// ReSharper disable StringLiteralTypo + +using System.Text.RegularExpressions; +using AngleSharp.Dom; +using AngleSharp.Html.Dom; +using AngleSharp.Html.Parser; +using Jint; +using Newtonsoft.Json; +using NovelParserBLL.Extensions; using NovelParserBLL.Models; +using NovelParserBLL.Parsers.DTO; using NovelParserBLL.Properties; using NovelParserBLL.Services; -using NovelParserBLL.Services.ChromeDriverHelper; +using NovelParserBLL.Services.Interfaces; using NovelParserBLL.Utilities; -namespace NovelParserBLL.Parsers.libme + +namespace NovelParserBLL.Parsers.LibMe; + +internal abstract class BaseLibMeParser { - internal abstract class BaseLibMeParser : INovelParser + private const string InfoScriptStartPattern = @"(?i)^window\.__DATA__(?-i)"; + + private readonly IWebClient _webClient; + protected readonly SetProgress SetProgress; + + protected BaseLibMeParser(SetProgress setProgress, IWebClient webClient) { - protected static readonly string checkChallengeRunningScript = Resources.CheckChallengeRunningScript; - protected static readonly string downloadImagesScript = Resources.DownloadImagesScript; - protected readonly HTMLHelper htmlHelper = new HTMLHelper(); - protected readonly SetProgress setProgress; - private static readonly string getChaptersScript = Resources.GetChaptersScript; - private static readonly string getNovelInfoScript = Resources.GetNovelInfoScript; + SetProgress = setProgress; + _webClient = webClient; + } + + public abstract string SiteDomain { get; } + public abstract string SiteName { get; } + public ParserInfo ParserInfo => new (SiteDomain, SiteName, "https://lib.social/login"); + + public async Task ParseCommonInfo(Novel novel, CancellationToken token) + { + SetProgress(0, 0, Resources.ProgressStatusLoading); + + if (!Directory.Exists(novel.DownloadFolderName)) + Directory.CreateDirectory(novel.DownloadFolderName); + + if (string.IsNullOrEmpty(novel.URL)) return novel; + + var content = await _webClient.GetStringContentAsync(novel.URL, token: token); + var htmlDoc = await ParseHtmlDocument(content, token); + if (htmlDoc == null) return novel; - public abstract string SiteDomen { get; } + var infoScript = FindScript(htmlDoc, InfoScriptStartPattern); + if (string.IsNullOrEmpty(infoScript)) return novel; - public abstract string SiteName { get; } + var novelInfo = GetNovelInfo(infoScript); + if (novelInfo == null) return novel; - public ParserInfo ParserInfo => new ParserInfo(SiteDomen, SiteName, "https://lib.social/login"); + var tempNovel = new Novel + { + Name = GetNovelName(novelInfo), + Author = GetNovelAuthor(htmlDoc), + Description = GetNovelDescription(htmlDoc) + }; - public BaseLibMeParser(SetProgress setProgress) + if (!(novel.Cover?.Exists ?? false)) { - this.setProgress = setProgress; + var coverUrl = GetNovelCoverUrl(htmlDoc, novelInfo.manga.name); + tempNovel.Cover = novel.Cover ?? new ImageInfo(novel.DownloadFolderName, coverUrl); + await DownloadFileAsync(tempNovel.Cover.URL, tempNovel.Cover.FullPath, token); } - public abstract Task LoadChapters(Novel novel, string group, string pattern, bool includeImages, CancellationToken cancellationToken); + tempNovel.ChaptersByGroup = GetChapters(novelInfo); - public async Task ParseCommonInfo(Novel novel, CancellationToken _) - { - return await Task.Run(async () => - { - Novel? tempNovel; + novel.Merge(tempNovel); + novel.Cover = FileHelper.UpdateImageInfo(novel.Cover, novel.DownloadFolderName); - setProgress(0, 0, Resources.ProgressStatusLoading); + return novel; + } - using (var driver = await ChromeDriverHelper.TryLoadPage(novel.URL!, checkChallengeRunningScript, novel.DownloadFolderName)) - { - tempNovel = JsonConvert.DeserializeObject((string)driver.ExecuteScript(getNovelInfoScript, !(novel.Cover?.Exists ?? false))); + protected async Task GetPageContent(string url, CancellationToken token = default) + { + return await _webClient.GetStringContentAsync(url, token: token); + } + protected async Task DownloadFileAsync(string url, string fullPath, CancellationToken token = default) + { + var content = await _webClient.GetBinaryContentAsync(url, token: token); + if (!content.Any()) return; - if (tempNovel == null) return novel; + await using var fs = new FileStream(fullPath, FileMode.CreateNew, FileAccess.Write); + await fs.WriteAsync(content, token); + await fs.FlushAsync(token); + } + protected async Task TryDownloadFileAsync(string source, string destination) + { + if (string.IsNullOrWhiteSpace(source)) return false; - tempNovel.ChaptersByGroup = JsonConvert.DeserializeObject>>((string)driver.ExecuteScript(getChaptersScript)); - novel.Merge(tempNovel); + await DownloadFileAsync(source, destination); + return File.Exists(destination); + } + protected static void SetImageSource(IHtmlImageElement image, string newSource) + { + var filename = Path.GetFileName(newSource); + image.ClearAttr(); + image.Source = newSource; + image.SetAttribute("src", newSource); + image.SetAttribute("name", filename); + image.SetAttribute("alt", filename); + } + protected static async Task ParseHtmlDocument(string content, CancellationToken token = default) + { + var parser = new HtmlParser(); + return await parser.ParseDocumentAsync(content, token); + } + protected static string FindScript(IParentNode doc, string pattern) + { + var scripts = doc.QuerySelectorAll("script"); - novel.Cover = FileHelper.UpdateImageInfo(novel.Cover!, novel.DownloadFolderName); - } + return scripts.FirstOrDefault(s => + Regex.IsMatch(s.InnerHtml.Trim(), pattern))? + .InnerHtml.Trim() + ?? throw new ApplicationException($"Cannot find pattern {pattern}."); + } + + private static SiteNovelInfo? GetNovelInfo(string contentScript) + { + var jsEngine = new Engine(); + jsEngine.Execute("var window = {__DATA__:{}};"); + jsEngine.Execute(contentScript); + jsEngine.Execute("var jsonData = JSON.stringify(window.__DATA__);"); - return novel; - }); - } + var novelInfoJson = jsEngine.GetValue("jsonData").AsString(); + + return JsonConvert.DeserializeObject(novelInfoJson); + } + private static string GetNovelName(SiteNovelInfo info) + { + var name = info.manga.engName; + if (string.IsNullOrWhiteSpace(name)) name = info.manga.rusName; + if (string.IsNullOrWhiteSpace(name)) name = info.manga.slug; + return name; + } + private static string GetNovelAuthor(IParentNode doc) + { + var elems = doc.QuerySelectorAll("div.media-info-list__item"); + var result = elems.FirstOrDefault(e=>string.Equals(e.Children[0].InnerHtml, "Автор", + StringComparison.OrdinalIgnoreCase)) + ?.Children[1] + .Children[0] + .InnerHtml + .Trim() + ?? "(Неизвестно)"; + return result; + } + private static string GetNovelDescription(IParentNode doc) + { + var elem = doc.QuerySelector(".media-description__text"); + return elem?.Text().Trim() ?? "(Нет описания)"; + } + private static string GetNovelCoverUrl(IParentNode doc, string title) + { + var image = (doc.QuerySelector(@$"img[alt=""{title}""]") + ?? doc.QuerySelector("img.media-header__cover") + ?? doc.QuerySelector("div.media-sidebar__cover.paper>img")) + as IHtmlImageElement; + return image?.Source ?? string.Empty; + } + private static Dictionary> GetChapters(SiteNovelInfo siteInfo) + { + var result = new Dictionary>(); - public abstract string PrepareUrl(string url); + var slug = siteInfo.manga.slug; + var branches = siteInfo.chapters.branches.Length > 0 + ? siteInfo.chapters.branches + : new SiteBranch[] { new() { id = "nobranches", name = "none" } }; + + foreach (var branch in branches) + { + var chapters = siteInfo.chapters.list + .Where(ch => ch.branch_id == branch.id || branch.id == "nobranches"); + var chaptersList = chapters.Select(chapter => + new Chapter + { + Name = chapter.chapter_name, + Number = chapter.chapter_number, + Volume = chapter.chapter_volume, + Url = BuildChapterUrl(slug, chapter.chapter_volume, chapter.chapter_number) + }).ToList(); - public abstract bool ValidateUrl(string url); + result.Add(branch.id, chaptersList.SortChapters()); + } + + return result; + } + + private static string BuildChapterUrl(string slug, int volume, string number) + { + return $@"https://ranobelib.me/{slug}/v{volume}/c{number}"; } } \ No newline at end of file diff --git a/NovelParserBLL/Parsers/libme/ComixLibMeParser.cs b/NovelParserBLL/Parsers/libme/ComixLibMeParser.cs index 93fee79..4c3d885 100644 --- a/NovelParserBLL/Parsers/libme/ComixLibMeParser.cs +++ b/NovelParserBLL/Parsers/libme/ComixLibMeParser.cs @@ -2,128 +2,194 @@ using NovelParserBLL.Models; using NovelParserBLL.Properties; using NovelParserBLL.Services; -using NovelParserBLL.Services.ChromeDriverHelper; using System.Text.RegularExpressions; - -namespace NovelParserBLL.Parsers.libme +using AngleSharp; +using AngleSharp.Dom; +using AngleSharp.Html.Dom; +using Jint; +using Newtonsoft.Json; +using NovelParserBLL.Parsers.DTO; +using NovelParserBLL.Services.Interfaces; +using NovelParserBLL.Utilities; + +namespace NovelParserBLL.Parsers.LibMe; + +internal abstract class ComicsLibMeParser : BaseLibMeParser, INovelParser { - internal abstract class ComicsLibMeParser : BaseLibMeParser + private record ComicInfo(ComicMediaInfo MediaInfo, ComicPage[] Pages); + private class ServerWithRate { - private static readonly string getComicsContentScript = Resources.GetComicsContentScript; - - public ComicsLibMeParser(SetProgress setProgress) : base(setProgress) + public ServerWithRate(string server) { + Server = server; } - protected virtual List servers => new List() - { - "https://img3.cdnlib.link/", - "https://img2.mixlib.me/", - "https://img4.imgslib.link/", - }; + public int CountImages { get; set; } + public string Server { get; } + } - public override Task LoadChapters(Novel novel, string group, string pattern, bool includeImages, CancellationToken cancellationToken) - { - return Task.Run(async () => - { - var parsed = 1; - var nonLoadedChapters = novel[group, pattern].ForLoad(includeImages); - setProgress(nonLoadedChapters.Count, 0, Resources.ProgressStatusParsing); - foreach (var item in nonLoadedChapters) - { - if (cancellationToken.IsCancellationRequested) return; - await ParseChapter(novel, item); - setProgress(nonLoadedChapters.Count, parsed++, Resources.ProgressStatusParsing); - } + protected ComicsLibMeParser(SetProgress setProgress, IWebClient webClient) + : base(setProgress, webClient) + { + } - if (includeImages) - { - var allImages = novel[group, pattern].SelectMany(ch => ch.Value.Images).ToList(); - await DownloadImages(allImages, novel.DownloadFolderName, cancellationToken); - } - }); - } + protected virtual List Servers => new() + { + "https://img3.cdnlib.link/", + "https://img2.mixlib.me/", + "https://img4.imgslib.link/", + }; - public override string PrepareUrl(string url) + public async Task LoadChapters(Novel novel, string group, string pattern, bool includeImages, CancellationToken cancellationToken) + { + var parsed = 1; + var nonLoadedChapters = novel[group, pattern].ForLoad(includeImages); + SetProgress(nonLoadedChapters.Count, 0, Resources.ProgressStatusParsing); + foreach (var item in nonLoadedChapters) { - return SiteDomen + Regex.Match(url.Substring(SiteDomen.Length), @"[^(?|\/)]*").Value; + if (cancellationToken.IsCancellationRequested) return; + await ParseChapter(novel, item); + SetProgress(nonLoadedChapters.Count, parsed++, Resources.ProgressStatusParsing); } - public override bool ValidateUrl(string url) + if (includeImages) { - return url.Length > SiteDomen.Length && url.StartsWith(SiteDomen) && PrepareUrl(url).Length > SiteDomen.Length; + var allImages = novel[group, pattern].SelectMany(ch => ch.Images).ToList(); + await DownloadImages(allImages, novel.DownloadFolderName, cancellationToken); } + } - private async Task DownloadImages(List images, string downloadFolderName, CancellationToken cancellationToken) - { - if (!images.Any()) return; + public string PrepareUrl(string url) + { + return SiteDomain + Regex.Match(url.Substring(SiteDomain.Length), @"[^(?|\/)]*").Value; + } + + public bool ValidateUrl(string url) + { + return url.Length > SiteDomain.Length && url.StartsWith(SiteDomain) && PrepareUrl(url).Length > SiteDomain.Length; + } - var notLoadedImages = images.Where(img => !img.Exists).ToList(); + //todo Возможно, нужно переработать + private async Task DownloadImages(IReadOnlyCollection images, string downloadFolderName, CancellationToken token) + { + if (!images.Any()) return; - var serversWithRate = servers.Select(s => new ServerWithRate(s)).ToList(); + var notLoadedImages = images.Where(img => !img.Exists).ToList(); + var serversWithRate = Servers.Select(s => new ServerWithRate(s)).ToList(); - var batchSize = 10; - setProgress(notLoadedImages.Count, 0, Resources.ProgressStatusImageLoading); - for (int i = 0; i < notLoadedImages.Count;) - { - var batch = notLoadedImages.GetRange(i, Math.Min(batchSize, notLoadedImages.Count - i)).Where(img => !img.Exists); + var batchSize = 10; + SetProgress(notLoadedImages.Count, 0, Resources.ProgressStatusImageLoading); + for (var i = 0; i < notLoadedImages.Count;) + { + var batch = notLoadedImages.GetRange(i, Math.Min(batchSize, notLoadedImages.Count - i)) + .Where(img => !img.Exists).ToList(); - foreach (var server in serversWithRate) + foreach (var server in serversWithRate) + { + foreach (var imageInfo in batch) { - var notLoadedImagesURLs = batch.Select(img => $"{server.Server + img.URL}?name={img.Name}").ToArray(); + var fullPath = Path.Combine(downloadFolderName, imageInfo.Name); + var downloadUrl = $"{server.Server + imageInfo.URL}?name={imageInfo.Name}"; + await DownloadFileAsync(downloadUrl, fullPath, token); + } - using (var driver = await ChromeDriverHelper.TryLoadPage(server.Server, checkChallengeRunningScript, downloadFolderName)) - { - driver.ExecuteScript(downloadImagesScript, notLoadedImagesURLs); - } + server.CountImages += batch.Count; + if (batch.Count == 0) break; + } - await Task.Delay(2000); + if (token.IsCancellationRequested) return; - var batchCount = batch.Count(); - server.CountImages += notLoadedImagesURLs.Count() - batchCount; - if (batchCount == 0) break; - } + i += batchSize; - if (cancellationToken.IsCancellationRequested) return; + SetProgress(notLoadedImages.Count, i, Resources.ProgressStatusImageLoading); + batchSize = Math.Min(50, batchSize + 10); + serversWithRate.Sort((s1, s2) => s2.CountImages - s1.CountImages); + } + } - i += batchSize; + private async Task ParseChapter(Novel novel, Chapter chapter) + { + if (string.IsNullOrEmpty(chapter.Url)) return; - setProgress(notLoadedImages.Count, i, Resources.ProgressStatusImageLoading); - batchSize = Math.Min(50, batchSize + 10); - serversWithRate.Sort((s1, s2) => s2.CountImages - s1.CountImages); - } - } + var downloadDir = novel.DownloadFolderName; + var pageContent = await GetPageContent(chapter.Url); + var pageDoc = await ParseHtmlDocument(pageContent) + ?? throw new ApplicationException("Can't parse page content"); - private async Task ParseChapter(Novel novel, Chapter chapter) - { - using (var driver = await ChromeDriverHelper.TryLoadPage(chapter.Url!, checkChallengeRunningScript)) - { - chapter.Content = (string?)driver.ExecuteScript(getComicsContentScript); - } + var chapterDoc = await CreateNewDocumentAsync(); + if (chapterDoc.Body == null) + throw new ApplicationException("Error creating html document."); - if (chapter.Content != null && chapter.Content.Contains("img")) - { - (chapter.Content, chapter.Images) = await htmlHelper.LoadImagesForHTML(chapter.Content, (img) => - { - string url = img.GetAttribute("src") ?? ""; - var imageInfo = new ImageInfo(novel.DownloadFolderName, url); - img.SetAttribute("src", imageInfo.Name); - return Task.FromResult(imageInfo); - }); - } + var comicInfo = GetMediaInfo(pageDoc); + var baseUrl = GetImagesUrl(comicInfo.MediaInfo); - chapter.ImagesLoaded = true; + foreach (var page in comicInfo.Pages) + { + var imageRemoteUrl = HtmlPathHelper.Combine(baseUrl,page.Url); + var imageInfo = new ImageInfo(downloadDir, imageRemoteUrl); + var imageLocalUrl = HtmlPathHelper.Combine(downloadDir, imageInfo.Name); + if (!await TryDownloadFileAsync(imageRemoteUrl, imageLocalUrl)) + continue; + + AddImageToDocument(chapterDoc, imageLocalUrl); + chapter.Images.Add(imageInfo); } - private class ServerWithRate + chapter.Content = chapterDoc.Body.InnerHtml; + chapter.ImagesLoaded = true; + } + + private static ComicInfo GetMediaInfo(IParentNode doc) + { + var infoScript = FindScript(doc, "window.__info"); + var pageScript = FindScript(doc, "window.__pg"); + + var jsEngine = new Engine(); + jsEngine.Execute("var window = {__pg:{},__DATA__:{},__info:{}};"); + jsEngine.Execute(infoScript); + jsEngine.Execute(pageScript); + jsEngine.Execute("var jsonPg = JSON.stringify(window.__pg);var jsonInfo = JSON.stringify(window.__info);"); + var pageInfoJson = jsEngine.GetValue("jsonPg").AsString(); + var mediaInfoJson = jsEngine.GetValue("jsonInfo").AsString(); + + var pages = JsonConvert.DeserializeObject(pageInfoJson) + ?? Array.Empty(); + var mediaInfo = JsonConvert.DeserializeObject(mediaInfoJson) + ?? throw new ApplicationException("Cannot parse comic."); + + return new ComicInfo(mediaInfo, pages); + } + + private static string GetImagesUrl(ComicMediaInfo mediaInfo) + { + var server = mediaInfo.img.server switch { - public ServerWithRate(string server) - { - Server = server; - } + "main" => mediaInfo.servers.main, + "secondary" => mediaInfo.servers.secondary, + "compress" => mediaInfo.servers.compress, + "fourth" => mediaInfo.servers.main, + _ => throw new ApplicationException("Cannot determine server.") + }; + var sitePath = mediaInfo.img.url.Trim('/'); + var url = HtmlPathHelper.Combine(server, sitePath); + return url; + } - public int CountImages { get; set; } - public string Server { get; } - } + private static void AddImageToDocument(IDocument document, string source) + { + if (document.Body == null) + throw new ApplicationException("Html document have no body section."); + + var div = (IHtmlDivElement)document.CreateElement("div"); + var image = (IHtmlImageElement)document.CreateElement("img"); + SetImageSource(image, source); + div.AppendChild(image); + document.Body.AppendChild(div); + } + + private static Task CreateNewDocumentAsync() + { + var context = new BrowsingContext(); + return context.OpenNewAsync(); } } \ No newline at end of file diff --git a/NovelParserBLL/Parsers/libme/HentaiLibMeParser.cs b/NovelParserBLL/Parsers/libme/HentaiLibMeParser.cs index 4f755c9..018f28a 100644 --- a/NovelParserBLL/Parsers/libme/HentaiLibMeParser.cs +++ b/NovelParserBLL/Parsers/libme/HentaiLibMeParser.cs @@ -1,22 +1,23 @@ using NovelParserBLL.Services; +using NovelParserBLL.Services.Interfaces; -namespace NovelParserBLL.Parsers.libme +namespace NovelParserBLL.Parsers.LibMe; + +internal class HentaiLibMeParser : ComicsLibMeParser { - internal class HentaiLibMeParser : ComicsLibMeParser + public HentaiLibMeParser(SetProgress setProgress, IWebClient webClient) + : base(setProgress, webClient) { - public HentaiLibMeParser(SetProgress setProgress) : base(setProgress) - { - } + } - public override string SiteName => "HentaiLib.me"; + public override string SiteName => "HentaiLib.me"; - public override string SiteDomen => "https://hentailib.me/"; + public override string SiteDomain => "https://hentailib.me/"; - protected override List servers => new List() - { - "https://img2.hentailib.org", - "https://img3.hentailib.org", - "https://img4.hentailib.org", - }; - } + protected override List Servers => new() + { + "https://img2.hentailib.org", + "https://img3.hentailib.org", + "https://img4.hentailib.org", + }; } \ No newline at end of file diff --git a/NovelParserBLL/Parsers/libme/MangaLibMeParser.cs b/NovelParserBLL/Parsers/libme/MangaLibMeParser.cs index a5d5362..b999401 100644 --- a/NovelParserBLL/Parsers/libme/MangaLibMeParser.cs +++ b/NovelParserBLL/Parsers/libme/MangaLibMeParser.cs @@ -1,15 +1,16 @@ using NovelParserBLL.Services; +using NovelParserBLL.Services.Interfaces; -namespace NovelParserBLL.Parsers.libme +namespace NovelParserBLL.Parsers.LibMe; + +internal class MangaLibMeParser : ComicsLibMeParser { - internal class MangaLibMeParser : ComicsLibMeParser + public MangaLibMeParser(SetProgress setProgress, IWebClient webClient) + : base(setProgress, webClient) { - public MangaLibMeParser(SetProgress setProgress) : base(setProgress) - { - } + } - public override string SiteDomen => "https://mangalib.me/"; + public override string SiteDomain => "https://mangalib.me/"; - public override string SiteName => "MangaLib.me"; - } + public override string SiteName => "MangaLib.me"; } \ No newline at end of file diff --git a/NovelParserBLL/Parsers/libme/RanobelibParser.cs b/NovelParserBLL/Parsers/libme/RanobelibParser.cs index 26acbec..9c99832 100644 --- a/NovelParserBLL/Parsers/libme/RanobelibParser.cs +++ b/NovelParserBLL/Parsers/libme/RanobelibParser.cs @@ -2,68 +2,95 @@ using NovelParserBLL.Models; using NovelParserBLL.Properties; using NovelParserBLL.Services; -using NovelParserBLL.Services.ChromeDriverHelper; -using NovelParserBLL.Utilities; using System.Text.RegularExpressions; +using AngleSharp.Html.Dom; +using NovelParserBLL.Services.Interfaces; +using NovelParserBLL.Utilities; -namespace NovelParserBLL.Parsers.libme +namespace NovelParserBLL.Parsers.LibMe; + +internal class RanobeLibMeParser : BaseLibMeParser, INovelParser { - internal class RanobeLibMeParser : BaseLibMeParser + public RanobeLibMeParser(SetProgress setProgress, IWebClient webClient) + : base(setProgress, webClient) { } + + public override string SiteDomain => "https://ranobelib.me/"; + public override string SiteName => "RanobeLib.me"; + + public async Task LoadChapters(Novel novel, string group, string pattern, bool includeImages, CancellationToken token) { - private static readonly string getRanobeContentScript = Resources.GetRanobeContentScript; + var parsed = 1; + var nonLoadedChapters = novel[group, pattern].ForLoad(includeImages); - public RanobeLibMeParser(SetProgress setProgress) : base(setProgress) + SetProgress(nonLoadedChapters.Count, 0, Resources.ProgressStatusParsing); + + foreach (var item in nonLoadedChapters) { + if (token.IsCancellationRequested) return; + + await ParseChapter(novel, item, includeImages); + + SetProgress(nonLoadedChapters.Count, parsed++, Resources.ProgressStatusParsing); } + } - public override string SiteDomen => "https://ranobelib.me/"; + public string PrepareUrl(string url) + { + return SiteDomain + Regex.Match(url[SiteDomain.Length..], @"[^(?|\/)]*").Value; + } - public override string SiteName => "RanobeLib.me"; + public bool ValidateUrl(string url) + { + return url.Length > SiteDomain.Length + && url.StartsWith(SiteDomain) + && PrepareUrl(url).Length > SiteDomain.Length; + } - public override Task LoadChapters(Novel novel, string group, string pattern, bool includeImages, CancellationToken cancellationToken) - { - return Task.Run(async () => - { - var parsed = 1; - var nonLoadedChapters = novel[group, pattern].ForLoad(includeImages); - setProgress(nonLoadedChapters.Count, 0, Resources.ProgressStatusParsing); - foreach (var item in nonLoadedChapters) - { - if (cancellationToken.IsCancellationRequested) return; - await ParseChapter(novel, item, includeImages); - setProgress(nonLoadedChapters.Count, parsed++, Resources.ProgressStatusParsing); - } - }); - } + private async Task ParseChapter(Novel novel, Chapter chapter, bool includeImages) + { + if (string.IsNullOrEmpty(chapter.Url)) return; - public override string PrepareUrl(string url) - { - return SiteDomen + Regex.Match(url.Substring(SiteDomen.Length), @"[^(?|\/)]*").Value; - } + var pageContent = await GetPageContent(chapter.Url); - public override bool ValidateUrl(string url) - { - return url.Length > SiteDomen.Length && url.StartsWith(SiteDomen) && PrepareUrl(url).Length > SiteDomen.Length; - } + var pageDoc = await ParseHtmlDocument(pageContent) + ?? throw new ApplicationException("Can't parse page content"); - private async Task ParseChapter(Novel novel, Chapter chapter, bool includeImages) - { - using (var driver = await ChromeDriverHelper.TryLoadPage(chapter.Url!, checkChallengeRunningScript, novel.DownloadFolderName)) - { - chapter.Content = (string?)driver.ExecuteScript(getRanobeContentScript, includeImages) ?? ""; - } + var chapterContent = pageDoc.QuerySelector(".reader-container") + ?? throw new ApplicationException("Can't parse page content"); + var images = chapterContent.QuerySelectorAll("img") + .Cast(); + + foreach (var image in images) + { if (includeImages) { - await Task.Delay(2000); - (chapter.Content, chapter.Images) = await htmlHelper.LoadImagesForHTML(chapter.Content, (img) => - { - string url = img.GetAttribute("src") ?? ""; - return Task.FromResult(FileHelper.UpdateImageInfo(new ImageInfo("", "", url), novel.DownloadFolderName)); - }); + var imageInfo = await ProcessImage(image, novel.DownloadFolderName); + if (imageInfo != null) + chapter.Images.Add(imageInfo); } + else + image.Remove(); + } + + chapter.Content = chapterContent.InnerHtml; + chapter.ImagesLoaded = includeImages; + } - chapter.ImagesLoaded = includeImages; + private async Task ProcessImage(IHtmlImageElement image, string downloadDir) + { + var filename = $"{Guid.NewGuid()}.png"; + var fullPath = Path.Combine(downloadDir, filename); + var source = image.GetAttribute("data-src") ?? image.Source ?? string.Empty; + if (!await TryDownloadFileAsync(source, fullPath)) + { + image.Remove(); + return null; } + + SetImageSource(image, fullPath); + var imageInfo = new ImageInfo(fullPath, filename, image.Source ?? string.Empty); + + return FileHelper.UpdateImageInfo(imageInfo, downloadDir); } } \ No newline at end of file diff --git a/NovelParserBLL/Parsers/libme/YaoiLibMeParser.cs b/NovelParserBLL/Parsers/libme/YaoiLibMeParser.cs index 421df86..a5c5c5f 100644 --- a/NovelParserBLL/Parsers/libme/YaoiLibMeParser.cs +++ b/NovelParserBLL/Parsers/libme/YaoiLibMeParser.cs @@ -1,15 +1,16 @@ using NovelParserBLL.Services; +using NovelParserBLL.Services.Interfaces; -namespace NovelParserBLL.Parsers.libme +namespace NovelParserBLL.Parsers.LibMe; + +internal class YaoiLibMeParser : ComicsLibMeParser { - internal class YaoiLibMeParser : ComicsLibMeParser + public YaoiLibMeParser(SetProgress setProgress, IWebClient webClient) + : base(setProgress, webClient) { - public YaoiLibMeParser(SetProgress setProgress) : base(setProgress) - { - } + } - public override string SiteDomen => "https://yaoilib.me/"; + public override string SiteDomain => "https://yaoilib.me/"; - public override string SiteName => "YaoiLib.me"; - } + public override string SiteName => "YaoiLib.me"; } \ No newline at end of file diff --git a/NovelParserBLL/Parsers/libme/scripts/CheckChallengeRunningScript.js b/NovelParserBLL/Parsers/libme/scripts/CheckChallengeRunningScript.js deleted file mode 100644 index 0d4cebb..0000000 --- a/NovelParserBLL/Parsers/libme/scripts/CheckChallengeRunningScript.js +++ /dev/null @@ -1,5 +0,0 @@ -if (document.querySelector(".auth-form a") !== null) { - document.querySelector(".auth-form a").click(); - return "true"; -} -else return JSON.stringify(document.querySelector("#challenge-running") !== null); \ No newline at end of file diff --git a/NovelParserBLL/Parsers/libme/scripts/GetChaptersScript.js b/NovelParserBLL/Parsers/libme/scripts/GetChaptersScript.js deleted file mode 100644 index 3a0e18c..0000000 --- a/NovelParserBLL/Parsers/libme/scripts/GetChaptersScript.js +++ /dev/null @@ -1,24 +0,0 @@ -return (() => { - const dict = {}; - (window.__DATA__.chapters.branches.length > 0 - ? window.__DATA__.chapters.branches - : [{ id: "nobranches", name: "none" }] - ).forEach( - (br) => - (dict[br.name] = (() => { - const chapter = {}; - window.__DATA__.chapters.list - .filter((ch) => ch.branch_id === br.id || br.id === "nobranches") - .forEach( - (ch) => - (chapter[ch.index] = { - Name: ch.chapter_name, - Url: `https://ranobelib.me/${window.__DATA__.manga.slug}/v${ch.chapter_volume}/c${ch.chapter_number}`, - Number: ch.chapter_number, - }) - ); - return chapter; - })()) - ); - return JSON.stringify(dict); -})(); \ No newline at end of file diff --git a/NovelParserBLL/Parsers/libme/scripts/GetComicsContentScript.js b/NovelParserBLL/Parsers/libme/scripts/GetComicsContentScript.js deleted file mode 100644 index 5ad82d0..0000000 --- a/NovelParserBLL/Parsers/libme/scripts/GetComicsContentScript.js +++ /dev/null @@ -1,7 +0,0 @@ -const getContent = () => { - return ( - "
" + window.__pg.map((i) => ``).join("") + "
" - ); -}; - -return getContent(); \ No newline at end of file diff --git a/NovelParserBLL/Parsers/libme/scripts/GetNovelInfoScript.js b/NovelParserBLL/Parsers/libme/scripts/GetNovelInfoScript.js deleted file mode 100644 index 62545b5..0000000 --- a/NovelParserBLL/Parsers/libme/scripts/GetNovelInfoScript.js +++ /dev/null @@ -1,62 +0,0 @@ -const forceDownload = async (url) => { - const fileName = url.substring(url.lastIndexOf("/") + 1); - await new Promise((resolve) => { - const img = document.createElement("img"); - img.onload = () => { - var tag = document.createElement("a"); - tag.href = url; - tag.download = fileName; - document.body.appendChild(tag); - tag.click(); - document.body.removeChild(tag); - resolve(undefined); - }; - img.onerror = () => { - resolve(undefined); - }; - - img.src = url; - }); -}; - -const getInfo = async () => { - const result = { - Name: - window.__DATA__.manga.engName || - window.__DATA__.manga.rusName || - window.__DATA__.manga.slug, - Author: - [ - ...[...document.querySelectorAll(".media-info-list__item")].find( - (item) => item.children[0].innerText === "Автор" - )?.children[1].children ?? [], - ] - .map((ch) => ch.textContent.trim()) - .join(", ") || "No Author", - Description: document - .querySelector(".media-description__text") - ?.textContent.trim(), - }; - - if (arguments[0] === true) { - const coverURL = ( - (window.__DATA__.manga.name.indexOf("'") < 0 && - document.querySelector(`img[alt='${window.__DATA__.manga.name}']`)) || - document.querySelector("img.media-header__cover") || - document.querySelector("div.media-sidebar__cover.paper > img") - )?.src; - - if (coverURL) { - await forceDownload(coverURL); - await new Promise((r) => setTimeout(r, 2000)); - } - - result.Cover = { - URL: coverURL, - }; - } - - return JSON.stringify(result); -}; - -return await getInfo(); \ No newline at end of file diff --git a/NovelParserBLL/Parsers/libme/scripts/GetRanobeContentScript.js b/NovelParserBLL/Parsers/libme/scripts/GetRanobeContentScript.js deleted file mode 100644 index 59bca4d..0000000 --- a/NovelParserBLL/Parsers/libme/scripts/GetRanobeContentScript.js +++ /dev/null @@ -1,45 +0,0 @@ -const forceDownload = async (url) => { - const fileName = url.substring(url.lastIndexOf("/") + 1); - await new Promise((resolve) => { - const img = document.createElement("img"); - img.onload = () => { - var tag = document.createElement("a"); - tag.href = url; - tag.download = fileName; - document.body.appendChild(tag); - tag.click(); - document.body.removeChild(tag); - resolve(undefined); - }; - img.onerror = () => { - resolve(undefined); - }; - - img.src = url; - }); -}; - -const getContent = async () => { - const content = document.querySelector(".reader-container"); - - for (let img of content.querySelectorAll("img") || []) { - console.log(arguments) - if (arguments[0] === true) { - const url = img.getAttribute("data-src"); - img.setAttribute("data-src", null); - await forceDownload(url); - await new Promise((r) => setTimeout(r, 200)); - img.src = url; - } else { - img.remove(); - } - } - - if (arguments[0] === true) { - await new Promise((r) => setTimeout(r, 1000)); - } - - return content?.innerHTML; -}; - -return await getContent(); \ No newline at end of file diff --git a/NovelParserBLL/Properties/Resources.Designer.cs b/NovelParserBLL/Properties/Resources.Designer.cs index 1995363..bb79e94 100644 --- a/NovelParserBLL/Properties/Resources.Designer.cs +++ b/NovelParserBLL/Properties/Resources.Designer.cs @@ -1,10 +1,10 @@ //------------------------------------------------------------------------------ // -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 +// Этот код создан программой. +// Исполняемая версия:4.0.30319.42000 // -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. +// Изменения в этом файле могут привести к неправильной работе и будут потеряны в случае +// повторной генерации кода. // //------------------------------------------------------------------------------ @@ -13,12 +13,12 @@ namespace NovelParserBLL.Properties { /// - /// A strongly-typed resource class, for looking up localized strings, etc. + /// Класс ресурса со строгой типизацией для поиска локализованных строк и т.д. /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. + // Этот класс создан автоматически классом StronglyTypedResourceBuilder + // с помощью такого средства, как ResGen или Visual Studio. + // Чтобы добавить или удалить член, измените файл .ResX и снова запустите ResGen + // с параметром /str или перестройте свой проект VS. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] @@ -33,7 +33,7 @@ internal Resources() { } /// - /// Returns the cached ResourceManager instance used by this class. + /// Возвращает кэшированный экземпляр ResourceManager, использованный этим классом. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Resources.ResourceManager ResourceManager { @@ -47,8 +47,8 @@ internal Resources() { } /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. + /// Перезаписывает свойство CurrentUICulture текущего потока для всех + /// обращений к ресурсу с помощью этого класса ресурса со строгой типизацией. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Globalization.CultureInfo Culture { @@ -61,7 +61,7 @@ internal Resources() { } /// - /// Looks up a localized string similar to cache. + /// Ищет локализованную строку, похожую на cache. /// internal static string CacheFolder { get { @@ -70,124 +70,7 @@ internal static string CacheFolder { } /// - /// Looks up a localized string similar to if (document.querySelector(".auth-form a") !== null) { - /// document.querySelector(".auth-form a").click(); - /// return "true"; - ///} - ///else return JSON.stringify(document.querySelector("#challenge-running") !== null);. - /// - internal static string CheckChallengeRunningScript { - get { - return ResourceManager.GetString("CheckChallengeRunningScript", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to const forceDownload = async (url) => { - /// const fileName = url.substring(url.lastIndexOf("/") + 1); - /// await new Promise((resolve) => { - /// const img = document.createElement("img"); - /// img.onload = () => { - /// var tag = document.createElement("a"); - /// tag.href = url; - /// tag.download = fileName; - /// document.body.appendChild(tag); - /// tag.click(); - /// document.body.removeChild(tag); - /// resolve(undefined); - /// }; - /// i [rest of string was truncated]";. - /// - internal static string DownloadImagesScript { - get { - return ResourceManager.GetString("DownloadImagesScript", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to return (() => { - /// const dict = {}; - /// (window.__DATA__.chapters.branches.length > 0 - /// ? window.__DATA__.chapters.branches - /// : [{ id: "nobranches", name: "none" }] - /// ).forEach( - /// (br) => - /// (dict[br.name] = (() => { - /// const chapter = {}; - /// window.__DATA__.chapters.list - /// .filter((ch) => ch.branch_id === br.id || br.id === "nobranches") - /// .forEach( - /// (ch) => - /// (chapter[ch.index] = { - /// [rest of string was truncated]";. - /// - internal static string GetChaptersScript { - get { - return ResourceManager.GetString("GetChaptersScript", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to const getContent = () => { - /// return ( - /// "<div>" + window.__pg.map((i) => `<img src="${window.__info.img.url}/${i.u}"/>`).join("") + "</div>" - /// ); - ///}; - /// - ///return getContent();. - /// - internal static string GetComicsContentScript { - get { - return ResourceManager.GetString("GetComicsContentScript", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to const forceDownload = async (url) => { - /// const fileName = url.substring(url.lastIndexOf("/") + 1); - /// await new Promise((resolve) => { - /// const img = document.createElement("img"); - /// img.onload = () => { - /// var tag = document.createElement("a"); - /// tag.href = url; - /// tag.download = fileName; - /// document.body.appendChild(tag); - /// tag.click(); - /// document.body.removeChild(tag); - /// resolve(undefined); - /// }; - /// i [rest of string was truncated]";. - /// - internal static string GetNovelInfoScript { - get { - return ResourceManager.GetString("GetNovelInfoScript", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to const forceDownload = async (url) => { - /// const fileName = url.substring(url.lastIndexOf("/") + 1); - /// await new Promise((resolve) => { - /// const img = document.createElement("img"); - /// img.onload = () => { - /// var tag = document.createElement("a"); - /// tag.href = url; - /// tag.download = fileName; - /// document.body.appendChild(tag); - /// tag.click(); - /// document.body.removeChild(tag); - /// resolve(undefined); - /// }; - /// i [rest of string was truncated]";. - /// - internal static string GetRanobeContentScript { - get { - return ResourceManager.GetString("GetRanobeContentScript", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Downloading Images. + /// Ищет локализованную строку, похожую на Downloading Images. /// internal static string ProgressStatusImageLoading { get { @@ -196,7 +79,7 @@ internal static string ProgressStatusImageLoading { } /// - /// Looks up a localized string similar to Loading. + /// Ищет локализованную строку, похожую на Loading. /// internal static string ProgressStatusLoading { get { @@ -205,7 +88,7 @@ internal static string ProgressStatusLoading { } /// - /// Looks up a localized string similar to Parsing. + /// Ищет локализованную строку, похожую на Parsing. /// internal static string ProgressStatusParsing { get { diff --git a/NovelParserBLL/Properties/Resources.resx b/NovelParserBLL/Properties/Resources.resx index 5c4e83b..91cb798 100644 --- a/NovelParserBLL/Properties/Resources.resx +++ b/NovelParserBLL/Properties/Resources.resx @@ -120,25 +120,6 @@ cache - - - ..\parsers\libme\scripts\checkchallengerunningscript.js;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 - - - ..\services\chromedriverhelper\downloadimagesscript.js;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 - - - ..\parsers\libme\scripts\getchaptersscript.js;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 - - - ..\parsers\libme\scripts\getcomicscontentscript.js;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 - - - ..\parsers\libme\scripts\getnovelinfoscript.js;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 - - - ..\parsers\libme\scripts\getranobecontentscript.js;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 - Downloading Images diff --git a/NovelParserBLL/ResourceReader.cs b/NovelParserBLL/ResourceReader.cs new file mode 100644 index 0000000..8199a72 --- /dev/null +++ b/NovelParserBLL/ResourceReader.cs @@ -0,0 +1,54 @@ +using System.Reflection; + +namespace NovelParserBLL; + +public static class ResourceReader +{ + private const string resourcesDir = "Resources"; + private static readonly string basePath; + private static readonly Assembly assembly; + + + static ResourceReader() + { + assembly = Assembly.GetAssembly(typeof(ResourceReader)) + ?? throw new ApplicationException("Can't get parser assembly"); + basePath = $"{assembly.GetName().Name}.{resourcesDir}"; + } + + public static string GetResourceData(string resourceName) + { + using var stream = GetResourceStream(resourceName); + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + + public static async Task GetResourceDataAsync(string resourceName) + { + await using var stream = GetResourceStream(resourceName); + using var reader = new StreamReader(stream); + return await reader.ReadToEndAsync(); + } + + public static byte[] GetResourceBinaryData(string resourceName) + { + using var stream = GetResourceStream(resourceName); + using var reader = new BinaryReader(stream); + return reader.ReadBytes((int)stream.Length); + } + + public static async Task GetResourceBinaryDataAsync(string resourceName) + { + await using var stream = GetResourceStream(resourceName); + using var reader = new BinaryReader(stream); + return await Task.Run(() => reader.ReadBytes((int)stream.Length)); + } + + private static Stream GetResourceStream(string resourceName) + { + var resourcePath = $"{basePath}.{resourceName}"; + var stream = assembly.GetManifestResourceStream(resourcePath) + ?? throw new ApplicationException("Resource stream is not created."); + return stream; + } +} diff --git a/NovelParserBLL/Resources/CoverTemplate.xhtml b/NovelParserBLL/Resources/CoverTemplate.xhtml new file mode 100644 index 0000000..9d2f42c --- /dev/null +++ b/NovelParserBLL/Resources/CoverTemplate.xhtml @@ -0,0 +1,18 @@ + + + + + + {title} + + + +

{title}

+ {start} +
+ Cover +
+ {end} + + \ No newline at end of file diff --git a/NovelParserBLL/Resources/PageTemplate.xhtml b/NovelParserBLL/Resources/PageTemplate.xhtml new file mode 100644 index 0000000..40a51cd --- /dev/null +++ b/NovelParserBLL/Resources/PageTemplate.xhtml @@ -0,0 +1,21 @@ + + + + + + {number} + + + +

{number}

+

{title}

+

* * *

+ {content} + + \ No newline at end of file diff --git a/NovelParserBLL/Services/ChromeDriverHelper/ChromeDriverHelper.cs b/NovelParserBLL/Services/ChromeDriverHelper/ChromeDriverHelper.cs deleted file mode 100644 index 9986e06..0000000 --- a/NovelParserBLL/Services/ChromeDriverHelper/ChromeDriverHelper.cs +++ /dev/null @@ -1,121 +0,0 @@ -using Newtonsoft.Json; -using NovelParserBLL.Properties; -using NovelParserBLL.Utilities; -using OpenQA.Selenium.Chrome; -using Sayaka.Common; -using System.Configuration; - -namespace NovelParserBLL.Services.ChromeDriverHelper -{ - public static class ChromeDriverHelper - { - public static readonly string DownloadPath = Path.Combine(Directory.GetCurrentDirectory(), Resources.CacheFolder); - - private static readonly string userDataPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "AppData\\Local\\Google\\Chrome\\User Data\\NovelParser"); - - public static void ClearCookies() - { - if (Directory.Exists(userDataPath)) - { - DirectoryHelper.Empty(userDataPath); - } - } - - public static ChromeDriver OpenPageWithAutoClose(string url, int time = 15 * 60_000) - { - var driver = StartChrome(true); - driver.GoTo(url); - - Task.Run(async () => - { - await Task.Delay(time); - driver?.Close(); - driver?.Dispose(); - }); - - return driver; - } - - internal static async Task TryLoadPage(string url, string checkChallengeRunningScript = "return false", string downloadFolder = "") - { - var timeSpan = new TimeSpan(0, 30, 0); - var driver = StartChrome(false, downloadFolder, timeSpan); - - driver.GoTo(url); - - var check = () => JsonConvert.DeserializeObject((string)driver.ExecuteScript(checkChallengeRunningScript)); - var attempt = 1; - while (check() && attempt < 4) - { - await Task.Delay(4000); - - if (!check()) break; - driver.Dispose(); - - driver = StartChrome(false, downloadFolder, timeSpan); - driver.GoTo(url); - - await Task.Delay(attempt++ * 2000); - } - - await Task.Delay(attempt * 1000); - - return driver; - } - - private static ChromeOptions GetChromeDriverOptions(bool visible = false, string downloadFolder = "") - { - ChromeOptions? chromeOptions = new ChromeOptions(); - Directory.CreateDirectory(DownloadPath); - - chromeOptions.AddArgument($"--user-agent={ProviderFakeUserAgent.Random}"); - chromeOptions.AddArgument("--disable-blink-features=AutomationControlled"); - chromeOptions.AddArgument("--ignore-certificate-errors"); - chromeOptions.AddExcludedArgument("enable-automation"); - chromeOptions.AddArgument("--allow-file-access-from-files"); - - if (visible) - { - chromeOptions.AddArgument("--window-size=1000,1000"); - chromeOptions.AddArgument("--window-position=100,100"); - } - else - { - chromeOptions.AddArgument("--window-size=1,1"); - chromeOptions.AddArgument("--window-position=-32000,-32000"); - } - - if (bool.Parse(ConfigurationManager.AppSettings["UseCookies"] ?? "false")) - { - chromeOptions.AddArguments(@$"user-data-dir={userDataPath}"); - chromeOptions.AddArgument("--enable-file-cookies"); - } - - downloadFolder = string.IsNullOrEmpty(downloadFolder) ? DownloadPath : Path.GetFullPath(downloadFolder); - chromeOptions.AddUserProfilePreference("download.default_directory", downloadFolder); - chromeOptions.AddUserProfilePreference("profile.default_content_setting_values.automatic_downloads", 1); - - return chromeOptions; - } - - private static void GoTo(this ChromeDriver drive, string url) - { - drive.Navigate().GoToUrl(url); - } - - private static ChromeDriver StartChrome(bool visible = false, string downloadFolder = "", TimeSpan? timeSpan = null) - { - var chromeDriverService = ChromeDriverService.CreateDefaultService(); - chromeDriverService.HideCommandPromptWindow = true; - - var driver = new ChromeDriver(chromeDriverService, GetChromeDriverOptions(visible, downloadFolder)); - if (timeSpan != null) - { - driver.Manage().Timeouts().ImplicitWait = timeSpan.Value; - } - driver.ExecuteScript("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"); - return driver; - } - } -} \ No newline at end of file diff --git a/NovelParserBLL/Services/ChromeDriverHelper/DownloadImagesScript.js b/NovelParserBLL/Services/ChromeDriverHelper/DownloadImagesScript.js deleted file mode 100644 index 28bfe41..0000000 --- a/NovelParserBLL/Services/ChromeDriverHelper/DownloadImagesScript.js +++ /dev/null @@ -1,31 +0,0 @@ -const forceDownload = async (img) => { - const url = img.substring(0, img.lastIndexOf("?name=")); - const fileName = img.substring(img.lastIndexOf("?name=") + 6); - await new Promise((resolve) => { - const img = document.createElement("img"); - img.onload = () => { - var tag = document.createElement("a"); - tag.href = url; - tag.download = fileName; - document.body.appendChild(tag); - tag.click(); - document.body.removeChild(tag); - resolve(undefined); - }; - img.onerror = () => { - resolve(undefined); - }; - - img.src = url; - }); -}; - -const downloadImages = async (imgs) => { - for (const img of imgs) { - await forceDownload(img); - await new Promise((r) => setTimeout(r, 200)); - } - await new Promise((r) => setTimeout(r, 1000)); -}; - -return await downloadImages(arguments); \ No newline at end of file diff --git a/NovelParserBLL/Services/CommonNovelParser.cs b/NovelParserBLL/Services/CommonNovelParser.cs index 9ac1b27..e89aadf 100644 --- a/NovelParserBLL/Services/CommonNovelParser.cs +++ b/NovelParserBLL/Services/CommonNovelParser.cs @@ -1,71 +1,65 @@ using NovelParserBLL.Models; using NovelParserBLL.Parsers; using NovelParserBLL.Parsers.kemono; -using NovelParserBLL.Parsers.libme; +using NovelParserBLL.Parsers.LibMe; -namespace NovelParserBLL.Services -{ - public delegate void SetProgress(int total, int current, string status); +namespace NovelParserBLL.Services; - public class CommonNovelParser - { - private readonly NovelCacheService novelCacheService; - private readonly SetProgress setProgress; - private readonly List novelParsers = new List(); +public delegate void SetProgress(int total, int current, string status); - public CommonNovelParser(NovelCacheService novelCacheService, SetProgress? setProgress = null) - { - this.novelCacheService = novelCacheService; +public class CommonNovelParser +{ + private readonly NovelCacheService novelCacheService; + private readonly List novelParsers = new (); - this.setProgress = setProgress ?? ((int _, int _, string _) => { }); + public CommonNovelParser(NovelCacheService novelCacheService, SetProgress? setProgress = null) + { + var webClient = new WebClient(); + + this.novelCacheService = novelCacheService; + var setProgressAction = setProgress ?? ((int _, int _, string _) => { }); - novelParsers.Add(new RanobeLibMeParser(this.setProgress)); - novelParsers.Add(new MangaLibMeParser(this.setProgress)); - novelParsers.Add(new HentaiLibMeParser(this.setProgress)); - novelParsers.Add(new YaoiLibMeParser(this.setProgress)); - novelParsers.Add(new KemonoParser(this.setProgress)); - } + novelParsers.Add(new RanobeLibMeParser(setProgressAction, webClient)); + novelParsers.Add(new KemonoParser(setProgressAction, webClient)); + novelParsers.Add(new MangaLibMeParser(setProgressAction, webClient)); + novelParsers.Add(new HentaiLibMeParser(setProgressAction, webClient)); + novelParsers.Add(new YaoiLibMeParser(setProgressAction, webClient)); + } - public List ParserInfos => novelParsers.Select(p => p.ParserInfo).ToList(); + public List ParserInfos => novelParsers.Select(p => p.ParserInfo).ToList(); - public async Task LoadChapters(Novel novel, string group, string pattern, bool includeImages, CancellationToken cancellationToken) + public async Task LoadChapters(Novel novel, string group, string pattern, bool includeImages, CancellationToken cancellationToken) + { + try { - try - { - await GetParser(novel.URL!)!.LoadChapters(novel, group, pattern, includeImages, cancellationToken); - } - finally - { - novelCacheService.SaveNovelToFile(novel); - } + await GetParser(novel.URL!)!.LoadChapters(novel, group, pattern, includeImages, cancellationToken); } - - public async Task ParseCommonInfo(string novelUrl, CancellationToken cancellationToken) + finally { - var parser = GetParser(novelUrl)!; - var url = parser.PrepareUrl(novelUrl); - - Novel novel = novelCacheService.TryGetNovelFromFile(url) ?? new Novel() { URL = url }; - novel = await parser.ParseCommonInfo(novel, cancellationToken); - novelCacheService.SaveNovelToFile(novel); - - return novel; } + } - public bool ValidateUrl(string url) - { - return !string.IsNullOrEmpty(url) && GetParser(url) != null; - } + public async Task ParseCommonInfo(string novelUrl, CancellationToken cancellationToken) + { + var parser = GetParser(novelUrl)!; + var url = parser.PrepareUrl(novelUrl); - private INovelParser? GetParser(string url) - { - foreach (var item in novelParsers) - { - if (item.ValidateUrl(url)) return item; - } + var novel = novelCacheService.TryGetNovelFromFile(url) ?? new Novel { URL = url }; + novel = await parser.ParseCommonInfo(novel, cancellationToken); + + novelCacheService.SaveNovelToFile(novel); - return null; - } + return novel; + } + + public bool ValidateUrl(string url) + { + return !string.IsNullOrEmpty(url) && GetParser(url) != null; + } + + private INovelParser? GetParser(string url) + { + return novelParsers.FirstOrDefault(item => item.ValidateUrl(url)); } } \ No newline at end of file diff --git a/NovelParserBLL/Services/FileGeneratorSerivce.cs b/NovelParserBLL/Services/FileGeneratorSerivce.cs index 0eed6d8..80dfadb 100644 --- a/NovelParserBLL/Services/FileGeneratorSerivce.cs +++ b/NovelParserBLL/Services/FileGeneratorSerivce.cs @@ -6,22 +6,17 @@ namespace NovelParserBLL.Services { public class FileGeneratorService { - private PDFFileGenerator pdfGenerator = new PDFFileGenerator(); - private EpubFileGenerator epubFileGenerator = new EpubFileGenerator(); + private PDFFileGenerator pdfGenerator = new(); + private EpubFileGenerator epubFileGenerator = new(); public Task Generate(GenerationParams generationParams) { - switch (generationParams) + return generationParams switch { - case PDFGenerationParams: - return pdfGenerator.Generate((PDFGenerationParams)generationParams); - - case EPUBGenerationParams: - return epubFileGenerator.Generate((EPUBGenerationParams)generationParams); - - default: - throw new ArgumentException(nameof(generationParams)); - } + PDFGenerationParams @params => pdfGenerator.Generate(@params), + EPUBGenerationParams @params => epubFileGenerator.Generate(@params), + _ => throw new ArgumentException(nameof(generationParams)) + }; } } } \ No newline at end of file diff --git a/NovelParserBLL/Services/Interfaces/IWebClient.cs b/NovelParserBLL/Services/Interfaces/IWebClient.cs new file mode 100644 index 0000000..26f7a43 --- /dev/null +++ b/NovelParserBLL/Services/Interfaces/IWebClient.cs @@ -0,0 +1,10 @@ +namespace NovelParserBLL.Services.Interfaces; + +public interface IWebClient +{ + public const string DefaultAgent = + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:101.0) Gecko/20100101 Firefox/101.0"; + + Task GetStringContentAsync(string url, string agent = DefaultAgent, CancellationToken token = default); + Task GetBinaryContentAsync(string url, string agent = DefaultAgent, CancellationToken token = default); +} \ No newline at end of file diff --git "a/NovelParserBLL/Services/Novel\320\241acheService.cs" "b/NovelParserBLL/Services/Novel\320\241acheService.cs" index 8b63b2e..aa9f13d 100644 --- "a/NovelParserBLL/Services/Novel\320\241acheService.cs" +++ "b/NovelParserBLL/Services/Novel\320\241acheService.cs" @@ -3,41 +3,37 @@ using NovelParserBLL.Properties; using NovelParserBLL.Utilities; -namespace NovelParserBLL.Services +namespace NovelParserBLL.Services; + +public class NovelCacheService { - public class NovelCacheService - { - private static string cacheFolder = Path.Combine(Directory.GetCurrentDirectory(), Resources.CacheFolder); + private static readonly string cacheFolder = Path.Combine(Directory.GetCurrentDirectory(), Resources.CacheFolder); - static NovelCacheService() - { - if (!Directory.Exists(cacheFolder)) Directory.CreateDirectory(cacheFolder); - } + static NovelCacheService() + { + if (!Directory.Exists(cacheFolder)) Directory.CreateDirectory(cacheFolder); + } - public void SaveNovelToFile(Novel novel) - { - string json = JsonConvert.SerializeObject(novel, Formatting.Indented); - var path = Path.Combine(cacheFolder, FileHelper.RemoveInvalidFilePathCharacters(novel.URL!) + ".json"); - File.WriteAllText(path, json); - } + public void SaveNovelToFile(Novel novel) + { + var json = JsonConvert.SerializeObject(novel, Formatting.Indented); + var path = Path.Combine(cacheFolder, FileHelper.RemoveInvalidFilePathCharacters(novel.URL!) + ".json"); + File.WriteAllText(path, json); + } - public Novel? TryGetNovelFromFile(string url) - { - var path = Path.Combine(cacheFolder, FileHelper.RemoveInvalidFilePathCharacters(url) + ".json"); - if (File.Exists(path)) - { - var json = File.ReadAllText(path); - return JsonConvert.DeserializeObject(json); - } - return null; - } + public Novel? TryGetNovelFromFile(string url) + { + var path = Path.Combine(cacheFolder, FileHelper.RemoveInvalidFilePathCharacters(url) + ".json"); + if (!File.Exists(path)) return null; + var json = File.ReadAllText(path); + return JsonConvert.DeserializeObject(json); + } - public void ClearCache() + public void ClearCache() + { + if (Directory.Exists(cacheFolder)) { - if (Directory.Exists(cacheFolder)) - { - DirectoryHelper.Empty(cacheFolder); - } + DirectoryHelper.Empty(cacheFolder); } } } \ No newline at end of file diff --git a/NovelParserBLL/Services/WebClient.cs b/NovelParserBLL/Services/WebClient.cs new file mode 100644 index 0000000..0b37200 --- /dev/null +++ b/NovelParserBLL/Services/WebClient.cs @@ -0,0 +1,71 @@ +using System.Net; +using NovelParserBLL.Services.Interfaces; + +namespace NovelParserBLL.Services; + +public class WebClient : IWebClient +{ + private static readonly HttpClient client; + private readonly int _maxRepeats; + static WebClient() + { + var clientHandler = new HttpClientHandler + { + //Disable SSL verification + ServerCertificateCustomValidationCallback = + HttpClientHandler.DangerousAcceptAnyServerCertificateValidator + }; + + client = new HttpClient(clientHandler, true); + } + + public WebClient(int requestRepeatsOnError = 4) + { + _maxRepeats = requestRepeatsOnError; + } + + public async Task GetStringContentAsync(string url, string agent = IWebClient.DefaultAgent, + CancellationToken token = default) + { + var response = await GetResponseAsync(url, agent, token); + + return response.IsSuccessStatusCode + ? await response.Content.ReadAsStringAsync(token) + : string.Empty; + } + + public async Task GetBinaryContentAsync(string url, string agent = IWebClient.DefaultAgent, + CancellationToken token = default) + { + var response = await GetResponseAsync(url, agent, token); + + return response.IsSuccessStatusCode + ? await response.Content.ReadAsByteArrayAsync(token) + : Array.Empty(); + } + + private async Task GetResponseAsync(string url, string agent = IWebClient.DefaultAgent, CancellationToken token = default) + { + var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Add("User-Agent", agent); + var repeat = 0; + + HttpResponseMessage? response = null; + do + { + try + { + response = await client.SendAsync(request, token); + } + catch (HttpRequestException) + { + repeat++; + } + if (response != null) break; + await Task.Delay(500 * repeat, token); + + } while (repeat < _maxRepeats); + + return response ?? new HttpResponseMessage(HttpStatusCode.NotFound); + } +} diff --git a/NovelParserBLL/Utilities/FileHelper.cs b/NovelParserBLL/Utilities/FileHelper.cs index 1919dcd..ef9e9aa 100644 --- a/NovelParserBLL/Utilities/FileHelper.cs +++ b/NovelParserBLL/Utilities/FileHelper.cs @@ -1,51 +1,50 @@ -using NovelParserBLL.FileGenerators; -using NovelParserBLL.Models; +using NovelParserBLL.Models; using System.Text.RegularExpressions; -namespace NovelParserBLL.Utilities +namespace NovelParserBLL.Utilities; + +public static class FileHelper { - public static class FileHelper + public static string RemoveInvalidFilePathCharacters(string filename, string replaceChar = "") { - public static string AddFileExtension(string file, FileFormat format) - { - return file + (file.ToUpper().EndsWith(format.ToString()) ? "" : $".{format.ToString().ToLower()}"); - } + var regexSearch = GetInvalidCharacters(); + var r = new Regex($"[{Regex.Escape(regexSearch)}]"); + var result = r.Replace(filename, replaceChar); + return result[..Math.Min(result.Length, 100)]; + } - public static string RemoveInvalidFilePathCharacters(string filename, string replaceChar = "") - { - string regexSearch = new string(Path.GetInvalidFileNameChars()) + new string(Path.GetInvalidPathChars()); - Regex r = new Regex(string.Format("[{0}]", Regex.Escape(regexSearch))); - var result = r.Replace(filename, replaceChar); - return result.Substring(0, Math.Min(result.Length, 100)); - } + public static ImageInfo UpdateImageInfo(ImageInfo? imageInfo, string downloadFolder) + { + if (imageInfo is not { Exists: false } || string.IsNullOrEmpty(imageInfo.NameFromURL)) + return imageInfo!; - public static ImageInfo UpdateImageInfo(ImageInfo imageInfo, string downloadFolder) - { - if (imageInfo != null && !imageInfo.Exists && !string.IsNullOrEmpty(imageInfo.NameFromURL)) - { - var downloadedImagePath = Path.Combine(downloadFolder, imageInfo.NameFromURL); - - if (ExistsRelatively(downloadedImagePath)) - { - var result = - !string.IsNullOrEmpty(imageInfo.Name) && !string.IsNullOrEmpty(imageInfo.FullPath) ? - new ImageInfo(imageInfo.FullPath, imageInfo.Name, imageInfo.URL) : - new ImageInfo(downloadFolder, imageInfo.URL); - - if (!File.Exists(result.FullPath)) - { - File.Move(downloadedImagePath, result.FullPath); - } - return result; - } - } + var downloadedImagePath = Path.Combine(downloadFolder, imageInfo.NameFromURL); - return imageInfo!; - } + if (!ExistsRelatively(downloadedImagePath)) + return imageInfo; + + var result = + !string.IsNullOrEmpty(imageInfo.Name) && !string.IsNullOrEmpty(imageInfo.FullPath) ? + new ImageInfo(imageInfo.FullPath, imageInfo.Name, imageInfo.URL) : + new ImageInfo(downloadFolder, imageInfo.URL); - public static bool ExistsRelatively(string path) + if (!File.Exists(result.FullPath)) { - return File.Exists(Path.GetFullPath(path)); + File.Move(downloadedImagePath, result.FullPath); } + return result; + + } + + private static bool ExistsRelatively(string path) + { + return File.Exists(Path.GetFullPath(path)); + } + + private static string GetInvalidCharacters() + { + return new string(Path.GetInvalidFileNameChars() + .Concat(Path.GetInvalidPathChars()) + .ToArray()); } } \ No newline at end of file diff --git a/NovelParserBLL/Utilities/HtmlPathHelper.cs b/NovelParserBLL/Utilities/HtmlPathHelper.cs new file mode 100644 index 0000000..bb66cf2 --- /dev/null +++ b/NovelParserBLL/Utilities/HtmlPathHelper.cs @@ -0,0 +1,9 @@ +namespace NovelParserBLL.Utilities; + +public static class HtmlPathHelper +{ + public static string Combine(params string[] paths) + { + return Path.Combine(paths).Replace("\\", "/"); + } +} diff --git a/NovelParserBLL/chromedriver.exe b/NovelParserBLL/chromedriver.exe deleted file mode 100644 index 05c1c09..0000000 Binary files a/NovelParserBLL/chromedriver.exe and /dev/null differ diff --git a/NovelParserBLLTests/Models/TestNovel.cs b/NovelParserBLLTests/Models/TestNovel.cs index 0aab44b..0831597 100644 --- a/NovelParserBLLTests/Models/TestNovel.cs +++ b/NovelParserBLLTests/Models/TestNovel.cs @@ -13,13 +13,13 @@ public static Novel GetTestNovel() Description = "Test", Cover = new ImageInfo("test", ""), Author = "Test", - ChaptersByGroup = new Dictionary>() + ChaptersByGroup = new Dictionary>() { { "test", - new SortedList() + new List () { - {1, new Chapter() {Name = "Test"} } + {new Chapter() {Name = "Test"} } } } } diff --git a/NovelParserBLLTests/NovelParserBLLTests.csproj b/NovelParserBLLTests/NovelParserBLLTests.csproj index f713166..11747d1 100644 --- a/NovelParserBLLTests/NovelParserBLLTests.csproj +++ b/NovelParserBLLTests/NovelParserBLLTests.csproj @@ -1,7 +1,7 @@ - + - net6.0 + net6.0-windows enable enable diff --git a/NovelParserBLLTests/Services/NovelCacheServiceTests.cs b/NovelParserBLLTests/Services/NovelCacheServiceTests.cs index 052dc0b..a445231 100644 --- a/NovelParserBLLTests/Services/NovelCacheServiceTests.cs +++ b/NovelParserBLLTests/Services/NovelCacheServiceTests.cs @@ -1,31 +1,31 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; +using NovelParserBLL.Services; using NovelParserBLLTests.Models; -namespace NovelParserBLL.Services.Tests +namespace NovelParserBLLTests.Services; + +[TestClass] +public class NovelCacheServiceTests { - [TestClass()] - public class NovelCacheServiceTests + [TestMethod] + public void SaveAndGetNovelTest() { - [TestMethod()] - public void SaveAndGetNovelTest() - { - var novelCacheService = new NovelCacheService(); + var novelCacheService = new NovelCacheService(); - var novel = TestNovel.GetTestNovel(); + var novel = TestNovel.GetTestNovel(); - novelCacheService.SaveNovelToFile(novel); - var cacheNovel = novelCacheService.TryGetNovelFromFile(novel.URL!)!; + novelCacheService.SaveNovelToFile(novel); + var cacheNovel = novelCacheService.TryGetNovelFromFile(novel.URL!)!; - Assert.AreEqual(novel.Name, cacheNovel.Name); - Assert.AreEqual(novel.Description, cacheNovel.Description); - Assert.AreEqual(novel.Cover, cacheNovel.Cover); - Assert.AreEqual(novel.Author, cacheNovel.Author); + Assert.AreEqual(novel.Name, cacheNovel.Name); + Assert.AreEqual(novel.Description, cacheNovel.Description); + Assert.AreEqual(novel.Cover, cacheNovel.Cover); + Assert.AreEqual(novel.Author, cacheNovel.Author); - Assert.AreEqual(novel.ChaptersByGroup!.Count, cacheNovel.ChaptersByGroup!.Count); - Assert.AreEqual( - novel.ChaptersByGroup!.First().Value.First().Value.Name, - cacheNovel.ChaptersByGroup!.First().Value.First().Value.Name - ); - } + Assert.AreEqual(novel.ChaptersByGroup!.Count, cacheNovel.ChaptersByGroup!.Count); + Assert.AreEqual( + novel.ChaptersByGroup!.First().Value.First().Name, + cacheNovel.ChaptersByGroup!.First().Value.First().Name + ); } } \ No newline at end of file diff --git a/NovelParserWPF/App.xaml b/NovelParserWPF/App.xaml index 4f6a7be..8ad2fa0 100644 --- a/NovelParserWPF/App.xaml +++ b/NovelParserWPF/App.xaml @@ -3,7 +3,8 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:hc="https://handyorg.github.io/handycontrol" - StartupUri="MainWindow.xaml"> + Startup="OnStartup" + Exit="OnExit"> diff --git a/NovelParserWPF/App.xaml.cs b/NovelParserWPF/App.xaml.cs index 572db73..c7f1563 100644 --- a/NovelParserWPF/App.xaml.cs +++ b/NovelParserWPF/App.xaml.cs @@ -1,11 +1,33 @@ -using System.Windows; +// ReSharper disable RedundantExtendsListEntry -namespace NovelParserWPF +using System.Windows; +using NovelParserWPF.ViewModels; + +namespace NovelParserWPF; + +/// +/// Interaction logic for App.xaml +/// +public partial class App : Application { - /// - /// Interaction logic for App.xaml - /// - public partial class App : Application + private readonly MainWindow _mainWindow; + + public App() + { + InitializeComponent(); + + _mainWindow = new MainWindow + { + DataContext = new MainWindowViewModel() + }; + } + private void OnStartup(object sender, StartupEventArgs e) + { + _mainWindow.Show(); + } + + private void OnExit(object sender, ExitEventArgs e) { + } } \ No newline at end of file diff --git a/NovelParserWPF/CodeGen/DevExpress.Mvvm.CodeGenerators.22.1.1.dll b/NovelParserWPF/CodeGen/DevExpress.Mvvm.CodeGenerators.22.1.1.dll new file mode 100644 index 0000000..3ba4e7b Binary files /dev/null and b/NovelParserWPF/CodeGen/DevExpress.Mvvm.CodeGenerators.22.1.1.dll differ diff --git a/NovelParserWPF/Converters/AuthPageToVisibilityConverter.cs b/NovelParserWPF/Converters/AuthPageToVisibilityConverter.cs new file mode 100644 index 0000000..a026cc8 --- /dev/null +++ b/NovelParserWPF/Converters/AuthPageToVisibilityConverter.cs @@ -0,0 +1,19 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace NovelParserWPF.Converters; + +public class AuthPageToVisibilityConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return string.IsNullOrEmpty((string)value) ? Visibility.Collapsed : Visibility.Visible; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return DependencyProperty.UnsetValue; + } +} diff --git a/NovelParserWPF/DialogWindows/FileDialogHelper.cs b/NovelParserWPF/DialogWindows/FileDialogHelper.cs index b079e08..ff39cfc 100644 --- a/NovelParserWPF/DialogWindows/FileDialogHelper.cs +++ b/NovelParserWPF/DialogWindows/FileDialogHelper.cs @@ -1,32 +1,47 @@ -using Microsoft.Win32; +using System.IO; +using Microsoft.Win32; using NovelParserBLL.FileGenerators; -namespace NovelParserWPF.DialogWindows +namespace NovelParserWPF.DialogWindows; + +internal static class FileDialogHelper { - internal static class FileDialogHelper + public static string GetSaveFilePath(string file, FileFormat fileFormat) { - public static string GetSaveFilePath(string file, FileFormat fileFormat) - { - SaveFileDialog saveFileDialog = new SaveFileDialog(); + file = file.Replace(" ", "_"); - saveFileDialog.Filter = GetFileFilterByFilterFormat(fileFormat); - saveFileDialog.Title = $"Save an {fileFormat} File"; - saveFileDialog.FileName = file; - saveFileDialog.ShowDialog(); + var saveFileDialog = new SaveFileDialog + { + Filter = GetFileFilterByFilterFormat(fileFormat), + Title = $"Save an {fileFormat} File", + FileName = Path.GetFileName(file), + InitialDirectory = Path.GetDirectoryName(file), + }; - if (saveFileDialog.FileName != "") - { - file = saveFileDialog.FileName; - } + saveFileDialog.ShowDialog(); + var filename = saveFileDialog.FileName; + if (string.IsNullOrWhiteSpace(saveFileDialog.FileName)) return file; - } - private static string GetFileFilterByFilterFormat(FileFormat fileFormat) => fileFormat switch + if (!string.IsNullOrWhiteSpace(Path.GetExtension(filename))) + return filename; + + filename += fileFormat switch { - FileFormat.EPUB => "EPUB file|*.epub", - FileFormat.PDF => "PDF file|*.pdf", - _ => throw new System.NotImplementedException(), + FileFormat.EPUB => ".epub", + FileFormat.PDF => ".pdf", + _ => "", }; + + return filename; } + + private static string GetFileFilterByFilterFormat(FileFormat fileFormat) + => fileFormat switch + { + FileFormat.EPUB => "EPUB file|*.epub", + FileFormat.PDF => "PDF file|*.pdf", + _ => throw new System.NotImplementedException(), + }; } \ No newline at end of file diff --git a/NovelParserWPF/MainWindow.xaml b/NovelParserWPF/MainWindow.xaml index 39732cc..306213a 100644 --- a/NovelParserWPF/MainWindow.xaml +++ b/NovelParserWPF/MainWindow.xaml @@ -8,6 +8,7 @@ xmlns:hc="https://handyorg.github.io/handycontrol" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="clr-namespace:NovelParserWPF.ViewModels" + xmlns:conv="clr-namespace:NovelParserWPF.Converters" x:Name="WindowMain" Title="NovelParser" Width="850" @@ -15,7 +16,12 @@ MinWidth="800" MinHeight="400" WindowStartupLocation="CenterScreen" + d:DataContext="{d:DesignInstance IsDesignTimeCreatable = False, Type = vm:MainWindowViewModel}" mc:Ignorable="d"> + + + + @@ -27,10 +33,6 @@ - - - - + Visibility="{Binding AuthPage, Converter={StaticResource AuthPageToVisibilityConverter}}" /> @@ -95,7 +96,6 @@ - @@ -263,7 +263,7 @@ Margin="5" HorizontalAlignment="Center" Style="{StaticResource TextBlockTitle}" - Text="Relorer" /> + Text="Relorer & Samael" /> @@ -290,32 +290,65 @@ TextTrimming="CharacterEllipsis" /> - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NovelParserWPF/NovelParserWPF.csproj b/NovelParserWPF/NovelParserWPF.csproj index 8920a39..3286a44 100644 --- a/NovelParserWPF/NovelParserWPF.csproj +++ b/NovelParserWPF/NovelParserWPF.csproj @@ -21,6 +21,13 @@ 10.0 + + + + + + + @@ -29,16 +36,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + - +
- @@ -48,4 +53,8 @@ + + + +
diff --git a/NovelParserWPF/ViewModels/MainWindowViewModel.cs b/NovelParserWPF/ViewModels/MainWindowViewModel.cs index de1a0c6..8e3e4f2 100644 --- a/NovelParserWPF/ViewModels/MainWindowViewModel.cs +++ b/NovelParserWPF/ViewModels/MainWindowViewModel.cs @@ -6,10 +6,8 @@ using NovelParserBLL.Models; using NovelParserBLL.Parsers; using NovelParserBLL.Services; -using NovelParserBLL.Services.ChromeDriverHelper; using NovelParserWPF.DialogWindows; using NovelParserWPF.Utilities; -using OpenQA.Selenium.Chrome; using System; using System.Collections.Generic; using System.Configuration; @@ -19,6 +17,7 @@ using System.Threading.Tasks; using System.Windows.Controls; using System.Windows.Media.Imaging; +using NovelParserBLL.Extensions; namespace NovelParserWPF.ViewModels { @@ -38,12 +37,12 @@ public MainWindowViewModel() ParserInfos = commonNovelParser.ParserInfos; FileFormatsForGenerator = new List() { - new RadioButton() { + new RadioButton { GroupName = nameof(FileFormatsForGenerator), Content = FileFormat.EPUB, IsChecked = true }, - new RadioButton() { + new RadioButton { GroupName = nameof(FileFormatsForGenerator), Content = FileFormat.PDF, } @@ -57,10 +56,10 @@ public MainWindowViewModel() public List TranslationTeams => Novel?.ChaptersByGroup?.Keys.ToList() ?? new List(); public int TotalChapters => chaptersCurrentTeam?.Count ?? 0; + public int TotalVolumes => chaptersCurrentTeam?.VolumesCount() ?? 0; + private List? chaptersCurrentTeam => Novel?[SelectedTranslationTeam, "all"]; - private SortedList? chaptersCurrentTeam => Novel?[SelectedTranslationTeam, "all"]; - - public SortedList? ChaptersToDownload => Novel?[SelectedTranslationTeam, ListChaptersPattern]; + public List? ChaptersToDownload => Novel?[SelectedTranslationTeam, ListChaptersPattern]; public BitmapImage? Cover => Novel?.Cover?.TryGetByteArray(out byte[]? cover) ?? false ? ImageHelper.BitmapImageFromBuffer(cover!) : null; @@ -126,19 +125,18 @@ public bool IsLoadingProgressButton { cancellationTokenSource?.Cancel(); ProgressButtonText = "Canceling"; - if (loadingTask == null || loadingTask.Status > TaskStatus.WaitingForChildrenToComplete) - { - cancellationTokenSource?.Dispose(); - isLoadingProgressButton = false; - ProgressButtonText = Novel == null ? "Get" : "Start"; - } + if (loadingTask != null && loadingTask.Status <= TaskStatus.WaitingForChildrenToComplete) + return; + cancellationTokenSource?.Dispose(); + isLoadingProgressButton = false; + ProgressButtonText = Novel == null ? "Get" : "Start"; } else if (value) { isLoadingProgressButton = true; cancellationTokenSource = new CancellationTokenSource(); loadingTask = StartButtonClick(cancellationTokenSource.Token); - loadingTask.ContinueWith((_) => + loadingTask.ContinueWith( _ => { IsLoadingProgressButton = false; }); @@ -152,8 +150,6 @@ private async Task StartButtonClick(CancellationToken cancellationToken) try { - TryCloseAuthDriver(); - if (Novel == null) { await GetNovelInfo(cancellationToken); @@ -177,7 +173,7 @@ private async Task GetNovelInfo(CancellationToken cancellationToken) Novel = await commonNovelParser.ParseCommonInfo(NovelLink, cancellationToken); if (Novel != null && !string.IsNullOrEmpty(Novel.Name)) { - string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); + var desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); SavePath = Path.Combine(desktop, NovelParserBLL.Utilities.FileHelper.RemoveInvalidFilePathCharacters(Novel.Name)); SelectedTranslationTeam = TranslationTeams.First(); } @@ -231,19 +227,16 @@ private void StartParse() public bool UseCookies { get => bool.Parse(ConfigurationManager.AppSettings["UseCookies"] ?? "false"); - set - { - SettingsHelper.AddOrUpdateAppSettings("UseCookies", value.ToString()); - } + set => SettingsHelper.AddOrUpdateAppSettings("UseCookies", value.ToString()); } - private ChromeDriver? authDriver; + //private ChromeDriver? authDriver; [GenerateCommand] private void OpenAuthClick(string url) { - TryCloseAuthDriver(); - authDriver = ChromeDriverHelper.OpenPageWithAutoClose(url); + //TryCloseAuthDriver(); + //authDriver = ChromeDriverHelper.OpenPageWithAutoClose(url); } private bool CanOpenAuthClick(string url) => UseCookies; @@ -251,17 +244,17 @@ private void OpenAuthClick(string url) [GenerateCommand] private void ClearCookiesClick() { - TryCloseAuthDriver(); - ChromeDriverHelper.ClearCookies(); + //TryCloseAuthDriver(); + //ChromeDriverHelper.ClearCookies(); } private bool CanClearCookiesClick() => UseCookies; - private void TryCloseAuthDriver() - { - authDriver?.Dispose(); - authDriver = null; - } + //private void TryCloseAuthDriver() + //{ + // authDriver?.Dispose(); + // authDriver = null; + //} #endregion CookiesSettings @@ -276,7 +269,7 @@ private void ClearCacheClick() [GenerateCommand] private void CloseSettingsHandler() { - TryCloseAuthDriver(); + //TryCloseAuthDriver(); } #endregion CloseSettings @@ -286,7 +279,7 @@ private void CloseSettingsHandler() [GenerateCommand] private void CloseWindowHandler() { - TryCloseAuthDriver(); + //TryCloseAuthDriver(); cancellationTokenSource?.Dispose(); } diff --git a/README.md b/README.md index effaa6b..f851026 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,20 @@ # NovelParser +This is a fork from https://github.com/Relorer/NovelParser -This application allows you to download novels from the [ranobelib.me](https://ranobelib.me/). [You can download the prepared exe here](https://github.com/Relorer/NovelParser/releases/tag/v1.0.1) +This application allows you to download novels from the [ranobelib.me](https://ranobelib.me/). [You can download the prepared exe here](https://github.com/Samael7777/NovelParser/releases/tag/v1.0.2) -At the moment, only [ranobelib.me](https://ranobelib.me/) is supported. If you want to see a specific site here, create an issue for it +Supported sites: + +https://ranobelib.me/, + +https://kemono.party/, + +https://hentailib.me/, + +https://mangalib.me/, + +https://yaoilib.me/ + +Authorization and partial download is not supported yet. ![image](https://user-images.githubusercontent.com/26045342/191115639-9e0fe050-9d52-4662-b011-2a846518f831.png)