lavende_core/sources/
http.rs1pub mod reader {
2 use crate::{
3 audio::source::{AudioSource, HttpSource, create_client},
4 common::types::AnyResult,
5 };
6 use std::io::{Read, Seek, SeekFrom};
7 use symphonia::core::io::MediaSource;
8 pub struct HttpReader {
9 inner: HttpSource,
10 }
11 impl HttpReader {
12 pub async fn new(
13 url: &str,
14 local_addr: Option<std::net::IpAddr>,
15 proxy: Option<crate::config::HttpProxyConfig>,
16 ) -> AnyResult<Self> {
17 let user_agent = crate::common::utils::default_user_agent();
18 let client = create_client(user_agent, local_addr, proxy, None)?;
19 let inner = HttpSource::new(client, url).await?;
20 Ok(Self { inner })
21 }
22 }
23 impl Read for HttpReader {
24 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
25 self.inner.read(buf)
26 }
27 }
28 impl Seek for HttpReader {
29 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
30 self.inner.seek(pos)
31 }
32 }
33 impl MediaSource for HttpReader {
34 fn is_seekable(&self) -> bool {
35 self.inner.is_seekable()
36 }
37 fn byte_len(&self) -> Option<u64> {
38 self.inner.byte_len()
39 }
40 }
41 impl HttpReader {
42 pub fn content_type(&self) -> Option<String> {
43 self.inner.content_type()
44 }
45 }
46}
47pub mod track {
48 use crate::{
49 common::types::AudioFormat,
50 config::HttpProxyConfig,
51 sources::{
52 http::reader,
53 playable_track::{PlayableTrack, ResolvedTrack},
54 },
55 };
56 use async_trait::async_trait;
57 pub struct HttpTrack {
58 pub url: String,
59 pub local_addr: Option<std::net::IpAddr>,
60 pub proxy: Option<HttpProxyConfig>,
61 }
62 #[async_trait]
63 impl PlayableTrack for HttpTrack {
64 async fn resolve(&self) -> Result<ResolvedTrack, String> {
65 let hint = std::path::Path::new(&self.url)
66 .extension()
67 .and_then(|s| s.to_str())
68 .map(AudioFormat::from_ext)
69 .filter(|f| *f != AudioFormat::Unknown);
70 let reader = reader::HttpReader::new(&self.url, self.local_addr, self.proxy.clone())
71 .await
72 .map(|r| Box::new(r) as Box<dyn symphonia::core::io::MediaSource>)
73 .map_err(|e| format!("Failed to open stream: {e}"))?;
74 Ok(ResolvedTrack::new(reader, hint))
75 }
76 }
77}
78use crate::{
79 common::types::AnyResult,
80 protocol::tracks::{LoadResult, Track, TrackInfo},
81 sources::{SourcePlugin, playable_track::PlayableTrack},
82};
83use async_trait::async_trait;
84use regex::Regex;
85use std::sync::{Arc, OnceLock};
86use symphonia::core::{
87 codecs::CODEC_TYPE_NULL,
88 formats::FormatOptions,
89 io::MediaSourceStream,
90 meta::{MetadataOptions, StandardTagKey},
91 probe::Hint,
92};
93use tracing::{debug, warn};
94pub use track::HttpTrack;
95fn url_regex() -> &'static Regex {
96 static REGEX: OnceLock<Regex> = OnceLock::new();
97 REGEX.get_or_init(|| Regex::new(r"^(?:https?|icy)://").unwrap())
98}
99pub struct HttpSource;
100impl Default for HttpSource {
101 fn default() -> Self {
102 Self::new()
103 }
104}
105impl HttpSource {
106 pub fn new() -> Self {
107 Self
108 }
109 async fn probe_metadata(
110 url: String,
111 local_addr: Option<std::net::IpAddr>,
112 ) -> AnyResult<TrackInfo> {
113 let source = reader::HttpReader::new(&url, local_addr, None).await?;
114 let mut hint = Hint::new();
115 if let Some(content_type) = source.content_type() {
116 hint.mime_type(content_type.as_str());
117 }
118 let mss = MediaSourceStream::new(Box::new(source), Default::default());
119 if let Some(ext) = std::path::Path::new(&url)
120 .extension()
121 .and_then(|s| s.to_str())
122 {
123 hint.with_extension(ext);
124 }
125 let probed = symphonia::default::get_probe().format(
126 &hint,
127 mss,
128 &FormatOptions::default(),
129 &MetadataOptions::default(),
130 )?;
131 let mut format = probed.format;
132 let track = format
133 .tracks()
134 .iter()
135 .find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
136 .ok_or("no audio track found")?;
137 let duration = if let Some(n_frames) = track.codec_params.n_frames {
138 if let Some(rate) = track.codec_params.sample_rate {
139 (n_frames as f64 / rate as f64 * 1000.0) as u64
140 } else {
141 0
142 }
143 } else {
144 0
145 };
146 let mut title = String::new();
147 let mut author = String::new();
148 if let Some(metadata) = format.metadata().current() {
149 if let Some(tag) = metadata
150 .tags()
151 .iter()
152 .find(|t| t.std_key == Some(StandardTagKey::TrackTitle))
153 {
154 title = tag.value.to_string();
155 }
156 if let Some(tag) = metadata
157 .tags()
158 .iter()
159 .find(|t| t.std_key == Some(StandardTagKey::Artist))
160 {
161 author = tag.value.to_string();
162 }
163 }
164 if title.is_empty() {
165 title = url
166 .split('/')
167 .next_back()
168 .and_then(|s| s.split('?').next())
169 .unwrap_or("Unknown Title")
170 .to_owned();
171 }
172 if author.is_empty() {
173 author = "Unknown Artist".to_owned();
174 }
175 Ok(TrackInfo {
176 identifier: url.clone(),
177 author,
178 length: duration,
179 is_seekable: true,
180 is_stream: false,
181 position: 0,
182 title,
183 uri: Some(url),
184 source_name: "http".to_owned(),
185 artwork_url: None,
186 isrc: None,
187 })
188 }
189}
190#[async_trait]
191impl SourcePlugin for HttpSource {
192 fn name(&self) -> &str {
193 "http"
194 }
195 fn can_handle(&self, identifier: &str) -> bool {
196 url_regex().is_match(identifier)
197 }
198 async fn load(
199 &self,
200 identifier: &str,
201 routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
202 ) -> LoadResult {
203 debug!("Probing HTTP source: {identifier}");
204 let identifier = identifier.to_owned();
205 let local_addr = routeplanner.as_ref().and_then(|rp| rp.get_address());
206 match HttpSource::probe_metadata(identifier, local_addr).await {
207 Ok(info) => LoadResult::Track(Track::new(info)),
208 Err(e) => {
209 warn!("Probing failed: {e}");
210 LoadResult::Empty {}
211 }
212 }
213 }
214 async fn get_track(
215 &self,
216 identifier: &str,
217 routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
218 ) -> Option<Arc<dyn PlayableTrack>> {
219 let clean = identifier
220 .trim()
221 .trim_start_matches('<')
222 .trim_end_matches('>');
223 if self.can_handle(clean) {
224 Some(Arc::new(HttpTrack {
225 url: clean.to_owned(),
226 local_addr: routeplanner.and_then(|rp| rp.get_address()),
227 proxy: None,
228 }))
229 } else {
230 None
231 }
232 }
233}