mirror of
https://github.com/OMGeeky/google_youtube.git
synced 2025-12-26 16:17:24 +01:00
builds under gnu stable?
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
/Cargo.lock
|
||||
25
Cargo.toml
Normal file
25
Cargo.toml
Normal file
@@ -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"
|
||||
0
data/category_ids.json
Normal file
0
data/category_ids.json
Normal file
119
src/auth.rs
Normal file
119
src/auth.rs
Normal file
@@ -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<Box<dyn Future<Output = Result<String, String>> + Send + 'a>> {
|
||||
Box::pin(present_user_url(url, need_code))
|
||||
}
|
||||
}
|
||||
|
||||
async fn present_user_url(url: &str, need_code: bool) -> Result<String, String> {
|
||||
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<String, Box<dyn Error>> {
|
||||
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<S: Into<String>>(
|
||||
path_to_application_secret: String,
|
||||
scopes: &Vec<String>,
|
||||
user: Option<S>,
|
||||
) -> Result<Authenticator<HttpsConnector<HttpConnector>>, Box<dyn Error>> {
|
||||
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<String, String> = 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)
|
||||
}
|
||||
32
src/config.rs
Normal file
32
src/config.rs
Normal file
@@ -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,
|
||||
// }
|
||||
// }
|
||||
294
src/lib.rs
Normal file
294
src/lib.rs
Normal file
@@ -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<HttpsConnector<HttpConnector>>,
|
||||
}
|
||||
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<S: Into<String>>(
|
||||
path_to_application_secret: Option<S>,
|
||||
scopes: Vec<S>,
|
||||
user: Option<S>,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
let scopes = scopes
|
||||
.into_iter()
|
||||
.map(|s| s.into())
|
||||
.collect::<Vec<String>>();
|
||||
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<HttpsConnector<HttpConnector>> = YouTube::new(hyper_client, auth);
|
||||
|
||||
let res = Self { client };
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn find_playlist_by_name(
|
||||
&self,
|
||||
name: &str,
|
||||
) -> Result<Option<Playlist>, Box<dyn Error>> {
|
||||
let part = vec!["snippet".to_string()];
|
||||
|
||||
struct PlaylistParams {
|
||||
part: Vec<String>,
|
||||
mine: bool,
|
||||
}
|
||||
async fn list_playlist(
|
||||
client: &YouTube<HttpsConnector<HttpConnector>>,
|
||||
params: &PlaylistParams,
|
||||
) -> google_youtube3::Result<(Response<Body>, PlaylistListResponse)> {
|
||||
client
|
||||
.playlists()
|
||||
.list(¶ms.part)
|
||||
.mine(params.mine)
|
||||
.doit()
|
||||
.await
|
||||
}
|
||||
let para = PlaylistParams { part, mine: true };
|
||||
let (_res, playlists): (Response<Body>, 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<Playlist, Box<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
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<HttpsConnector<HttpConnector>>,
|
||||
playlist_item: &PlaylistItem,
|
||||
) -> google_youtube3::Result<(Response<Body>, 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<S: Into<String>, V: Into<Vec<String>>>(
|
||||
&self,
|
||||
path: impl AsRef<Path>,
|
||||
title: S,
|
||||
description: S,
|
||||
tags: V,
|
||||
privacy_status: PrivacyStatus,
|
||||
) -> Result<Video, Box<dyn Error>> {
|
||||
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<HttpsConnector<HttpConnector>>,
|
||||
para: &UploadParameters,
|
||||
) -> Result<(Response<Body>, 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<Body>, 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<Playlist, Box<dyn Error>> {
|
||||
let playlist = Playlist {
|
||||
snippet: Some(PlaylistSnippet {
|
||||
title: Some(name.to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
async fn create_playlist(
|
||||
client: &YouTube<HttpsConnector<HttpConnector>>,
|
||||
params: &Playlist,
|
||||
) -> google_youtube3::Result<(Response<Body>, 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<dyn Error>> {
|
||||
println!("Hello from the youtube lib!");
|
||||
Ok(())
|
||||
}
|
||||
82
src/main.rs
Normal file
82
src/main.rs
Normal file
@@ -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<dyn Error>> {
|
||||
println!("Hello, world!");
|
||||
sample().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn sample() -> Result<(), Box<dyn Error>> {
|
||||
// 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(())
|
||||
}
|
||||
3
src/scopes.rs
Normal file
3
src/scopes.rs
Normal file
@@ -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";
|
||||
Reference in New Issue
Block a user