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 crate::error::JsonError;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use symphonia_core::{meta::Metadata as ContainerMetadata, probe::ProbedMetadata};
|
use symphonia_core::{meta::Metadata as ContainerMetadata, probe::ProbedMetadata};
|
||||||
|
|
||||||
pub(crate) mod ffprobe;
|
pub(crate) mod ffprobe;
|
||||||
pub(crate) mod ytdl;
|
mod ytdl;
|
||||||
|
pub use ytdl::Output as YoutubeDlOutput;
|
||||||
|
|
||||||
use super::Parsed;
|
use super::Parsed;
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,49 @@
|
|||||||
|
//! `YoutubeDl` track metadata.
|
||||||
|
|
||||||
use super::AuxMetadata;
|
use super::AuxMetadata;
|
||||||
use crate::constants::SAMPLE_RATE_RAW;
|
use crate::constants::SAMPLE_RATE_RAW;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{collections::HashMap, time::Duration};
|
use std::{collections::HashMap, time::Duration};
|
||||||
|
|
||||||
|
/// Information returned by yt-dlp about a URL.
|
||||||
|
///
|
||||||
|
/// Returned by [`crate::input::YoutubeDl::query`].
|
||||||
#[derive(Deserialize, Serialize, Debug)]
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
pub struct Output {
|
pub struct Output {
|
||||||
|
/// The main artist.
|
||||||
pub artist: Option<String>,
|
pub artist: Option<String>,
|
||||||
|
/// The album name.
|
||||||
pub album: Option<String>,
|
pub album: Option<String>,
|
||||||
|
/// The channel name.
|
||||||
pub channel: Option<String>,
|
pub channel: Option<String>,
|
||||||
|
/// The duration of the stream in seconds.
|
||||||
pub duration: Option<f64>,
|
pub duration: Option<f64>,
|
||||||
|
/// The size of the stream.
|
||||||
pub filesize: Option<u64>,
|
pub filesize: Option<u64>,
|
||||||
|
/// Required HTTP headers to fetch the track stream.
|
||||||
pub http_headers: Option<HashMap<String, String>>,
|
pub http_headers: Option<HashMap<String, String>>,
|
||||||
|
/// Release date of this track.
|
||||||
pub release_date: Option<String>,
|
pub release_date: Option<String>,
|
||||||
|
/// The thumbnail URL for this track.
|
||||||
pub thumbnail: Option<String>,
|
pub thumbnail: Option<String>,
|
||||||
|
/// The title of this track.
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
|
/// The track name.
|
||||||
pub track: Option<String>,
|
pub track: Option<String>,
|
||||||
|
/// The date this track was uploaded on.
|
||||||
pub upload_date: Option<String>,
|
pub upload_date: Option<String>,
|
||||||
|
/// The name of the uploader.
|
||||||
pub uploader: Option<String>,
|
pub uploader: Option<String>,
|
||||||
|
/// The stream URL.
|
||||||
pub url: String,
|
pub url: String,
|
||||||
|
/// The URL of the public-facing webpage for this track.
|
||||||
pub webpage_url: Option<String>,
|
pub webpage_url: Option<String>,
|
||||||
|
/// The stream protocol.
|
||||||
pub protocol: Option<String>,
|
pub protocol: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Output {
|
impl Output {
|
||||||
|
/// Requests auxiliary metadata which can be accessed without parsing the file.
|
||||||
pub fn as_aux_metadata(&self) -> AuxMetadata {
|
pub fn as_aux_metadata(&self) -> AuxMetadata {
|
||||||
let album = self.album.clone();
|
let album = self.album.clone();
|
||||||
let track = self.track.clone();
|
let track = self.track.clone();
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ mod error;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod input_tests;
|
pub mod input_tests;
|
||||||
mod live_input;
|
mod live_input;
|
||||||
mod metadata;
|
pub mod metadata;
|
||||||
mod parsed;
|
mod parsed;
|
||||||
mod sources;
|
mod sources;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
@@ -70,7 +70,7 @@ pub use self::{
|
|||||||
compose::*,
|
compose::*,
|
||||||
error::*,
|
error::*,
|
||||||
live_input::*,
|
live_input::*,
|
||||||
metadata::*,
|
metadata::{AuxMetadata, Metadata},
|
||||||
parsed::*,
|
parsed::*,
|
||||||
sources::*,
|
sources::*,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::input::{
|
use crate::input::{
|
||||||
metadata::ytdl::Output,
|
metadata::YoutubeDlOutput,
|
||||||
AudioStream,
|
AudioStream,
|
||||||
AudioStreamError,
|
AudioStreamError,
|
||||||
AuxMetadata,
|
AuxMetadata,
|
||||||
@@ -8,7 +8,6 @@ use crate::input::{
|
|||||||
Input,
|
Input,
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use either::Either;
|
|
||||||
use reqwest::{
|
use reqwest::{
|
||||||
header::{HeaderMap, HeaderName, HeaderValue},
|
header::{HeaderMap, HeaderName, HeaderValue},
|
||||||
Client,
|
Client,
|
||||||
@@ -118,19 +117,19 @@ impl<'a> YoutubeDl<'a> {
|
|||||||
) -> Result<impl Iterator<Item = AuxMetadata>, AudioStreamError> {
|
) -> Result<impl Iterator<Item = AuxMetadata>, AudioStreamError> {
|
||||||
let n_results = n_results.unwrap_or(5);
|
let n_results = n_results.unwrap_or(5);
|
||||||
|
|
||||||
Ok(match &self.query {
|
Ok(self
|
||||||
// Safer to just return the metadata for the pointee if possible
|
.query(n_results)
|
||||||
QueryType::Url(_) => Either::Left(std::iter::once(self.aux_metadata().await?)),
|
.await?
|
||||||
QueryType::Search(_) => Either::Right(
|
.into_iter()
|
||||||
self.query(n_results)
|
.map(|v| v.as_aux_metadata()))
|
||||||
.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 query_str = self.query.as_cow_str(n_results);
|
||||||
let ytdl_args = [
|
let ytdl_args = [
|
||||||
"-j",
|
"-j",
|
||||||
@@ -169,7 +168,7 @@ impl<'a> YoutubeDl<'a> {
|
|||||||
.split(|&b| b == b'\n')
|
.split(|&b| b == b'\n')
|
||||||
.filter(|&x| (!x.is_empty()))
|
.filter(|&x| (!x.is_empty()))
|
||||||
.map(serde_json::from_slice)
|
.map(serde_json::from_slice)
|
||||||
.collect::<Result<Vec<Output>, _>>()
|
.collect::<Result<Vec<YoutubeDlOutput>, _>>()
|
||||||
.map_err(|e| AudioStreamError::Fail(Box::new(e)))?;
|
.map_err(|e| AudioStreamError::Fail(Box::new(e)))?;
|
||||||
|
|
||||||
let meta = out
|
let meta = out
|
||||||
@@ -183,6 +182,41 @@ impl<'a> YoutubeDl<'a> {
|
|||||||
|
|
||||||
Ok(out)
|
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 {
|
impl From<YoutubeDl<'static>> for Input {
|
||||||
@@ -204,34 +238,7 @@ impl Compose for YoutubeDl<'_> {
|
|||||||
let mut results = self.query(1).await?;
|
let mut results = self.query(1).await?;
|
||||||
let result = results.swap_remove(0);
|
let result = results.swap_remove(0);
|
||||||
|
|
||||||
let mut headers = HeaderMap::default();
|
self.get_stream(&result).await
|
||||||
|
|
||||||
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
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_create_async(&self) -> bool {
|
fn should_create_async(&self) -> bool {
|
||||||
|
|||||||
Reference in New Issue
Block a user