Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 165adb2775 | |||
| e832ccf07e | |||
| 6df6de098b | |||
| 364b23e62a | |||
| 7b2ed32043 | |||
| d094f7bbef | |||
| f98429159f | |||
| 3dca408356 | |||
| 1655e342b7 | |||
| 7223c35658 | |||
| b85bcd1c7a | |||
| 8536d335bc | |||
| b34f7436c2 | |||
| 8b5803c085 | |||
| 5e2a7b53c9 | |||
| d88cfa2cd3 |
46
.github/workflows/.woodpecker.yaml
vendored
Normal file
46
.github/workflows/.woodpecker.yaml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
pipeline:
|
||||
build-and-push:
|
||||
image: plugins/docker
|
||||
settings:
|
||||
dockerfile: AobaServer/Dockerfile
|
||||
context: .
|
||||
repo: git.kaisei.app/amatsugu/aoba
|
||||
tags: latest
|
||||
username:
|
||||
from_secret: docker_user
|
||||
password:
|
||||
from_secret: docker_pass
|
||||
|
||||
deploy:
|
||||
image: appleboy/ssh
|
||||
settings:
|
||||
host: your-app-host.internal
|
||||
username: deploy
|
||||
key:
|
||||
from_secret: ssh_key
|
||||
port: 22
|
||||
script:
|
||||
- docker pull git.kaisei.app/amatsugu/aoba:latest
|
||||
|
||||
# Run temporary container on docker network, no port binding
|
||||
- docker run -d --rm \
|
||||
--name aoba-temp \
|
||||
--network aoba-net \
|
||||
git.kaisei.app/amatsugu/aoba:latest
|
||||
|
||||
# Wait for it to become healthy
|
||||
- sleep 3
|
||||
- curl -f http://aoba-temp:8080 --connect-timeout 2 || (echo "Health check failed" && docker stop aoba-temp && exit 1)
|
||||
|
||||
# Stop old container (bound to host port)
|
||||
- docker stop aoba || true
|
||||
|
||||
# Start new container on network and bind to host port 8080
|
||||
- docker run -d --rm \
|
||||
--name aoba \
|
||||
--network aoba-net \
|
||||
-p 9432:8080 \
|
||||
git.kaisei.app/amatsugu/aoba:latest
|
||||
|
||||
# Stop temp container
|
||||
- docker stop aoba-temp || true
|
||||
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@@ -33,5 +33,7 @@ jobs:
|
||||
file: AobaServer/Dockerfile
|
||||
context: .
|
||||
push: true
|
||||
tags: git.kaisei.app/amatsugu/aoba:${{ env.VERSION }}
|
||||
tags: |
|
||||
git.kaisei.app/amatsugu/aoba:${{ env.VERSION }}
|
||||
git.kaisei.app/amatsugu/aoba:latest
|
||||
build-args: VERSION=${{ env.VERSION }}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
$mainBGColor: #584577;
|
||||
$featureColor: #ce2d4f;
|
||||
$focusColor: #ff3862;
|
||||
$accentColor: #f0eaf8;
|
||||
|
||||
$mainTextColor: #eee;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
@import "colors";
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
scrollbar-color: $accentColor transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
:root {
|
||||
@@ -18,6 +20,7 @@
|
||||
.stickyTop {
|
||||
top: 0;
|
||||
position: sticky;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -34,6 +37,8 @@ body {
|
||||
#content {
|
||||
grid-area: Content;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
height: 100dvh;
|
||||
padding: 10px;
|
||||
/* margin-left: $navBarSize; */
|
||||
}
|
||||
@@ -152,3 +157,41 @@ form {
|
||||
padding: 5px;
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
.contextMenu {
|
||||
backdrop-filter: blur(10px) brightness(0.5) grayscale(1);
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
|
||||
.itemList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.contextItem {
|
||||
border-left: 4px solid $featureColor;
|
||||
$size: 30px;
|
||||
display: grid;
|
||||
grid-template-columns: $size 1fr auto;
|
||||
height: $size;
|
||||
transition: border 0.1s ease-out;
|
||||
cursor: default;
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $accentColor;
|
||||
color: $invertTextColor;
|
||||
border-left: 10px solid $focusColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ pub struct InputProps {
|
||||
pub label: Option<String>,
|
||||
pub placeholder: Option<String>,
|
||||
pub name: String,
|
||||
pub oninput: Option<EventHandler<FormEvent>>,
|
||||
pub oninput: Option<EventHandler<Event<FormData>>>,
|
||||
pub required: Option<bool>,
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ pub fn Input(props: InputProps) -> Element {
|
||||
if let Some(mut s) = props.value {
|
||||
s.set(e.value());
|
||||
}
|
||||
if let Some(handler) = props.oninput{
|
||||
handler.call(e);
|
||||
}
|
||||
},
|
||||
name: props.name,
|
||||
placeholder: ph,
|
||||
|
||||
97
AobaClient/src/components/context_menu.rs
Normal file
97
AobaClient/src/components/context_menu.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use core::str;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
mod props {
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(PartialEq, Clone, Props)]
|
||||
pub struct ContextMenu {
|
||||
pub top: f64,
|
||||
pub left: f64,
|
||||
pub items: Element,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Props, Default)]
|
||||
pub struct ContextMenuItem {
|
||||
pub name: String,
|
||||
pub sub_items: Option<Element>,
|
||||
pub onclick: Option<EventHandler<MouseEvent>>,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct ContextMenuRenderer {
|
||||
pub menu: Signal<Option<Element>>,
|
||||
}
|
||||
|
||||
impl ContextMenuRenderer {
|
||||
pub fn close(&mut self) {
|
||||
self.menu.set(None);
|
||||
}
|
||||
|
||||
pub fn render(&self) -> Element {
|
||||
if let Some(menu) = self.menu.cloned() {
|
||||
rsx! {
|
||||
{menu}
|
||||
}
|
||||
} else {
|
||||
rsx! {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ContextMenuRoot() -> Element {
|
||||
let renderer = use_context::<ContextMenuRenderer>();
|
||||
rsx! {
|
||||
{renderer.render()}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ContextMenu(props: props::ContextMenu) -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
class: "contextMenu",
|
||||
style: "left: {props.left}px; top: {props.top}px;",
|
||||
ItemList { items: props.items }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ItemList(items: Element) -> Element {
|
||||
rsx! {
|
||||
div{
|
||||
class: "itemList",
|
||||
{items}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ContextMenuItem(props: props::ContextMenuItem) -> Element {
|
||||
let mut renderer = use_context::<ContextMenuRenderer>();
|
||||
if let Some(_sub) = props.sub_items {
|
||||
todo!("Sub Menu");
|
||||
}
|
||||
rsx! {
|
||||
div{
|
||||
onclick: move |e|{
|
||||
if let Some(handler) = props.onclick{
|
||||
handler.call(e);
|
||||
}
|
||||
renderer.close();
|
||||
},
|
||||
class: "contextItem",
|
||||
div {
|
||||
class: "icon"
|
||||
},
|
||||
div {
|
||||
class: "label",
|
||||
{props.name}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,8 +51,11 @@ pub fn MediaGrid(props: MediaGridProps) -> Element {
|
||||
Ok(result) => rsx! {
|
||||
div {
|
||||
class: "mediaGrid",
|
||||
// oncontextmenu: oncontext,
|
||||
{result.items.iter().map(|itm| rsx!{
|
||||
MediaItem { item: Some(itm.clone()) }
|
||||
MediaItem {
|
||||
item: itm.clone()
|
||||
}
|
||||
})},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,21 +1,73 @@
|
||||
use dioxus::prelude::*;
|
||||
use web_sys::window;
|
||||
|
||||
use crate::{HOST, rpc::aoba::MediaModel};
|
||||
use crate::{
|
||||
HOST,
|
||||
components::{ContextMenu, ContextMenuItem, ContextMenuRenderer},
|
||||
rpc::aoba::MediaModel,
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Clone, Props)]
|
||||
pub struct MediaItemProps {
|
||||
pub item: Option<MediaModel>,
|
||||
// pub oncontextmenu: Option<EventHandler<Event<MouseData>>>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn MediaItem(props: MediaItemProps) -> Element {
|
||||
let mut ct_renderer = use_context::<ContextMenuRenderer>();
|
||||
|
||||
if let Some(item) = props.item {
|
||||
let mtype = item.media_type().as_str_name();
|
||||
let filename = item.file_name;
|
||||
let id = item.id.unwrap().value;
|
||||
let thumb = item.thumb_url;
|
||||
let url = item.media_url;
|
||||
let download = format!("{HOST}{url}");
|
||||
|
||||
let oncontext = move |event: Event<MouseData>| {
|
||||
println!("ContextMenu");
|
||||
event.prevent_default();
|
||||
event.stop_propagation();
|
||||
let data = event.data();
|
||||
if data.modifiers().ctrl() {
|
||||
return;
|
||||
}
|
||||
let pos = data.coordinates().client();
|
||||
let left = pos.x;
|
||||
let top = pos.y;
|
||||
let download = download.clone();
|
||||
|
||||
let menu: Element = rsx! {
|
||||
ContextMenu {
|
||||
left: left,
|
||||
top: top,
|
||||
items: rsx! {
|
||||
ContextMenuItem {
|
||||
name: "Details",
|
||||
},
|
||||
ContextMenuItem {
|
||||
name: "Download",
|
||||
onclick: move |_|{
|
||||
_ = window().unwrap().open_with_url_and_target(&download, "_blank");
|
||||
}
|
||||
},
|
||||
ContextMenuItem {
|
||||
name: "Delete",
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
ct_renderer.menu.set(Some(menu));
|
||||
};
|
||||
|
||||
return rsx! {
|
||||
a { class: "mediaItem", href: "{HOST}/m/{id}", target: "_blank",
|
||||
a {
|
||||
class: "mediaItem",
|
||||
href: "{HOST}{url}",
|
||||
target: "_blank",
|
||||
oncontextmenu: oncontext,
|
||||
"data-id" : id,
|
||||
img { src: "{HOST}{thumb}" }
|
||||
span { class: "info",
|
||||
span { class: "name", "{filename}" }
|
||||
@@ -24,7 +76,7 @@ pub fn MediaItem(props: MediaItemProps) -> Element {
|
||||
span { "{item.view_count}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return rsx! {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
pub mod basic;
|
||||
mod context_menu;
|
||||
mod media_grid;
|
||||
mod media_item;
|
||||
mod metrics_token;
|
||||
mod navbar;
|
||||
mod notif;
|
||||
mod search;
|
||||
pub use context_menu::*;
|
||||
pub use media_grid::*;
|
||||
pub use media_item::*;
|
||||
pub use metrics_token::*;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::{Route, components::Navbar, contexts::AuthContext, views::Login};
|
||||
use crate::{
|
||||
Route,
|
||||
components::{ContextMenuRenderer, ContextMenuRoot, Navbar},
|
||||
contexts::AuthContext,
|
||||
views::Login,
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn MainLayout() -> Element {
|
||||
@@ -12,8 +17,20 @@ pub fn MainLayout() -> Element {
|
||||
};
|
||||
}
|
||||
|
||||
let mut ct_renderer = use_context::<ContextMenuRenderer>();
|
||||
|
||||
return rsx! {
|
||||
ContextMenuRoot { }
|
||||
Navbar { }
|
||||
div { id: "content", Outlet::<Route> {} }
|
||||
div {
|
||||
id: "content",
|
||||
onclick: move |_| {
|
||||
ct_renderer.close();
|
||||
},
|
||||
oncontextmenu: move |_| {
|
||||
ct_renderer.close();
|
||||
},
|
||||
Outlet::<Route> { }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ use contexts::AuthContext;
|
||||
use dioxus::prelude::*;
|
||||
use route::Route;
|
||||
|
||||
use crate::components::ContextMenuRenderer;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub const HOST: &'static str = "http://localhost:8081";
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -30,7 +32,8 @@ fn main() {
|
||||
|
||||
#[component]
|
||||
fn App() -> Element {
|
||||
let _auth_state = use_context_provider(|| AuthContext::new());
|
||||
use_context_provider(|| AuthContext::new());
|
||||
use_context_provider(|| ContextMenuRenderer::default());
|
||||
rsx! {
|
||||
document::Link { rel: "icon", href: FAVICON }
|
||||
document::Link { rel: "preconnect", href: "https://fonts.googleapis.com" }
|
||||
@@ -41,6 +44,7 @@ fn App() -> Element {
|
||||
rel: "stylesheet",
|
||||
href: "https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap",
|
||||
}
|
||||
|
||||
Router::<Route> { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FFMpegCore" Version="5.2.0" />
|
||||
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.7" />
|
||||
<PackageReference Include="MaybeError" Version="1.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.6" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.4.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.7" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.4.1" />
|
||||
<PackageReference Include="MongoDB.Driver.Core.Extensions.DiagnosticSources" Version="2.1.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.6" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.12.1" />
|
||||
<PackageReference Include="ZLinq" Version="1.4.12" />
|
||||
<PackageReference Include="ZLinq" Version="1.5.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
using SixLabors.ImageSharp;
|
||||
|
||||
namespace AobaCore.Models;
|
||||
|
||||
[BsonIgnoreExtraElements]
|
||||
@@ -16,23 +18,27 @@ public class Media
|
||||
public ObjectId Owner { get; set; }
|
||||
public DateTime UploadDate { get; set; }
|
||||
public string[] Tags { get; set; } = [];
|
||||
public Size? Dimensions { get; set; }
|
||||
public Dictionary<ThumbnailSize, ObjectId> Thumbnails { get; set; } = [];
|
||||
|
||||
|
||||
public static readonly Dictionary<string, MediaType> KnownTypes = new()
|
||||
{
|
||||
{ ".jpg", MediaType.Image },
|
||||
{ ".avif", MediaType.Image },
|
||||
{ ".jpeg", MediaType.Image },
|
||||
{ ".jxr", MediaType.Image },
|
||||
{ ".avif", MediaType.Image },
|
||||
{ ".png", MediaType.Image },
|
||||
{ ".apng", MediaType.Image },
|
||||
{ ".webp", MediaType.Image },
|
||||
{ ".qoi", MediaType.Image },
|
||||
{ ".ico", MediaType.Image },
|
||||
{ ".gif", MediaType.Image },
|
||||
{ ".mp3", MediaType.Audio },
|
||||
{ ".flac", MediaType.Audio },
|
||||
{ ".alac", MediaType.Audio },
|
||||
{ ".mp4", MediaType.Video },
|
||||
{ ".m4v", MediaType.Video },
|
||||
{ ".webm", MediaType.Video },
|
||||
{ ".mov", MediaType.Video },
|
||||
{ ".avi", MediaType.Video },
|
||||
@@ -50,6 +56,10 @@ public class Media
|
||||
{ ".xml", MediaType.Code },
|
||||
{ ".json", MediaType.Code },
|
||||
{ ".py", MediaType.Code },
|
||||
{ ".rs", MediaType.Code },
|
||||
{ ".zed", MediaType.Code },
|
||||
{ ".ts", MediaType.Code },
|
||||
{ ".astro", MediaType.Code },
|
||||
};
|
||||
|
||||
[BsonConstructor]
|
||||
@@ -75,7 +85,7 @@ public class Media
|
||||
{
|
||||
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}"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ using AobaCore.Models;
|
||||
|
||||
using MaybeError.Errors;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using MongoDB.Driver.GridFS;
|
||||
@@ -69,6 +71,7 @@ public class AobaService(IMongoDatabase db)
|
||||
{
|
||||
var fileId = await _gridFs.UploadFromStreamAsync(filename, data, cancellationToken: cancellationToken);
|
||||
var media = new Media(fileId, filename, owner);
|
||||
|
||||
await AddMediaAsync(media, cancellationToken);
|
||||
return media;
|
||||
}
|
||||
@@ -76,6 +79,10 @@ public class AobaService(IMongoDatabase db)
|
||||
{
|
||||
return ex;
|
||||
}
|
||||
finally
|
||||
{
|
||||
data.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<MaybeEx<GridFSDownloadStream, GridFSException>> GetFileStreamAsync(ObjectId mediaId, bool seekable = false, CancellationToken cancellationToken = default)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
using FFMpegCore;
|
||||
using FFMpegCore.Pipes;
|
||||
|
||||
|
||||
using MaybeError.Errors;
|
||||
|
||||
using MongoDB.Bson;
|
||||
@@ -10,6 +11,7 @@ using MongoDB.Driver;
|
||||
using MongoDB.Driver.GridFS;
|
||||
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
using System;
|
||||
@@ -24,7 +26,6 @@ namespace AobaCore.Services;
|
||||
public class ThumbnailService(IMongoDatabase db, AobaService aobaService)
|
||||
{
|
||||
private readonly GridFSBucket _gridfs = new GridFSBucket(db);
|
||||
private Lock _lock = new();
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
@@ -92,17 +93,29 @@ public class ThumbnailService(IMongoDatabase db, AobaService aobaService)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
MediaType.Image => await GenerateImageThumbnailAsync(stream, size, cancellationToken),
|
||||
MediaType.Image => await GenerateImageThumbnailAsync(stream, size, ext, cancellationToken),
|
||||
MediaType.Video => GenerateVideoThumbnail(stream, size, cancellationToken),
|
||||
MediaType.Text or MediaType.Code => await GenerateDocumentThumbnailAsync(stream, size, cancellationToken),
|
||||
_ => new Error($"No Thumbnail for {type}"),
|
||||
};
|
||||
}
|
||||
|
||||
public static async Task<Stream> GenerateImageThumbnailAsync(Stream stream, ThumbnailSize size, CancellationToken cancellationToken = default)
|
||||
private static Maybe<Image> LoadImage(Stream stream, string ext)
|
||||
{
|
||||
var img = Image.Load(stream);
|
||||
img.Mutate(o =>
|
||||
if (ext is ".heif" or ".avif")
|
||||
{
|
||||
return new Error("Unsupported image type");
|
||||
}
|
||||
else
|
||||
return Image.Load(stream);
|
||||
}
|
||||
|
||||
public static async Task<Maybe<Stream>> GenerateImageThumbnailAsync(Stream stream, ThumbnailSize size, string ext, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var img = LoadImage(stream, ext);
|
||||
if (img.HasError)
|
||||
return img.Error;
|
||||
img.Value.Mutate(o =>
|
||||
{
|
||||
var size =
|
||||
o.Resize(new ResizeOptions
|
||||
@@ -113,7 +126,8 @@ public class ThumbnailService(IMongoDatabase db, AobaService aobaService)
|
||||
});
|
||||
});
|
||||
var result = new MemoryStream();
|
||||
await img.SaveAsWebpAsync(result, cancellationToken);
|
||||
await img.Value.SaveAsWebpAsync(result, cancellationToken);
|
||||
img.Value.Dispose();
|
||||
result.Position = 0;
|
||||
return result;
|
||||
}
|
||||
@@ -121,26 +135,36 @@ public class ThumbnailService(IMongoDatabase db, AobaService aobaService)
|
||||
public Maybe<Stream> GenerateVideoThumbnail(Stream data, ThumbnailSize size, CancellationToken cancellationToken = default)
|
||||
{
|
||||
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);
|
||||
source.Position = 0;
|
||||
source.Flush();
|
||||
source.Dispose();
|
||||
data.Dispose();
|
||||
try
|
||||
{
|
||||
var output = new MemoryStream();
|
||||
FFMpegArguments.FromPipeInput(new StreamPipeSource(source), 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");
|
||||
}).Configure(cfg =>
|
||||
{
|
||||
#if !DEBUG
|
||||
cfg.BinaryFolder = "/usr/bin";
|
||||
#endif
|
||||
}).ProcessSynchronously();
|
||||
output.Position = 0;
|
||||
return output;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ex;
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Maybe<Stream>> GenerateDocumentThumbnailAsync(Stream data, ThumbnailSize size, CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.12.1" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||
<PackageReference Include="MimeTypesMap" Version="1.0.9" />
|
||||
|
||||
@@ -22,6 +22,7 @@ public class MediaApi(AobaService aoba) : ControllerBase
|
||||
if (media.HasError)
|
||||
return Problem(detail: media.Error.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
media = media.Value,
|
||||
|
||||
@@ -14,10 +14,11 @@ namespace AobaServer.Controllers;
|
||||
public class MediaController(AobaService aobaService, ILogger<MediaController> logger) : Controller
|
||||
{
|
||||
[HttpGet("{id}")]
|
||||
[HttpGet("{id}/*")]
|
||||
[ResponseCache(Duration = int.MaxValue)]
|
||||
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)
|
||||
{
|
||||
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}")]
|
||||
[ResponseCache(Duration = int.MaxValue)]
|
||||
public async Task<IActionResult> ThumbAsync(ObjectId id, [FromServices] ThumbnailService thumbnailService, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var thumb = await thumbnailService.GetThumbnailByFileIdAsync(id, cancellationToken);
|
||||
|
||||
@@ -82,6 +82,7 @@ message MediaModel {
|
||||
int32 viewCount = 5;
|
||||
Id owner = 6;
|
||||
string thumbUrl = 7;
|
||||
string mediaUrl = 8;
|
||||
}
|
||||
|
||||
enum MediaType {
|
||||
@@ -99,3 +100,14 @@ message ShareXResponse {
|
||||
string error = 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
message SearchQuery {
|
||||
optional string queryText = 1;
|
||||
repeated Filter filters = 2;
|
||||
}
|
||||
|
||||
message Filter {
|
||||
string key = 1;
|
||||
repeated string values = 2;
|
||||
}
|
||||
@@ -56,6 +56,7 @@ public static class ProtoExtensions
|
||||
Owner = media.Owner.ToId(),
|
||||
ViewCount = media.ViewCount,
|
||||
ThumbUrl = thumbUrl,
|
||||
MediaUrl = media.GetMediaUrl()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user