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

View File

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

View File

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

View File

@@ -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)
Ok(self
.query(n_results)
.await?
.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 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 {