mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
fix(subsonic): sync Rust HTTP UA with main webview UA (#235)
Use the main window WebView user agent as the runtime source for Rust-side HTTP clients and refresh the audio engine client when the UA changes. This keeps backend and WebView requests aligned while preserving a simple startup sync flow.
This commit is contained in:
+41
-19
@@ -1,5 +1,5 @@
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom};
|
||||
use std::sync::{Arc, Mutex, OnceLock, TryLockError};
|
||||
use std::sync::{Arc, Mutex, OnceLock, RwLock, TryLockError};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
#[cfg(unix)]
|
||||
@@ -1980,7 +1980,7 @@ pub struct AudioEngine {
|
||||
pub current: Arc<Mutex<AudioCurrent>>,
|
||||
/// Monotonically incremented on each audio_play (non-chain) / audio_stop call.
|
||||
pub generation: Arc<AtomicU64>,
|
||||
pub http_client: reqwest::Client,
|
||||
pub http_client: Arc<RwLock<reqwest::Client>>,
|
||||
pub eq_gains: Arc<[AtomicU32; 10]>,
|
||||
pub eq_enabled: Arc<AtomicBool>,
|
||||
pub eq_pre_gain: Arc<AtomicU32>,
|
||||
@@ -2240,12 +2240,14 @@ pub fn create_engine() -> (AudioEngine, std::thread::JoinHandle<()>) {
|
||||
fadeout_samples: None,
|
||||
})),
|
||||
generation: Arc::new(AtomicU64::new(0)),
|
||||
http_client: reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.use_rustls_tls()
|
||||
.user_agent(format!("psysonic/{}", env!("CARGO_PKG_VERSION")))
|
||||
.build()
|
||||
.unwrap_or_default(),
|
||||
http_client: Arc::new(RwLock::new(
|
||||
reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.use_rustls_tls()
|
||||
.user_agent(crate::subsonic_wire_user_agent())
|
||||
.build()
|
||||
.unwrap_or_default(),
|
||||
)),
|
||||
eq_gains: Arc::new(std::array::from_fn(|_| AtomicU32::new(0f32.to_bits()))),
|
||||
eq_enabled: Arc::new(AtomicBool::new(false)),
|
||||
eq_pre_gain: Arc::new(AtomicU32::new(0f32.to_bits())),
|
||||
@@ -2267,6 +2269,26 @@ pub fn create_engine() -> (AudioEngine, std::thread::JoinHandle<()>) {
|
||||
(engine, thread)
|
||||
}
|
||||
|
||||
fn audio_http_client(state: &AudioEngine) -> reqwest::Client {
|
||||
state
|
||||
.http_client
|
||||
.read()
|
||||
.map(|c| c.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn refresh_http_user_agent(state: &AudioEngine, ua: &str) {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.use_rustls_tls()
|
||||
.user_agent(ua)
|
||||
.build()
|
||||
.unwrap_or_default();
|
||||
if let Ok(mut slot) = state.http_client.write() {
|
||||
*slot = client;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Event payloads ───────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
@@ -2355,7 +2377,7 @@ async fn fetch_data(
|
||||
return Ok(Some(data));
|
||||
}
|
||||
|
||||
let response = state.http_client.get(url).send().await.map_err(|e| e.to_string())?;
|
||||
let response = audio_http_client(&state).get(url).send().await.map_err(|e| e.to_string())?;
|
||||
let status = response.status();
|
||||
let ct = response.headers()
|
||||
.get(reqwest::header::CONTENT_TYPE)
|
||||
@@ -2581,7 +2603,7 @@ pub async fn audio_play(
|
||||
tag: "local-file",
|
||||
}
|
||||
} else if manual && !stream_cache_hit && !preloaded_hit && !is_local {
|
||||
let response = state.http_client.get(&url).send().await.map_err(|e| e.to_string())?;
|
||||
let response = audio_http_client(&state).get(&url).send().await.map_err(|e| e.to_string())?;
|
||||
if !response.status().is_success() {
|
||||
if state.generation.load(Ordering::SeqCst) != gen {
|
||||
return Ok(()); // superseded
|
||||
@@ -2619,7 +2641,7 @@ pub async fn audio_play(
|
||||
tokio::spawn(ranged_download_task(
|
||||
gen,
|
||||
state.generation.clone(),
|
||||
state.http_client.clone(),
|
||||
audio_http_client(&state),
|
||||
url.clone(),
|
||||
response,
|
||||
buf.clone(),
|
||||
@@ -2656,7 +2678,7 @@ pub async fn audio_play(
|
||||
tokio::spawn(track_download_task(
|
||||
gen,
|
||||
state.generation.clone(),
|
||||
state.http_client.clone(),
|
||||
audio_http_client(&state),
|
||||
url.clone(),
|
||||
response,
|
||||
prod,
|
||||
@@ -3016,7 +3038,7 @@ pub async fn audio_chain_preload(
|
||||
if let Some(path) = url.strip_prefix("psysonic-local://") {
|
||||
tokio::fs::read(path).await.map_err(|e| e.to_string())?
|
||||
} else {
|
||||
let resp = state.http_client.get(&url).send().await
|
||||
let resp = audio_http_client(&state).get(&url).send().await
|
||||
.map_err(|e| e.to_string())?;
|
||||
if !resp.status().is_success() {
|
||||
return Ok(()); // silently fail — audio_play will retry
|
||||
@@ -3318,7 +3340,7 @@ pub async fn audio_resume(state: State<'_, AudioEngine>, app: AppHandle) -> Resu
|
||||
gen,
|
||||
state.generation.clone(),
|
||||
None, // task performs its own fresh GET
|
||||
state.http_client.clone(),
|
||||
audio_http_client(&state),
|
||||
url,
|
||||
new_prod,
|
||||
flags.clone(),
|
||||
@@ -3498,7 +3520,7 @@ pub fn audio_update_replay_gain(
|
||||
/// Proxy: fetches https://autoeq.app/entries via Rust to bypass WebView CORS restrictions.
|
||||
#[tauri::command]
|
||||
pub async fn autoeq_entries(state: State<'_, AudioEngine>) -> Result<String, String> {
|
||||
state.http_client
|
||||
audio_http_client(&state)
|
||||
.get("https://autoeq.app/entries")
|
||||
.send().await.map_err(|e| e.to_string())?
|
||||
.text().await.map_err(|e| e.to_string())
|
||||
@@ -3532,7 +3554,7 @@ pub async fn autoeq_fetch_profile(
|
||||
};
|
||||
|
||||
for url in &candidates {
|
||||
let resp = state.http_client.get(url).send().await.map_err(|e| e.to_string())?;
|
||||
let resp = audio_http_client(&state).get(url).send().await.map_err(|e| e.to_string())?;
|
||||
if resp.status().is_success() {
|
||||
return resp.text().await.map_err(|e| e.to_string());
|
||||
}
|
||||
@@ -3575,7 +3597,7 @@ pub async fn audio_preload(
|
||||
let data: Vec<u8> = if let Some(path) = url.strip_prefix("psysonic-local://") {
|
||||
tokio::fs::read(path).await.map_err(|e| e.to_string())?
|
||||
} else {
|
||||
let response = state.http_client.get(&url).send().await.map_err(|e| e.to_string())?;
|
||||
let response = audio_http_client(&state).get(&url).send().await.map_err(|e| e.to_string())?;
|
||||
if !response.status().is_success() {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -3613,7 +3635,7 @@ pub async fn audio_play_radio(
|
||||
if let Some(old) = state.fading_out_sink.lock().unwrap().take() { old.stop(); }
|
||||
|
||||
// ── Open initial HTTP connection ──────────────────────────────────────────
|
||||
let response = state.http_client
|
||||
let response = audio_http_client(&state)
|
||||
.get(&url)
|
||||
.header("Icy-MetaData", "1")
|
||||
.send()
|
||||
@@ -3653,7 +3675,7 @@ pub async fn audio_play_radio(
|
||||
gen,
|
||||
state.generation.clone(),
|
||||
Some(response),
|
||||
state.http_client.clone(),
|
||||
audio_http_client(&state),
|
||||
url.clone(),
|
||||
prod,
|
||||
flags.clone(),
|
||||
|
||||
+60
-11
@@ -9,7 +9,7 @@ pub(crate) mod logging;
|
||||
mod taskbar_win;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
use std::sync::{Arc, Mutex, OnceLock, RwLock};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use tauri::{
|
||||
@@ -30,6 +30,25 @@ type ShortcutMap = Mutex<HashMap<String, String>>;
|
||||
/// The frontend queues more tasks than this; Rust is the real throttle.
|
||||
const MAX_DL_CONCURRENCY: usize = 4;
|
||||
|
||||
fn default_subsonic_wire_user_agent() -> String {
|
||||
format!("psysonic/{}", env!("CARGO_PKG_VERSION"))
|
||||
}
|
||||
|
||||
fn runtime_subsonic_wire_user_agent() -> &'static RwLock<String> {
|
||||
static UA: OnceLock<RwLock<String>> = OnceLock::new();
|
||||
UA.get_or_init(|| RwLock::new(default_subsonic_wire_user_agent()))
|
||||
}
|
||||
|
||||
/// Unified outbound User-Agent for all Rust-side HTTP requests.
|
||||
/// It is initialized with `psysonic/<version>` and then overridden from
|
||||
/// the main WebView `navigator.userAgent` at app startup.
|
||||
pub(crate) fn subsonic_wire_user_agent() -> String {
|
||||
runtime_subsonic_wire_user_agent()
|
||||
.read()
|
||||
.map(|ua| ua.clone())
|
||||
.unwrap_or_else(|_| default_subsonic_wire_user_agent())
|
||||
}
|
||||
|
||||
/// Shared semaphore that caps simultaneous `download_track_offline` executions.
|
||||
type DownloadSemaphore = Arc<tokio::sync::Semaphore>;
|
||||
|
||||
@@ -134,6 +153,30 @@ fn export_runtime_logs(path: String) -> Result<usize, String> {
|
||||
crate::logging::export_logs_to_file(&path)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_subsonic_wire_user_agent(
|
||||
user_agent: String,
|
||||
window_label: String,
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> Result<(), String> {
|
||||
if window_label != "main" {
|
||||
return Ok(());
|
||||
}
|
||||
let ua = user_agent.trim();
|
||||
if ua.is_empty() {
|
||||
return Err("user agent is empty".to_string());
|
||||
}
|
||||
let mut guard = runtime_subsonic_wire_user_agent()
|
||||
.write()
|
||||
.map_err(|_| "user agent state poisoned".to_string())?;
|
||||
guard.clear();
|
||||
guard.push_str(ua);
|
||||
drop(guard);
|
||||
|
||||
crate::audio::refresh_http_user_agent(&app_handle.state::<crate::audio::AudioEngine>(), ua);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Authenticate with Navidrome's own REST API and return a Bearer token.
|
||||
async fn navidrome_token(server_url: &str, username: &str, password: &str) -> Result<String, String> {
|
||||
@@ -449,7 +492,7 @@ async fn search_radio_browser(query: String, offset: u32) -> Result<Vec<serde_js
|
||||
let offset_s = offset.to_string();
|
||||
let resp = client
|
||||
.get("https://de1.api.radio-browser.info/json/stations/search")
|
||||
.header("User-Agent", "psysonic/1.0")
|
||||
.header("User-Agent", subsonic_wire_user_agent())
|
||||
.query(&[
|
||||
("name", query.as_str()),
|
||||
("hidebroken", "true"),
|
||||
@@ -472,7 +515,7 @@ async fn get_top_radio_stations(offset: u32) -> Result<Vec<serde_json::Value>, S
|
||||
let offset_s = offset.to_string();
|
||||
let resp = client
|
||||
.get("https://de1.api.radio-browser.info/json/stations/topvote")
|
||||
.header("User-Agent", "psysonic/1.0")
|
||||
.header("User-Agent", subsonic_wire_user_agent())
|
||||
.query(&[("limit", limit_s.as_str()), ("offset", offset_s.as_str())])
|
||||
.send()
|
||||
.await
|
||||
@@ -487,12 +530,12 @@ async fn get_top_radio_stations(offset: u32) -> Result<Vec<serde_json::Value>, S
|
||||
#[tauri::command]
|
||||
async fn fetch_url_bytes(url: String) -> Result<(Vec<u8>, String), String> {
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent(subsonic_wire_user_agent())
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let resp = client
|
||||
.get(&url)
|
||||
.header("User-Agent", "psysonic/1.0")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
@@ -517,12 +560,12 @@ async fn fetch_url_bytes(url: String) -> Result<(Vec<u8>, String), String> {
|
||||
#[tauri::command]
|
||||
async fn fetch_json_url(url: String) -> Result<String, String> {
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent(subsonic_wire_user_agent())
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let resp = client
|
||||
.get(&url)
|
||||
.header("User-Agent", "psysonic/1.0")
|
||||
.header("Accept", "application/json")
|
||||
.send()
|
||||
.await
|
||||
@@ -581,7 +624,6 @@ async fn resolve_playlist_url(client: &reqwest::Client, url: &str) -> Option<Str
|
||||
|
||||
let resp = client
|
||||
.get(url)
|
||||
.header("User-Agent", "psysonic/1.0")
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.send()
|
||||
.await
|
||||
@@ -617,6 +659,7 @@ async fn fetch_icy_metadata(url: String) -> Result<IcyMetadata, String> {
|
||||
use futures_util::StreamExt;
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent(subsonic_wire_user_agent())
|
||||
.timeout(std::time::Duration::from_secs(15))
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
@@ -627,7 +670,6 @@ async fn fetch_icy_metadata(url: String) -> Result<IcyMetadata, String> {
|
||||
let resp = client
|
||||
.get(&url)
|
||||
.header("Icy-MetaData", "1")
|
||||
.header("User-Agent", "psysonic/1.0")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
@@ -762,14 +804,14 @@ async fn lastfm_request(
|
||||
client
|
||||
.get("https://ws.audioscrobbler.com/2.0/")
|
||||
.query(&map)
|
||||
.header("User-Agent", "psysonic/1.13.0")
|
||||
.header("User-Agent", subsonic_wire_user_agent())
|
||||
.send()
|
||||
.await
|
||||
} else {
|
||||
client
|
||||
.post("https://ws.audioscrobbler.com/2.0/")
|
||||
.form(&map)
|
||||
.header("User-Agent", "psysonic/1.13.0")
|
||||
.header("User-Agent", subsonic_wire_user_agent())
|
||||
.send()
|
||||
.await
|
||||
}.map_err(|e| e.to_string())?;
|
||||
@@ -957,6 +999,7 @@ async fn download_track_offline(
|
||||
let _permit = dl_sem.acquire().await.map_err(|e| e.to_string())?;
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent(subsonic_wire_user_agent())
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
@@ -1136,6 +1179,7 @@ async fn download_update(url: String, filename: String, app: tauri::AppHandle) -
|
||||
let part_path = dest_dir.join(format!("{}.part", filename));
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent(subsonic_wire_user_agent())
|
||||
.connect_timeout(Duration::from_secs(30))
|
||||
.timeout(Duration::from_secs(3600))
|
||||
.build()
|
||||
@@ -1198,6 +1242,7 @@ async fn download_update(url: String, filename: String, app: tauri::AppHandle) -
|
||||
#[tauri::command]
|
||||
async fn fetch_netease_lyrics(artist: String, title: String) -> Result<Option<String>, String> {
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent(subsonic_wire_user_agent())
|
||||
.timeout(std::time::Duration::from_secs(8))
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
@@ -1206,7 +1251,6 @@ async fn fetch_netease_lyrics(artist: String, title: String) -> Result<Option<St
|
||||
let params = [("s", query.as_str()), ("type", "1"), ("limit", "5")];
|
||||
let search: serde_json::Value = client
|
||||
.post("https://music.163.com/api/search/get")
|
||||
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36")
|
||||
.header("Referer", "https://music.163.com")
|
||||
.form(¶ms)
|
||||
.send()
|
||||
@@ -1226,7 +1270,6 @@ async fn fetch_netease_lyrics(artist: String, title: String) -> Result<Option<St
|
||||
"https://music.163.com/api/song/lyric?id={}&lv=1&kv=1&tv=-1",
|
||||
song_id
|
||||
))
|
||||
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36")
|
||||
.header("Referer", "https://music.163.com")
|
||||
.send()
|
||||
.await
|
||||
@@ -1401,6 +1444,7 @@ async fn download_zip(
|
||||
const EMIT_INTERVAL: Duration = Duration::from_millis(500);
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent(subsonic_wire_user_agent())
|
||||
.connect_timeout(Duration::from_secs(30))
|
||||
.timeout(Duration::from_secs(7200)) // up to 2 h for large on-the-fly ZIPs
|
||||
.build()
|
||||
@@ -1503,6 +1547,7 @@ async fn download_track_hot_cache(
|
||||
}
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent(subsonic_wire_user_agent())
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
@@ -2007,6 +2052,7 @@ async fn sync_track_to_device(
|
||||
}
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent(subsonic_wire_user_agent())
|
||||
.timeout(std::time::Duration::from_secs(300))
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
@@ -2192,6 +2238,7 @@ async fn calculate_sync_payload(
|
||||
target_dir: String,
|
||||
) -> Result<SyncDeltaResult, String> {
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent(subsonic_wire_user_agent())
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
@@ -2419,6 +2466,7 @@ async fn sync_batch_to_device(
|
||||
|
||||
// Shared reqwest client — reused across all downloads.
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent(subsonic_wire_user_agent())
|
||||
.timeout(Duration::from_secs(300))
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
@@ -3355,6 +3403,7 @@ pub fn run() {
|
||||
set_linux_webkit_smooth_scrolling,
|
||||
set_logging_mode,
|
||||
export_runtime_logs,
|
||||
set_subsonic_wire_user_agent,
|
||||
no_compositing_mode,
|
||||
is_tiling_wm_cmd,
|
||||
open_mini_player,
|
||||
|
||||
Reference in New Issue
Block a user