summaryrefslogtreecommitdiff
path: root/Services
diff options
context:
space:
mode:
authorJake Mannens <jake@asger.xyz>2026-03-25 01:57:19 +1100
committerJake Mannens <jake@asger.xyz>2026-03-25 01:57:46 +1100
commit6c06dfc4f83f30292e65c08a3cb0c48401d4bfa7 (patch)
tree511f88873fa6173637115a38c31ec5f8018e108e /Services
parentc751709b1b4fe6f16fd84647e8e071455e7b78d6 (diff)
v0.2av0.2a
Diffstat (limited to 'Services')
-rw-r--r--Services/ConfigService.cs4
-rw-r--r--Services/FeedService.cs155
-rw-r--r--Services/MediaService.cs75
-rw-r--r--Services/OcrService.cs18
-rw-r--r--Services/SearchService.cs117
5 files changed, 210 insertions, 159 deletions
diff --git a/Services/ConfigService.cs b/Services/ConfigService.cs
index b42b80c..8460fd0 100644
--- a/Services/ConfigService.cs
+++ b/Services/ConfigService.cs
@@ -5,6 +5,7 @@ public interface IConfigService {
public string DbConnectionString { get; }
public string MediaBasePath { get; }
public string ThumbnailBasePath { get; }
+ public bool EnableOcr { get; }
}
public class ConfigService : IConfigService {
@@ -46,6 +47,9 @@ public class ConfigService : IConfigService {
public string ThumbnailBasePath =>
Path.Join(DataPath, "thumb");
+ public bool EnableOcr =>
+ bool.TryParse(config["DisableOcr"], out bool x) ? !x : true;
+
public ConfigService(IConfiguration config) {
this.config = config;
InitDirectoryStructure();
diff --git a/Services/FeedService.cs b/Services/FeedService.cs
new file mode 100644
index 0000000..864a751
--- /dev/null
+++ b/Services/FeedService.cs
@@ -0,0 +1,155 @@
+using Microsoft.EntityFrameworkCore;
+
+namespace HyperBooru.Services;
+
+public interface IFeedService {
+ public Media[] LoadChunk(
+ bool selectIngest,
+ bool includeNsfw,
+ Media? key = null,
+ int count = 50);
+
+ public Media[] LoadChunk(
+ bool selectIngest,
+ bool includeNsfw,
+ string query,
+ Media? key = null,
+ int count = 50);
+
+ public Media[] LoadChunk(
+ bool selectIngest,
+ bool includeNsfw,
+ Guid tagId,
+ Media? key = null,
+ int count = 50);
+}
+
+public class FeedService : IFeedService {
+ private IDbContextFactory<HBContext> dbFactory;
+
+ public FeedService(IDbContextFactory<HBContext> dbFactory) =>
+ this.dbFactory = dbFactory;
+
+ public Media[] LoadChunk(
+ bool selectIngest,
+ bool includeNsfw,
+ Media? key,
+ int count) => LoadChunkInternal(selectIngest, includeNsfw, null, null, key, count);
+
+ public Media[] LoadChunk(
+ bool selectIngest,
+ bool includeNsfw,
+ string query,
+ Media? key,
+ int count) => LoadChunkInternal(selectIngest, includeNsfw, query, null, key, count);
+
+ public Media[] LoadChunk(
+ bool selectIngest,
+ bool includeNsfw,
+ Guid tagId,
+ Media? key,
+ int count) => LoadChunkInternal(selectIngest, includeNsfw, null, tagId, key, count);
+
+ private Media[] LoadChunkInternal(
+ bool selectIngest,
+ bool includeNsfw,
+ string? query,
+ Guid? tagId,
+ Media? key,
+ int count) {
+
+ 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(key is not null)
+ media = media.Where(m => m.ObjectId > key.ObjectId);
+
+ return media
+ .OrderBy(m => m.ObjectId)
+ .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);
+}
diff --git a/Services/MediaService.cs b/Services/MediaService.cs
index 104d0db..a5803f9 100644
--- a/Services/MediaService.cs
+++ b/Services/MediaService.cs
@@ -31,8 +31,10 @@ public interface IMediaService {
public void DeleteThumbnails(Media media);
public Stream GetThumbnail(Guid media, int? width, int? height);
public Stream GetThumbnail(Media media, int? width, int? height);
+ public string GetPath(Guid media);
+ public string GetPath(Guid media, int? width, int? height);
public string GetPath(Media media);
- public string GetPath(Media media, int width, int height);
+ public string GetPath(Media media, int? width, int? height);
}
@@ -259,37 +261,29 @@ public class MediaService : IMediaService {
public void DeleteThumbnails(Media media) =>
DeleteThumbnails(media.Guid);
- public Stream GetThumbnail(Guid media, int? width, int? height) {
- using var db = dbFactory.CreateDbContext();
+ public Stream GetThumbnail(Guid mediaId, int? width, int? height) {
+ if(width is null && height is null)
+ throw new ThumbnailException(
+ "Both width and height cannot be null!",
+ mediaId);
- var m = db.Media
- .Include(m => m.CurrentUploadedFile)
- .First(m => m.Guid == media);
- if(m is null)
- throw new ObjectNotFoundException(media);
+ var thumbPath = GetPath(mediaId, width, height);
- if(m.CurrentUploadedFile.MimeType.Split("/")[0] != "image")
- throw new ThumbnailException("Media object not an image", m);
+ if(File.Exists(thumbPath))
+ return System.IO.File.OpenRead(thumbPath);
- using var image = new MagickImage(GetPath(m));
+ if(!File.Exists(GetPath(mediaId)))
+ throw new ObjectNotFoundException(mediaId);
- if(width is null && height is null)
- throw new ThumbnailException("Both width and height cannot be null!", m);
+ using var image = new MagickImage(GetPath(mediaId));
if(width > image.Width || height > image.Height)
- throw new ThumbnailException("Requested thumbnail size is larger than original media", m);
-
- #pragma warning disable CS8629
- int w = (int) (width is not null ? width : image.Width * height / image.Height);
- int h = (int) (height is not null ? height : image.Height * width / image.Width);
- #pragma warning restore CS8629
-
- var thumbPath = GetPath(m, w, h);
+ throw new ThumbnailException(
+ "Requested thumbnail size is larger than original media",
+ mediaId);
- if(!File.Exists(thumbPath)) {
- image.Resize((uint) w, (uint) h);
- image.Write(thumbPath);
- }
+ image.Thumbnail((uint) (width ?? -1), (uint) (height ?? -1));
+ image.Write(thumbPath, MagickFormat.Jpeg);
return System.IO.File.OpenRead(thumbPath);
}
@@ -297,29 +291,40 @@ public class MediaService : IMediaService {
public Stream GetThumbnail(Media media, int? width, int? height) =>
GetThumbnail(media.Guid, width, height);
- public string GetPath(Media media) {
+ public string GetPath(Guid mediaId) {
var fileInfo = new FileInfo(
Path.Join(
config.MediaBasePath,
- media.Guid.ToString().Substring(0, 2),
- media.Guid.ToString().Substring(2, 2),
- media.Guid.ToString()));
+ mediaId.ToString().Substring(0, 2),
+ mediaId.ToString().Substring(2, 2),
+ mediaId.ToString()));
- Directory.CreateDirectory(fileInfo.Directory.FullName);
+ Directory.CreateDirectory(fileInfo.Directory!.FullName);
return fileInfo.FullName;
}
- public string GetPath(Media media, int width, int height) {
+ public string GetPath(Guid mediaId, int? width, int? height) {
+ if(width is null && height is null)
+ throw new ThumbnailException(
+ "Both width and height cannot be null!",
+ mediaId);
+
var fileInfo = new FileInfo(Path.Join(
config.ThumbnailBasePath,
- media.Guid.ToString().Substring(0, 2),
- media.Guid.ToString().Substring(2, 2),
- $"{media.Guid.ToString()}-{width}-{height}"));
+ mediaId.ToString().Substring(0, 2),
+ mediaId.ToString().Substring(2, 2),
+ $"{mediaId.ToString()}-{(width ?? 0)}-{(height ?? 0)}"));
- Directory.CreateDirectory(fileInfo.Directory.FullName);
+ Directory.CreateDirectory(fileInfo.Directory!.FullName);
return fileInfo.FullName;
}
+ public string GetPath(Media media) =>
+ GetPath(media.Guid);
+
+ public string GetPath(Media media, int? width, int? height) =>
+ GetPath(media.Guid, width, height);
+
private int GetUploadedFileHash(UploadedFile uf) => (
uf.CreateTime,
uf.LastWriteTime,
diff --git a/Services/OcrService.cs b/Services/OcrService.cs
index 4d21705..40905aa 100644
--- a/Services/OcrService.cs
+++ b/Services/OcrService.cs
@@ -18,18 +18,21 @@ public class OcrService : IHostedService {
private Timer timer;
+ private IConfigService configService;
private IServiceScopeFactory scopeFactory;
private ILogger<OcrService> logger;
private IDbContextFactory<HBContext> dbFactory;
public OcrService(
+ IConfigService configService,
IServiceScopeFactory scopeFactory,
ILogger<OcrService> logger,
IDbContextFactory<HBContext> dbFactory) {
- this.scopeFactory = scopeFactory;
- this.logger = logger;
- this.dbFactory = dbFactory;
+ this.configService = configService;
+ this.scopeFactory = scopeFactory;
+ this.logger = logger;
+ this.dbFactory = dbFactory;
timer = new((object? state) => {
if(task is not null && !task.IsCompleted)
@@ -40,8 +43,11 @@ public class OcrService : IHostedService {
}
public Task StartAsync(CancellationToken ct) {
- logger.LogInformation("Service starting...");
- timer.Change(StartupDelay, ProcessInterval);
+ if(configService.EnableOcr) {
+ logger.LogInformation("Service starting...");
+ timer.Change(StartupDelay, ProcessInterval);
+ }
+
return Task.CompletedTask;
}
@@ -53,8 +59,6 @@ public class OcrService : IHostedService {
}
async Task ProcessAllAsync(CancellationToken ct) {
- return;
-
using var scope = scopeFactory.CreateScope();
var mediaService = scope.ServiceProvider
.GetRequiredService<IMediaService>();
diff --git a/Services/SearchService.cs b/Services/SearchService.cs
deleted file mode 100644
index 5ca12e1..0000000
--- a/Services/SearchService.cs
+++ /dev/null
@@ -1,117 +0,0 @@
-using Microsoft.EntityFrameworkCore;
-
-namespace HyperBooru.Services;
-
-public interface ISearchService {
- public Media[] Search(string query);
-}
-
-public class SearchService : ISearchService {
- private ITagService tagService;
-
- private IDbContextFactory<HBContext> dbFactory;
-
- public SearchService(
- IDbContextFactory<HBContext> dbFactory,
- ITagService tagService) {
-
- this.tagService = tagService;
- this.dbFactory = dbFactory;
- }
-
- public Media[] Search(string query) {
- var db = dbFactory.CreateDbContext();
-
- query = query.ToLower().Trim();
-
- int[] descriptionResults = SearchDescription(query);
- int[] filenameResults = SearchFilenames(query);
- int[] ocrResults = SearchOcr(query);
-
- var matchedTag = db.TagDefinitions
- .FirstOrDefault(td => td.Name.ToLower() == query);
-
- int[] tags;
- if(matchedTag is not null) {
- tags = tagService
- .TagsThatImply(matchedTag)
- .Select(td => td.ObjectId)
- .ToArray();
- } else {
- // TODO: Expand scope to all tags that imply
- tags = db.TagDefinitions
- .Where(td => td.Name.ToLower().Contains(query))
- .Select(td => td.ObjectId)
- .ToArray();
- }
-
- int[] tagResults = SearchTags(tags);
-
- int[] mediaIds = descriptionResults
- .Union(filenameResults)
- .Union(ocrResults)
- .Union(tagResults)
- .OrderDescending()
- .ToArray();
-
- return db.Media
- .Include(m => m.Tags)
- .Include(m => m.CurrentUploadedFile)
- .Where(m => mediaIds.Contains(m.ObjectId))
- .ToArray();
- }
-
- // TODO: Make asynchronous
- private int[] SearchDescription(string query) {
- return Task.Run(() => {
- using var db = dbFactory.CreateDbContext();
- query = query.ToLower();
- return db.Media
- .Where(m =>
- (m.ShortDescription != null && m.ShortDescription.ToLower().Contains(query)) ||
- (m.LongDescription != null && m.LongDescription.ToLower().Contains(query)))
- .Select(m => m.ObjectId)
- .ToArray();
- }).GetAwaiter().GetResult();
- }
-
- // TODO: Make asynchronous
- private int[] SearchFilenames(string query) {
- return Task.Run(() => {
- using var db = dbFactory.CreateDbContext();
- query = query.ToLower();
- return db.UploadedFiles
- .Include(uf => uf.Media)
- .Where(uf => uf.Filename != null && uf.Filename.ToLower().Contains(query))
- .Select(uf => uf.Media.ObjectId)
- .Distinct()
- .ToArray();
- }).GetAwaiter().GetResult();
- }
-
- // TODO: Make asynchronous
- private int[] SearchOcr(string query) {
- return Task.Run(() => {
- using var db = dbFactory.CreateDbContext();
- query = query.ToLower();
- return db.OcrData
- .Include(o => o.Media)
- .Where(o => o.SearchableText.Contains(query))
- .Select(o => o.Media.ObjectId)
- .ToArray();
- }).GetAwaiter().GetResult();
- }
-
- // TODO: Make asynchronous
- private int[] SearchTags(int[] tags) {
- return Task.Run(() => {
- using var db = dbFactory.CreateDbContext();
- return db.Media
- .Include(m => m.Tags)
- .AsEnumerable()
- .Where(m => m.Tags.IntersectBy(tags, t => t.TagDefinitionId).Any())
- .Select(m => m.ObjectId)
- .ToArray();
- }).GetAwaiter().GetResult();
- }
-}