mirror of
https://framagit.org/veretcle/tootube.git
synced 2025-07-20 20:41:17 +02:00
feat: async + stream
This commit is contained in:
68
src/lib.rs
68
src/lib.rs
@@ -1,13 +1,9 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
fs::create_dir_all,
|
||||
fs::{remove_file, File},
|
||||
};
|
||||
use std::error::Error;
|
||||
|
||||
use reqwest::Url;
|
||||
use bytes::Bytes;
|
||||
use futures_core::stream::Stream;
|
||||
|
||||
mod error;
|
||||
use error::TootubeError;
|
||||
|
||||
mod config;
|
||||
pub use config::parse_toml;
|
||||
@@ -17,58 +13,34 @@ mod peertube;
|
||||
use peertube::{get_latest_video, get_max_resolution_dl};
|
||||
|
||||
mod youtube;
|
||||
use youtube::{create_resumable_upload, upload_video};
|
||||
use youtube::{create_resumable_upload, now_kiss};
|
||||
|
||||
const TMP_DIR: &str = "/tmp/tootube";
|
||||
|
||||
fn dl_video(u: &str) -> Result<String, Box<dyn Error>> {
|
||||
// create dir
|
||||
create_dir_all(TMP_DIR)?;
|
||||
|
||||
// get file
|
||||
let mut response = reqwest::blocking::get(u)?;
|
||||
|
||||
// create local file
|
||||
let url = Url::parse(u)?;
|
||||
let dest_filename = url
|
||||
.path_segments()
|
||||
.ok_or_else(|| {
|
||||
TootubeError::new(&format!(
|
||||
"Cannot determine the destination filename for {u}"
|
||||
))
|
||||
})?
|
||||
.last()
|
||||
.ok_or_else(|| {
|
||||
TootubeError::new(&format!(
|
||||
"Cannot determine the destination filename for {u}"
|
||||
))
|
||||
})?;
|
||||
|
||||
let dest_filepath = format!("{TMP_DIR}/{dest_filename}");
|
||||
|
||||
let mut dest_file = File::create(&dest_filepath)?;
|
||||
|
||||
response.copy_to(&mut dest_file)?;
|
||||
|
||||
Ok(dest_filepath)
|
||||
async fn get_dl_video_stream(
|
||||
u: &str,
|
||||
) -> Result<impl Stream<Item = Result<Bytes, reqwest::Error>>, Box<dyn Error>> {
|
||||
Ok(reqwest::get(u).await?.bytes_stream())
|
||||
}
|
||||
|
||||
pub fn run(config: Config) {
|
||||
#[tokio::main]
|
||||
pub async fn run(config: Config) {
|
||||
// Get the latest video object
|
||||
let latest_vid = get_latest_video(&config.peertube.base_url).unwrap_or_else(|e| {
|
||||
panic!("Cannot retrieve the latest video, something must have gone terribly wrong: {e}")
|
||||
});
|
||||
let latest_vid = get_latest_video(&config.peertube.base_url)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
panic!("Cannot retrieve the latest video, something must have gone terribly wrong: {e}")
|
||||
});
|
||||
|
||||
let dl_url = get_max_resolution_dl(latest_vid.streaming_playlists.as_ref().unwrap());
|
||||
|
||||
let local_path = dl_video(&dl_url)
|
||||
let pt_stream = get_dl_video_stream(&dl_url)
|
||||
.await
|
||||
.unwrap_or_else(|e| panic!("Cannot download video at URL {}: {}", dl_url, e));
|
||||
|
||||
let resumable_upload_url = create_resumable_upload(&config.youtube, &latest_vid)
|
||||
.await
|
||||
.unwrap_or_else(|e| panic!("Cannot retrieve the upload’s resumable id: {e}"));
|
||||
|
||||
upload_video(&local_path, &resumable_upload_url, &config.youtube)
|
||||
now_kiss(pt_stream, &resumable_upload_url, &config.youtube)
|
||||
.await
|
||||
.unwrap_or_else(|e| panic!("Cannot resume upload!: {e}"));
|
||||
|
||||
remove_file(&local_path).unwrap_or_else(|e| panic!("Cannot delete file {}: {}", local_path, e));
|
||||
}
|
||||
|
@@ -36,11 +36,13 @@ pub struct PeerTubeVideoStreamingPlaylistsFilesResolution {
|
||||
}
|
||||
|
||||
/// This gets the last video uploaded to the PeerTube server
|
||||
pub fn get_latest_video(u: &str) -> Result<PeerTubeVideo, Box<dyn Error>> {
|
||||
let body = reqwest::blocking::get(format!("{}/api/v1/videos?count=1&sort=-publishedAt", u))?
|
||||
.json::<PeerTubeVideos>()?;
|
||||
pub async fn get_latest_video(u: &str) -> Result<PeerTubeVideo, Box<dyn Error>> {
|
||||
let body = reqwest::get(format!("{}/api/v1/videos?count=1&sort=-publishedAt", u))
|
||||
.await?
|
||||
.json::<PeerTubeVideos>()
|
||||
.await?;
|
||||
|
||||
let vid = get_video_detail(u, &body.data[0].uuid)?;
|
||||
let vid = get_video_detail(u, &body.data[0].uuid).await?;
|
||||
|
||||
Ok(vid)
|
||||
}
|
||||
@@ -61,9 +63,11 @@ pub fn get_max_resolution_dl(p: &[PeerTubeVideoStreamingPlaylists]) -> String {
|
||||
}
|
||||
|
||||
/// This gets all the crispy details about one particular video
|
||||
fn get_video_detail(u: &str, v: &str) -> Result<PeerTubeVideo, Box<dyn Error>> {
|
||||
let body =
|
||||
reqwest::blocking::get(format!("{}/api/v1/videos/{}", u, v))?.json::<PeerTubeVideo>()?;
|
||||
async fn get_video_detail(u: &str, v: &str) -> Result<PeerTubeVideo, Box<dyn Error>> {
|
||||
let body = reqwest::get(format!("{}/api/v1/videos/{}", u, v))
|
||||
.await?
|
||||
.json::<PeerTubeVideo>()
|
||||
.await?;
|
||||
|
||||
Ok(body)
|
||||
}
|
||||
|
@@ -1,8 +1,12 @@
|
||||
use crate::{config::YoutubeConfig, error::TootubeError, peertube::PeerTubeVideo};
|
||||
use bytes::Bytes;
|
||||
use futures_core::stream::Stream;
|
||||
use reqwest::{Body, Client};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{error::Error, fs::File, sync::Mutex};
|
||||
use std::error::Error;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
static ACCESS_TOKEN: Mutex<String> = Mutex::new(String::new());
|
||||
static ACCESS_TOKEN: OnceCell<String> = OnceCell::const_new();
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct RefreshTokenRequest {
|
||||
@@ -79,9 +83,9 @@ impl Default for YoutubeUploadParamsStatus {
|
||||
}
|
||||
|
||||
/// Ensures that Token has been refreshed and that it is unique
|
||||
fn refresh_token(config: &YoutubeConfig) -> Result<String, Box<dyn Error>> {
|
||||
if let Ok(mut unlocked_access_token) = ACCESS_TOKEN.lock() {
|
||||
if unlocked_access_token.is_empty() {
|
||||
async fn refresh_token(config: &YoutubeConfig) -> Result<String, reqwest::Error> {
|
||||
ACCESS_TOKEN
|
||||
.get_or_try_init(|| async {
|
||||
let refresh_token = RefreshTokenRequest {
|
||||
refresh_token: config.refresh_token.clone(),
|
||||
client_id: config.client_id.clone(),
|
||||
@@ -89,26 +93,26 @@ fn refresh_token(config: &YoutubeConfig) -> Result<String, Box<dyn Error>> {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let client = Client::new();
|
||||
let res = client
|
||||
.post("https://accounts.google.com/o/oauth2/token")
|
||||
.json(&refresh_token)
|
||||
.send()?;
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let access_token: AccessTokenResponse = res.json()?;
|
||||
let access_token: AccessTokenResponse = res.json().await?;
|
||||
|
||||
*unlocked_access_token = access_token.access_token.clone();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ACCESS_TOKEN.lock().unwrap().to_string())
|
||||
Ok(access_token.access_token)
|
||||
})
|
||||
.await
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn create_resumable_upload(
|
||||
pub async fn create_resumable_upload(
|
||||
config: &YoutubeConfig,
|
||||
vid: &PeerTubeVideo,
|
||||
) -> Result<String, Box<dyn Error>> {
|
||||
let access_token = refresh_token(config)?;
|
||||
let access_token = refresh_token(config).await?;
|
||||
|
||||
let upload_params = YoutubeUploadParams {
|
||||
snippet: {
|
||||
@@ -126,12 +130,12 @@ pub fn create_resumable_upload(
|
||||
},
|
||||
};
|
||||
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let client = Client::new();
|
||||
|
||||
let res = client.post("https://www.googleapis.com/upload/youtube/v3/videos?uploadType=resumable&part=snippet%2Cstatus")
|
||||
.header("Authorization", format!("Bearer {}", access_token))
|
||||
.json(&upload_params)
|
||||
.send()?;
|
||||
.send().await?;
|
||||
|
||||
if res.status().is_success() {
|
||||
Ok(res
|
||||
@@ -145,28 +149,30 @@ pub fn create_resumable_upload(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn upload_video(
|
||||
f_path: &str,
|
||||
r_url: &str,
|
||||
config: &YoutubeConfig,
|
||||
pub async fn now_kiss<'a>(
|
||||
stream: impl Stream<Item = Result<Bytes, reqwest::Error>>
|
||||
+ std::marker::Send
|
||||
+ std::marker::Sync
|
||||
+ 'a + 'static,
|
||||
r_url: &'a str,
|
||||
config: &'a YoutubeConfig,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
// Get access token
|
||||
let access_token = refresh_token(config)?;
|
||||
let access_token = refresh_token(config).await?;
|
||||
|
||||
// Create client
|
||||
let client = reqwest::blocking::Client::new();
|
||||
|
||||
let file = File::open(f_path)?;
|
||||
let client = Client::new();
|
||||
|
||||
let res = client
|
||||
.put(r_url)
|
||||
.header("Authorization", format!("Bearer {}", access_token))
|
||||
.body(file)
|
||||
.send()?;
|
||||
.body(Body::wrap_stream(stream))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if res.status().is_success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(TootubeError::new(&format!("Cannot upload video: {:?}", res.text()?)).into())
|
||||
Err(TootubeError::new(&format!("Cannot upload video: {:?}", res.text().await?)).into())
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user