diff --git a/Cargo.lock b/Cargo.lock index d1f4b88..0bf12e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1525,7 +1525,7 @@ dependencies = [ ] [[package]] -name = "youtube" +name = "youtube-api" version = "0.1.0" dependencies = [ "json", diff --git a/Cargo.toml b/Cargo.toml index a32cbe8..355e5ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,11 @@ [package] -name = "youtube" +name = "youtube-api" version = "0.1.0" edition = "2021" +[lib] +crate-type = ["staticlib", "cdylib", "lib"] + [dependencies] reqwest = { version = "0.12.12", features = ["json"] } tokio = { version = "1", features = ["full"] } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..1deda2b --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,171 @@ +use std::error::Error; + +use objects::{YoutubePlaylist, YoutubeVideo}; + +pub mod objects; + +const YOUTUBE_API_URL: &str = "https://www.youtube.com/youtubei/v1/browse?prettyPrint=false"; + +/*#[tokio::main] +async fn main() -> Result<(), Box> { + println!("Hello, world!!"); + let rid = get_channel("".to_string()).await.unwrap(); + println!("rid: {}", rid); + for playlist in &get_playlists("".to_string(), rid).await.unwrap() { + println!("Playlist: {}", playlist.title); + let videos = get_playlist(playlist.browse_id.clone()).await.unwrap(); + println!("{:?}", videos); + } + Ok(()) +}*/ + +pub async fn get_playlist(browse_id: String) -> Result, Box> { + let client = reqwest::Client::new(); + + let d= r#"{ + "context": { + "client": { + "clientName": "WEB", + "clientVersion": "2.20201210.01.00", + "originalUrl": "https://www.youtube.com/", + "platform": "DESKTOP", + "clientFormFactor": "UNKNOWN_FORM_FACTOR", + "newVisitorCookie": true + } + }, + "browseId": "browse_id", + }"#; + + let d= d.replace("browse_id", &browse_id); + + + let resp = client.post(YOUTUBE_API_URL) + .body(d) + .send() + .await? + .text() + .await?; + + let mut videos = Vec::new(); + + let resp = json::parse(&resp).unwrap(); + + if let json::JsonValue::Array(resp) = &resp["contents"]["twoColumnBrowseResultsRenderer"]["tabs"] { + let tab = &resp[0]; + if let json::JsonValue::Array(contents) = &tab["tabRenderer"]["content"]["sectionListRenderer"]["contents"][0]["itemSectionRenderer"]["contents"][0]["playlistVideoListRenderer"]["contents"] { + for video in contents { + let video = &video["playlistVideoRenderer"]; + let mut yt = YoutubeVideo::default(); + yt.videoId = video["videoId"].to_string(); + yt.title = video["title"]["runs"][0]["text"].to_string(); + yt.publisher = video["shortBylineText"]["runs"][0]["text"].to_string(); + yt.lengthSeconds = video["lengthSeconds"].to_string().parse().unwrap(); + videos.push(yt); + } + } + } + + Ok(videos) +} + +pub async fn get_playlists(channel_id: String, request_id: String) -> Result, Box> { + let client = reqwest::Client::new(); + + let d= r#"{ + "context": { + "client": { + "clientName": "WEB", + "clientVersion": "2.20201210.01.00", + "originalUrl": "https://www.youtube.com/", + "platform": "DESKTOP", + "clientFormFactor": "UNKNOWN_FORM_FACTOR", + "newVisitorCookie": true + } + }, + "browseId": "channel_id", + "params": "request_id" + }"#; + + let d = d.replace("channel_id", &channel_id); + let d = d.replace("request_id", &request_id); + + let resp = client.post(YOUTUBE_API_URL) + .body(d) + .send() + .await? + .text() + .await?; + + let resp = json::parse(&resp).unwrap(); + + let mut playlists = Vec::new(); + + if let json::JsonValue::Array(resp) = &resp["contents"]["twoColumnBrowseResultsRenderer"]["tabs"] { + for tab in resp { + if let json::JsonValue::Short(title) = &tab["tabRenderer"]["title"] { + if title.as_str() == "Playlists" { + if let json::JsonValue::Array(contents) = &tab["tabRenderer"]["content"]["sectionListRenderer"]["contents"] { + if let json::JsonValue::Array(columns) = &contents[0]["itemSectionRenderer"]["contents"] { + for column in columns { + if let json::JsonValue::Array(items) = &column["gridRenderer"]["items"] { + for item in items { + let item = &item["lockupViewModel"]["metadata"]["lockupMetadataViewModel"]; + let title = item["title"]["content"].as_str().unwrap(); + let browse_id = item["metadata"]["contentMetadataViewModel"]["metadataRows"][1]["metadataParts"][0]["text"]["commandRuns"][0]["onTap"]["innertubeCommand"]["browseEndpoint"]["browseId"].as_str().unwrap(); + let pl_url = item["metadata"]["contentMetadataViewModel"]["metadataRows"][1]["metadataParts"][0]["text"]["commandRuns"][0]["onTap"]["innertubeCommand"]["commandMetadata"]["webCommandMetadata"]["url"].as_str().unwrap(); + playlists.push(YoutubePlaylist{ title: title.to_string(), browse_id: browse_id.to_string(), pl_url: pl_url.to_string()}); + } + } + } + } + } + } + } + } + } + Ok(playlists) +} + +pub async fn get_channel(channel_id: String) -> Result>{ + let client = reqwest::Client::new(); + + let d = r#"{ + "context": { + "client": { + "clientName": "WEB", + "clientVersion": "2.20201210.01.00", + "originalUrl": "https://www.youtube.com/", + "platform": "DESKTOP", + "clientFormFactor": "UNKNOWN_FORM_FACTOR", + "newVisitorCookie": true + } + }, + "browseId": "browse_id", + "params": "EglwbGF5bGlzdHPyBgQKAkIA" + }"#; + + let d = d.replace("browse_id", &channel_id); + + let resp = client.post(YOUTUBE_API_URL) + .body(d) + .send() + .await? + .text() + .await?; + + let resp = json::parse(&resp).unwrap(); + + if let json::JsonValue::Array(resp) = &resp["contents"]["twoColumnBrowseResultsRenderer"]["tabs"] { + for tab in resp { + if let json::JsonValue::Short(title) = &tab["tabRenderer"]["title"] { + if title.as_str() == "Playlists" { + if let json::JsonValue::Short(params) = &tab["tabRenderer"]["endpoint"]["browseEndpoint"]["params"] { + return Ok(params.to_string()); + } + } + } + } + } + + Ok("".to_string()) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 04a7943..0000000 --- a/src/main.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::error::Error; - -use objects::YoutubeChannel; - -mod objects; - -#[tokio::main] -async fn main() -> Result<(), Box> { - println!("Hello, world!!"); - let rid = get_channel("UCpDEe6JKiI5o8luXpk6UuiA".to_string()).await.unwrap(); - get_playlists("UCpDEe6JKiI5o8luXpk6UuiA".to_string(), rid).await; - Ok(()) -} - -pub async fn get_playlists(channel_id: String, request_id: String) -> Result<(), Box> { - let client = reqwest::Client::new(); - - let d= serde_json::json!({ - "context": { - "client": { - "clientName": "WEB", - "clientVersion": "2.20201210.01.00", - "originalUrl": "https://www.youtube.com/", - "platform": "DESKTOP", - "clientFormFactor": "UNKNOWN_FORM_FACTOR", - "newVisitorCookie": true - } - }, - "browseId": channel_id, - "params": request_id - }); - - let resp = client.post("https://www.youtube.com/youtubei/v1/browse?prettyPrint=false") - .body(d.to_string()) - .send() - .await? - .text() - .await?; - - println!("{}", resp); - - let resp: YoutubeChannel = serde_json::from_str(&resp).unwrap(); - - println!("{:?}", resp.get_playlists()); - Ok(()) -} - -pub async fn get_channel(channel_id: String) -> Result>{ - let client = reqwest::Client::new(); - - let resp = client.post("https://www.youtube.com/youtubei/v1/browse?prettyPrint=false") - .body(r#"{ - "context": { - "client": { - "clientName": "WEB", - "clientVersion": "2.20201210.01.00", - "originalUrl": "https://www.youtube.com/", - "platform": "DESKTOP", - "clientFormFactor": "UNKNOWN_FORM_FACTOR", - "newVisitorCookie": true - } - }, - "browseId": "UCpDEe6JKiI5o8luXpk6UuiA", - "params": "EgZ2aWRlb3M%3D" - }"#) - .send() - .await? - .text() - .await?; - - let resp: YoutubeChannel = serde_json::from_str(&resp).unwrap(); - - Ok(resp.get_playlists_request_str().unwrap()) -} \ No newline at end of file diff --git a/src/objects.rs b/src/objects.rs index 8ef4af5..8922abd 100644 --- a/src/objects.rs +++ b/src/objects.rs @@ -1,157 +1,13 @@ -use serde::Deserialize; - - -#[derive(Deserialize)] -pub struct YoutubeChannel { - pub contents: ChannelContents, -} - -#[derive(Deserialize)] -pub struct ChannelContents { - pub twoColumnBrowseResultsRenderer: TwoColumns, -} - -#[derive(Deserialize)] -pub struct TwoColumns { - pub tabs: Vec -} - -#[derive(Deserialize)] -pub struct ChannelTab { - pub tabRenderer: Option -} - -#[derive(Deserialize)] -pub struct TabRenderer { +pub struct YoutubePlaylist { pub title: String, - pub endpoint: TabEndpoint, - pub content: Option + pub browse_id: String, + pub pl_url: String } -#[derive(Deserialize)] -pub struct SectionListRenderer{ - pub contents: Option> -} - -#[derive(Deserialize)] -pub struct Content { - pub itemSectionRenderer: Option -} - -#[derive(Deserialize)] -pub struct ItemSectionRenderer { - pub contents: Vec -} - -#[derive(Deserialize)] -pub struct ItemSectionContent { - pub gridRenderer: GridRenderer -} - -#[derive(Deserialize)] -pub struct GridRenderer { - pub items: Vec -} - -#[derive(Deserialize)] -pub struct GridItem { - pub lockupViewModel: LookUpViewModel -} - -#[derive(Deserialize)] -pub struct LookUpViewModel { - pub metadata: ModelMetadata -} - -#[derive(Deserialize)] -pub struct ModelMetadata { - pub lockupMetadataViewModel: LockupMetadataViewModel -} - -#[derive(Deserialize)] -pub struct LockupMetadataViewModel { - pub title: PlaylistTitle, - pub metadata: PlaylistMetadata, -} - -#[derive(Deserialize)] -pub struct PlaylistMetadata { - pub contentMetadataViewModel: ContentMetadataViewModel -} - -#[derive(Deserialize)] -pub struct ContentMetadataViewModel { - pub metadataRows: Vec -} - -#[derive(Deserialize)] -pub struct MetadataRow { - pub metadataParts: Vec -} - -#[derive(Deserialize, Debug)] -pub struct PlaylistTitle { - pub content: Option, - pub commandRuns: Option>, -} - -#[derive(Deserialize, Debug)] -pub struct CommandRun { - onTap: OnTap -} - -#[derive(Deserialize, Debug)] -pub struct OnTap { - innertubeCommand: InnertubeCommand -} - -#[derive(Deserialize, Debug)] -pub struct InnertubeCommand { - pub browseEndpoint: BrowseEndpoint, -} - -#[derive(Deserialize)] -pub struct TabContent { - pub sectionListRenderer: SectionListRenderer -} - -#[derive(Deserialize)] -pub struct TabEndpoint { - pub browseEndpoint: BrowseEndpoint, -} - -#[derive(Deserialize, Debug)] -pub struct BrowseEndpoint { - pub browseId: String, - pub params: Option, -} - -impl YoutubeChannel { - pub fn get_playlists_request_str(&self) -> Option { - for tab in &self.contents.twoColumnBrowseResultsRenderer.tabs { - if let Some(t) = &tab.tabRenderer { - if t.title == String::from("Playlists") { - return Some(t.endpoint.browseEndpoint.params.as_ref().unwrap().clone()); - } - } - } - None - } - - pub fn get_playlists(&self) -> Vec<(String, String)> { - let mut arr = Vec::new(); - for tab in &self.contents.twoColumnBrowseResultsRenderer.tabs { - if let Some(t) = &tab.tabRenderer { - if t.title == String::from("Playlists") { - for item in &t.content.as_ref().unwrap().sectionListRenderer.contents.as_ref().unwrap()[0].itemSectionRenderer.as_ref().unwrap().contents[0].gridRenderer.items { - let title = item.lockupViewModel.metadata.lockupMetadataViewModel.title.content.clone().unwrap_or("!".to_string()); - println!("{:#?}", item.lockupViewModel.metadata.lockupMetadataViewModel.metadata.contentMetadataViewModel.metadataRows[1].metadataParts[0]); - let url = item.lockupViewModel.metadata.lockupMetadataViewModel.metadata.contentMetadataViewModel.metadataRows[1].metadataParts[0].commandRuns.as_ref().unwrap()[0].onTap.innertubeCommand.browseEndpoint.browseId.clone(); - arr.push((title, url)); - } - } - } - } - arr - } +#[derive(Default)] +pub struct YoutubeVideo { + pub videoId: String, + pub title: String, + pub publisher: String, + pub lengthSeconds: u32 } \ No newline at end of file