diff options
| author | Jake Mannens <jake@asger.xyz> | 2026-03-25 01:57:19 +1100 |
|---|---|---|
| committer | Jake Mannens <jake@asger.xyz> | 2026-03-25 01:57:46 +1100 |
| commit | 6c06dfc4f83f30292e65c08a3cb0c48401d4bfa7 (patch) | |
| tree | 511f88873fa6173637115a38c31ec5f8018e108e /Services | |
| parent | c751709b1b4fe6f16fd84647e8e071455e7b78d6 (diff) | |
v0.2av0.2a
Diffstat (limited to 'Services')
| -rw-r--r-- | Services/ConfigService.cs | 4 | ||||
| -rw-r--r-- | Services/FeedService.cs | 155 | ||||
| -rw-r--r-- | Services/MediaService.cs | 75 | ||||
| -rw-r--r-- | Services/OcrService.cs | 18 | ||||
| -rw-r--r-- | Services/SearchService.cs | 117 |
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(); - } -} |
