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