From 1938f32e3ef90beb87da8171593907901a27d2be Mon Sep 17 00:00:00 2001 From: OMGeeky Date: Sat, 1 Apr 2023 15:00:16 +0200 Subject: [PATCH] builds under gnu stable? --- .gitignore | 2 + Cargo.toml | 25 ++++ data/category_ids.json | 0 src/auth.rs | 119 +++++++++++++++++ src/config.rs | 32 +++++ src/lib.rs | 294 +++++++++++++++++++++++++++++++++++++++++ src/main.rs | 82 ++++++++++++ src/scopes.rs | 3 + 8 files changed, 557 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 data/category_ids.json create mode 100644 src/auth.rs create mode 100644 src/config.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/scopes.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8312803 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "google_youtube" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +downloader_config = { path = "../downloader_config" } +exponential_backoff = { path = "../exponential_backoff" } + +google-youtube3 = "4.0.1" + +reqwest = { version = "0.11.13", features = ["default", "json"] } +tokio = { version = "1.23.0", features = ["full"] } +serde = { version = "1.0.130", features = ["derive", "default"] } +serde_json = "1.0" + +async-trait = "0.1.60" +strfmt = "0.2.2" + + +#[patch.crates-io.yup-oauth2] +#path = "../../Documents/GitHub/OMGeeky/yup-oauth2" +#version = "7.1.0" diff --git a/data/category_ids.json b/data/category_ids.json new file mode 100644 index 0000000..e69de29 diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..1cfa268 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,119 @@ +use std::collections::HashMap; +use std::error::Error; +use std::future::Future; +use std::path::Path; +use std::pin::Pin; +use std::time::Duration; + +use google_youtube3::hyper::client::HttpConnector; +use google_youtube3::hyper_rustls::HttpsConnector; +use google_youtube3::oauth2; +use google_youtube3::oauth2::authenticator::Authenticator; +use google_youtube3::oauth2::authenticator_delegate::InstalledFlowDelegate; +use strfmt::strfmt; +use tokio::time::sleep; + +use crate::auth; +use downloader_config::{load_config, Config}; + +struct CustomFlowDelegate {} + +impl InstalledFlowDelegate for CustomFlowDelegate { + fn redirect_uri(&self) -> Option<&str> { + if load_config().use_local_auth_redirect { + Some("http://localhost:8080/googleapi/auth") + } else { + Some("https://game-omgeeky.de:7443/googleapi/auth") + } + } + + fn present_user_url<'a>( + &'a self, + url: &'a str, + need_code: bool, + ) -> Pin> + Send + 'a>> { + Box::pin(present_user_url(url, need_code)) + } +} + +async fn present_user_url(url: &str, need_code: bool) -> Result { + println!("Please open this URL in your browser:\n{}\n", url); + if need_code { + let conf = load_config(); + + let mut code = String::new(); + if conf.use_file_auth_response { + code = get_auth_code(&conf).await.unwrap_or("".to_string()); + } else { + println!("Enter the code you get after authorization here: "); + std::io::stdin().read_line(&mut code).unwrap(); + } + Ok(code.trim().to_string()) + } else { + println!("No code needed"); + Ok("".to_string()) + } +} + +async fn get_auth_code(config: &Config) -> Result> { + let code: String; + + let path = &config.path_auth_code; + let path = Path::new(path); + if let Err(e) = std::fs::remove_file(path) { + if e.kind() != std::io::ErrorKind::NotFound { + println!("Error removing file: {:?}", e); + panic!("Error removing file: {:?}", e); + } + } + + println!("Waiting for auth code in file: {}", path.display()); + loop { + let res = std::fs::read_to_string(path); //try reading the file + if let Ok(content) = res { + let l = content.lines().next(); //code should be on first line of the file + let re = match l { + Some(s) => s, + None => { + sleep(Duration::from_secs(config.auth_file_read_timeout)).await; + continue; + } + }; + + code = re.to_string(); + // std::fs::remove_file(path)?; + break; + } + // wait a few seconds + sleep(Duration::from_secs(config.auth_file_read_timeout)).await; + } + + Ok(code) +} + +pub(crate) async fn get_authenticator>( + path_to_application_secret: String, + scopes: &Vec, + user: Option, +) -> Result>, Box> { + let app_secret = oauth2::read_application_secret(path_to_application_secret).await?; + let method = oauth2::InstalledFlowReturnMethod::Interactive; + let config = load_config(); + let mut vars: HashMap = HashMap::new(); + let user = match user { + Some(u) => u.into(), + None => "unknown".to_string(), + }; + vars.insert("user".to_string(), user.clone()); + let persistent_path: String = + strfmt(&config.path_authentications, &vars).expect("Error formatting path"); + println!("Persistent auth path for user:{} => {}", user, persistent_path); + let auth = oauth2::InstalledFlowAuthenticator::builder(app_secret, method) + .flow_delegate(Box::new(auth::CustomFlowDelegate {})) + .persist_tokens_to_disk(persistent_path) + .build() + .await?; //TODO: somehow get rid of this unwrap that is happening in the library + + auth.token(&scopes).await?; + Ok(auth) +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..a053c9d --- /dev/null +++ b/src/config.rs @@ -0,0 +1,32 @@ +// use std::env; +// +// #[derive(Clone)] +// pub struct Config { +// pub path_auth_code: String, +// pub path_authentications: String, +// pub use_file_auth_response: bool, +// pub use_local_auth_redirect: bool, +// pub auth_file_read_timeout: u64, +// } +// +// pub fn load_config() -> Config { +// let path_auth_code = +// env::var("PATH_AUTH_CODE").unwrap_or("/tmp/twba/auth/code.txt".to_string()); +// let path_authentications = +// env::var("PATH_AUTHENTICATIONS").unwrap_or("/tmp/twba/auth/{user}.json".to_string()); +// let use_file_auth_response = +// env::var("USE_FILE_AUTH_RESPONSE").unwrap_or("1".to_string()) == "1"; +// let use_local_auth_redirect = +// env::var("USE_LOCAL_AUTH_REDIRECT").unwrap_or("0".to_string()) == "1"; +// let auth_file_read_timeout = env::var("AUTH_FILE_READ_TIMEOUT") +// .unwrap_or("5".to_string()) +// .parse() +// .unwrap(); +// Config { +// path_auth_code, +// use_file_auth_response, +// path_authentications, +// use_local_auth_redirect, +// auth_file_read_timeout, +// } +// } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..fc5f397 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,294 @@ +use std::default::Default; +use std::error::Error; +use std::path::{Path, PathBuf}; + +use exponential_backoff::youtube::generic_check_backoff_youtube; +use google_youtube3::{ + self as youtube, + api::Playlist, + api::PlaylistItem, + api::PlaylistItemSnippet, + api::PlaylistListResponse, + api::PlaylistSnippet, + api::ResourceId, + api::Video, + api::VideoSnippet, + api::VideoStatus, + hyper::client::HttpConnector, + hyper::{Body, Response}, + hyper_rustls::HttpsConnector, +}; +use youtube::YouTube; +use youtube::{hyper, hyper_rustls::HttpsConnectorBuilder}; + +mod auth; +pub mod scopes; +// mod config; + +pub struct YoutubeClient { + pub client: YouTube>, +} +pub enum PrivacyStatus { + Public, + Unlisted, + Private, +} +impl PrivacyStatus { + fn to_string(&self) -> String { + match self { + PrivacyStatus::Public => "public".to_string(), + PrivacyStatus::Unlisted => "unlisted".to_string(), + PrivacyStatus::Private => "private".to_string(), + } + } +} +impl YoutubeClient { + pub async fn new>( + path_to_application_secret: Option, + scopes: Vec, + user: Option, + ) -> Result> { + let scopes = scopes + .into_iter() + .map(|s| s.into()) + .collect::>(); + let hyper_client = hyper::Client::builder().build( + HttpsConnectorBuilder::new() + .with_native_roots() + .https_or_http() + .enable_http1() + .enable_http2() + .build(), + ); + + let path_to_application_secret = match path_to_application_secret { + None => "auth/service_account2.json".to_string(), + Some(s) => s.into(), + }; + + let auth = auth::get_authenticator(path_to_application_secret, &scopes, user).await?; + + let client: YouTube> = YouTube::new(hyper_client, auth); + + let res = Self { client }; + Ok(res) + } + + pub async fn find_playlist_by_name( + &self, + name: &str, + ) -> Result, Box> { + let part = vec!["snippet".to_string()]; + + struct PlaylistParams { + part: Vec, + mine: bool, + } + async fn list_playlist( + client: &YouTube>, + params: &PlaylistParams, + ) -> google_youtube3::Result<(Response, PlaylistListResponse)> { + client + .playlists() + .list(¶ms.part) + .mine(params.mine) + .doit() + .await + } + let para = PlaylistParams { part, mine: true }; + let (_res, playlists): (Response, PlaylistListResponse) = + generic_check_backoff_youtube(&self.client, ¶, list_playlist).await??; + + if let Some(items) = playlists.items { + for element in items { + if let Some(snippet) = &element.snippet { + if let Some(title) = &snippet.title { + if title == name { + return Ok(Some(element)); + } + } + } + } + } + Ok(None) + } + + pub async fn find_playlist_or_create_by_name( + &self, + name: &str, + ) -> Result> { + let playlist = self.find_playlist_by_name(name).await?; + if let Some(playlist) = playlist { + return Ok(playlist); + } + let playlist = self.create_playlist(name).await?; + Ok(playlist) + } + + pub async fn add_video_to_playlist( + &self, + video: &Video, + playlist: &Playlist, + ) -> Result<(), Box> { + let playlist_item = PlaylistItem { + snippet: Some(PlaylistItemSnippet { + playlist_id: Some(playlist.id.clone().unwrap()), + resource_id: Some(ResourceId { + kind: Some("youtube#video".to_string()), + video_id: Some(video.id.clone().unwrap()), + ..Default::default() + }), + ..Default::default() + }), + ..Default::default() + }; + async fn insert_playlist_item( + client: &YouTube>, + playlist_item: &PlaylistItem, + ) -> google_youtube3::Result<(Response, PlaylistItem)> { + client + .playlist_items() + .insert(playlist_item.clone()) + .doit() + .await + } + + // let res = self.client.playlist_items().insert(playlist_item).doit().await?; + + let (res, _) = + generic_check_backoff_youtube(&self.client, &playlist_item, insert_playlist_item) + .await??; + if res.status().is_success() { + Ok(()) + } else { + Err(format!("got status: {}", res.status().as_u16()).into()) + } + } + + pub async fn upload_video, V: Into>>( + &self, + path: impl AsRef, + title: S, + description: S, + tags: V, + privacy_status: PrivacyStatus, + ) -> Result> { + println!("test 123"); + let video = Video { + snippet: Some(VideoSnippet { + title: Some(title.into()), + description: Some(description.into()), + category_id: Some("20".to_string()), + tags: Some(tags.into()), + ..Default::default() + }), + + status: Some(VideoStatus { + privacy_status: Some(privacy_status.to_string()), + public_stats_viewable: Some(true), + embeddable: Some(true), + self_declared_made_for_kids: Some(false), + ..Default::default() + }), + ..Default::default() + }; + // let file = file.into_std().await; + + struct UploadParameters { + video: Video, + path: PathBuf, + } + + let params = UploadParameters { + video: video.clone(), + path: path.as_ref().into(), + }; + + async fn upload_fn( + client: &YouTube>, + para: &UploadParameters, + ) -> Result<(Response, Video), google_youtube3::Error> { + println!("Opening file: {:?}", para.path); + let stream = std::fs::File::open(¶.path)?; + println!("Uploading file: {:?}", para.path); + let insert_call = client + .videos() + .insert(para.video.clone()); + println!("Insert call created"); + let res = insert_call + .upload(stream, "video/mp4".parse().unwrap()); + println!("Upload request"); + res + .await + } + + println!("Starting upload..."); + let (response, video) = + generic_check_backoff_youtube(&self.client, ¶ms, upload_fn).await??; + + // let (response, video) = exponential_backoff::youtube::check_backoff_youtube_upload( + // &self.client, + // video, + // &path, + // "video/mp4".parse().unwrap(), + // ) + // .await??; + + if response.status().is_success() { + println!("Upload successful!"); + Ok(video) + } else { + println!("Upload failed!\n=====================================\n"); + println!("Status: {}", response.status()); + println!("Body: {:?}", response); + println!("Video: {:?}", video); + Err(format!("got status: {}", response.status().as_u16()).into()) + } + + // return Ok(video); + // let insert: google_youtube3::Result<(Response, Video)> = self + // .client + // .videos() + // .insert(video) + // .upload(file, "video/mp4".parse().unwrap()) + // .await; + // + // match insert { + // Ok(insert) => Ok(insert), + // Err(e) => { + // println!("Error: {:?}", e); + // Err(Box::new(e)) + // } + // } + } + async fn create_playlist(&self, name: &str) -> Result> { + let playlist = Playlist { + snippet: Some(PlaylistSnippet { + title: Some(name.to_string()), + ..Default::default() + }), + ..Default::default() + }; + + async fn create_playlist( + client: &YouTube>, + params: &Playlist, + ) -> google_youtube3::Result<(Response, Playlist)> { + client.playlists().insert(params.clone()).doit().await + } + + let (res, playlist) = + generic_check_backoff_youtube(&self.client, &playlist, create_playlist).await??; + + if res.status().is_success() { + Ok(playlist) + } else { + Err(format!("got status: {}", res.status().as_u16()).into()) + } + } +} + +pub async fn sample() -> Result<(), Box> { + println!("Hello from the youtube lib!"); + Ok(()) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9a0f492 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,82 @@ +use std::error::Error; +use std::path::Path; +use google_youtube3::api::Playlist; + +use tokio::fs::File; + +use google_youtube::{PrivacyStatus, scopes, YoutubeClient}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("Hello, world!"); + sample().await?; + Ok(()) +} + +pub async fn sample() -> Result<(), Box> { + // get client + let scopes = vec![ + // google_youtube::scopes::YOUTUBE, + google_youtube::scopes::YOUTUBE_UPLOAD, + google_youtube::scopes::YOUTUBE_READONLY, + ]; + // let client_secret_path = "auth/youtube_client_secret.json"; + let client_secret_path = "auth/test_rust_client_secret_2.json"; + let user = "nopixelvods"; + let client = YoutubeClient::new(Some(client_secret_path), scopes, Some(user)).await?; + + /* + // get list of channels of the authenticated user + let part = vec!["snippet".to_string()]; + let (_res, channels) = client + .client + .channels() + .list(&part) + .mine(true) + .doit() + .await?; + for element in channels.items.unwrap() { + println!( + "channel name: {:?}", + element.snippet.unwrap().title.unwrap() + ); + } + + println!("Channels done!\n\n"); + */ + + // get a playlist by name or create it if it does not exist('LunaOni Clips' for example) + let playlist = client.find_playlist_or_create_by_name("LunaOni Clips").await; + println!("playlist: {:?}", playlist); + + println!("Playlist done!\n\n"); + println!("Uploading video... (30 times"); + for i in 0..30 { + println!("+==={:2}==;uploading video...", i); + let path = Path::new("test/test.mp4"); + // let file = File::open(path).await?; + let description = "test video description"; + let title = "test video2"; + let tags = vec!["test".to_string(), "test2".to_string()]; + let privacy_status = PrivacyStatus::Private; + + println!("uploading video..."); + let insert = client + .upload_video(&path, description, title, tags, privacy_status) + .await; + println!("uploading video... (done)"); + + println!("adding to playlist..."); + if let Ok(video) = &insert{ + if let Ok(playlist) = &playlist { + println!("adding video to playlist: {:?}", playlist); + let _ = client.add_video_to_playlist(&video, &playlist).await; + } + } + println!("adding to playlist... (done)"); + + println!("\n\n{:?}\n\n/==={:2}========;", insert, i); + } + println!("Done!"); + Ok(()) +} diff --git a/src/scopes.rs b/src/scopes.rs new file mode 100644 index 0000000..474dc46 --- /dev/null +++ b/src/scopes.rs @@ -0,0 +1,3 @@ +pub const YOUTUBE_READONLY: &str = "https://www.googleapis.com/auth/youtube.readonly"; +pub const YOUTUBE: &str = "https://www.googleapis.com/auth/youtube"; +pub const YOUTUBE_UPLOAD: &str = "https://www.googleapis.com/auth/youtube.upload"; \ No newline at end of file