pub mod related;
use crate::{
youtube::{innertube::Next, next, parse_date, player_response::PlayerResponse},
Client, Stream, Thumbnail,
};
use std::time::Duration;
#[derive(Clone)]
pub struct Video {
player_response: PlayerResponse,
initial_data: next::Root,
client: Client,
}
impl Video {
pub(crate) async fn get(client: Client, id: Id) -> crate::Result<Self> {
Ok(Self {
player_response: client.api.player(id).await?.into_std()?,
initial_data: client.api.next(Next::Video(id)).await?,
client,
})
}
pub fn title(&self) -> &str {
&self.player_response.video_details.title
}
pub fn id(&self) -> Id {
self.player_response.video_details.video_id
}
pub fn duration(&self) -> Duration {
self.player_response.video_details.length_seconds
}
pub fn keywords(&self) -> &Vec<String> {
&self.player_response.video_details.keywords
}
pub fn channel(&self) -> Channel<'_> {
let owner = &self
.initial_data
.contents
.two_column_watch_next_results
.results
.results
.secondary()
.owner
.video_owner_renderer;
Channel {
client: &self.client,
id: self.player_response.video_details.channel_id,
name: &self.player_response.video_details.author,
subscribers: owner.subscribers(),
thumbnails: owner.thumbnails(),
}
}
pub fn description(&self) -> &str {
&self.player_response.video_details.short_description
}
pub fn views(&self) -> u64 {
self.player_response.video_details.view_count
}
pub fn ratings(&self) -> Ratings {
if let Some((likes, dislikes)) = self
.initial_data
.contents
.two_column_watch_next_results
.results
.results
.primary()
.ratings()
{
Ratings::Allowed { likes, dislikes }
} else {
Ratings::NotAllowed
}
}
pub fn hashtags(&self) -> impl Iterator<Item = &str> {
self.initial_data
.contents
.two_column_watch_next_results
.results
.results
.primary()
.hashtags()
}
pub fn live(&self) -> bool {
self.player_response.video_details.is_live_content
}
pub fn thumbnails(&self) -> &Vec<Thumbnail> {
&self.player_response.video_details.thumbnail.thumbnails
}
pub fn date(&self) -> chrono::NaiveDate {
parse_date(
&self
.initial_data
.contents
.two_column_watch_next_results
.results
.results
.primary()
.date_text,
)
.expect("Unable to parse date")
}
pub fn related(&self) -> Option<impl futures_core::Stream<Item = Related>> {
let initial_items = self
.initial_data
.contents
.two_column_watch_next_results
.secondary_results
.as_ref()?
.secondary_results
.results
.clone();
let client = self.client.clone();
Some(async_stream::stream! {
let mut items: Box<dyn Iterator<Item = next::RelatedItem> + Send + Sync> =
Box::new(initial_items.into_iter());
while let Some(item) = items.next() {
match item {
next::RelatedItem::ContinuationItemRenderer(continuation) => {
assert!(
items.next().is_none(),
"Found a continuation in the middle of items!"
);
let response: next::Continuation = client
.api
.next(Next::Continuation(continuation.get()))
.await
.expect("Continuation request failed");
items = Box::new(response.into_videos());
}
next::RelatedItem::CompactVideoRenderer(video) => {
yield Related::Video(related::Video(video, client.clone()));
}
next::RelatedItem::CompactPlaylistRenderer(playlist) => {
yield Related::Playlist(related::Playlist(playlist, client.clone()));
}
next::RelatedItem::CompactRadioRenderer(radio) => {
yield Related::Radio(related::Radio(radio, client.clone()));
}
next::RelatedItem::CompactMovieRenderer(movie) => {
yield Related::Movie(related::Movie(movie, client.clone()));
},
next::RelatedItem::PromotedSparklesWebRenderer {} | next::RelatedItem::CompactPromotedVideoRenderer {} => continue,
}
}
})
}
pub async fn streams(&self) -> crate::Result<impl Iterator<Item = Stream>> {
crate::stream::get(self.client.clone(), self.id()).await
}
}
impl std::fmt::Debug for Video {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Video")
.field("id", &self.id())
.field("title", &self.title())
.field("duration", &self.duration())
.field("keywords", &self.keywords())
.field("channel", &self.channel())
.field("description", &self.description())
.field("views", &self.views())
.field("ratings", &self.ratings())
.field("live", &self.live())
.field("thumbnails", &self.thumbnails())
.field("date", &self.date())
.finish()
}
}
impl PartialEq for Video {
fn eq(&self, other: &Self) -> bool {
self.id() == other.id()
}
}
impl Eq for Video {}
pub struct Channel<'a> {
client: &'a Client,
id: crate::channel::Id,
name: &'a str,
subscribers: Option<u64>,
thumbnails: &'a Vec<Thumbnail>,
}
impl<'a> Channel<'a> {
pub fn id(&self) -> crate::channel::Id {
self.id
}
pub fn name(&self) -> &str {
self.name
}
pub fn subscribers(&self) -> Option<u64> {
self.subscribers
}
pub fn thumbnails(&self) -> impl Iterator<Item = &Thumbnail> {
self.thumbnails.iter()
}
pub async fn upgrade(&self) -> crate::Result<crate::Channel> {
self.client.channel(self.id).await
}
}
impl<'a> std::fmt::Debug for Channel<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Channel")
.field("id", &self.id)
.field("name", &self.name)
.field("subscribers", &self.subscribers)
.field("thumbnails", &self.thumbnails)
.finish()
}
}
impl<'a> PartialEq for Channel<'a> {
fn eq(&self, other: &Self) -> bool {
self.id() == other.id()
}
}
impl<'a> Eq for Channel<'a> {}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Ratings {
Allowed {
likes: u64,
dislikes: u64,
},
NotAllowed,
}
define_id! {
11,
"An Id describing a [`Video`]",
[
"watch?v=",
"embed/",
]
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Related {
Video(related::Video),
Playlist(related::Playlist),
Movie(related::Movie),
Radio(related::Radio),
}