diff --git a/MangaReader.Core/Common/Language.cs b/MangaReader.Core/Common/Language.cs index 059f1dd..9525d6c 100644 --- a/MangaReader.Core/Common/Language.cs +++ b/MangaReader.Core/Common/Language.cs @@ -2,7 +2,8 @@ public enum Language { + Unknown, Japanese, - Romanji, + Romaji, English } \ No newline at end of file diff --git a/MangaReader.Core/Extensions/ServiceCollectionExtensions.cs b/MangaReader.Core/Extensions/ServiceCollectionExtensions.cs index ea08459..6ea4405 100644 --- a/MangaReader.Core/Extensions/ServiceCollectionExtensions.cs +++ b/MangaReader.Core/Extensions/ServiceCollectionExtensions.cs @@ -31,7 +31,7 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); - //services.AddScoped(); + ///services.AddScoped(); services.AddScoped(); return services; diff --git a/MangaReader.Core/HttpService/HttpService.cs b/MangaReader.Core/HttpService/HttpService.cs index 54abc12..192c18d 100644 --- a/MangaReader.Core/HttpService/HttpService.cs +++ b/MangaReader.Core/HttpService/HttpService.cs @@ -1,16 +1,25 @@ namespace MangaReader.Core.HttpService; -public class HttpService : IHttpService +public class HttpService(HttpClient httpClient) : IHttpService { - private readonly HttpClient _httpClient; + public Task GetStringAsync(string url, CancellationToken cancellationToken) + => GetStringAsync(url, new Dictionary(), cancellationToken); - public HttpService(HttpClient httpClient) + public async Task GetStringAsync(string url, IDictionary headers, CancellationToken cancellationToken) { + using HttpRequestMessage request = new(HttpMethod.Get, url); + + foreach (KeyValuePair header in headers) + { + request.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + //httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("MangaReader/1.0"); httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0"); - _httpClient = httpClient; - } + using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); - public Task GetStringAsync(string url, CancellationToken cancellationToken) - => _httpClient.GetStringAsync(url, cancellationToken); + return await response.Content.ReadAsStringAsync(cancellationToken); + } } \ No newline at end of file diff --git a/MangaReader.Core/HttpService/IHttpService.cs b/MangaReader.Core/HttpService/IHttpService.cs index be23c5d..3ded7b9 100644 --- a/MangaReader.Core/HttpService/IHttpService.cs +++ b/MangaReader.Core/HttpService/IHttpService.cs @@ -3,4 +3,5 @@ public interface IHttpService { Task GetStringAsync(string url, CancellationToken cancellationToken); + Task GetStringAsync(string url, IDictionary headers, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/MangaReader.Core/Metadata/SourceMangaLanguage.cs b/MangaReader.Core/Metadata/SourceMangaLanguage.cs index f022220..c96f59e 100644 --- a/MangaReader.Core/Metadata/SourceMangaLanguage.cs +++ b/MangaReader.Core/Metadata/SourceMangaLanguage.cs @@ -1,9 +1,9 @@ namespace MangaReader.Core.Metadata; -public enum SourceMangaLanguage -{ - Unknown, - Japanese, - Romanji, - English -} \ No newline at end of file +//public enum SourceMangaLanguage +//{ +// Unknown, +// Japanese, +// Romanji, +// English +//} \ No newline at end of file diff --git a/MangaReader.Core/Metadata/SourceMangaTitle.cs b/MangaReader.Core/Metadata/SourceMangaTitle.cs index c72bb30..634d338 100644 --- a/MangaReader.Core/Metadata/SourceMangaTitle.cs +++ b/MangaReader.Core/Metadata/SourceMangaTitle.cs @@ -1,7 +1,9 @@ -namespace MangaReader.Core.Metadata; +using MangaReader.Core.Common; + +namespace MangaReader.Core.Metadata; public class SourceMangaTitle { public required string Title { get; set; } - public SourceMangaLanguage Language { get; set; } + public Language Language { get; set; } } \ No newline at end of file diff --git a/MangaReader.Core/Pipeline/MangaPipeline.cs b/MangaReader.Core/Pipeline/MangaPipeline.cs index e33fbc2..04498b9 100644 --- a/MangaReader.Core/Pipeline/MangaPipeline.cs +++ b/MangaReader.Core/Pipeline/MangaPipeline.cs @@ -10,11 +10,14 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline public async Task RunAsync(MangaPipelineRequest request) { string sourceName = request.SourceName; + string sourceUrl = request.SourceUrl; SourceManga sourceManga = request.SourceManga; Source source = await GetOrAddSourceAsync(sourceName); Manga manga = await GetOrAddMangaAsync(sourceManga); + await AddMangaSourceAsync(sourceUrl, manga, source); + foreach (SourceMangaTitle alternateTitle in sourceManga.AlternateTitles) { await AddTitleAsync(manga, alternateTitle); @@ -83,6 +86,24 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline [GeneratedRegex(@"\s+")] private static partial Regex RemoveSpacesWithDashRegex(); + private async Task AddMangaSourceAsync(string sourceUrl, Manga manga, Source source) + { + MangaSource? mangaSource = await context.MangaSources.FirstOrDefaultAsync(ms => + ms.Manga == manga && ms.Source == source && ms.Url == sourceUrl); + + if (mangaSource != null) + return; + + mangaSource = new() + { + Manga = manga, + Source = source, + Url = sourceUrl + }; + + context.MangaSources.Add(mangaSource); + } + private async Task AddTitleAsync(Manga manga, SourceMangaTitle sourceMangaTitle) { MangaTitle? mangaTitle = await context.MangaTitles.FirstOrDefaultAsync(mt => diff --git a/MangaReader.Core/Pipeline/MangaPipelineRequest.cs b/MangaReader.Core/Pipeline/MangaPipelineRequest.cs index d8186c9..2894e71 100644 --- a/MangaReader.Core/Pipeline/MangaPipelineRequest.cs +++ b/MangaReader.Core/Pipeline/MangaPipelineRequest.cs @@ -5,5 +5,6 @@ namespace MangaReader.Core.Pipeline; public class MangaPipelineRequest { public required string SourceName { get; init; } + public required string SourceUrl { get; init; } public required SourceManga SourceManga { get; init; } } \ No newline at end of file diff --git a/MangaReader.Core/Sources/MangaDex/Metadata/MangaDexMetadataProvider.cs b/MangaReader.Core/Sources/MangaDex/Metadata/MangaDexMetadataProvider.cs index a9e662e..1126c48 100644 --- a/MangaReader.Core/Sources/MangaDex/Metadata/MangaDexMetadataProvider.cs +++ b/MangaReader.Core/Sources/MangaDex/Metadata/MangaDexMetadataProvider.cs @@ -1,4 +1,5 @@ -using MangaReader.Core.Metadata; +using MangaReader.Core.Common; +using MangaReader.Core.Metadata; using MangaReader.Core.Sources.MangaDex.Api; namespace MangaReader.Core.Sources.MangaDex.Metadata; @@ -62,11 +63,11 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe if (attributes.AltTitles == null || attributes.AltTitles.Count == 0) return []; - Dictionary languageIdMap = new() + Dictionary languageIdMap = new() { - { "en", SourceMangaLanguage.English }, - { "ja", SourceMangaLanguage.Japanese }, - { "ja-ro", SourceMangaLanguage.Romanji }, + { "en", Language.English }, + { "ja", Language.Japanese }, + { "ja-ro", Language.Romaji }, }; List sourceMangaTitles = []; @@ -75,7 +76,7 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe { foreach (string alternateTitleKey in alternateTitle.Keys) { - if (languageIdMap.TryGetValue(alternateTitleKey, out SourceMangaLanguage language) == false) + if (languageIdMap.TryGetValue(alternateTitleKey, out Language language) == false) continue; SourceMangaTitle sourceMangaTitle = new() diff --git a/MangaReader.Core/Sources/MangaDex/Search/MangaDexSearchProvider.cs b/MangaReader.Core/Sources/MangaDex/Search/MangaDexSearchProvider.cs index ee808a2..34e59f3 100644 --- a/MangaReader.Core/Sources/MangaDex/Search/MangaDexSearchProvider.cs +++ b/MangaReader.Core/Sources/MangaDex/Search/MangaDexSearchProvider.cs @@ -45,7 +45,7 @@ public partial class MangaDexSearchProvider(IMangaDexClient mangaDexClient) : IM if (thing.Count > 0) { - Guid[] mangaGuids = thing.Select(x => x.Key).ToArray(); + Guid[] mangaGuids = [.. thing.Select(x => x.Key)]; var reults = await GetCoverArtFileNamesAsync(mangaGuids, cancellationToken); //var reults = await mangaDexClient.GetCoverArtAsync(mangaGuids, cancellationToken); } diff --git a/MangaReader.Core/Sources/MangaNato/Metadata/MangaNatoWebCrawler.cs b/MangaReader.Core/Sources/MangaNato/Metadata/MangaNatoWebCrawler.cs index b3b18b0..e9ed1bd 100644 --- a/MangaReader.Core/Sources/MangaNato/Metadata/MangaNatoWebCrawler.cs +++ b/MangaReader.Core/Sources/MangaNato/Metadata/MangaNatoWebCrawler.cs @@ -1,4 +1,5 @@ using HtmlAgilityPack; +using MangaReader.Core.Common; using MangaReader.Core.Metadata; using System.Text; using System.Web; @@ -46,7 +47,7 @@ public class MangaNatoWebCrawler : MangaWebCrawler SourceMangaTitle sourceMangaTitle = new() { Title = title, - Language = SourceMangaLanguage.Unknown + Language = Language.Unknown }; sourceMangaTitles.Add(sourceMangaTitle); diff --git a/MangaReader.Core/Sources/NatoManga/Api/NatoMangaClient.cs b/MangaReader.Core/Sources/NatoManga/Api/NatoMangaClient.cs index a0a3d38..56c7ebc 100644 --- a/MangaReader.Core/Sources/NatoManga/Api/NatoMangaClient.cs +++ b/MangaReader.Core/Sources/NatoManga/Api/NatoMangaClient.cs @@ -23,12 +23,15 @@ public partial class NatoMangaClient(IHttpService httpService) : INatoMangaClien { string url = GetSearchUrl(searchWord); - string response = await httpService.GetStringAsync(url, cancellationToken); + Dictionary requestHeader = []; + requestHeader.Add("Referer", "https://www.natomanga.com/"); + + string response = await httpService.GetStringAsync(url, requestHeader, cancellationToken); return JsonSerializer.Deserialize(response, _jsonSerializerOptions) ?? []; } - protected string GetSearchUrl(string searchWord) + protected static string GetSearchUrl(string searchWord) { string formattedSeachWord = GetFormattedSearchWord(searchWord); diff --git a/MangaReader.Tests/Pipeline/MangaPipelineTests.cs b/MangaReader.Tests/Pipeline/MangaPipelineTests.cs index 79c345f..c1e67e2 100644 --- a/MangaReader.Tests/Pipeline/MangaPipelineTests.cs +++ b/MangaReader.Tests/Pipeline/MangaPipelineTests.cs @@ -1,7 +1,9 @@ -using MangaReader.Core.Data; +using MangaReader.Core.Common; +using MangaReader.Core.Data; using MangaReader.Core.Metadata; using MangaReader.Core.Pipeline; using MangaReader.Tests.Utilities; +using Shouldly; namespace MangaReader.Tests.Pipeline; @@ -21,7 +23,7 @@ public class MangaPipelineTests(TestDbContextFactory factory) : IClassFixture + WinExe net9.0-windows10.0.19041.0 @@ -51,6 +51,8 @@ + + diff --git a/MangaReader.WinUI/ViewModels/SearchViewModel.cs b/MangaReader.WinUI/ViewModels/SearchViewModel.cs index d08c58b..1c7e296 100644 --- a/MangaReader.WinUI/ViewModels/SearchViewModel.cs +++ b/MangaReader.WinUI/ViewModels/SearchViewModel.cs @@ -1,7 +1,15 @@ -using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using MangaReader.Core.Search; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml.Media.Imaging; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.IO; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using System.Windows.Input; @@ -10,6 +18,8 @@ namespace MangaReader.WinUI.ViewModels; public partial class SearchViewModel(IMangaSearchCoordinator searchCoordinator) : ViewModelBase { + private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + private CancellationTokenSource? _cancellationTokenSource; private string? _keyword; @@ -40,6 +50,20 @@ public partial class SearchViewModel(IMangaSearchCoordinator searchCoordinator) } } + private ObservableCollection _searchResults2 = []; + + public ObservableCollection SearchResults2 + { + get + { + return _searchResults2; + } + set + { + SetProperty(ref _searchResults2, value); + } + } + public ICommand SearchCommand => new AsyncRelayCommand(SearchAsync); public async Task SearchAsync() @@ -53,15 +77,114 @@ public partial class SearchViewModel(IMangaSearchCoordinator searchCoordinator) Dictionary result = await searchCoordinator.SearchAsync(Keyword, _cancellationTokenSource.Token); List searchResults = []; - + List mangaSearchResults = []; + foreach (var item in result) { foreach (MangaSearchResult searchResult in item.Value) { + //searchResults.Add(searchResult); + + ObservableMangaSearchResult mangaSearchResult = new() + { + Title = searchResult.Title, + Thumbnail = searchResult.Thumbnail, + Description = searchResult.Description, + Genres = searchResult.Genres + }; + + Task.Run(() => mangaSearchResult.LoadThumbnailAsync(_dispatcherQueue)); // or defer this if you want lazy loading + searchResults.Add(searchResult); + mangaSearchResults.Add(mangaSearchResult); } } SearchResults = new(searchResults); + SearchResults2 = new(mangaSearchResults); + } + + public static async Task LoadWebpAsBitmapImageAsync(string? url) + { + if (string.IsNullOrWhiteSpace(url)) + return null; + + using var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0"); + using var webpStream = await httpClient.GetStreamAsync(url); + + using var image = await Image.LoadAsync(webpStream); // from SixLabors.ImageSharp + using var ms = new MemoryStream(); + //await image.SaveAsPngAsync(ms); // Convert to PNG in memory + await image.SaveAsJpegAsync(ms); + ms.Position = 0; + + var bitmap = new BitmapImage(); + await bitmap.SetSourceAsync(ms.AsRandomAccessStream()); + + return bitmap; + } +} + +public partial class ObservableMangaSearchResult : ObservableObject +{ + public string? Title { get; init; } + public string? Description { get; init; } + public string? Thumbnail { get; init; } + public string[] Genres { get; init; } = []; + + private BitmapImage? _thumbnailBitmap; + + public BitmapImage? ThumbnailBitmap + { + get + { + return _thumbnailBitmap; + } + set + { + SetProperty(ref _thumbnailBitmap, value); + } + } + + public async Task LoadThumbnailAsync(DispatcherQueue dispatchQueue) + { + if (string.IsNullOrWhiteSpace(Thumbnail)) + return; + + try + { + using var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0"); + + using var stream = await httpClient.GetStreamAsync(Thumbnail); + using var image = await Image.LoadAsync(stream); // Important: use a pixel type + + using var ms = new MemoryStream(); + await image.SaveAsJpegAsync(ms); // or SaveAsPngAsync + ms.Position = 0; + + TaskCompletionSource taskCompletionSource = new(); + + dispatchQueue.TryEnqueue(async () => { + var bitmap = new BitmapImage(); + await bitmap.SetSourceAsync(ms.AsRandomAccessStream()); + + ThumbnailBitmap = bitmap; + + taskCompletionSource.SetResult(); + }); + + taskCompletionSource.Task.GetAwaiter().GetResult(); + + //var bitmap = new BitmapImage(); + //await bitmap.SetSourceAsync(ms.AsRandomAccessStream()); + + //ThumbnailBitmap = bitmap; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[Thumbnail Load Failed] {ex.Message}"); + } } } \ No newline at end of file diff --git a/MangaReader.WinUI/Views/SearchView.xaml b/MangaReader.WinUI/Views/SearchView.xaml index ef4c833..e44ae8c 100644 --- a/MangaReader.WinUI/Views/SearchView.xaml +++ b/MangaReader.WinUI/Views/SearchView.xaml @@ -16,16 +16,16 @@ mc:Ignorable="d"> - - - + + + - + @@ -35,8 +35,8 @@ - - + + @@ -53,7 +53,7 @@ - + @@ -74,7 +74,7 @@ - +