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:
Sapphire
2025-05-20 12:59:26 -05:00
committed by GitHub
parent 8956352f13
commit c910d7087d
4 changed files with 76 additions and 45 deletions

View File

@@ -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;

View File

@@ -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();

View File

@@ -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::*,
}; };

View File

@@ -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?)),
QueryType::Search(_) => Either::Right(
self.query(n_results)
.await? .await?
.into_iter() .into_iter()
.map(|v| v.as_aux_metadata()), .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 {