use super::{
children_to_reader,
error::{Error, Result},
Codec,
Container,
Input,
Metadata,
};
use serde_json::Value;
use std::{
io::{BufRead, BufReader, Read},
process::{Command, Stdio},
};
use tokio::{process::Command as TokioCommand, task};
use tracing::trace;
const YOUTUBE_DL_COMMAND: &str = if cfg!(feature = "youtube-dlc") {
"youtube-dlc"
} else {
"youtube-dl"
};
/// Creates a streamed audio source with `youtube-dl` and `ffmpeg`.
///
/// This source is not seek-compatible.
/// If you need looping or track seeking, then consider using
/// [`Restartable::ytdl`].
///
/// Uses `youtube-dlc` if the `"youtube-dlc"` feature is enabled.
///
/// [`Restartable::ytdl`]: crate::input::restartable::Restartable::ytdl
pub async fn ytdl(uri: &str) -> Result {
_ytdl(uri, &[]).await
}
pub(crate) async fn _ytdl(uri: &str, pre_args: &[&str]) -> Result {
let ytdl_args = [
"--print-json",
"-f",
"webm[abr>0]/bestaudio/best",
"-R",
"infinite",
"--no-playlist",
"--ignore-config",
"--no-warnings",
uri,
"-o",
"-",
];
let ffmpeg_args = [
"-f",
"s16le",
"-ac",
"2",
"-ar",
"48000",
"-acodec",
"pcm_f32le",
"-",
];
let mut youtube_dl = Command::new(YOUTUBE_DL_COMMAND)
.args(&ytdl_args)
.stdin(Stdio::null())
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
// This rigmarole is required due to the inner synchronous reading context.
let stderr = youtube_dl.stderr.take();
let (returned_stderr, value) = task::spawn_blocking(move || {
let mut s = stderr.unwrap();
let out: Result = {
let mut o_vec = vec![];
let mut serde_read = BufReader::new(s.by_ref());
// Newline...
if let Ok(len) = serde_read.read_until(0xA, &mut o_vec) {
serde_json::from_slice(&o_vec[..len]).map_err(|err| Error::Json {
error: err,
parsed_text: std::str::from_utf8(&o_vec).unwrap_or_default().to_string(),
})
} else {
Result::Err(Error::Metadata)
}
};
(s, out)
})
.await
.map_err(|_| Error::Metadata)?;
youtube_dl.stderr = Some(returned_stderr);
let taken_stdout = youtube_dl.stdout.take().ok_or(Error::Stdout)?;
let ffmpeg = Command::new("ffmpeg")
.args(pre_args)
.arg("-i")
.arg("-")
.args(&ffmpeg_args)
.stdin(taken_stdout)
.stderr(Stdio::null())
.stdout(Stdio::piped())
.spawn()?;
let metadata = Metadata::from_ytdl_output(value?);
trace!("ytdl metadata {:?}", metadata);
Ok(Input::new(
true,
children_to_reader::(vec![youtube_dl, ffmpeg]),
Codec::FloatPcm,
Container::Raw,
Some(metadata),
))
}
pub(crate) async fn _ytdl_metadata(uri: &str) -> Result {
// Most of these flags are likely unused, but we want identical search
// and/or selection as the above functions.
let ytdl_args = [
"-j",
"-f",
"webm[abr>0]/bestaudio/best",
"-R",
"infinite",
"--no-playlist",
"--ignore-config",
"--no-warnings",
uri,
"-o",
"-",
];
let youtube_dl_output = TokioCommand::new(YOUTUBE_DL_COMMAND)
.args(&ytdl_args)
.stdin(Stdio::null())
.output()
.await?;
let o_vec = youtube_dl_output.stderr;
let end = (&o_vec)
.iter()
.position(|el| *el == 0xA)
.unwrap_or_else(|| o_vec.len());
let value = serde_json::from_slice(&o_vec[..end]).map_err(|err| Error::Json {
error: err,
parsed_text: std::str::from_utf8(&o_vec).unwrap_or_default().to_string(),
})?;
let metadata = Metadata::from_ytdl_output(value);
Ok(metadata)
}
/// Creates a streamed audio source from YouTube search results with `youtube-dl(c)`,`ffmpeg`, and `ytsearch`.
/// Takes the first video listed from the YouTube search.
///
/// This source is not seek-compatible.
/// If you need looping or track seeking, then consider using
/// [`Restartable::ytdl_search`].
///
/// Uses `youtube-dlc` if the `"youtube-dlc"` feature is enabled.
///
/// [`Restartable::ytdl_search`]: crate::input::restartable::Restartable::ytdl_search
pub async fn ytdl_search(name: &str) -> Result {
ytdl(&format!("ytsearch1:{}", name)).await
}