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}