1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
//! Channel types.

use std::sync::Arc;

use crate::{
    youtube::{
        browse,
        innertube::{Browse, ChannelPage},
    },
    Client,
};

define_id! {
    24,
    "An Id describing a [`Channel`]",
    [
        "https://www.youtube.com/channel/",
    ]
}

impl Id {
    /// Get the playlist id that contains the uploads for this channel
    pub fn uploads(self) -> crate::playlist::Id {
        String::from(&*self)
            .chars()
            .enumerate()
            // Turn `UCdktGrgQlqxPsvHo6cHF0Ng`
            // into `UUdktGrgQlqxPsvHo6cHF0Ng`
            .map(|(i, ch)| if i == 1 { 'U' } else { ch })
            .collect::<String>()
            .parse()
            .expect("Unable to convert")
    }
}

/// A badge that a [`Channel`] can have
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Badge {
    /// A artist that is verified
    VerifiedArtist,

    /// A channel that is verified
    Verified,
}

impl Badge {
    pub(crate) fn from(badge: &browse::channel::Badge) -> Self {
        match badge.metadata_badge_renderer.style.as_str() {
            "BADGE_STYLE_TYPE_VERIFIED_ARTIST" => Self::VerifiedArtist,
            "BADGE_STYLE_TYPE_VERIFIED" => Self::Verified,
            badge => unreachable!("Unknown badge: '{}'", badge),
        }
    }
}

/// A Channel
pub struct Channel {
    client: Arc<Client>,
    response: browse::channel::about::Root,
}

impl Channel {
    pub(crate) async fn get(client: Arc<Client>, id: Id) -> crate::Result<Self> {
        let response: browse::channel::about::Result = client
            .api
            .browse(Browse::Channel {
                id,
                page: ChannelPage::About,
            })
            .await?;

        let response = response.into_std()?;

        Ok(Self {
            client: Arc::clone(&client),
            response,
        })
    }

    fn contents(&self) -> &browse::channel::about::ChannelAboutFullMetadataRenderer {
        &self
            .response
            .contents()
            .section_list_renderer
            .contents
            .0
            .item_section_renderer
            .contents
            .0
            .channel_about_full_metadata_renderer
    }

    fn header(&self) -> &browse::channel::C4TabbedHeaderRenderer {
        &self.response.header.c4_tabbed_header_renderer
    }

    /// The [`Id`] of a channel
    pub fn id(&self) -> Id {
        self.header().channel_id
    }

    /// The name of the channel
    pub fn name(&self) -> &str {
        &self.header().title
    }

    /// The description of the channel
    pub fn description(&self) -> &str {
        &self.contents().description.simple_text
    }

    /// The country that this channel resides in
    pub fn country(&self) -> Option<&str> {
        self.contents()
            .country
            .as_ref()
            .map(|x| x.simple_text.as_str())
    }

    /// The views that this channel received
    pub fn views(&self) -> u64 {
        self.contents()
            .view_count_text
            .simple_text
            .split_once(' ')
            .expect("No space in view count")
            .0
            .replace(',', "")
            .parse()
            .expect("not int")
    }

    /// The amount of subscribers this channel has.
    pub fn subscribers(&self) -> u64 {
        parse_amount(
            self.header()
                .subscriber_count_text
                .simple_text
                .split_once(' ')
                .expect("no space in subscriber_count_text")
                .0,
        )
        .expect("Unable to parse subscriber count")
    }

    /// The avatar of the channel in various sizes
    pub fn avatar(&self) -> impl Iterator<Item = &crate::Thumbnail> {
        self.header().avatar.thumbnails.iter()
    }

    /// The banner of the channel in various sizes
    pub fn banner(&self) -> impl Iterator<Item = &crate::Thumbnail> {
        self.header().banner.thumbnails.iter()
    }

    /// The [`Badges`](Badge) of this channel.
    pub fn badges(&self) -> impl Iterator<Item = Badge> + '_ {
        self.header().badges.iter().map(Badge::from)
    }

    /// The uploads of a channel in a [`Playlist`](crate::Playlist)
    pub async fn uploads(&self) -> crate::Result<crate::Playlist> {
        self.client.playlist(self.id().uploads()).await
    }

    // TODO: Playlist
    // TODO: Channels
}

fn parse_amount(value: &str) -> Option<u64> {
    let last = value.chars().last()?;
    if last.is_numeric() {
        value.parse().ok()
    } else {
        let val = &value[..value.len() - 1];
        let val: f64 = val.parse().ok()?;
        let mul = match last {
            'K' => 1_000.0,
            'M' => 1_000_000.0,
            modifier => unreachable!("Unknown modifier '{}'", modifier),
        };

        Some((val * mul) as u64)
    }
}