diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b07161ff..6588d221 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -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" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6e876909..fc39b8ff 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -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" diff --git a/src-tauri/crates/psysonic-integration/Cargo.toml b/src-tauri/crates/psysonic-integration/Cargo.toml new file mode 100644 index 00000000..428be40f --- /dev/null +++ b/src-tauri/crates/psysonic-integration/Cargo.toml @@ -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" diff --git a/src-tauri/src/lib_commands/ui/bandsintown.rs b/src-tauri/crates/psysonic-integration/src/bandsintown.rs similarity index 94% rename from src-tauri/src/lib_commands/ui/bandsintown.rs rename to src-tauri/crates/psysonic-integration/src/bandsintown.rs index 21c01088..abd85c21 100644 --- a/src-tauri/src/lib_commands/ui/bandsintown.rs +++ b/src-tauri/crates/psysonic-integration/src/bandsintown.rs @@ -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, String> { +pub async fn fetch_bandsintown_events(artist_name: String) -> Result, String> { let trimmed = artist_name.trim(); if trimmed.is_empty() { return Ok(vec![]); diff --git a/src-tauri/src/discord.rs b/src-tauri/crates/psysonic-integration/src/discord.rs similarity index 100% rename from src-tauri/src/discord.rs rename to src-tauri/crates/psysonic-integration/src/discord.rs diff --git a/src-tauri/crates/psysonic-integration/src/lib.rs b/src-tauri/crates/psysonic-integration/src/lib.rs new file mode 100644 index 00000000..3182b29c --- /dev/null +++ b/src-tauri/crates/psysonic-integration/src/lib.rs @@ -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; diff --git a/src-tauri/src/lib_commands/app_api/navidrome/client.rs b/src-tauri/crates/psysonic-integration/src/navidrome/client.rs similarity index 92% rename from src-tauri/src/lib_commands/app_api/navidrome/client.rs rename to src-tauri/crates/psysonic-integration/src/navidrome/client.rs index 8a706159..5b40ec86 100644 --- a/src-tauri/src/lib_commands/app_api/navidrome/client.rs +++ b/src-tauri/crates/psysonic-integration/src/navidrome/client.rs @@ -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 { +pub async fn navidrome_token(server_url: &str, username: &str, password: &str) -> Result { 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(mut build_and_send: F) -> Result +pub async fn nd_retry(mut build_and_send: F) -> Result where F: FnMut() -> Fut, Fut: std::future::Future>, @@ -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 diff --git a/src-tauri/src/lib_commands/app_api/navidrome/covers.rs b/src-tauri/crates/psysonic-integration/src/navidrome/covers.rs similarity index 95% rename from src-tauri/src/lib_commands/app_api/navidrome/covers.rs rename to src-tauri/crates/psysonic-integration/src/navidrome/covers.rs index c061b3ce..7ea0162f 100644 --- a/src-tauri/src/lib_commands/app_api/navidrome/covers.rs +++ b/src-tauri/crates/psysonic-integration/src/navidrome/covers.rs @@ -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, diff --git a/src-tauri/crates/psysonic-integration/src/navidrome/mod.rs b/src-tauri/crates/psysonic-integration/src/navidrome/mod.rs new file mode 100644 index 00000000..d838a950 --- /dev/null +++ b/src-tauri/crates/psysonic-integration/src/navidrome/mod.rs @@ -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; diff --git a/src-tauri/src/lib_commands/app_api/navidrome/playlists.rs b/src-tauri/crates/psysonic-integration/src/navidrome/playlists.rs similarity index 94% rename from src-tauri/src/lib_commands/app_api/navidrome/playlists.rs rename to src-tauri/crates/psysonic-integration/src/navidrome/playlists.rs index cd4d9f23..2d2d9c36 100644 --- a/src-tauri/src/lib_commands/app_api/navidrome/playlists.rs +++ b/src-tauri/crates/psysonic-integration/src/navidrome/playlists.rs @@ -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, @@ -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, diff --git a/src-tauri/src/lib_commands/app_api/navidrome/queries.rs b/src-tauri/crates/psysonic-integration/src/navidrome/queries.rs similarity index 96% rename from src-tauri/src/lib_commands/app_api/navidrome/queries.rs rename to src-tauri/crates/psysonic-integration/src/navidrome/queries.rs index 193f3977..58ff89f2 100644 --- a/src-tauri/src/lib_commands/app_api/navidrome/queries.rs +++ b/src-tauri/crates/psysonic-integration/src/navidrome/queries.rs @@ -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, 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__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 { @@ -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, diff --git a/src-tauri/src/lib_commands/app_api/navidrome/users.rs b/src-tauri/crates/psysonic-integration/src/navidrome/users.rs similarity index 95% rename from src-tauri/src/lib_commands/app_api/navidrome/users.rs rename to src-tauri/crates/psysonic-integration/src/navidrome/users.rs index 572f574a..90b501d4 100644 --- a/src-tauri/src/lib_commands/app_api/navidrome/users.rs +++ b/src-tauri/crates/psysonic-integration/src/navidrome/users.rs @@ -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 { @@ -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, diff --git a/src-tauri/src/lib_commands/app_api/remote.rs b/src-tauri/crates/psysonic-integration/src/remote.rs similarity index 92% rename from src-tauri/src/lib_commands/app_api/remote.rs rename to src-tauri/crates/psysonic-integration/src/remote.rs index a9bbe185..11a6c896 100644 --- a/src-tauri/src/lib_commands/app_api/remote.rs +++ b/src-tauri/crates/psysonic-integration/src/remote.rs @@ -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, String> { +pub async fn search_radio_browser(query: String, offset: u32) -> Result, 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 Result, String> { +pub async fn get_top_radio_stations(offset: u32) -> Result, 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 Result<(Vec, String), String> { +pub async fn fetch_url_bytes(url: String) -> Result<(Vec, 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, 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 { +pub async fn fetch_json_url(url: String) -> Result { 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 { /// 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, /// 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 { +pub fn parse_pls_stream_url(content: &str) -> Option { 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 { } /// Extract the first non-comment HTTP URL from an M3U/M3U8 playlist file. -pub(crate) fn parse_m3u_stream_url(content: &str) -> Option { +pub fn parse_m3u_stream_url(content: &str) -> Option { 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 { /// 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 { +pub async fn resolve_playlist_url(client: &reqwest::Client, url: &str) -> Option { 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 { +pub async fn fetch_icy_metadata(url: String) -> Result { use futures_util::StreamExt; let client = reqwest::Client::builder() @@ -277,7 +277,7 @@ pub(crate) async fn fetch_icy_metadata(url: String) -> Result 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, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e104cb9f..0bf06e4c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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, ]) diff --git a/src-tauri/src/lib_commands/app_api/mod.rs b/src-tauri/src/lib_commands/app_api/mod.rs index 8f861846..30820c11 100644 --- a/src-tauri/src/lib_commands/app_api/mod.rs +++ b/src-tauri/src/lib_commands/app_api/mod.rs @@ -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. diff --git a/src-tauri/src/lib_commands/app_api/navidrome/mod.rs b/src-tauri/src/lib_commands/app_api/navidrome/mod.rs deleted file mode 100644 index b074f30a..00000000 --- a/src-tauri/src/lib_commands/app_api/navidrome/mod.rs +++ /dev/null @@ -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, -}; diff --git a/src-tauri/src/lib_commands/ui/mod.rs b/src-tauri/src/lib_commands/ui/mod.rs index f919926f..cb084ca9 100644 --- a/src-tauri/src/lib_commands/ui/mod.rs +++ b/src-tauri/src/lib_commands/ui/mod.rs @@ -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.