Added UI app.
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
using MangaReader.Core.Search;
|
||||
using MangaReader.Core.HttpService;
|
||||
using MangaReader.Core.Metadata;
|
||||
using MangaReader.Core.Search;
|
||||
using MangaReader.Core.Sources.MangaDex.Api;
|
||||
using MangaReader.Core.Sources.MangaDex.Metadata;
|
||||
using MangaReader.Core.Sources.MangaDex.Search;
|
||||
using MangaReader.Core.Sources.NatoManga.Api;
|
||||
using MangaReader.Core.Sources.NatoManga.Metadata;
|
||||
using MangaReader.Core.Sources.NatoManga.Search;
|
||||
|
||||
#pragma warning disable IDE0130 // Namespace does not match folder structure
|
||||
@@ -10,10 +16,24 @@ public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddMangaReader(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IMangaSearchProvider, NatoMangaSearchProvider>();
|
||||
// Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0
|
||||
services.AddHttpClient<IHttpService, HttpService>(client =>
|
||||
{
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0");
|
||||
});
|
||||
|
||||
services.AddScoped<IHttpService, HttpService>();
|
||||
|
||||
//services.AddScoped<INatoMangaClient, NatoMangaClient>();
|
||||
services.AddScoped<IMangaDexClient, MangaDexClient>();
|
||||
|
||||
//services.AddScoped<IMangaSearchProvider, NatoMangaSearchProvider>();
|
||||
services.AddScoped<IMangaSearchProvider, MangaDexSearchProvider>();
|
||||
services.AddScoped<IMangaSearchCoordinator, MangaSearchCoordinator>();
|
||||
|
||||
//services.AddScoped<IMangaMetadataProvider, NatoMangaWebCrawler>();
|
||||
services.AddScoped<IMangaMetadataProvider, MangaDexMetadataProvider>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,16 @@
|
||||
namespace MangaReader.Core.HttpService;
|
||||
|
||||
public class HttpService(HttpClient httpClient) : IHttpService
|
||||
public class HttpService : IHttpService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public HttpService(HttpClient httpClient)
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0");
|
||||
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public Task<string> GetStringAsync(string url, CancellationToken cancellationToken)
|
||||
=> httpClient.GetStringAsync(url, cancellationToken);
|
||||
=> _httpClient.GetStringAsync(url, cancellationToken);
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -9,4 +9,5 @@ public interface IMangaDexClient
|
||||
Task<MangaDexResponse?> GetFeedAsync(Guid mangaGuid, CancellationToken cancellationToken);
|
||||
Task<MangaDexChapterResponse?> GetChapterAsync(Guid chapterGuid, CancellationToken cancellationToken);
|
||||
Task<MangaDexResponse?> GetCoverArtAsync(Guid mangaGuid, CancellationToken cancellationToken);
|
||||
Task<MangaDexResponse?> GetCoverArtAsync(Guid[] mangaGuid, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -20,17 +20,28 @@ namespace MangaReader.Core.Sources.MangaDex.Api
|
||||
}
|
||||
|
||||
private async Task<MangaDexResponse?> GetAsync(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
//string response = await httpService.GetStringAsync(url, cancellationToken);
|
||||
|
||||
//return JsonSerializer.Deserialize<MangaDexResponse>(response, _jsonSerializerOptions);
|
||||
|
||||
return await GetAsync<MangaDexResponse>(url, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<T?> GetAsync<T>(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
string response = await httpService.GetStringAsync(url, cancellationToken);
|
||||
|
||||
return JsonSerializer.Deserialize<MangaDexResponse>(response, _jsonSerializerOptions);
|
||||
return JsonSerializer.Deserialize<T?>(response, _jsonSerializerOptions);
|
||||
}
|
||||
|
||||
public async Task<MangaDexResponse?> SearchMangaByTitleAsync(string title, CancellationToken cancellationToken)
|
||||
{
|
||||
string normalizedKeyword = GetNormalizedKeyword(title);
|
||||
|
||||
return await GetAsync($"https://api.mangadex.org/manga?title={normalizedKeyword}&limit=5", cancellationToken);
|
||||
//return await GetAsync($"https://api.mangadex.org/manga?title={normalizedKeyword}&limit=5", cancellationToken);
|
||||
return await GetAsync($"https://api.mangadex.org/manga?title={normalizedKeyword}&limit=10&contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&includes[]=cover_art&order[followedCount]=desc&order[relevance]=desc", cancellationToken);
|
||||
//
|
||||
}
|
||||
|
||||
public async Task<MangaDexResponse?> SearchMangaByAuthorAsync(string author, CancellationToken cancellationToken)
|
||||
@@ -64,15 +75,24 @@ namespace MangaReader.Core.Sources.MangaDex.Api
|
||||
|
||||
public async Task<MangaDexChapterResponse?> GetChapterAsync(Guid chapterGuid, CancellationToken cancellationToken)
|
||||
{
|
||||
string url = $"https://api.mangadex.org/at-home/server/{chapterGuid}?forcePort443=false";
|
||||
string response = await httpService.GetStringAsync(url, cancellationToken);
|
||||
//string url = $"https://api.mangadex.org/at-home/server/{chapterGuid}?forcePort443=false";
|
||||
//string response = await httpService.GetStringAsync(url, cancellationToken);
|
||||
|
||||
return JsonSerializer.Deserialize<MangaDexChapterResponse>(response, _jsonSerializerOptions);
|
||||
//return JsonSerializer.Deserialize<MangaDexChapterResponse>(response, _jsonSerializerOptions);
|
||||
|
||||
return await GetAsync<MangaDexChapterResponse>($"https://api.mangadex.org/at-home/server/{chapterGuid}?forcePort443=false", cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<MangaDexResponse?> GetCoverArtAsync(Guid mangaGuid, CancellationToken cancellationToken)
|
||||
{
|
||||
return await GetAsync($"https://api.mangadex.org/cover?order[volume]=asc&manga[]={mangaGuid}&limit=100&offset=0", cancellationToken);
|
||||
return await GetCoverArtAsync([mangaGuid], cancellationToken);
|
||||
//return await GetAsync($"https://api.mangadex.org/cover?order[volume]=asc&manga[]={mangaGuid}&limit=100&offset=0", cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<MangaDexResponse?> GetCoverArtAsync(Guid[] mangaGuids, CancellationToken cancellationToken)
|
||||
{
|
||||
string mangaGuidQuery = string.Join("&manga[]=", mangaGuids);
|
||||
return await GetAsync($"https://api.mangadex.org/cover?order[volume]=asc&manga[]={mangaGuidQuery}&limit=100&offset=0", cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,43 +21,83 @@ public partial class MangaDexSearchProvider(IMangaDexClient mangaDexClient) : IM
|
||||
if (response == null || (response is not MangaDexCollectionResponse collectionResponse))
|
||||
return [];
|
||||
|
||||
List<MangaSearchResult> mangaSearchResults = [];
|
||||
MangaEntity[] mangaEntities = [.. collectionResponse.Data.Where(entity => entity is MangaEntity).Cast<MangaEntity>()];
|
||||
|
||||
foreach (MangaDexEntity entity in collectionResponse.Data)
|
||||
if (mangaEntities.Length == 0)
|
||||
return [];
|
||||
|
||||
Dictionary<Guid, List<CoverArtEntity>> mangaCoverArtMap = await GetCoverArtFileNamesAsync(mangaEntities, cancellationToken);
|
||||
|
||||
List<MangaSearchResult> mangaSearchResults = [];
|
||||
Dictionary<Guid, MangaSearchResult> thing = [];
|
||||
|
||||
foreach (MangaEntity mangaEntity in mangaEntities)
|
||||
{
|
||||
MangaSearchResult? mangaSearchResult = GetMangaSearchResult(entity);
|
||||
CoverArtEntity[] coverArtEntites = [.. mangaCoverArtMap[mangaEntity.Id]];
|
||||
|
||||
MangaSearchResult? mangaSearchResult = GetMangaSearchResult(mangaEntity, coverArtEntites);
|
||||
|
||||
if (mangaSearchResult == null)
|
||||
continue;
|
||||
|
||||
mangaSearchResults.Add(mangaSearchResult);
|
||||
}
|
||||
|
||||
if (thing.Count > 0)
|
||||
{
|
||||
Guid[] mangaGuids = thing.Select(x => x.Key).ToArray();
|
||||
var reults = await GetCoverArtFileNamesAsync(mangaGuids, cancellationToken);
|
||||
//var reults = await mangaDexClient.GetCoverArtAsync(mangaGuids, cancellationToken);
|
||||
}
|
||||
|
||||
return [.. mangaSearchResults];
|
||||
}
|
||||
|
||||
private static MangaSearchResult? GetMangaSearchResult(MangaDexEntity entity)
|
||||
private static MangaSearchResult? GetMangaSearchResult(MangaEntity mangaEntity, CoverArtEntity[] coverArtEntites)
|
||||
{
|
||||
if (entity is not MangaEntity mangaEntity)
|
||||
MangaAttributes? mangaAttributes = mangaEntity.Attributes;
|
||||
|
||||
if (mangaAttributes == null)
|
||||
return null;
|
||||
|
||||
if (mangaEntity.Attributes == null)
|
||||
return null;
|
||||
|
||||
string title = mangaEntity.Attributes.Title.FirstOrDefault().Value;
|
||||
string title = GetTitle(mangaAttributes);
|
||||
string slug = GenerateSlug(title);
|
||||
|
||||
MangaSearchResult mangaSearchResult = new()
|
||||
{
|
||||
Title = title,
|
||||
Description = GetDescription(mangaAttributes),
|
||||
Url = $"https://mangadex.org/title/{mangaEntity.Id}/{slug}",
|
||||
Thumbnail = GetThumbnail(mangaEntity)
|
||||
Thumbnail = GetThumbnail(mangaEntity, coverArtEntites)
|
||||
};
|
||||
|
||||
return mangaSearchResult;
|
||||
}
|
||||
|
||||
private static string GetTitle(MangaAttributes attributes)
|
||||
{
|
||||
var alternateTitle = attributes.AltTitles.Where(x => x.ContainsKey("en")).FirstOrDefault();
|
||||
|
||||
if (alternateTitle?.Count > 0)
|
||||
return alternateTitle["en"];
|
||||
|
||||
if (attributes.Title.TryGetValue("en", out string? title))
|
||||
return title;
|
||||
|
||||
if (attributes.Title.Count > 0)
|
||||
return attributes.Title.ElementAt(0).Value;
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string GetDescription(MangaAttributes attributes)
|
||||
{
|
||||
if (attributes.Description.TryGetValue("en", out string? description))
|
||||
return description;
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public static string GenerateSlug(string title)
|
||||
{
|
||||
// title.ToLowerInvariant().Normalize(NormalizationForm.FormD);
|
||||
@@ -70,7 +110,18 @@ public partial class MangaDexSearchProvider(IMangaDexClient mangaDexClient) : IM
|
||||
return title.Trim('-');
|
||||
}
|
||||
|
||||
private static string? GetThumbnail(MangaDexEntity mangaDexEntity)
|
||||
private static string? GetThumbnail(MangaDexEntity mangaDexEntity, CoverArtEntity[] coverArtEntites)
|
||||
{
|
||||
string? fileName = GetCoverArtFileNameFromMangaEntity(mangaDexEntity)
|
||||
?? GetCoverArtFileNameFromCoverArtEntities(coverArtEntites);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
return null;
|
||||
|
||||
return $"https://mangadex.org/covers/{mangaDexEntity.Id}/{fileName}";
|
||||
}
|
||||
|
||||
private static string? GetCoverArtFileNameFromMangaEntity(MangaDexEntity mangaDexEntity)
|
||||
{
|
||||
CoverArtEntity? coverArtEntity = (CoverArtEntity?)mangaDexEntity.Relationships.FirstOrDefault(entity =>
|
||||
entity is CoverArtEntity);
|
||||
@@ -78,11 +129,59 @@ public partial class MangaDexSearchProvider(IMangaDexClient mangaDexClient) : IM
|
||||
if (coverArtEntity == null || string.IsNullOrWhiteSpace(coverArtEntity.Attributes?.FileName))
|
||||
return null;
|
||||
|
||||
string? fileName = coverArtEntity.Attributes?.FileName;
|
||||
return coverArtEntity.Attributes?.FileName;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(coverArtEntity.Attributes?.FileName))
|
||||
return null;
|
||||
private static string? GetCoverArtFileNameFromCoverArtEntities(CoverArtEntity[] coverArtEntites)
|
||||
{
|
||||
return coverArtEntites.Where(coverArtEntity =>
|
||||
string.IsNullOrWhiteSpace(coverArtEntity.Attributes?.FileName) == false).FirstOrDefault()?.Attributes!.FileName;
|
||||
}
|
||||
|
||||
return $"https://mangadex.org/covers/{mangaDexEntity.Id}/{fileName}";
|
||||
private async Task<Dictionary<Guid, List<CoverArtEntity>>> GetCoverArtFileNamesAsync(MangaEntity[] mangaEntities, CancellationToken cancellationToken)
|
||||
{
|
||||
Guid[] mangaGuids = [.. mangaEntities.Select(entity => entity.Id)];
|
||||
|
||||
return await GetCoverArtFileNamesAsync(mangaGuids, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Dictionary<Guid, List<CoverArtEntity>>> GetCoverArtFileNamesAsync(Guid[] mangaGuids, CancellationToken cancellationToken)
|
||||
{
|
||||
Dictionary<Guid, List<CoverArtEntity>> result = [];
|
||||
|
||||
foreach (Guid mangaGuid in mangaGuids)
|
||||
{
|
||||
result.Add(mangaGuid, []);
|
||||
}
|
||||
|
||||
MangaDexResponse? response = await mangaDexClient.GetCoverArtAsync(mangaGuids, cancellationToken);
|
||||
|
||||
if (response == null || (response is not MangaDexCollectionResponse collectionResponse))
|
||||
return result;
|
||||
|
||||
CoverArtEntity[] coverArtEntities = [.. collectionResponse.Data.Where(entity => entity is CoverArtEntity).Cast<CoverArtEntity>()];
|
||||
|
||||
if (coverArtEntities.Length == 0)
|
||||
return result;
|
||||
|
||||
CoverArtEntity[] orderedCoverArtEntities = [.. coverArtEntities.OrderBy(x => x.Attributes?.Volume)];
|
||||
|
||||
foreach (var coverArtEntity in orderedCoverArtEntities)
|
||||
{
|
||||
if (coverArtEntity.Attributes == null)
|
||||
continue;
|
||||
|
||||
MangaEntity? mangaEntity = (MangaEntity?)coverArtEntity.Relationships.FirstOrDefault(relationship => relationship is MangaEntity);
|
||||
|
||||
if (mangaEntity == null)
|
||||
continue;
|
||||
|
||||
if (result.ContainsKey(mangaEntity.Id) == false)
|
||||
continue;
|
||||
|
||||
result[mangaEntity.Id].Add(coverArtEntity);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user