11 Commits

Author SHA1 Message Date
VC
bf622d6989 Merge branch 'rust_1_77' into 'master'
🔖: bump version and dependencies

See merge request veretcle/tootube!18
2024-04-12 07:22:09 +00:00
VC
a687f008df 🔖: bump version and dependencies 2024-04-12 09:09:45 +02:00
VC
e2ab93c04e Merge branch 'fix_title_length' into 'master'
fix: truncate YT vid title to 100 chars

See merge request veretcle/tootube!17
2023-12-28 09:40:26 +00:00
VC
9e1c27be29 fix: truncate YT vid title to 100 chars 2023-12-28 10:37:23 +01:00
VC
ab9ea1a8ee Merge branch 'tamerelol' into 'master'
feat: add --vice option

See merge request veretcle/tootube!16
2023-10-26 09:06:47 +00:00
VC
66f288c069 feat: add --vice option 2023-10-25 15:33:10 +02:00
VC
b208daa0ea Merge branch 'feat/remove_max_dl' into 'master'
Refactor: remove get_max_resolution_dl

See merge request veretcle/tootube!15
2023-10-19 09:19:02 +00:00
VC
f3ad81716d refactor: remove get_max_resolution_dl (ord makes it redundant) 2023-10-19 11:12:16 +02:00
VC
f18ce899aa chore: bump version + update dep 2023-10-19 11:01:14 +02:00
VC
bf0b1c4e3f Merge branch 'refactor_dl_url' into 'master'
refactor: regroup now_kiss and get_dl_video_stream functions

See merge request veretcle/tootube!14
2023-10-17 11:57:48 +00:00
VC
eb398a880a refactor: regroup now_kiss and get_dl_video_stream functions 2023-10-17 13:55:03 +02:00
7 changed files with 361 additions and 352 deletions

544
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "tootube" name = "tootube"
authors = ["VC <veretcle+framagit@mateu.be>"] authors = ["VC <veretcle+framagit@mateu.be>"]
version = "0.5.0" version = "0.5.5"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -14,7 +14,6 @@ serde = { version = "^1", features = ["derive"] }
toml = "^0.5" toml = "^0.5"
log = "^0.4" log = "^0.4"
env_logger = "^0.10" env_logger = "^0.10"
bytes = "^1.5"
indicatif = "^0.17" indicatif = "^0.17"
async-stream = "^0.3" async-stream = "^0.3"
futures-util = "^0.3" futures-util = "^0.3"

View File

@@ -6,6 +6,8 @@ use std::fs::read_to_string;
pub struct Config { pub struct Config {
pub peertube: PeertubeConfig, pub peertube: PeertubeConfig,
pub youtube: YoutubeConfig, pub youtube: YoutubeConfig,
#[serde(default)]
pub tootube: TootubeConfig,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@@ -20,6 +22,21 @@ pub struct YoutubeConfig {
pub client_secret: String, pub client_secret: String,
} }
#[derive(Debug, Deserialize)]
pub struct TootubeConfig {
pub progress_bar: String,
pub progress_chars: String,
}
impl Default for TootubeConfig {
fn default() -> Self {
TootubeConfig {
progress_bar: "{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})".to_string(),
progress_chars: "#>-".to_string(),
}
}
}
/// Parses the TOML file into a Config struct /// Parses the TOML file into a Config struct
pub fn parse_toml(toml_file: &str) -> Config { pub fn parse_toml(toml_file: &str) -> Config {
let toml_config = let toml_config =

View File

@@ -1,7 +1,4 @@
use bytes::Bytes;
use futures_util::Stream;
use log::debug; use log::debug;
use std::error::Error;
mod error; mod error;
@@ -10,23 +7,12 @@ pub use config::parse_toml;
use config::Config; use config::Config;
mod peertube; mod peertube;
use peertube::{get_latest_video, get_max_resolution_dl}; use peertube::get_latest_video;
mod youtube; mod youtube;
pub use youtube::register; pub use youtube::register;
use youtube::{add_video_to_playlists, create_resumable_upload, now_kiss}; use youtube::{add_video_to_playlists, create_resumable_upload, now_kiss};
async fn get_dl_video_stream(
u: &str,
) -> Result<(impl Stream<Item = Result<Bytes, reqwest::Error>>, u64), Box<dyn Error>> {
let res = reqwest::get(u).await?;
let content_lengh = res
.content_length()
.ok_or(format!("Cannot get content length from {}", u))?;
Ok((res.bytes_stream(), content_lengh))
}
#[tokio::main] #[tokio::main]
pub async fn run(config: Config, pl: Vec<String>) { pub async fn run(config: Config, pl: Vec<String>) {
// Get the latest video object // Get the latest video object
@@ -36,21 +22,28 @@ pub async fn run(config: Config, pl: Vec<String>) {
panic!("Cannot retrieve the latest video, something must have gone terribly wrong: {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 dl_url = latest_vid.streaming_playlists.as_ref().unwrap()[0]
.files
.iter()
.max()
.unwrap()
.file_download_url
.clone();
debug!("PT download URL: {}", &dl_url); debug!("PT download URL: {}", &dl_url);
let (pt_stream, size) = 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) let resumable_upload_url = create_resumable_upload(&config.youtube, &latest_vid)
.await .await
.unwrap_or_else(|e| panic!("Cannot retrieve the uploads resumable id: {e}")); .unwrap_or_else(|e| panic!("Cannot retrieve the uploads resumable id: {e}"));
debug!("YT upload URL: {}", &resumable_upload_url); debug!("YT upload URL: {}", &resumable_upload_url);
let yt_video_id = now_kiss(size, pt_stream, &resumable_upload_url, &config.youtube) let yt_video_id = now_kiss(
.await &dl_url,
.unwrap_or_else(|e| panic!("Cannot resume upload!: {e}")); &resumable_upload_url,
&config.youtube,
&config.tootube,
)
.await
.unwrap_or_else(|e| panic!("Cannot resume upload!: {e}"));
debug!("YT video ID: {}", &yt_video_id); debug!("YT video ID: {}", &yt_video_id);
if !pl.is_empty() { if !pl.is_empty() {

View File

@@ -26,6 +26,14 @@ fn main() {
.num_args(0..) .num_args(0..)
.display_order(2), .display_order(2),
) )
.arg(
Arg::new("vice")
.long("vice")
.alias("chybrare")
.alias("coquinou")
.action(clap::ArgAction::SetTrue)
.display_order(3),
)
.subcommand( .subcommand(
Command::new("register") Command::new("register")
.version(env!("CARGO_PKG_VERSION")) .version(env!("CARGO_PKG_VERSION"))
@@ -50,7 +58,12 @@ fn main() {
return; return;
} }
let config = parse_toml(matches.get_one::<String>("config").unwrap()); let mut config = parse_toml(matches.get_one::<String>("config").unwrap());
if matches.get_flag("vice") {
config.tootube.progress_bar = "{msg}\n[{elapsed_precise}] 8{wide_bar:.magenta}ᗺ {bytes}/{total_bytes} ({bytes_per_sec}, {eta})".to_string();
config.tootube.progress_chars = "=D ".to_string();
}
let playlists: Vec<String> = matches let playlists: Vec<String> = matches
.get_many::<String>("playlists") .get_many::<String>("playlists")

View File

@@ -65,11 +65,6 @@ pub async fn get_latest_video(u: &str) -> Result<PeerTubeVideo, Box<dyn Error>>
Ok(vid) Ok(vid)
} }
/// This returns the direct download URL for with the maximum resolution
pub fn get_max_resolution_dl(p: &[PeerTubeVideoStreamingPlaylists]) -> String {
p[0].files.iter().max().unwrap().file_download_url.clone()
}
/// This gets all the crispy details about one particular video /// This gets all the crispy details about one particular video
async fn get_video_detail(u: &str, v: &str) -> Result<PeerTubeVideo, Box<dyn Error>> { async fn get_video_detail(u: &str, v: &str) -> Result<PeerTubeVideo, Box<dyn Error>> {
let body = reqwest::get(format!("{}/api/v1/videos/{}", u, v)) let body = reqwest::get(format!("{}/api/v1/videos/{}", u, v))
@@ -79,34 +74,3 @@ async fn get_video_detail(u: &str, v: &str) -> Result<PeerTubeVideo, Box<dyn Err
Ok(body) Ok(body)
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_max_resolution_dl() {
let str_plist: Vec<PeerTubeVideoStreamingPlaylists> =
vec![PeerTubeVideoStreamingPlaylists {
files: vec![
PeerTubeVideoStreamingPlaylistsFiles {
id: 1025,
resolution: PeerTubeVideoStreamingPlaylistsFilesResolution { id: 990 },
file_download_url: "meh!".to_string(),
},
PeerTubeVideoStreamingPlaylistsFiles {
id: 1027,
resolution: PeerTubeVideoStreamingPlaylistsFilesResolution { id: 1720 },
file_download_url: "blah".to_string(),
},
PeerTubeVideoStreamingPlaylistsFiles {
id: 1026,
resolution: PeerTubeVideoStreamingPlaylistsFilesResolution { id: 360 },
file_download_url: "nope".to_string(),
},
],
}];
assert_eq!("blah".to_string(), get_max_resolution_dl(&str_plist));
}
}

View File

@@ -1,17 +1,15 @@
use crate::{config::YoutubeConfig, error::TootubeError, peertube::PeerTubeVideo}; use crate::{
config::{TootubeConfig, YoutubeConfig},
error::TootubeError,
peertube::PeerTubeVideo,
};
use async_stream::stream; use async_stream::stream;
use bytes::Bytes; use futures_util::StreamExt;
use futures_util::{Stream, StreamExt};
use indicatif::{ProgressBar, ProgressStyle}; use indicatif::{ProgressBar, ProgressStyle};
use log::debug; use log::{debug, warn};
use reqwest::{multipart::Form, Body, Client}; use reqwest::{multipart::Form, Body, Client};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{cmp::min, error::Error, io::stdin};
cmp::min,
error::Error,
io::stdin,
marker::{Send, Sync, Unpin},
};
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
static ACCESS_TOKEN: OnceCell<String> = OnceCell::const_new(); static ACCESS_TOKEN: OnceCell<String> = OnceCell::const_new();
@@ -311,10 +309,17 @@ pub async fn create_resumable_upload(
) -> Result<String, Box<dyn Error>> { ) -> Result<String, Box<dyn Error>> {
let access_token = refresh_token(config).await?; let access_token = refresh_token(config).await?;
if vid.name.chars().count() > 100 {
warn!(
"PT Video Title ({}) is too long, it will be truncated",
&vid.name
);
}
let upload_params = YoutubeUploadParams { let upload_params = YoutubeUploadParams {
snippet: { snippet: {
YoutubeUploadParamsSnippet { YoutubeUploadParamsSnippet {
title: vid.name.clone(), title: vid.name.chars().take(100).collect::<String>(),
description: vid.description.clone(), description: vid.description.clone(),
tags: vid.tags.clone(), tags: vid.tags.clone(),
..Default::default() ..Default::default()
@@ -347,30 +352,40 @@ pub async fn create_resumable_upload(
} }
/// This takes the PT stream for download, connects it to YT stream for upload /// This takes the PT stream for download, connects it to YT stream for upload
pub async fn now_kiss<'a>( pub async fn now_kiss(
size: u64, dl_url: &str,
mut stream: impl Stream<Item = Result<Bytes, reqwest::Error>> + Send + Sync + Unpin + 'a + 'static, r_url: &str,
r_url: &'a str, config: &YoutubeConfig,
config: &'a YoutubeConfig, pg_conf: &TootubeConfig,
) -> Result<String, Box<dyn Error>> { ) -> Result<String, Box<dyn Error>> {
// Get access token // Get access token
let access_token = refresh_token(config).await?; let access_token = refresh_token(config).await?;
// Get the upstream bytes stream
let res = reqwest::get(dl_url).await?;
let content_lengh = res
.content_length()
.ok_or(format!("Cannot get content length from {}", dl_url))?;
let mut stream = res.bytes_stream();
// Create the progress bar // Create the progress bar
let pb = ProgressBar::new(size); let pb = ProgressBar::new(content_lengh);
pb.set_style(ProgressStyle::default_bar() pb.set_style(
.template("{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})")? ProgressStyle::default_bar()
.progress_chars("#>-")); .template(&pg_conf.progress_bar)?
.progress_chars(&pg_conf.progress_chars),
);
pb.set_message("Transferring…"); pb.set_message("Transferring…");
let mut transferring: u64 = 0; let mut transferring: u64 = 0;
// yields the stream chunk by chunk, updating the progress bar at the same time
let async_stream = stream! { let async_stream = stream! {
while let Some(chunk) = stream.next().await { while let Some(chunk) = stream.next().await {
if let Ok(chunk) = &chunk { if let Ok(chunk) = &chunk {
let new = min(transferring + (chunk.len() as u64), size); let new = min(transferring + (chunk.len() as u64), content_lengh);
transferring = new; transferring = new;
pb.set_position(new); pb.set_position(new);
if transferring >= size { if transferring >= content_lengh {
pb.finish(); pb.finish();
} }
} }