yt_api/
search.rs

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/// custom error type for the search endpoint
16#[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
39/// request struct for the search endpoint
40pub 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	/// create struct with an [`ApiKey`](../struct.ApiKey.html)
114	#[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}