From 6a4991d0993e88082608a1d3124357226e00d701 Mon Sep 17 00:00:00 2001 From: SylveonDeko <59923820+SylveonDeko@users.noreply.github.com> Date: Fri, 15 Nov 2024 13:54:20 -0500 Subject: [PATCH] System.Text.Json migration and .NET 8 update - Replaced Newtonsoft.Json with System.Text.Json - Added type converters for proper JSON handling - Updated to .NET 8.0 - Improved async implementation - Fixed thumbnail/image URLs for load balancing support --- LICENSE | 2 +- NHentaiAPI.Tests/BaseUnitTest.cs | 33 +- NHentaiAPI.Tests/NHentaiAPI.Tests.csproj | 30 +- NHentaiAPI.Tests/NHentaiBookUnitTest.cs | 76 +-- NHentaiAPI.Tests/NHentaiPictureUnitTest.cs | 281 +++++---- NHentaiAPI.Tests/NHentaiSearchUnitTest.cs | 105 ++-- .../JsonConverters/ImageTypeConverter.cs | 48 ++ .../JsonConverters/UnixTimestampConverter.cs | 40 ++ NHentaiAPI/Models/Books/Book.cs | 83 ++- NHentaiAPI/Models/Books/Image.cs | 64 +- NHentaiAPI/Models/Books/Images.cs | 36 +- NHentaiAPI/Models/Books/Page.cs | 30 +- NHentaiAPI/Models/Books/Tag.cs | 49 +- NHentaiAPI/Models/Books/Title.cs | 35 +- NHentaiAPI/Models/Recommends/BookRecommend.cs | 22 +- NHentaiAPI/Models/Searches/SearchResults.cs | 36 +- NHentaiAPI/Models/Searches/SortBy.cs | 28 +- NHentaiAPI/NHentaiAPI.csproj | 65 +- NHentaiAPI/NHentaiClient.cs | 591 ++++++++++++------ README.md | 111 ++-- 20 files changed, 1121 insertions(+), 644 deletions(-) create mode 100644 NHentaiAPI/JsonConverters/ImageTypeConverter.cs create mode 100644 NHentaiAPI/JsonConverters/UnixTimestampConverter.cs diff --git a/LICENSE b/LICENSE index 5629b31..c39d32b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Sylveon +Copyright (c) 2024 SylveonDeko Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/NHentaiAPI.Tests/BaseUnitTest.cs b/NHentaiAPI.Tests/BaseUnitTest.cs index 188832a..c32f7b1 100644 --- a/NHentaiAPI.Tests/BaseUnitTest.cs +++ b/NHentaiAPI.Tests/BaseUnitTest.cs @@ -1,29 +1,28 @@ using System.Collections.Generic; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace NHentaiAPI.Tests +namespace NHentaiAPI.Tests; + +public class BaseUnitTest { - public class BaseUnitTest - { - protected NHentaiClient NHentaiClient { get; private set; } + protected NHentaiClient NHentaiClient { get; private set; } - [TestInitialize] - public void InitializeTest() - { - NHentaiClient = new TestNHentaiClient("a", new Dictionary()); - } + [TestInitialize] + public void InitializeTest() + { + NHentaiClient = new TestNHentaiClient("a", new Dictionary()); } +} - public class TestNHentaiClient : NHentaiClient +public class TestNHentaiClient : NHentaiClient +{ + public TestNHentaiClient(string userAgent, Dictionary cookies = null) : base(userAgent, cookies) { - #region Urls + } - //protected override string ApiRootUrl => "https://nhent.ai"; + #region Urls - #endregion + //protected override string ApiRootUrl => "https://nhent.ai"; - public TestNHentaiClient(string userAgent, Dictionary cookies = null) : base(userAgent, cookies) - { - } - } + #endregion } \ No newline at end of file diff --git a/NHentaiAPI.Tests/NHentaiAPI.Tests.csproj b/NHentaiAPI.Tests/NHentaiAPI.Tests.csproj index 7085b7a..6252dcd 100644 --- a/NHentaiAPI.Tests/NHentaiAPI.Tests.csproj +++ b/NHentaiAPI.Tests/NHentaiAPI.Tests.csproj @@ -1,22 +1,22 @@ - - net6.0 + + net8.0 - false - + false + - - - - - - - - + + + + + + + + - - - + + + diff --git a/NHentaiAPI.Tests/NHentaiBookUnitTest.cs b/NHentaiAPI.Tests/NHentaiBookUnitTest.cs index c201871..f2f614c 100644 --- a/NHentaiAPI.Tests/NHentaiBookUnitTest.cs +++ b/NHentaiAPI.Tests/NHentaiBookUnitTest.cs @@ -2,49 +2,49 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace NHentaiAPI.Tests +namespace NHentaiAPI.Tests; + +/// +/// see : +/// https://github.com/andy840119/NHentaiSharp/blob/master/NHentaiSharp.UnitTests/Program.cs +/// +[TestClass] +public class NHentaiBookUnitTest : BaseUnitTest { /// - /// see : - /// https://github.com/andy840119/NHentaiSharp/blob/master/NHentaiSharp.UnitTests/Program.cs + /// Get book detail + /// https://nhentai.net/g/161194/ /// - [TestClass] - public class NHentaiBookUnitTest : BaseUnitTest + /// + [TestMethod] + public async Task TestBookResult() { - /// - /// Get book detail - /// https://nhentai.net/g/161194/ - /// - /// - [TestMethod] - public async Task TestBookResult() - { - // https://nhentai.net/api/gallery/161194 - var result = await NHentaiClient.GetBookAsync(161194); + // https://nhentai.net/api/gallery/161194 + var result = await NHentaiClient.GetBookAsync(161194); - Assert.AreEqual("[ユイザキカズヤ] つなかん。 (COMIC ポプリクラブ 2013年8月号) [英訳]", result.Title.Japanese); - Assert.AreEqual("Tsuna-kan. | Tuna Can", result.Title.Pretty); - Assert.AreEqual("[Yuizaki Kazuya] Tsuna-kan. | Tuna Can (COMIC Potpourri Club 2013-08) [English] [PSYN]", result.Title.English); - Assert.AreEqual("160413", result.UploadDate.ToString("yyMMdd")); - Assert.AreEqual(true, result.Tags.Any(x => x.Id == 19440)); - Assert.AreEqual(17, result.NumPages); - Assert.AreEqual(17, result.Images.Pages.Count); - Assert.AreEqual(161194, result.Id); - } + Assert.AreEqual("[ユイザキカズヤ] つなかん。 (COMIC ポプリクラブ 2013年8月号) [英訳]", result.Title.Japanese); + Assert.AreEqual("Tsuna-kan. | Tuna Can", result.Title.Pretty); + Assert.AreEqual("[Yuizaki Kazuya] Tsuna-kan. | Tuna Can (COMIC Potpourri Club 2013-08) [English] [PSYN]", + result.Title.English); + Assert.AreEqual("160413", result.UploadDate.ToString("yyMMdd")); + Assert.AreEqual(true, result.Tags.Any(x => x.Id == 19440)); + Assert.AreEqual(17, result.NumPages); + Assert.AreEqual(17, result.Images.Pages.Count); + Assert.AreEqual(161194, result.Id); + } - /// - /// Get recommend book - /// https://nhentai.net/g/161194/ - /// - /// - [TestMethod] - public async Task TestBookRecommendResult() - { - // https://nhentai.net/api/gallery/161194/related - var result = await NHentaiClient.GetBookRecommendAsync(161194); + /// + /// Get recommend book + /// https://nhentai.net/g/161194/ + /// + /// + [TestMethod] + public async Task TestBookRecommendResult() + { + // https://nhentai.net/api/gallery/161194/related + var result = await NHentaiClient.GetBookRecommendAsync(161194); - // At least one recommend - Assert.AreEqual(true, result.Result.Any()); - } + // At least one recommend + Assert.AreEqual(true, result.Result.Any()); } -} +} \ No newline at end of file diff --git a/NHentaiAPI.Tests/NHentaiPictureUnitTest.cs b/NHentaiAPI.Tests/NHentaiPictureUnitTest.cs index cd37553..7418834 100644 --- a/NHentaiAPI.Tests/NHentaiPictureUnitTest.cs +++ b/NHentaiAPI.Tests/NHentaiPictureUnitTest.cs @@ -1,147 +1,146 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace NHentaiAPI.Tests +namespace NHentaiAPI.Tests; + +[TestClass] +public class NHentaiPictureUnitTest : BaseUnitTest { - [TestClass] - public class NHentaiPictureUnitTest : BaseUnitTest + /// + /// Get picture by book's media id and pageNumber + /// https://nhentai.net/g/123/ + /// + /// + [TestMethod] + public async Task TestGetPictureResult() + { + // Get book no 123 + var book = await NHentaiClient.GetBookAsync(123); + + // Check this book is right media number + Assert.AreEqual(635, book.MediaId); + + // Check url + var imageUrl = NHentaiClient.GetPictureUrl(book, 1); + Assert.AreEqual(imageUrl, "https://i.nhentai.net/galleries/635/1.jpg"); + + // Make sure image is downloaded + var result = await NHentaiClient.GetPictureAsync(book, 1); + Assert.AreEqual(true, result.Length > 0); + } + + /// + /// Get picture by book's media id and pageNumber + /// https://nhentai.net/g/288869 / + /// + /// + [TestMethod] + public async Task TestGetGifPictureResult() + { + // Get book no 288869 + var book = await NHentaiClient.GetBookAsync(288869); + + // Check this book is right media number + Assert.AreEqual(1504878, book.MediaId); + + // Check url + var imageUrl = NHentaiClient.GetPictureUrl(book, 22); + Assert.AreEqual(imageUrl, "https://i.nhentai.net/galleries/1504878/22.gif"); + + // Make sure image is downloaded + var result = await NHentaiClient.GetPictureAsync(book, 22); + Assert.AreEqual(true, result.Length > 0); + } + + /// + /// Get thumbnail by book's media id and pageNumber + /// https://nhentai.net/g/123/ + /// + /// + [TestMethod] + public async Task TestGetThumbPictureResult() + { + // Get book no 123 + var book = await NHentaiClient.GetBookAsync(123); + + // Check this book is right media number + Assert.AreEqual(635, book.MediaId); + + // Check url + var imageUrl = NHentaiClient.GetThumbPictureUrl(book, 1); + Assert.AreEqual(imageUrl, "https://t.nhentai.net/galleries/635/1t.jpg"); + + // Make sure image is downloaded + var result = await NHentaiClient.GetThumbPictureAsync(book, 1); + Assert.AreEqual(true, result.Length > 0); + } + + /// + /// Get big cover by book's media id + /// https://nhentai.net/g/123/ + /// + /// + [TestMethod] + public async Task TestGetBigCoverPictureResult() { - /// - /// Get picture by book's media id and pageNumber - /// https://nhentai.net/g/123/ - /// - /// - [TestMethod] - public async Task TestGetPictureResult() - { - // Get book no 123 - var book = await NHentaiClient.GetBookAsync(123); - - // Check this book is right media number - Assert.AreEqual(635, book.MediaId); - - // Check url - var imageUrl = NHentaiClient.GetPictureUrl(book, 1); - Assert.AreEqual(imageUrl, "https://i.nhentai.net/galleries/635/1.jpg"); - - // Make sure image is downloaded - var result = await NHentaiClient.GetPictureAsync(book, 1); - Assert.AreEqual(true, result.Length>0); - } - - /// - /// Get picture by book's media id and pageNumber - /// https://nhentai.net/g/288869 / - /// - /// - [TestMethod] - public async Task TestGetGifPictureResult() - { - // Get book no 288869 - var book = await NHentaiClient.GetBookAsync(288869); - - // Check this book is right media number - Assert.AreEqual(1504878, book.MediaId); - - // Check url - var imageUrl = NHentaiClient.GetPictureUrl(book, 22); - Assert.AreEqual(imageUrl, "https://i.nhentai.net/galleries/1504878/22.gif"); - - // Make sure image is downloaded - var result = await NHentaiClient.GetPictureAsync(book, 22); - Assert.AreEqual(true, result.Length > 0); - } - - /// - /// Get thumbnail by book's media id and pageNumber - /// https://nhentai.net/g/123/ - /// - /// - [TestMethod] - public async Task TestGetThumbPictureResult() - { - // Get book no 123 - var book = await NHentaiClient.GetBookAsync(123); - - // Check this book is right media number - Assert.AreEqual(635, book.MediaId); - - // Check url - var imageUrl = NHentaiClient.GetThumbPictureUrl(book, 1); - Assert.AreEqual(imageUrl, "https://t.nhentai.net/galleries/635/1t.jpg"); - - // Make sure image is downloaded - var result = await NHentaiClient.GetThumbPictureAsync(book, 1); - Assert.AreEqual(true, result.Length > 0); - } - - /// - /// Get big cover by book's media id - /// https://nhentai.net/g/123/ - /// - /// - [TestMethod] - public async Task TestGetBigCoverPictureResult() - { - // Get book no 123 - var book = await NHentaiClient.GetBookAsync(123); - - // Check this book is right media number - Assert.AreEqual(635, book.MediaId); - - // Check url - var imageUrl = NHentaiClient.GetBigCoverUrl(book); - Assert.AreEqual(imageUrl, "https://t.nhentai.net/galleries/635/cover.jpg"); - - // Make sure image is downloaded - var result = await NHentaiClient.GetBigCoverPictureAsync(book); - Assert.AreEqual(true, result.Length > 0); - } - - /// - /// Get origin picture by book's media id and pageNumber - /// https://nhentai.net/g/123/ - /// - /// - [TestMethod] - public async Task TestGetOriginPictureResult() - { - // Get book no 123 - var book = await NHentaiClient.GetBookAsync(123); - - // Check this book is right media number - Assert.AreEqual(635, book.MediaId); - - // Check url - var imageUrl = NHentaiClient.GetOriginPictureUrl(book, 1); - Assert.AreEqual(imageUrl, "https://i.nhentai.net/galleries/635/1.jpg"); - - // Make sure image is downloaded - var result = await NHentaiClient.GetOriginPictureAsync(book,1); - Assert.AreEqual(true, result.Length > 0); - } - - /// - /// Get thumbnail cover by book's media id - /// https://nhentai.net/g/123/ - /// - /// - [TestMethod] - public async Task TestBookThumbPictureResult() - { - // Get book no 123 - var book = await NHentaiClient.GetBookAsync(123); - - // Check this book is right media number - Assert.AreEqual(635, book.MediaId); - - // Check url - var imageUrl = NHentaiClient.GetBookThumbUrl(book); - Assert.AreEqual(imageUrl, "https://t.nhentai.net/galleries/635/thumb.jpg"); - - // Make sure image is downloaded - var result = await NHentaiClient.GetBookThumbPictureAsync(book); - Assert.AreEqual(true, result.Length > 0); - } + // Get book no 123 + var book = await NHentaiClient.GetBookAsync(123); + + // Check this book is right media number + Assert.AreEqual(635, book.MediaId); + + // Check url + var imageUrl = NHentaiClient.GetBigCoverUrl(book); + Assert.AreEqual(imageUrl, "https://t.nhentai.net/galleries/635/cover.jpg"); + + // Make sure image is downloaded + var result = await NHentaiClient.GetBigCoverPictureAsync(book); + Assert.AreEqual(true, result.Length > 0); + } + + /// + /// Get origin picture by book's media id and pageNumber + /// https://nhentai.net/g/123/ + /// + /// + [TestMethod] + public async Task TestGetOriginPictureResult() + { + // Get book no 123 + var book = await NHentaiClient.GetBookAsync(123); + + // Check this book is right media number + Assert.AreEqual(635, book.MediaId); + + // Check url + var imageUrl = NHentaiClient.GetOriginPictureUrl(book, 1); + Assert.AreEqual(imageUrl, "https://i.nhentai.net/galleries/635/1.jpg"); + + // Make sure image is downloaded + var result = await NHentaiClient.GetOriginPictureAsync(book, 1); + Assert.AreEqual(true, result.Length > 0); + } + + /// + /// Get thumbnail cover by book's media id + /// https://nhentai.net/g/123/ + /// + /// + [TestMethod] + public async Task TestBookThumbPictureResult() + { + // Get book no 123 + var book = await NHentaiClient.GetBookAsync(123); + + // Check this book is right media number + Assert.AreEqual(635, book.MediaId); + + // Check url + var imageUrl = NHentaiClient.GetBookThumbUrl(book); + Assert.AreEqual(imageUrl, "https://t.nhentai.net/galleries/635/thumb.jpg"); + + // Make sure image is downloaded + var result = await NHentaiClient.GetBookThumbPictureAsync(book); + Assert.AreEqual(true, result.Length > 0); } -} +} \ No newline at end of file diff --git a/NHentaiAPI.Tests/NHentaiSearchUnitTest.cs b/NHentaiAPI.Tests/NHentaiSearchUnitTest.cs index 3342713..c7df169 100644 --- a/NHentaiAPI.Tests/NHentaiSearchUnitTest.cs +++ b/NHentaiAPI.Tests/NHentaiSearchUnitTest.cs @@ -4,67 +4,66 @@ using NHentaiAPI.Models.Books; using NHentaiAPI.Models.Searches; -namespace NHentaiAPI.Tests +namespace NHentaiAPI.Tests; + +/// +/// see : +/// https://github.com/NHMoeDev/NHentai-android/issues/27 +/// +[TestClass] +public class NHentaiSearchUnitTest : BaseUnitTest { /// - /// see : - /// https://github.com/NHMoeDev/NHentai-android/issues/27 + /// Target number of record in single page /// - [TestClass] - public class NHentaiSearchUnitTest : BaseUnitTest - { - /// - /// Target number of record in single page - /// - protected virtual int ResultNumber => 25; + protected virtual int ResultNumber => 25; - /// - /// Get home page search result - /// https://nhentai.net/galleries/all?page=1 - /// - /// - [TestMethod] - public async Task TestSearchHomePageResult() - { - // https://nhentai.net/api/galleries/all?page=1 - var result = await NHentaiClient.GetHomePageListAsync(1); + /// + /// Get home page search result + /// https://nhentai.net/galleries/all?page=1 + /// + /// + [TestMethod] + public async Task TestSearchHomePageResult() + { + // https://nhentai.net/api/galleries/all?page=1 + var result = await NHentaiClient.GetHomePageListAsync(1); - Assert.AreEqual(ResultNumber, result.PerPage); - Assert.AreEqual(ResultNumber, result.Result.Count); - } + Assert.AreEqual(ResultNumber, result.PerPage); + Assert.AreEqual(ResultNumber, result.Result.Count); + } - /// - /// Get search result by keyword - /// https://nhentai.net/galleries/search?query=school%20swimsuit%20loli%20full%20color&page=2 - /// - /// - [TestMethod] - public async Task TestSearchResult() - { - // https://nhentai.net/api/galleries/search?query=school - var result = await NHentaiClient.GetSearchPageListAsync("school",1); + /// + /// Get search result by keyword + /// https://nhentai.net/galleries/search?query=school%20swimsuit%20loli%20full%20color&page=2 + /// + /// + [TestMethod] + public async Task TestSearchResult() + { + // https://nhentai.net/api/galleries/search?query=school + var result = await NHentaiClient.GetSearchPageListAsync("school", 1); - Assert.AreEqual(ResultNumber, result.PerPage); - Assert.AreEqual(ResultNumber, result.Result.Count); - } + Assert.AreEqual(ResultNumber, result.PerPage); + Assert.AreEqual(ResultNumber, result.Result.Count); + } - /// - /// Get search result by tag, can be sort by popular - /// https://nhentai.net/galleries/tagged?tag_id=1&page=1&sort=popular - /// - /// - [TestMethod] - public async Task TestTagResult() + /// + /// Get search result by tag, can be sort by popular + /// https://nhentai.net/galleries/tagged?tag_id=1&page=1&sort=popular + /// + /// + [TestMethod] + public async Task TestTagResult() + { + // https://nhentai.net/api/galleries/tagged?tag_id=1&page=1&sort=popular + var tag = new Tag { - // https://nhentai.net/api/galleries/tagged?tag_id=1&page=1&sort=popular - var tag = new Tag - { - Id = 1 - }; - var result = await NHentaiClient.GetTagPageListAsync(tag, SortBy.Popular, 1); + Id = 1 + }; + var result = await NHentaiClient.GetTagPageListAsync(tag, SortBy.Popular, 1); - Assert.AreEqual(ResultNumber, result.PerPage); - Assert.AreEqual(true, result.Result.Any()); - } + Assert.AreEqual(ResultNumber, result.PerPage); + Assert.AreEqual(true, result.Result.Any()); } -} +} \ No newline at end of file diff --git a/NHentaiAPI/JsonConverters/ImageTypeConverter.cs b/NHentaiAPI/JsonConverters/ImageTypeConverter.cs new file mode 100644 index 0000000..6fb04eb --- /dev/null +++ b/NHentaiAPI/JsonConverters/ImageTypeConverter.cs @@ -0,0 +1,48 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using NHentaiAPI.Models.Books; + +namespace NHentaiAPI.JsonConverters; + +/// +/// Converts between single-character string representations and ImageType enum values +/// +public class ImageTypeConverter : JsonConverter +{ + /// + /// Reads a JSON string value and converts it to an ImageType enum value + /// + /// The Utf8JsonReader to read from + /// The type to convert to + /// The JsonSerializerOptions to use + /// An ImageType enum value corresponding to the JSON string + public override ImageType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return value?.ToLower() switch + { + "j" => ImageType.Jpg, + "p" => ImageType.Png, + "g" => ImageType.Gif, + _ => ImageType.Jpg // Default to JPG if unknown + }; + } + + /// + /// Writes an ImageType enum value as a single-character JSON string + /// + /// The JsonWriter to write to + /// The ImageType value to convert + /// The JsonSerializerOptions to use + public override void Write(Utf8JsonWriter writer, ImageType value, JsonSerializerOptions options) + { + var stringValue = value switch + { + ImageType.Jpg => "j", + ImageType.Png => "p", + ImageType.Gif => "g", + _ => "j" + }; + writer.WriteStringValue(stringValue); + } +} \ No newline at end of file diff --git a/NHentaiAPI/JsonConverters/UnixTimestampConverter.cs b/NHentaiAPI/JsonConverters/UnixTimestampConverter.cs new file mode 100644 index 0000000..5e7151b --- /dev/null +++ b/NHentaiAPI/JsonConverters/UnixTimestampConverter.cs @@ -0,0 +1,40 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace NHentaiAPI.JsonConverters; + +/// +/// Converts between Unix timestamps and DateTime values during JSON serialization/deserialization +/// +public class UnixTimestampConverter : JsonConverter +{ + /// + /// Converts a Unix timestamp from JSON to a DateTime value + /// + /// The JSON reader + /// The type to convert to (DateTime) + /// Serialization options + /// A DateTime converted from the Unix timestamp + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // Read the Unix timestamp (seconds since epoch) + var unixTime = reader.GetInt64(); + + // Convert Unix timestamp to DateTime + // Unix epoch starts from January 1, 1970 + return DateTimeOffset.FromUnixTimeSeconds(unixTime).DateTime; + } + + /// + /// Converts a DateTime value to a Unix timestamp for JSON + /// + /// The JSON writer + /// The DateTime value to convert + /// Serialization options + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + { + // Convert DateTime to Unix timestamp + var unixTime = new DateTimeOffset(value).ToUnixTimeSeconds(); + writer.WriteNumberValue(unixTime); + } +} \ No newline at end of file diff --git a/NHentaiAPI/Models/Books/Book.cs b/NHentaiAPI/Models/Books/Book.cs index 7173765..42978a6 100644 --- a/NHentaiAPI/Models/Books/Book.cs +++ b/NHentaiAPI/Models/Books/Book.cs @@ -1,38 +1,65 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; +using System.Text.Json.Serialization; +using NHentaiAPI.JsonConverters; -namespace NHentaiAPI.Models.Books +namespace NHentaiAPI.Models.Books; + +/// +/// Represents a complete book with all its metadata +/// +public class Book { - public class Book - { - [JsonProperty("id")] - public int Id { get; set; } + /// + /// Gets or sets the unique identifier for the book + /// + [JsonPropertyName("id")] + public int Id { get; set; } - [JsonProperty("media_id")] - public int MediaId { get; set; } + /// + /// Gets or sets the media identifier for the book + /// + [JsonPropertyName("media_id")] + public int MediaId { get; set; } - [JsonProperty("title")] - public Title Title { get; set; } + /// + /// Gets or sets the titles of the book in different languages + /// + [JsonPropertyName("title")] + public Title Title { get; set; } - [JsonProperty("images")] - public Images Images { get; set; } + /// + /// Gets or sets the collection of images associated with the book + /// + [JsonPropertyName("images")] + public Images Images { get; set; } - [JsonProperty("scanlator")] - public string Scanlator { get; set; } + /// + /// Gets or sets the scanlator/translator of the book + /// + [JsonPropertyName("scanlator")] + public string Scanlator { get; set; } - [JsonProperty("upload_date")] - [JsonConverter(typeof(UnixDateTimeConverter))] - public DateTime UploadDate { get; set; } + /// + /// Gets or sets the date when the book was uploaded + /// + [JsonPropertyName("upload_date")] + [JsonConverter(typeof(UnixTimestampConverter))] + public DateTime UploadDate { get; set; } - [JsonProperty("tags")] - public List Tags { get; set; } + /// + /// Gets or sets the list of tags associated with the book + /// + [JsonPropertyName("tags")] + public List Tags { get; set; } - [JsonProperty("num_pages")] - public int NumPages { get; set; } + /// + /// Gets or sets the total number of pages in the book + /// + [JsonPropertyName("num_pages")] + public int NumPages { get; set; } - [JsonProperty("num_favorites")] - public int NumFavorites { get; set; } - } -} + /// + /// Gets or sets the number of times the book has been favorited + /// + [JsonPropertyName("num_favorites")] + public int NumFavorites { get; set; } +} \ No newline at end of file diff --git a/NHentaiAPI/Models/Books/Image.cs b/NHentaiAPI/Models/Books/Image.cs index 9e89b47..58def57 100644 --- a/NHentaiAPI/Models/Books/Image.cs +++ b/NHentaiAPI/Models/Books/Image.cs @@ -1,29 +1,51 @@ using System.Runtime.Serialization; -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using NHentaiAPI.JsonConverters; -namespace NHentaiAPI.Models.Books +namespace NHentaiAPI.Models.Books; + +/// +/// Represents an image's metadata +/// +public class Image { - public class Image - { - [JsonProperty("t")] - public ImageType Type { get; set; } + /// + /// Gets or sets the type/format of the image + /// + [JsonPropertyName("t")] + [JsonConverter(typeof(ImageTypeConverter))] + public ImageType Type { get; set; } - [JsonProperty("w")] - public int Width { get; set; } + /// + /// Gets or sets the width of the image in pixels + /// + [JsonPropertyName("w")] + public int Width { get; set; } - [JsonProperty("h")] - public int Height { get; set; } - } + /// + /// Gets or sets the height of the image in pixels + /// + [JsonPropertyName("h")] + public int Height { get; set; } +} - public enum ImageType - { - [EnumMember(Value = "j")] - Jpg, +/// +/// Defines the supported image formats +/// +public enum ImageType +{ + /// + /// JPEG image format + /// + [EnumMember(Value = "j")] Jpg, - [EnumMember(Value = "p")] - Png, + /// + /// PNG image format + /// + [EnumMember(Value = "p")] Png, - [EnumMember(Value = "g")] - Gif - } -} + /// + /// GIF image format + /// + [EnumMember(Value = "g")] Gif +} \ No newline at end of file diff --git a/NHentaiAPI/Models/Books/Images.cs b/NHentaiAPI/Models/Books/Images.cs index 0f2456f..6d1bd27 100644 --- a/NHentaiAPI/Models/Books/Images.cs +++ b/NHentaiAPI/Models/Books/Images.cs @@ -1,17 +1,27 @@ -using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; -namespace NHentaiAPI.Models.Books +namespace NHentaiAPI.Models.Books; + +/// +/// Contains all images associated with a book +/// +public class Images { - public class Images - { - [JsonProperty("pages")] - public List Pages { get; set; } + /// + /// Gets or sets the list of page images + /// + [JsonPropertyName("pages")] + public List Pages { get; set; } - [JsonProperty("cover")] - public Image Cover { get; set; } + /// + /// Gets or sets the cover image + /// + [JsonPropertyName("cover")] + public Image Cover { get; set; } - [JsonProperty("thumbnail")] - public Image Thumbnail { get; set; } - } -} + /// + /// Gets or sets the thumbnail image + /// + [JsonPropertyName("thumbnail")] + public Image Thumbnail { get; set; } +} \ No newline at end of file diff --git a/NHentaiAPI/Models/Books/Page.cs b/NHentaiAPI/Models/Books/Page.cs index 7c17a80..81b27a3 100644 --- a/NHentaiAPI/Models/Books/Page.cs +++ b/NHentaiAPI/Models/Books/Page.cs @@ -1,13 +1,27 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; -namespace NHentaiAPI.Models.Books +namespace NHentaiAPI.Models.Books; + +/// +/// Represents a page's basic information +/// +public class Page { - public class Page - { - [JsonProperty("t")] public string T { get; set; } + /// + /// Gets or sets the type identifier of the page + /// + [JsonPropertyName("t")] + public string T { get; set; } - [JsonProperty("w")] public int W { get; set; } + /// + /// Gets or sets the width of the page image + /// + [JsonPropertyName("w")] + public int W { get; set; } - [JsonProperty("h")] public int H { get; set; } - } + /// + /// Gets or sets the height of the page image + /// + [JsonPropertyName("h")] + public int H { get; set; } } \ No newline at end of file diff --git a/NHentaiAPI/Models/Books/Tag.cs b/NHentaiAPI/Models/Books/Tag.cs index d7a144b..732b3cc 100644 --- a/NHentaiAPI/Models/Books/Tag.cs +++ b/NHentaiAPI/Models/Books/Tag.cs @@ -1,22 +1,39 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; -namespace NHentaiAPI.Models.Books +namespace NHentaiAPI.Models.Books; + +/// +/// Represents a tag used to categorize books +/// +public class Tag { - public class Tag - { - [JsonProperty("id")] - public int Id{ get; set; } + /// + /// Gets or sets the unique identifier for the tag + /// + [JsonPropertyName("id")] + public int Id { get; set; } - [JsonProperty("type")] - public string Type{ get; set; } + /// + /// Gets or sets the type of the tag + /// + [JsonPropertyName("type")] + public string Type { get; set; } - [JsonProperty("name")] - public string Name{ get; set; } + /// + /// Gets or sets the name of the tag + /// + [JsonPropertyName("name")] + public string Name { get; set; } - [JsonProperty("url")] - public string Url { get; set; } + /// + /// Gets or sets the URL for the tag's page + /// + [JsonPropertyName("url")] + public string Url { get; set; } - [JsonProperty("count")] - public int Count { get; set; } - } -} + /// + /// Gets or sets the number of books with this tag + /// + [JsonPropertyName("count")] + public int Count { get; set; } +} \ No newline at end of file diff --git a/NHentaiAPI/Models/Books/Title.cs b/NHentaiAPI/Models/Books/Title.cs index 24aa7cf..072a3c7 100644 --- a/NHentaiAPI/Models/Books/Title.cs +++ b/NHentaiAPI/Models/Books/Title.cs @@ -1,16 +1,27 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; -namespace NHentaiAPI.Models.Books +namespace NHentaiAPI.Models.Books; + +/// +/// Represents a book's title in different languages +/// +public class Title { - public class Title - { - [JsonProperty("english")] - public string English { get; set; } + /// + /// Gets or sets the English title + /// + [JsonPropertyName("english")] + public string English { get; set; } - [JsonProperty("japanese")] - public string Japanese { get; set; } + /// + /// Gets or sets the Japanese title + /// + [JsonPropertyName("japanese")] + public string Japanese { get; set; } - [JsonProperty("pretty")] - public string Pretty { get; set; } - } -} + /// + /// Gets or sets the formatted/pretty title + /// + [JsonPropertyName("pretty")] + public string Pretty { get; set; } +} \ No newline at end of file diff --git a/NHentaiAPI/Models/Recommends/BookRecommend.cs b/NHentaiAPI/Models/Recommends/BookRecommend.cs index 47e6c3c..41fe6e8 100644 --- a/NHentaiAPI/Models/Recommends/BookRecommend.cs +++ b/NHentaiAPI/Models/Recommends/BookRecommend.cs @@ -1,12 +1,16 @@ -using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; using NHentaiAPI.Models.Books; -namespace NHentaiAPI.Models.Recommends +namespace NHentaiAPI.Models.Recommends; + +/// +/// Represents a list of recommended books +/// +public class BookRecommend { - public class BookRecommend - { - [JsonProperty("result")] - public List Result { get; set; } - } -} + /// + /// Gets or sets the list of recommended books + /// + [JsonPropertyName("result")] + public List Result { get; set; } +} \ No newline at end of file diff --git a/NHentaiAPI/Models/Searches/SearchResults.cs b/NHentaiAPI/Models/Searches/SearchResults.cs index f01d381..7d5a22d 100644 --- a/NHentaiAPI/Models/Searches/SearchResults.cs +++ b/NHentaiAPI/Models/Searches/SearchResults.cs @@ -1,18 +1,28 @@ -using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; using NHentaiAPI.Models.Books; -namespace NHentaiAPI.Models.Searches +namespace NHentaiAPI.Models.Searches; + +/// +/// Represents search results containing a list of books and pagination information +/// +public class SearchResults { - public class SearchResults - { - [JsonProperty("result")] - public List Result { get; set; } + /// + /// Gets or sets the list of books returned in the search results + /// + [JsonPropertyName("result")] + public List Result { get; set; } - [JsonProperty("num_pages")] - public int NumPages { get; set; } + /// + /// Gets or sets the total number of pages in the search results + /// + [JsonPropertyName("num_pages")] + public int NumPages { get; set; } - [JsonProperty("per_page")] - public int PerPage { get; set; } - } -} + /// + /// Gets or sets the number of items per page + /// + [JsonPropertyName("per_page")] + public int PerPage { get; set; } +} \ No newline at end of file diff --git a/NHentaiAPI/Models/Searches/SortBy.cs b/NHentaiAPI/Models/Searches/SortBy.cs index ee3515b..ade4318 100644 --- a/NHentaiAPI/Models/Searches/SortBy.cs +++ b/NHentaiAPI/Models/Searches/SortBy.cs @@ -1,15 +1,17 @@ -namespace NHentaiAPI.Models.Searches +namespace NHentaiAPI.Models.Searches; + +/// +/// Search sort method +/// +public enum SortBy { - public enum SortBy - { - /// - /// Default sorting - /// - Default, + /// + /// Default sorting + /// + Default, - /// - /// Sorting by popular - /// - Popular - } -} + /// + /// Sorting by popular + /// + Popular +} \ No newline at end of file diff --git a/NHentaiAPI/NHentaiAPI.csproj b/NHentaiAPI/NHentaiAPI.csproj index 82aba83..6ea9fe8 100644 --- a/NHentaiAPI/NHentaiAPI.csproj +++ b/NHentaiAPI/NHentaiAPI.csproj @@ -1,29 +1,42 @@  + + net8.0 + enable + enable + latest + 1.8.0 + 1.8.0 + 1.8.0 + 1.8.0 + true + SylveonDeko + SylveonDeko + nHentai + A (full?) nHentai API implementation for .NET + README.md + LICENSE + https://github.com/SylveonDeko/NHentaiAPI + https://github.com/SylveonDeko/NHentaiAPI.git + git + nhentai;n-hentai;hentai;api;csharp;dotnet + true + $(NoWarn);CS1591 + + [1.8.0] + - Switched to using System.Text.Json instead of Newtonsoft. + - Added proper JSON type handling via converters. + - Updated to .NET 8.0 + - Added proper async handling + - Updated thumbnail and image urls to work with nhentais' load balancing urls. + + - - net5.0;net6.0 - true - Sylveon76, andy840119 - nHentai - A (full?) nHentai API implementation for .NET - - https://github.com/Sylveon76/NHentaiAPI - https://github.com/Sylveon76/NHentaiAPI.git - git - nhentai n-hentai hentai api csharp Sylveon76, andy840119 - - [1.5.0] -Refactor api client - - LICENSE - 1.7.0 - + + + + - - - - - - - - + + + + \ No newline at end of file diff --git a/NHentaiAPI/NHentaiClient.cs b/NHentaiAPI/NHentaiClient.cs index 0e414fc..a5f0b1c 100644 --- a/NHentaiAPI/NHentaiClient.cs +++ b/NHentaiAPI/NHentaiClient.cs @@ -1,248 +1,479 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using Newtonsoft.Json; +using System.Net; +using System.Text.Json; +using System.Text.Json.Serialization; using NHentaiAPI.Models.Books; using NHentaiAPI.Models.Recommends; using NHentaiAPI.Models.Searches; -namespace NHentaiAPI +namespace NHentaiAPI; + +/// +/// Client for interacting with the n-Hentai API +/// +public class NHentaiClient : IDisposable { /// - /// n-Hentai Client - /// copied from : https://github.com/NHMoeDev/NHentai-android/blob/master/app/src/main/kotlin/moe/feng/nhentai/api/ApiConstants.kt + /// Releases the resources used by the HTTP client /// - public class NHentaiClient : IDisposable + public void Dispose() { - #region Client + _client.Dispose(); + } - private readonly HttpClient _client; + #region Client - public NHentaiClient(string userAgent, Dictionary cookies = null) - { - var cookies1 = new CookieContainer(); - var handler = new HttpClientHandler { CookieContainer = cookies1 }; - _client = new HttpClient(handler); - - _client.DefaultRequestHeaders.Add("User-Agent", userAgent); - - if (cookies == null) return; - var cookieUri = new Uri(ApiRootUrl); - - foreach (var cookie in cookies) - { - cookies1.Add(cookieUri, new Cookie(cookie.Key, cookie.Value)); - } - } + private readonly HttpClient _client; + private readonly Random _rand; + + private readonly JsonSerializerOptions _options = new() + { + NumberHandling = JsonNumberHandling.AllowReadingFromString, + PropertyNameCaseInsensitive = true, + Converters = { new JsonStringEnumConverter() } + }; - #endregion + /// + /// Initializes a new instance of the NHentaiClient + /// + /// User agent string to use for requests + /// Optional dictionary of cookies to include with requests + public NHentaiClient(string userAgent, Dictionary? cookies = null) + { + _rand = new Random(); + var cookies1 = new CookieContainer(); + var handler = new HttpClientHandler { CookieContainer = cookies1 }; + _client = new HttpClient(handler); - #region Urls + _client.DefaultRequestHeaders.Add("User-Agent", userAgent); + + if (cookies == null) return; + var cookieUri = new Uri(ApiRootUrl); + + foreach (var cookie in cookies) cookies1.Add(cookieUri, new Cookie(cookie.Key, cookie.Value)); + } - protected virtual string ApiRootUrl => "https://nhentai.net"; + #endregion - protected virtual string ImageRootUrl => "https://i.nhentai.net"; + #region Urls - protected virtual string ThumbnailRootUrl => "https://t.nhentai.net"; + /// + /// Gets the base API endpoint URL + /// + private string ApiRootUrl => "https://nhentai.net"; + + /// + /// Gets the base URL for full-resolution images + /// + private string ImageRootUrl() + { + var rand = _rand.Next(1, 4); + return $"https://i{rand}.nhentai.net"; + } + + /// + /// Gets the base URL for thumbnail images + /// + private string ThumbnailRootUrl() + { + var rand = _rand.Next(1, 4); + return $"https://t{rand}.nhentai.net"; + } - #endregion + #endregion - #region Data urls + #region Data urls - protected virtual string GetHomePageUrl(int pageNum) - => $"{ApiRootUrl}/api/galleries/all?page={pageNum}"; + /// + /// Constructs the URL for retrieving the homepage content + /// + /// Page number to retrieve + /// URL for the specified homepage + private string GetHomePageUrl(int pageNum) + { + return $"{ApiRootUrl}/api/galleries/all?page={pageNum}"; + } - protected virtual string GetSearchUrl(string content,int pageNum) - => $"{ApiRootUrl}/api/galleries/search?" + + /// + /// Constructs the URL for performing a search + /// + /// Search query text + /// Page number of results + /// URL for the search with specified parameters + private string GetSearchUrl(string content, int pageNum) + { + return $"{ApiRootUrl}/api/galleries/search?" + $"query={content.Replace(" ", "+")}&" + $"page={pageNum}"; + } - protected virtual string GetTagUrl(Tag tag, bool isPopularList, int pageNum) - => $"{ApiRootUrl}/api/galleries/tagged?" + + /// + /// Constructs the URL for retrieving content with a specific tag + /// + /// Tag to filter by + /// Whether to sort by popularity + /// Page number to retrieve + /// URL for the tag search with specified parameters + private string GetTagUrl(Tag tag, bool isPopularList, int pageNum) + { + return $"{ApiRootUrl}/api/galleries/tagged?" + $"tag_id={tag.Id}" + $"&page={pageNum}" + (isPopularList ? "&sort=popular" : ""); + } - protected virtual string GetBookDetailsUrl(int bookId) - => $"{ApiRootUrl}/api/gallery/{bookId}"; + /// + /// Constructs the URL for retrieving book details + /// + /// ID of the book + /// URL for the specified book's details + private string GetBookDetailsUrl(int bookId) + { + return $"{ApiRootUrl}/api/gallery/{bookId}"; + } - protected virtual string GetBookRecommendUrl(int bookId) - => $"{ApiRootUrl}/api/gallery/{bookId}/related"; + /// + /// Constructs the URL for retrieving book recommendations + /// + /// ID of the book to get recommendations for + /// URL for recommendations related to the specified book + private string GetBookRecommendUrl(int bookId) + { + return $"{ApiRootUrl}/api/gallery/{bookId}/related"; + } - protected virtual string GetGalleryUrl(int galleryId) - => $"{ImageRootUrl}/galleries/{galleryId}"; + /// + /// Constructs the URL for accessing a gallery's images + /// + /// ID of the gallery + /// Base URL for the gallery's full-size images + private string GetGalleryUrl(int galleryId) + { + return $"{ImageRootUrl()}/galleries/{galleryId}"; + } - protected virtual string GetThumbGalleryUrl(int galleryId) - => $"{ThumbnailRootUrl}/galleries/{galleryId}"; + /// + /// Constructs the URL for accessing a gallery's thumbnail images + /// + /// ID of the gallery + /// Base URL for the gallery's thumbnail images + private string GetThumbGalleryUrl(int galleryId) + { + return $"{ThumbnailRootUrl()}/galleries/{galleryId}"; + } - #endregion + #endregion - #region Picture urls + #region Picture urls - public virtual string GetPictureUrl(Book book, int pageNum) - { - var image = GetImage(book, pageNum); - var fileType = ConvertType(image.Type); - return GetPictureUrl(book.MediaId, pageNum, fileType); - } + /// + /// Gets the URL for a specific page image in a book + /// + /// Book containing the image + /// Page number to retrieve + /// Full URL for the specified page image + public string GetPictureUrl(Book book, int pageNum) + { + var image = GetImage(book, pageNum); + var fileType = ConvertType(image.Type); + return GetPictureUrl(book.MediaId, pageNum, fileType); + } - public virtual string GetThumbPictureUrl(Book book, int pageNum) - { - var image = GetImage(book, pageNum); - var fileType = ConvertType(image.Type); - return GetThumbPictureUrl(book.MediaId, pageNum, fileType); - } + /// + /// Gets the URL for a thumbnail of a specific page in a book + /// + /// Book containing the image + /// Page number to retrieve + /// Full URL for the specified page's thumbnail + public string GetThumbPictureUrl(Book book, int pageNum) + { + var image = GetImage(book, pageNum); + var fileType = ConvertType(image.Type); + return GetThumbPictureUrl(book.MediaId, pageNum, fileType); + } - public virtual string GetBigCoverUrl(Book book) - => GetBigCoverUrl(book.MediaId); + /// + /// Gets the URL for a book's large cover image + /// + /// Book to get the cover for + /// Full URL for the book's cover image + public string GetBigCoverUrl(Book book) + { + return GetBigCoverUrl(book.MediaId); + } - public virtual string GetOriginPictureUrl(Book book, int pageNum) - => GetOriginPictureUrl(book.MediaId, pageNum); + /// + /// Gets the URL for the original full-size version of a page + /// + /// Book containing the image + /// Page number to retrieve + /// Full URL for the original version of the specified page + public string GetOriginPictureUrl(Book book, int pageNum) + { + return GetOriginPictureUrl(book.MediaId, pageNum); + } - public virtual string GetBookThumbUrl(Book book) - { - var fileType = ConvertType(book.Images.Cover.Type); - return GetBookThumbUrl(book.MediaId, fileType); - } + /// + /// Gets the URL for a book's thumbnail image + /// + /// Book to get the thumbnail for + /// Full URL for the book's thumbnail image + public string GetBookThumbUrl(Book book) + { + var fileType = ConvertType(book.Images.Cover.Type); + return GetBookThumbUrl(book.MediaId, fileType); + } - protected virtual string GetPictureUrl(int galleryId , int pageNum ,string fileType) - => $"{GetGalleryUrl(galleryId)}/{pageNum}.{fileType}"; + /// + /// Constructs the URL for a full-size page image + /// + /// ID of the gallery containing the image + /// Page number of the image + /// File extension of the image + /// Full URL for the specified page image + private string GetPictureUrl(int galleryId, int pageNum, string fileType) + { + return $"{GetGalleryUrl(galleryId)}/{pageNum}.{fileType}"; + } - protected virtual string GetThumbPictureUrl(int galleryId , int pageNum ,string fileType) - => $"{GetThumbGalleryUrl(galleryId)}/{pageNum}t.{fileType}"; + /// + /// Constructs the URL for a page's thumbnail image + /// + /// ID of the gallery containing the image + /// Page number of the image + /// File extension of the image + /// Full URL for the specified page's thumbnail + private string GetThumbPictureUrl(int galleryId, int pageNum, string fileType) + { + return $"{GetThumbGalleryUrl(galleryId)}/{pageNum}t.{fileType}"; + } - protected virtual string GetBigCoverUrl(int galleryId) - => $"{GetThumbGalleryUrl(galleryId)}/cover.jpg"; + /// + /// Constructs the URL for a gallery's cover image + /// + /// ID of the gallery + /// Full URL for the gallery's cover image + private string GetBigCoverUrl(int galleryId) + { + return $"{GetThumbGalleryUrl(galleryId)}/cover.jpg"; + } - protected virtual string GetOriginPictureUrl(int galleryId , int pageNum) - => GetPictureUrl(galleryId, pageNum, "jpg"); + /// + /// Constructs the URL for a page's original full-size image + /// + /// ID of the gallery containing the image + /// Page number of the image + /// Full URL for the original version of the specified page + private string GetOriginPictureUrl(int galleryId, int pageNum) + { + return GetPictureUrl(galleryId, pageNum, "jpg"); + } - protected virtual string GetBookThumbUrl(int galleryId ,string fileType = "jpg") - => $"{GetThumbGalleryUrl(galleryId)}/thumb.{fileType ?? "jpg"}"; + /// + /// Constructs the URL for a gallery's thumbnail image + /// + /// ID of the gallery + /// File extension of the image, defaults to jpg + /// Full URL for the gallery's thumbnail image + private string GetBookThumbUrl(int galleryId, string fileType = "jpg") + { + return $"{GetThumbGalleryUrl(galleryId)}/thumb.{fileType ?? "jpg"}"; + } - #endregion + #endregion - #region Utilities + #region Utilities - protected virtual async Task GetData(string rootUrl) - { - var json = await _client.GetStringAsync(rootUrl); - return JsonConvert.DeserializeObject(json); - } + /// + /// Retrieves and deserializes JSON data from the specified URL + /// + /// Type to deserialize the JSON into + /// URL to retrieve the JSON from + /// Deserialized object of type TOutput + private async Task GetData(string rootUrl) + { + var json = await _client.GetStreamAsync(rootUrl); + return await JsonSerializer.DeserializeAsync(json, _options); + } - protected virtual async Task GetByteData(string rootUrl) - { - var data = await _client.GetByteArrayAsync(rootUrl); - return data; - } + /// + /// Downloads binary data from the specified URL + /// + /// URL to download from + /// Byte array containing the downloaded data + private async Task GetByteData(string rootUrl) + { + var data = await _client.GetByteArrayAsync(rootUrl); + return data; + } - protected virtual Image GetImage(Book book, int pageNum) - { - if (book == null) - throw new ArgumentNullException(nameof(book)); + /// + /// Gets the image information for a specific page in a book + /// + /// Book containing the image + /// Page number to retrieve (1-based index) + /// Image information for the specified page + /// Thrown when book is null + private Image GetImage(Book book, int pageNum) + { + if (book == null) + throw new ArgumentNullException(nameof(book)); - var page = book.Images.Pages[pageNum - 1]; - return page; - } + var page = book.Images.Pages[pageNum - 1]; + return page; + } - protected virtual string ConvertType(ImageType type) + /// + /// Converts an ImageType enum value to its corresponding file extension + /// + /// ImageType to convert + /// File extension string (without dot) + /// Thrown when the image type is not supported + private string ConvertType(ImageType type) + { + switch (type) { - switch (type) - { - case ImageType.Gif: - return "gif"; - case ImageType.Jpg: - return "jpg"; - case ImageType.Png: - return "png"; - default: - throw new NotSupportedException($"Format {nameof(type)} does not support."); - } + case ImageType.Gif: + return "gif"; + case ImageType.Jpg: + return "jpg"; + case ImageType.Png: + return "png"; + default: + throw new NotSupportedException($"Format {nameof(type)} does not support."); } + } - #endregion + #endregion - #region Search + #region Search - public virtual Task GetHomePageListAsync(int pageNum) - { - var url = GetHomePageUrl(pageNum); - return GetData(url); - } + /// + /// Retrieves a list of content from the homepage + /// + /// Page number to retrieve + /// Search results containing content from the specified page + public async Task GetHomePageListAsync(int pageNum) + { + var url = GetHomePageUrl(pageNum); + return await GetData(url); + } - public virtual Task GetSearchPageListAsync(string keyword,int pageNum) - { - var url = GetSearchUrl(keyword, pageNum); - return GetData(url); - } + /// + /// Searches for content using the specified keyword + /// + /// Search term to look for + /// Page number of results to retrieve + /// Search results matching the keyword + public async Task GetSearchPageListAsync(string keyword, int pageNum) + { + var url = GetSearchUrl(keyword, pageNum); + return await GetData(url); + } - public virtual Task GetTagPageListAsync(Tag tag, SortBy sortBy, int pageNum) - { - var url = GetTagUrl(tag, sortBy == SortBy.Popular, pageNum); - return GetData(url); - } + /// + /// Retrieves content tagged with a specific tag + /// + /// Tag to filter by + /// How to sort the results + /// Page number to retrieve + /// Search results for content with the specified tag + public async Task GetTagPageListAsync(Tag tag, SortBy sortBy, int pageNum) + { + var url = GetTagUrl(tag, sortBy == SortBy.Popular, pageNum); + return await GetData(url); + } - #endregion + #endregion - #region Books + #region Books - public virtual Task GetBookAsync(int bookId) - { - var url = GetBookDetailsUrl(bookId); - return GetData(url); - } + /// + /// Retrieves details for a specific book + /// + /// ID of the book to retrieve + /// Book details for the specified ID + public async Task GetBookAsync(int bookId) + { + var url = GetBookDetailsUrl(bookId); + return await GetData(url); + } - public virtual async Task GetBookRecommendAsync(int bookId) + /// + /// Gets recommendations based on a specific book + /// + /// ID of the book to get recommendations for + /// List of recommended books + public async Task GetBookRecommendAsync(int bookId) + { + var url = GetBookRecommendUrl(bookId); + var book = await GetData(url); + return new BookRecommend { - var url = GetBookRecommendUrl(bookId); - var book = await GetData(url); - return new BookRecommend - { - Result = new List { book } - }; - } - - #endregion - - #region Picture + Result = new List { book } + }; + } - public virtual Task GetPictureAsync(Book book, int pageNum) - { - var url = GetPictureUrl(book, pageNum); - return GetByteData(url); - } + #endregion - public virtual Task GetThumbPictureAsync(Book book, int pageNum) - { - var url = GetThumbPictureUrl(book, pageNum); - return GetByteData(url); - } + #region Picture - public virtual Task GetBigCoverPictureAsync(Book book) - { - var url = GetBigCoverUrl(book.MediaId); - return GetByteData(url); - } + /// + /// Downloads a full-size page image + /// + /// Book containing the image + /// Page number to download + /// Byte array containing the image data + public async Task GetPictureAsync(Book book, int pageNum) + { + var url = GetPictureUrl(book, pageNum); + return await GetByteData(url); + } - public virtual Task GetOriginPictureAsync(Book book, int pageNum) - { - var url = GetOriginPictureUrl(book.MediaId, pageNum); - return GetByteData(url); - } + /// + /// Downloads a thumbnail image for a page + /// + /// Book containing the image + /// Page number to download + /// Byte array containing the thumbnail data + public async Task GetThumbPictureAsync(Book book, int pageNum) + { + var url = GetThumbPictureUrl(book, pageNum); + return await GetByteData(url); + } - public virtual Task GetBookThumbPictureAsync(Book book) - { - var url = GetBookThumbUrl(book); - return GetByteData(url); - } + /// + /// Downloads a book's large cover image + /// + /// Book to get the cover for + /// Byte array containing the cover image data + public async Task GetBigCoverPictureAsync(Book book) + { + var url = GetBigCoverUrl(book.MediaId); + return await GetByteData(url); + } - #endregion + /// + /// Downloads an original full-size page image + /// + /// Book containing the image + /// Page number to download + /// Byte array containing the original image data + public async Task GetOriginPictureAsync(Book book, int pageNum) + { + var url = GetOriginPictureUrl(book.MediaId, pageNum); + return await GetByteData(url); + } - public void Dispose() - { - _client.Dispose(); - } + /// + /// Downloads a book's thumbnail image + /// + /// Book to get the thumbnail for + /// Byte array containing the thumbnail data + public async Task GetBookThumbPictureAsync(Book book) + { + var url = GetBookThumbUrl(book); + return await GetByteData(url); } -} + + #endregion +} \ No newline at end of file diff --git a/README.md b/README.md index 4b89b98..40332f8 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,106 @@ # NHentaiAPI -[![Build status](https://ci.appveyor.com/api/projects/status/32r7s2skrgm9ubva?svg=true)](https://ci.appveyor.com/project/Sylveon76/nhentaiapi) +[![Build status](https://ci.appveyor.com/api/projects/status/32r7s2skrgm9ubva?svg=true)](https://ci.appveyor.com/project/SylveonDeko/nhentaiapi) [![NuGet](https://img.shields.io/nuget/v/NHentaiAPI.svg)](https://www.nuget.org/packages/NHentaiAPI) [![NuGet](https://img.shields.io/nuget/dt/NHentaiAPI.svg)](https://www.nuget.org/packages/NHentaiAPI) -[![NuGet](https://img.shields.io/badge/月子我婆-passed-ff69b4.svg)](https://github.com/Sylveon76/NHentaiAPI) +[![NuGet](https://img.shields.io/badge/月子我婆-passed-ff69b4.svg)](https://github.com/SylveonDeko/NHentaiAPI) -A (full) nHentai API implementation for .NET +A full nHentai API implementation for .NET -If N-Hentai change the api format, please throw a issue to let me know :) +⚠️ If nHentai changes their API format, please create an issue to let me know! -# Check out my other projects -- Mewdeko https://github.com/Sylveon76/Mewdeko -- MartineApi https://github.com/Sylveon76/MartineApi.Net -- NekosBestApi https://github.com/Sylveon76/Nekos.Best-API +## Important Notes -## This package can get +### User Agent Requirements +A User-Agent is **required** to use this API. The client will throw an error if none is provided. You can get your User-Agent by: +1. Going to https://www.whatismybrowser.com/detect/what-is-my-user-agent/ +2. Or by opening Developer Tools (F12) in your browser, going to Network tab, and looking at the "User-Agent" header in any request -Search: - -1. Home page search result +Example: +```csharp +var client = new NHentaiClient("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"); +``` -2. Search result by `keyword` +### CSRF Token (If Required) +If nHentai implements CSRF protection, you can get the token by: +1. Opening Developer Tools (F12) +2. Going to Network tab +3. Looking for a request header named 'x-csrf-token' or a cookie named 'csrf_token' +4. Pass it to the client using the cookies dictionary + +**Important**: The CSRF token must be obtained from the same IP address and User-Agent that will be used with the API. Using a token from a different IP or User-Agent will result in authentication failures. + +```csharp +var cookies = new Dictionary +{ + {"csrf_token", "your-token-here"} +}; +var client = new NHentaiClient("your-user-agent", cookies); +``` -3. Search result by `tag`, can be sort by popular +## Features -4. Search tags can be filtered by putting `-` in front of them +### Search Capabilities: -Book detail: +1. Browse homepage content +2. Search by keywords +3. Search by tags with optional popularity sorting +4. Filter tags using `-` prefix (exclusion) -1. Book detail +### Book Operations: -2. Related book +1. Fetch book details +2. Get related books -Picture: +### Image Operations: -1. Page picture (preview picture, thumbnail and origin picture) -2. Cover picture (preview picture and thumbnail) +1. Page images (preview, thumbnail, and original quality) +2. Cover images (preview and thumbnail) -## Demo +## Usage Examples -Search book: +### Search Books: +```csharp +// Initialize client with User-Agent +var client = new NHentaiClient("your-user-agent-string"); -```CSharp -//generate client -var client = new NHentaiClient(); +// Search with filters +var result = await client.GetSearchPageListAsync("school swimsuit full color -loli", 2); -//https://nhentai.net/api/galleries/search?query=school%20swimsuit%20loli%20full%20color&page=2 -var result = await client.GetSearchPageListAsync("school swimsuit full color -loli",2); +// Browse homepage +var homeResults = await client.GetHomePageListAsync(1); ``` -Get book detail: - -```CSharp -//generate client -var client = new NHentaiClient(); +### Get Book Details: +```csharp +var client = new NHentaiClient("your-user-agent-string"); -//get book no 123 +// Get book by ID var book = await client.GetBookAsync(123); +// Get related books +var related = await client.GetBookRecommendAsync(123); ``` -Get cover and image: - -```CSharp -//get book no 123 +### Get Images: +```csharp var book = await client.GetBookAsync(123); -//https://i.nhentai.net/galleries/635/1.jpg +// Get full page image byte[] picture = await client.GetPictureAsync(book, 1); -//https://i.nhentai.net/galleries/635/1.jpg +// Get cover image byte[] cover = await client.GetBigCoverPictureAsync(book); + +// Get thumbnails +byte[] thumbnail = await client.GetThumbPictureAsync(book, 1); +byte[] coverThumb = await client.GetBookThumbPictureAsync(book); ``` +## Check Out My Other Projects +- [Mewdeko](https://github.com/SylveonDeko/Mewdeko) - Discord Bot +- [MartineApi](https://github.com/SylveonDeko/MartineApi.Net) - Image API Wrapper +- [NekosBestApi](https://github.com/SylveonDeko/Nekos.Best-API) - Anime Image API + +## License +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. \ No newline at end of file