diff options
| author | Jake Mannens <jake@asger.xyz> | 2026-03-17 03:04:36 +1100 |
|---|---|---|
| committer | Jake Mannens <jake@asger.xyz> | 2026-06-07 12:32:37 +1000 |
| commit | c51ff4e755f009ca0bc8e935a92c04e583c4ee8a (patch) | |
| tree | 0a9a311c5404a96495df1047e613dc3aea3d0f15 /Services/FeedService.cs | |
Initial commit
Diffstat (limited to 'Services/FeedService.cs')
| -rw-r--r-- | Services/FeedService.cs | 212 |
1 files changed, 212 insertions, 0 deletions
diff --git a/Services/FeedService.cs b/Services/FeedService.cs new file mode 100644 index 0000000..3744e73 --- /dev/null +++ b/Services/FeedService.cs @@ -0,0 +1,212 @@ +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<HBContext> dbFactory; + + public FeedService(IDbContextFactory<HBContext> 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<Media>(); + + using var db = dbFactory.CreateDbContext(); + + IQueryable<Media> 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<Media> Search(IQueryable<Media> 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<int> TagsThatImply(HBContext db, Guid tagId) => + db.Database.SqlQueryRaw<int>(""" + 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); +} |
