Skip to main content

lavende_core/sources/
manager.rs

1pub mod resolver {
2    use crate::sources::{manager::SourceManager, playable_track::BoxedTrack};
3    use std::sync::Arc;
4    pub async fn resolve_with_mirrors(
5        manager: &SourceManager,
6        track_info: &crate::protocol::tracks::TrackInfo,
7        identifier: &str,
8        mirrors: &crate::config::server::MirrorsConfig,
9        routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
10    ) -> Result<BoxedTrack, String> {
11        if mirrors.best_match.scoring {
12            return super::best_match::resolve_scored(
13                manager,
14                track_info,
15                identifier,
16                mirrors,
17                routeplanner,
18            )
19            .await;
20        }
21        let isrc = track_info.isrc.as_deref().unwrap_or("");
22        let query = format!("{} - {}", track_info.title, track_info.author);
23        let original_source_name = manager
24            .sources
25            .iter()
26            .find(|s| s.can_handle(identifier))
27            .map(|s| s.name());
28        for provider in &mirrors.providers {
29            if isrc.is_empty() && provider.contains("%ISRC%") {
30                tracing::debug!("Skipping mirror provider '{}': track has no ISRC", provider);
31                continue;
32            }
33            let resolved = provider.replace("%ISRC%", isrc).replace("%QUERY%", &query);
34            if let Some(handling_source) = manager.sources.iter().find(|s| s.can_handle(&resolved))
35            {
36                if handling_source.is_mirror() {
37                    tracing::warn!(
38                        "Skipping mirror provider '{}': '{}' is a Mirror-type source",
39                        resolved,
40                        handling_source.name()
41                    );
42                    continue;
43                }
44                if Some(handling_source.name()) == original_source_name {
45                    tracing::debug!(
46                        "Skipping mirror provider '{}': would loop back to '{}'",
47                        resolved,
48                        handling_source.name()
49                    );
50                    continue;
51                }
52            }
53            let res = match manager.load(&resolved, routeplanner.clone()).await {
54                crate::protocol::tracks::LoadResult::Track(t) => {
55                    let id = t.info.uri.as_deref().unwrap_or(&t.info.identifier);
56                    resolve_nested_track(manager, id, routeplanner.clone()).await
57                }
58                crate::protocol::tracks::LoadResult::Search(tracks) => {
59                    if let Some(first) = tracks.first() {
60                        let id = first.info.uri.as_deref().unwrap_or(&first.info.identifier);
61                        resolve_nested_track(manager, id, routeplanner.clone()).await
62                    } else {
63                        None
64                    }
65                }
66                _ => None,
67            };
68            if let Some(track) = res {
69                return Ok(track);
70            }
71        }
72        tracing::warn!(
73            "[Mirror] no valid mirror found for track: {} - {}",
74            track_info.title,
75            track_info.author
76        );
77        Err(format!(
78            "No mirror found for track: {} - {}",
79            track_info.title, track_info.author
80        ))
81    }
82    pub async fn resolve_nested_track(
83        manager: &SourceManager,
84        identifier: &str,
85        routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
86    ) -> Option<BoxedTrack> {
87        for source in &manager.sources {
88            if source.can_handle(identifier) {
89                if let Some(track) = source.get_track(identifier, routeplanner.clone()).await {
90                    return Some(track);
91                }
92                if source.name() != "http" {
93                    return None;
94                }
95            }
96        }
97        None
98    }
99}
100pub mod best_match {
101    use crate::sources::{manager::SourceManager, playable_track::BoxedTrack};
102    use futures::stream::{FuturesOrdered, FuturesUnordered, StreamExt};
103    use std::sync::Arc;
104    pub struct MirrorResult {
105        pub track: BoxedTrack,
106        pub score: f64,
107        pub provider: String,
108    }
109    fn normalize(s: &str) -> String {
110        let lower = s.to_lowercase();
111        let mut stripped = String::with_capacity(lower.len());
112        let mut depth: usize = 0;
113        for ch in lower.chars() {
114            match ch {
115                '(' | '[' => depth += 1,
116                ')' | ']' => depth = depth.saturating_sub(1),
117                _ if depth == 0 => stripped.push(ch),
118                _ => {}
119            }
120        }
121        let stripped = stripped
122            .replace("feat.", " ")
123            .replace("feat ", " ")
124            .replace("ft.", " ")
125            .replace("ft ", " ");
126        let clean: String = stripped
127            .chars()
128            .map(|c| {
129                if c.is_alphanumeric() || c == ' ' {
130                    c
131                } else {
132                    ' '
133                }
134            })
135            .collect();
136        clean.split_whitespace().collect::<Vec<_>>().join(" ")
137    }
138    fn levenshtein(a: &str, b: &str) -> usize {
139        let a: Vec<char> = a.chars().collect();
140        let b: Vec<char> = b.chars().collect();
141        let (m, n) = (a.len(), b.len());
142        let mut prev: Vec<usize> = (0..=n).collect();
143        let mut curr = vec![0usize; n + 1];
144        for i in 1..=m {
145            curr[0] = i;
146            for j in 1..=n {
147                let cost = usize::from(a[i - 1] != b[j - 1]);
148                curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
149            }
150            std::mem::swap(&mut prev, &mut curr);
151        }
152        prev[n]
153    }
154    fn string_similarity(a: &str, b: &str) -> f64 {
155        if a == b {
156            return 1.0;
157        }
158        if a.is_empty() || b.is_empty() {
159            return 0.0;
160        }
161        let na = normalize(a);
162        let nb = normalize(b);
163        if na == nb {
164            return 1.0;
165        }
166        if na.contains(&nb) || nb.contains(&na) {
167            let shorter = na.len().min(nb.len()) as f64;
168            let longer = na.len().max(nb.len()) as f64;
169            return 0.80 + (shorter / longer) * 0.15;
170        }
171        let max_len = na.len().max(nb.len());
172        if max_len == 0 {
173            return 1.0;
174        }
175        1.0 - levenshtein(&na, &nb) as f64 / max_len as f64
176    }
177    fn duration_similarity(d1: u64, d2: u64, tolerance_ms: u64) -> f64 {
178        if d1 == 0 || d2 == 0 {
179            return 0.5;
180        }
181        let diff = d1.abs_diff(d2);
182        if diff <= tolerance_ms {
183            1.0
184        } else {
185            (1.0 - diff as f64 / d1.max(d2) as f64).max(0.0)
186        }
187    }
188    fn score_match(
189        orig_title: &str,
190        orig_author: &str,
191        orig_length: u64,
192        cand_title: &str,
193        cand_author: &str,
194        cand_length: u64,
195        cfg: &crate::config::server::BestMatchConfig,
196    ) -> f64 {
197        let nt = normalize(orig_title);
198        let nc = normalize(cand_title);
199        let title_score = if nt == nc {
200            1.0
201        } else if nc.starts_with(&nt) {
202            0.95
203        } else if nc.contains(&nt) || nt.contains(&nc) {
204            let shorter = nt.len().min(nc.len()) as f64;
205            let longer = nt.len().max(nc.len()) as f64;
206            0.82 + (shorter / longer) * 0.10
207        } else {
208            string_similarity(&nt, &nc)
209        };
210        title_score * cfg.weight_title
211            + string_similarity(orig_author, cand_author) * cfg.weight_artist
212            + duration_similarity(orig_length, cand_length, cfg.duration_tolerance_ms)
213                * cfg.weight_duration
214    }
215    fn fmt_ms(ms: u64) -> String {
216        let s = ms / 1_000;
217        format!("{}:{:02}", s / 60, s % 60)
218    }
219    pub async fn resolve_scored(
220        manager: &SourceManager,
221        track_info: &crate::protocol::tracks::TrackInfo,
222        identifier: &str,
223        mirrors: &crate::config::server::MirrorsConfig,
224        routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
225    ) -> Result<BoxedTrack, String> {
226        let isrc = track_info.isrc.as_deref().unwrap_or("");
227        let query = format!("{} {}", track_info.title, track_info.author);
228        let cfg = &mirrors.best_match;
229        let original_source_name = manager
230            .sources
231            .iter()
232            .find(|s| s.can_handle(identifier))
233            .map(|s| s.name().to_string());
234        let mut isrc_providers: Vec<String> = Vec::new();
235        let mut free_providers: Vec<String> = Vec::new();
236        let mut throttled_providers: Vec<String> = Vec::new();
237        for provider in &mirrors.providers {
238            let is_isrc_provider = provider.contains("%ISRC%");
239            if is_isrc_provider && isrc.is_empty() {
240                tracing::debug!("Skipping mirror provider '{}': track has no ISRC", provider);
241                continue;
242            }
243            let resolved = provider.replace("%ISRC%", isrc).replace("%QUERY%", &query);
244            if let Some(src) = manager.sources.iter().find(|s| s.can_handle(&resolved)) {
245                if src.is_mirror() {
246                    tracing::warn!(
247                        "Skipping mirror provider '{}': '{}' is a Mirror-type source",
248                        resolved,
249                        src.name()
250                    );
251                    continue;
252                }
253                if Some(src.name().to_string()) == original_source_name {
254                    tracing::debug!(
255                        "Skipping mirror provider '{}': would loop back to '{}'",
256                        resolved,
257                        src.name()
258                    );
259                    continue;
260                }
261            }
262            if is_isrc_provider {
263                isrc_providers.push(resolved);
264            } else if cfg
265                .throttled_prefixes
266                .iter()
267                .any(|p| resolved.starts_with(p.as_str()))
268            {
269                throttled_providers.push(resolved);
270            } else {
271                free_providers.push(resolved);
272            }
273        }
274        if !isrc_providers.is_empty() {
275            let mut futs: FuturesOrdered<_> = isrc_providers
276                .iter()
277                .map(|p| search_provider(manager, track_info, p, routeplanner.clone(), cfg, true))
278                .collect();
279            while let Some(result) = futs.next().await {
280                if let Some(mr) = result {
281                    tracing::info!(
282                        "[Mirror] ISRC match \"{}\" | {} | {} => {} | score: {:.3}",
283                        track_info.title,
284                        track_info.author,
285                        fmt_ms(track_info.length),
286                        mr.provider,
287                        mr.score,
288                    );
289                    return Ok(mr.track);
290                }
291            }
292        }
293        let mut global_best: Option<MirrorResult> = None;
294        if !free_providers.is_empty() {
295            let mut futs: FuturesUnordered<_> = free_providers
296                .iter()
297                .map(|p| search_provider(manager, track_info, p, routeplanner.clone(), cfg, false))
298                .collect();
299            while let Some(result) = futs.next().await {
300                if let Some(mr) = result {
301                    tracing::info!(
302                        "[Mirror] \"{}\" | {} | {} => {} | score: {:.3}",
303                        track_info.title,
304                        track_info.author,
305                        fmt_ms(track_info.length),
306                        mr.provider,
307                        mr.score,
308                    );
309                    if mr.score >= cfg.immediate_use {
310                        return Ok(mr.track);
311                    }
312                    if global_best.as_ref().is_none_or(|b| mr.score > b.score) {
313                        global_best = Some(mr);
314                    }
315                }
316            }
317        }
318        for provider in &throttled_providers {
319            if let Some(mr) = search_provider(
320                manager,
321                track_info,
322                provider,
323                routeplanner.clone(),
324                cfg,
325                true,
326            )
327            .await
328            {
329                tracing::info!(
330                    "[Mirror] throttled match \"{}\" via {} (score {:.3})",
331                    track_info.title,
332                    mr.provider,
333                    mr.score
334                );
335                return Ok(mr.track);
336            }
337        }
338        if let Some(best) = global_best {
339            tracing::info!(
340                "[Mirror] fallback match \"{}\" via {} (score {:.3})",
341                track_info.title,
342                best.provider,
343                best.score
344            );
345            return Ok(best.track);
346        }
347        tracing::warn!(
348            "[Mirror] no valid mirror found for \"{}\" | {}",
349            track_info.title,
350            track_info.author
351        );
352        Err(format!(
353            "No mirror found for track: {} - {}",
354            track_info.title, track_info.author
355        ))
356    }
357    async fn search_provider(
358        manager: &SourceManager,
359        original: &crate::protocol::tracks::TrackInfo,
360        resolved_provider: &str,
361        routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
362        cfg: &crate::config::server::BestMatchConfig,
363        trust_any: bool,
364    ) -> Option<MirrorResult> {
365        use crate::protocol::tracks::LoadResult;
366        let candidates: Vec<crate::protocol::tracks::TrackInfo> =
367            match manager.load(resolved_provider, routeplanner.clone()).await {
368                LoadResult::Track(t) => vec![t.info],
369                LoadResult::Search(tracks) => tracks.into_iter().take(10).map(|t| t.info).collect(),
370                _ => return None,
371            };
372        if candidates.is_empty() {
373            return None;
374        }
375        let mut scored: Vec<(f64, crate::protocol::tracks::TrackInfo)> = candidates
376            .into_iter()
377            .map(|info| {
378                let s = score_match(
379                    &original.title,
380                    &original.author,
381                    original.length,
382                    &info.title,
383                    &info.author,
384                    info.length,
385                    cfg,
386                );
387                (s, info)
388            })
389            .collect();
390        scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
391        let top_score = scored[0].0;
392        let (limit, threshold): (usize, f64) = if trust_any {
393            (scored.len(), 0.0)
394        } else if top_score >= cfg.immediate_use {
395            (1, cfg.immediate_use)
396        } else if top_score >= cfg.high_confidence {
397            (2, cfg.high_confidence)
398        } else {
399            (3, cfg.min_similarity)
400        };
401        for (score, info) in scored.into_iter().take(limit) {
402            if score < threshold {
403                break;
404            }
405            let id = info.uri.as_deref().unwrap_or(&info.identifier);
406            if let Some(track) =
407                super::resolver::resolve_nested_track(manager, id, routeplanner.clone()).await
408            {
409                return Some(MirrorResult {
410                    track,
411                    score,
412                    provider: resolved_provider.to_string(),
413                });
414            }
415        }
416        None
417    }
418}
419pub mod registration {
420    use crate::{
421        common::HttpClientPool,
422        sources::{
423            amazonmusic::AmazonMusicSource,
424            anghami::AnghamiSource,
425            applemusic::AppleMusicSource,
426            audiomack::AudiomackSource,
427            audius::AudiusSource,
428            bandcamp::BandcampSource,
429            deezer::DeezerSource,
430            flowery::FlowerySource,
431            gaana::GaanaSource,
432            google_tts::GoogleTtsSource,
433            http::HttpSource,
434            jiosaavn::JioSaavnSource,
435            lastfm::LastFMSource,
436            local::LocalSource,
437            mixcloud::MixcloudSource,
438            netease::NeteaseSource,
439            pandora::PandoraSource,
440            playable_track::BoxedSource,
441            qobuz::QobuzSource,
442            reddit::RedditSource,
443            shazam::ShazamSource,
444            soundcloud::SoundCloudSource,
445            spotify::SpotifySource,
446            tidal::TidalSource,
447            twitch::TwitchSource,
448            vkmusic::VkMusicSource,
449            yandexmusic::YandexMusicSource,
450            youtube::{YouTubeSource, YoutubeStreamContext, cipher::YouTubeCipherManager},
451        },
452    };
453    use std::sync::Arc;
454    pub fn register_all(
455        sources: &mut Vec<BoxedSource>,
456        config: &crate::config::AppConfig,
457        http_pool: &Arc<HttpClientPool>,
458    ) -> (
459        Option<Arc<YouTubeCipherManager>>,
460        Option<Arc<YoutubeStreamContext>>,
461    ) {
462        let yt_ctx = register_core_sources(sources, config, http_pool);
463        register_extra_sources(sources, config);
464        yt_ctx
465    }
466    fn register_core_sources(
467        sources: &mut Vec<BoxedSource>,
468        config: &crate::config::AppConfig,
469        http_pool: &Arc<HttpClientPool>,
470    ) -> (
471        Option<Arc<YouTubeCipherManager>>,
472        Option<Arc<YoutubeStreamContext>>,
473    ) {
474        let mut yt_ctx = (None, None);
475        macro_rules! register {
476            ($enabled:expr, $name:literal, $proxy:expr, $ctor:expr) => {
477                if $enabled {
478                    if let Some(p) = &$proxy {
479                        tracing::info!(
480                            "Loading {} with proxy: {}",
481                            $name,
482                            p.url.as_ref().unwrap_or(&"enabled".to_owned())
483                        );
484                    }
485                    match $ctor {
486                        Ok(src) => {
487                            tracing::info!("Loaded source: {}", $name);
488                            sources.push(Box::new(src));
489                        }
490                        Err(e) => {
491                            tracing::error!("{} source failed to initialize: {}", $name, e);
492                        }
493                    }
494                }
495            };
496        }
497        if config.sources.youtube.as_ref().is_some_and(|c| c.enabled) {
498            tracing::info!("Loaded source: YouTube");
499            let yt_client = http_pool.get(None);
500            let yt = YouTubeSource::new(config.sources.youtube.clone(), yt_client);
501            yt_ctx = (Some(yt.cipher_manager()), Some(yt.stream_context()));
502            sources.push(Box::new(yt));
503        }
504        let soundcloud_proxy = config
505            .sources
506            .soundcloud
507            .as_ref()
508            .and_then(|c| c.proxy.clone());
509        register!(
510            config
511                .sources
512                .soundcloud
513                .as_ref()
514                .is_some_and(|c| c.enabled),
515            "SoundCloud",
516            soundcloud_proxy,
517            SoundCloudSource::new(
518                config.sources.soundcloud.clone().unwrap(),
519                http_pool.get(soundcloud_proxy.clone())
520            )
521        );
522        register!(
523            config.sources.spotify.as_ref().is_some_and(|c| c.enabled),
524            "Spotify",
525            None::<crate::config::HttpProxyConfig>,
526            SpotifySource::new(config.sources.spotify.clone(), http_pool.get(None))
527        );
528        let jiosaavn_proxy = config
529            .sources
530            .jiosaavn
531            .as_ref()
532            .and_then(|c| c.proxy.clone());
533        register!(
534            config.sources.jiosaavn.as_ref().is_some_and(|c| c.enabled),
535            "JioSaavn",
536            jiosaavn_proxy,
537            JioSaavnSource::new(
538                config.sources.jiosaavn.clone(),
539                http_pool.get(jiosaavn_proxy.clone())
540            )
541        );
542        register_deezer(sources, config, http_pool);
543        let apple_proxy = config
544            .sources
545            .applemusic
546            .as_ref()
547            .and_then(|c| c.proxy.clone());
548        register!(
549            config
550                .sources
551                .applemusic
552                .as_ref()
553                .is_some_and(|c| c.enabled),
554            "Apple Music",
555            apple_proxy,
556            AppleMusicSource::new(
557                config.sources.applemusic.clone(),
558                http_pool.get(apple_proxy.clone())
559            )
560        );
561        let gaana_proxy = config.sources.gaana.as_ref().and_then(|c| c.proxy.clone());
562        register!(
563            config.sources.gaana.as_ref().is_some_and(|c| c.enabled),
564            "Gaana",
565            gaana_proxy,
566            GaanaSource::new(
567                config.sources.gaana.clone(),
568                http_pool.get(gaana_proxy.clone())
569            )
570        );
571        let tidal_proxy = config.sources.tidal.as_ref().and_then(|c| c.proxy.clone());
572        register!(
573            config.sources.tidal.as_ref().is_some_and(|c| c.enabled),
574            "Tidal",
575            tidal_proxy,
576            TidalSource::new(
577                config.sources.tidal.clone(),
578                http_pool.get(tidal_proxy.clone())
579            )
580        );
581        let audiomack_proxy = config
582            .sources
583            .audiomack
584            .as_ref()
585            .and_then(|c| c.proxy.clone());
586        register!(
587            config.sources.audiomack.as_ref().is_some_and(|c| c.enabled),
588            "Audiomack",
589            audiomack_proxy,
590            AudiomackSource::new(
591                config.sources.audiomack.clone(),
592                http_pool.get(audiomack_proxy.clone())
593            )
594        );
595        let pandora_proxy = config
596            .sources
597            .pandora
598            .as_ref()
599            .and_then(|c| c.proxy.clone());
600        register!(
601            config.sources.pandora.as_ref().is_some_and(|c| c.enabled),
602            "Pandora",
603            pandora_proxy,
604            PandoraSource::new(
605                config.sources.pandora.clone(),
606                http_pool.get(pandora_proxy.clone())
607            )
608        );
609        let qobuz_proxy = config.sources.qobuz.as_ref().and_then(|c| c.proxy.clone());
610        if config.sources.qobuz.as_ref().is_some_and(|c| c.enabled) {
611            let token_provided = config
612                .sources
613                .qobuz
614                .as_ref()
615                .and_then(|c| c.user_token.as_ref())
616                .is_some_and(|t| !t.is_empty());
617            if !token_provided {
618                tracing::warn!(
619                    "Qobuz user_token is missing; all playback will fall back to mirrors."
620                );
621            }
622            register!(
623                true,
624                "Qobuz",
625                qobuz_proxy,
626                QobuzSource::new(config, http_pool.get(qobuz_proxy.clone()))
627            );
628        }
629        let anghami_proxy = config
630            .sources
631            .anghami
632            .as_ref()
633            .and_then(|c| c.proxy.clone());
634        register!(
635            config.sources.anghami.as_ref().is_some_and(|c| c.enabled),
636            "Anghami",
637            anghami_proxy,
638            AnghamiSource::new(config, http_pool.get(anghami_proxy.clone()))
639        );
640        let shazam_proxy = config.sources.shazam.as_ref().and_then(|c| c.proxy.clone());
641        register!(
642            config.sources.shazam.as_ref().is_some_and(|c| c.enabled),
643            "Shazam",
644            shazam_proxy,
645            ShazamSource::new(config, http_pool.get(shazam_proxy.clone()))
646        );
647        let mixcloud_proxy = config
648            .sources
649            .mixcloud
650            .as_ref()
651            .and_then(|c| c.proxy.clone());
652        register!(
653            config.sources.mixcloud.as_ref().is_some_and(|c| c.enabled),
654            "Mixcloud",
655            mixcloud_proxy,
656            MixcloudSource::new(
657                config.sources.mixcloud.clone(),
658                http_pool.get(mixcloud_proxy.clone())
659            )
660        );
661        let bandcamp_proxy = config
662            .sources
663            .bandcamp
664            .as_ref()
665            .and_then(|c| c.proxy.clone());
666        register!(
667            config.sources.bandcamp.as_ref().is_some_and(|c| c.enabled),
668            "Bandcamp",
669            bandcamp_proxy,
670            BandcampSource::new(
671                config.sources.bandcamp.clone(),
672                http_pool.get(bandcamp_proxy.clone())
673            )
674        );
675        let reddit_proxy = config.sources.reddit.as_ref().and_then(|c| c.proxy.clone());
676        register!(
677            config.sources.reddit.as_ref().is_some_and(|c| c.enabled),
678            "Reddit",
679            reddit_proxy,
680            RedditSource::new(
681                config.sources.reddit.clone(),
682                http_pool.get(reddit_proxy.clone())
683            )
684        );
685        register!(
686            config.sources.lastfm.as_ref().is_some_and(|c| c.enabled),
687            "Last.fm",
688            None::<crate::config::HttpProxyConfig>,
689            LastFMSource::new(config.sources.lastfm.clone(), http_pool.get(None))
690        );
691        let audius_proxy = config.sources.audius.as_ref().and_then(|c| c.proxy.clone());
692        register!(
693            config.sources.audius.as_ref().is_some_and(|c| c.enabled),
694            "Audius",
695            audius_proxy,
696            AudiusSource::new(
697                config.sources.audius.clone(),
698                http_pool.get(audius_proxy.clone())
699            )
700        );
701        register_yandex(sources, config, http_pool);
702        register_vkmusic(sources, config, http_pool);
703        register_netease(sources, config, http_pool);
704        register_twitch(sources, config, http_pool);
705        register_amazonmusic(sources, config, http_pool);
706        if config.sources.http.as_ref().is_some_and(|c| c.enabled) {
707            tracing::info!("Loaded source: http");
708            sources.push(Box::new(HttpSource::new()));
709        }
710        yt_ctx
711    }
712    fn register_deezer(
713        sources: &mut Vec<BoxedSource>,
714        config: &crate::config::AppConfig,
715        http_pool: &Arc<HttpClientPool>,
716    ) {
717        let (token_provided, key_provided) = if let Some(c) = config.sources.deezer.as_ref() {
718            let arls_provided = c
719                .arls
720                .as_ref()
721                .is_some_and(|a| !a.is_empty() && a.iter().any(|s| !s.is_empty()));
722            let key_provided = c
723                .master_decryption_key
724                .as_ref()
725                .is_some_and(|k| !k.is_empty());
726            (arls_provided, key_provided)
727        } else {
728            (false, false)
729        };
730        if config.sources.deezer.as_ref().is_some_and(|c| c.enabled) {
731            if !token_provided || !key_provided {
732                let mut missing = Vec::new();
733                if !token_provided {
734                    missing.push("arls");
735                }
736                if !key_provided {
737                    missing.push("master_decryption_key");
738                }
739                tracing::warn!(
740                    "Deezer source is enabled but {} {} missing; it will be disabled.",
741                    missing.join(" and "),
742                    if missing.len() > 1 { "are" } else { "is" }
743                );
744            } else {
745                let proxy = config.sources.deezer.as_ref().and_then(|c| c.proxy.clone());
746                let source = DeezerSource::new(
747                    config.sources.deezer.clone().unwrap(),
748                    http_pool.get(proxy.clone()),
749                );
750                match source {
751                    Ok(src) => {
752                        tracing::info!("Loaded source: Deezer");
753                        sources.push(Box::new(src));
754                    }
755                    Err(e) => {
756                        tracing::error!("Deezer source failed to initialize: {}", e);
757                    }
758                }
759            }
760        }
761    }
762    fn register_yandex(
763        sources: &mut Vec<BoxedSource>,
764        config: &crate::config::AppConfig,
765        http_pool: &Arc<HttpClientPool>,
766    ) {
767        if let Some(c) = config.sources.yandexmusic.as_ref()
768            && c.enabled
769        {
770            if c.access_token.is_none() {
771                tracing::warn!(
772                    "Yandex Music source is enabled but the access_token is missing; it will be disabled."
773                );
774            } else {
775                let proxy = c.proxy.clone();
776                let source = YandexMusicSource::new(
777                    config.sources.yandexmusic.clone(),
778                    http_pool.get(proxy.clone()),
779                );
780                match source {
781                    Ok(src) => {
782                        tracing::info!("Loaded source: Yandex Music");
783                        sources.push(Box::new(src));
784                    }
785                    Err(e) => {
786                        tracing::error!("Yandex Music source failed to initialize: {}", e);
787                    }
788                }
789            }
790        }
791    }
792    fn register_vkmusic(
793        sources: &mut Vec<BoxedSource>,
794        config: &crate::config::AppConfig,
795        http_pool: &Arc<HttpClientPool>,
796    ) {
797        if let Some(c) = config.sources.vkmusic.as_ref()
798            && c.enabled
799        {
800            if c.user_token.is_none() && c.user_cookie.is_none() {
801                tracing::warn!(
802                    "VK Music source is enabled but neither user_token nor user_cookie is set; API calls will fail."
803                );
804            }
805            let proxy = c.proxy.clone();
806            match VkMusicSource::new(config.sources.vkmusic.clone(), http_pool.get(proxy.clone())) {
807                Ok(src) => {
808                    tracing::info!("Loaded source: VK Music");
809                    sources.push(Box::new(src));
810                }
811                Err(e) => {
812                    tracing::error!("VK Music source failed to initialize: {}", e);
813                }
814            }
815        }
816    }
817    fn register_netease(
818        sources: &mut Vec<BoxedSource>,
819        config: &crate::config::AppConfig,
820        http_pool: &Arc<HttpClientPool>,
821    ) {
822        if let Some(c) = config.sources.netease.as_ref()
823            && c.enabled
824        {
825            let proxy = c.proxy.clone();
826            match NeteaseSource::new(config.sources.netease.clone(), http_pool.get(proxy.clone())) {
827                Ok(src) => {
828                    tracing::info!("Loaded source: Netease Music");
829                    sources.push(Box::new(src));
830                }
831                Err(e) => {
832                    tracing::error!("Netease Music source failed to initialize: {}", e);
833                }
834            }
835        }
836    }
837    fn register_twitch(
838        sources: &mut Vec<BoxedSource>,
839        config: &crate::config::AppConfig,
840        http_pool: &Arc<HttpClientPool>,
841    ) {
842        if let Some(c) = config.sources.twitch.as_ref()
843            && c.enabled
844        {
845            let proxy = c.proxy.clone();
846            tracing::info!("Loaded source: Twitch");
847            sources.push(Box::new(TwitchSource::new(c.clone(), http_pool.get(proxy))));
848        }
849    }
850    fn register_amazonmusic(
851        sources: &mut Vec<BoxedSource>,
852        config: &crate::config::AppConfig,
853        http_pool: &Arc<HttpClientPool>,
854    ) {
855        if let Some(c) = config.sources.amazonmusic.as_ref()
856            && c.enabled
857        {
858            let proxy = c.proxy.clone();
859            match AmazonMusicSource::new(c.clone(), http_pool.get(proxy)) {
860                Ok(src) => {
861                    tracing::info!("Loaded source: Amazon Music");
862                    sources.push(Box::new(src));
863                }
864                Err(e) => {
865                    tracing::error!("Amazon Music source failed to initialize: {}", e);
866                }
867            }
868        }
869    }
870    fn register_extra_sources(sources: &mut Vec<BoxedSource>, config: &crate::config::AppConfig) {
871        if let Some(c) = config.sources.google_tts.as_ref()
872            && c.enabled
873        {
874            tracing::info!("Loaded source: Google TTS");
875            sources.push(Box::new(GoogleTtsSource::new(c.clone())));
876        }
877        if let Some(c) = config.sources.flowery.as_ref()
878            && c.enabled
879        {
880            tracing::info!("Loaded source: Flowery");
881            sources.push(Box::new(FlowerySource::new(c.clone())));
882        }
883        if config.sources.local.as_ref().is_some_and(|c| c.enabled) {
884            tracing::info!("Loaded source: local");
885            sources.push(Box::new(LocalSource::new()));
886        }
887    }
888}
889use crate::{
890    common::HttpClientPool,
891    sources::playable_track::{BoxedSource, BoxedTrack},
892};
893use std::sync::Arc;
894pub struct SourceManager {
895    pub sources: Vec<BoxedSource>,
896    pub mirrors: crate::config::server::MirrorsConfig,
897    pub youtube_cipher_manager: Option<Arc<crate::sources::youtube::cipher::YouTubeCipherManager>>,
898    pub youtube_stream_ctx: Option<Arc<crate::sources::youtube::YoutubeStreamContext>>,
899    pub http_pool: Arc<HttpClientPool>,
900    pub player_config: crate::config::player::PlayerConfig,
901}
902impl SourceManager {
903    pub fn new(config: &crate::config::AppConfig) -> Self {
904        let http_pool = Arc::new(HttpClientPool::new());
905        let mut sources = Vec::new();
906        let (youtube_cipher_manager, youtube_stream_ctx) =
907            registration::register_all(&mut sources, config, &http_pool);
908        Self {
909            sources,
910            mirrors: config.player.mirrors.clone(),
911            youtube_cipher_manager,
912            youtube_stream_ctx,
913            http_pool,
914            player_config: config.player.clone(),
915        }
916    }
917    pub async fn load(
918        &self,
919        identifier: &str,
920        routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
921    ) -> crate::protocol::tracks::LoadResult {
922        for source in &self.sources {
923            if source.can_handle(identifier) {
924                tracing::debug!(
925                    "SourceManager: Loading '{}' with source: {}",
926                    identifier,
927                    source.name()
928                );
929                return source.load(identifier, routeplanner.clone()).await;
930            }
931        }
932        tracing::debug!(
933            "SourceManager: No source matched identifier: '{}'",
934            identifier
935        );
936        crate::protocol::tracks::LoadResult::Empty {}
937    }
938    pub async fn load_search(
939        &self,
940        query: &str,
941        types: &[String],
942        routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
943    ) -> Option<crate::protocol::tracks::SearchResult> {
944        for source in &self.sources {
945            if source.can_handle(query) {
946                tracing::trace!("Loading search '{}' with source: {}", query, source.name());
947                return source.load_search(query, types, routeplanner.clone()).await;
948            }
949        }
950        tracing::debug!("No source could handle search query: {}", query);
951        None
952    }
953    pub async fn resolve_track(
954        &self,
955        track_info: &crate::protocol::tracks::TrackInfo,
956        routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
957    ) -> Result<BoxedTrack, String> {
958        let identifier = track_info.uri.as_deref().unwrap_or(&track_info.identifier);
959        for source in &self.sources {
960            if source.can_handle(identifier) {
961                tracing::trace!(
962                    "Resolving playable track for '{}' with source: {}",
963                    identifier,
964                    source.name()
965                );
966                if let Some(track) = source.get_track(identifier, routeplanner.clone()).await {
967                    return Ok(track);
968                }
969                break;
970            }
971        }
972        resolver::resolve_with_mirrors(self, track_info, identifier, &self.mirrors, routeplanner)
973            .await
974    }
975    pub fn source_names(&self) -> Vec<String> {
976        self.sources.iter().map(|s| s.name().to_string()).collect()
977    }
978    pub fn get_proxy_config(&self, source_name: &str) -> Option<crate::config::HttpProxyConfig> {
979        self.sources
980            .iter()
981            .find(|s| s.name() == source_name)
982            .and_then(|s| s.get_proxy_config())
983    }
984}