diff --git a/Cargo.toml b/Cargo.toml index 961d828..4f1ed28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ crypto_secretbox = { optional = true, features = ["std"], version = "0.1" } dashmap = { optional = true, version = "5" } derivative = "2" discortp = { default-features = false, features = ["discord", "pnet", "rtp"], optional = true, version = "0.6" } +either = "1.9.0" flume = { optional = true, version = "0.11" } futures = "0.3" nohash-hasher = { optional = true, version = "0.2.0" } diff --git a/src/input/mod.rs b/src/input/mod.rs index e69d3e2..bf72363 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -114,7 +114,7 @@ use tokio::runtime::Handle as TokioHandle; /// let mut lazy = YoutubeDl::new( /// reqwest::Client::new(), /// // Referenced under CC BY-NC-SA 3.0 -- https://creativecommons.org/licenses/by-nc-sa/3.0/ -/// "https://cloudkicker.bandcamp.com/track/94-days".to_string(), +/// "https://cloudkicker.bandcamp.com/track/94-days", /// ); /// let lazy_c = lazy.clone(); /// diff --git a/src/input/sources/ytdl.rs b/src/input/sources/ytdl.rs index 5ee4782..6a539f4 100644 --- a/src/input/sources/ytdl.rs +++ b/src/input/sources/ytdl.rs @@ -8,11 +8,12 @@ use crate::input::{ Input, }; use async_trait::async_trait; +use either::Either; use reqwest::{ header::{HeaderMap, HeaderName, HeaderValue}, Client, }; -use std::{error::Error, io::ErrorKind}; +use std::{borrow::Cow, error::Error, io::ErrorKind}; use symphonia_core::io::MediaSource; use tokio::process::Command; @@ -21,9 +22,19 @@ use super::HlsRequest; const YOUTUBE_DL_COMMAND: &str = "yt-dlp"; #[derive(Clone, Debug)] -enum QueryType { - Url(String), - Search(String), +enum QueryType<'a> { + Url(Cow<'a, str>), + Search(Cow<'a, str>), +} + +impl<'a> QueryType<'a> { + fn as_cow_str(&'a self, n_results: usize) -> Cow<'a, str> { + match self { + Self::Url(Cow::Owned(u)) => Cow::Borrowed(u), + Self::Url(Cow::Borrowed(u)) => Cow::Borrowed(u), + Self::Search(s) => Cow::Owned(format!("ytsearch{n_results}:{s}")), + } + } } /// A lazily instantiated call to download a file, finding its URL via youtube-dl. @@ -34,21 +45,21 @@ enum QueryType { /// /// [`HttpRequest`]: super::HttpRequest #[derive(Clone, Debug)] -pub struct YoutubeDl { - program: &'static str, +pub struct YoutubeDl<'a> { + program: &'a str, client: Client, metadata: Option, - query: QueryType, + query: QueryType<'a>, user_args: Vec, } -impl YoutubeDl { +impl<'a> YoutubeDl<'a> { /// Creates a lazy request to select an audio stream from `url`, using "yt-dlp". /// /// This requires a reqwest client: ideally, one should be created and shared between /// all requests. #[must_use] - pub fn new(client: Client, url: String) -> Self { + pub fn new(client: Client, url: impl Into>) -> Self { Self::new_ytdl_like(YOUTUBE_DL_COMMAND, client, url) } @@ -56,12 +67,12 @@ impl YoutubeDl { /// /// [`new`]: Self::new #[must_use] - pub fn new_ytdl_like(program: &'static str, client: Client, url: String) -> Self { + pub fn new_ytdl_like(program: &'a str, client: Client, url: impl Into>) -> Self { Self { program, client, metadata: None, - query: QueryType::Url(url), + query: QueryType::Url(url.into()), user_args: Vec::new(), } } @@ -69,19 +80,23 @@ impl YoutubeDl { /// Creates a request to search youtube for an optionally specified number of videos matching `query`, /// using "yt-dlp". #[must_use] - pub fn new_search(client: Client, query: String) -> Self { + pub fn new_search(client: Client, query: impl Into>) -> Self { Self::new_search_ytdl_like(YOUTUBE_DL_COMMAND, client, query) } /// Creates a request to search youtube for an optionally specified number of videos matching `query`, /// using `program`. #[must_use] - pub fn new_search_ytdl_like(program: &'static str, client: Client, query: String) -> Self { + pub fn new_search_ytdl_like( + program: &'a str, + client: Client, + query: impl Into>, + ) -> Self { Self { program, client, metadata: None, - query: QueryType::Search(query), + query: QueryType::Search(query.into()), user_args: Vec::new(), } } @@ -100,33 +115,26 @@ impl YoutubeDl { pub async fn search( &mut self, n_results: Option, - ) -> Result, AudioStreamError> { + ) -> Result, AudioStreamError> { let n_results = n_results.unwrap_or(5); Ok(match &self.query { // Safer to just return the metadata for the pointee if possible - QueryType::Url(_) => vec![self.aux_metadata().await?], - QueryType::Search(_) => self - .query(n_results) - .await? - .into_iter() - .map(|v| v.as_aux_metadata()) - .collect(), + QueryType::Url(_) => Either::Left(std::iter::once(self.aux_metadata().await?)), + QueryType::Search(_) => Either::Right( + self.query(n_results) + .await? + .into_iter() + .map(|v| v.as_aux_metadata()), + ), }) } async fn query(&mut self, n_results: usize) -> Result, AudioStreamError> { - let new_query; - let query_str = match &self.query { - QueryType::Url(url) => url, - QueryType::Search(query) => { - new_query = format!("ytsearch{n_results}:{query}"); - &new_query - }, - }; + let query_str = self.query.as_cow_str(n_results); let ytdl_args = [ "-j", - query_str, + &query_str, "-f", "ba[abr>0][vcodec=none]/best", "--no-playlist", @@ -177,14 +185,14 @@ impl YoutubeDl { } } -impl From for Input { - fn from(val: YoutubeDl) -> Self { +impl From> for Input { + fn from(val: YoutubeDl<'static>) -> Self { Input::Lazy(Box::new(val)) } } #[async_trait] -impl Compose for YoutubeDl { +impl<'a> Compose for YoutubeDl<'a> { fn create(&mut self) -> Result>, AudioStreamError> { Err(AudioStreamError::Unsupported) } @@ -254,33 +262,32 @@ mod tests { #[tokio::test] #[ntest::timeout(20_000)] async fn ytdl_track_plays() { - track_plays_mixed(|| YoutubeDl::new(Client::new(), YTDL_TARGET.into())).await; + track_plays_mixed(|| YoutubeDl::new(Client::new(), YTDL_TARGET)).await; } #[tokio::test] #[ignore] #[ntest::timeout(20_000)] async fn ytdl_page_with_playlist_plays() { - track_plays_passthrough(|| YoutubeDl::new(Client::new(), YTDL_PLAYLIST_TARGET.into())) - .await; + track_plays_passthrough(|| YoutubeDl::new(Client::new(), YTDL_PLAYLIST_TARGET)).await; } #[tokio::test] #[ntest::timeout(20_000)] async fn ytdl_forward_seek_correct() { - forward_seek_correct(|| YoutubeDl::new(Client::new(), YTDL_TARGET.into())).await; + forward_seek_correct(|| YoutubeDl::new(Client::new(), YTDL_TARGET)).await; } #[tokio::test] #[ntest::timeout(20_000)] async fn ytdl_backward_seek_correct() { - backward_seek_correct(|| YoutubeDl::new(Client::new(), YTDL_TARGET.into())).await; + backward_seek_correct(|| YoutubeDl::new(Client::new(), YTDL_TARGET)).await; } #[tokio::test] #[ntest::timeout(20_000)] async fn fake_exe_errors() { - let mut ytdl = YoutubeDl::new_ytdl_like("yt-dlq", Client::new(), YTDL_TARGET.into()); + let mut ytdl = YoutubeDl::new_ytdl_like("yt-dlq", Client::new(), YTDL_TARGET); assert!(ytdl.aux_metadata().await.is_err()); } @@ -289,11 +296,11 @@ mod tests { #[ignore] #[ntest::timeout(20_000)] async fn ytdl_search_plays() { - let mut ytdl = YoutubeDl::new_search(Client::new(), "cloudkicker 94 days".into()); + let mut ytdl = YoutubeDl::new_search(Client::new(), "cloudkicker 94 days"); let res = ytdl.search(Some(1)).await; let res = res.unwrap(); - assert_eq!(res.len(), 1); + assert_eq!(res.count(), 1); track_plays_passthrough(move || ytdl).await; } @@ -302,9 +309,9 @@ mod tests { #[ignore] #[ntest::timeout(20_000)] async fn ytdl_search_3() { - let mut ytdl = YoutubeDl::new_search(Client::new(), "test".into()); + let mut ytdl = YoutubeDl::new_search(Client::new(), "test"); let res = ytdl.search(Some(3)).await; - assert_eq!(res.unwrap().len(), 3); + assert_eq!(res.unwrap().count(), 3); } } diff --git a/src/tracks/state.rs b/src/tracks/state.rs index 5aee7f4..3f2bb65 100644 --- a/src/tracks/state.rs +++ b/src/tracks/state.rs @@ -49,7 +49,7 @@ mod tests { let (t_handle, config) = Config::test_cfg(true); let mut driver = Driver::new(config.clone()); - let file = YoutubeDl::new(Client::new(), YTDL_TARGET.into()); + let file = YoutubeDl::new(Client::new(), YTDL_TARGET); let handle = driver.play(Track::from(file)); let state = t_handle