From fb227660bb539a8b1c37a9661874c163fe6d9e04 Mon Sep 17 00:00:00 2001 From: uku Date: Mon, 12 May 2025 14:17:51 +0200 Subject: [PATCH] fix: make request sending asynchronous too --- Cargo.lock | 16 ++++++++- Cargo.toml | 2 +- src/main.rs | 93 +++++++++++++++++++++++++------------------------- src/zipline.rs | 59 ++++++++++++++++++++------------ 4 files changed, 100 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9ecb3bb..378b334 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1654,7 +1654,6 @@ dependencies = [ "base64", "bytes", "encoding_rs", - "futures-channel", "futures-core", "futures-util", "h2", @@ -1682,11 +1681,13 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", + "tokio-util", "tower", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "webpki-roots 0.26.11", "windows-registry", @@ -2451,6 +2452,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.77" diff --git a/Cargo.toml b/Cargo.toml index 91fa2cb..4e84d11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2024" color-eyre = "0.6.4" dirs = "6.0.0" relm4 = { version = "0.9.1", features = ["gnome_47", "libadwaita"] } -reqwest = { version = "0.12.15", default-features = false, features = ["http2", "charset", "rustls-tls", "blocking", "json", "multipart"] } +reqwest = { version = "0.12.15", default-features = false, features = ["http2", "charset", "rustls-tls", "json", "multipart", "stream"] } rfd = { version = "0.15.3", default-features = false, features = ["tokio", "xdg-portal"] } serde = { version = "1.0.219", features = ["derive"] } toml = "0.8.22" diff --git a/src/main.rs b/src/main.rs index 2a57c63..10f94fe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,16 +8,18 @@ use color_eyre::eyre::{OptionExt, Result, bail}; use gobject::GtkZiplineFolder; use relm::{Dialog, DialogInput}; use relm4::{ - Component, ComponentController, ComponentParts, Controller, RelmApp, + Component, ComponentController, Controller, RelmApp, adw::{self, prelude::*}, gtk::{self, gio, glib::clone}, + prelude::{AsyncComponent, AsyncComponentParts}, + tokio, }; use tracing::Level; use tracing_subscriber::EnvFilter; use urlencoding::Encoded; use zipline::ZiplineFolder; -#[derive(Debug, Default, serde::Deserialize, serde::Serialize)] +#[derive(Debug, Default, Clone, serde::Deserialize, serde::Serialize)] struct Config { zipline_url: String, zipline_token: String, @@ -45,11 +47,6 @@ enum Message { Nothing, } -#[derive(Debug)] -enum CommandMessage { - SelectedFile(Option), -} - struct Widgets { file_picker_row: adw::ActionRow, send_button: gtk::Button, @@ -71,11 +68,11 @@ impl Tyrolienne { } } -impl Component for Tyrolienne { +impl AsyncComponent for Tyrolienne { type Input = Message; type Output = (); - type CommandOutput = CommandMessage; - type Init = (Config, Vec); + type CommandOutput = (); + type Init = Config; type Root = adw::ApplicationWindow; type Widgets = Widgets; @@ -86,11 +83,11 @@ impl Component for Tyrolienne { .build() } - fn init( - (config, folders): Self::Init, + async fn init( + config: Self::Init, root: Self::Root, - sender: relm4::ComponentSender, - ) -> relm4::ComponentParts { + sender: relm4::AsyncComponentSender, + ) -> AsyncComponentParts { let model = Tyrolienne { config, video_path: None, @@ -100,6 +97,19 @@ impl Component 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) => { + model.dialog.emit(DialogInput::Show { + heading: "Could not fetch folders".into(), + body: e.to_string(), + }); + Vec::new() + } + }; + let file_picker_row = adw::ActionRow::builder() .activatable(true) .title("Video file") @@ -179,13 +189,13 @@ impl Component for Tyrolienne { send_button, }; - ComponentParts { model, widgets } + AsyncComponentParts { model, widgets } } - fn update( + async fn update( &mut self, message: Self::Input, - sender: relm4::ComponentSender, + _sender: relm4::AsyncComponentSender, _root: &Self::Root, ) { match message { @@ -193,38 +203,27 @@ impl Component for Tyrolienne { Message::SetFolder(folder) => { self.folder = (folder.id != GtkZiplineFolder::NONE_ID).then_some(folder) } - Message::StartTheProcess => match the_process(self) { + Message::StartTheProcess => match the_process(self).await { Ok(url) => tracing::info!("{url}"), Err(e) => self.dialog.emit(DialogInput::Show { heading: "An error occurred".into(), body: e.to_string(), }), }, - Message::OpenFilePicker => sender.oneshot_command(async { - CommandMessage::SelectedFile( - rfd::AsyncFileDialog::new() - .add_filter("Video file", &["mp4", "mkv", "webm"]) - .pick_file() - .await - .map(|h| h.path().to_owned()), - ) - }), + Message::OpenFilePicker => { + let file = rfd::AsyncFileDialog::new() + .add_filter("Video file", &["mp4", "mkv", "webm"]) + .pick_file() + .await; + + if let Some(file) = file { + self.video_path = Some(file.path().to_owned()); + } + } } } - fn update_cmd( - &mut self, - message: Self::CommandOutput, - _sender: relm4::ComponentSender, - _root: &Self::Root, - ) { - match message { - CommandMessage::SelectedFile(Some(path)) => self.video_path = Some(path), - CommandMessage::SelectedFile(None) => {} - } - } - - fn update_view(&self, widgets: &mut Self::Widgets, _sender: relm4::ComponentSender) { + fn update_view(&self, widgets: &mut Self::Widgets, _sender: relm4::AsyncComponentSender) { widgets .file_picker_row .set_subtitle(&self.display_video_path()); @@ -242,10 +241,9 @@ fn main() -> Result<()> { // TODO: show dialog in case these error let config = get_config()?; - let folders = zipline::get_folders(&config)?; let app = RelmApp::new("net.uku3lig.Tyrolienne"); - app.run::((config, folders)); + app.run_async::(config); Ok(()) } @@ -279,7 +277,7 @@ fn get_config() -> Result { Ok(config) } -fn the_process(app: &Tyrolienne) -> Result { +async fn the_process(app: &Tyrolienne) -> Result { if let Some(folder) = app.folder.as_ref() { tracing::info!("uploading to folder '{}'...", folder.name); } else { @@ -290,18 +288,19 @@ fn the_process(app: &Tyrolienne) -> Result { &app.config, app.folder.as_ref(), app.video_path.as_ref().unwrap(), - )?; + ) + .await?; let zp_file = &res.files[0]; tracing::info!("recalculating thumbnails..."); - zipline::recalc_thumbnails(&app.config)?; + zipline::recalc_thumbnails(&app.config).await?; - std::thread::sleep(Duration::from_secs(2)); + tokio::time::sleep(Duration::from_secs(2)).await; tracing::info!("fetching thumbnail url..."); - let res = zipline::get_file_details(&app.config, &zp_file.id)?; + let res = zipline::get_file_details(&app.config, &zp_file.id).await?; let thumbnail_url = res .thumbnail_url(&app.config) .ok_or_eyre("could not get thumbnail url")?; diff --git a/src/zipline.rs b/src/zipline.rs index 601d216..a39985a 100644 --- a/src/zipline.rs +++ b/src/zipline.rs @@ -1,11 +1,7 @@ use std::{path::Path, sync::LazyLock}; use color_eyre::eyre::{Result, bail}; -use reqwest::{ - StatusCode, - blocking::{Client, multipart::Form}, - header::AUTHORIZATION, -}; +use reqwest::{Client, StatusCode, header::AUTHORIZATION, multipart::Form}; use crate::Config; @@ -46,29 +42,36 @@ impl ZiplineFileInfo { } } -pub fn get_folders(config: &Config) -> Result> { +pub async fn get_folders(config: &Config) -> Result> { let url = format!("{}api/user/folders?noincl=true", config.fixed_url()); let res = CLIENT .get(url) .header(AUTHORIZATION, &config.zipline_token) - .send()?; + .send() + .await?; if res.status() != StatusCode::OK { - bail!("an error occurred ({}): {}", res.status(), res.text()?); + bail!( + "an error occurred ({}): {}", + res.status(), + res.text().await? + ); } else { - res.json().map_err(Into::into) + res.json().await.map_err(Into::into) } } -pub fn upload_file( +pub async fn upload_file( config: &Config, folder: Option<&ZiplineFolder>, file_path: &Path, ) -> Result { let url = format!("{}api/upload", config.fixed_url()); - let form = Form::new().file("file", file_path)?; + // TODO use Part::stream to provide a wrapped file with a custom stream impl to send progress + // (i hope it works) + let form = Form::new().file("file", file_path).await?; let mut req = CLIENT .post(url) @@ -80,42 +83,56 @@ pub fn upload_file( req = req.header("x-zipline-folder", &folder.id); } - let res = req.send()?; + let res = req.send().await?; if res.status() != StatusCode::OK { - bail!("an error occurred ({}): {}", res.status(), res.text()?); + bail!( + "an error occurred ({}): {}", + res.status(), + res.text().await? + ); } else { - res.json().map_err(Into::into) + res.json().await.map_err(Into::into) } } -pub fn recalc_thumbnails(config: &Config) -> Result<()> { +pub async fn recalc_thumbnails(config: &Config) -> Result<()> { let url = format!("{}api/server/thumbnails", config.fixed_url()); let res = CLIENT .post(url) .header(AUTHORIZATION, &config.zipline_token) .json(&[("rerun", false)]) - .send()?; + .send() + .await?; if res.status() != StatusCode::OK { - bail!("an error occurred ({}): {}", res.status(), res.text()?); + bail!( + "an error occurred ({}): {}", + res.status(), + res.text().await? + ); } else { Ok(()) } } -pub fn get_file_details(config: &Config, id: &str) -> Result { +pub async fn get_file_details(config: &Config, id: &str) -> Result { let url = format!("{}api/user/files/{id}", config.fixed_url()); let res = CLIENT .get(url) .header(AUTHORIZATION, &config.zipline_token) - .send()?; + .send() + .await?; if res.status() != StatusCode::OK { - bail!("an error occurred ({}): {}", res.status(), res.text()?); + bail!( + "an error occurred ({}): {}", + res.status(), + res.text().await? + ); } else { - res.json().map_err(Into::into) + res.json().await.map_err(Into::into) } }