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:
cucadmuh
2026-04-21 20:04:04 +03:00
committed by GitHub
parent 3b3833007b
commit bac0afe6ae
4 changed files with 122 additions and 35 deletions
+41 -19
View File
@@ -1,5 +1,5 @@
use std::io::{Cursor, Read, Seek, SeekFrom}; 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::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
#[cfg(unix)] #[cfg(unix)]
@@ -1980,7 +1980,7 @@ pub struct AudioEngine {
pub current: Arc<Mutex<AudioCurrent>>, pub current: Arc<Mutex<AudioCurrent>>,
/// Monotonically incremented on each audio_play (non-chain) / audio_stop call. /// Monotonically incremented on each audio_play (non-chain) / audio_stop call.
pub generation: Arc<AtomicU64>, pub generation: Arc<AtomicU64>,
pub http_client: reqwest::Client, pub http_client: Arc<RwLock<reqwest::Client>>,
pub eq_gains: Arc<[AtomicU32; 10]>, pub eq_gains: Arc<[AtomicU32; 10]>,
pub eq_enabled: Arc<AtomicBool>, pub eq_enabled: Arc<AtomicBool>,
pub eq_pre_gain: Arc<AtomicU32>, pub eq_pre_gain: Arc<AtomicU32>,
@@ -2240,12 +2240,14 @@ pub fn create_engine() -> (AudioEngine, std::thread::JoinHandle<()>) {
fadeout_samples: None, fadeout_samples: None,
})), })),
generation: Arc::new(AtomicU64::new(0)), generation: Arc::new(AtomicU64::new(0)),
http_client: reqwest::Client::builder() http_client: Arc::new(RwLock::new(
.timeout(Duration::from_secs(30)) reqwest::Client::builder()
.use_rustls_tls() .timeout(Duration::from_secs(30))
.user_agent(format!("psysonic/{}", env!("CARGO_PKG_VERSION"))) .use_rustls_tls()
.build() .user_agent(crate::subsonic_wire_user_agent())
.unwrap_or_default(), .build()
.unwrap_or_default(),
)),
eq_gains: Arc::new(std::array::from_fn(|_| AtomicU32::new(0f32.to_bits()))), eq_gains: Arc::new(std::array::from_fn(|_| AtomicU32::new(0f32.to_bits()))),
eq_enabled: Arc::new(AtomicBool::new(false)), eq_enabled: Arc::new(AtomicBool::new(false)),
eq_pre_gain: Arc::new(AtomicU32::new(0f32.to_bits())), eq_pre_gain: Arc::new(AtomicU32::new(0f32.to_bits())),
@@ -2267,6 +2269,26 @@ pub fn create_engine() -> (AudioEngine, std::thread::JoinHandle<()>) {
(engine, thread) (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 ─────────────────────────────────────────────────────────── // ─── Event payloads ───────────────────────────────────────────────────────────
#[derive(Clone, Serialize)] #[derive(Clone, Serialize)]
@@ -2355,7 +2377,7 @@ async fn fetch_data(
return Ok(Some(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 status = response.status();
let ct = response.headers() let ct = response.headers()
.get(reqwest::header::CONTENT_TYPE) .get(reqwest::header::CONTENT_TYPE)
@@ -2581,7 +2603,7 @@ pub async fn audio_play(
tag: "local-file", tag: "local-file",
} }
} else if manual && !stream_cache_hit && !preloaded_hit && !is_local { } 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 !response.status().is_success() {
if state.generation.load(Ordering::SeqCst) != gen { if state.generation.load(Ordering::SeqCst) != gen {
return Ok(()); // superseded return Ok(()); // superseded
@@ -2619,7 +2641,7 @@ pub async fn audio_play(
tokio::spawn(ranged_download_task( tokio::spawn(ranged_download_task(
gen, gen,
state.generation.clone(), state.generation.clone(),
state.http_client.clone(), audio_http_client(&state),
url.clone(), url.clone(),
response, response,
buf.clone(), buf.clone(),
@@ -2656,7 +2678,7 @@ pub async fn audio_play(
tokio::spawn(track_download_task( tokio::spawn(track_download_task(
gen, gen,
state.generation.clone(), state.generation.clone(),
state.http_client.clone(), audio_http_client(&state),
url.clone(), url.clone(),
response, response,
prod, prod,
@@ -3016,7 +3038,7 @@ pub async fn audio_chain_preload(
if let Some(path) = url.strip_prefix("psysonic-local://") { if let Some(path) = url.strip_prefix("psysonic-local://") {
tokio::fs::read(path).await.map_err(|e| e.to_string())? tokio::fs::read(path).await.map_err(|e| e.to_string())?
} else { } 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())?; .map_err(|e| e.to_string())?;
if !resp.status().is_success() { if !resp.status().is_success() {
return Ok(()); // silently fail — audio_play will retry return Ok(()); // silently fail — audio_play will retry
@@ -3318,7 +3340,7 @@ pub async fn audio_resume(state: State<'_, AudioEngine>, app: AppHandle) -> Resu
gen, gen,
state.generation.clone(), state.generation.clone(),
None, // task performs its own fresh GET None, // task performs its own fresh GET
state.http_client.clone(), audio_http_client(&state),
url, url,
new_prod, new_prod,
flags.clone(), 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. /// Proxy: fetches https://autoeq.app/entries via Rust to bypass WebView CORS restrictions.
#[tauri::command] #[tauri::command]
pub async fn autoeq_entries(state: State<'_, AudioEngine>) -> Result<String, String> { pub async fn autoeq_entries(state: State<'_, AudioEngine>) -> Result<String, String> {
state.http_client audio_http_client(&state)
.get("https://autoeq.app/entries") .get("https://autoeq.app/entries")
.send().await.map_err(|e| e.to_string())? .send().await.map_err(|e| e.to_string())?
.text().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 { 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() { if resp.status().is_success() {
return resp.text().await.map_err(|e| e.to_string()); 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://") { let data: Vec<u8> = if let Some(path) = url.strip_prefix("psysonic-local://") {
tokio::fs::read(path).await.map_err(|e| e.to_string())? tokio::fs::read(path).await.map_err(|e| e.to_string())?
} else { } 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() { if !response.status().is_success() {
return Ok(()); 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(); } if let Some(old) = state.fading_out_sink.lock().unwrap().take() { old.stop(); }
// ── Open initial HTTP connection ────────────────────────────────────────── // ── Open initial HTTP connection ──────────────────────────────────────────
let response = state.http_client let response = audio_http_client(&state)
.get(&url) .get(&url)
.header("Icy-MetaData", "1") .header("Icy-MetaData", "1")
.send() .send()
@@ -3653,7 +3675,7 @@ pub async fn audio_play_radio(
gen, gen,
state.generation.clone(), state.generation.clone(),
Some(response), Some(response),
state.http_client.clone(), audio_http_client(&state),
url.clone(), url.clone(),
prod, prod,
flags.clone(), flags.clone(),
+60 -11
View File
@@ -9,7 +9,7 @@ pub(crate) mod logging;
mod taskbar_win; mod taskbar_win;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock}; use std::sync::{Arc, Mutex, OnceLock, RwLock};
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use tauri::{ use tauri::{
@@ -30,6 +30,25 @@ type ShortcutMap = Mutex<HashMap<String, String>>;
/// The frontend queues more tasks than this; Rust is the real throttle. /// The frontend queues more tasks than this; Rust is the real throttle.
const MAX_DL_CONCURRENCY: usize = 4; 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. /// Shared semaphore that caps simultaneous `download_track_offline` executions.
type DownloadSemaphore = Arc<tokio::sync::Semaphore>; 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) 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. /// 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> { 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 offset_s = offset.to_string();
let resp = client let resp = client
.get("https://de1.api.radio-browser.info/json/stations/search") .get("https://de1.api.radio-browser.info/json/stations/search")
.header("User-Agent", "psysonic/1.0") .header("User-Agent", subsonic_wire_user_agent())
.query(&[ .query(&[
("name", query.as_str()), ("name", query.as_str()),
("hidebroken", "true"), ("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 offset_s = offset.to_string();
let resp = client let resp = client
.get("https://de1.api.radio-browser.info/json/stations/topvote") .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())]) .query(&[("limit", limit_s.as_str()), ("offset", offset_s.as_str())])
.send() .send()
.await .await
@@ -487,12 +530,12 @@ async fn get_top_radio_stations(offset: u32) -> Result<Vec<serde_json::Value>, S
#[tauri::command] #[tauri::command]
async fn fetch_url_bytes(url: String) -> Result<(Vec<u8>, String), String> { async fn fetch_url_bytes(url: String) -> Result<(Vec<u8>, String), String> {
let client = reqwest::Client::builder() let client = reqwest::Client::builder()
.user_agent(subsonic_wire_user_agent())
.timeout(std::time::Duration::from_secs(10)) .timeout(std::time::Duration::from_secs(10))
.build() .build()
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let resp = client let resp = client
.get(&url) .get(&url)
.header("User-Agent", "psysonic/1.0")
.send() .send()
.await .await
.map_err(|e| e.to_string())? .map_err(|e| e.to_string())?
@@ -517,12 +560,12 @@ async fn fetch_url_bytes(url: String) -> Result<(Vec<u8>, String), String> {
#[tauri::command] #[tauri::command]
async fn fetch_json_url(url: String) -> Result<String, String> { async fn fetch_json_url(url: String) -> Result<String, String> {
let client = reqwest::Client::builder() let client = reqwest::Client::builder()
.user_agent(subsonic_wire_user_agent())
.timeout(std::time::Duration::from_secs(10)) .timeout(std::time::Duration::from_secs(10))
.build() .build()
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let resp = client let resp = client
.get(&url) .get(&url)
.header("User-Agent", "psysonic/1.0")
.header("Accept", "application/json") .header("Accept", "application/json")
.send() .send()
.await .await
@@ -581,7 +624,6 @@ async fn resolve_playlist_url(client: &reqwest::Client, url: &str) -> Option<Str
let resp = client let resp = client
.get(url) .get(url)
.header("User-Agent", "psysonic/1.0")
.timeout(std::time::Duration::from_secs(10)) .timeout(std::time::Duration::from_secs(10))
.send() .send()
.await .await
@@ -617,6 +659,7 @@ async fn fetch_icy_metadata(url: String) -> Result<IcyMetadata, String> {
use futures_util::StreamExt; use futures_util::StreamExt;
let client = reqwest::Client::builder() let client = reqwest::Client::builder()
.user_agent(subsonic_wire_user_agent())
.timeout(std::time::Duration::from_secs(15)) .timeout(std::time::Duration::from_secs(15))
.build() .build()
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
@@ -627,7 +670,6 @@ async fn fetch_icy_metadata(url: String) -> Result<IcyMetadata, String> {
let resp = client let resp = client
.get(&url) .get(&url)
.header("Icy-MetaData", "1") .header("Icy-MetaData", "1")
.header("User-Agent", "psysonic/1.0")
.send() .send()
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
@@ -762,14 +804,14 @@ async fn lastfm_request(
client client
.get("https://ws.audioscrobbler.com/2.0/") .get("https://ws.audioscrobbler.com/2.0/")
.query(&map) .query(&map)
.header("User-Agent", "psysonic/1.13.0") .header("User-Agent", subsonic_wire_user_agent())
.send() .send()
.await .await
} else { } else {
client client
.post("https://ws.audioscrobbler.com/2.0/") .post("https://ws.audioscrobbler.com/2.0/")
.form(&map) .form(&map)
.header("User-Agent", "psysonic/1.13.0") .header("User-Agent", subsonic_wire_user_agent())
.send() .send()
.await .await
}.map_err(|e| e.to_string())?; }.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 _permit = dl_sem.acquire().await.map_err(|e| e.to_string())?;
let client = reqwest::Client::builder() let client = reqwest::Client::builder()
.user_agent(subsonic_wire_user_agent())
.timeout(std::time::Duration::from_secs(120)) .timeout(std::time::Duration::from_secs(120))
.build() .build()
.map_err(|e| e.to_string())?; .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 part_path = dest_dir.join(format!("{}.part", filename));
let client = reqwest::Client::builder() let client = reqwest::Client::builder()
.user_agent(subsonic_wire_user_agent())
.connect_timeout(Duration::from_secs(30)) .connect_timeout(Duration::from_secs(30))
.timeout(Duration::from_secs(3600)) .timeout(Duration::from_secs(3600))
.build() .build()
@@ -1198,6 +1242,7 @@ async fn download_update(url: String, filename: String, app: tauri::AppHandle) -
#[tauri::command] #[tauri::command]
async fn fetch_netease_lyrics(artist: String, title: String) -> Result<Option<String>, String> { async fn fetch_netease_lyrics(artist: String, title: String) -> Result<Option<String>, String> {
let client = reqwest::Client::builder() let client = reqwest::Client::builder()
.user_agent(subsonic_wire_user_agent())
.timeout(std::time::Duration::from_secs(8)) .timeout(std::time::Duration::from_secs(8))
.build() .build()
.map_err(|e| e.to_string())?; .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 params = [("s", query.as_str()), ("type", "1"), ("limit", "5")];
let search: serde_json::Value = client let search: serde_json::Value = client
.post("https://music.163.com/api/search/get") .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") .header("Referer", "https://music.163.com")
.form(&params) .form(&params)
.send() .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", "https://music.163.com/api/song/lyric?id={}&lv=1&kv=1&tv=-1",
song_id song_id
)) ))
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36")
.header("Referer", "https://music.163.com") .header("Referer", "https://music.163.com")
.send() .send()
.await .await
@@ -1401,6 +1444,7 @@ async fn download_zip(
const EMIT_INTERVAL: Duration = Duration::from_millis(500); const EMIT_INTERVAL: Duration = Duration::from_millis(500);
let client = reqwest::Client::builder() let client = reqwest::Client::builder()
.user_agent(subsonic_wire_user_agent())
.connect_timeout(Duration::from_secs(30)) .connect_timeout(Duration::from_secs(30))
.timeout(Duration::from_secs(7200)) // up to 2 h for large on-the-fly ZIPs .timeout(Duration::from_secs(7200)) // up to 2 h for large on-the-fly ZIPs
.build() .build()
@@ -1503,6 +1547,7 @@ async fn download_track_hot_cache(
} }
let client = reqwest::Client::builder() let client = reqwest::Client::builder()
.user_agent(subsonic_wire_user_agent())
.timeout(std::time::Duration::from_secs(120)) .timeout(std::time::Duration::from_secs(120))
.build() .build()
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
@@ -2007,6 +2052,7 @@ async fn sync_track_to_device(
} }
let client = reqwest::Client::builder() let client = reqwest::Client::builder()
.user_agent(subsonic_wire_user_agent())
.timeout(std::time::Duration::from_secs(300)) .timeout(std::time::Duration::from_secs(300))
.build() .build()
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
@@ -2192,6 +2238,7 @@ async fn calculate_sync_payload(
target_dir: String, target_dir: String,
) -> Result<SyncDeltaResult, String> { ) -> Result<SyncDeltaResult, String> {
let client = reqwest::Client::builder() let client = reqwest::Client::builder()
.user_agent(subsonic_wire_user_agent())
.timeout(std::time::Duration::from_secs(30)) .timeout(std::time::Duration::from_secs(30))
.build() .build()
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
@@ -2419,6 +2466,7 @@ async fn sync_batch_to_device(
// Shared reqwest client — reused across all downloads. // Shared reqwest client — reused across all downloads.
let client = reqwest::Client::builder() let client = reqwest::Client::builder()
.user_agent(subsonic_wire_user_agent())
.timeout(Duration::from_secs(300)) .timeout(Duration::from_secs(300))
.build() .build()
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
@@ -3355,6 +3403,7 @@ pub fn run() {
set_linux_webkit_smooth_scrolling, set_linux_webkit_smooth_scrolling,
set_logging_mode, set_logging_mode,
export_runtime_logs, export_runtime_logs,
set_subsonic_wire_user_agent,
no_compositing_mode, no_compositing_mode,
is_tiling_wm_cmd, is_tiling_wm_cmd,
open_mini_player, open_mini_player,
+7 -5
View File
@@ -9,6 +9,8 @@ import {
type SubsonicServerIdentity, type SubsonicServerIdentity,
} from '../utils/subsonicServerIdentity'; } from '../utils/subsonicServerIdentity';
const SUBSONIC_CLIENT = `psysonic/${version}`;
// ─── Secure random salt ──────────────────────────────────────── // ─── Secure random salt ────────────────────────────────────────
function secureRandomSalt(): string { function secureRandomSalt(): string {
const buf = new Uint8Array(8); const buf = new Uint8Array(8);
@@ -20,7 +22,7 @@ function secureRandomSalt(): string {
function getAuthParams(username: string, password: string) { function getAuthParams(username: string, password: string) {
const salt = secureRandomSalt(); const salt = secureRandomSalt();
const token = md5(password + salt); const token = md5(password + salt);
return { u: username, t: token, s: salt, v: '1.16.1', c: `psysonic/${version}`, f: 'json' }; return { u: username, t: token, s: salt, v: '1.16.1', c: SUBSONIC_CLIENT, f: 'json' };
} }
export function getClient() { export function getClient() {
@@ -287,7 +289,7 @@ export async function pingWithCredentials(
const salt = secureRandomSalt(); const salt = secureRandomSalt();
const token = md5(password + salt); const token = md5(password + salt);
const resp = await axios.get(`${base}/rest/ping.view`, { const resp = await axios.get(`${base}/rest/ping.view`, {
params: { u: username, t: token, s: salt, v: '1.16.1', c: 'psysonic', f: 'json' }, params: { u: username, t: token, s: salt, v: '1.16.1', c: SUBSONIC_CLIENT, f: 'json' },
paramsSerializer: { indexes: null }, paramsSerializer: { indexes: null },
timeout: 15000, timeout: 15000,
}); });
@@ -944,7 +946,7 @@ export function buildStreamUrl(id: string): string {
const p = new URLSearchParams({ const p = new URLSearchParams({
id, id,
u: server?.username ?? '', u: server?.username ?? '',
t: token, s: salt, v: '1.16.1', c: 'psysonic', f: 'json', t: token, s: salt, v: '1.16.1', c: SUBSONIC_CLIENT, f: 'json',
}); });
return `${baseUrl}/rest/stream.view?${p.toString()}`; return `${baseUrl}/rest/stream.view?${p.toString()}`;
} }
@@ -964,7 +966,7 @@ export function buildCoverArtUrl(id: string, size = 256): string {
const p = new URLSearchParams({ const p = new URLSearchParams({
id, size: String(size), id, size: String(size),
u: server?.username ?? '', u: server?.username ?? '',
t: token, s: salt, v: '1.16.1', c: 'psysonic', f: 'json', t: token, s: salt, v: '1.16.1', c: SUBSONIC_CLIENT, f: 'json',
}); });
return `${baseUrl}/rest/getCoverArt.view?${p.toString()}`; return `${baseUrl}/rest/getCoverArt.view?${p.toString()}`;
} }
@@ -978,7 +980,7 @@ export function buildDownloadUrl(id: string): string {
const p = new URLSearchParams({ const p = new URLSearchParams({
id, id,
u: server?.username ?? '', u: server?.username ?? '',
t: token, s: salt, v: '1.16.1', c: 'psysonic', f: 'json', t: token, s: salt, v: '1.16.1', c: SUBSONIC_CLIENT, f: 'json',
}); });
return `${baseUrl}/rest/download.view?${p.toString()}`; return `${baseUrl}/rest/download.view?${p.toString()}`;
} }
+14
View File
@@ -1,5 +1,6 @@
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import { invoke } from '@tauri-apps/api/core';
import { getCurrentWindow } from '@tauri-apps/api/window'; import { getCurrentWindow } from '@tauri-apps/api/window';
import App from './App'; import App from './App';
import './i18n'; import './i18n';
@@ -15,6 +16,19 @@ try {
(window as any).__PSY_WINDOW_LABEL__ = 'main'; (window as any).__PSY_WINDOW_LABEL__ = 'main';
} }
// Sync backend HTTP User-Agent from the main webview once at startup.
try {
const windowLabel = (window as any).__PSY_WINDOW_LABEL__ ?? 'main';
if (windowLabel === 'main') {
const ua = window.navigator.userAgent?.trim();
if (ua) {
void invoke('set_subsonic_wire_user_agent', { userAgent: ua, windowLabel });
}
}
} catch {
// Ignore in non-Tauri runtimes.
}
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<App /> <App />