using MangaReader.Core.Search; using MangaReader.Core.Sources.MangaDex.Api; using System.Text.RegularExpressions; namespace MangaReader.Core.Sources.MangaDex.Search; public partial class MangaDexSearchProvider(IMangaDexClient mangaDexClient) : IMangaSearchProvider { [GeneratedRegex(@"[^a-z0-9\s-]")] private static partial Regex InvalidSlugCharactersRegex(); [GeneratedRegex(@"\s+")] private static partial Regex WhitespaceRegex(); public string SourceId => "MangaDex"; public async Task SearchAsync(string keyword, CancellationToken cancellationToken) { MangaDexResponse? response = await mangaDexClient.SearchMangaByTitleAsync(keyword, cancellationToken); if (response == null || (response is not MangaDexCollectionResponse collectionResponse)) return []; MangaEntity[] mangaEntities = [.. collectionResponse.Data.Where(entity => entity is MangaEntity).Cast()]; if (mangaEntities.Length == 0) return []; Dictionary> mangaCoverArtMap = await GetCoverArtFileNamesAsync(mangaEntities, cancellationToken); List mangaSearchResults = []; Dictionary thing = []; foreach (MangaEntity mangaEntity in mangaEntities) { 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)]; var reults = await GetCoverArtFileNamesAsync(mangaGuids, cancellationToken); //var reults = await mangaDexClient.GetCoverArtAsync(mangaGuids, cancellationToken); } return [.. mangaSearchResults]; } private static MangaSearchResult? GetMangaSearchResult(MangaEntity mangaEntity, CoverArtEntity[] coverArtEntites) { MangaAttributes? mangaAttributes = mangaEntity.Attributes; if (mangaAttributes == null) return null; string title = GetTitle(mangaAttributes); string slug = GenerateSlug(title); MangaSearchResult mangaSearchResult = new() { Title = title, Description = GetDescription(mangaAttributes), Genres = GetGenres(mangaAttributes), Url = $"https://mangadex.org/title/{mangaEntity.Id}/{slug}", 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; if (attributes.Description.Count > 0) return attributes.Description.ElementAt(0).Value; return string.Empty; } private static string[] GetGenres(MangaAttributes attributes) { if (attributes.Tags == null || attributes.Tags.Count == 0) return []; List tags = []; foreach (TagEntity tagEntity in attributes.Tags) { if (tagEntity.Attributes == null) continue; if (tagEntity.Attributes.Name == null || tagEntity.Attributes.Name.Count == 0) continue; tags.Add(tagEntity.Attributes.Name.FirstOrDefault().Value); } return [.. tags]; } public static string GenerateSlug(string title) { // title.ToLowerInvariant().Normalize(NormalizationForm.FormD); title = title.ToLowerInvariant(); //title = InvalidSlugCharactersRegex().Replace(title, ""); // remove invalid chars title = InvalidSlugCharactersRegex().Replace(title, "-"); // replace invalid chars with dash title = WhitespaceRegex().Replace(title, "-"); // replace spaces with dash return title.Trim('-'); } 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); if (coverArtEntity == null || string.IsNullOrWhiteSpace(coverArtEntity.Attributes?.FileName)) return null; return coverArtEntity.Attributes?.FileName; } private static string? GetCoverArtFileNameFromCoverArtEntities(CoverArtEntity[] coverArtEntites) { return coverArtEntites.Where(coverArtEntity => string.IsNullOrWhiteSpace(coverArtEntity.Attributes?.FileName) == false).FirstOrDefault()?.Attributes!.FileName; } private async Task>> GetCoverArtFileNamesAsync(MangaEntity[] mangaEntities, CancellationToken cancellationToken) { Guid[] mangaGuids = [.. mangaEntities.Select(entity => entity.Id)]; return await GetCoverArtFileNamesAsync(mangaGuids, cancellationToken); } private async Task>> GetCoverArtFileNamesAsync(Guid[] mangaGuids, CancellationToken cancellationToken) { Dictionary> 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()]; 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; } }