Skip to content

Commit 8c815e0

Browse files
committed
add support for fetching media info using ffprobe
1 parent b6beb4a commit 8c815e0

File tree

2 files changed

+132
-11
lines changed

2 files changed

+132
-11
lines changed

backend/src/main.rs

+37-9
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use path_dedot::*;
1414
use serde::Deserialize;
1515
use std::net::{IpAddr, Ipv6Addr, SocketAddr};
1616
use std::path::PathBuf;
17+
use std::process::Command;
1718
use std::str::FromStr;
1819
use tokio::io::AsyncWriteExt;
1920
use tower_http::services::ServeDir;
@@ -78,6 +79,7 @@ async fn main() {
7879
.route("/api/listing/*path", get(list_files).post(create_dir))
7980
.route("/api/upload/*path", post(save_request_body))
8081
.route("/api/delete/*path", post(delete_path))
82+
.route("/api/ffprobe/*path", get(ffprobe))
8183
.nest(
8284
"/api/static",
8385
get_service(ServeDir::new(unsafe {
@@ -217,18 +219,15 @@ async fn serve_root() -> impl IntoResponse {
217219
}
218220

219221
async fn list_files(Path(path): Path<String>) -> impl IntoResponse {
220-
let mut abs_path;
221-
unsafe {
222-
abs_path = SERVE_DIR.as_ref().unwrap().to_path_buf();
223-
}
224222
let path = path.trim_start_matches('/');
225-
abs_path.push(path);
223+
let parent_dir = unsafe { SERVE_DIR.as_ref().unwrap() };
224+
let full_path = parent_dir.join(path.trim_start_matches('/'));
226225

227-
log::debug!("list files for path: {:?}", abs_path);
226+
log::debug!("list files for path: {:?}", full_path);
228227

229-
if abs_path.is_dir() {
228+
if full_path.is_dir() {
230229
let mut descendants = vec![];
231-
for entry in WalkDir::new(&abs_path)
230+
for entry in WalkDir::new(&full_path)
232231
.follow_links(true)
233232
.max_depth(1)
234233
.into_iter()
@@ -246,7 +245,7 @@ async fn list_files(Path(path): Path<String>) -> impl IntoResponse {
246245
};
247246

248247
return (StatusCode::OK, Json(dir_desc)).into_response();
249-
} else if abs_path.is_symlink() || abs_path.is_file() {
248+
} else if full_path.is_symlink() || full_path.is_file() {
250249
return Redirect::permanent(format!("/static/{}", path).as_str()).into_response();
251250
}
252251

@@ -287,6 +286,35 @@ fn convert_dir_entry(entry: &walkdir::DirEntry) -> DirEntry {
287286
}
288287
}
289288

289+
async fn ffprobe(Path(path): Path<String>) -> impl IntoResponse {
290+
let path = path.trim_start_matches('/');
291+
let parent_dir = unsafe { SERVE_DIR.as_ref().unwrap() };
292+
let full_path = parent_dir.join(path.trim_start_matches('/'));
293+
294+
if full_path.is_file() {
295+
if let Ok(output) = Command::new("ffprobe")
296+
.args([
297+
"-v",
298+
"quiet",
299+
"-print_format",
300+
"json",
301+
"-show_format",
302+
"-show_streams",
303+
])
304+
.arg(full_path)
305+
.output()
306+
{
307+
let json_str = String::from_utf8(output.stdout).unwrap_or("{}".to_string());
308+
return (StatusCode::OK, Json(json_str)).into_response();
309+
}
310+
}
311+
312+
let json_resp = Json(JsonResponse::Failed {
313+
msg: Some("ffprobe not found".to_string()),
314+
});
315+
(StatusCode::OK, json_resp).into_response()
316+
}
317+
290318
#[derive(Debug)]
291319
struct AppError(String);
292320

frontend/src/main.rs

+95-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use fast_qr::{
88
QRBuilder, Version, ECL,
99
};
1010
use gloo_net::http::Request;
11-
use log::info;
11+
use log::{error, info};
1212
use reqwest::Url;
1313

1414
fn main() {
@@ -63,6 +63,8 @@ fn Listing(cx: Scope) -> Element {
6363
});
6464
}
6565

66+
let info_state = use_state(&cx, || None as Option<String>);
67+
6668
cx.render(match fut.value() {
6769
Some(Ok(dir_desc)) => rsx!(
6870
div {
@@ -76,7 +78,9 @@ fn Listing(cx: Scope) -> Element {
7678
create_dir_state: create_dir_state.clone(),
7779
}
7880

79-
ListingTable{ dir_desc: dir_desc, cur_url: &url, update_state: update_state },
81+
ListingTable{ dir_desc: dir_desc, cur_url: &url, update_state: update_state, info_state: info_state },
82+
83+
InfoDialog { info_state: info_state }
8084
),
8185
Some(Err(err)) => rsx!("Error: {err}"),
8286
_ => rsx!("Loading..."),
@@ -244,6 +248,7 @@ fn ListingTable<'a>(cx: Scope<'a, DirDescProps<'a>>) -> Element {
244248
entry: entry,
245249
cur_path: cur_path,
246250
update_state: cx.props.update_state,
251+
info_state: cx.props.info_state,
247252
qrcode_state: qrcode_state,
248253
}))
249254

@@ -288,6 +293,37 @@ fn TableRow<'a>(cx: Scope<'a, DirEntryProps<'a>>) -> Element {
288293
img { src:"data:image/x-icon;base64,AAABAAEAEBACAAAAAACwAAAAFgAAACgAAAAQAAAAIAAAAAEAAQAAAAAAQAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAA////AAKnAAB6MgAASlIAAEtCAAB7AAAAAnkAAP/YAACDBQAAUGMAAPy/AAACQAAAel4AAEpSAABK0gAAel4AAAJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", alt:"QRCode" }
289294
}
290295

296+
a {
297+
href: "#",
298+
prevent_default: "onclick",
299+
onclick: move |_| {
300+
let path = format!("/api/ffprobe{}/{}", cx.props.cur_path, entry.file_name);
301+
let info_state = cx.props.info_state.clone();
302+
cx.spawn(async move {
303+
let resp = Request::get(path.as_str())
304+
.send()
305+
.await;
306+
307+
match resp {
308+
Ok(resp) => {
309+
let text = resp.text().await.unwrap_or("".to_string());
310+
if text.is_empty() || text == "\"{\\n\\n}\\n\"" {
311+
info_state.set(Some("Not Available!".to_string()));
312+
} else {
313+
let json_resp: serde_json::Value = serde_json::from_str(text.as_str()).unwrap_or(serde_json::Value::default());
314+
let json_str = json_resp.to_string().replace("\\n", "\n").replace("\\", "");
315+
info_state.set(Some(json_str));
316+
}
317+
}
318+
Err(err) => {
319+
error!("failed: {}", err);
320+
}
321+
}
322+
});
323+
},
324+
"ℹ️ ",
325+
}
326+
291327
a {
292328
href: "{api_link}",
293329
if entry.file_type == common::FileType::Directory {
@@ -398,6 +434,61 @@ fn Tooltip<'a>(cx: Scope<'a, TooltipProps<'a>>) -> Element {
398434
// }))
399435
// }
400436

437+
#[inline_props]
438+
fn InfoDialog<'a>(cx: Scope<'a>, info_state: &'a UseState<Option<String>>) -> Element {
439+
cx.render(if let Some(str_info) = info_state.get() {
440+
rsx!(div {
441+
style: "
442+
position: fixed;
443+
width: 860px;
444+
height: 640px;
445+
left: 50%;
446+
margin-left: -430px;
447+
top: 50%;
448+
margin-top: -320px;
449+
z-index: 20;
450+
border-radius: 5px;
451+
border: 2px solid #ccc;
452+
background: #eee;
453+
overflow: scroll;
454+
",
455+
textarea {
456+
style: "
457+
width:850px;
458+
height: 580px;
459+
margin:5px;
460+
border: none;
461+
box-sizing:border-box;
462+
resize: none;
463+
border-bottom: 2px solid #ccc;
464+
",
465+
disabled: "true",
466+
value: "{str_info}",
467+
}
468+
p {
469+
style: "
470+
text-align: center;
471+
width: 100%;
472+
position: absolute;
473+
bottom: 0px;
474+
margin:0px;
475+
box-sizing:border-box;
476+
padding: 10px;
477+
",
478+
input {
479+
style: "width: 200px; height: 30px",
480+
prevent_default: "onclick",
481+
r#type: "button",
482+
value: "Close",
483+
onclick: |_| info_state.set(None),
484+
}
485+
}
486+
})
487+
} else {
488+
rsx!("")
489+
})
490+
}
491+
401492
#[derive(Props)]
402493
struct TooltipProps<'a> {
403494
w: i32,
@@ -412,6 +503,7 @@ pub struct DirDescProps<'a> {
412503
cur_url: &'a Url,
413504
dir_desc: &'a DirDesc,
414505
update_state: &'a UseState<bool>,
506+
info_state: &'a UseState<Option<String>>,
415507
}
416508

417509
#[derive(Props)]
@@ -421,6 +513,7 @@ struct DirEntryProps<'a> {
421513
cur_path: &'a str,
422514
update_state: &'a UseState<bool>,
423515
qrcode_state: &'a UseState<Option<QRCodeParams>>,
516+
info_state: &'a UseState<Option<String>>,
424517
}
425518

426519
struct QRCodeParams {

0 commit comments

Comments
 (0)