Add get_video_information endpoint
Signed-off-by: trivernis <trivernis@protonmail.com>pull/1/head
commit
7643ca9faf
@ -0,0 +1,3 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
.idea
|
@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "youtube-metadata"
|
||||
version = "0.1.0"
|
||||
authors = ["trivernis <trivernis@protonmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
reqwest = "0.11.3"
|
||||
scraper = "0.12.0"
|
||||
thiserror = "1.0.24"
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = {version = "1.5.0", features = ["macros", "rt-multi-thread"]}
|
@ -0,0 +1,11 @@
|
||||
use crate::error::YoutubeResult;
|
||||
use crate::parsing::video_information::parse_video_information;
|
||||
use crate::types::VideoInformation;
|
||||
|
||||
/// Returns information about a video
|
||||
pub async fn get_video_information(url: &str) -> YoutubeResult<VideoInformation> {
|
||||
let response = reqwest::get(url).await?;
|
||||
let response_text = response.text().await?;
|
||||
|
||||
parse_video_information(&response_text)
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
use thiserror::Error;
|
||||
|
||||
pub type YoutubeResult<T> = Result<T, YoutubeError>;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum YoutubeError {
|
||||
#[error(transparent)]
|
||||
Reqwest(#[from] reqwest::Error),
|
||||
|
||||
#[error("Parse Error: {0}")]
|
||||
ParseError(String),
|
||||
}
|
||||
|
||||
impl From<&str> for YoutubeError {
|
||||
fn from(s: &str) -> Self {
|
||||
Self::ParseError(s.to_string())
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
pub(crate) mod endpoints;
|
||||
pub mod error;
|
||||
pub(crate) mod parsing;
|
||||
pub(crate) mod types;
|
||||
|
||||
pub use endpoints::get_video_information;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
@ -0,0 +1,31 @@
|
||||
use crate::error::{YoutubeError, YoutubeResult};
|
||||
use scraper::{ElementRef, Html, Selector};
|
||||
|
||||
pub mod video_information;
|
||||
|
||||
/// Tries selecting one element or fails if the element can't be found
|
||||
fn try_select_one<'a>(document: &'a Html, selector: &Selector) -> YoutubeResult<ElementRef<'a>> {
|
||||
document
|
||||
.select(selector)
|
||||
.next()
|
||||
.ok_or(YoutubeError::ParseError(format!(
|
||||
"Missing Element: {:?}",
|
||||
selector
|
||||
)))
|
||||
}
|
||||
|
||||
/// Tries to select a given attribute
|
||||
fn try_select_attribute<'a>(
|
||||
document: &'a Html,
|
||||
selector: &Selector,
|
||||
attribute: &str,
|
||||
) -> YoutubeResult<&'a str> {
|
||||
let element = try_select_one(document, selector)?;
|
||||
element
|
||||
.value()
|
||||
.attr(attribute)
|
||||
.ok_or(YoutubeError::ParseError(format!(
|
||||
"Missing attribute '{}'",
|
||||
attribute
|
||||
)))
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
use crate::error::YoutubeResult;
|
||||
use crate::parsing::try_select_attribute;
|
||||
use crate::types::VideoInformation;
|
||||
use scraper::{Html, Selector};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref TITLE_SELECTOR: Selector = Selector::parse(r#"meta[property="og:title"]"#).unwrap();
|
||||
static ref THUMBNAIL_SELECTOR: Selector = Selector::parse(r#"meta[property="og:image"]"#).unwrap();
|
||||
static ref URL_SELECTOR: Selector = Selector::parse(r#"link[rel="canonical"]"#).unwrap();
|
||||
}
|
||||
|
||||
/// Parses information about a video from the html
|
||||
pub fn parse_video_information(html: &str) -> YoutubeResult<VideoInformation> {
|
||||
let document = Html::parse_document(html);
|
||||
let url = try_select_attribute(&document, &URL_SELECTOR, "href")?;
|
||||
let title = try_select_attribute(&document, &TITLE_SELECTOR, "content")?;
|
||||
let thumbnail = try_select_attribute(&document, &THUMBNAIL_SELECTOR, "content").ok();
|
||||
|
||||
Ok(VideoInformation {
|
||||
url: url.to_string(),
|
||||
title: title.to_string(),
|
||||
thumbnail: thumbnail.map(|s| s.to_string()),
|
||||
})
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
use crate::endpoints::get_video_information;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_video_information() {
|
||||
let information = get_video_information("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
information.url,
|
||||
"https://www.youtube.com/watch?v=dQw4w9WgXcQ".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
information.title,
|
||||
"Rick Astley - Never Gonna Give You Up (Video)".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
information.thumbnail,
|
||||
Some("https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg".to_string())
|
||||
);
|
||||
|
||||
assert!(
|
||||
get_video_information("https://www.youtube.com/watch?v=FFFFFFFFFFF")
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
#[cfg(test)]
|
||||
mod endpoints_test;
|
@ -0,0 +1,6 @@
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct VideoInformation {
|
||||
pub url: String,
|
||||
pub title: String,
|
||||
pub thumbnail: Option<String>,
|
||||
}
|
Loading…
Reference in New Issue