diff options
| author | Jake Mannens <jake@asger.xyz> | 2026-05-22 12:46:00 +1000 |
|---|---|---|
| committer | Jake Mannens <jake@asger.xyz> | 2026-05-23 22:13:00 +1000 |
| commit | 4ea3ddb38d010c2f85c22b7f1c3f2d7e0c1355e3 (patch) | |
| tree | 90af9203059d645eb77216f1a091722ee9702438 /Services | |
| parent | 6de5d7f5364fe1d54703da6d6b7cb08ea26e939f (diff) | |
Initial commitwasm-oldserver
Diffstat (limited to 'Services')
| -rw-r--r-- | Services/ConfigService.cs | 72 | ||||
| -rw-r--r-- | Services/FeedService.cs | 212 | ||||
| -rw-r--r-- | Services/MediaService.cs | 400 | ||||
| -rw-r--r-- | Services/OcrService.cs | 128 | ||||
| -rw-r--r-- | Services/SourceService.cs | 20 | ||||
| -rw-r--r-- | Services/TagService.cs | 305 | ||||
| -rw-r--r-- | Services/UserService.cs | 76 |
7 files changed, 0 insertions, 1213 deletions
diff --git a/Services/ConfigService.cs b/Services/ConfigService.cs deleted file mode 100644 index ac1f155..0000000 --- a/Services/ConfigService.cs +++ /dev/null @@ -1,72 +0,0 @@ -using HyperBooru.ApiModels; - -namespace HyperBooru.Services; - -public interface IConfigService { - public string DataPath { get; } - public string KeyPath { get; } - public string DbConnectionString { get; } - public string MediaBasePath { get; } - public string ThumbnailBasePath { get; } - public string ConvertedMediaBasePath { get; } - public bool EnableOcr { get; } -} - -public class ConfigService : IConfigService { - private IConfiguration config; - - private const string AppName = "HyperBooru"; - - public string DataPath { - get { - #if DEBUG - return "Data"; - #else - string? path = config["DataPath"]; - if(path is not null) - return path; - - switch(Environment.OSVersion.Platform) { - case PlatformID.Win32NT: - return Path.Join( - Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), - AppName); - case PlatformID.Unix: - return $"/var/lib/{AppName.ToLower()}"; - default: - throw new NotImplementedException( - $"Unknown Operating System: {Environment.OSVersion.Platform}"); - } - #endif - } - } - - public string KeyPath => - Path.Join(DataPath, "keys"); - - public string DbConnectionString => - config.GetConnectionString("DefaultConnection") ?? - throw new HBException("Unable to get default connection string"); - - public string MediaBasePath => - Path.Join(DataPath, "media"); - - public string ThumbnailBasePath => - Path.Join(DataPath, "thumb"); - - public string ConvertedMediaBasePath => - Path.Join(DataPath, "converted"); - - public bool EnableOcr => - bool.TryParse(config["DisableOcr"], out bool x) ? !x : true; - - public ConfigService(IConfiguration config) { - this.config = config; - InitDirectoryStructure(); - } - - private void InitDirectoryStructure() { - Directory.CreateDirectory(DataPath); - Directory.CreateDirectory(MediaBasePath); - } -}
\ No newline at end of file diff --git a/Services/FeedService.cs b/Services/FeedService.cs deleted file mode 100644 index 3744e73..0000000 --- a/Services/FeedService.cs +++ /dev/null @@ -1,212 +0,0 @@ -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); -} diff --git a/Services/MediaService.cs b/Services/MediaService.cs deleted file mode 100644 index e497570..0000000 --- a/Services/MediaService.cs +++ /dev/null @@ -1,400 +0,0 @@ -using HyperBooru.ApiModels; -using ImageMagick; -using Microsoft.EntityFrameworkCore; -using MimeDetective; -using MimeDetective.Definitions; -using System.Security.Cryptography; -using System.Text.RegularExpressions; - -namespace HyperBooru.Services; - -public interface IMediaService { - public void SetDescription( - Media media, - string? shortDescription, - string? longDescription); - - public void SetIngest(Media media, bool ingest); - - public Media Create( - Stream fileData, - string fileName, - string? checksum = null, - DateTime? lastAccessTime = null, - DateTime? lastWriteTime = null, - DateTime? createTime = null, - string? path = null, - PathType? pathType = null, - Guid[]? tagIds = null); - - public void Delete(Guid media); - public void Delete(Media media); - public void DeleteThumbnails(Guid media); - public void DeleteThumbnails(Media media); - public Stream GetThumbnail(Guid media, int? width, int? height); - public Stream GetThumbnail(Media media, int? width, int? height); - public Stream GetConverted(Guid mediaId, string mimeType = "image/png"); - public Stream GetConverted(Media media, string mimeType = "image/png"); - public string GetPath(Guid media); - public string GetPath(Media media); - -} - -public class MediaService : IMediaService { - private readonly Dictionary<string,MagickFormat> FormatMap = new() { - ["image/jpeg"] = MagickFormat.Jpeg, - ["image/jpg"] = MagickFormat.Jpg, - ["image/png"] = MagickFormat.Png, - ["image/webp"] = MagickFormat.WebP - }; - - private IDbContextFactory<HBContext> dbFactory; - private IConfigService config; - - private IContentInspector inspector; - - public MediaService(IDbContextFactory<HBContext> dbFactory, - IConfigService config) { - - this.dbFactory = dbFactory; - this.config = config; - - ContentInspectorBuilder inspectorBuilder = new() { - Definitions = - DefaultDefinitions.FileTypes.Images.All() - .Union(DefaultDefinitions.FileTypes.Video.All()) - .ToList() - }; - - inspector = inspectorBuilder.Build(); - } - - public void SetIngest(Media media, bool ingest) { - using var db = dbFactory.CreateDbContext(); - media = db.Media - .Include(m => m.Tags) - .ThenInclude(t => t.TagDefinition) - .First(m => m.Guid == media.Guid); - var ingestTag = db.TagDefinitions - .First(td => td.Guid == HBContext.IngestTag); - - if(ingest) { - if(!media.Tags.Select(t => t.TagDefinition.Guid).Contains(HBContext.IngestTag)) - media.Tags.Add(new(ingestTag)); - } else { - media.Tags.RemoveAll(t => t.TagDefinition.Guid == HBContext.IngestTag); - } - - db.SaveChanges(); - } - - public void SetDescription( - Media media, - string? shortDescription, - string? longDescription) { - - using var db = dbFactory.CreateDbContext(); - var m = db.Media.First(m => m.Guid == media.Guid); - - shortDescription = shortDescription?.Trim(); - longDescription = longDescription?.Trim(); - - if(string.IsNullOrEmpty(shortDescription)) - shortDescription = null; - if(string.IsNullOrEmpty(longDescription)) - longDescription = null; - - m.ShortDescription = shortDescription; - m.LongDescription = longDescription; - - db.SaveChanges(); - } - - public Media Create( - Stream fileData, - string fileName, - string? checksum = null, - DateTime? lastAccessTime = null, - DateTime? lastWriteTime = null, - DateTime? createTime = null, - string? path = null, - PathType? pathType = null, - Guid[]? tagIds = null) { - - using var db = dbFactory.CreateDbContext(); - using var transaction = db.Database.BeginTransaction(); - - if(fileData.Length == 0) - throw new MediaCreateException("File is empty"); - - // Calculate the checksum using the in-memory file contents - var hash = BitConverter - .ToString(MD5.Create().ComputeHash(fileData)) - .Replace("-", "") - .ToLower(); - - if(checksum is not null && hash != checksum.ToLower()) - throw new MediaCreateException("Checksum does not match"); - - // Determine the MIME type - fileData.Seek(0, SeekOrigin.Begin); - var defs = inspector.Inspect(fileData); - var mime = defs.ByMimeType().FirstOrDefault()?.MimeType; - if(mime is null) - throw new MediaCreateException("Unsupported file type"); - - // Read the image with ImageMagick to determine the width and height - fileData.Seek(0, SeekOrigin.Begin); - using var magickImage = new MagickImage(fileData); - - var media = db.Media - .Include(m => m.UploadedFiles) - .Include(m => m.Tags) - .FirstOrDefault(m => m.UploadedFiles.Any(uf => uf.Checksum == hash)); - - var fileRecord = new UploadedFile() { - Filename = fileName, - Length = fileData.Length, - Checksum = hash, - ChecksumVerified = checksum is not null, - MimeType = mime, - Width = (int) magickImage.Width, - Height = (int) magickImage.Height, - UploadTime = DateTime.UtcNow, - LastAccessTime = lastAccessTime, - LastWriteTime = lastWriteTime, - CreateTime = createTime, - Path = pathType is null ? null : path, - PathType = pathType - }; - - var tags = Array.Empty<TagDefinition>(); - if(tagIds is not null) { - tagIds = tagIds.Distinct().ToArray(); - - tags = db.TagDefinitions - .Where(td => tagIds.Contains(td.Guid)) - .ToArray(); - - if(tags.Count() < tagIds.Count()) { - var badIds = tagIds - .Where(x => !tags.Select(td => td.Guid).Contains(x)) - .Order(); - - throw new MediaCreateException( - $"Non-existent tags specified: {string.Join(", ", badIds)}"); - } - } - - if(media is null) { - var ingestTagDef = db.TagDefinitions - .First(td => td.Guid == HBContext.IngestTag); - - media = new() { - UploadedFiles = new() { - fileRecord - }, - Tags = tags is null ? [ new() { TagDefinition = ingestTagDef } ] : tags - .Select(td => new Tag() { TagDefinition = td }) - .ToList() - }; - - using var newFile = File.Create(GetPath(media)); - - fileData.Seek(0, SeekOrigin.Begin); - fileData.CopyTo(newFile); - newFile.Flush(); - - db.Media.Add(media); - db.SaveChanges(); - media.CurrentUploadedFile = fileRecord; - db.SaveChanges(); - } else { - var fileHashes = media.UploadedFiles - .Select(uf => GetUploadedFileHash(uf)); - // Only add the uploaded file record if it contains new information - if(!fileHashes.Contains(GetUploadedFileHash(fileRecord))) - media.UploadedFiles.Add(fileRecord); - // Add new tags if needed - var missingTags = tags - .Where(td => !media.Tags.Select(t => t.TagDefinition.Guid).Contains(td.Guid)); - media.Tags.AddRange(missingTags.Select(td => new Tag() { TagDefinition = td })); - db.Update(media); - db.SaveChanges(); - } - - transaction.Commit(); - - return media; - } - - public void Delete(Guid media) { - using var db = dbFactory.CreateDbContext(); - var m = db.Media.First(m => m.Guid == media); - - var path = Path.Join( - config.MediaBasePath, - m.Guid.ToString().Substring(0, 2), - m.Guid.ToString().Substring(2, 2), - m.Guid.ToString()); - - try { - var fileInfo = new FileInfo(path); - fileInfo.Delete(); - fileInfo.Directory?.Delete(); - fileInfo.Directory?.Parent?.Delete(); - } catch(IOException) {} - - try { - DeleteThumbnails(media); - } catch {} - - db.Media.Remove(m); - db.SaveChanges(); - } - - public void Delete(Media media) => - Delete(media.Guid); - - public void DeleteThumbnails(Guid media) { - var dir = new DirectoryInfo(Path.Join( - config.ThumbnailBasePath, - media.ToString().Substring(0, 2), - media.ToString().Substring(2, 2))); - - var pattern = new Regex($"^{media}-[0-9]+-[0-9]+$"); - var toDelete = dir.GetFiles() - .Where(f => pattern.IsMatch(f.Name)) - .ToList(); - - List<Exception> exceptions = new(); - - foreach(var file in toDelete) { - try { - file.Delete(); - } catch(Exception e) { - exceptions.Add(e); - } - } - - try { - dir.Delete(); - dir.Parent?.Delete(); - } catch(Exception e) { - exceptions.Add(e); - } - - // TODO: wrap the AggregateException in a ThumbnailException - if(exceptions.Count() > 1) - throw new AggregateException(exceptions); - } - - public void DeleteThumbnails(Media media) => - DeleteThumbnails(media.Guid); - - 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 thumbPath = GetThumbnailPath(mediaId, width, height); - - if(File.Exists(thumbPath)) - return System.IO.File.OpenRead(thumbPath); - - if(!File.Exists(GetPath(mediaId))) - throw new ObjectNotFoundException(mediaId); - - using var image = new MagickImage(GetPath(mediaId)); - - if(width > image.Width || height > image.Height) { - width = (int) image.Width; - height = (int) image.Height; - } - - image.Thumbnail((uint) (width ?? -1), (uint) (height ?? -1)); - image.Write(thumbPath, MagickFormat.Jpeg); - - return System.IO.File.OpenRead(thumbPath); - } - - public Stream GetConverted(Guid mediaId, string mimeType) { - if(!FormatMap.TryGetValue(mimeType, out var format)) - throw new MediaException($"Cannot convert to unknown format ({mimeType})", mediaId); - - var convertedPath = GetConvertedPath(mediaId, mimeType); - - if(File.Exists(convertedPath)) - return System.IO.File.OpenRead(convertedPath); - - if(!File.Exists(GetPath(mediaId))) - throw new ObjectNotFoundException(mediaId); - - using var image = new MagickImage(GetPath(mediaId)); - image.Write(convertedPath, format); - - return System.IO.File.OpenRead(convertedPath); - } - - public Stream GetThumbnail(Media media, int? width, int? height) => - GetThumbnail(media.Guid, width, height); - - public Stream GetConverted(Media media, string mimeType) => - GetConverted(media.Guid, mimeType); - - public string GetPath(Guid mediaId) { - var fileInfo = new FileInfo( - Path.Join( - config.MediaBasePath, - mediaId.ToString().Substring(0, 2), - mediaId.ToString().Substring(2, 2), - mediaId.ToString())); - - Directory.CreateDirectory(fileInfo.Directory!.FullName); - return fileInfo.FullName; - } - - public string GetThumbnailPath(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, - mediaId.ToString().Substring(0, 2), - mediaId.ToString().Substring(2, 2), - $"{mediaId.ToString()}-{(width ?? 0)}-{(height ?? 0)}")); - - Directory.CreateDirectory(fileInfo.Directory!.FullName); - return fileInfo.FullName; - } - - public string GetConvertedPath(Guid mediaId, string mimeType) { - var fileInfo = new FileInfo(Path.Join( - config.ConvertedMediaBasePath, - mediaId.ToString().Substring(0, 2), - mediaId.ToString().Substring(2, 2), - $"{mediaId.ToString()}-{mimeType.Split('/')[1]}")); - - Directory.CreateDirectory(fileInfo.Directory!.FullName); - return fileInfo.FullName; - } - - public string GetPath(Media media) => - GetPath(media.Guid); - - public string GetThumbnailPath(Media media, int? width, int? height) => - GetThumbnailPath(media.Guid, width, height); - - public string GetConvertedPath(Media media, string mimeType) => - GetConvertedPath(media.Guid, mimeType); - - private int GetUploadedFileHash(UploadedFile uf) => ( - uf.CreateTime, - uf.LastWriteTime, - uf.Filename, - uf.Length, - uf.Checksum).GetHashCode(); -} diff --git a/Services/OcrService.cs b/Services/OcrService.cs deleted file mode 100644 index d43db2e..0000000 --- a/Services/OcrService.cs +++ /dev/null @@ -1,128 +0,0 @@ -using HyperBooru.Util; -using Microsoft.EntityFrameworkCore; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Text.RegularExpressions; -using Tesseract; - -namespace HyperBooru.Services; - -public class OcrService : IHostedService { - private readonly string[] InvalidMimeTypes = [ "image/heic", "image/webp" ]; - - private readonly TimeSpan ProcessInterval = TimeSpan.FromMinutes(30); - private readonly TimeSpan StartupDelay = TimeSpan.FromSeconds(30); - - private readonly Regex SpaceRegex = new(@"[^0-9a-z]+", RegexOptions.Compiled); - - private Task? task; - private CancellationTokenSource cts = new(); - - 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.configService = configService; - this.scopeFactory = scopeFactory; - this.logger = logger; - this.dbFactory = dbFactory; - - timer = new((object? state) => { - if(task is not null && !task.IsCompleted) - return; - cts = new(); - task = ProcessAllAsync(cts.Token); - }); - } - - public Task StartAsync(CancellationToken ct) { - if(configService.EnableOcr) { - logger.LogInformation("Service starting..."); - timer.Change(StartupDelay, ProcessInterval); - } - - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken ct) { - logger.LogInformation("Service stopping..."); - timer.Change(Timeout.Infinite, Timeout.Infinite); - cts.Cancel(); - return Task.CompletedTask; - } - - async Task ProcessAllAsync(CancellationToken ct) { - using var scope = scopeFactory.CreateScope(); - var mediaService = scope.ServiceProvider - .GetRequiredService<IMediaService>(); - - using var db = dbFactory.CreateDbContext(); - Guid[] guids = db.Media - .AsNoTracking() - .Include(m => m.CurrentUploadedFile) - .Include(m => m.OcrData) - .Where(m => m.OcrData == null) - .Where(m => m.CurrentUploadedFile!.MimeType.Contains("image/")) - .Where(m => !InvalidMimeTypes.Contains(m.CurrentUploadedFile!.MimeType)) - .Select(m => m.Guid) - .ToArray(); - db.Dispose(); - - logger.LogInformation($"Performing OCR pass on {guids.Count()} media items"); - - var factory = new TaskFactory(new LimitedConcurrencyTaskScheduler()); - var tasks = new List<Task>(); - - var stopwatch = Stopwatch.StartNew(); - - foreach(var guid in guids) - tasks.Add(factory.StartNew(() => Process(guid, mediaService), ct)); - - await Task.WhenAll(tasks); - stopwatch.Stop(); - - var time = stopwatch.Elapsed.ToStringHumanReadable(); - logger.LogInformation( - $"Performed OCR pass on {guids.Count()} media items in {time}"); - } - - private void Process(Guid media, IMediaService mediaService) { - logger.LogDebug($"Performing OCR on media item {media}"); - - using var db = dbFactory.CreateDbContext(); - var m = db.Media - .Include(m => m.OcrData) - .First(m => m.Guid == media); - - OcrData o = m.OcrData ?? new(); - - using var engine = new TesseractEngine("tessdata", "eng", EngineMode.Default); - using var image = Pix.LoadFromFile(mediaService.GetPath(m)); - engine.SetVariable("debug_file", NullFile); - - o.Timestamp = DateTime.UtcNow; - o.Text = engine.Process(image).GetText().Trim(); - o.SearchableText = SpaceRegex.Replace(o.Text.ToLower(), " ").Trim(); - - m.OcrData = o; - db.SaveChanges(); - } - - private string NullFile { - get { - if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - return "NUL"; - else - return "/dev/null"; - } - } -} diff --git a/Services/SourceService.cs b/Services/SourceService.cs deleted file mode 100644 index d145346..0000000 --- a/Services/SourceService.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Text.RegularExpressions; - -namespace HyperBooru.Services; - -public interface ISourceService { - public string? GetUrlFromFilename(string filename); -} - -public class SourceService : ISourceService { - private Regex PixivRegex = - new(@"^([0-9]+)_p[0-9]+(_master1200)?\.[^.]+$", RegexOptions.Compiled); - - public string? GetUrlFromFilename(string filename) { - var pixivMatch = PixivRegex.Match(filename); - if(pixivMatch.Success) - return $"https://pixiv.net/en/artworks/{pixivMatch.Groups[1].Value}"; - - return null; - } -} diff --git a/Services/TagService.cs b/Services/TagService.cs deleted file mode 100644 index f7b91dc..0000000 --- a/Services/TagService.cs +++ /dev/null @@ -1,305 +0,0 @@ -using HyperBooru.ApiModels; -using Microsoft.EntityFrameworkCore; - -namespace HyperBooru.Services; - -public interface ITagService { - public void AddTag(Guid obj, Guid tagDef); - public void AddTag(HBObject obj, TagDefinition tagDef); - public void RemoveTag(Guid obj, Guid tagDef); - public void RemoveTag(HBObject obj, TagDefinition tagDef); - public void SetImplicitTags(TagDefinition tagDef, TagDefinition[] implicitTagDefs); - public void SetImplicitTags(Guid tagDef, Guid[] implicitTagDefs); - public void AddImplicitTag(Guid tagDef, Guid implicitTagDef); - public void AddImplicitTag(TagDefinition tagDef, TagDefinition implicitTagDef); - public void RemoveImplicitTag(Guid tagDef, Guid implicitTagDef); - public void RemoveImplicitTag(TagDefinition tagDef, TagDefinition implicitTagDef); - public void CreateTagDefinition(string name, string? @namespace = null, string? alias = null); - public void DeleteTagDefinition(Guid tagDef); - public void DeleteTagDefinition(TagDefinition tagDef); - public void UpdateTagDefinition(Guid tagDef, string name, string? @namespace = null, string? alias = null); - public void UpdateTagDefinition(TagDefinition tagDef, string name, string? @namespace = null, string? alias = null); - public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(Guid obj); - public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(HBObject obj); - public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(TagDefinition tagDef); - public TagDefinition[] TagsThatImply(Guid tagDef); - public TagDefinition[] TagsThatImply(TagDefinition tagDef); -} - -public class TagService : ITagService { - private IDbContextFactory<HBContext> dbFactory; - - public TagService(IDbContextFactory<HBContext> dbFactory) => - this.dbFactory = dbFactory; - - public void AddTag(Guid obj, Guid tagDef) { - using var db = dbFactory.CreateDbContext(); - - var tag = db.TagDefinitions.First(td => td.Guid == tagDef); - - db.Objects - .Include(o => o.Tags) - .ThenInclude(t => t.TagDefinition) - .Where(o => !o.Tags.Select(t => t.TagDefinition.Guid).Contains(tagDef)) - .FirstOrDefault(o => o.Guid == obj)? - .Tags - .Add(new(tag)); - - db.SaveChanges(); - } - - public void AddTag(HBObject obj, TagDefinition tagDef) => - AddTag(obj.Guid, tagDef.Guid); - - public void RemoveTag(Guid obj, Guid tagDef) { - using var db = dbFactory.CreateDbContext(); - - db.Objects - .Include(o => o.Tags) - .ThenInclude(t => t.TagDefinition) - .First(o => o.Guid == obj) - .Tags - .RemoveAll(t => t.TagDefinition.Guid == tagDef); - - db.SaveChanges(); - } - - public void RemoveTag(HBObject obj, TagDefinition tagDef) => - RemoveTag(obj.Guid, tagDef.Guid); - - public void SetImplicitTags(Guid tagDef, Guid[] implicitTagDefs) { - using var db = dbFactory.CreateDbContext(); - using var transaction = db.Database.BeginTransaction(); - - var tag = db.TagDefinitions - .Include(td => td.ImplicitTags) - .First(td => td.Guid == tagDef); - - tag.ImplicitTags.RemoveAll(td => !implicitTagDefs.Contains(td.Guid)); - tag.ImplicitTags.AddRange( - db.TagDefinitions - .Where(td => implicitTagDefs.Contains(td.Guid)) - .Where(td => !tag.ImplicitTags - .Select(td => td.Guid) - .Contains(td.Guid))); - - db.SaveChanges(); - transaction.Commit(); - } - - public void SetImplicitTags(TagDefinition tagDef, TagDefinition[] implicitTagDefs) => - SetImplicitTags(tagDef.Guid, implicitTagDefs.Select(td => td.Guid).ToArray()); - - public void AddImplicitTag(Guid tagDef, Guid implicitTagDef) { - using var db = dbFactory.CreateDbContext(); - - var tag = db.TagDefinitions - .Include(td => td.ImplicitTags) - .First(td => td.Guid == tagDef); - var implicitTag = db.TagDefinitions.First(td => td.Guid == implicitTagDef); - - tag.ImplicitTags.Add(implicitTag); - db.SaveChanges(); - } - - public void AddImplicitTag(TagDefinition tagDef, TagDefinition implicitTagDef) => - AddImplicitTag(tagDef, implicitTagDef); - - public void RemoveImplicitTag(Guid tagDef, Guid implicitTagDef) { - using var db = dbFactory.CreateDbContext(); - - var tag = db.TagDefinitions - .Include(td => td.ImplicitTags) - .First(td => td.Guid == tagDef); - - tag.ImplicitTags.RemoveAll(td => td.Guid == implicitTagDef); - db.SaveChanges(); - } - - public void RemoveImplicitTag(TagDefinition tagDef, TagDefinition implicitTagDef) => - RemoveImplicitTag(tagDef, implicitTagDef); - - public void CreateTagDefinition(string name, string? @namespace = null, string? alias = null) { - using var db = dbFactory.CreateDbContext(); - - if(string.IsNullOrEmpty(@namespace)) - @namespace = null; - if(string.IsNullOrEmpty(alias)) - alias = null; - - // Remove leading and trailing whitespace - name = name.Trim(); - @namespace = @namespace?.Trim(); - alias = alias?.Trim(); - - TagDefinition tagDef = new() { - Source = TagSource.UserTag, - Namespace = @namespace, - Name = name, - Alias = alias - }; - - bool nameExists = db.TagDefinitions.Any(td => td.Name.ToLower() == name.ToLower()); - bool aliasExists = false; - if(alias is not null) - aliasExists = db.TagDefinitions - .Where(td => td.Alias != null) - .Any(td => td.Alias!.ToLower() == alias.ToLower()); - if(nameExists || aliasExists) - throw new TagDuplicateException(nameExists, aliasExists); - - if(!db.TagDefinitions.Contains(tagDef)) - db.TagDefinitions.Add(tagDef); - db.SaveChanges(); - } - - public void DeleteTagDefinition(Guid tagDef) { - using var db = dbFactory.CreateDbContext(); - - var tag = db.TagDefinitions.First(td => td.Guid == tagDef); - - using var transaction = db.Database.BeginTransaction(); - - db.Tags.RemoveRange( - db.Tags - .Include(t => t.TagDefinition) - .Where(t => t.TagDefinition.Guid == tagDef)); - db.TagDefinitions.Remove(tag); - db.SaveChanges(); - - transaction.Commit(); - } - - public void DeleteTagDefinition(TagDefinition tagDef) => - DeleteTagDefinition(tagDef.Guid); - - public void UpdateTagDefinition(Guid tagDef, string name, string? @namespace = null, string? alias = null) { - using var db = dbFactory.CreateDbContext(); - - if(string.IsNullOrEmpty(@namespace)) - @namespace = null; - if(string.IsNullOrEmpty(alias)) - alias = null; - - // Remove leading and trailing whitespace - name = name.Trim(); - @namespace = @namespace?.Trim(); - alias = alias?.Trim(); - - var tag = db.TagDefinitions.First(td => td.Guid == tagDef); - - TagDefinition? nameExisting = db.TagDefinitions.FirstOrDefault(td => td.Name.ToLower() == name.ToLower()); - TagDefinition? aliasExisting = null; - if(alias is not null) - aliasExisting = db.TagDefinitions - .Where(td => td.Alias != null) - .FirstOrDefault(td => td.Alias!.ToLower() == alias.ToLower()); - bool nameExists = nameExisting is not null && nameExisting != tag; - bool aliasExists = aliasExisting is not null && aliasExisting != tag; - if(nameExists || aliasExists) - throw new TagDuplicateException(nameExists, aliasExists); - - tag.Name = name; - tag.Namespace = @namespace; - tag.Alias = alias; - - db.SaveChanges(); - } - - public void UpdateTagDefinition(TagDefinition tagDef, string name, string? @namespace = null, string? alias = null) => - UpdateTagDefinition(tagDef.Guid, name, @namespace, alias); - - private (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(IEnumerable<TagDefinition> tagDefs) { - using var db = dbFactory.CreateDbContext(); - - var tagGuids = tagDefs - .Select(td => td.Guid) - .ToArray(); - - // Query all tag definitions - var allTags = db.TagDefinitions - .Include(td => td.ImplicitTags) - .ToArray(); - - var tags = new List<TagDefinition>( - allTags.IntersectBy( - tagGuids, - td => td.Guid)); - - while(true) { - var toAdd = tags - .SelectMany(td => td.ImplicitTags) - .ExceptBy(tags.Select(td => td.Guid), td => td.Guid) - .ToArray(); - - if(toAdd.Count() == 0) - break; - - tags.AddRange(toAdd); - } - - return tags - .Select(td => new ValueTuple<TagDefinition, bool>(td, !tagGuids.Contains(td.Guid))) - .ToArray(); - } - - public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(Guid obj) { - using var db = dbFactory.CreateDbContext(); - - // Query a list of tag GUIDs for this object - var tags = db.Objects - .Include(o => o.Tags) - .ThenInclude(t => t.TagDefinition) - .First(o => o.Guid == obj) - .Tags - .Select(t => t.TagDefinition) - .ToArray(); - - return GetAllTags(tags); - } - - - public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(HBObject obj) => - GetAllTags(obj.Guid); - - public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(TagDefinition tagDef) { - using var db = dbFactory.CreateDbContext(); - - var tags = db.TagDefinitions - .Include(td => td.ImplicitTags) - .First(td => td.Guid == tagDef.Guid) - .ImplicitTags - .ToArray(); - - return GetAllTags(tags); - } - - public TagDefinition[] TagsThatImply(Guid tagDef) { - using var db = dbFactory.CreateDbContext(); - - var tagDefs = db.TagDefinitions - .Include(td => td.ImplicitTags) - .ToArray(); - - var tags = new List<TagDefinition>() { - db.TagDefinitions.First(td => td.Guid == tagDef) - }; - - while(true) { - var toAdd = tagDefs - .Where(td => td.ImplicitTags.Select(it => it.Guid).Intersect(tags.Select(td => td.Guid)).Any()) - .ExceptBy(tags.Select(td => td.Guid), td => td.Guid) - .ToArray(); - - if(toAdd.Count() == 0) - break; - - tags.AddRange(toAdd); - } - - return tags.ToArray(); - } - - public TagDefinition[] TagsThatImply(TagDefinition tagDef) => - TagsThatImply(tagDef.Guid); -} diff --git a/Services/UserService.cs b/Services/UserService.cs deleted file mode 100644 index 9e79dc6..0000000 --- a/Services/UserService.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Microsoft.AspNetCore.Cryptography.KeyDerivation; - -namespace HyperBooru.Services; - -public interface IUserService { - public UserSessionState UserSessionState { get; } -} - -public class UserService : IUserService { - public UserSessionState UserSessionState => - globalUserService.GetSessionState(httpContext.Session.Id); - - private IHttpContextAccessor httpContextAccessor; - private IGlobalUserService globalUserService; - - private HttpContext httpContext => - httpContextAccessor.HttpContext!; - - public UserService( - IHttpContextAccessor httpContextAccessor, - IGlobalUserService globalUserService) { - - this.httpContextAccessor = httpContextAccessor; - this.globalUserService = globalUserService; - - // HTTP context session states are discarded if no values - // are set. Set a dummy value so that the session state - // will not be discarded later when we actually need it. - httpContext.Session.SetInt32("Persist", 1); - } - - public static string HashPassword(string password) => - Convert.ToBase64String( - KeyDerivation.Pbkdf2( - password, - Array.Empty<byte>(), - KeyDerivationPrf.HMACSHA512, - 100_000, - 512 / 8)); -} - -public interface IGlobalUserService { - public UserSessionState GetSessionState(string id); -} - -public class GlobalUserService : IGlobalUserService { - // TODO: prune this list periodically - private Dictionary<string, UserSessionState> sessionStates = new(); - - public UserSessionState GetSessionState(string id) { - sessionStates.TryGetValue(id, out var state); - - if(state is null) { - state = new(); - sessionStates[id] = state; - } - - return state; - } -} - -public record UserSessionState { - public event UserSessionStateChange OnStateChange; - - public bool ShowNsfw { - get => showNsfw; - set { - showNsfw = value; - OnStateChange.Invoke(this); - } - } - - private bool showNsfw = false; -} - -public delegate void UserSessionStateChange(UserSessionState sessionState); |
