feat: add initial ffmpeg implementation

This commit is contained in:
uku 2025-05-13 14:22:03 +02:00
parent 1d93482209
commit 8f3c7e4052
Signed by: uku
SSH key fingerprint: SHA256:4P0aN6M8ajKukNi6aPOaX0LacanGYtlfjmN+m/sHY/o
6 changed files with 237 additions and 32 deletions

View file

@ -1,3 +1,4 @@
mod ffmpeg;
mod gobject;
mod relm;
mod zipline;
@ -42,6 +43,8 @@ impl Config {
#[derive(Debug)]
enum Message {
OpenFilePicker,
SetOutFilename(String),
SetMergeTracks(bool),
SetFolder(ZiplineFolder),
StartTheProcess,
Nothing,
@ -49,24 +52,49 @@ enum Message {
#[derive(Debug)]
enum ProgressMessage {
SetStep(Step),
SetTotal(usize),
Progress(usize),
AbsProgress(usize),
IncProgress(usize),
Finish(String),
Error(String),
}
#[derive(Debug)]
enum Step {
Waiting,
Converting,
Uploading,
Thumbnail,
}
impl Step {
fn button_text(&self) -> &'static str {
match self {
Step::Waiting => "Send",
Step::Converting => "Converting...",
Step::Uploading => "Uploading...",
Step::Thumbnail => "Generating thumbnail...",
}
}
}
struct UploadInfo {
config: Config,
video_path: Option<PathBuf>,
out_filename: Option<String>,
merge_tracks: bool,
folder: Option<ZiplineFolder>,
}
struct Tyrolienne {
config: Config,
locked: bool,
step: Step,
progress: usize,
total: usize,
video_path: Option<PathBuf>,
out_filename: Option<String>,
merge_tracks: bool,
folder: Option<ZiplineFolder>,
dialog: Controller<Dialog>,
toast: Controller<Toast>,
@ -83,6 +111,8 @@ impl Tyrolienne {
fn clone_as_info(&self) -> UploadInfo {
UploadInfo {
config: self.config.clone(),
out_filename: self.out_filename.clone(),
merge_tracks: self.merge_tracks,
video_path: self.video_path.clone(),
folder: self.folder.clone(),
}
@ -128,11 +158,26 @@ impl AsyncComponent for Tyrolienne {
set_title: "Video file",
#[watch]
set_subtitle: &model.display_video_path(),
add_suffix = &gtk::Image {
set_margin_end: 8,
set_icon_name: Some("document-open-symbolic"),
},
connect_activated => Message::OpenFilePicker,
},
adw::EntryRow {
set_title: "Output file name",
connect_changed[sender] => move |e| sender.input(Message::SetOutFilename(e.text().into())),
},
adw::SwitchRow {
set_title: "Merge audio tracks",
set_active: true,
connect_active_notify[sender] => move |s| sender.input(Message::SetMergeTracks(s.is_active())),
},
adw::ComboRow {
set_title: "Folder",
set_title: "Zipline folder",
set_model: Some(&folder_store),
set_expression: Some(&folder_expression),
connect_activated[sender] => move |r| {
@ -158,9 +203,10 @@ impl AsyncComponent for Tyrolienne {
gtk::Button {
#[watch]
set_label: if model.locked { "Uploading..." } else { "Send" },
set_label: model.step.button_text(),
#[watch]
set_sensitive: !model.locked && model.video_path.is_some(),
set_sensitive: matches!(model.step, Step::Waiting) && model.video_path.is_some(),
set_css_classes: &["suggested-action"],
connect_clicked => Message::StartTheProcess,
}
}
@ -179,10 +225,12 @@ impl AsyncComponent for Tyrolienne {
let model = Tyrolienne {
config,
locked: false,
step: Step::Waiting,
progress: 0,
total: 1,
video_path: None,
out_filename: None,
merge_tracks: true,
folder: None,
dialog: Dialog::builder()
.launch(toast_overlay.clone())
@ -232,9 +280,6 @@ impl AsyncComponent for Tyrolienne {
) {
match message {
Message::Nothing => {}
Message::SetFolder(folder) => {
self.folder = (folder.id != GtkZiplineFolder::NONE_ID).then_some(folder)
}
Message::OpenFilePicker => {
let file = rfd::AsyncFileDialog::new()
.add_filter("Video file", &["mp4", "mkv", "webm"])
@ -245,8 +290,14 @@ impl AsyncComponent for Tyrolienne {
self.video_path = Some(file.path().to_owned());
}
}
Message::SetOutFilename(name) => {
self.out_filename = if name.is_empty() { None } else { Some(name) }
}
Message::SetMergeTracks(m) => self.merge_tracks = m,
Message::SetFolder(folder) => {
self.folder = (folder.id != GtkZiplineFolder::NONE_ID).then_some(folder)
}
Message::StartTheProcess => {
self.locked = true;
let info = self.clone_as_info();
sender.command(|out, shutdown| {
Box::pin(
@ -271,21 +322,30 @@ impl AsyncComponent for Tyrolienne {
_root: &Self::Root,
) {
match message {
ProgressMessage::SetTotal(total) => self.total = total,
ProgressMessage::Progress(prog) => self.progress += prog,
ProgressMessage::Finish(_) | ProgressMessage::Error(_) => {
self.locked = false;
ProgressMessage::SetStep(step) => self.step = step,
ProgressMessage::SetTotal(total) => {
self.progress = 0;
self.total = total;
}
ProgressMessage::AbsProgress(prog) => self.progress = prog,
ProgressMessage::IncProgress(prog) => self.progress += prog,
ProgressMessage::Finish(url) => {
self.step = Step::Waiting;
self.progress = 0;
self.total = 1;
if let ProgressMessage::Finish(url) = message {
self.toast.emit(ToastInput::Show(url));
} else if let ProgressMessage::Error(e) = message {
self.dialog.emit(DialogInput::Show {
heading: "An error occurred".into(),
body: e.to_string(),
});
}
// TODO copy to clipboard here instead, the toast disappears after a short while
self.toast.emit(ToastInput::Show(url));
}
ProgressMessage::Error(e) => {
self.step = Step::Waiting;
self.progress = 0;
self.total = 1;
self.dialog.emit(DialogInput::Show {
heading: "An error occurred".into(),
body: e.to_string(),
});
}
}
}
@ -337,19 +397,28 @@ fn get_config() -> Result<Config> {
}
async fn the_process(app: UploadInfo, sender: &Sender<ProgressMessage>) -> Result<String> {
let Some(ref video_path) = app.video_path else {
bail!("No video given!");
};
sender.emit(ProgressMessage::SetStep(Step::Converting));
let video_meta = ffmpeg::get_video_meta(video_path).await?;
sender.emit(ProgressMessage::SetTotal(video_meta.duration_us));
let out_path = ffmpeg::convert_video(video_path, sender.clone()).await?;
sender.emit(ProgressMessage::SetStep(Step::Uploading));
if let Some(folder) = app.folder.as_ref() {
tracing::info!("uploading to folder '{}'...", folder.name);
} else {
tracing::info!("uploading video...");
}
let Some(ref video_path) = app.video_path else {
bail!("No video given!");
};
let res = zipline::upload_file(&app.config, sender, app.folder.as_ref(), video_path).await?;
let res = zipline::upload_file(&app.config, sender, app.folder.as_ref(), &out_path).await?;
let zp_file = &res.files[0];
sender.emit(ProgressMessage::SetStep(Step::Thumbnail));
tracing::info!("recalculating thumbnails...");
zipline::recalc_thumbnails(&app.config).await?;
@ -365,8 +434,10 @@ async fn the_process(app: UploadInfo, sender: &Sender<ProgressMessage>) -> Resul
// TODO get w&h from video
Ok(format!(
"https://autocompressor.net/av1?v={}&i={}&w=1920&h=1080",
"https://autocompressor.net/av1?v={}&i={}&w={}&h={}",
Encoded(&zp_file.url),
Encoded(&thumbnail_url)
Encoded(&thumbnail_url),
video_meta.width,
video_meta.height,
))
}