10 Commits

Author SHA1 Message Date
b85bcd1c7a Add media url to MediaModel+Start work on tonenized search query model
Some checks failed
Build and Push Image / build-and-push (push) Has been cancelled
2025-07-12 18:10:21 -04:00
8536d335bc Make Search Sticky 2025-07-12 13:52:32 -04:00
b34f7436c2 fix cirucular deps
Some checks failed
Build and Push Image / build-and-push (push) Has been cancelled
added additonal file types
2025-07-09 21:08:10 -04:00
8b5803c085 Make media seekable
Some checks failed
Build and Push Image / build-and-push (push) Has been cancelled
2025-07-09 20:48:55 -04:00
5e2a7b53c9 Added additional media types
Some checks failed
Build and Push Image / build-and-push (push) Has been cancelled
2025-07-09 20:30:45 -04:00
d88cfa2cd3 Fixes to video thumb generation
Some checks failed
Build and Push Image / build-and-push (push) Has been cancelled
2025-07-09 20:06:59 -04:00
cd30876beb Fixed user selection
Some checks failed
Build and Push Image / build-and-push (push) Has been cancelled
2025-07-09 17:44:49 -04:00
544645b73b Fixed ffmpeg install
Some checks failed
Build and Push Image / build-and-push (push) Has been cancelled
2025-07-09 17:21:11 -04:00
76be6dc8e7 Fix Auth info
Some checks failed
Build and Push Image / build-and-push (push) Has been cancelled
2025-07-06 01:43:14 -04:00
bc8a4414cd Fix login
All checks were successful
Build and Push Image / build-and-push (push) Successful in 5m23s
2025-07-06 01:37:32 -04:00
12 changed files with 78 additions and 28 deletions

View File

@@ -2,6 +2,8 @@
@import "colors"; @import "colors";
* { * {
box-sizing: border-box; box-sizing: border-box;
scrollbar-color: $accentColor transparent;
scrollbar-width: thin;
} }
:root { :root {
@@ -18,6 +20,7 @@
.stickyTop { .stickyTop {
top: 0; top: 0;
position: sticky; position: sticky;
z-index: 100;
} }
body { body {
@@ -34,6 +37,8 @@ body {
#content { #content {
grid-area: Content; grid-area: Content;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto;
height: 100dvh;
padding: 10px; padding: 10px;
/* margin-left: $navBarSize; */ /* margin-left: $navBarSize; */
} }

View File

@@ -14,8 +14,13 @@ pub fn MediaItem(props: MediaItemProps) -> Element {
let filename = item.file_name; let filename = item.file_name;
let id = item.id.unwrap().value; let id = item.id.unwrap().value;
let thumb = item.thumb_url; let thumb = item.thumb_url;
let url = item.media_url;
return rsx! { return rsx! {
a { class: "mediaItem", href: "{HOST}/m/{id}", target: "_blank", a {
class: "mediaItem",
href: "{HOST}/{url}",
target: "_blank",
"data-id" : id,
img { src: "{HOST}{thumb}" } img { src: "{HOST}{thumb}" }
span { class: "info", span { class: "info",
span { class: "name", "{filename}" } span { class: "name", "{filename}" }

View File

@@ -1,6 +1,8 @@
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Attributes;
using SixLabors.ImageSharp;
namespace AobaCore.Models; namespace AobaCore.Models;
[BsonIgnoreExtraElements] [BsonIgnoreExtraElements]
@@ -16,23 +18,27 @@ public class Media
public ObjectId Owner { get; set; } public ObjectId Owner { get; set; }
public DateTime UploadDate { get; set; } public DateTime UploadDate { get; set; }
public string[] Tags { get; set; } = []; public string[] Tags { get; set; } = [];
public Size? Dimensions { get; set; }
public Dictionary<ThumbnailSize, ObjectId> Thumbnails { get; set; } = []; public Dictionary<ThumbnailSize, ObjectId> Thumbnails { get; set; } = [];
public static readonly Dictionary<string, MediaType> KnownTypes = new() public static readonly Dictionary<string, MediaType> KnownTypes = new()
{ {
{ ".jpg", MediaType.Image }, { ".jpg", MediaType.Image },
{ ".avif", MediaType.Image },
{ ".jpeg", MediaType.Image }, { ".jpeg", MediaType.Image },
{ ".jxr", MediaType.Image },
{ ".avif", MediaType.Image },
{ ".png", MediaType.Image }, { ".png", MediaType.Image },
{ ".apng", MediaType.Image }, { ".apng", MediaType.Image },
{ ".webp", MediaType.Image }, { ".webp", MediaType.Image },
{ ".qoi", MediaType.Image },
{ ".ico", MediaType.Image }, { ".ico", MediaType.Image },
{ ".gif", MediaType.Image }, { ".gif", MediaType.Image },
{ ".mp3", MediaType.Audio }, { ".mp3", MediaType.Audio },
{ ".flac", MediaType.Audio }, { ".flac", MediaType.Audio },
{ ".alac", MediaType.Audio }, { ".alac", MediaType.Audio },
{ ".mp4", MediaType.Video }, { ".mp4", MediaType.Video },
{ ".m4v", MediaType.Video },
{ ".webm", MediaType.Video }, { ".webm", MediaType.Video },
{ ".mov", MediaType.Video }, { ".mov", MediaType.Video },
{ ".avi", MediaType.Video }, { ".avi", MediaType.Video },
@@ -50,6 +56,10 @@ public class Media
{ ".xml", MediaType.Code }, { ".xml", MediaType.Code },
{ ".json", MediaType.Code }, { ".json", MediaType.Code },
{ ".py", MediaType.Code }, { ".py", MediaType.Code },
{ ".rs", MediaType.Code },
{ ".zed", MediaType.Code },
{ ".ts", MediaType.Code },
{ ".astro", MediaType.Code },
}; };
[BsonConstructor] [BsonConstructor]
@@ -75,7 +85,7 @@ public class Media
{ {
return this switch return this switch
{ {
//Media { MediaType: MediaType.Raw or MediaType.Text or MediaType.Code} => $"/i/dl/{MediaId}/{Filename}", Media { MediaType: MediaType.Raw or MediaType.Text or MediaType.Code } => $"/m/{MediaId}/{Uri.EscapeDataString(Filename)}",
_ => $"/m/{MediaId}" _ => $"/m/{MediaId}"
}; };
} }

View File

@@ -2,6 +2,8 @@ using AobaCore.Models;
using MaybeError.Errors; using MaybeError.Errors;
using Microsoft.Extensions.Logging;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using MongoDB.Driver.GridFS; using MongoDB.Driver.GridFS;
@@ -69,6 +71,7 @@ public class AobaService(IMongoDatabase db)
{ {
var fileId = await _gridFs.UploadFromStreamAsync(filename, data, cancellationToken: cancellationToken); var fileId = await _gridFs.UploadFromStreamAsync(filename, data, cancellationToken: cancellationToken);
var media = new Media(fileId, filename, owner); var media = new Media(fileId, filename, owner);
await AddMediaAsync(media, cancellationToken); await AddMediaAsync(media, cancellationToken);
return media; return media;
} }

View File

@@ -24,7 +24,6 @@ namespace AobaCore.Services;
public class ThumbnailService(IMongoDatabase db, AobaService aobaService) public class ThumbnailService(IMongoDatabase db, AobaService aobaService)
{ {
private readonly GridFSBucket _gridfs = new GridFSBucket(db); private readonly GridFSBucket _gridfs = new GridFSBucket(db);
private Lock _lock = new();
/// <summary> /// <summary>
/// ///
@@ -121,29 +120,39 @@ public class ThumbnailService(IMongoDatabase db, AobaService aobaService)
public Maybe<Stream> GenerateVideoThumbnail(Stream data, ThumbnailSize size, CancellationToken cancellationToken = default) public Maybe<Stream> GenerateVideoThumbnail(Stream data, ThumbnailSize size, CancellationToken cancellationToken = default)
{ {
var w = (int)size; var w = (int)size;
var source = new MemoryStream(); var fn = ObjectId.GenerateNewId().ToString();
var filePath = $"/tmp/{fn}.in";
using var source = new FileStream(filePath, FileMode.CreateNew);
data.CopyTo(source); data.CopyTo(source);
source.Position = 0; source.Flush();
var output = new MemoryStream(); source.Dispose();
FFMpegArguments.FromPipeInput(new StreamPipeSource(source), opt => data.Dispose();
try
{ {
opt.WithCustomArgument("-t 5"); var output = new MemoryStream();
}).OutputToPipe(new StreamPipeSink(output), opt => FFMpegArguments.FromFileInput(filePath, false, 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;
}
catch(Exception ex)
{ {
opt.WithCustomArgument($"-vf \"crop='min(in_w,in_h)':'min(in_w,in_h)',scale={w}:{w}\" -loop 0 -r 15") return ex;
.ForceFormat("webp"); }
}).Configure(cfg => finally
{ {
#if !DEBUG File.Delete(filePath);
cfg.BinaryFolder = "/usr/bin"; }
#endif
}).ProcessSynchronously();
output.Position = 0;
return output;
} }
public async Task<Maybe<Stream>> GenerateDocumentThumbnailAsync(Stream data, ThumbnailSize size, CancellationToken cancellationToken = default) public async Task<Maybe<Stream>> GenerateDocumentThumbnailAsync(Stream data, ThumbnailSize size, CancellationToken cancellationToken = default)
{ {
return new NotImplementedException(); return new NotImplementedException();
} }
} }

View File

@@ -14,10 +14,11 @@ namespace AobaServer.Controllers;
public class MediaController(AobaService aobaService, ILogger<MediaController> logger) : Controller public class MediaController(AobaService aobaService, ILogger<MediaController> logger) : Controller
{ {
[HttpGet("{id}")] [HttpGet("{id}")]
[HttpGet("{id}/*")]
[ResponseCache(Duration = int.MaxValue)] [ResponseCache(Duration = int.MaxValue)]
public async Task<IActionResult> MediaAsync(ObjectId id, [FromServices] MongoClient client, CancellationToken cancellationToken) public async Task<IActionResult> MediaAsync(ObjectId id, [FromServices] MongoClient client, CancellationToken cancellationToken)
{ {
var file = await aobaService.GetFileStreamAsync(id, cancellationToken: cancellationToken); var file = await aobaService.GetFileStreamAsync(id, seekable: true, cancellationToken: cancellationToken);
if (file.HasError) if (file.HasError)
{ {
logger.LogError(file.Error.Exception, "Failed to load media stream"); logger.LogError(file.Error.Exception, "Failed to load media stream");
@@ -58,6 +59,7 @@ public class MediaController(AobaService aobaService, ILogger<MediaController> l
} }
[HttpGet("/t/{id}")] [HttpGet("/t/{id}")]
[ResponseCache(Duration = int.MaxValue)]
public async Task<IActionResult> ThumbAsync(ObjectId id, [FromServices] ThumbnailService thumbnailService, CancellationToken cancellationToken = default) public async Task<IActionResult> ThumbAsync(ObjectId id, [FromServices] ThumbnailService thumbnailService, CancellationToken cancellationToken = default)
{ {
var thumb = await thumbnailService.GetThumbnailByFileIdAsync(id, cancellationToken); var thumb = await thumbnailService.GetThumbnailByFileIdAsync(id, cancellationToken);

View File

@@ -32,13 +32,14 @@ RUN dx bundle --platform web
# Server Build # Server Build
# This stage is used when running from VS in fast mode (Default for Debug configuration) # This stage is used when running from VS in fast mode (Default for Debug configuration)
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
RUN apt-get update && apt-get install -y ffmpeg
USER $APP_UID USER $APP_UID
WORKDIR /app WORKDIR /app
EXPOSE 8080 EXPOSE 8080
EXPOSE 8081 EXPOSE 8081
# This stage is used to build the service project # This stage is used to build the service project
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build FROM mcr.microsoft.com/dotnet/sdk:9.0-noble AS build
ARG BUILD_CONFIGURATION=Release ARG BUILD_CONFIGURATION=Release
WORKDIR /src WORKDIR /src
COPY ["AobaServer/AobaServer.csproj", "AobaServer/"] COPY ["AobaServer/AobaServer.csproj", "AobaServer/"]
@@ -47,7 +48,7 @@ COPY . .
# Copy Built bundle from client builder # Copy Built bundle from client builder
COPY --from=client-builder /app/AobaClient/target/dx/aoba-client/release/web/public /src/AobaServer/wwwroot COPY --from=client-builder /app/AobaClient/target/dx/aoba-client/release/web/public /src/AobaServer/wwwroot
WORKDIR "/src/AobaServer" WORKDIR "/src/AobaServer"
RUN dotnet build "./AobaServer.csproj" -c $BUILD_CONFIGURATION -o /app/build # RUN dotnet build "./AobaServer.csproj" -c $BUILD_CONFIGURATION #-o /app/build
# This stage is used to publish the service project to be copied to the final stage # This stage is used to publish the service project to be copied to the final stage
FROM build AS publish FROM build AS publish
@@ -57,8 +58,8 @@ RUN dotnet publish "./AobaServer.csproj" -c $BUILD_CONFIGURATION -o /app/publish
# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration) # This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
FROM base AS final FROM base AS final
WORKDIR /app WORKDIR /app
COPY --from=publish /app/publish . COPY --from=publish /app/publish .
COPY --from=client-builder /bin/ffmpeg /bin/ffprobe /bin/ffplay /usr/bin/ ARG VERSION
ENV APP_VERSION=$VERSION ENV APP_VERSION=$VERSION
ENTRYPOINT ["dotnet", "AobaServer.dll"] ENTRYPOINT ["dotnet", "AobaServer.dll"]

View File

@@ -82,6 +82,7 @@ message MediaModel {
int32 viewCount = 5; int32 viewCount = 5;
Id owner = 6; Id owner = 6;
string thumbUrl = 7; string thumbUrl = 7;
string mediaUrl = 8;
} }
enum MediaType { enum MediaType {
@@ -99,3 +100,14 @@ message ShareXResponse {
string error = 2; string error = 2;
} }
} }
message SearchQuery {
optional string queryText = 1;
repeated Filter filters = 2;
}
message Filter {
string key = 1;
repeated string values = 2;
}

View File

@@ -14,8 +14,9 @@ using Aoba.RPC;
namespace AobaServer.Services; namespace AobaServer.Services;
public class AobaAuthService(AccountsService accountsService, AuthInfo authInfo) : AuthRpc.AuthRpcBase public class AobaAuthService(AccountsService accountsService, AuthConfigService authConfig) : AuthRpc.AuthRpcBase
{ {
[AllowAnonymous]
public override async Task<LoginResponse> Login(Credentials request, ServerCallContext context) public override async Task<LoginResponse> Login(Credentials request, ServerCallContext context)
{ {
var user = await accountsService.VerifyLoginAsync(request.User, request.Password, context.CancellationToken); var user = await accountsService.VerifyLoginAsync(request.User, request.Password, context.CancellationToken);
@@ -27,6 +28,7 @@ public class AobaAuthService(AccountsService accountsService, AuthInfo authInfo)
Message = "Invalid login credentials" Message = "Invalid login credentials"
} }
}; };
var authInfo = await authConfig.GetDefaultAuthInfoAsync();
var token = user.GetToken(authInfo); var token = user.GetToken(authInfo);
return new LoginResponse return new LoginResponse
{ {

View File

@@ -16,7 +16,7 @@ using System.Text.Json.Serialization;
namespace AobaServer.Services; namespace AobaServer.Services;
public class AobaRpcService(AobaService aobaService, AccountsService accountsService, AuthInfo authInfo) : AobaRpc.AobaRpcBase public class AobaRpcService(AobaService aobaService, AccountsService accountsService, AuthConfigService authConfig) : AobaRpc.AobaRpcBase
{ {
public override async Task<MediaResponse> GetMedia(Id request, ServerCallContext context) public override async Task<MediaResponse> GetMedia(Id request, ServerCallContext context)
{ {
@@ -37,6 +37,7 @@ public class AobaRpcService(AobaService aobaService, AccountsService accountsSer
var user = await accountsService.GetUserAsync(userId, context.CancellationToken); var user = await accountsService.GetUserAsync(userId, context.CancellationToken);
if (user == null) if (user == null)
return new ShareXResponse { Error = "User does not exist" }; return new ShareXResponse { Error = "User does not exist" };
var authInfo = await authConfig.GetDefaultAuthInfoAsync();
var token = user.GetToken(authInfo); var token = user.GetToken(authInfo);
var dest = new ShareXDestination var dest = new ShareXDestination
{ {

View File

@@ -14,7 +14,6 @@ namespace AobaServer.Services;
public class MetricsRpcService(AuthConfigService authConfig): Aoba.RPC.Metrics.MetricsRpc.MetricsRpcBase public class MetricsRpcService(AuthConfigService authConfig): Aoba.RPC.Metrics.MetricsRpc.MetricsRpcBase
{ {
[AllowAnonymous]
public override async Task<Jwt> GetToken(Empty request, ServerCallContext context) public override async Task<Jwt> GetToken(Empty request, ServerCallContext context)
{ {
var authInfo = await authConfig.GetAuthInfoAsync("aoba", "metrics"); var authInfo = await authConfig.GetAuthInfoAsync("aoba", "metrics");

View File

@@ -56,6 +56,7 @@ public static class ProtoExtensions
Owner = media.Owner.ToId(), Owner = media.Owner.ToId(),
ViewCount = media.ViewCount, ViewCount = media.ViewCount,
ThumbUrl = thumbUrl, ThumbUrl = thumbUrl,
MediaUrl = media.GetMediaUrl()
}; };
} }