diff --git a/AobaClient/src/components/media_item.rs b/AobaClient/src/components/media_item.rs index b601560..e792a4d 100644 --- a/AobaClient/src/components/media_item.rs +++ b/AobaClient/src/components/media_item.rs @@ -12,12 +12,11 @@ pub fn MediaItem(props: MediaItemProps) -> Element { if let Some(item) = props.item { let mtype = item.media_type().as_str_name(); let filename = item.file_name; - let id = item.media_id.unwrap().value; - - let src = format!("{HOST}/m/thumb/{id}"); + let id = item.id.unwrap().value; + let thumb = item.thumb_url; return rsx! { a { class: "mediaItem", href: "{HOST}/m/{id}", target: "_blank", - img { src } + img { src: "{HOST}{thumb}" } span { class: "info", span { class: "name", "{filename}" } span { class: "details", diff --git a/AobaCore/Models/Media.cs b/AobaCore/Models/Media.cs index 8eb43aa..60d827f 100644 --- a/AobaCore/Models/Media.cs +++ b/AobaCore/Models/Media.cs @@ -7,7 +7,7 @@ namespace AobaCore.Models; public class Media { [BsonId] - public ObjectId Id { get; set; } + public ObjectId LegacyId { get; set; } public ObjectId MediaId { get; set; } public string Filename { get; set; } public MediaType MediaType { get; set; } @@ -16,6 +16,7 @@ public class Media public ObjectId Owner { get; set; } public DateTime UploadDate { get; set; } public string[] Tags { get; set; } = []; + public Dictionary Thumbnails { get; set; } = []; public static readonly Dictionary KnownTypes = new() @@ -65,7 +66,7 @@ public class Media Filename = filename; MediaId = fileId; Owner = owner; - Id = ObjectId.GenerateNewId(); + LegacyId = ObjectId.GenerateNewId(); UploadDate = DateTime.UtcNow; Tags = DeriveTags(filename); } diff --git a/AobaCore/Services/AobaIndexCreationService.cs b/AobaCore/Services/AobaIndexCreationService.cs index 12112a7..97f8229 100644 --- a/AobaCore/Services/AobaIndexCreationService.cs +++ b/AobaCore/Services/AobaIndexCreationService.cs @@ -16,6 +16,16 @@ public class AobaIndexCreationService(IMongoDatabase db): BackgroundService protected override async Task ExecuteAsync(CancellationToken stoppingToken) { BsonSerializer.RegisterSerializer(new EnumSerializer(BsonType.String)); + + var mediaId = Builders.IndexKeys.Ascending(m => m.MediaId); + + var mediaIdModel = new CreateIndexModel(mediaId, new CreateIndexOptions + { + Name = "Media", + Unique = true, + Background = true + }); + var textKeys = Builders.IndexKeys .Text(m => m.Filename) .Text(m => m.Ext) @@ -27,6 +37,7 @@ public class AobaIndexCreationService(IMongoDatabase db): BackgroundService Background = true }); + await _media.EnsureIndexAsync(mediaIdModel); await _media.EnsureIndexAsync(textModel); } } \ No newline at end of file diff --git a/AobaCore/Services/AobaService.cs b/AobaCore/Services/AobaService.cs index 59cf5be..2d2247d 100644 --- a/AobaCore/Services/AobaService.cs +++ b/AobaCore/Services/AobaService.cs @@ -13,9 +13,9 @@ public class AobaService(IMongoDatabase db) private readonly IMongoCollection _media = db.GetCollection("media"); private readonly GridFSBucket _gridFs = new(db); - public async Task GetMediaAsync(ObjectId id, CancellationToken cancellationToken = default) + public async Task GetMediaFromLegacyIdAsync(ObjectId id, CancellationToken cancellationToken = default) { - return await _media.Find(m => m.Id == id).FirstOrDefaultAsync(cancellationToken); + return await _media.Find(m => m.LegacyId == id).FirstOrDefaultAsync(cancellationToken); } public async Task GetMediaFromFileAsync(ObjectId id, CancellationToken cancellationToken = default) @@ -44,14 +44,22 @@ public class AobaService(IMongoDatabase db) return _media.InsertOneAsync(media, null, cancellationToken); } - public Task IncrementViewCountAsync(ObjectId id, CancellationToken cancellationToken = default) + public async Task AddThumbnailAsync(ObjectId mediaId, ObjectId thumbId, ThumbnailSize size, CancellationToken cancellationToken = default) { - return _media.UpdateOneAsync(m => m.Id == id, Builders.Update.Inc(m => m.ViewCount, 1), cancellationToken: cancellationToken); + var upate = Builders.Update.Set(m => m.Thumbnails[size], thumbId); + + await _media.UpdateOneAsync(m => m.MediaId == mediaId, upate, cancellationToken: cancellationToken); } - public Task IncrementFileViewCountAsync(ObjectId fileId, CancellationToken cancellationToken = default) + public async Task GetThumbnailIdAsync(ObjectId mediaId, ThumbnailSize size, CancellationToken cancellationToken = default) { - return _media.UpdateOneAsync(m => m.MediaId == fileId, Builders.Update.Inc(m => m.ViewCount, 1), cancellationToken: cancellationToken); + var thumb = await _media.Find(m => m.MediaId == mediaId).Project(m => m.Thumbnails[size]).FirstOrDefaultAsync(cancellationToken); + return thumb; + } + + public Task IncrementViewCountAsync(ObjectId mediaId, CancellationToken cancellationToken = default) + { + return _media.UpdateOneAsync(m => m.MediaId == mediaId, Builders.Update.Inc(m => m.ViewCount, 1), cancellationToken: cancellationToken); } @@ -70,11 +78,11 @@ public class AobaService(IMongoDatabase db) } } - public async Task> GetFileStreamAsync(ObjectId id, bool seekable = false, CancellationToken cancellationToken = default) + public async Task> GetFileStreamAsync(ObjectId mediaId, bool seekable = false, CancellationToken cancellationToken = default) { try { - return await _gridFs.OpenDownloadStreamAsync(id, new GridFSDownloadOptions { Seekable = seekable }, cancellationToken); + return await _gridFs.OpenDownloadStreamAsync(mediaId, new GridFSDownloadOptions { Seekable = seekable }, cancellationToken); } catch (GridFSException ex) { @@ -82,13 +90,13 @@ public class AobaService(IMongoDatabase db) } } - public async Task DeleteFileAsync(ObjectId fileId, CancellationToken cancellationToken = default) + public async Task DeleteFileAsync(ObjectId mediaId, CancellationToken cancellationToken = default) { try { cancellationToken.ThrowIfCancellationRequested(); - await _gridFs.DeleteAsync(fileId, CancellationToken.None); - await _media.DeleteOneAsync(m => m.MediaId == fileId, CancellationToken.None); + await _gridFs.DeleteAsync(mediaId, CancellationToken.None); + await _media.DeleteOneAsync(m => m.MediaId == mediaId, CancellationToken.None); } catch (GridFSFileNotFoundException) { @@ -96,6 +104,8 @@ public class AobaService(IMongoDatabase db) } } + + public async Task DeriveTagsAsync(CancellationToken cancellationToken = default) { var mediaItems = await _media.Find(Builders.Filter.Exists(m => m.Tags, false)) @@ -104,7 +114,7 @@ public class AobaService(IMongoDatabase db) foreach (var mediaItem in mediaItems) { mediaItem.Tags = Media.DeriveTags(mediaItem.Filename); - await _media.UpdateOneAsync(m => m.Id == mediaItem.Id, Builders.Update.Set(m => m.Tags, mediaItem.Tags), null, cancellationToken); + await _media.UpdateOneAsync(m => m.MediaId == mediaItem.MediaId, Builders.Update.Set(m => m.Tags, mediaItem.Tags), null, cancellationToken); } Console.WriteLine("All Tags Derived"); } diff --git a/AobaCore/Services/ThumbnailService.cs b/AobaCore/Services/ThumbnailService.cs index ec3d170..cafd77a 100644 --- a/AobaCore/Services/ThumbnailService.cs +++ b/AobaCore/Services/ThumbnailService.cs @@ -20,33 +20,32 @@ using System.Text; using System.Threading.Tasks; namespace AobaCore.Services; + public class ThumbnailService(IMongoDatabase db, AobaService aobaService) { private readonly GridFSBucket _gridfs = new GridFSBucket(db); - private readonly IMongoCollection _thumbnails = db.GetCollection("thumbs"); - + private Lock _lock = new(); /// - /// + /// /// - /// File id + /// Media id /// /// /// - public async Task> GetOrCreateThumbnailAsync(ObjectId id, ThumbnailSize size, CancellationToken cancellationToken = default) + public async Task> GetOrCreateThumbnailAsync(ObjectId mediaId, ThumbnailSize size, CancellationToken cancellationToken = default) { - var existingThumb = await GetThumbnailAsync(id, size, cancellationToken); + var existingThumb = await GetThumbnailAsync(mediaId, size, cancellationToken); if (existingThumb != null) return existingThumb; - var media = await aobaService.GetMediaFromFileAsync(id, cancellationToken); + var media = await aobaService.GetMediaFromFileAsync(mediaId, cancellationToken); if (media == null) return new Error("Media does not exist"); try { - using var mediaData = await _gridfs.OpenDownloadStreamAsync(media.MediaId, new GridFSDownloadOptions { Seekable = true }, cancellationToken); var thumb = await GenerateThumbnailAsync(mediaData, size, media.MediaType, media.Ext, cancellationToken); @@ -54,59 +53,58 @@ public class ThumbnailService(IMongoDatabase db, AobaService aobaService) return thumb.Error; cancellationToken.ThrowIfCancellationRequested(); -#if !DEBUG var thumbId = await _gridfs.UploadFromStreamAsync($"{media.Filename}.webp", thumb, cancellationToken: CancellationToken.None); - var update = Builders.Update.Set(t => t.Sizes[size], thumbId); - await _thumbnails.UpdateOneAsync(t => t.Id == id, update, cancellationToken: CancellationToken.None); -#endif + await aobaService.AddThumbnailAsync(mediaId, thumbId, size, cancellationToken); + thumb.Value.Position = 0; return thumb; - } catch (Exception ex) { + } + catch (Exception ex) + { return ex; } - - - } /// - /// + /// /// - /// File Id + /// Media Id /// /// /// - public async Task GetThumbnailAsync(ObjectId id, ThumbnailSize size, CancellationToken cancellationToken = default) + public async Task GetThumbnailAsync(ObjectId mediaId, ThumbnailSize size, CancellationToken cancellationToken = default) { - var thumb = await _thumbnails.Find(t => t.Id == id).FirstOrDefaultAsync(cancellationToken); - if (thumb == null) + var thumb = await aobaService.GetThumbnailIdAsync(mediaId, size, cancellationToken); + if (thumb == default) return null; - if (!thumb.Sizes.TryGetValue(size, out var tid)) - return null; - - var thumbData = await _gridfs.OpenDownloadStreamAsync(tid, cancellationToken: cancellationToken); + var thumbData = await _gridfs.OpenDownloadStreamAsync(thumb, cancellationToken: cancellationToken); return thumbData; } + public async Task GetThumbnailByFileIdAsync(ObjectId thumbId, CancellationToken cancellationToken = default) + { + var thumbData = await _gridfs.OpenDownloadStreamAsync(thumbId, cancellationToken: cancellationToken); + return thumbData; + } public async Task> GenerateThumbnailAsync(Stream stream, ThumbnailSize size, MediaType type, string ext, CancellationToken cancellationToken = default) { return type switch { MediaType.Image => await GenerateImageThumbnailAsync(stream, size, cancellationToken), - MediaType.Video => await GenerateVideoThumbnailAsync(stream, size, cancellationToken), + MediaType.Video => GenerateVideoThumbnail(stream, size, cancellationToken), MediaType.Text or MediaType.Code => await GenerateDocumentThumbnailAsync(stream, size, cancellationToken), _ => new Error($"No Thumbnail for {type}"), }; } - public async Task GenerateImageThumbnailAsync(Stream stream, ThumbnailSize size, CancellationToken cancellationToken = default) + public static async Task GenerateImageThumbnailAsync(Stream stream, ThumbnailSize size, CancellationToken cancellationToken = default) { var img = Image.Load(stream); img.Mutate(o => { - var size = + var size = o.Resize(new ResizeOptions { Position = AnchorPositionMode.Center, @@ -120,21 +118,22 @@ public class ThumbnailService(IMongoDatabase db, AobaService aobaService) return result; } - public async Task> GenerateVideoThumbnailAsync(Stream data, ThumbnailSize size, CancellationToken cancellationToken = default) + public Maybe GenerateVideoThumbnail(Stream data, ThumbnailSize size, CancellationToken cancellationToken = default) { var w = (int)size; var source = new MemoryStream(); data.CopyTo(source); source.Position = 0; var output = new MemoryStream(); - await FFMpegArguments.FromPipeInput(new StreamPipeSource(source)) - .OutputToPipe(new StreamPipeSink(output), opt => - { - opt.WithCustomArgument($"-t 5 -vf \"crop='min(in_w,in_h)':'min(in_w,in_h)',scale={w}:{w}\" -loop 0") - .ForceFormat("webp"); - }).ProcessAsynchronously(); + FFMpegArguments.FromPipeInput(new StreamPipeSource(source), opt => + { + opt.WithCustomArgument("-t 5"); + }).OutputToPipe(new StreamPipeSink(output), opt => + { + opt.WithCustomArgument($"-vf \"crop='min(in_w,in_h)':'min(in_w,in_h)',scale={w}:{w}\" -loop 0 -r 15") + .ForceFormat("webp"); + }).ProcessSynchronously(); output.Position = 0; - return output; } @@ -142,4 +141,4 @@ public class ThumbnailService(IMongoDatabase db, AobaService aobaService) { return new NotImplementedException(); } -} +} \ No newline at end of file diff --git a/AobaServer/Controllers/MediaController.cs b/AobaServer/Controllers/MediaController.cs index 9c3e1b9..c19bfe4 100644 --- a/AobaServer/Controllers/MediaController.cs +++ b/AobaServer/Controllers/MediaController.cs @@ -24,27 +24,27 @@ public class MediaController(AobaService aobaService, ILogger l return NotFound(); } var mime = MimeTypesMap.GetMimeType(file.Value.FileInfo.Filename); - _ = aobaService.IncrementFileViewCountAsync(id, cancellationToken); + _ = aobaService.IncrementViewCountAsync(id, cancellationToken); return File(file, mime, true); } /// /// Redirect legacy media urls to the new url /// - /// + /// /// /// /// - [HttpGet("/i/{id}/{*rest}")] - public async Task LegacyRedirectAsync(ObjectId id, string rest, CancellationToken cancellationToken) + [HttpGet("/i/{legacyId}/{*rest}")] + public async Task LegacyRedirectAsync(ObjectId legacyId, string rest, CancellationToken cancellationToken) { - var media = await aobaService.GetMediaAsync(id, cancellationToken); + var media = await aobaService.GetMediaFromLegacyIdAsync(legacyId, cancellationToken); if (media == null) return NotFound(); return LocalRedirectPermanent($"/m/{media.MediaId}/{rest}"); } - [HttpGet("thumb/{id}")] + [HttpGet("{id}/thumb")] [ResponseCache(Duration = int.MaxValue)] public async Task ThumbAsync(ObjectId id, [FromServices] ThumbnailService thumbnailService, [FromQuery] ThumbnailSize size = ThumbnailSize.Medium, CancellationToken cancellationToken = default) { @@ -57,6 +57,15 @@ public class MediaController(AobaService aobaService, ILogger l return File(thumb, "image/webp", true); } + [HttpGet("/t/{id}")] + public async Task ThumbAsync(ObjectId id, [FromServices] ThumbnailService thumbnailService, CancellationToken cancellationToken = default) + { + var thumb = await thumbnailService.GetThumbnailByFileIdAsync(id, cancellationToken); + if(thumb == null) + return NotFound(); + return File(thumb, "image/webp", true); + } + [NonAction] private IActionResult DefaultThumbnailAsync() { diff --git a/AobaServer/Proto/Aoba.proto b/AobaServer/Proto/Aoba.proto index b196f5f..04b86c8 100644 --- a/AobaServer/Proto/Aoba.proto +++ b/AobaServer/Proto/Aoba.proto @@ -60,12 +60,12 @@ message UserModel { message MediaModel { Id id = 1; - Id mediaId = 2; - string fileName = 3; - MediaType mediaType = 4; - string ext = 5; - int32 viewCount = 6; - Id owner = 7; + string fileName = 2; + MediaType mediaType = 3; + string ext = 4; + int32 viewCount = 5; + Id owner = 6; + string thumbUrl = 7; } enum MediaType { diff --git a/AobaServer/Services/AobaRpcService.cs b/AobaServer/Services/AobaRpcService.cs index 0643643..c5b4132 100644 --- a/AobaServer/Services/AobaRpcService.cs +++ b/AobaServer/Services/AobaRpcService.cs @@ -20,7 +20,7 @@ public class AobaRpcService(AobaService aobaService, AccountsService accountsSer { public override async Task GetMedia(Id request, ServerCallContext context) { - var media = await aobaService.GetMediaAsync(request.ToObjectId()); + var media = await aobaService.GetMediaFromLegacyIdAsync(request.ToObjectId()); return media.ToResponse(); } diff --git a/AobaServer/Utils/ProtoExtensions.cs b/AobaServer/Utils/ProtoExtensions.cs index 7ae1de4..f7bd73a 100644 --- a/AobaServer/Utils/ProtoExtensions.cs +++ b/AobaServer/Utils/ProtoExtensions.cs @@ -44,15 +44,18 @@ public static class ProtoExtensions public static MediaModel ToMediaModel(this Media media) { + var thumbUrl = $"/m/{media.MediaId}/thumb?size={ThumbnailSize.Medium}"; + if (media.Thumbnails.TryGetValue(ThumbnailSize.Medium, out var thumb)) + thumbUrl = $"/t/{thumb}"; return new MediaModel() { Ext = media.Ext, FileName = media.Filename, - Id = media.Id.ToId(), - MediaId = media.MediaId.ToId(), + Id = media.MediaId.ToId(), MediaType = (Aoba.RPC.MediaType)media.MediaType, Owner = media.Owner.ToId(), ViewCount = media.ViewCount, + ThumbUrl = thumbUrl, }; }