Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/kasper9n/kadium. App for staying ontop of YouTube channels' uploads kadium.kasper.space
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197use crate::throw;use serde::de::DeserializeOwned;
pub async fn yt_request<T: DeserializeOwned>(url: &str, key: &str) -> Result<T, String> { let client = reqwest::Client::new(); let json: serde_json::Value = client .get(url) .header("X-Goog-Api-Key", key) .send() .await .map_err(|e| format!("API request failed: {}", e))? .json() .await .map_err(|e| format!("API response was not JSON: {}", e))?;
match json.get("error") { Some(error_obj) => { let code = error_obj.get("code").and_then(|v| v.as_i64()); let code_str = code.map(|n| n.to_string()).unwrap_or_default(); let message = error_obj.get("message").and_then(|v| v.as_str()); println!("{:?}", json); throw!("{} {}", code_str, message.unwrap_or_default()); } _ => {} } match serde_json::from_value::<T>(json) { Ok(v) => Ok(v), Err(e) => { throw!("Unexpected API response: {}", e); } }}
pub async fn channel_id_from_video_id(id: &str, key: &str) -> Result<String, String> { let url = "https://youtube.googleapis.com/youtube/v3/videos".to_string() + "?part=snippet" + "&id=" + id; let videos = yt_request::<videos::Response>(&url, key) .await .map_err(|e| format!("Failed to get video: {}", e))?; if let Some(video) = videos.items.first() { return Ok(video.snippet.channelId.clone()); } Err("No video returned".to_string())}
pub async fn channel_id_from_username(username: &str, key: &str) -> Result<String, String> { let url = "https://youtube.googleapis.com/youtube/v3/channels".to_string() + "?part=contentDetails,id,snippet" + "&forUsername=" + username; let channels = yt_request::<channels::Response>(&url, key) .await .map_err(|e| format!("Failed to get video: {}", e))?; if channels.items.len() > 1 { return Err("YouTube username search returned in multiple channels".to_string()); } if let Some(channel) = channels.items.first() { return Ok(channel.id.clone()); } Err("No video returned".to_string())}
pub mod channels { use serde::Deserialize;
/// Lists the fields we use only. Documentation: /// https://developers.google.com/youtube/v3/docs/channels/list#properties #[derive(Deserialize, Debug)] pub struct Response { pub items: Vec<Channel>, } #[derive(Deserialize, Debug)] #[allow(non_snake_case)] pub struct Channel { pub id: String, pub contentDetails: ContentDetails, pub snippet: Snippet, }
#[derive(Deserialize, Debug)] #[allow(non_snake_case)] pub struct ContentDetails { pub relatedPlaylists: RelatedPlaylists, } #[derive(Deserialize, Debug)] pub struct RelatedPlaylists { pub uploads: String, }
#[derive(Deserialize, Debug)] pub struct Snippet { pub title: String, pub thumbnails: Thumbnails, } /// default 88x88, medium 240x240, high 800x800 #[derive(Deserialize, Debug)] pub struct Thumbnails { pub medium: Thumbnail, } #[derive(Deserialize, Debug)] pub struct Thumbnail { pub url: String, }}
pub mod videos { use serde::Deserialize;
/// Lists the fields we use only. Documentation: /// https://developers.google.com/youtube/v3/docs/videos/list#properties #[derive(Deserialize, Debug)] pub struct Response { pub items: Vec<Video>, } #[derive(Deserialize, Debug)] #[allow(non_snake_case)] pub struct Video { pub id: String, pub contentDetails: Option<ContentDetails>, pub liveStreamingDetails: Option<LiveStreamingDetails>, pub snippet: Snippet, } #[derive(Deserialize, Debug)] pub struct ContentDetails { pub duration: Option<String>, } #[derive(Deserialize, Debug)] #[allow(non_snake_case)] pub struct LiveStreamingDetails { pub actualStartTime: Option<String>, }
#[derive(Deserialize, Debug)] #[allow(non_snake_case)] pub struct Snippet { pub publishedAt: String, pub title: String, pub description: String, pub thumbnails: Thumbnails, pub channelId: String, pub channelTitle: String, } /// default, medium and high always exist: /// default 120x90: https://i.ytimg.com/vi/___ID___/default.jpg /// medium 320x180: https://i.ytimg.com/vi/___ID___/mqdefault.jpg /// high 480x360: https://i.ytimg.com/vi/___ID___/hqdefault.jpg /// standard 640x480: https://i.ytimg.com/vi/___ID___/sddefault.jpg /// maxres 1280x720: https://i.ytimg.com/vi/___ID___/maxresdefault.jpg #[derive(Deserialize, Debug)] pub struct Thumbnails { pub standard: Option<Thumbnail>, pub maxres: Option<Thumbnail>, } #[derive(Deserialize, Debug)] pub struct Thumbnail { #[allow(dead_code)] pub url: String, }}
pub mod playlist_items { use serde::Deserialize;
/// Lists the fields we use only. Documentation: /// https://developers.google.com/youtube/v3/docs/playlistItems/list#properties #[derive(Deserialize, Debug)] pub struct Response { pub items: Vec<Playlist>, } /// weird date situation: /// `snippet.publishedAt` is when the video was added to the uploads playlist. /// `contentDetails.videoPublishedAt` is when the video was published /// Soemtimes these are a few seconds different, other times an hour /// (like with Monstercat). No idea why. /// Additionally, I tried to compare with what YouTube shows: /// - What YouTube shows: 19:56 (should be up to 1 hour inaccurate) /// - publishedAt: 17:31 /// - 18:00 /// YouTube only shows "9 hours ago", so you'd expect it to be up to /// an hour off... But it's almost 2 hours off, if not 2.5 hours. /// :/ #[derive(Deserialize, Debug)] #[allow(non_snake_case)] pub struct Playlist { pub contentDetails: ContentDetails, } #[derive(Deserialize, Debug)] #[allow(non_snake_case)] pub struct ContentDetails { /// Soemtimes videoPublishedAt is missing when videos are privated, but still included in the channel video list. pub videoPublishedAt: Option<String>, pub videoId: String, }}