fix: make request sending asynchronous too

This commit is contained in:
uku 2025-05-12 14:17:51 +02:00
parent 2f96459f58
commit fb227660bb
Signed by: uku
SSH key fingerprint: SHA256:4P0aN6M8ajKukNi6aPOaX0LacanGYtlfjmN+m/sHY/o
4 changed files with 100 additions and 70 deletions

16
Cargo.lock generated
View file

@ -1654,7 +1654,6 @@ dependencies = [
"base64", "base64",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-channel",
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2", "h2",
@ -1682,11 +1681,13 @@ dependencies = [
"sync_wrapper", "sync_wrapper",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
"tokio-util",
"tower", "tower",
"tower-service", "tower-service",
"url", "url",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-streams",
"web-sys", "web-sys",
"webpki-roots 0.26.11", "webpki-roots 0.26.11",
"windows-registry", "windows-registry",
@ -2451,6 +2452,19 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.77" version = "0.3.77"

View file

@ -7,7 +7,7 @@ edition = "2024"
color-eyre = "0.6.4" color-eyre = "0.6.4"
dirs = "6.0.0" dirs = "6.0.0"
relm4 = { version = "0.9.1", features = ["gnome_47", "libadwaita"] } 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"] } rfd = { version = "0.15.3", default-features = false, features = ["tokio", "xdg-portal"] }
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
toml = "0.8.22" toml = "0.8.22"

View file

@ -8,16 +8,18 @@ use color_eyre::eyre::{OptionExt, Result, bail};
use gobject::GtkZiplineFolder; use gobject::GtkZiplineFolder;
use relm::{Dialog, DialogInput}; use relm::{Dialog, DialogInput};
use relm4::{ use relm4::{
Component, ComponentController, ComponentParts, Controller, RelmApp, Component, ComponentController, Controller, RelmApp,
adw::{self, prelude::*}, adw::{self, prelude::*},
gtk::{self, gio, glib::clone}, gtk::{self, gio, glib::clone},
prelude::{AsyncComponent, AsyncComponentParts},
tokio,
}; };
use tracing::Level; use tracing::Level;
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
use urlencoding::Encoded; use urlencoding::Encoded;
use zipline::ZiplineFolder; use zipline::ZiplineFolder;
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)] #[derive(Debug, Default, Clone, serde::Deserialize, serde::Serialize)]
struct Config { struct Config {
zipline_url: String, zipline_url: String,
zipline_token: String, zipline_token: String,
@ -45,11 +47,6 @@ enum Message {
Nothing, Nothing,
} }
#[derive(Debug)]
enum CommandMessage {
SelectedFile(Option<PathBuf>),
}
struct Widgets { struct Widgets {
file_picker_row: adw::ActionRow, file_picker_row: adw::ActionRow,
send_button: gtk::Button, send_button: gtk::Button,
@ -71,11 +68,11 @@ impl Tyrolienne {
} }
} }
impl Component for Tyrolienne { impl AsyncComponent for Tyrolienne {
type Input = Message; type Input = Message;
type Output = (); type Output = ();
type CommandOutput = CommandMessage; type CommandOutput = ();
type Init = (Config, Vec<ZiplineFolder>); type Init = Config;
type Root = adw::ApplicationWindow; type Root = adw::ApplicationWindow;
type Widgets = Widgets; type Widgets = Widgets;
@ -86,11 +83,11 @@ impl Component for Tyrolienne {
.build() .build()
} }
fn init( async fn init(
(config, folders): Self::Init, config: Self::Init,
root: Self::Root, root: Self::Root,
sender: relm4::ComponentSender<Self>, sender: relm4::AsyncComponentSender<Self>,
) -> relm4::ComponentParts<Self> { ) -> AsyncComponentParts<Self> {
let model = Tyrolienne { let model = Tyrolienne {
config, config,
video_path: None, video_path: None,
@ -100,6 +97,19 @@ impl Component for Tyrolienne {
.forward(sender.input_sender(), |_| Message::Nothing), .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() let file_picker_row = adw::ActionRow::builder()
.activatable(true) .activatable(true)
.title("Video file") .title("Video file")
@ -179,13 +189,13 @@ impl Component for Tyrolienne {
send_button, send_button,
}; };
ComponentParts { model, widgets } AsyncComponentParts { model, widgets }
} }
fn update( async fn update(
&mut self, &mut self,
message: Self::Input, message: Self::Input,
sender: relm4::ComponentSender<Self>, _sender: relm4::AsyncComponentSender<Self>,
_root: &Self::Root, _root: &Self::Root,
) { ) {
match message { match message {
@ -193,38 +203,27 @@ impl Component for Tyrolienne {
Message::SetFolder(folder) => { Message::SetFolder(folder) => {
self.folder = (folder.id != GtkZiplineFolder::NONE_ID).then_some(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}"), Ok(url) => tracing::info!("{url}"),
Err(e) => self.dialog.emit(DialogInput::Show { Err(e) => self.dialog.emit(DialogInput::Show {
heading: "An error occurred".into(), heading: "An error occurred".into(),
body: e.to_string(), body: e.to_string(),
}), }),
}, },
Message::OpenFilePicker => sender.oneshot_command(async { Message::OpenFilePicker => {
CommandMessage::SelectedFile( let file = rfd::AsyncFileDialog::new()
rfd::AsyncFileDialog::new() .add_filter("Video file", &["mp4", "mkv", "webm"])
.add_filter("Video file", &["mp4", "mkv", "webm"]) .pick_file()
.pick_file() .await;
.await
.map(|h| h.path().to_owned()), if let Some(file) = file {
) self.video_path = Some(file.path().to_owned());
}), }
}
} }
} }
fn update_cmd( fn update_view(&self, widgets: &mut Self::Widgets, _sender: relm4::AsyncComponentSender<Self>) {
&mut self,
message: Self::CommandOutput,
_sender: relm4::ComponentSender<Self>,
_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<Self>) {
widgets widgets
.file_picker_row .file_picker_row
.set_subtitle(&self.display_video_path()); .set_subtitle(&self.display_video_path());
@ -242,10 +241,9 @@ fn main() -> Result<()> {
// TODO: show dialog in case these error // TODO: show dialog in case these error
let config = get_config()?; let config = get_config()?;
let folders = zipline::get_folders(&config)?;
let app = RelmApp::new("net.uku3lig.Tyrolienne"); let app = RelmApp::new("net.uku3lig.Tyrolienne");
app.run::<Tyrolienne>((config, folders)); app.run_async::<Tyrolienne>(config);
Ok(()) Ok(())
} }
@ -279,7 +277,7 @@ fn get_config() -> Result<Config> {
Ok(config) Ok(config)
} }
fn the_process(app: &Tyrolienne) -> Result<String> { async fn the_process(app: &Tyrolienne) -> Result<String> {
if let Some(folder) = app.folder.as_ref() { if let Some(folder) = app.folder.as_ref() {
tracing::info!("uploading to folder '{}'...", folder.name); tracing::info!("uploading to folder '{}'...", folder.name);
} else { } else {
@ -290,18 +288,19 @@ fn the_process(app: &Tyrolienne) -> Result<String> {
&app.config, &app.config,
app.folder.as_ref(), app.folder.as_ref(),
app.video_path.as_ref().unwrap(), app.video_path.as_ref().unwrap(),
)?; )
.await?;
let zp_file = &res.files[0]; let zp_file = &res.files[0];
tracing::info!("recalculating thumbnails..."); 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..."); 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 let thumbnail_url = res
.thumbnail_url(&app.config) .thumbnail_url(&app.config)
.ok_or_eyre("could not get thumbnail url")?; .ok_or_eyre("could not get thumbnail url")?;

View file

@ -1,11 +1,7 @@
use std::{path::Path, sync::LazyLock}; use std::{path::Path, sync::LazyLock};
use color_eyre::eyre::{Result, bail}; use color_eyre::eyre::{Result, bail};
use reqwest::{ use reqwest::{Client, StatusCode, header::AUTHORIZATION, multipart::Form};
StatusCode,
blocking::{Client, multipart::Form},
header::AUTHORIZATION,
};
use crate::Config; use crate::Config;
@ -46,29 +42,36 @@ impl ZiplineFileInfo {
} }
} }
pub fn get_folders(config: &Config) -> Result<Vec<ZiplineFolder>> { pub async fn get_folders(config: &Config) -> Result<Vec<ZiplineFolder>> {
let url = format!("{}api/user/folders?noincl=true", config.fixed_url()); let url = format!("{}api/user/folders?noincl=true", config.fixed_url());
let res = CLIENT let res = CLIENT
.get(url) .get(url)
.header(AUTHORIZATION, &config.zipline_token) .header(AUTHORIZATION, &config.zipline_token)
.send()?; .send()
.await?;
if res.status() != StatusCode::OK { if res.status() != StatusCode::OK {
bail!("an error occurred ({}): {}", res.status(), res.text()?); bail!(
"an error occurred ({}): {}",
res.status(),
res.text().await?
);
} else { } 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, config: &Config,
folder: Option<&ZiplineFolder>, folder: Option<&ZiplineFolder>,
file_path: &Path, file_path: &Path,
) -> Result<ZiplineUploadResponse> { ) -> Result<ZiplineUploadResponse> {
let url = format!("{}api/upload", config.fixed_url()); 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 let mut req = CLIENT
.post(url) .post(url)
@ -80,42 +83,56 @@ pub fn upload_file(
req = req.header("x-zipline-folder", &folder.id); req = req.header("x-zipline-folder", &folder.id);
} }
let res = req.send()?; let res = req.send().await?;
if res.status() != StatusCode::OK { if res.status() != StatusCode::OK {
bail!("an error occurred ({}): {}", res.status(), res.text()?); bail!(
"an error occurred ({}): {}",
res.status(),
res.text().await?
);
} else { } 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 url = format!("{}api/server/thumbnails", config.fixed_url());
let res = CLIENT let res = CLIENT
.post(url) .post(url)
.header(AUTHORIZATION, &config.zipline_token) .header(AUTHORIZATION, &config.zipline_token)
.json(&[("rerun", false)]) .json(&[("rerun", false)])
.send()?; .send()
.await?;
if res.status() != StatusCode::OK { if res.status() != StatusCode::OK {
bail!("an error occurred ({}): {}", res.status(), res.text()?); bail!(
"an error occurred ({}): {}",
res.status(),
res.text().await?
);
} else { } else {
Ok(()) Ok(())
} }
} }
pub fn get_file_details(config: &Config, id: &str) -> Result<ZiplineFileInfo> { pub async fn get_file_details(config: &Config, id: &str) -> Result<ZiplineFileInfo> {
let url = format!("{}api/user/files/{id}", config.fixed_url()); let url = format!("{}api/user/files/{id}", config.fixed_url());
let res = CLIENT let res = CLIENT
.get(url) .get(url)
.header(AUTHORIZATION, &config.zipline_token) .header(AUTHORIZATION, &config.zipline_token)
.send()?; .send()
.await?;
if res.status() != StatusCode::OK { if res.status() != StatusCode::OK {
bail!("an error occurred ({}): {}", res.status(), res.text()?); bail!(
"an error occurred ({}): {}",
res.status(),
res.text().await?
);
} else { } else {
res.json().map_err(Into::into) res.json().await.map_err(Into::into)
} }
} }