feat: add timeago parser, playlist_cont
This commit is contained in:
parent
5b8c3d646a
commit
346406c1c8
25 changed files with 11374 additions and 183 deletions
|
|
@ -1,5 +1,3 @@
|
|||
// REQUEST
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use reqwest::Method;
|
||||
use serde::Serialize;
|
||||
|
|
@ -18,10 +16,11 @@ struct QPlaylist {
|
|||
browse_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TmpEntry {
|
||||
pub title: String,
|
||||
pub video_id: String,
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct QPlaylistCont {
|
||||
context: ContextYT,
|
||||
continuation: String,
|
||||
}
|
||||
|
||||
impl RustyTube {
|
||||
|
|
@ -46,6 +45,51 @@ impl RustyTube {
|
|||
|
||||
map_playlist(&playlist_response)
|
||||
}
|
||||
|
||||
pub async fn get_playlist_cont(&self, playlist: &mut Playlist) -> Result<()> {
|
||||
match &playlist.ctoken {
|
||||
Some(ctoken) => {
|
||||
let client = self.get_ytclient(ClientType::Desktop);
|
||||
let context = client.get_context(true).await;
|
||||
|
||||
let request_body = QPlaylistCont {
|
||||
context,
|
||||
continuation: ctoken.to_owned(),
|
||||
};
|
||||
|
||||
let resp = client
|
||||
.request_builder(Method::POST, "browse")
|
||||
.await
|
||||
.json(&request_body)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
let cont_response = resp.json::<response::playlist::PlaylistCont>().await?;
|
||||
|
||||
let action = some_or_bail!(
|
||||
cont_response
|
||||
.on_response_received_actions
|
||||
.iter()
|
||||
.find(|a| a.append_continuation_items_action.target_id == playlist.id),
|
||||
Err(anyhow!("no continuation action"))
|
||||
);
|
||||
|
||||
let (mut videos, ctoken) =
|
||||
map_playlist_items(&action.append_continuation_items_action.continuation_items);
|
||||
|
||||
playlist.videos.append(&mut videos);
|
||||
playlist.ctoken = ctoken;
|
||||
|
||||
if playlist.ctoken.is_none() {
|
||||
playlist.n_videos = playlist.videos.len() as u32;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
None => Err(anyhow!("no ctoken")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn map_playlist(response: &response::Playlist) -> Result<Playlist> {
|
||||
|
|
@ -74,49 +118,7 @@ fn map_playlist(response: &response::Playlist) -> Result<Playlist> {
|
|||
.playlist_video_list_renderer
|
||||
.contents;
|
||||
|
||||
let mut ctoken: Option<String> = None;
|
||||
let videos = video_items
|
||||
.iter()
|
||||
.filter_map(|it| match it {
|
||||
response::playlist::PlaylistVideoItem::PlaylistVideoRenderer { video } => {
|
||||
match &video.channel {
|
||||
TextLink::Browse {
|
||||
text,
|
||||
page_type,
|
||||
browse_id,
|
||||
} => match page_type {
|
||||
PageType::Channel => Some(Video {
|
||||
id: video.video_id.to_owned(),
|
||||
title: video.title.to_owned(),
|
||||
length: video.length_seconds,
|
||||
thumbnails: video
|
||||
.thumbnail
|
||||
.thumbnails
|
||||
.iter()
|
||||
.map(|t| Thumbnail {
|
||||
url: t.url.to_owned(),
|
||||
width: t.width,
|
||||
height: t.height,
|
||||
})
|
||||
.collect(),
|
||||
channel: Channel {
|
||||
id: browse_id.to_string(),
|
||||
name: text.to_owned(),
|
||||
},
|
||||
}),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
response::playlist::PlaylistVideoItem::ContinuationItemRenderer {
|
||||
continuation_endpoint,
|
||||
} => {
|
||||
ctoken = Some(continuation_endpoint.continuation_command.token.to_owned());
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let (videos, ctoken) = map_playlist_items(video_items);
|
||||
|
||||
let thumbnail_renderer = some_or_bail!(
|
||||
response
|
||||
|
|
@ -151,7 +153,7 @@ fn map_playlist(response: &response::Playlist) -> Result<Playlist> {
|
|||
match &response.header.playlist_header_renderer.num_videos_text {
|
||||
Text::Multiple { runs } =>
|
||||
if runs.len() == 2 && runs[1] == " videos" {
|
||||
runs[0].parse().ok()
|
||||
runs[0].replace(",", "").replace(".", "").parse().ok()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
|
|
@ -175,6 +177,11 @@ fn map_playlist(response: &response::Playlist) -> Result<Playlist> {
|
|||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let id = response
|
||||
.header
|
||||
.playlist_header_renderer
|
||||
.playlist_id
|
||||
.to_owned();
|
||||
let name = response.header.playlist_header_renderer.title.to_owned();
|
||||
let description = response
|
||||
.header
|
||||
|
|
@ -201,16 +208,65 @@ fn map_playlist(response: &response::Playlist) -> Result<Playlist> {
|
|||
};
|
||||
|
||||
Ok(Playlist {
|
||||
id,
|
||||
name,
|
||||
videos,
|
||||
n_videos,
|
||||
ctoken,
|
||||
name,
|
||||
thumbnails,
|
||||
description,
|
||||
channel,
|
||||
last_update: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_playlist_items(
|
||||
items: &Vec<response::VideoListItem<response::playlist::PlaylistVideo>>,
|
||||
) -> (Vec<Video>, Option<String>) {
|
||||
let mut ctoken: Option<String> = None;
|
||||
let videos = items
|
||||
.iter()
|
||||
.filter_map(|it| match it {
|
||||
response::VideoListItem::GridVideoRenderer { video } => match &video.channel {
|
||||
TextLink::Browse {
|
||||
text,
|
||||
page_type,
|
||||
browse_id,
|
||||
} => match page_type {
|
||||
PageType::Channel => Some(Video {
|
||||
id: video.video_id.to_owned(),
|
||||
title: video.title.to_owned(),
|
||||
length: video.length_seconds,
|
||||
thumbnails: video
|
||||
.thumbnail
|
||||
.thumbnails
|
||||
.iter()
|
||||
.map(|t| Thumbnail {
|
||||
url: t.url.to_owned(),
|
||||
width: t.width,
|
||||
height: t.height,
|
||||
})
|
||||
.collect(),
|
||||
channel: Channel {
|
||||
id: browse_id.to_string(),
|
||||
name: text.to_owned(),
|
||||
},
|
||||
}),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
},
|
||||
response::VideoListItem::ContinuationItemRenderer {
|
||||
continuation_endpoint,
|
||||
} => {
|
||||
ctoken = Some(continuation_endpoint.continuation_command.token.to_owned());
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
(videos, ctoken)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs::File, io::BufReader, path::Path};
|
||||
|
|
@ -299,6 +355,7 @@ mod tests {
|
|||
let rt = RustyTube::new();
|
||||
let playlist = rt.get_playlist(id).await.unwrap();
|
||||
|
||||
assert_eq!(playlist.id, id);
|
||||
assert_eq!(playlist.name, name);
|
||||
assert!(!playlist.videos.is_empty());
|
||||
assert_eq!(playlist.ctoken.is_some(), is_long);
|
||||
|
|
@ -323,4 +380,19 @@ mod tests {
|
|||
let playlist_data = map_playlist(&playlist).unwrap();
|
||||
insta::assert_yaml_snapshot!(format!("map_playlist_data_{}", name), playlist_data);
|
||||
}
|
||||
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn t_playlist_cont() {
|
||||
let rt = RustyTube::new();
|
||||
let mut playlist = rt
|
||||
.get_playlist("PLbZIPy20-1pN7mqjckepWF78ndb6ci_qi")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
while playlist.ctoken.is_some() {
|
||||
rt.get_playlist_cont(&mut playlist).await.unwrap();
|
||||
}
|
||||
|
||||
assert!(playlist.videos.len() > 100);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Reference in a new issue