using JSMR.Application.Common; using JSMR.Application.Common.Search; using JSMR.Application.VoiceWorks.Queries.Search; using JSMR.Domain.Entities; using JSMR.Infrastructure.Common.Queries; using Microsoft.EntityFrameworkCore; using System.Linq.Expressions; using CircleStatus = JSMR.Application.VoiceWorks.Queries.Search.CircleStatus; namespace JSMR.Infrastructure.Data.Repositories.VoiceWorks; public class VoiceWorkQuery { public required VoiceWork VoiceWork { get; init; } public EnglishVoiceWork? EnglishVoiceWork { get; init; } public required Circle Circle { get; init; } //public VoiceWorkLocalization? VoiceWorkLocalization { get; init; } //public VoiceWorkSearch? VoiceWorkSearch { get; init; } } public class VoiceWorkSearchProvider(AppDbContext context, IVoiceWorkFullTextSearch fullTextSearch) : SearchProvider, IVoiceWorkSearchProvider { protected override IQueryable GetBaseQuery() { return from voiceWork in context.VoiceWorks.AsNoTracking() join englishVoiceWork in context.EnglishVoiceWorks.AsNoTracking() on voiceWork.VoiceWorkId equals englishVoiceWork.VoiceWorkId into ps from englishVoiceWork in ps.DefaultIfEmpty() join circle in context.Circles.AsNoTracking() on voiceWork.CircleId equals circle.CircleId into cs from circle in cs.DefaultIfEmpty() //join voiceWorkLocalization in context.VoiceWorkLocalizations on voiceWork.VoiceWorkId equals voiceWorkLocalization.VoiceWorkId into vwl //from voiceWorkLocalization in vwl.DefaultIfEmpty() //join voiceWorkSearch in context.VoiceWorkSearches on voiceWork.VoiceWorkId equals voiceWorkSearch.VoiceWorkId into vws //from voiceWorkSearch in vws.DefaultIfEmpty() select new VoiceWorkQuery { VoiceWork = voiceWork, EnglishVoiceWork = englishVoiceWork, Circle = circle }; } protected override IQueryable ApplyFilters(IQueryable query, VoiceWorkSearchCriteria criteria) { IQueryable filteredQuery = query; filteredQuery = ApplyKeywordsFilter(filteredQuery, criteria); filteredQuery = ApplyCircleStatusFilter(filteredQuery, criteria); filteredQuery = ApplyTagStatusFilter(filteredQuery, criteria); filteredQuery = ApplyCreatorStatusFilter(filteredQuery, criteria); filteredQuery = ApplyTagIdsFilter(filteredQuery, criteria); filteredQuery = ApplyCreatorIdsFilter(filteredQuery, criteria); switch (criteria.SaleStatus) { case SaleStatus.Available: filteredQuery = filteredQuery.Where(x => x.VoiceWork.SalesDate != null); break; case SaleStatus.Upcoming: filteredQuery = filteredQuery.Where(x => x.VoiceWork.ExpectedDate != null); break; } if (criteria.ReleaseDateStart is not null) filteredQuery = filteredQuery.Where(x => x.VoiceWork.SalesDate >= criteria.ReleaseDateStart.Value); if (criteria.ReleaseDateEnd is not null) filteredQuery = filteredQuery.Where(x => x.VoiceWork.SalesDate <= criteria.ReleaseDateEnd.Value); if (criteria.AgeRatings.Length > 0) filteredQuery = filteredQuery.Where(x => criteria.AgeRatings.Contains((AgeRating)x.VoiceWork.Rating)); if (criteria.SupportedLanguages.Length > 0) filteredQuery = filteredQuery.Where(x => criteria.SupportedLanguages.Contains((Language)x.VoiceWork.SubtitleLanguage)); if (criteria.AIGenerationOptions.Length > 0) filteredQuery = filteredQuery.Where(x => criteria.AIGenerationOptions.Contains((AIGeneration)x.VoiceWork.AIGeneration)); if (criteria.ShowFavoriteVoiceWorks) filteredQuery = filteredQuery.Where(x => x.VoiceWork.Favorite); if (criteria.ShowInvalidVoiceWorks) filteredQuery = filteredQuery.Where(x => x.VoiceWork.IsValid != true); if (criteria.MinDownloads is not null) filteredQuery = filteredQuery.Where(x => x.VoiceWork.Downloads >= criteria.MinDownloads.Value); if (criteria.MaxDownloads is not null) filteredQuery = filteredQuery.Where(x => x.VoiceWork.Downloads <= criteria.MaxDownloads.Value); return filteredQuery; } private IQueryable ApplyKeywordsFilter(IQueryable query, VoiceWorkSearchCriteria criteria) { if (string.IsNullOrWhiteSpace(criteria.Keywords)) return query; var voiceWorkIds = fullTextSearch.MatchingIds(context, criteria.Keywords); return query.Where(x => voiceWorkIds.Contains(x.VoiceWork.VoiceWorkId)); } private IQueryable ApplyCircleStatusFilter(IQueryable query, VoiceWorkSearchCriteria criteria) { if (criteria.CircleStatus is null) return query; switch (criteria.CircleStatus) { case CircleStatus.NotBlacklisted: query = query.Where(q => !context.Circles.Any(c => c.CircleId == q.VoiceWork.CircleId && c.Blacklisted)); break; case CircleStatus.Blacklisted: query = query.Where(q => context.Circles.Any(c => c.CircleId == q.VoiceWork.CircleId && c.Blacklisted)); break; case CircleStatus.Favorited: query = query.Where(q => context.Circles.Any(c => c.CircleId == q.VoiceWork.CircleId && c.Favorite)); break; } return query; } private IQueryable ApplyTagStatusFilter(IQueryable query, VoiceWorkSearchCriteria criteria) { if (criteria.TagStatus is null) return query; // Handy local predicates that translate to EXISTS subqueries bool HasFav(int voiceWorkId) => context.VoiceWorkTags .Join(context.Tags, vwt => vwt.TagId, t => t.TagId, (vwt, t) => new { vwt, t }) .Any(x => x.vwt.VoiceWorkId == voiceWorkId && x.t.Favorite); bool HasBlk(int voiceWorkId) => context.VoiceWorkTags .Join(context.Tags, vwt => vwt.TagId, t => t.TagId, (vwt, t) => new { vwt, t }) .Any(x => x.vwt.VoiceWorkId == voiceWorkId && x.t.Blacklisted); return criteria.TagStatus switch { TagStatus.NotBlacklisted => query.Where(q => !context.VoiceWorkTags .Join(context.Tags, vwt => vwt.TagId, t => t.TagId, (vwt, t) => new { vwt, t }) .Any(x => x.vwt.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.t.Blacklisted)), TagStatus.Blacklisted => query.Where(q => context.VoiceWorkTags .Join(context.Tags, vwt => vwt.TagId, t => t.TagId, (vwt, t) => new { vwt, t }) .Any(x => x.vwt.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.t.Blacklisted)), TagStatus.FavoriteIncludeBlacklist => query.Where(q => context.VoiceWorkTags .Join(context.Tags, vwt => vwt.TagId, t => t.TagId, (vwt, t) => new { vwt, t }) .Any(x => x.vwt.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.t.Favorite)), TagStatus.FavoriteExcludeBlacklist => query.Where(q => context.VoiceWorkTags .Join(context.Tags, vwt => vwt.TagId, t => t.TagId, (vwt, t) => new { vwt, t }) .Any(x => x.vwt.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.t.Favorite) && !context.VoiceWorkTags .Join(context.Tags, vwt => vwt.TagId, t => t.TagId, (vwt, t) => new { vwt, t }) .Any(x => x.vwt.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.t.Blacklisted) ), _ => query }; } private IQueryable ApplyCreatorStatusFilter(IQueryable query, VoiceWorkSearchCriteria criteria) { if (criteria.CreatorStatus is null) return query; // Handy local predicates that translate to EXISTS subqueries bool HasFav(int voiceWorkId) => context.VoiceWorkCreators .Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c }) .Any(x => x.vwc.VoiceWorkId == voiceWorkId && x.c.Favorite); bool HasBlk(int voiceWorkId) => context.VoiceWorkCreators .Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c }) .Any(x => x.vwc.VoiceWorkId == voiceWorkId && x.c.Blacklisted); return criteria.CreatorStatus switch { CreatorStatus.NotBlacklisted => query.Where(q => !context.VoiceWorkCreators .Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c }) .Any(x => x.vwc.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.c.Blacklisted)), CreatorStatus.Blacklisted => query.Where(q => context.VoiceWorkCreators .Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c }) .Any(x => x.vwc.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.c.Blacklisted)), CreatorStatus.FavoriteIncludeBlacklist => query.Where(q => context.VoiceWorkCreators .Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c }) .Any(x => x.vwc.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.c.Favorite)), CreatorStatus.FavoriteExcludeBlacklist => query.Where(q => context.VoiceWorkCreators .Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c }) .Any(x => x.vwc.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.c.Favorite) && !context.VoiceWorkCreators .Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c }) .Any(x => x.vwc.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.c.Blacklisted) ), _ => query }; } private IQueryable ApplyTagIdsFilter(IQueryable filteredQuery, VoiceWorkSearchCriteria criteria) { if (criteria.TagIds.Length == 0) return filteredQuery; if (criteria.IncludeAllTags == false) { var tagQuery = from voiceWorkTag in context.VoiceWorkTags.AsNoTracking() where criteria.TagIds.Contains(voiceWorkTag.TagId) select new { voiceWorkTag }; var finalTagQuery = tagQuery.Select(x => x.voiceWorkTag.VoiceWorkId); filteredQuery = filteredQuery.Where(x => finalTagQuery.Contains(x.VoiceWork.VoiceWorkId)); } else { foreach (int tagId in criteria.TagIds) { var tagIdQuery = from voiceWorkTag in context.VoiceWorkTags.AsNoTracking() where voiceWorkTag.TagId == tagId select voiceWorkTag.VoiceWorkId; filteredQuery = from query in filteredQuery join voiceWorkId in tagIdQuery on query.VoiceWork.VoiceWorkId equals voiceWorkId select new VoiceWorkQuery { VoiceWork = query.VoiceWork, EnglishVoiceWork = query.EnglishVoiceWork, Circle = query.Circle }; } } return filteredQuery; } private IQueryable ApplyCreatorIdsFilter(IQueryable filteredQuery, VoiceWorkSearchCriteria criteria) { if (criteria.CreatorIds.Length == 0) return filteredQuery; if (criteria.IncludeAllCreators == false) { var creatorQuery = from voiceWorkCreator in context.VoiceWorkCreators.AsNoTracking() where criteria.CreatorIds.Contains(voiceWorkCreator.CreatorId) select new { voiceWorkCreator }; var finalCreatorQuery = creatorQuery.Select(x => x.voiceWorkCreator.VoiceWorkId); filteredQuery = filteredQuery.Where(x => finalCreatorQuery.Contains(x.VoiceWork.VoiceWorkId)); } else { foreach (int creatorId in criteria.CreatorIds) { var creatorIdQuery = from voiceWorkCreator in context.VoiceWorkCreators.AsNoTracking() where voiceWorkCreator.CreatorId == creatorId select voiceWorkCreator.VoiceWorkId; filteredQuery = from query in filteredQuery join voiceWorkId in creatorIdQuery on query.VoiceWork.VoiceWorkId equals voiceWorkId select new VoiceWorkQuery { VoiceWork = query.VoiceWork, EnglishVoiceWork = query.EnglishVoiceWork, Circle = query.Circle }; } } return filteredQuery; } protected override IEnumerable<(Expression> Selector, SortDirection Dir)> GetDefaultSortChain() { yield return (x => x.VoiceWork.ProductId, SortDirection.Ascending); } protected override Expression> GetSortExpression(VoiceWorkSortField field) => field switch { VoiceWorkSortField.ReleaseDate => x => x.VoiceWork.SalesDate ?? x.VoiceWork.ExpectedDate ?? DateTime.MinValue, VoiceWorkSortField.Downloads => x => x.VoiceWork.Downloads ?? 0, VoiceWorkSortField.WishlistCount => x => x.VoiceWork.WishlistCount ?? 0, VoiceWorkSortField.StarRating => x => x.VoiceWork.StarRating ?? 0, _ => x => x.VoiceWork.ProductId }; protected override IQueryable GetSelectQuery(IOrderedQueryable query) { var result = from q in query let voiceWork = q.VoiceWork let englishVoiceWork = q.EnglishVoiceWork let circle = q.Circle let productLinkPage = voiceWork.SalesDate.HasValue ? "work" : "announce" select new VoiceWorkSearchResult() { VoiceWorkId = voiceWork.VoiceWorkId, ProductId = voiceWork.ProductId, OriginalProductId = voiceWork.OriginalProductId, ProductName = englishVoiceWork != null ? englishVoiceWork.ProductName : voiceWork.ProductName, ProductUrl = "http://www.dlsite.com/maniax/" + productLinkPage + "/=/product_id/" + voiceWork.ProductId + ".html", Description = englishVoiceWork != null ? englishVoiceWork.Description : voiceWork.Description, Favorite = voiceWork.Favorite, HasImage = voiceWork.HasImage, Maker = circle.Name, MakerId = circle.MakerId, ExpectedDate = voiceWork.ExpectedDate, SalesDate = voiceWork.SalesDate, PlannedReleaseDate = voiceWork.PlannedReleaseDate, Downloads = voiceWork.Downloads, WishlistCount = voiceWork.WishlistCount, Status = voiceWork.Status, SubtitleLanguage = voiceWork.SubtitleLanguage, HasTrial = voiceWork.HasTrial, HasChobit = voiceWork.HasChobit, IsValid = voiceWork.IsValid }; return result; } protected override async Task PostLoadAsync(IList items, CancellationToken cancellationToken) { if (items.Count == 0) return; int[] voiceWorkIds = [.. items.Select(i => i.VoiceWorkId)]; Dictionary tagsByVw = await GetTagsAsync(voiceWorkIds, cancellationToken); Dictionary creatorsByVw = await GetCreatorsAsync(voiceWorkIds, cancellationToken); foreach (VoiceWorkSearchResult item in items) { if (tagsByVw.TryGetValue(item.VoiceWorkId, out VoiceWorkTagItem[]? tags)) item.Tags = tags; if (creatorsByVw.TryGetValue(item.VoiceWorkId, out VoiceWorkCreatorItem[]? creators)) item.Creators = creators; } } private async Task> GetTagsAsync(int[] voiceWorkIds, CancellationToken cancellationToken) { var tagRows = await ( from voiceWorkTag in context.VoiceWorkTags.AsNoTracking() join tag in context.Tags.AsNoTracking() on voiceWorkTag.TagId equals tag.TagId where voiceWorkIds.Contains(voiceWorkTag.VoiceWorkId) orderby voiceWorkTag.VoiceWorkId, voiceWorkTag.Position select new { voiceWorkTag.VoiceWorkId, voiceWorkTag.TagId, tag.Name } ).ToListAsync(cancellationToken); return tagRows .GroupBy(r => r.VoiceWorkId) .ToDictionary( g => g.Key, g => g.Select(r => new VoiceWorkTagItem { TagId = r.TagId, Name = r.Name }).ToArray() ); } private async Task> GetCreatorsAsync(int[] voiceWorkIds, CancellationToken cancellationToken) { var creatorRows = await ( from voiceWorkCreator in context.VoiceWorkCreators.AsNoTracking() join creator in context.Creators.AsNoTracking() on voiceWorkCreator.CreatorId equals creator.CreatorId where voiceWorkIds.Contains(voiceWorkCreator.VoiceWorkId) orderby voiceWorkCreator.VoiceWorkId, voiceWorkCreator.Position select new { voiceWorkCreator.VoiceWorkId, creator.CreatorId, creator.Name } ).ToListAsync(cancellationToken); return creatorRows .GroupBy(r => r.VoiceWorkId) .ToDictionary( g => g.Key, g => g.Select(r => new VoiceWorkCreatorItem { CreatorId = r.CreatorId, Name = r.Name }).ToArray() ); } }