pub mod reader {
use crate::{
audio::source::{AudioSource, HttpSource, create_client},
common::types::AnyResult,
};
use std::io::{Read, Seek, SeekFrom};
use symphonia::core::io::MediaSource;
pub struct HttpReader {
inner: HttpSource,
}
impl HttpReader {
pub async fn new(
url: &str,
local_addr: Option<std::net::IpAddr>,
proxy: Option<crate::config::HttpProxyConfig>,
) -> AnyResult<Self> {
let user_agent = crate::common::utils::default_user_agent();
let client = create_client(user_agent, local_addr, proxy, None)?;
let inner = HttpSource::new(client, url).await?;
Ok(Self { inner })
}
}
impl Read for HttpReader {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.inner.read(buf)
}
}
impl Seek for HttpReader {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
self.inner.seek(pos)
}
}
impl MediaSource for HttpReader {
fn is_seekable(&self) -> bool {
self.inner.is_seekable()
}
fn byte_len(&self) -> Option<u64> {
self.inner.byte_len()
}
}
impl HttpReader {
pub fn content_type(&self) -> Option<String> {
self.inner.content_type()
}
}
}
pub mod track {
use crate::{
common::types::AudioFormat,
config::HttpProxyConfig,
sources::{
http::reader,
playable_track::{PlayableTrack, ResolvedTrack},
},
};
use async_trait::async_trait;
pub struct HttpTrack {
pub url: String,
pub local_addr: Option<std::net::IpAddr>,
pub proxy: Option<HttpProxyConfig>,
}
#[async_trait]
impl PlayableTrack for HttpTrack {
async fn resolve(&self) -> Result<ResolvedTrack, String> {
let hint = std::path::Path::new(&self.url)
.extension()
.and_then(|s| s.to_str())
.map(AudioFormat::from_ext)
.filter(|f| *f != AudioFormat::Unknown);
let reader = reader::HttpReader::new(&self.url, self.local_addr, self.proxy.clone())
.await
.map(|r| Box::new(r) as Box<dyn symphonia::core::io::MediaSource>)
.map_err(|e| format!("Failed to open stream: {e}"))?;
Ok(ResolvedTrack::new(reader, hint))
}
}
}
use crate::{
common::types::AnyResult,
protocol::tracks::{LoadResult, Track, TrackInfo},
sources::{SourcePlugin, playable_track::PlayableTrack},
};
use async_trait::async_trait;
use regex::Regex;
use std::sync::{Arc, OnceLock};
use symphonia::core::{
codecs::CODEC_TYPE_NULL,
formats::FormatOptions,
io::MediaSourceStream,
meta::{MetadataOptions, StandardTagKey},
probe::Hint,
};
use tracing::{debug, warn};
pub use track::HttpTrack;
fn url_regex() -> &'static Regex {
static REGEX: OnceLock<Regex> = OnceLock::new();
REGEX.get_or_init(|| Regex::new(r"^(?:https?|icy)://").unwrap())
}
pub struct HttpSource;
impl Default for HttpSource {
fn default() -> Self {
Self::new()
}
}
impl HttpSource {
pub fn new() -> Self {
Self
}
async fn probe_metadata(
url: String,
local_addr: Option<std::net::IpAddr>,
) -> AnyResult<TrackInfo> {
let source = reader::HttpReader::new(&url, local_addr, None).await?;
let mut hint = Hint::new();
if let Some(content_type) = source.content_type() {
hint.mime_type(content_type.as_str());
}
let mss = MediaSourceStream::new(Box::new(source), Default::default());
if let Some(ext) = std::path::Path::new(&url)
.extension()
.and_then(|s| s.to_str())
{
hint.with_extension(ext);
}
let probed = symphonia::default::get_probe().format(
&hint,
mss,
&FormatOptions::default(),
&MetadataOptions::default(),
)?;
let mut format = probed.format;
let track = format
.tracks()
.iter()
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
.ok_or("no audio track found")?;
let duration = if let Some(n_frames) = track.codec_params.n_frames {
if let Some(rate) = track.codec_params.sample_rate {
(n_frames as f64 / rate as f64 * 1000.0) as u64
} else {
0
}
} else {
0
};
let mut title = String::new();
let mut author = String::new();
if let Some(metadata) = format.metadata().current() {
if let Some(tag) = metadata
.tags()
.iter()
.find(|t| t.std_key == Some(StandardTagKey::TrackTitle))
{
title = tag.value.to_string();
}
if let Some(tag) = metadata
.tags()
.iter()
.find(|t| t.std_key == Some(StandardTagKey::Artist))
{
author = tag.value.to_string();
}
}
if title.is_empty() {
title = url
.split('/')
.next_back()
.and_then(|s| s.split('?').next())
.unwrap_or("Unknown Title")
.to_owned();
}
if author.is_empty() {
author = "Unknown Artist".to_owned();
}
Ok(TrackInfo {
identifier: url.clone(),
author,
length: duration,
is_seekable: true,
is_stream: false,
position: 0,
title,
uri: Some(url),
source_name: "http".to_owned(),
artwork_url: None,
isrc: None,
})
}
}
#[async_trait]
impl SourcePlugin for HttpSource {
fn name(&self) -> &str {
"http"
}
fn can_handle(&self, identifier: &str) -> bool {
url_regex().is_match(identifier)
}
async fn load(
&self,
identifier: &str,
routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
) -> LoadResult {
debug!("Probing HTTP source: {identifier}");
let identifier = identifier.to_owned();
let local_addr = routeplanner.as_ref().and_then(|rp| rp.get_address());
match HttpSource::probe_metadata(identifier, local_addr).await {
Ok(info) => LoadResult::Track(Track::new(info)),
Err(e) => {
warn!("Probing failed: {e}");
LoadResult::Empty {}
}
}
}
async fn get_track(
&self,
identifier: &str,
routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
) -> Option<Arc<dyn PlayableTrack>> {
let clean = identifier
.trim()
.trim_start_matches('<')
.trim_end_matches('>');
if self.can_handle(clean) {
Some(Arc::new(HttpTrack {
url: clean.to_owned(),
local_addr: routeplanner.and_then(|rp| rp.get_address()),
proxy: None,
}))
} else {
None
}
}
}