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",
"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"

View file

@ -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"

View file

@ -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<PathBuf>),
}
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<ZiplineFolder>);
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<Self>,
) -> relm4::ComponentParts<Self> {
sender: relm4::AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> {
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<Self>,
_sender: relm4::AsyncComponentSender<Self>,
_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<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>) {
fn update_view(&self, widgets: &mut Self::Widgets, _sender: relm4::AsyncComponentSender<Self>) {
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::<Tyrolienne>((config, folders));
app.run_async::<Tyrolienne>(config);
Ok(())
}
@ -279,7 +277,7 @@ fn get_config() -> Result<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() {
tracing::info!("uploading to folder '{}'...", folder.name);
} else {
@ -290,18 +288,19 @@ fn the_process(app: &Tyrolienne) -> Result<String> {
&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")?;

View file

@ -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<Vec<ZiplineFolder>> {
pub async fn get_folders(config: &Config) -> Result<Vec<ZiplineFolder>> {
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<ZiplineUploadResponse> {
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<ZiplineFileInfo> {
pub async fn get_file_details(config: &Config, id: &str) -> Result<ZiplineFileInfo> {
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)
}
}