Skip to main content

lavende_core/sources/
http.rs

1pub 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}