Playlist fixes, add YoutubeDl::get_stream function (#279)
* Fix(ytdl): Return all results when querying a URL * Publicly export `songbird::input::metadata::ytdl::Output` Closes: https://github.com/serenity-rs/songbird/issues/277 * Make `YoutubeDl::query` public * Abstract getting streams from YoutubeDl into functions `get_stream` and `get_streams` * fmt, remove get_streams * Fix doc comment * fixup doc comments, export `metadata::ytdl::Output` as `YoutubeDlOutput` * fmt * export metadata * fixup! fixup doc comments, export `metadata::ytdl::Output` as `YoutubeDlOutput`
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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<String>,
|
||||
/// The album name.
|
||||
pub album: Option<String>,
|
||||
/// The channel name.
|
||||
pub channel: Option<String>,
|
||||
/// The duration of the stream in seconds.
|
||||
pub duration: Option<f64>,
|
||||
/// The size of the stream.
|
||||
pub filesize: Option<u64>,
|
||||
/// Required HTTP headers to fetch the track stream.
|
||||
pub http_headers: Option<HashMap<String, String>>,
|
||||
/// Release date of this track.
|
||||
pub release_date: Option<String>,
|
||||
/// The thumbnail URL for this track.
|
||||
pub thumbnail: Option<String>,
|
||||
/// The title of this track.
|
||||
pub title: Option<String>,
|
||||
/// The track name.
|
||||
pub track: Option<String>,
|
||||
/// The date this track was uploaded on.
|
||||
pub upload_date: Option<String>,
|
||||
/// The name of the uploader.
|
||||
pub uploader: Option<String>,
|
||||
/// The stream URL.
|
||||
pub url: String,
|
||||
/// The URL of the public-facing webpage for this track.
|
||||
pub webpage_url: Option<String>,
|
||||
/// The stream protocol.
|
||||
pub protocol: Option<String>,
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -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::*,
|
||||
};
|
||||
|
||||
@@ -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<impl Iterator<Item = AuxMetadata>, 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<Vec<Output>, 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<Vec<YoutubeDlOutput>, 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::<Result<Vec<Output>, _>>()
|
||||
.collect::<Result<Vec<YoutubeDlOutput>, _>>()
|
||||
.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<AudioStream<Box<dyn MediaSource>>, 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<YoutubeDl<'static>> 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 {
|
||||
|
||||
Reference in New Issue
Block a user