1use std::{
2 future::Future,
3 pin::Pin,
4 task::{Context, Poll},
5};
6
7use chrono::{DateTime, Utc};
8use futures::future::BoxFuture;
9use log::debug;
10use serde::{Deserialize, Serialize, Serializer};
11use snafu::{ResultExt, Snafu};
12
13use super::ApiKey;
14
15#[derive(Debug, Snafu)]
17pub enum Error {
18 #[snafu(display("failed to connect to the api: {}", string))]
19 Connection { string: String },
20 #[snafu(display("failed to deserialize: {} {}", string, source))]
21 Deserialization {
22 string: String,
23 source: serde_json::Error,
24 },
25 #[snafu(display("failed to serialize: {}", source))]
26 Serialization {
27 source: serde_urlencoded::ser::Error,
28 },
29}
30
31impl From<surf::Error> for Error {
32 fn from(surf_error: surf::Error) -> Self {
33 Error::Connection {
34 string: surf_error.to_string(),
35 }
36 }
37}
38
39pub struct SearchList {
41 future: Option<BoxFuture<'static, Result<Response, Error>>>,
42 data: Option<SearchListData>,
43}
44
45#[derive(Debug, Clone, Serialize)]
46#[serde(rename_all = "camelCase")]
47struct SearchListData {
48 key: ApiKey,
49 part: String,
50 #[serde(skip_serializing_if = "std::ops::Not::not")]
51 for_content_owner: bool,
52 #[serde(skip_serializing_if = "std::ops::Not::not")]
53 for_developer: bool,
54 #[serde(skip_serializing_if = "std::ops::Not::not")]
55 for_mine: bool,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 related_to_video_id: Option<String>,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 channel_id: Option<String>,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 channel_type: Option<ChannelType>,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 event_type: Option<EventType>,
64 #[serde(skip_serializing_if = "Option::is_none")]
65 location: Option<VideoLocation>,
66 #[serde(skip_serializing_if = "Option::is_none")]
67 location_radius: Option<String>,
68 #[serde(skip_serializing_if = "Option::is_none")]
69 max_results: Option<u8>,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 on_behalf_of_content_owner: Option<String>,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 order: Option<Order>,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 page_token: Option<String>,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 published_after: Option<DateTime<Utc>>,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 published_before: Option<DateTime<Utc>>,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 q: Option<String>,
82 #[serde(skip_serializing_if = "Option::is_none")]
83 region_code: Option<String>,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 relevance_language: Option<String>,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 safe_search: Option<SafeSearch>,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 topic_id: Option<String>,
90 #[serde(skip_serializing_if = "Option::is_none", rename = "type")]
91 item_type: Option<ItemType>,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 video_caption: Option<String>,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 video_category_id: Option<String>,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 video_definition: Option<VideoDefinition>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 video_dimension: Option<VideoDimension>,
100 #[serde(skip_serializing_if = "std::ops::Not::not")]
101 video_embeddable: bool,
102 #[serde(skip_serializing_if = "Option::is_none")]
103 video_license: Option<VideoLicense>,
104 #[serde(skip_serializing_if = "std::ops::Not::not")]
105 video_syndicated: bool,
106 #[serde(skip_serializing_if = "Option::is_none")]
107 video_type: Option<VideoType>,
108}
109
110impl SearchList {
111 const URL: &'static str = "https://www.googleapis.com/youtube/v3/search";
112
113 #[must_use]
115 pub fn new(key: ApiKey) -> Self {
116 Self {
117 future: None,
118 data: Some(SearchListData {
119 key,
120 part: String::from("snippet"),
121 for_content_owner: false,
122 for_developer: false,
123 for_mine: false,
124 related_to_video_id: None,
125 channel_id: None,
126 channel_type: None,
127 event_type: None,
128 location: None,
129 location_radius: None,
130 max_results: None,
131 on_behalf_of_content_owner: None,
132 order: None,
133 page_token: None,
134 published_after: None,
135 published_before: None,
136 q: None,
137 region_code: None,
138 relevance_language: None,
139 safe_search: None,
140 topic_id: None,
141 item_type: None,
142 video_caption: None,
143 video_category_id: None,
144 video_definition: None,
145 video_dimension: None,
146 video_embeddable: false,
147 video_license: None,
148 video_syndicated: false,
149 video_type: None,
150 }),
151 }
152 }
153
154 #[must_use]
155 pub fn for_content_owner(mut self) -> Self {
156 let mut data = self.data.take().unwrap();
157 data.for_content_owner = true;
158 self.data = Some(data);
159 self
160 }
161
162 #[must_use]
163 pub fn for_developer(mut self) -> Self {
164 let mut data = self.data.take().unwrap();
165 data.for_developer = true;
166 self.data = Some(data);
167 self
168 }
169
170 #[must_use]
171 pub fn for_mine(mut self) -> Self {
172 let mut data = self.data.take().unwrap();
173 data.for_mine = true;
174 self.data = Some(data);
175 self
176 }
177
178 #[must_use]
179 pub fn related_to_video_id(mut self, related_to_video_id: impl Into<String>) -> Self {
180 let mut data = self.data.take().unwrap();
181 data.related_to_video_id = Some(related_to_video_id.into());
182 self.data = Some(data);
183 self
184 }
185
186 #[must_use]
187 pub fn channel_id(mut self, channel_id: impl Into<String>) -> Self {
188 let mut data = self.data.take().unwrap();
189 data.channel_id = Some(channel_id.into());
190 self.data = Some(data);
191 self
192 }
193
194 #[must_use]
195 pub fn channel_type(mut self, channel_type: impl Into<ChannelType>) -> Self {
196 let mut data = self.data.take().unwrap();
197 data.channel_type = Some(channel_type.into());
198 self.data = Some(data);
199 self
200 }
201
202 #[must_use]
203 pub fn event_type(mut self, event_type: impl Into<EventType>) -> Self {
204 let mut data = self.data.take().unwrap();
205 data.event_type = Some(event_type.into());
206 self.data = Some(data);
207 self
208 }
209
210 #[must_use]
211 pub fn location(mut self, location: impl Into<VideoLocation>) -> Self {
212 let mut data = self.data.take().unwrap();
213 data.location = Some(location.into());
214 self.data = Some(data);
215 self
216 }
217
218 #[must_use]
219 pub fn location_radius(mut self, location_radius: impl Into<String>) -> Self {
220 let mut data = self.data.take().unwrap();
221 data.location_radius = Some(location_radius.into());
222 self.data = Some(data);
223 self
224 }
225
226 #[must_use]
227 pub fn max_results(mut self, max_results: impl Into<u8>) -> Self {
228 let mut data = self.data.take().unwrap();
229 data.max_results = Some(max_results.into());
230 self.data = Some(data);
231 self
232 }
233
234 #[must_use]
235 pub fn on_behalf_of_content_owner(
236 mut self,
237 on_behalf_of_content_owner: impl Into<String>,
238 ) -> Self {
239 let mut data = self.data.take().unwrap();
240 data.on_behalf_of_content_owner = Some(on_behalf_of_content_owner.into());
241 self.data = Some(data);
242 self
243 }
244
245 #[must_use]
246 pub fn order(mut self, order: impl Into<Order>) -> Self {
247 let mut data = self.data.take().unwrap();
248 data.order = Some(order.into());
249 self.data = Some(data);
250 self
251 }
252
253 #[must_use]
254 pub fn page_token(mut self, page_token: impl Into<String>) -> Self {
255 let mut data = self.data.take().unwrap();
256 data.page_token = Some(page_token.into());
257 self.data = Some(data);
258 self
259 }
260
261 #[must_use]
262 pub fn published_after(mut self, published_after: impl Into<DateTime<Utc>>) -> Self {
263 let mut data = self.data.take().unwrap();
264 data.published_after = Some(published_after.into());
265 self.data = Some(data);
266 self
267 }
268
269 #[must_use]
270 pub fn published_before(mut self, published_before: impl Into<DateTime<Utc>>) -> Self {
271 let mut data = self.data.take().unwrap();
272 data.published_before = Some(published_before.into());
273 self.data = Some(data);
274 self
275 }
276
277 #[must_use]
278 pub fn q(mut self, q: impl Into<String>) -> Self {
279 let mut data = self.data.unwrap();
280 data.q = Some(q.into());
281 self.data = Some(data);
282 self
283 }
284
285 #[must_use]
286 pub fn region_code(mut self, region_code: impl Into<String>) -> Self {
287 let mut data = self.data.take().unwrap();
288 data.region_code = Some(region_code.into());
289 self.data = Some(data);
290 self
291 }
292
293 #[must_use]
294 pub fn relevance_language(mut self, relevance_language: impl Into<String>) -> Self {
295 let mut data = self.data.take().unwrap();
296 data.relevance_language = Some(relevance_language.into());
297 self.data = Some(data);
298 self
299 }
300
301 #[must_use]
302 pub fn safe_search(mut self, safe_search: impl Into<SafeSearch>) -> Self {
303 let mut data = self.data.take().unwrap();
304 data.safe_search = Some(safe_search.into());
305 self.data = Some(data);
306 self
307 }
308
309 #[must_use]
310 pub fn topic_id(mut self, topic_id: impl Into<String>) -> Self {
311 let mut data = self.data.take().unwrap();
312 data.topic_id = Some(topic_id.into());
313 self.data = Some(data);
314 self
315 }
316
317 #[must_use]
318 pub fn item_type(mut self, item_type: impl Into<ItemType>) -> Self {
319 let mut data = self.data.take().unwrap();
320 data.item_type = Some(item_type.into());
321 self.data = Some(data);
322 self
323 }
324
325 #[must_use]
326 pub fn video_caption(mut self, video_caption: impl Into<String>) -> Self {
327 let mut data = self.data.take().unwrap();
328 data.video_caption = Some(video_caption.into());
329 self.data = Some(data);
330 self
331 }
332
333 #[must_use]
334 pub fn video_category_id(mut self, video_category_id: impl Into<String>) -> Self {
335 let mut data = self.data.take().unwrap();
336 data.video_category_id = Some(video_category_id.into());
337 self.data = Some(data);
338 self
339 }
340
341 #[must_use]
342 pub fn video_definition(mut self, video_definition: impl Into<VideoDefinition>) -> Self {
343 let mut data = self.data.take().unwrap();
344 data.video_definition = Some(video_definition.into());
345 self.data = Some(data);
346 self
347 }
348
349 #[must_use]
350 pub fn video_dimension(mut self, video_dimension: impl Into<VideoDimension>) -> Self {
351 let mut data = self.data.take().unwrap();
352 data.video_dimension = Some(video_dimension.into());
353 self.data = Some(data);
354 self
355 }
356
357 #[must_use]
358 pub fn video_embeddable(mut self) -> Self {
359 let mut data = self.data.take().unwrap();
360 data.video_embeddable = true;
361 self.data = Some(data);
362 self
363 }
364
365 #[must_use]
366 pub fn video_license(mut self, video_license: impl Into<VideoLicense>) -> Self {
367 let mut data = self.data.take().unwrap();
368 data.video_license = Some(video_license.into());
369 self.data = Some(data);
370 self
371 }
372
373 #[must_use]
374 pub fn video_syndicated(mut self) -> Self {
375 let mut data = self.data.take().unwrap();
376 data.video_syndicated = true;
377 self.data = Some(data);
378 self
379 }
380
381 #[must_use]
382 pub fn video_type(mut self, video_type: impl Into<VideoType>) -> Self {
383 let mut data = self.data.take().unwrap();
384 data.video_type = Some(video_type.into());
385 self.data = Some(data);
386 self
387 }
388}
389
390impl Future for SearchList {
391 type Output = Result<Response, Error>;
392
393 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
394 if self.future.is_none() {
395 let data = self.data.take().unwrap();
396 self.future = Some(Box::pin(async move {
397 let url = format!(
398 "{}?{}",
399 Self::URL,
400 serde_urlencoded::to_string(&data).context(Serialization)?
401 );
402 debug!("getting {}", url);
403 let response = surf::get(&url).recv_string().await?;
404 serde_json::from_str(&response)
405 .with_context(move || Deserialization { string: response })
406 }));
407 }
408
409 self.future.as_mut().unwrap().as_mut().poll(cx)
410 }
411}
412
413#[derive(Debug, Clone, Serialize)]
414#[serde(rename_all = "camelCase")]
415pub enum ChannelType {
416 Any,
417 Show,
418}
419
420#[derive(Debug, Clone, Serialize)]
421#[serde(rename_all = "camelCase")]
422pub enum EventType {
423 Completed,
424 Live,
425 Upcoming,
426}
427
428#[derive(Debug, Clone)]
429pub struct VideoLocation {
430 longitude: f32,
431 latitude: f32,
432}
433
434impl VideoLocation {
435 #[must_use]
436 pub fn new(longitude: f32, latitude: f32) -> Self {
437 Self {
438 longitude,
439 latitude,
440 }
441 }
442}
443
444impl Serialize for VideoLocation {
445 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
446 where
447 S: Serializer,
448 {
449 serializer.serialize_str(&format!("{},{}", self.longitude, self.latitude))
450 }
451}
452
453#[derive(Debug, Clone, Serialize)]
454#[serde(rename_all = "camelCase")]
455pub enum Order {
456 Date,
457 Rating,
458 Relevance,
459 Title,
460 VideoCount,
461 ViewCount,
462}
463
464#[derive(Debug, Clone, Serialize)]
465#[serde(rename_all = "camelCase")]
466pub enum SafeSearch {
467 Moderate,
468 Strict,
469}
470
471#[derive(Debug, Clone, Serialize)]
472#[serde(rename_all = "camelCase")]
473pub enum ItemType {
474 Channel,
475 Playlist,
476 Video,
477}
478
479#[derive(Debug, Clone, Serialize)]
480#[serde(rename_all = "camelCase")]
481pub enum VideoCaption {
482 ClosedCaption,
483 None,
484}
485
486#[derive(Debug, Clone, Serialize)]
487#[serde(rename_all = "camelCase")]
488pub enum VideoDefinition {
489 High,
490 Standard,
491}
492
493#[derive(Debug, Clone, Serialize)]
494pub enum VideoDimension {
495 #[serde(rename = "3d")]
496 Three,
497 #[serde(rename = "2d")]
498 Two,
499}
500
501#[derive(Debug, Clone, Serialize)]
502#[serde(rename_all = "camelCase")]
503pub enum VideoDuration {
504 Long,
505 Medium,
506 Short,
507}
508
509#[derive(Debug, Clone, Serialize)]
510#[serde(rename_all = "camelCase")]
511pub enum VideoLicense {
512 CreativeCommon,
513 Youtube,
514}
515
516#[derive(Debug, Clone, Serialize)]
517#[serde(rename_all = "camelCase")]
518pub enum VideoType {
519 Episode,
520 Movie,
521}
522
523#[derive(Debug, Clone, Deserialize)]
524#[serde(rename_all = "camelCase")]
525pub struct Response {
526 pub kind: String,
527 pub etag: String,
528 pub prev_page_token: Option<String>,
529 pub region_code: String,
530 pub page_info: PageInfo,
531 pub items: Vec<SearchResult>,
532}
533
534#[derive(Debug, Clone, Deserialize)]
535#[serde(rename_all = "camelCase")]
536pub struct PageInfo {
537 pub total_results: i64,
538 pub results_per_page: i64,
539}
540
541#[derive(Debug, Clone, Deserialize)]
542pub struct SearchResult {
543 pub kind: String,
544 pub etag: String,
545 pub id: Id,
546 pub snippet: Snippet,
547}
548
549#[derive(Debug, Clone, Deserialize)]
550#[serde(rename_all = "camelCase")]
551pub struct Id {
552 pub kind: String,
553 pub video_id: Option<String>,
554 pub channel_id: Option<String>,
555 pub playlist_id: Option<String>,
556}
557
558#[derive(Debug, Clone, Deserialize)]
559#[serde(rename_all = "camelCase")]
560pub struct Snippet {
561 pub published_at: Option<DateTime<Utc>>,
562 pub channel_id: Option<String>,
563 pub title: Option<String>,
564 pub description: Option<String>,
565 pub thumbnails: Option<Thumbnails>,
566 pub channel_title: Option<String>,
567 pub live_broadcast_content: Option<String>,
568}
569
570#[derive(Debug, Clone, Deserialize)]
571pub struct Thumbnails {
572 pub default: Option<Thumbnail>,
573 pub medium: Option<Thumbnail>,
574 pub high: Option<Thumbnail>,
575 pub standard: Option<Thumbnail>,
576 pub maxres: Option<Thumbnail>,
577}
578
579#[derive(Debug, Clone, Deserialize)]
580pub struct Thumbnail {
581 pub url: String,
582 pub width: Option<u64>,
583 pub height: Option<u64>,
584}