using HyperBooru.ApiModels; using Microsoft.EntityFrameworkCore; namespace HyperBooru.Services; public interface IFeedService { public Media[] LoadChunk( bool selectIngest, bool includeNsfw, Media? key = null, int count = 50, SortOrder sortOrder = SortOrder.ObjectId); public Media[] LoadChunk( bool selectIngest, bool includeNsfw, string query, Media? key = null, int count = 50, SortOrder sortOrder = SortOrder.ObjectId); public Media[] LoadChunk( bool selectIngest, bool includeNsfw, Guid tagId, Media? key = null, int count = 50, SortOrder sortOrder = SortOrder.ObjectId); public Media[] LoadChunk(FeedRequest feedRequest); } public class FeedService : IFeedService { private IDbContextFactory dbFactory; public FeedService(IDbContextFactory dbFactory) => this.dbFactory = dbFactory; public Media[] LoadChunk( bool selectIngest, bool includeNsfw, Media? continuationToken, int count, SortOrder sortOrder) => LoadChunkInternal( selectIngest, includeNsfw, null, null, continuationToken?.Guid, count, sortOrder); public Media[] LoadChunk( bool selectIngest, bool includeNsfw, string query, Media? continuationToken, int count, SortOrder sortOrder) => LoadChunkInternal( selectIngest, includeNsfw, query, null, continuationToken?.Guid, count, sortOrder); public Media[] LoadChunk( bool selectIngest, bool includeNsfw, Guid tagId, Media? continuationToken, int count, SortOrder sortOrder) => LoadChunkInternal( selectIngest, includeNsfw, null, tagId, continuationToken?.Guid, count, sortOrder); public Media[] LoadChunk(FeedRequest feedRequest) { switch(feedRequest) { case FeedSearchRequest searchRequest: return LoadChunkInternal( selectIngest: searchRequest.SelectIngest, includeNsfw: searchRequest.IncludeNsfw, query: searchRequest.Query, tagId: null, continuationToken: searchRequest.ContinuationToken, count: searchRequest.Count, sortOrder: searchRequest.SortOrder); case FeedTagRequest tagRequest: return LoadChunkInternal( selectIngest: tagRequest.SelectIngest, includeNsfw: tagRequest.IncludeNsfw, query: null, tagId: tagRequest.TagId, continuationToken: tagRequest.ContinuationToken, count: tagRequest.Count, sortOrder: tagRequest.SortOrder); default: return LoadChunkInternal( selectIngest: feedRequest.SelectIngest, includeNsfw: feedRequest.IncludeNsfw, query: null, tagId: null, continuationToken: feedRequest.ContinuationToken, count: feedRequest.Count, sortOrder: feedRequest.SortOrder); } } private Media[] LoadChunkInternal( bool selectIngest, bool includeNsfw, string? query, Guid? tagId, Guid? continuationToken, int count, SortOrder sortOrder) { if(selectIngest && !includeNsfw) return Array.Empty(); using var db = dbFactory.CreateDbContext(); IQueryable media = db.Media .AsSingleQuery() .AsNoTracking() .Include(m => m.Tags) .Include(m => m.CurrentUploadedFile); if(!includeNsfw) media = media .Where(m => !TagsThatImply(db, HBContext.NsfwTag) .Intersect(m.Tags.Select(t => t.TagDefinitionId)) .Any()); if(selectIngest) { media = media .Where(m => m.Tags .Select(t => t.TagDefinitionId) .Contains((int) HBObjectId.IngestTag)); } else { media = media .Where(m => !m.Tags .Select(t => t.TagDefinitionId) .Contains((int) HBObjectId.IngestTag)); } if(query is not null) { media = Search(media, query); } else if(tagId is not null) { media = media .Where(m => TagsThatImply(db, (Guid) tagId) .Intersect(m.Tags.Select(t => t.TagDefinitionId)) .Any()); } if(continuationToken is not null) media = media .Where(m => m.ObjectId > db.Media.First(m => m.Guid == continuationToken).ObjectId); switch(sortOrder) { case SortOrder.ObjectId: media = media.OrderBy(m => m.ObjectId); break; case SortOrder.LastWriteTime: media = media.OrderBy(m => m.CurrentUploadedFile!.LastWriteTime); break; case SortOrder.Random: media = media.OrderBy(m => EF.Functions.Random()); break; } return media .Take(count) .ToArray(); } private static IQueryable Search(IQueryable media, string query) { // TODO: search implicit tags as well query = query.ToLower().Trim(); return media .Where(m => (m.ShortDescription != null && m.ShortDescription.ToLower().Contains(query)) || (m.LongDescription != null && m.LongDescription.ToLower().Contains(query)) || (m.UploadedFiles.Any(uf => uf.Filename != null && uf.Filename.ToLower().Contains(query))) || (m.OcrData != null && m.OcrData.SearchableText.ToLower().Contains(query)) || (m.Tags.Any(t => t.TagDefinition.Name.ToLower().Contains(query)))); } private static IQueryable TagsThatImply(HBContext db, Guid tagId) => db.Database.SqlQueryRaw(""" WITH RECURSIVE basetag AS ( SELECT "ObjectId" FROM "Objects" WHERE "Guid" = {0} ), impliedtags AS ( SELECT "TagDefinitionObjectId" FROM "TagDefinitionTagDefinition" INNER JOIN basetag ON "ImplicitTagsObjectId" = basetag."ObjectId" UNION SELECT "TagDefinitionTagDefinition"."TagDefinitionObjectId" FROM "TagDefinitionTagDefinition" INNER JOIN impliedtags ON impliedtags."TagDefinitionObjectId" = "TagDefinitionTagDefinition"."ImplicitTagsObjectId" ) SELECT DISTINCT "TagDefinitionObjectId" AS "Value" FROM impliedtags UNION SELECT "ObjectId" AS "Value" FROM basetag """, tagId); }