From 1d0e6a07e34558d138f5154a094553816fb7e983 Mon Sep 17 00:00:00 2001 From: Julius Riegel Date: Sat, 17 Jul 2021 18:45:09 +0200 Subject: [PATCH] Revert "Reimagine model & add search method." --- Cargo.toml | 19 +-- src/endpoints.rs | 243 +++++-------------------------- src/error.rs | 60 +++----- src/lib.rs | 18 +-- src/model.rs | 98 ------------- src/model/id.rs | 121 --------------- src/model/search.rs | 104 ------------- src/model/thumbnail.rs | 65 --------- src/parsing.rs | 170 +++------------------ src/parsing/search.rs | 188 ------------------------ src/parsing/video_information.rs | 32 ++++ src/tests.rs | 1 + src/tests/endpoints.rs | 10 ++ src/types.rs | 8 + 14 files changed, 138 insertions(+), 999 deletions(-) delete mode 100644 src/model.rs delete mode 100644 src/model/id.rs delete mode 100644 src/model/search.rs delete mode 100644 src/model/thumbnail.rs delete mode 100644 src/parsing/search.rs create mode 100644 src/parsing/video_information.rs create mode 100644 src/tests.rs create mode 100644 src/tests/endpoints.rs create mode 100644 src/types.rs diff --git a/Cargo.toml b/Cargo.toml index 612eb68..c64524a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,7 @@ [package] name = "youtube-metadata" version = "0.2.0" -authors = [ - "trivernis ", - "Vilgot Fredenberg ", -] +authors = ["trivernis "] edition = "2018" description = "YouTube video metadata fetcher" readme = "README.md" @@ -14,17 +11,9 @@ license = "MIT" # 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" lazy_static = "1.4.0" -reqwest = { default-features = false, version = "0.11.3" } -regex = "1" -serde = { features = ["derive"], optional = true, version = "1" } -serde_json = "1" -tracing = "0.1" [dev-dependencies] -tokio = { features = ["macros", "rt-multi-thread"], version = "1.5.0" } - -[features] -default = ["native"] -native = ["reqwest/default-tls"] -rustls = ["reqwest/rustls-tls"] +tokio = { version = "1.5.0", features = ["macros", "rt-multi-thread"] } diff --git a/src/endpoints.rs b/src/endpoints.rs index 2a712bc..93f5628 100644 --- a/src/endpoints.rs +++ b/src/endpoints.rs @@ -1,211 +1,34 @@ -use crate::{ - error::Error, - model::{id::VideoId, search::SearchResult, Resource, Video}, - parsing::{search::search_information, video_information}, -}; - -/// Reusable client, [`NotReusable`]s cousin. -/// -/// Internally wraps around an [`Arc`], so cloning is cheap. -/// -/// [`Arc`]: std::sync::Arc -#[derive(Clone, Debug, Default)] -pub struct Reusable(reqwest::Client); - -impl Reusable { - /// Create a new reusable client. - pub fn new() -> Self { - Self(reqwest::Client::new()) - } - - // Not implemented - /* - * /// Get a playlist by its id. - * pub async fn playlist(&self, playlist: PlaylistId) -> Result { - * todo!() - * } - */ - - /// Search for some query on youtube - /// - /// # Example - /// - /// ```no_run - /// # use youtube_metadata::Reusable; - /// use std::time::Duration; - /// - /// # async fn doc() -> Result<(), Box> { - /// let reusable = Reusable::new(); - /// let first = reusable.search("Rick Astley - Never Gonna Give You Up (Official Music Video)") - /// .await? - /// .videos() - /// .next() - /// .expect("atleast one result"); - /// assert_eq!(first.id.as_str(), "dQw4w9WgXcQ"); - /// assert_eq!(first.length, Duration::from_secs(213)); - /// assert_eq!(first.title, - /// String::from("Rick Astley - Never Gonna Give You Up (Official Music Video)")); - /// assert_eq!(first.uploader.name, "Rick Astley"); - /// # Ok(()) - /// # } - /// ``` - pub async fn search(&self, search: &str) -> Result { - let request = self - .0 - .get("https://youtube.com/results?") - .query(&[("q", search)]) - .build()?; - - let response_text = self.0.execute(request).await?.text().await?; - - search_information(&response_text) - } - - /// Get a video by its id. - pub async fn video(&self, video: VideoId) -> Result { - let url = format!("https://www.youtube.com/watch?v={}", video); - match self.query(&url).await? { - Resource::Video(v) => (Ok(v)), - _ => unreachable!(), - } - } - - /// Fetch a resource from a url. - /// - /// Will only resolve to [`Resource::Video`] right now due to playlists being unsupported. - /// - /// [`Resource`] will currently only contain a video due to playlists being unimplemented. - pub async fn query(&self, query: &str) -> Result { - let request = self.0.get(query).build()?; - - let response_text = self.0.execute(request).await?.text().await?; - - // for now call this since only videos are supported. - Ok(Resource::Video(video_information(&response_text)?)) - } -} - -/// Zero sized associated function holder, [`Reusable`]s cousin. -/// -/// Creates a new client on each invocation. -#[derive(Debug)] -pub struct NotReusable; - -impl NotReusable { - // Not implemented - /* - * /// Get a playlist by its id. - * pub async fn playlist(playlist: PlaylistId) -> Result { - * todo!() - * } - */ - - /// Search for some query on youtube - /// - /// # Example - /// - /// ```no_run - /// # use youtube_metadata::NotReusable; - /// # - /// use std::time::Duration; - /// - /// # async fn doc() -> Result<(), Box> { - /// let first = NotReusable::search("Rick Astley - Never Gonna Give You Up (Official Music Video)") - /// .await? - /// .videos() - /// .next() - /// .expect("atleast one result"); - /// assert_eq!(first.id.as_str(), "dQw4w9WgXcQ"); - /// assert_eq!(first.length, Duration::from_secs(213)); - /// assert_eq!(first.title, - /// String::from("Rick Astley - Never Gonna Give You Up (Official Music Video)")); - /// assert_eq!(first.uploader.name, "Rick Astley"); - /// # Ok(()) - /// # } - /// ``` - pub async fn search(search: &str) -> Result { - let client = reqwest::Client::new(); - - let request = client - .get("https://youtube.com/results?") - .query(&[("q", search)]) - .build()?; - - let response_text = client.execute(request).await?.text().await?; - - search_information(&response_text) - } - - /// Get a video by its id. - pub async fn video(video: VideoId) -> Result { - let url = format!("https://www.youtube.com/watch?v={}", video); - match Self::query(&url).await? { - Resource::Video(v) => (Ok(v)), - _ => unreachable!(), - } - } - - /// Fetch a resource from a url. - /// - /// Will only resolve to [`Resource::Video`] right now due to playlists being unsupported. - /// - /// [`Resource`] will currently only contain a video due to playlists being unimplemented. - pub async fn query(query: &str) -> Result { - let client = reqwest::Client::new(); - - let request = client.get(query).build()?; - - let response_text = client.execute(request).await?.text().await?; - - // for now call this since only videos are supported. - Ok(Resource::Video(video_information(&response_text)?)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use std::time::Duration; - - #[tokio::test] - async fn rickroll() -> Result<(), Box> { - let search = - NotReusable::search("Rick Astley - Never Gonna Give You Up (Official Music Video)") - .await? - .videos() - .next() - .expect("atleast one result"); - let video = NotReusable::video(VideoId::new("dQw4w9WgXcQ")).await?; - - assert_eq!(search.id.as_str(), "dQw4w9WgXcQ"); - assert_eq!(video.id.as_str(), "dQw4w9WgXcQ"); - assert_eq!(search.length, Duration::from_secs(213)); - assert_eq!(video.length, Duration::from_millis(212091)); - assert_eq!( - search.title.as_str(), - "Rick Astley - Never Gonna Give You Up (Official Music Video)" - ); - assert_eq!( - video.title.as_str(), - "Rick Astley - Never Gonna Give You Up (Official Music Video)" - ); - assert_eq!(search.uploader.name, "Rick Astley"); - assert_eq!(video.uploader.name, "Rick Astley"); - Ok(()) - } - - #[tokio::test] - async fn live() -> Result<(), Box> { - NotReusable::search("live music").await?; - - Ok(()) - } - - #[tokio::test] - async fn playlist() -> Result<(), Box> { - NotReusable::search("music playlist").await?; - - Ok(()) - } +use crate::error::Result; +use crate::parsing::video_information::parse_video_information; +use crate::types::VideoInformation; + +/// Returns information about a video +/// ``` +/// use youtube_metadata::get_video_information; +/// # #[tokio::test] +/// # async fn doctest() { +/// let information = get_video_information("https://www.youtube.com/watch?v=dQw4w9WgXcQ") +/// .await +/// .unwrap(); +/// assert_eq!(information.id, "dQw4w9WgXcQ".to_string()); +/// assert_eq!( +/// information.url, +/// "https://www.youtube.com/watch?v=dQw4w9WgXcQ".to_string() +/// ); +/// assert_eq!(information.uploader, "RickAstleyVEVO".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()) +/// ); +/// # } +/// ``` +pub async fn get_video_information(url: &str) -> Result { + let response = reqwest::get(url).await?; + let response_text = response.text().await?; + + parse_video_information(&response_text) } diff --git a/src/error.rs b/src/error.rs index 6c6c9c1..332fd29 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,3 @@ -//! Error types of this library. -//! -//! Note that parsing should never fail and is indicative of an interal error. use std::{ error::Error as StdError, fmt::{Display, Formatter, Result as FmtResult}, @@ -8,39 +5,12 @@ use std::{ use reqwest::Error as ReqwestError; -#[derive(Debug)] -#[doc(hidden)] -pub struct ParseError { - pub(crate) kind: ParseErrorKind, -} - -impl Display for ParseError { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - use ParseErrorKind::*; - - match self.kind { - Other => f.write_str("something failed to parse"), - Regex => f.write_str("fetching json using regex failed"), - } - } -} - -impl StdError for ParseError {} - -#[derive(Debug)] -pub(crate) enum ParseErrorKind { - Other, - Regex, -} +pub type Result = std::result::Result; -/// Error types. #[derive(Debug)] pub enum Error { - /// Error doing http. Reqwest(ReqwestError), - /// Internal parsing error. - /// Hitting this should never happen and is a bug. - Parse(ParseError), + Parse(Parsing), } impl Display for Error { @@ -48,7 +18,7 @@ impl Display for Error { use Error::*; match self { Reqwest(e) => e.fmt(f), - Parse(_) => write!(f, "json parsing error"), + Parse(_) => write!(f, "parse error"), } } } @@ -63,14 +33,32 @@ impl StdError for Error { } } +impl From for Error { + fn from(s: Parsing) -> Self { + Self::Parse(s) + } +} + impl From for Error { fn from(e: ReqwestError) -> Self { Self::Reqwest(e) } } -impl From for Error { - fn from(e: ParseError) -> Self { - Self::Parse(e) +#[derive(Debug)] +pub enum Parsing { + MissingElement(String), + MissingAttribute(String), +} + +impl Display for Parsing { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + use Parsing::*; + match self { + MissingAttribute(s) => write!(f, "missing attribute: {}", s), + MissingElement(s) => write!(f, "missing element: {}", s), + } } } + +impl StdError for Parsing {} diff --git a/src/lib.rs b/src/lib.rs index dd688ea..eb7160f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,9 @@ -//! Library that searches youtube and parses the result to [`model`]. -//! -//! [`Reusable`] reuses the same http client on each `GET` request. -//! This takes advantage of keep-alive connections. -#![deny(clippy::inconsistent_struct_constructor)] -#![deny(missing_docs)] -#![deny(missing_debug_implementations)] -#![deny(rustdoc::broken_intra_doc_links)] - -mod endpoints; +pub(crate) mod endpoints; pub mod error; -pub mod model; pub(crate) mod parsing; +pub(crate) mod types; + +pub use endpoints::get_video_information; -pub use endpoints::{NotReusable, Reusable}; +#[cfg(test)] +mod tests; diff --git a/src/model.rs b/src/model.rs deleted file mode 100644 index fc915f0..0000000 --- a/src/model.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! Mapping of output. -//! -//! Use the resource's id's to get thumbnails or urls. - -use std::time::Duration; - -use id::{PlaylistId, VideoId}; -use search::{PartialPlaylist, PartialPlaylistVideo}; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -use self::id::ChannelId; - -pub mod id; -pub mod search; -pub mod thumbnail; - -/// Information about a channel. -/// -/// Note that this is *not* a user so its [`Channel::id`] is of the form of `/channel/ID`, not -/// `/user/ID`. -/// The link still resolves to the same page, so this should not be an issue in most cases. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct Channel { - /// The channel's unique Id. - pub id: ChannelId, - /// The channel's name. - pub name: String, -} - -/// Information about a playlist. -// Hide since not implemented. -#[doc(hidden)] -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct Playlist { - /// The playlist's unique Id. - pub id: PlaylistId, - /// The playlist's tracks. - pub tracks: Vec