pub mod resolver {
use crate::sources::{manager::SourceManager, playable_track::BoxedTrack};
use std::sync::Arc;
pub async fn resolve_with_mirrors(
manager: &SourceManager,
track_info: &crate::protocol::tracks::TrackInfo,
identifier: &str,
mirrors: &crate::config::server::MirrorsConfig,
routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
) -> Result<BoxedTrack, String> {
if mirrors.best_match.scoring {
return super::best_match::resolve_scored(
manager,
track_info,
identifier,
mirrors,
routeplanner,
)
.await;
}
let isrc = track_info.isrc.as_deref().unwrap_or("");
let query = format!("{} - {}", track_info.title, track_info.author);
let original_source_name = manager
.sources
.iter()
.find(|s| s.can_handle(identifier))
.map(|s| s.name());
for provider in &mirrors.providers {
if isrc.is_empty() && provider.contains("%ISRC%") {
tracing::debug!("Skipping mirror provider '{}': track has no ISRC", provider);
continue;
}
let resolved = provider.replace("%ISRC%", isrc).replace("%QUERY%", &query);
if let Some(handling_source) = manager.sources.iter().find(|s| s.can_handle(&resolved))
{
if handling_source.is_mirror() {
tracing::warn!(
"Skipping mirror provider '{}': '{}' is a Mirror-type source",
resolved,
handling_source.name()
);
continue;
}
if Some(handling_source.name()) == original_source_name {
tracing::debug!(
"Skipping mirror provider '{}': would loop back to '{}'",
resolved,
handling_source.name()
);
continue;
}
}
let res = match manager.load(&resolved, routeplanner.clone()).await {
crate::protocol::tracks::LoadResult::Track(t) => {
let id = t.info.uri.as_deref().unwrap_or(&t.info.identifier);
resolve_nested_track(manager, id, routeplanner.clone()).await
}
crate::protocol::tracks::LoadResult::Search(tracks) => {
if let Some(first) = tracks.first() {
let id = first.info.uri.as_deref().unwrap_or(&first.info.identifier);
resolve_nested_track(manager, id, routeplanner.clone()).await
} else {
None
}
}
_ => None,
};
if let Some(track) = res {
return Ok(track);
}
}
tracing::warn!(
"[Mirror] no valid mirror found for track: {} - {}",
track_info.title,
track_info.author
);
Err(format!(
"No mirror found for track: {} - {}",
track_info.title, track_info.author
))
}
pub async fn resolve_nested_track(
manager: &SourceManager,
identifier: &str,
routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
) -> Option<BoxedTrack> {
for source in &manager.sources {
if source.can_handle(identifier) {
if let Some(track) = source.get_track(identifier, routeplanner.clone()).await {
return Some(track);
}
if source.name() != "http" {
return None;
}
}
}
None
}
}
pub mod best_match {
use crate::sources::{manager::SourceManager, playable_track::BoxedTrack};
use futures::stream::{FuturesOrdered, FuturesUnordered, StreamExt};
use std::sync::Arc;
pub struct MirrorResult {
pub track: BoxedTrack,
pub score: f64,
pub provider: String,
}
fn normalize(s: &str) -> String {
let lower = s.to_lowercase();
let mut stripped = String::with_capacity(lower.len());
let mut depth: usize = 0;
for ch in lower.chars() {
match ch {
'(' | '[' => depth += 1,
')' | ']' => depth = depth.saturating_sub(1),
_ if depth == 0 => stripped.push(ch),
_ => {}
}
}
let stripped = stripped
.replace("feat.", " ")
.replace("feat ", " ")
.replace("ft.", " ")
.replace("ft ", " ");
let clean: String = stripped
.chars()
.map(|c| {
if c.is_alphanumeric() || c == ' ' {
c
} else {
' '
}
})
.collect();
clean.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn levenshtein(a: &str, b: &str) -> usize {
let a: Vec<char> = a.chars().collect();
let b: Vec<char> = b.chars().collect();
let (m, n) = (a.len(), b.len());
let mut prev: Vec<usize> = (0..=n).collect();
let mut curr = vec![0usize; n + 1];
for i in 1..=m {
curr[0] = i;
for j in 1..=n {
let cost = usize::from(a[i - 1] != b[j - 1]);
curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
}
std::mem::swap(&mut prev, &mut curr);
}
prev[n]
}
fn string_similarity(a: &str, b: &str) -> f64 {
if a == b {
return 1.0;
}
if a.is_empty() || b.is_empty() {
return 0.0;
}
let na = normalize(a);
let nb = normalize(b);
if na == nb {
return 1.0;
}
if na.contains(&nb) || nb.contains(&na) {
let shorter = na.len().min(nb.len()) as f64;
let longer = na.len().max(nb.len()) as f64;
return 0.80 + (shorter / longer) * 0.15;
}
let max_len = na.len().max(nb.len());
if max_len == 0 {
return 1.0;
}
1.0 - levenshtein(&na, &nb) as f64 / max_len as f64
}
fn duration_similarity(d1: u64, d2: u64, tolerance_ms: u64) -> f64 {
if d1 == 0 || d2 == 0 {
return 0.5;
}
let diff = d1.abs_diff(d2);
if diff <= tolerance_ms {
1.0
} else {
(1.0 - diff as f64 / d1.max(d2) as f64).max(0.0)
}
}
fn score_match(
orig_title: &str,
orig_author: &str,
orig_length: u64,
cand_title: &str,
cand_author: &str,
cand_length: u64,
cfg: &crate::config::server::BestMatchConfig,
) -> f64 {
let nt = normalize(orig_title);
let nc = normalize(cand_title);
let title_score = if nt == nc {
1.0
} else if nc.starts_with(&nt) {
0.95
} else if nc.contains(&nt) || nt.contains(&nc) {
let shorter = nt.len().min(nc.len()) as f64;
let longer = nt.len().max(nc.len()) as f64;
0.82 + (shorter / longer) * 0.10
} else {
string_similarity(&nt, &nc)
};
title_score * cfg.weight_title
+ string_similarity(orig_author, cand_author) * cfg.weight_artist
+ duration_similarity(orig_length, cand_length, cfg.duration_tolerance_ms)
* cfg.weight_duration
}
fn fmt_ms(ms: u64) -> String {
let s = ms / 1_000;
format!("{}:{:02}", s / 60, s % 60)
}
pub async fn resolve_scored(
manager: &SourceManager,
track_info: &crate::protocol::tracks::TrackInfo,
identifier: &str,
mirrors: &crate::config::server::MirrorsConfig,
routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
) -> Result<BoxedTrack, String> {
let isrc = track_info.isrc.as_deref().unwrap_or("");
let query = format!("{} {}", track_info.title, track_info.author);
let cfg = &mirrors.best_match;
let original_source_name = manager
.sources
.iter()
.find(|s| s.can_handle(identifier))
.map(|s| s.name().to_string());
let mut isrc_providers: Vec<String> = Vec::new();
let mut free_providers: Vec<String> = Vec::new();
let mut throttled_providers: Vec<String> = Vec::new();
for provider in &mirrors.providers {
let is_isrc_provider = provider.contains("%ISRC%");
if is_isrc_provider && isrc.is_empty() {
tracing::debug!("Skipping mirror provider '{}': track has no ISRC", provider);
continue;
}
let resolved = provider.replace("%ISRC%", isrc).replace("%QUERY%", &query);
if let Some(src) = manager.sources.iter().find(|s| s.can_handle(&resolved)) {
if src.is_mirror() {
tracing::warn!(
"Skipping mirror provider '{}': '{}' is a Mirror-type source",
resolved,
src.name()
);
continue;
}
if Some(src.name().to_string()) == original_source_name {
tracing::debug!(
"Skipping mirror provider '{}': would loop back to '{}'",
resolved,
src.name()
);
continue;
}
}
if is_isrc_provider {
isrc_providers.push(resolved);
} else if cfg
.throttled_prefixes
.iter()
.any(|p| resolved.starts_with(p.as_str()))
{
throttled_providers.push(resolved);
} else {
free_providers.push(resolved);
}
}
if !isrc_providers.is_empty() {
let mut futs: FuturesOrdered<_> = isrc_providers
.iter()
.map(|p| search_provider(manager, track_info, p, routeplanner.clone(), cfg, true))
.collect();
while let Some(result) = futs.next().await {
if let Some(mr) = result {
tracing::info!(
"[Mirror] ISRC match \"{}\" | {} | {} => {} | score: {:.3}",
track_info.title,
track_info.author,
fmt_ms(track_info.length),
mr.provider,
mr.score,
);
return Ok(mr.track);
}
}
}
let mut global_best: Option<MirrorResult> = None;
if !free_providers.is_empty() {
let mut futs: FuturesUnordered<_> = free_providers
.iter()
.map(|p| search_provider(manager, track_info, p, routeplanner.clone(), cfg, false))
.collect();
while let Some(result) = futs.next().await {
if let Some(mr) = result {
tracing::info!(
"[Mirror] \"{}\" | {} | {} => {} | score: {:.3}",
track_info.title,
track_info.author,
fmt_ms(track_info.length),
mr.provider,
mr.score,
);
if mr.score >= cfg.immediate_use {
return Ok(mr.track);
}
if global_best.as_ref().is_none_or(|b| mr.score > b.score) {
global_best = Some(mr);
}
}
}
}
for provider in &throttled_providers {
if let Some(mr) = search_provider(
manager,
track_info,
provider,
routeplanner.clone(),
cfg,
true,
)
.await
{
tracing::info!(
"[Mirror] throttled match \"{}\" via {} (score {:.3})",
track_info.title,
mr.provider,
mr.score
);
return Ok(mr.track);
}
}
if let Some(best) = global_best {
tracing::info!(
"[Mirror] fallback match \"{}\" via {} (score {:.3})",
track_info.title,
best.provider,
best.score
);
return Ok(best.track);
}
tracing::warn!(
"[Mirror] no valid mirror found for \"{}\" | {}",
track_info.title,
track_info.author
);
Err(format!(
"No mirror found for track: {} - {}",
track_info.title, track_info.author
))
}
async fn search_provider(
manager: &SourceManager,
original: &crate::protocol::tracks::TrackInfo,
resolved_provider: &str,
routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
cfg: &crate::config::server::BestMatchConfig,
trust_any: bool,
) -> Option<MirrorResult> {
use crate::protocol::tracks::LoadResult;
let candidates: Vec<crate::protocol::tracks::TrackInfo> =
match manager.load(resolved_provider, routeplanner.clone()).await {
LoadResult::Track(t) => vec![t.info],
LoadResult::Search(tracks) => tracks.into_iter().take(10).map(|t| t.info).collect(),
_ => return None,
};
if candidates.is_empty() {
return None;
}
let mut scored: Vec<(f64, crate::protocol::tracks::TrackInfo)> = candidates
.into_iter()
.map(|info| {
let s = score_match(
&original.title,
&original.author,
original.length,
&info.title,
&info.author,
info.length,
cfg,
);
(s, info)
})
.collect();
scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
let top_score = scored[0].0;
let (limit, threshold): (usize, f64) = if trust_any {
(scored.len(), 0.0)
} else if top_score >= cfg.immediate_use {
(1, cfg.immediate_use)
} else if top_score >= cfg.high_confidence {
(2, cfg.high_confidence)
} else {
(3, cfg.min_similarity)
};
for (score, info) in scored.into_iter().take(limit) {
if score < threshold {
break;
}
let id = info.uri.as_deref().unwrap_or(&info.identifier);
if let Some(track) =
super::resolver::resolve_nested_track(manager, id, routeplanner.clone()).await
{
return Some(MirrorResult {
track,
score,
provider: resolved_provider.to_string(),
});
}
}
None
}
}
pub mod registration {
use crate::{
common::HttpClientPool,
sources::{
amazonmusic::AmazonMusicSource,
anghami::AnghamiSource,
applemusic::AppleMusicSource,
audiomack::AudiomackSource,
audius::AudiusSource,
bandcamp::BandcampSource,
deezer::DeezerSource,
flowery::FlowerySource,
gaana::GaanaSource,
google_tts::GoogleTtsSource,
http::HttpSource,
jiosaavn::JioSaavnSource,
lastfm::LastFMSource,
local::LocalSource,
mixcloud::MixcloudSource,
netease::NeteaseSource,
pandora::PandoraSource,
playable_track::BoxedSource,
qobuz::QobuzSource,
reddit::RedditSource,
shazam::ShazamSource,
soundcloud::SoundCloudSource,
spotify::SpotifySource,
tidal::TidalSource,
twitch::TwitchSource,
vkmusic::VkMusicSource,
yandexmusic::YandexMusicSource,
youtube::{YouTubeSource, YoutubeStreamContext, cipher::YouTubeCipherManager},
},
};
use std::sync::Arc;
pub fn register_all(
sources: &mut Vec<BoxedSource>,
config: &crate::config::AppConfig,
http_pool: &Arc<HttpClientPool>,
) -> (
Option<Arc<YouTubeCipherManager>>,
Option<Arc<YoutubeStreamContext>>,
) {
let yt_ctx = register_core_sources(sources, config, http_pool);
register_extra_sources(sources, config);
yt_ctx
}
fn register_core_sources(
sources: &mut Vec<BoxedSource>,
config: &crate::config::AppConfig,
http_pool: &Arc<HttpClientPool>,
) -> (
Option<Arc<YouTubeCipherManager>>,
Option<Arc<YoutubeStreamContext>>,
) {
let mut yt_ctx = (None, None);
macro_rules! register {
($enabled:expr, $name:literal, $proxy:expr, $ctor:expr) => {
if $enabled {
if let Some(p) = &$proxy {
tracing::info!(
"Loading {} with proxy: {}",
$name,
p.url.as_ref().unwrap_or(&"enabled".to_owned())
);
}
match $ctor {
Ok(src) => {
tracing::info!("Loaded source: {}", $name);
sources.push(Box::new(src));
}
Err(e) => {
tracing::error!("{} source failed to initialize: {}", $name, e);
}
}
}
};
}
if config.sources.youtube.as_ref().is_some_and(|c| c.enabled) {
tracing::info!("Loaded source: YouTube");
let yt_client = http_pool.get(None);
let yt = YouTubeSource::new(config.sources.youtube.clone(), yt_client);
yt_ctx = (Some(yt.cipher_manager()), Some(yt.stream_context()));
sources.push(Box::new(yt));
}
let soundcloud_proxy = config
.sources
.soundcloud
.as_ref()
.and_then(|c| c.proxy.clone());
register!(
config
.sources
.soundcloud
.as_ref()
.is_some_and(|c| c.enabled),
"SoundCloud",
soundcloud_proxy,
SoundCloudSource::new(
config.sources.soundcloud.clone().unwrap(),
http_pool.get(soundcloud_proxy.clone())
)
);
register!(
config.sources.spotify.as_ref().is_some_and(|c| c.enabled),
"Spotify",
None::<crate::config::HttpProxyConfig>,
SpotifySource::new(config.sources.spotify.clone(), http_pool.get(None))
);
let jiosaavn_proxy = config
.sources
.jiosaavn
.as_ref()
.and_then(|c| c.proxy.clone());
register!(
config.sources.jiosaavn.as_ref().is_some_and(|c| c.enabled),
"JioSaavn",
jiosaavn_proxy,
JioSaavnSource::new(
config.sources.jiosaavn.clone(),
http_pool.get(jiosaavn_proxy.clone())
)
);
register_deezer(sources, config, http_pool);
let apple_proxy = config
.sources
.applemusic
.as_ref()
.and_then(|c| c.proxy.clone());
register!(
config
.sources
.applemusic
.as_ref()
.is_some_and(|c| c.enabled),
"Apple Music",
apple_proxy,
AppleMusicSource::new(
config.sources.applemusic.clone(),
http_pool.get(apple_proxy.clone())
)
);
let gaana_proxy = config.sources.gaana.as_ref().and_then(|c| c.proxy.clone());
register!(
config.sources.gaana.as_ref().is_some_and(|c| c.enabled),
"Gaana",
gaana_proxy,
GaanaSource::new(
config.sources.gaana.clone(),
http_pool.get(gaana_proxy.clone())
)
);
let tidal_proxy = config.sources.tidal.as_ref().and_then(|c| c.proxy.clone());
register!(
config.sources.tidal.as_ref().is_some_and(|c| c.enabled),
"Tidal",
tidal_proxy,
TidalSource::new(
config.sources.tidal.clone(),
http_pool.get(tidal_proxy.clone())
)
);
let audiomack_proxy = config
.sources
.audiomack
.as_ref()
.and_then(|c| c.proxy.clone());
register!(
config.sources.audiomack.as_ref().is_some_and(|c| c.enabled),
"Audiomack",
audiomack_proxy,
AudiomackSource::new(
config.sources.audiomack.clone(),
http_pool.get(audiomack_proxy.clone())
)
);
let pandora_proxy = config
.sources
.pandora
.as_ref()
.and_then(|c| c.proxy.clone());
register!(
config.sources.pandora.as_ref().is_some_and(|c| c.enabled),
"Pandora",
pandora_proxy,
PandoraSource::new(
config.sources.pandora.clone(),
http_pool.get(pandora_proxy.clone())
)
);
let qobuz_proxy = config.sources.qobuz.as_ref().and_then(|c| c.proxy.clone());
if config.sources.qobuz.as_ref().is_some_and(|c| c.enabled) {
let token_provided = config
.sources
.qobuz
.as_ref()
.and_then(|c| c.user_token.as_ref())
.is_some_and(|t| !t.is_empty());
if !token_provided {
tracing::warn!(
"Qobuz user_token is missing; all playback will fall back to mirrors."
);
}
register!(
true,
"Qobuz",
qobuz_proxy,
QobuzSource::new(config, http_pool.get(qobuz_proxy.clone()))
);
}
let anghami_proxy = config
.sources
.anghami
.as_ref()
.and_then(|c| c.proxy.clone());
register!(
config.sources.anghami.as_ref().is_some_and(|c| c.enabled),
"Anghami",
anghami_proxy,
AnghamiSource::new(config, http_pool.get(anghami_proxy.clone()))
);
let shazam_proxy = config.sources.shazam.as_ref().and_then(|c| c.proxy.clone());
register!(
config.sources.shazam.as_ref().is_some_and(|c| c.enabled),
"Shazam",
shazam_proxy,
ShazamSource::new(config, http_pool.get(shazam_proxy.clone()))
);
let mixcloud_proxy = config
.sources
.mixcloud
.as_ref()
.and_then(|c| c.proxy.clone());
register!(
config.sources.mixcloud.as_ref().is_some_and(|c| c.enabled),
"Mixcloud",
mixcloud_proxy,
MixcloudSource::new(
config.sources.mixcloud.clone(),
http_pool.get(mixcloud_proxy.clone())
)
);
let bandcamp_proxy = config
.sources
.bandcamp
.as_ref()
.and_then(|c| c.proxy.clone());
register!(
config.sources.bandcamp.as_ref().is_some_and(|c| c.enabled),
"Bandcamp",
bandcamp_proxy,
BandcampSource::new(
config.sources.bandcamp.clone(),
http_pool.get(bandcamp_proxy.clone())
)
);
let reddit_proxy = config.sources.reddit.as_ref().and_then(|c| c.proxy.clone());
register!(
config.sources.reddit.as_ref().is_some_and(|c| c.enabled),
"Reddit",
reddit_proxy,
RedditSource::new(
config.sources.reddit.clone(),
http_pool.get(reddit_proxy.clone())
)
);
register!(
config.sources.lastfm.as_ref().is_some_and(|c| c.enabled),
"Last.fm",
None::<crate::config::HttpProxyConfig>,
LastFMSource::new(config.sources.lastfm.clone(), http_pool.get(None))
);
let audius_proxy = config.sources.audius.as_ref().and_then(|c| c.proxy.clone());
register!(
config.sources.audius.as_ref().is_some_and(|c| c.enabled),
"Audius",
audius_proxy,
AudiusSource::new(
config.sources.audius.clone(),
http_pool.get(audius_proxy.clone())
)
);
register_yandex(sources, config, http_pool);
register_vkmusic(sources, config, http_pool);
register_netease(sources, config, http_pool);
register_twitch(sources, config, http_pool);
register_amazonmusic(sources, config, http_pool);
if config.sources.http.as_ref().is_some_and(|c| c.enabled) {
tracing::info!("Loaded source: http");
sources.push(Box::new(HttpSource::new()));
}
yt_ctx
}
fn register_deezer(
sources: &mut Vec<BoxedSource>,
config: &crate::config::AppConfig,
http_pool: &Arc<HttpClientPool>,
) {
let (token_provided, key_provided) = if let Some(c) = config.sources.deezer.as_ref() {
let arls_provided = c
.arls
.as_ref()
.is_some_and(|a| !a.is_empty() && a.iter().any(|s| !s.is_empty()));
let key_provided = c
.master_decryption_key
.as_ref()
.is_some_and(|k| !k.is_empty());
(arls_provided, key_provided)
} else {
(false, false)
};
if config.sources.deezer.as_ref().is_some_and(|c| c.enabled) {
if !token_provided || !key_provided {
let mut missing = Vec::new();
if !token_provided {
missing.push("arls");
}
if !key_provided {
missing.push("master_decryption_key");
}
tracing::warn!(
"Deezer source is enabled but {} {} missing; it will be disabled.",
missing.join(" and "),
if missing.len() > 1 { "are" } else { "is" }
);
} else {
let proxy = config.sources.deezer.as_ref().and_then(|c| c.proxy.clone());
let source = DeezerSource::new(
config.sources.deezer.clone().unwrap(),
http_pool.get(proxy.clone()),
);
match source {
Ok(src) => {
tracing::info!("Loaded source: Deezer");
sources.push(Box::new(src));
}
Err(e) => {
tracing::error!("Deezer source failed to initialize: {}", e);
}
}
}
}
}
fn register_yandex(
sources: &mut Vec<BoxedSource>,
config: &crate::config::AppConfig,
http_pool: &Arc<HttpClientPool>,
) {
if let Some(c) = config.sources.yandexmusic.as_ref()
&& c.enabled
{
if c.access_token.is_none() {
tracing::warn!(
"Yandex Music source is enabled but the access_token is missing; it will be disabled."
);
} else {
let proxy = c.proxy.clone();
let source = YandexMusicSource::new(
config.sources.yandexmusic.clone(),
http_pool.get(proxy.clone()),
);
match source {
Ok(src) => {
tracing::info!("Loaded source: Yandex Music");
sources.push(Box::new(src));
}
Err(e) => {
tracing::error!("Yandex Music source failed to initialize: {}", e);
}
}
}
}
}
fn register_vkmusic(
sources: &mut Vec<BoxedSource>,
config: &crate::config::AppConfig,
http_pool: &Arc<HttpClientPool>,
) {
if let Some(c) = config.sources.vkmusic.as_ref()
&& c.enabled
{
if c.user_token.is_none() && c.user_cookie.is_none() {
tracing::warn!(
"VK Music source is enabled but neither user_token nor user_cookie is set; API calls will fail."
);
}
let proxy = c.proxy.clone();
match VkMusicSource::new(config.sources.vkmusic.clone(), http_pool.get(proxy.clone())) {
Ok(src) => {
tracing::info!("Loaded source: VK Music");
sources.push(Box::new(src));
}
Err(e) => {
tracing::error!("VK Music source failed to initialize: {}", e);
}
}
}
}
fn register_netease(
sources: &mut Vec<BoxedSource>,
config: &crate::config::AppConfig,
http_pool: &Arc<HttpClientPool>,
) {
if let Some(c) = config.sources.netease.as_ref()
&& c.enabled
{
let proxy = c.proxy.clone();
match NeteaseSource::new(config.sources.netease.clone(), http_pool.get(proxy.clone())) {
Ok(src) => {
tracing::info!("Loaded source: Netease Music");
sources.push(Box::new(src));
}
Err(e) => {
tracing::error!("Netease Music source failed to initialize: {}", e);
}
}
}
}
fn register_twitch(
sources: &mut Vec<BoxedSource>,
config: &crate::config::AppConfig,
http_pool: &Arc<HttpClientPool>,
) {
if let Some(c) = config.sources.twitch.as_ref()
&& c.enabled
{
let proxy = c.proxy.clone();
tracing::info!("Loaded source: Twitch");
sources.push(Box::new(TwitchSource::new(c.clone(), http_pool.get(proxy))));
}
}
fn register_amazonmusic(
sources: &mut Vec<BoxedSource>,
config: &crate::config::AppConfig,
http_pool: &Arc<HttpClientPool>,
) {
if let Some(c) = config.sources.amazonmusic.as_ref()
&& c.enabled
{
let proxy = c.proxy.clone();
match AmazonMusicSource::new(c.clone(), http_pool.get(proxy)) {
Ok(src) => {
tracing::info!("Loaded source: Amazon Music");
sources.push(Box::new(src));
}
Err(e) => {
tracing::error!("Amazon Music source failed to initialize: {}", e);
}
}
}
}
fn register_extra_sources(sources: &mut Vec<BoxedSource>, config: &crate::config::AppConfig) {
if let Some(c) = config.sources.google_tts.as_ref()
&& c.enabled
{
tracing::info!("Loaded source: Google TTS");
sources.push(Box::new(GoogleTtsSource::new(c.clone())));
}
if let Some(c) = config.sources.flowery.as_ref()
&& c.enabled
{
tracing::info!("Loaded source: Flowery");
sources.push(Box::new(FlowerySource::new(c.clone())));
}
if config.sources.local.as_ref().is_some_and(|c| c.enabled) {
tracing::info!("Loaded source: local");
sources.push(Box::new(LocalSource::new()));
}
}
}
use crate::{
common::HttpClientPool,
sources::playable_track::{BoxedSource, BoxedTrack},
};
use std::sync::Arc;
pub struct SourceManager {
pub sources: Vec<BoxedSource>,
pub mirrors: crate::config::server::MirrorsConfig,
pub youtube_cipher_manager: Option<Arc<crate::sources::youtube::cipher::YouTubeCipherManager>>,
pub youtube_stream_ctx: Option<Arc<crate::sources::youtube::YoutubeStreamContext>>,
pub http_pool: Arc<HttpClientPool>,
pub player_config: crate::config::player::PlayerConfig,
}
impl SourceManager {
pub fn new(config: &crate::config::AppConfig) -> Self {
let http_pool = Arc::new(HttpClientPool::new());
let mut sources = Vec::new();
let (youtube_cipher_manager, youtube_stream_ctx) =
registration::register_all(&mut sources, config, &http_pool);
Self {
sources,
mirrors: config.player.mirrors.clone(),
youtube_cipher_manager,
youtube_stream_ctx,
http_pool,
player_config: config.player.clone(),
}
}
pub async fn load(
&self,
identifier: &str,
routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
) -> crate::protocol::tracks::LoadResult {
for source in &self.sources {
if source.can_handle(identifier) {
tracing::debug!(
"SourceManager: Loading '{}' with source: {}",
identifier,
source.name()
);
return source.load(identifier, routeplanner.clone()).await;
}
}
tracing::debug!(
"SourceManager: No source matched identifier: '{}'",
identifier
);
crate::protocol::tracks::LoadResult::Empty {}
}
pub async fn load_search(
&self,
query: &str,
types: &[String],
routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
) -> Option<crate::protocol::tracks::SearchResult> {
for source in &self.sources {
if source.can_handle(query) {
tracing::trace!("Loading search '{}' with source: {}", query, source.name());
return source.load_search(query, types, routeplanner.clone()).await;
}
}
tracing::debug!("No source could handle search query: {}", query);
None
}
pub async fn resolve_track(
&self,
track_info: &crate::protocol::tracks::TrackInfo,
routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
) -> Result<BoxedTrack, String> {
let identifier = track_info.uri.as_deref().unwrap_or(&track_info.identifier);
for source in &self.sources {
if source.can_handle(identifier) {
tracing::trace!(
"Resolving playable track for '{}' with source: {}",
identifier,
source.name()
);
if let Some(track) = source.get_track(identifier, routeplanner.clone()).await {
return Ok(track);
}
break;
}
}
resolver::resolve_with_mirrors(self, track_info, identifier, &self.mirrors, routeplanner)
.await
}
pub fn source_names(&self) -> Vec<String> {
self.sources.iter().map(|s| s.name().to_string()).collect()
}
pub fn get_proxy_config(&self, source_name: &str) -> Option<crate::config::HttpProxyConfig> {
self.sources
.iter()
.find(|s| s.name() == source_name)
.and_then(|s| s.get_proxy_config())
}
}