diff --git a/src/input/metadata/mod.rs b/src/input/metadata/mod.rs index 5cdce91..6501980 100644 --- a/src/input/metadata/mod.rs +++ b/src/input/metadata/mod.rs @@ -1,9 +1,12 @@ +//! Metadata formats specific to [`crate::input::Compose`] types. + use crate::error::JsonError; use std::time::Duration; use symphonia_core::{meta::Metadata as ContainerMetadata, probe::ProbedMetadata}; pub(crate) mod ffprobe; -pub(crate) mod ytdl; +mod ytdl; +pub use ytdl::Output as YoutubeDlOutput; use super::Parsed; diff --git a/src/input/metadata/ytdl.rs b/src/input/metadata/ytdl.rs index 38c7025..3a43bde 100644 --- a/src/input/metadata/ytdl.rs +++ b/src/input/metadata/ytdl.rs @@ -1,28 +1,49 @@ +//! `YoutubeDl` track metadata. + use super::AuxMetadata; use crate::constants::SAMPLE_RATE_RAW; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, time::Duration}; +/// Information returned by yt-dlp about a URL. +/// +/// Returned by [`crate::input::YoutubeDl::query`]. #[derive(Deserialize, Serialize, Debug)] pub struct Output { + /// The main artist. pub artist: Option, + /// The album name. pub album: Option, + /// The channel name. pub channel: Option, + /// The duration of the stream in seconds. pub duration: Option, + /// The size of the stream. pub filesize: Option, + /// Required HTTP headers to fetch the track stream. pub http_headers: Option>, + /// Release date of this track. pub release_date: Option, + /// The thumbnail URL for this track. pub thumbnail: Option, + /// The title of this track. pub title: Option, + /// The track name. pub track: Option, + /// The date this track was uploaded on. pub upload_date: Option, + /// The name of the uploader. pub uploader: Option, + /// The stream URL. pub url: String, + /// The URL of the public-facing webpage for this track. pub webpage_url: Option, + /// The stream protocol. pub protocol: Option, } impl Output { + /// Requests auxiliary metadata which can be accessed without parsing the file. pub fn as_aux_metadata(&self) -> AuxMetadata { let album = self.album.clone(); let track = self.track.clone(); diff --git a/src/input/mod.rs b/src/input/mod.rs index 0eb7a6e..dc2703e 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -59,7 +59,7 @@ mod error; #[cfg(test)] pub mod input_tests; mod live_input; -mod metadata; +pub mod metadata; mod parsed; mod sources; pub mod utils; @@ -70,7 +70,7 @@ pub use self::{ compose::*, error::*, live_input::*, - metadata::*, + metadata::{AuxMetadata, Metadata}, parsed::*, sources::*, }; diff --git a/src/input/sources/ytdl.rs b/src/input/sources/ytdl.rs index a51341f..837faf2 100644 --- a/src/input/sources/ytdl.rs +++ b/src/input/sources/ytdl.rs @@ -1,5 +1,5 @@ use crate::input::{ - metadata::ytdl::Output, + metadata::YoutubeDlOutput, AudioStream, AudioStreamError, AuxMetadata, @@ -8,7 +8,6 @@ use crate::input::{ Input, }; use async_trait::async_trait; -use either::Either; use reqwest::{ header::{HeaderMap, HeaderName, HeaderValue}, Client, @@ -118,19 +117,19 @@ impl<'a> YoutubeDl<'a> { ) -> 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(_) => 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()), - ), - }) + Ok(self + .query(n_results) + .await? + .into_iter() + .map(|v| v.as_aux_metadata())) } - async fn query(&mut self, n_results: usize) -> Result, AudioStreamError> { + /// Runs a search for the given query, returning a list of up to `n_results` + /// possible matches. + pub async fn query( + &mut self, + n_results: usize, + ) -> Result, AudioStreamError> { let query_str = self.query.as_cow_str(n_results); let ytdl_args = [ "-j", @@ -169,7 +168,7 @@ impl<'a> YoutubeDl<'a> { .split(|&b| b == b'\n') .filter(|&x| (!x.is_empty())) .map(serde_json::from_slice) - .collect::, _>>() + .collect::, _>>() .map_err(|e| AudioStreamError::Fail(Box::new(e)))?; let meta = out @@ -183,6 +182,41 @@ impl<'a> YoutubeDl<'a> { Ok(out) } + + /// Get the audio stream from a [`YoutubeDlOutput`]. + pub async fn get_stream( + &self, + result: &YoutubeDlOutput, + ) -> Result>, AudioStreamError> { + let mut headers = HeaderMap::default(); + + if let Some(map) = &result.http_headers { + headers.extend(map.iter().filter_map(|(k, v)| { + Some(( + HeaderName::from_bytes(k.as_bytes()).ok()?, + HeaderValue::from_str(v).ok()?, + )) + })); + } + + #[allow(clippy::single_match_else)] + match result.protocol.as_deref() { + Some("m3u8_native") => { + let mut req = + HlsRequest::new_with_headers(self.client.clone(), result.url.clone(), headers); + req.create() + }, + _ => { + let mut req = HttpRequest { + client: self.client.clone(), + request: result.url.clone(), + headers, + content_length: result.filesize, + }; + req.create_async().await + }, + } + } } impl From> for Input { @@ -204,34 +238,7 @@ impl Compose for YoutubeDl<'_> { let mut results = self.query(1).await?; let result = results.swap_remove(0); - let mut headers = HeaderMap::default(); - - if let Some(map) = result.http_headers { - headers.extend(map.iter().filter_map(|(k, v)| { - Some(( - HeaderName::from_bytes(k.as_bytes()).ok()?, - HeaderValue::from_str(v).ok()?, - )) - })); - } - - #[allow(clippy::single_match_else)] - match result.protocol.as_deref() { - Some("m3u8_native") => { - let mut req = - HlsRequest::new_with_headers(self.client.clone(), result.url, headers); - req.create() - }, - _ => { - let mut req = HttpRequest { - client: self.client.clone(), - request: result.url, - headers, - content_length: result.filesize, - }; - req.create_async().await - }, - } + self.get_stream(&result).await } fn should_create_async(&self) -> bool {