Skip to content

Commit 3f47867

Browse files
committed
Version 1.0.1
1 parent aa8afb2 commit 3f47867

File tree

6 files changed

+117
-52
lines changed

6 files changed

+117
-52
lines changed

Diff for: Cargo.toml

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "youtube_search"
3-
version = "1.0.0"
3+
version = "1.0.1"
44
edition = "2021"
55
authors = ["Marc Cámara"]
66
description = "A crate to navigate the YouTube API"
@@ -17,9 +17,6 @@ categories = [
1717
"api", 'youtube'
1818
]
1919

20-
21-
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
22-
2320
[dependencies]
2421
anyhow = "1.0.44"
2522
async-trait = "0.1.72"

Diff for: README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ This Rust library provides an asynchronous interface for interacting with YouTub
2121
Add the following line to your `Cargo.toml` file under the `[dependencies]` section:
2222

2323
```toml
24-
youtube_search = "1.0.0"
24+
youtube_search = "1.0.1"
2525
```
2626

2727
## Usage
@@ -37,7 +37,7 @@ use youtube_search::{
3737
#[tokio::main]
3838
async fn main() -> Result<(), Box<dyn std::error::Error>> {
3939
let channel = find_youtube_channel("ChannelName").await?;
40-
println!("Channel ID: {}", channel.channel_id);
40+
println!("Channel Title: {}", channel.title);
4141

4242
let videos = find_latest_videos(&channel, 5).await?;
4343
println!("Latest videos: {:?}", videos);

Diff for: src/lib.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ use youtube::{
1111
video::{Video, VideoError},
1212
};
1313

14-
/// Find a youtube channel by name
15-
pub async fn find_youtube_channel(name: &str) -> Result<Channel, ChannelError> {
14+
/// Find a youtube channel by handle: https://www.youtube.com/user/@{handle}
15+
pub async fn find_youtube_channel(handle: &str) -> Result<Channel, ChannelError> {
1616
let client = Arc::new(HttpClient::new());
17-
Channel::initialize(name.to_string(), client.clone()).await
17+
Channel::initialize(handle.to_string(), client.clone()).await
1818
}
1919

2020
/// Find latest videos from a channel, will return an error if the channel has no videos

Diff for: src/test_utils.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ impl HttpClientTrait for MockHttpClient {
3030
let mut server = mockito::Server::new();
3131

3232
let mut responses = self.responses.lock().await;
33-
let response = responses.pop().unwrap_or_else(|| "".to_string());
33+
let response = responses.pop().unwrap_or_default();
3434

3535
let _mock = server
3636
.mock("GET", "/")

Diff for: src/youtube/channel.rs

+70-28
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ use super::video::Video;
1111
// A youtube channel with some useful data
1212
#[derive(Debug)]
1313
pub struct Channel {
14-
pub name: String,
14+
pub handle: String,
15+
pub title: String,
1516
pub channel_id: String,
1617
}
1718

@@ -23,22 +24,27 @@ pub struct ChannelError {
2324
}
2425

2526
impl Channel {
26-
fn new(name: String, channel_id: String) -> Self {
27-
Self { name, channel_id }
27+
fn new(handle: String, title: String, channel_id: String) -> Self {
28+
Self {
29+
handle,
30+
title,
31+
channel_id,
32+
}
2833
}
2934

3035
pub async fn initialize<T: HttpClientTrait>(
31-
name: String,
36+
handle: String,
3237
client: Arc<T>,
3338
) -> Result<Self, ChannelError> {
34-
let channel_id = retrieve_channel_id(&name, &client)
35-
.await
36-
.map_err(|e| ChannelError {
37-
source: Some(e.into()),
38-
msg: "Failed to get channel id".to_owned(),
39-
})?;
39+
let (channel_id, title) =
40+
retrieve_channel_id(&handle, &client)
41+
.await
42+
.map_err(|e| ChannelError {
43+
source: Some(e.into()),
44+
msg: "Failed to get channel id".to_owned(),
45+
})?;
4046

41-
Ok(Self::new(name, channel_id))
47+
Ok(Self::new(handle, title, channel_id))
4248
}
4349

4450
async fn get_main_playlist_id<T: HttpClientTrait>(
@@ -78,19 +84,33 @@ mod tests {
7884
#[tokio::test]
7985
async fn channel_initialization_succeeds_with_valid_id() {
8086
let client = create_client_with_responses(vec![
81-
r#"{"items": [
82-
{
83-
"id": "id_channel1"
84-
}
85-
]}"#,
87+
r#"{
88+
"items": [
89+
{
90+
"snippet": {
91+
"channelId": "channel_id",
92+
"channelTitle": "Another title",
93+
"channelHandle": "@another_channel"
94+
}
95+
},
96+
{
97+
"snippet": {
98+
"channelId": "channel_id",
99+
"channelTitle": "Channel title",
100+
"channelHandle": "@channel1"
101+
}
102+
}
103+
]
104+
}"#,
86105
])
87106
.await;
88107
let channel = Channel::initialize("channel1".to_string(), client)
89108
.await
90109
.ok()
91110
.unwrap();
92-
assert_eq!(channel.channel_id, "id_channel1");
93-
assert_eq!(channel.name, "channel1");
111+
assert_eq!(channel.channel_id, "channel_id");
112+
assert_eq!(channel.title, "Channel title");
113+
assert_eq!(channel.handle, "channel1");
94114
}
95115

96116
#[tokio::test]
@@ -108,13 +128,19 @@ mod tests {
108128
}
109129

110130
#[tokio::test]
111-
async fn channel_initialization_fails_with_null_id() {
131+
async fn channel_initialization_fails_with_no_id() {
112132
let client = create_client_with_responses(vec![
113-
r#"{"items": [
114-
{
115-
"id": null
116-
}
117-
]}"#,
133+
r#"{
134+
"items": [
135+
{
136+
"snippet": {
137+
"channelId": "channel_id",
138+
"channelTitle": "Channel title",
139+
"channelHandle": "@no_match"
140+
}
141+
}
142+
]
143+
}"#,
118144
])
119145
.await;
120146
let channel = Channel::initialize("channel1".to_string(), client).await;
@@ -123,7 +149,11 @@ mod tests {
123149

124150
#[tokio::test]
125151
async fn main_playlist_is_found_for_a_channel() {
126-
let channel = Channel::new("channel1".to_string(), "id_channel1".to_string());
152+
let channel = Channel::new(
153+
"channel1".to_string(),
154+
"title".to_string(),
155+
"id_channel1".to_string(),
156+
);
127157
let client = create_client_with_responses(vec![
128158
r#"{"items": [
129159
{
@@ -143,7 +173,11 @@ mod tests {
143173

144174
#[tokio::test]
145175
async fn main_playlist_search_fails_if_channel_do_not_have_any_playlist() {
146-
let channel = Channel::new("channel1".to_string(), "id_channel1".to_string());
176+
let channel = Channel::new(
177+
"channel1".to_string(),
178+
"title".to_string(),
179+
"id_channel1".to_string(),
180+
);
147181
let client = create_client_with_responses(vec![
148182
r#"{"items": [
149183
{
@@ -163,7 +197,11 @@ mod tests {
163197

164198
#[tokio::test]
165199
async fn videos_from_channel_are_returned() {
166-
let channel = Channel::new("channel1".to_string(), "id_channel1".to_string());
200+
let channel = Channel::new(
201+
"channel1".to_string(),
202+
"title".to_string(),
203+
"id_channel1".to_string(),
204+
);
167205
let playlist_response = r#"{"items": [
168206
{
169207
"contentDetails": {
@@ -222,7 +260,11 @@ mod tests {
222260

223261
#[tokio::test]
224262
async fn videos_from_channel_cannot_be_retrieved_when_response_is_empty() {
225-
let channel = Channel::new("channel1".to_string(), "id_channel1".to_string());
263+
let channel = Channel::new(
264+
"channel1".to_string(),
265+
"title".to_string(),
266+
"id_channel1".to_string(),
267+
);
226268
let playlist_response = r#"{"items": [
227269
{
228270
"contentDetails": {

Diff for: src/youtube/requests/channel.rs

+40-14
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,54 @@ struct ChannelReturn {
1111

1212
#[derive(Deserialize)]
1313
struct ChannelItemsReturn {
14-
id: Option<String>,
14+
snippet: ChannelSnippetReturn,
15+
}
16+
17+
#[derive(Clone, Deserialize)]
18+
#[serde(rename_all = "camelCase")]
19+
struct ChannelSnippetReturn {
20+
channel_id: String,
21+
channel_title: String,
22+
channel_handle: String,
1523
}
1624

1725
pub async fn retrieve_channel_id<T: HttpClientTrait>(
18-
name: &str,
26+
handle: &str,
1927
client: &Arc<T>,
20-
) -> Result<String, RequestError> {
21-
let url = Url::parse_with_params("https://yt.lemnoslife.com/channels", &[("cId", name)])
22-
.map_err(|e| RequestError::Other(e.to_string()))?;
28+
) -> Result<(String, String), RequestError> {
29+
let url = Url::parse_with_params(
30+
"https://yt.lemnoslife.com/search",
31+
&[
32+
("q", handle),
33+
("type", "channel"),
34+
("part", "snippet"),
35+
("maxResults", "10"),
36+
],
37+
)
38+
.map_err(|e| RequestError::Other(e.to_string()))?;
2339

2440
let response = client.get(url.as_str()).await.map_err(RequestError::Http)?;
2541
let channel_data: ChannelReturn = process_response(response).await?;
2642

27-
let channel_id = channel_data
28-
.items
29-
.first()
30-
.map(|item| item.id.clone())
31-
.ok_or(RequestError::NotFound);
43+
match find_channel_by_handle(&channel_data.items, handle) {
44+
Ok(channel_snippet) => Ok((
45+
channel_snippet.channel_id.clone(),
46+
channel_snippet.channel_title.clone(),
47+
)),
48+
Err(e) => Err(e),
49+
}
50+
}
3251

33-
match channel_id {
34-
Ok(Some(channel_id)) => Ok(channel_id),
35-
Ok(None) => Err(RequestError::NotFound),
36-
Err(e) => Err(RequestError::ResponseNotParsed(e.into())),
52+
fn find_channel_by_handle(
53+
channels: &[ChannelItemsReturn],
54+
target_handle: &str,
55+
) -> Result<ChannelSnippetReturn, RequestError> {
56+
let target_handle = format!("@{}", target_handle);
57+
match channels
58+
.iter()
59+
.find(|&channel| channel.snippet.channel_handle == target_handle)
60+
{
61+
Some(channel) => Ok(channel.snippet.clone()),
62+
None => Err(RequestError::NotFound),
3763
}
3864
}

0 commit comments

Comments
 (0)