use anyhow::{anyhow, Context}; use std::default::Default; use std::error::Error; use std::fmt::{Debug, Formatter}; 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::PlaylistStatus, api::ResourceId, api::Video, api::VideoSnippet, api::VideoStatus, hyper::{client::HttpConnector, Body, Response}, hyper_rustls::HttpsConnector, }; #[cfg(feature = "tracing")] use tracing::instrument; use youtube::YouTube; use youtube::{hyper, hyper_rustls::HttpsConnectorBuilder}; use crate::prelude::*; mod auth; pub mod prelude; pub mod scopes; // mod config; pub struct YoutubeClient { pub client: YouTube>, } impl Debug for YoutubeClient { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("YoutubeClient").finish() } } #[derive(Debug, Copy, Clone)] 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 { #[cfg_attr(feature = "tracing", instrument)] pub async fn new( path_to_application_secret: Option + Debug>, scopes: Vec + Debug>, user: Option + Debug>, ) -> anyhow::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 = path_to_application_secret .map(|x| x.into()) .unwrap_or_else(|| { warn!("the path to the application secret was not provided. Using default!"); "auth/service_account2.json".to_string() }); trace!( "getting authenticator from path: {}", path_to_application_secret ); let auth = auth::get_authenticator(path_to_application_secret, &scopes, user) .await .map_err(|e| anyhow!("error while getting authenticator: {}", e))?; trace!("creating youtube client"); let client: YouTube> = YouTube::new(hyper_client, auth); let res = Self { client }; Ok(res) } #[cfg_attr(feature = "tracing", instrument)] pub async fn find_playlist_by_name(&self, name: &str) -> Result> { 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 .map_err(|e| anyhow!("backoff error: {}", e))? .context("list_playlist returned an error")?; 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) } #[cfg_attr(feature = "tracing", instrument)] pub async fn find_playlist_or_create_by_name( &self, name: &str, privacy: PrivacyStatus, ) -> Result { let playlist = self.find_playlist_by_name(name).await?; if let Some(playlist) = playlist { return Ok(playlist); } let playlist = self.create_playlist(name, privacy).await?; Ok(playlist) } #[cfg_attr(feature = "tracing", instrument)] pub async fn add_video_to_playlist(&self, video: &Video, playlist: &Playlist) -> Result<()> { 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 .map_err(|e| anyhow!("backoff error: {}", e))? .context("insert playlist item returned an error")?; if res.status().is_success() { Ok(()) } else { Err(anyhow!("got status: {}", res.status().as_u16())) } } #[cfg_attr(feature = "tracing", instrument)] pub async fn upload_video( &self, path: impl AsRef + Debug, title: impl Into + Debug, description: impl Into + Debug, tags: impl Into> + Debug, privacy_status: PrivacyStatus, ) -> Result