use std::{path::Path, sync::LazyLock}; use anyhow::{Result, bail}; use futures::StreamExt; use relm4::Sender; use reqwest::{ Body, Client, StatusCode, header::AUTHORIZATION, multipart::{Form, Part}, }; use tokio::fs::File; use tokio_util::io::ReaderStream; use crate::{Config, ProgressMessage}; static CLIENT: LazyLock = LazyLock::new(Client::new); #[derive(Debug, Clone, serde::Deserialize)] pub struct ZiplineFolder { pub id: String, pub name: String, } #[derive(Debug, serde::Deserialize)] pub struct ZiplineUploadResponse { pub files: Vec, } #[derive(Debug, serde::Deserialize)] pub struct ZiplineFile { pub id: String, pub url: String, } #[derive(Debug, serde::Deserialize)] pub struct ZiplineFileInfo { pub thumbnail: Option, } #[derive(Debug, serde::Deserialize)] pub struct ZiplineThumbnail { pub path: String, } #[derive(Debug, serde::Deserialize)] pub struct ZiplineShortUrl { url: String, } impl ZiplineFileInfo { pub fn thumbnail_url(&self, config: &Config) -> Option { self.thumbnail .as_ref() .map(|t| format!("{}raw/{}", config.fixed_url(), t.path)) } } async fn wrap_file(path: &Path, sender: Sender) -> Result { let file_name = path .file_name() .map(|filename| filename.to_string_lossy().into_owned()); let file = File::open(path).await?; let len = file.metadata().await?.len(); sender.emit(ProgressMessage::SetTotal(len as usize)); let stream = ReaderStream::new(file).map(move |b| { if let Ok(ref bytes) = b { sender.emit(ProgressMessage::IncProgress(bytes.len())); } b }); let field = Part::stream_with_length(Body::wrap_stream(stream), len).mime_str("video/webm")?; Ok(if let Some(file_name) = file_name { field.file_name(file_name) } else { field }) } 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() .await?; if res.status() != StatusCode::OK { bail!( "an error occurred ({}): {}", res.status(), res.text().await? ); } else { res.json().await.map_err(Into::into) } } pub async fn upload_file( config: &Config, sender: &Sender, folder: Option<&ZiplineFolder>, file_path: &Path, ) -> Result { let url = format!("{}api/upload", config.fixed_url()); let wrapped_file = wrap_file(file_path, sender.clone()).await?; let form = Form::new().part("file", wrapped_file); let mut req = CLIENT .post(url) .header(AUTHORIZATION, &config.zipline_token) .header("x-zipline-format", "name") .multipart(form); if let Some(folder) = folder { req = req.header("x-zipline-folder", &folder.id); } let res = req.send().await?; if res.status() != StatusCode::OK { bail!( "an error occurred ({}): {}", res.status(), res.text().await? ); } else { res.json().await.map_err(Into::into) } } 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() .await?; if res.status() != StatusCode::OK { bail!( "an error occurred ({}): {}", res.status(), res.text().await? ); } else { Ok(()) } } 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() .await?; if res.status() != StatusCode::OK { bail!( "an error occurred ({}): {}", res.status(), res.text().await? ); } else { res.json().await.map_err(Into::into) } } pub async fn shorten_url(config: &Config, to_shorten: &str) -> Result { let url = format!("{}api/user/urls", config.fixed_url()); let body = serde_json::json!({ "destination": to_shorten, "enabled": true, "vanity": null }); let res = CLIENT .post(url) .json(&body) .header(AUTHORIZATION, &config.zipline_token) .send() .await?; if res.status() != StatusCode::OK { bail!( "an error occurred ({}): {}", res.status(), res.text().await? ); } else { let out: ZiplineShortUrl = res.json().await?; Ok(out.url) } }