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
use futures::{
    compat::{Future01CompatExt, Stream01CompatExt},
    TryStreamExt,
};
use log::debug;
use reqwest::r#async::Client;
use serde::{Deserialize, Serialize};
use snafu::{ResultExt, Snafu};
use chrono::{Utc, DateTime};

use super::ApiKey;

const URL: &str = "https://www.googleapis.com/youtube/v3/search";

/// searches for a video, channel or playlist
pub async fn perform(client: Client, query: &SearchList) -> Result<SearchListResponse, Error> {
    let url = format!(
        "{}?{}",
        URL,
        serde_qs::to_string(&query).context(Serialization)?
    );
    debug!("getting {}", url);
    let response = client.get(&url).send().compat().await.context(Connection)?;

    let chunks = response
        .into_body()
        .compat()
        .try_concat()
        .await
        .context(Connection)?;
    let response = String::from_utf8_lossy(&chunks);

    serde_json::from_str(&response).context(Deserialization { string: response })
}

/// custom error type for youtube searching
#[derive(Debug, Snafu)]
pub enum Error {
    #[snafu(display("failed to connect to the api: {}", source))]
    Connection { source: reqwest::Error },
    #[snafu(display("failed to deserialize: {} {}", string, source))]
    Deserialization {
        string: String,
        source: serde_json::Error,
    },
    #[snafu(display("failed to serialize: {}", source))]
    Serialization { source: serde_qs::Error },
    #[snafu(display("failed to build: missing fields"))]
    BuilderMissingField,
}

#[derive(Debug, Clone, Serialize)]
pub struct SearchList {
    key: ApiKey,
    part: String,
    q: Option<String>,
}

impl SearchList {
    pub fn new(key: ApiKey) -> SearchList {
        SearchList {
            key: key,
            part: String::from("snippet"),
            q: None,
        }
    }

    pub fn q(mut self, q: String) -> SearchList {
        self.q = Some(q);
        self
    }
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SearchListResponse {
    pub kind: String,
    pub etag: String,
    pub prev_page_token: Option<String>,
    pub region_code: String,
    pub page_info: PageInfo,
    pub items: Vec<SearchResult>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PageInfo {
    pub total_results: i64,
    pub results_per_page: i64,
}

#[derive(Debug, Clone, Deserialize)]
pub struct SearchResult {
    pub kind: String,
    pub etag: String,
    pub id: SearchResultId,
    pub snippet: SearchResultSnippet,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SearchResultId {
    pub kind: String,
    pub video_id: Option<String>,
    pub channel_id: Option<String>,
    pub playlist_id: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SearchResultSnippet {
    pub published_at: Option<DateTime<Utc>>,
    pub channel_id: Option<String>,
    pub title: Option<String>,
    pub description: Option<String>,
    pub thumbnails: Option<SearchResultThumbnails>,
    pub channel_title: Option<String>,
    pub live_broadcast_content: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct SearchResultThumbnails {
    default: Option<SearchResultThumbnail>,
    medium: Option<SearchResultThumbnail>,
    high: Option<SearchResultThumbnail>,
    standard: Option<SearchResultThumbnail>,
    maxres: Option<SearchResultThumbnail>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct SearchResultThumbnail {
    pub url: String,
    pub width: Option<u64>,
    pub height: Option<u64>,
}