summaryrefslogtreecommitdiff
path: root/Services/FeedService.cs
diff options
context:
space:
mode:
Diffstat (limited to 'Services/FeedService.cs')
-rw-r--r--Services/FeedService.cs212
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);
+}