fix: make request sending asynchronous too
This commit is contained in:
parent
2f96459f58
commit
fb227660bb
4 changed files with 100 additions and 70 deletions
16
Cargo.lock
generated
16
Cargo.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
89
src/main.rs
89
src/main.rs
|
@ -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")?;
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue