mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
refactor: extract psysonic-integration crate (M5/7)
Moves all outbound external-service bridges out of the top crate into a
new psysonic-integration crate.
crates/psysonic-integration/
src/discord.rs Rich Presence + iTunes artwork
src/navidrome/{client,covers,...} Native REST API admin (5 modules)
src/remote.rs radio-browser, last.fm, ICY meta,
generic CORS proxy (fetch_url_bytes
/ fetch_json_url / resolve_stream_url)
src/bandsintown.rs events for an artist
src/lib.rs module declarations + macro re-exports
Top crate keeps `crate::discord` working via
`pub use psysonic_integration::discord;`.
invoke_handler! in lib.rs uses full deepest paths
(`psysonic_integration::navidrome::users::nd_list_users`, etc.) so
Tauri's `__cmd__*` magic macros resolve at the right module — same
pattern audio + syncfs already use.
Cross-crate refs migrated:
crate::subsonic_wire_user_agent → psysonic_core::user_agent::*
pub(crate) → pub (cross-crate visibility)
Behaviour preserving. Cargo check + clippy --workspace clean.
This commit is contained in:
Generated
+17
@@ -3611,6 +3611,7 @@ dependencies = [
|
||||
"psysonic-analysis",
|
||||
"psysonic-audio",
|
||||
"psysonic-core",
|
||||
"psysonic-integration",
|
||||
"psysonic-syncfs",
|
||||
"reqwest",
|
||||
"ringbuf",
|
||||
@@ -3695,6 +3696,22 @@ dependencies = [
|
||||
"tauri",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psysonic-integration"
|
||||
version = "1.46.0-dev"
|
||||
dependencies = [
|
||||
"discord-rich-presence",
|
||||
"futures-util",
|
||||
"md5",
|
||||
"psysonic-core",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psysonic-syncfs"
|
||||
version = "1.46.0-dev"
|
||||
|
||||
@@ -34,6 +34,7 @@ psysonic-core = { path = "crates/psysonic-core" }
|
||||
psysonic-analysis = { path = "crates/psysonic-analysis" }
|
||||
psysonic-audio = { path = "crates/psysonic-audio" }
|
||||
psysonic-syncfs = { path = "crates/psysonic-syncfs" }
|
||||
psysonic-integration = { path = "crates/psysonic-integration" }
|
||||
tauri = { version = "2", features = ["tray-icon", "image-png"] }
|
||||
tauri-plugin-single-instance = "2"
|
||||
tauri-plugin-shell = "2"
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "psysonic-integration"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
psysonic-core = { path = "../psysonic-core" }
|
||||
|
||||
tauri = { version = "2" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["rt", "time", "sync"] }
|
||||
reqwest = { version = "0.13", default-features = false, features = ["stream", "json", "rustls", "blocking"] }
|
||||
futures-util = "0.3"
|
||||
discord-rich-presence = "1.1"
|
||||
url = "2"
|
||||
md5 = "0.8"
|
||||
+3
-3
@@ -2,10 +2,10 @@
|
||||
// Public REST API: https://rest.bandsintown.com/artists/{name}/events?app_id=…
|
||||
// Bandsintown whitelists app IDs — arbitrary strings now return 403 Forbidden.
|
||||
// `js_app_id` is the ID their own embeddable widget uses and is broadly accepted.
|
||||
pub(crate) const BANDSINTOWN_APP_ID: &str = "js_app_id";
|
||||
pub const BANDSINTOWN_APP_ID: &str = "js_app_id";
|
||||
|
||||
#[derive(serde::Serialize, Default)]
|
||||
pub(crate) struct BandsintownEvent {
|
||||
pub struct BandsintownEvent {
|
||||
datetime: String, // ISO 8601 (e.g. "2026-04-23T20:30:00")
|
||||
venue_name: String,
|
||||
venue_city: String,
|
||||
@@ -20,7 +20,7 @@ pub(crate) struct BandsintownEvent {
|
||||
/// Returns an empty list on any failure (404, network, parse) — the UI
|
||||
/// just hides the section in that case.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn fetch_bandsintown_events(artist_name: String) -> Result<Vec<BandsintownEvent>, String> {
|
||||
pub async fn fetch_bandsintown_events(artist_name: String) -> Result<Vec<BandsintownEvent>, String> {
|
||||
let trimmed = artist_name.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Ok(vec![]);
|
||||
@@ -0,0 +1,17 @@
|
||||
//! `psysonic-integration` — outbound bridges to external services that the
|
||||
//! UI surfaces but the WebView can't talk to directly (CORS, custom auth, etc.).
|
||||
//!
|
||||
//! Domains:
|
||||
//! - `discord` — Discord Rich Presence (album artwork via iTunes)
|
||||
//! - `navidrome` — Navidrome's native REST API (admin: users/playlists/covers/queries)
|
||||
//! - `remote` — radio-browser, last.fm, ICY-meta probe, generic CORS proxy
|
||||
//! - `bandsintown` — bandsintown events for an artist
|
||||
|
||||
// Re-export the logging facade so submodules keep using
|
||||
// `crate::app_eprintln!()` and `crate::app_deprintln!()`.
|
||||
pub use psysonic_core::{app_deprintln, app_eprintln, logging};
|
||||
|
||||
pub mod bandsintown;
|
||||
pub mod discord;
|
||||
pub mod navidrome;
|
||||
pub mod remote;
|
||||
+5
-5
@@ -2,7 +2,7 @@
|
||||
//! Used by every other navidrome submodule for `/auth/*` and `/api/*` calls.
|
||||
|
||||
/// Authenticate with Navidrome's own REST API and return a Bearer token.
|
||||
pub(crate) async fn navidrome_token(server_url: &str, username: &str, password: &str) -> Result<String, String> {
|
||||
pub async fn navidrome_token(server_url: &str, username: &str, password: &str) -> Result<String, String> {
|
||||
let client = reqwest::Client::new();
|
||||
let resp = client
|
||||
.post(format!("{}/auth/login", server_url))
|
||||
@@ -19,7 +19,7 @@ pub(crate) async fn navidrome_token(server_url: &str, username: &str, password:
|
||||
|
||||
/// Payload returned by Navidrome's `/auth/login`.
|
||||
#[derive(serde::Serialize)]
|
||||
pub(crate) struct NdLoginResult {
|
||||
pub struct NdLoginResult {
|
||||
pub(super) token: String,
|
||||
#[serde(rename = "userId")]
|
||||
pub(super) user_id: String,
|
||||
@@ -31,7 +31,7 @@ pub(crate) struct NdLoginResult {
|
||||
/// frontend toasts can show the actual transport cause (connection refused,
|
||||
/// tls handshake fail, cert expired, etc.) instead of reqwest's opaque
|
||||
/// "error sending request for url (…)" wrapper.
|
||||
pub(crate) fn nd_err(e: reqwest::Error) -> String {
|
||||
pub fn nd_err(e: reqwest::Error) -> String {
|
||||
let mut msg = e.to_string();
|
||||
let mut src: Option<&(dyn std::error::Error + 'static)> = std::error::Error::source(&e);
|
||||
while let Some(s) = src {
|
||||
@@ -50,7 +50,7 @@ pub(crate) fn nd_err(e: reqwest::Error) -> String {
|
||||
/// turns a transient glitch into a visible outage. Status-level failures
|
||||
/// (401/403/400 with body) return immediately — we don't retry logic
|
||||
/// errors.
|
||||
pub(crate) async fn nd_retry<F, Fut>(mut build_and_send: F) -> Result<reqwest::Response, String>
|
||||
pub async fn nd_retry<F, Fut>(mut build_and_send: F) -> Result<reqwest::Response, String>
|
||||
where
|
||||
F: FnMut() -> Fut,
|
||||
Fut: std::future::Future<Output = Result<reqwest::Response, reqwest::Error>>,
|
||||
@@ -90,7 +90,7 @@ where
|
||||
/// keep-alive connections in the pool caused intermittent "tls handshake
|
||||
/// eof" errors on the second call to an admin endpoint when a server or
|
||||
/// proxy had already closed the TCP connection between calls.
|
||||
pub(crate) fn nd_http_client() -> reqwest::Client {
|
||||
pub fn nd_http_client() -> reqwest::Client {
|
||||
// TLS 1.2 only: rustls + nginx with TLS-1.3 session resumption caches
|
||||
// produces intermittent ECONNRESET mid-handshake when the upstream
|
||||
// starts churning keep-alive connections. Pinning TLS 1.2 matches what
|
||||
+4
-4
@@ -5,7 +5,7 @@
|
||||
use super::client::navidrome_token;
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn upload_playlist_cover(
|
||||
pub async fn upload_playlist_cover(
|
||||
server_url: String,
|
||||
playlist_id: String,
|
||||
username: String,
|
||||
@@ -32,7 +32,7 @@ pub(crate) async fn upload_playlist_cover(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn upload_radio_cover(
|
||||
pub async fn upload_radio_cover(
|
||||
server_url: String,
|
||||
radio_id: String,
|
||||
username: String,
|
||||
@@ -59,7 +59,7 @@ pub(crate) async fn upload_radio_cover(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn upload_artist_image(
|
||||
pub async fn upload_artist_image(
|
||||
server_url: String,
|
||||
artist_id: String,
|
||||
username: String,
|
||||
@@ -86,7 +86,7 @@ pub(crate) async fn upload_artist_image(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn delete_radio_cover(
|
||||
pub async fn delete_radio_cover(
|
||||
server_url: String,
|
||||
radio_id: String,
|
||||
username: String,
|
||||
@@ -0,0 +1,11 @@
|
||||
//! Navidrome native REST API: split into a small client/auth/retry core
|
||||
//! plus per-domain submodules (covers, users, queries, playlists). Each
|
||||
//! Tauri command goes through `nd_http_client()` + `nd_retry()` so flaky
|
||||
//! reverse proxies in front of the server don't surface as user-visible
|
||||
//! transport errors on a single retry-able blip.
|
||||
|
||||
mod client;
|
||||
pub mod covers;
|
||||
pub mod playlists;
|
||||
pub mod queries;
|
||||
pub mod users;
|
||||
+5
-5
@@ -6,7 +6,7 @@ use super::client::{nd_err, nd_http_client, nd_retry};
|
||||
|
||||
/// GET `/api/playlist` — list playlists; pass `smart=true` to filter smart playlists.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn nd_list_playlists(
|
||||
pub async fn nd_list_playlists(
|
||||
server_url: String,
|
||||
token: String,
|
||||
smart: Option<bool>,
|
||||
@@ -30,7 +30,7 @@ pub(crate) async fn nd_list_playlists(
|
||||
|
||||
/// POST `/api/playlist` — create playlist (supports smart rules payload).
|
||||
#[tauri::command]
|
||||
pub(crate) async fn nd_create_playlist(
|
||||
pub async fn nd_create_playlist(
|
||||
server_url: String,
|
||||
token: String,
|
||||
body: serde_json::Value,
|
||||
@@ -53,7 +53,7 @@ pub(crate) async fn nd_create_playlist(
|
||||
|
||||
/// PUT `/api/playlist/{id}` — update playlist (supports smart rules payload).
|
||||
#[tauri::command]
|
||||
pub(crate) async fn nd_update_playlist(
|
||||
pub async fn nd_update_playlist(
|
||||
server_url: String,
|
||||
token: String,
|
||||
id: String,
|
||||
@@ -77,7 +77,7 @@ pub(crate) async fn nd_update_playlist(
|
||||
|
||||
/// GET `/api/playlist/{id}` — get a single playlist (includes smart rules if available).
|
||||
#[tauri::command]
|
||||
pub(crate) async fn nd_get_playlist(
|
||||
pub async fn nd_get_playlist(
|
||||
server_url: String,
|
||||
token: String,
|
||||
id: String,
|
||||
@@ -99,7 +99,7 @@ pub(crate) async fn nd_get_playlist(
|
||||
|
||||
/// DELETE `/api/playlist/{id}` — delete playlist.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn nd_delete_playlist(
|
||||
pub async fn nd_delete_playlist(
|
||||
server_url: String,
|
||||
token: String,
|
||||
id: String,
|
||||
+6
-6
@@ -7,7 +7,7 @@ use super::client::{navidrome_token, nd_err, nd_http_client, nd_retry};
|
||||
/// GET `/api/song?_sort=...&_order=...&_start=...&_end=...` — paginated song list.
|
||||
/// Available to any authenticated user (no admin required). Returns raw JSON array.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn nd_list_songs(
|
||||
pub async fn nd_list_songs(
|
||||
server_url: String,
|
||||
token: String,
|
||||
sort: String,
|
||||
@@ -53,7 +53,7 @@ fn nd_build_filters(seed: serde_json::Map<String, serde_json::Value>, library_id
|
||||
/// Navidrome 0.55.0+ (uses `library_artist.stats` JSON aggregate). Available to any
|
||||
/// authenticated user. Returns raw JSON array.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn nd_list_artists_by_role(
|
||||
pub async fn nd_list_artists_by_role(
|
||||
server_url: String,
|
||||
token: String,
|
||||
role: String,
|
||||
@@ -93,7 +93,7 @@ pub(crate) async fn nd_list_artists_by_role(
|
||||
/// (or conductor-only, lyricist-only, …) credits are unreachable there. Navidrome
|
||||
/// generates `role_<role>_id` filters dynamically from `model.AllRoles`.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn nd_list_albums_by_artist_role(
|
||||
pub async fn nd_list_albums_by_artist_role(
|
||||
server_url: String,
|
||||
token: String,
|
||||
artist_id: String,
|
||||
@@ -131,7 +131,7 @@ pub(crate) async fn nd_list_albums_by_artist_role(
|
||||
|
||||
/// GET `/api/library` — list all libraries (admin only). Returns the raw JSON array.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn nd_list_libraries(
|
||||
pub async fn nd_list_libraries(
|
||||
server_url: String,
|
||||
token: String,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
@@ -150,7 +150,7 @@ pub(crate) async fn nd_list_libraries(
|
||||
/// PUT `/api/user/{id}/library` — assign libraries to a non-admin user.
|
||||
/// Admin users auto-receive all libraries; calling this for an admin returns HTTP 400.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn nd_set_user_libraries(
|
||||
pub async fn nd_set_user_libraries(
|
||||
server_url: String,
|
||||
token: String,
|
||||
id: String,
|
||||
@@ -184,7 +184,7 @@ pub(crate) async fn nd_set_user_libraries(
|
||||
/// Returns `Ok(None)` when the response has no `path` field — Navidrome can omit
|
||||
/// it for non-admin users on some configurations.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn nd_get_song_path(
|
||||
pub async fn nd_get_song_path(
|
||||
server_url: String,
|
||||
username: String,
|
||||
password: String,
|
||||
+5
-5
@@ -6,7 +6,7 @@ use super::client::{nd_err, nd_http_client, nd_retry, NdLoginResult};
|
||||
|
||||
/// Log in to Navidrome's native REST API. Returns a Bearer token and whether the user is admin.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn navidrome_login(
|
||||
pub async fn navidrome_login(
|
||||
server_url: String,
|
||||
username: String,
|
||||
password: String,
|
||||
@@ -30,7 +30,7 @@ pub(crate) async fn navidrome_login(
|
||||
|
||||
/// GET `/api/user` — admin only. Returns the raw JSON array verbatim so the frontend can pick fields.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn nd_list_users(
|
||||
pub async fn nd_list_users(
|
||||
server_url: String,
|
||||
token: String,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
@@ -48,7 +48,7 @@ pub(crate) async fn nd_list_users(
|
||||
|
||||
/// POST `/api/user` — create a user.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn nd_create_user(
|
||||
pub async fn nd_create_user(
|
||||
server_url: String,
|
||||
token: String,
|
||||
user_name: String,
|
||||
@@ -81,7 +81,7 @@ pub(crate) async fn nd_create_user(
|
||||
|
||||
/// PUT `/api/user/{id}` — update a user. Pass an empty `password` to leave it unchanged.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn nd_update_user(
|
||||
pub async fn nd_update_user(
|
||||
server_url: String,
|
||||
token: String,
|
||||
id: String,
|
||||
@@ -118,7 +118,7 @@ pub(crate) async fn nd_update_user(
|
||||
|
||||
/// DELETE `/api/user/{id}`.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn nd_delete_user(
|
||||
pub async fn nd_delete_user(
|
||||
server_url: String,
|
||||
token: String,
|
||||
id: String,
|
||||
+13
-13
@@ -1,10 +1,10 @@
|
||||
use crate::subsonic_wire_user_agent;
|
||||
use psysonic_core::user_agent::subsonic_wire_user_agent;
|
||||
|
||||
pub(crate) const RADIO_PAGE_SIZE: u32 = 25;
|
||||
pub const RADIO_PAGE_SIZE: u32 = 25;
|
||||
|
||||
/// Search the radio-browser.info directory (needs User-Agent header — CORS would block WebView).
|
||||
#[tauri::command]
|
||||
pub(crate) async fn search_radio_browser(query: String, offset: u32) -> Result<Vec<serde_json::Value>, String> {
|
||||
pub async fn search_radio_browser(query: String, offset: u32) -> Result<Vec<serde_json::Value>, String> {
|
||||
let client = reqwest::Client::new();
|
||||
let limit_s = RADIO_PAGE_SIZE.to_string();
|
||||
let offset_s = offset.to_string();
|
||||
@@ -27,7 +27,7 @@ pub(crate) async fn search_radio_browser(query: String, offset: u32) -> Result<V
|
||||
|
||||
/// Fetch top-voted stations from radio-browser.info for initial suggestions.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn get_top_radio_stations(offset: u32) -> Result<Vec<serde_json::Value>, String> {
|
||||
pub async fn get_top_radio_stations(offset: u32) -> Result<Vec<serde_json::Value>, String> {
|
||||
let client = reqwest::Client::new();
|
||||
let limit_s = RADIO_PAGE_SIZE.to_string();
|
||||
let offset_s = offset.to_string();
|
||||
@@ -46,7 +46,7 @@ pub(crate) async fn get_top_radio_stations(offset: u32) -> Result<Vec<serde_json
|
||||
/// Fetch arbitrary URL bytes (e.g. radio station favicon) through Rust to bypass CORS.
|
||||
/// Returns (bytes, content_type).
|
||||
#[tauri::command]
|
||||
pub(crate) async fn fetch_url_bytes(url: String) -> Result<(Vec<u8>, String), String> {
|
||||
pub 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))
|
||||
@@ -76,7 +76,7 @@ pub(crate) async fn fetch_url_bytes(url: String) -> Result<(Vec<u8>, String), St
|
||||
/// Fetch a JSON API endpoint through Rust to bypass CORS/WebView networking restrictions.
|
||||
/// Returns the response body as a UTF-8 string for parsing on the JS side.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn fetch_json_url(url: String) -> Result<String, String> {
|
||||
pub 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))
|
||||
@@ -96,7 +96,7 @@ pub(crate) async fn fetch_json_url(url: String) -> Result<String, String> {
|
||||
|
||||
/// ICY metadata response returned to the frontend.
|
||||
#[derive(serde::Serialize)]
|
||||
pub(crate) struct IcyMetadata {
|
||||
pub struct IcyMetadata {
|
||||
/// The `StreamTitle` from the inline ICY metadata block in the stream (e.g. `"Artist - Title"`).
|
||||
stream_title: Option<String>,
|
||||
/// Value of the `icy-name` response header.
|
||||
@@ -110,7 +110,7 @@ pub(crate) struct IcyMetadata {
|
||||
}
|
||||
|
||||
/// Extract the first `File1=` stream URL from a PLS playlist file.
|
||||
pub(crate) fn parse_pls_stream_url(content: &str) -> Option<String> {
|
||||
pub fn parse_pls_stream_url(content: &str) -> Option<String> {
|
||||
content.lines()
|
||||
.map(str::trim)
|
||||
.find(|l| l.to_lowercase().starts_with("file1="))
|
||||
@@ -122,7 +122,7 @@ pub(crate) fn parse_pls_stream_url(content: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
/// Extract the first non-comment HTTP URL from an M3U/M3U8 playlist file.
|
||||
pub(crate) fn parse_m3u_stream_url(content: &str) -> Option<String> {
|
||||
pub fn parse_m3u_stream_url(content: &str) -> Option<String> {
|
||||
content.lines()
|
||||
.map(str::trim)
|
||||
.find(|l| !l.is_empty() && !l.starts_with('#')
|
||||
@@ -132,7 +132,7 @@ pub(crate) fn parse_m3u_stream_url(content: &str) -> Option<String> {
|
||||
|
||||
/// If `url` points to a PLS or M3U playlist, fetch it and return the first
|
||||
/// stream URL it contains. Returns `None` for direct stream URLs.
|
||||
pub(crate) async fn resolve_playlist_url(client: &reqwest::Client, url: &str) -> Option<String> {
|
||||
pub async fn resolve_playlist_url(client: &reqwest::Client, url: &str) -> Option<String> {
|
||||
let path = url.split('?').next().unwrap_or(url).to_lowercase();
|
||||
let is_pls = path.ends_with(".pls");
|
||||
let is_m3u = path.ends_with(".m3u") || path.ends_with(".m3u8");
|
||||
@@ -173,7 +173,7 @@ pub(crate) async fn resolve_playlist_url(client: &reqwest::Client, url: &str) ->
|
||||
/// If `url` is a PLS or M3U playlist file it is resolved to the first direct
|
||||
/// stream URL before the ICY request is made.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn fetch_icy_metadata(url: String) -> Result<IcyMetadata, String> {
|
||||
pub async fn fetch_icy_metadata(url: String) -> Result<IcyMetadata, String> {
|
||||
use futures_util::StreamExt;
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
@@ -277,7 +277,7 @@ pub(crate) async fn fetch_icy_metadata(url: String) -> Result<IcyMetadata, Strin
|
||||
/// Returns the original URL unchanged if it is not a recognised playlist format
|
||||
/// or if the playlist cannot be fetched/parsed.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn resolve_stream_url(url: String) -> String {
|
||||
pub async fn resolve_stream_url(url: String) -> String {
|
||||
let Ok(client) = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
@@ -291,7 +291,7 @@ pub(crate) async fn resolve_stream_url(url: String) -> String {
|
||||
/// `params` is a list of [key, value] pairs (method must be included).
|
||||
/// If `sign` is true an api_sig is computed. If `get` is true, a GET request is made.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn lastfm_request(
|
||||
pub async fn lastfm_request(
|
||||
params: Vec<[String; 2]>,
|
||||
sign: bool,
|
||||
get: bool,
|
||||
+32
-31
@@ -2,9 +2,10 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
pub mod cli;
|
||||
mod discord;
|
||||
mod lib_commands;
|
||||
|
||||
pub use psysonic_integration::discord;
|
||||
|
||||
pub use psysonic_core::logging;
|
||||
pub use psysonic_core::{app_eprintln, app_deprintln};
|
||||
pub use psysonic_core::user_agent::{
|
||||
@@ -391,35 +392,35 @@ pub fn run() {
|
||||
audio::device_commands::audio_default_output_device_name,
|
||||
audio::device_commands::audio_set_device,
|
||||
audio::commands::audio_chain_preload,
|
||||
discord::discord_update_presence,
|
||||
discord::discord_clear_presence,
|
||||
lastfm_request,
|
||||
upload_playlist_cover,
|
||||
upload_radio_cover,
|
||||
upload_artist_image,
|
||||
delete_radio_cover,
|
||||
navidrome_login,
|
||||
nd_list_users,
|
||||
nd_create_user,
|
||||
nd_update_user,
|
||||
nd_delete_user,
|
||||
nd_list_libraries,
|
||||
nd_list_songs,
|
||||
nd_list_artists_by_role,
|
||||
nd_list_albums_by_artist_role,
|
||||
nd_set_user_libraries,
|
||||
nd_list_playlists,
|
||||
nd_create_playlist,
|
||||
nd_update_playlist,
|
||||
nd_get_playlist,
|
||||
nd_delete_playlist,
|
||||
nd_get_song_path,
|
||||
search_radio_browser,
|
||||
get_top_radio_stations,
|
||||
fetch_url_bytes,
|
||||
fetch_json_url,
|
||||
fetch_icy_metadata,
|
||||
resolve_stream_url,
|
||||
psysonic_integration::discord::discord_update_presence,
|
||||
psysonic_integration::discord::discord_clear_presence,
|
||||
psysonic_integration::remote::lastfm_request,
|
||||
psysonic_integration::navidrome::covers::upload_playlist_cover,
|
||||
psysonic_integration::navidrome::covers::upload_radio_cover,
|
||||
psysonic_integration::navidrome::covers::upload_artist_image,
|
||||
psysonic_integration::navidrome::covers::delete_radio_cover,
|
||||
psysonic_integration::navidrome::users::navidrome_login,
|
||||
psysonic_integration::navidrome::users::nd_list_users,
|
||||
psysonic_integration::navidrome::users::nd_create_user,
|
||||
psysonic_integration::navidrome::users::nd_update_user,
|
||||
psysonic_integration::navidrome::users::nd_delete_user,
|
||||
psysonic_integration::navidrome::queries::nd_list_libraries,
|
||||
psysonic_integration::navidrome::queries::nd_list_songs,
|
||||
psysonic_integration::navidrome::queries::nd_list_artists_by_role,
|
||||
psysonic_integration::navidrome::queries::nd_list_albums_by_artist_role,
|
||||
psysonic_integration::navidrome::queries::nd_set_user_libraries,
|
||||
psysonic_integration::navidrome::playlists::nd_list_playlists,
|
||||
psysonic_integration::navidrome::playlists::nd_create_playlist,
|
||||
psysonic_integration::navidrome::playlists::nd_update_playlist,
|
||||
psysonic_integration::navidrome::playlists::nd_get_playlist,
|
||||
psysonic_integration::navidrome::playlists::nd_delete_playlist,
|
||||
psysonic_integration::navidrome::queries::nd_get_song_path,
|
||||
psysonic_integration::remote::search_radio_browser,
|
||||
psysonic_integration::remote::get_top_radio_stations,
|
||||
psysonic_integration::remote::fetch_url_bytes,
|
||||
psysonic_integration::remote::fetch_json_url,
|
||||
psysonic_integration::remote::fetch_icy_metadata,
|
||||
psysonic_integration::remote::resolve_stream_url,
|
||||
analysis_get_waveform,
|
||||
analysis_get_waveform_for_track,
|
||||
analysis_get_loudness_for_track,
|
||||
@@ -457,7 +458,7 @@ pub fn run() {
|
||||
psysonic_syncfs::cache::downloads::open_folder,
|
||||
psysonic_syncfs::cache::downloads::get_embedded_lyrics,
|
||||
psysonic_syncfs::cache::downloads::fetch_netease_lyrics,
|
||||
fetch_bandsintown_events,
|
||||
psysonic_integration::bandsintown::fetch_bandsintown_events,
|
||||
#[cfg(target_os = "windows")]
|
||||
taskbar_win::update_taskbar_icon,
|
||||
])
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
mod analysis;
|
||||
mod cli_bridge;
|
||||
mod core;
|
||||
mod navidrome;
|
||||
mod integration;
|
||||
mod perf;
|
||||
mod platform;
|
||||
mod remote;
|
||||
mod integration;
|
||||
mod analysis;
|
||||
|
||||
// Tauri commands re-exported for the lib.rs invoke_handler.
|
||||
pub(crate) use cli_bridge::{
|
||||
@@ -16,19 +14,8 @@ pub(crate) use core::{
|
||||
exit_app, export_runtime_logs, frontend_debug_log, greet, set_logging_mode,
|
||||
set_subsonic_wire_user_agent,
|
||||
};
|
||||
pub(crate) use navidrome::{
|
||||
delete_radio_cover, navidrome_login, nd_create_playlist, nd_create_user, nd_delete_playlist,
|
||||
nd_delete_user, nd_get_playlist, nd_get_song_path, nd_list_albums_by_artist_role,
|
||||
nd_list_artists_by_role, nd_list_libraries, nd_list_playlists, nd_list_songs, nd_list_users,
|
||||
nd_set_user_libraries, nd_update_playlist, nd_update_user, upload_artist_image,
|
||||
upload_playlist_cover, upload_radio_cover,
|
||||
};
|
||||
pub(crate) use perf::performance_cpu_snapshot;
|
||||
pub(crate) use platform::{set_linux_webkit_smooth_scrolling, set_window_decorations};
|
||||
pub(crate) use remote::{
|
||||
fetch_icy_metadata, fetch_json_url, fetch_url_bytes, get_top_radio_stations, lastfm_request,
|
||||
resolve_stream_url, search_radio_browser,
|
||||
};
|
||||
pub(crate) use integration::{
|
||||
check_dir_accessible, mpris_set_metadata, mpris_set_playback, register_global_shortcut,
|
||||
unregister_global_shortcut,
|
||||
@@ -38,3 +25,7 @@ pub(crate) use analysis::{
|
||||
analysis_enqueue_seed_from_url, analysis_get_loudness_for_track, analysis_get_waveform,
|
||||
analysis_get_waveform_for_track, analysis_prune_pending_to_track_ids,
|
||||
};
|
||||
|
||||
// Discord, Navidrome admin, last.fm + radio-browser + CORS proxy, bandsintown
|
||||
// now live in `psysonic_integration`. invoke_handler! in lib.rs registers
|
||||
// them with their full paths so Tauri's `__cmd__*` macros resolve correctly.
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
//! Navidrome native REST API: split into a small client/auth/retry core
|
||||
//! plus per-domain submodules (covers, users, queries, playlists). Each
|
||||
//! Tauri command goes through `nd_http_client()` + `nd_retry()` so flaky
|
||||
//! reverse proxies in front of the server don't surface as user-visible
|
||||
//! transport errors on a single retry-able blip.
|
||||
|
||||
mod client;
|
||||
mod covers;
|
||||
mod users;
|
||||
mod queries;
|
||||
mod playlists;
|
||||
|
||||
// Re-export only the Tauri commands — `client` items (nd_http_client,
|
||||
// nd_retry, navidrome_token, NdLoginResult, nd_err) are internal helpers
|
||||
// used by the other submodules and don't need crate-wide visibility.
|
||||
pub(crate) use covers::{
|
||||
delete_radio_cover, upload_artist_image, upload_playlist_cover, upload_radio_cover,
|
||||
};
|
||||
pub(crate) use users::{
|
||||
navidrome_login, nd_create_user, nd_delete_user, nd_list_users, nd_update_user,
|
||||
};
|
||||
pub(crate) use queries::{
|
||||
nd_get_song_path, nd_list_albums_by_artist_role, nd_list_artists_by_role, nd_list_libraries,
|
||||
nd_list_songs, nd_set_user_libraries,
|
||||
};
|
||||
pub(crate) use playlists::{
|
||||
nd_create_playlist, nd_delete_playlist, nd_get_playlist, nd_list_playlists, nd_update_playlist,
|
||||
};
|
||||
@@ -1,9 +1,8 @@
|
||||
mod mini;
|
||||
mod bandsintown;
|
||||
|
||||
pub(crate) use mini::{
|
||||
close_mini_player, open_mini_player, pause_rendering, persist_mini_pos_throttled,
|
||||
preload_mini_player, resize_mini_player, resume_rendering, set_mini_player_always_on_top,
|
||||
show_main_window, PAUSE_RENDERING_JS, RESUME_RENDERING_JS,
|
||||
};
|
||||
pub(crate) use bandsintown::fetch_bandsintown_events;
|
||||
// Bandsintown moved to psysonic-integration; lib.rs uses the full path.
|
||||
|
||||
Reference in New Issue
Block a user