From 61c759d25ec12338d5e9c30364bd4dd0ce15f587 Mon Sep 17 00:00:00 2001 From: uku Date: Mon, 19 May 2025 18:18:38 +0200 Subject: [PATCH 1/6] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'crane': 'github:ipetkov/crane/dfd9a8dfd09db9aad544c4d3b6c47b12562544a5?narHash=sha256-DdWJLA%2BD5tcmrRSg5Y7tp/qWaD05ATI4Z7h22gd1h7Q%3D' (2025-05-03) → 'github:ipetkov/crane/76603d32f18e0e378d9f6335c8fc286413493655?narHash=sha256-Zay3WJdSvC2VQmNqWSVLBOg/1iS/0/Q0c9JOBsB%2B3qw%3D' (2025-05-18) • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/3730d8a308f94996a9ba7c7138ede69c1b9ac4ae?narHash=sha256-7%2BpG1I9jvxNlmln4YgnlW4o%2Bw0TZX24k688mibiFDUE%3D' (2025-05-05) → 'github:NixOS/nixpkgs/292fa7d4f6519c074f0a50394dbbe69859bb6043?narHash=sha256-GaOZntlJ6gPPbbkTLjbd8BMWaDYafhuuYRNrxCGnPJw%3D' (2025-05-18) --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 8c45108..18679db 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "crane": { "locked": { - "lastModified": 1746291859, - "narHash": "sha256-DdWJLA+D5tcmrRSg5Y7tp/qWaD05ATI4Z7h22gd1h7Q=", + "lastModified": 1747587869, + "narHash": "sha256-Zay3WJdSvC2VQmNqWSVLBOg/1iS/0/Q0c9JOBsB+3qw=", "owner": "ipetkov", "repo": "crane", - "rev": "dfd9a8dfd09db9aad544c4d3b6c47b12562544a5", + "rev": "76603d32f18e0e378d9f6335c8fc286413493655", "type": "github" }, "original": { @@ -37,11 +37,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1746461020, - "narHash": "sha256-7+pG1I9jvxNlmln4YgnlW4o+w0TZX24k688mibiFDUE=", + "lastModified": 1747542820, + "narHash": "sha256-GaOZntlJ6gPPbbkTLjbd8BMWaDYafhuuYRNrxCGnPJw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "3730d8a308f94996a9ba7c7138ede69c1b9ac4ae", + "rev": "292fa7d4f6519c074f0a50394dbbe69859bb6043", "type": "github" }, "original": { From a9837b7938e8c527bd101cd6f88ee361e442ad0c Mon Sep 17 00:00:00 2001 From: uku Date: Mon, 19 May 2025 18:39:48 +0200 Subject: [PATCH 2/6] feat: support changing file name --- src/ffmpeg.rs | 13 +++++++++++-- src/main.rs | 1 - 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/ffmpeg.rs b/src/ffmpeg.rs index 0c3e036..df2d6e7 100644 --- a/src/ffmpeg.rs +++ b/src/ffmpeg.rs @@ -98,7 +98,16 @@ pub async fn convert_video( merge_tracks: bool, sender: Sender, ) -> Result { - let out_path = PathBuf::from("/tmp/out.webm"); + let out_filename = out_filename + .or_else(|| { + path.file_name() + .and_then(|s| s.to_str()) + .map(|s| s.to_owned()) + }) + .unwrap_or("out.webm".into()); + + let mut out_path = std::env::temp_dir().join(out_filename); + out_path.set_extension("webm"); let codec_args: &[&str] = match out_codec { Codec::AV1 => &["-c:v", "libsvtav1"], @@ -118,7 +127,7 @@ pub async fn convert_video( .args(["-c:a", "libopus", "-b:a", "96k"]) .args(codec_args) .args(merge_args) - .args(["-loglevel", "error", "-progress", "-", "-nostats"]) + .args(["-y", "-loglevel", "error", "-progress", "-", "-nostats"]) .arg(&out_path) .stdout(Stdio::piped()) .spawn()?; diff --git a/src/main.rs b/src/main.rs index fb0be26..d5c807c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -459,7 +459,6 @@ async fn the_process(app: UploadInfo, sender: &Sender) -> Resul .thumbnail_url(&app.config) .ok_or_eyre("could not get thumbnail url")?; - // TODO get w&h from video Ok(format!( "https://autocompressor.net/av1?v={}&i={}&w={}&h={}", Encoded(&zp_file.url), From c0049cbb8dfb05ff0930a35472e65ca46faa11d1 Mon Sep 17 00:00:00 2001 From: uku Date: Mon, 19 May 2025 18:55:21 +0200 Subject: [PATCH 3/6] fix: copy url to clipboard directly --- src/main.rs | 4 +--- src/relm.rs | 18 ++++++------------ 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/main.rs b/src/main.rs index d5c807c..19caac3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -353,9 +353,7 @@ impl AsyncComponent for Tyrolienne { self.step = Step::Waiting; self.progress = 0; self.total = 1; - - // TODO copy to clipboard here instead, the toast disappears after a short while - self.toast.emit(ToastInput::Show(url)); + self.toast.emit(ToastInput::ShowAndCopy(url)); } ProgressMessage::Error(e) => { self.step = Step::Waiting; diff --git a/src/relm.rs b/src/relm.rs index 4a230f4..0b0c578 100644 --- a/src/relm.rs +++ b/src/relm.rs @@ -1,3 +1,4 @@ +use arboard::Clipboard; use relm4::{ SimpleComponent, adw::{self, prelude::*}, @@ -91,8 +92,7 @@ pub struct Toast { #[derive(Debug)] pub enum ToastInput { - Show(String), - Copy, + ShowAndCopy(String), Dismiss, } @@ -107,8 +107,6 @@ impl SimpleComponent for Toast { adw::Toast { #[watch] set_title: &model.text, - set_button_label: Some("Copy"), - connect_button_clicked => ToastInput::Copy, connect_dismissed => ToastInput::Dismiss, } } @@ -130,18 +128,14 @@ impl SimpleComponent for Toast { fn update(&mut self, message: Self::Input, _sender: relm4::ComponentSender) { match message { - ToastInput::Show(text) => { + ToastInput::ShowAndCopy(text) => { self.visible = true; - self.text = text; - } - ToastInput::Copy => { - if let Err(e) = - arboard::Clipboard::new().and_then(|mut c| c.set_text(self.text.clone())) - { + + if let Err(e) = Clipboard::new().and_then(|mut c| c.set_text(text)) { tracing::error!("could not copy url to clipboard: {e}"); self.text = "Could not copy".into(); } else { - self.visible = false; + self.text = "Copied url to clipboard".into(); } } ToastInput::Dismiss => self.visible = false, From 0475c52529ae6dc64fbb0cd4b26041005a70afb9 Mon Sep 17 00:00:00 2001 From: uku Date: Mon, 19 May 2025 18:58:06 +0200 Subject: [PATCH 4/6] fix: abort if ffmpeg process errors --- src/ffmpeg.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/ffmpeg.rs b/src/ffmpeg.rs index df2d6e7..56635cb 100644 --- a/src/ffmpeg.rs +++ b/src/ffmpeg.rs @@ -3,7 +3,8 @@ use std::{ process::Stdio, }; -use color_eyre::eyre::{ContextCompat, Result}; +use color_eyre::eyre::{ContextCompat, Result, bail}; +use futures::channel::oneshot; use relm4::{ Sender, tokio::{ @@ -136,11 +137,10 @@ pub async fn convert_video( let mut reader = BufReader::new(stdout).lines(); // make sure the process is actually started and awaited + let (tx, rx) = oneshot::channel(); tokio::spawn(async move { - child - .wait() - .await - .expect("ffmpeg process encountered an error"); + tx.send(child.wait().await) + .expect("could not send exit status"); }); while let Some(line) = reader.next_line().await? { @@ -157,5 +157,10 @@ pub async fn convert_video( } } + let status = rx.await??; + if !status.success() { + bail!("ffmpeg process errored: {status}"); + } + Ok(out_path) } From dd2616f0e5b1c90c50bee34e971de350ec57c4d7 Mon Sep 17 00:00:00 2001 From: uku Date: Mon, 19 May 2025 19:24:11 +0200 Subject: [PATCH 5/6] fix: conditionally disable audio track merging --- src/ffmpeg.rs | 32 +++++++++++++++++++++----------- src/main.rs | 11 +++++++++-- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/ffmpeg.rs b/src/ffmpeg.rs index 56635cb..65f12c9 100644 --- a/src/ffmpeg.rs +++ b/src/ffmpeg.rs @@ -23,9 +23,10 @@ struct FfprobeOut { } #[derive(serde::Deserialize)] -struct StreamInfo { - width: usize, - height: usize, +#[serde(tag = "codec_type", rename_all = "lowercase")] +enum StreamInfo { + Video { width: usize, height: usize }, + Audio, } #[derive(serde::Deserialize)] @@ -37,6 +38,7 @@ struct FormatInfo { pub struct VideoMeta { pub width: usize, pub height: usize, + pub audio_streams: usize, pub duration_us: usize, } @@ -62,10 +64,8 @@ pub async fn get_video_meta(path: &Path) -> Result { .args([ "-v", "error", - "-select_streams", - "v:0", "-show_entries", - "stream=width,height : format=duration", + "stream=width,height,codec_type : format=duration", "-of", "json", ]) @@ -77,17 +77,28 @@ pub async fn get_video_meta(path: &Path) -> Result { let str = String::from_utf8(output.stdout)?; let output: FfprobeOut = serde_json::from_str(&str)?; - let stream = output + let (width, height) = output .streams - .first() + .iter() + .find_map(|s| match s { + StreamInfo::Video { width, height } => Some((*width, *height)), + _ => None, + }) .wrap_err("could not get stream information from ffprobe")?; + let audio_streams = output + .streams + .iter() + .filter(|s| matches!(s, StreamInfo::Audio)) + .count(); + let duration_sec = output.format.duration.parse::()?; let duration_us = (duration_sec * 1_000_000.0).ceil() as usize; Ok(VideoMeta { - width: stream.width, - height: stream.height, + width, + height, + audio_streams, duration_us, }) } @@ -115,7 +126,6 @@ pub async fn convert_video( Codec::VP9 => &["-c:v", "libvpx-vp9", "-row-mt", "1"], }; - // TODO: maybe check if the video has 2 audio tracks? or at least use a "fail-safe" method let merge_args: &[&str] = if merge_tracks { &["-ac", "2", "-filter_complex", "amerge=inputs=2"] } else { diff --git a/src/main.rs b/src/main.rs index 19caac3..a445eee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -94,6 +94,7 @@ struct Tyrolienne { step: Step, progress: usize, total: usize, + can_merge: bool, video_path: Option, out_filename: Option, out_codec: ffmpeg::Codec, @@ -116,7 +117,7 @@ impl Tyrolienne { config: self.config.clone(), out_filename: self.out_filename.clone(), out_codec: self.out_codec, - merge_tracks: self.merge_tracks, + merge_tracks: self.can_merge && self.merge_tracks, video_path: self.video_path.clone(), folder: self.folder.clone(), } @@ -190,7 +191,10 @@ impl AsyncComponent for Tyrolienne { adw::SwitchRow { set_title: "Merge audio tracks", - set_active: true, + #[watch] + set_sensitive: model.can_merge, + #[watch] + set_active: model.can_merge && model.merge_tracks, connect_active_notify[sender] => move |s| sender.input(Message::SetMergeTracks(s.is_active())), }, @@ -246,6 +250,7 @@ impl AsyncComponent for Tyrolienne { step: Step::Waiting, progress: 0, total: 1, + can_merge: false, video_path: None, out_filename: None, out_codec: ffmpeg::Codec::VP9, @@ -306,6 +311,8 @@ impl AsyncComponent for Tyrolienne { .await; if let Some(file) = file { + let meta = ffmpeg::get_video_meta(file.path()).await; + self.can_merge = meta.map(|m| m.audio_streams == 2).unwrap_or(false); self.video_path = Some(file.path().to_owned()); } } From 3315b38806f962da718492d9428a64b85eec8397 Mon Sep 17 00:00:00 2001 From: uku Date: Mon, 19 May 2025 19:26:47 +0200 Subject: [PATCH 6/6] fix: show loading spinner during startup --- src/main.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index a445eee..b8926dd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ use relm4::{ Component, ComponentController, Controller, RelmApp, Sender, adw::{self, prelude::*}, gtk::{self, gio}, + loading_widgets::LoadingWidgets, prelude::{AsyncComponent, AsyncComponentParts}, tokio, }; @@ -264,8 +265,6 @@ impl AsyncComponent for Tyrolienne { .forward(sender.input_sender(), |_| Message::Nothing), }; - // TODO consider using the "loading" (?) mechanism from relm4 - // https://relm4.org/book/stable/threads_and_async/async.html let folders = match zipline::get_folders(&model.config).await { Ok(v) => v, Err(e) => { @@ -296,6 +295,21 @@ impl AsyncComponent for Tyrolienne { AsyncComponentParts { model, widgets } } + fn init_loading_widgets(root: Self::Root) -> Option { + relm4::view! { + #[local] + root { + #[name(spinner)] + gtk::Spinner { + start: (), + set_halign: gtk::Align::Center, + } + } + } + + Some(LoadingWidgets::new(root, spinner)) + } + async fn update( &mut self, message: Self::Input,