cutting 160 dead/duplicated lines and one unused dependency

This commit is contained in:
2026-07-25 15:59:19 +02:00
parent 2c17e82ac2
commit e38c2ddcf8
15 changed files with 58 additions and 267 deletions
Generated
-26
View File
@@ -1740,16 +1740,6 @@ dependencies = [
"jni-sys 0.3.1", "jni-sys 0.3.1",
] ]
[[package]]
name = "niri-ipc"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4aa541cad3b426dd1ab72765ba71d1c20a1b8a17d80c692b099a759074f74242"
dependencies = [
"serde",
"serde_json",
]
[[package]] [[package]]
name = "nix" name = "nix"
version = "0.29.0" version = "0.29.0"
@@ -2365,7 +2355,6 @@ dependencies = [
"riptune-cache", "riptune-cache",
"riptune-core", "riptune-core",
"riptune-mpris", "riptune-mpris",
"riptune-niri",
"riptune-tui", "riptune-tui",
"riptune-types", "riptune-types",
"rodio", "rodio",
@@ -2407,25 +2396,10 @@ dependencies = [
name = "riptune-mpris" name = "riptune-mpris"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"riptune-types",
"tokio", "tokio",
"tracing",
"zbus", "zbus",
] ]
[[package]]
name = "riptune-niri"
version = "0.1.0"
dependencies = [
"niri-ipc",
"riptune-types",
"serde",
"serde_json",
"thiserror 1.0.69",
"tokio",
"tracing",
]
[[package]] [[package]]
name = "riptune-tui" name = "riptune-tui"
version = "0.1.0" version = "0.1.0"
-2
View File
@@ -4,7 +4,6 @@ members = [
"crates/riptune-core", "crates/riptune-core",
"crates/riptune-cache", "crates/riptune-cache",
"crates/riptune-mpris", "crates/riptune-mpris",
"crates/riptune-niri",
"crates/riptune-tui", "crates/riptune-tui",
"crates/riptune-types", "crates/riptune-types",
] ]
@@ -42,7 +41,6 @@ path = "src/main.rs"
riptune-core = { path = "crates/riptune-core" } riptune-core = { path = "crates/riptune-core" }
riptune-cache = { path = "crates/riptune-cache" } riptune-cache = { path = "crates/riptune-cache" }
riptune-mpris = { path = "crates/riptune-mpris" } riptune-mpris = { path = "crates/riptune-mpris" }
riptune-niri = { path = "crates/riptune-niri" }
riptune-tui = { path = "crates/riptune-tui" } riptune-tui = { path = "crates/riptune-tui" }
riptune-types = { path = "crates/riptune-types" } riptune-types = { path = "crates/riptune-types" }
tokio = { workspace = true } tokio = { workspace = true }
-4
View File
@@ -3,10 +3,6 @@ url = "https://your-navidrome-server.com"
username = "your_username" username = "your_username"
password = "your_password_or_api_key" password = "your_password_or_api_key"
[niri]
enabled = true
workspace_notifications = true
[cache] [cache]
path = "~/.cache/riptune/library.sqlite" path = "~/.cache/riptune/library.sqlite"
+24 -35
View File
@@ -102,18 +102,7 @@ impl Cache {
.fetch_all(&self.pool) .fetch_all(&self.pool)
.await?; .await?;
let records = rows.into_iter().map(|row| { Ok(rows.into_iter().map(row_to_album).collect())
let count: i64 = row.get("song_count");
let year: Option<i64> = row.get("year");
AlbumSummary {
id: row.get("id"),
name: row.get("name"),
song_count: count as u32,
year: year.map(|y| y as u32),
}
}).collect();
Ok(records)
} }
/// Tracklist for an album, in soundtrack order (disc, then track number) /// Tracklist for an album, in soundtrack order (disc, then track number)
@@ -144,17 +133,7 @@ impl Cache {
.fetch_all(&self.pool) .fetch_all(&self.pool)
.await?; .await?;
let records = rows.into_iter().map(|row| { Ok(rows.into_iter().map(row_to_artist).collect())
let count: i64 = row.get("album_count");
Artist {
id: row.get("id"),
name: row.get("name"),
album_count: count as u32,
cover_art: None,
}
}).collect();
Ok(records)
} }
pub async fn search_local_albums(&self, query: &str) -> Result<Vec<AlbumSummary>, CacheError> { pub async fn search_local_albums(&self, query: &str) -> Result<Vec<AlbumSummary>, CacheError> {
@@ -166,18 +145,7 @@ impl Cache {
.fetch_all(&self.pool) .fetch_all(&self.pool)
.await?; .await?;
let records = rows.into_iter().map(|row| { Ok(rows.into_iter().map(row_to_album).collect())
let count: i64 = row.get("song_count");
let year: Option<i64> = row.get("year");
AlbumSummary {
id: row.get("id"),
name: row.get("name"),
song_count: count as u32,
year: year.map(|y| y as u32),
}
}).collect();
Ok(records)
} }
pub async fn search_local_tracks(&self, query: &str) -> Result<Vec<Track>, CacheError> { pub async fn search_local_tracks(&self, query: &str) -> Result<Vec<Track>, CacheError> {
@@ -198,6 +166,27 @@ impl Cache {
} }
} }
fn row_to_artist(row: sqlx::sqlite::SqliteRow) -> Artist {
let count: i64 = row.get("album_count");
Artist {
id: row.get("id"),
name: row.get("name"),
album_count: count as u32,
cover_art: None,
}
}
fn row_to_album(row: sqlx::sqlite::SqliteRow) -> AlbumSummary {
let count: i64 = row.get("song_count");
let year: Option<i64> = row.get("year");
AlbumSummary {
id: row.get("id"),
name: row.get("name"),
song_count: count as u32,
year: year.map(|y| y as u32),
}
}
fn row_to_track(row: sqlx::sqlite::SqliteRow) -> Track { fn row_to_track(row: sqlx::sqlite::SqliteRow) -> Track {
let duration: Option<i64> = row.get("duration_secs"); let duration: Option<i64> = row.get("duration_secs");
let track_number: Option<i64> = row.get("track_number"); let track_number: Option<i64> = row.get("track_number");
+17 -27
View File
@@ -165,35 +165,25 @@ impl SubsonicClient {
// We fetch the raw JSON value first so we can robustly extract the array // We fetch the raw JSON value first so we can robustly extract the array
// regardless of whether the server uses a flat index or nested structure. // regardless of whether the server uses a flat index or nested structure.
let response: serde_json::Value = self.call("getArtists", &[]).await?; let response: serde_json::Value = self.call("getArtists", &[]).await?;
let mut artists = Vec::new();
// Navigate safely through 'artists' -> 'index' array // Some servers nest the index under 'artists', others put it at the
if let Some(index_array) = response.get("artists").and_then(|a| a.get("index")).and_then(|i| i.as_array()) { // root -- try 'artists.index' first, then fall back to root 'index'.
for group in index_array { let index_array = response
if let Some(artist_list) = group.get("artist").and_then(|a| a.as_array()) { .get("artists")
for artist_val in artist_list { .and_then(|a| a.get("index"))
if let Ok(artist) = serde_json::from_value::<models::Artist>(artist_val.clone()) { .or_else(|| response.get("index"))
artists.push(artist); .and_then(|i| i.as_array());
}
}
}
}
}
// Fallback: Check if the server responds with a root level 'index' block directly
else if let Some(index_array) = response.get("index").and_then(|i| i.as_array()) {
for group in index_array {
if let Some(artist_list) = group.get("artist").and_then(|a| a.as_array()) {
for artist_val in artist_list {
if let Ok(artist) = serde_json::from_value::<models::Artist>(artist_val.clone()) {
artists.push(artist);
}
}
}
}
}
Ok(artists) let Some(index_array) = index_array else {
return Ok(Vec::new());
};
Ok(index_array
.iter()
.filter_map(|group| group.get("artist").and_then(|a| a.as_array()))
.flatten()
.filter_map(|artist_val| serde_json::from_value::<models::Artist>(artist_val.clone()).ok())
.collect())
} }
pub async fn get_album(&self, album_id: &str) -> Result<models::Album, SubsonicError> { pub async fn get_album(&self, album_id: &str) -> Result<models::Album, SubsonicError> {
-2
View File
@@ -6,7 +6,5 @@ license.workspace = true
description = "MPRIS2 D-Bus interface so waybar/notification daemons can control riptune" description = "MPRIS2 D-Bus interface so waybar/notification daemons can control riptune"
[dependencies] [dependencies]
riptune-types = { path = "../riptune-types" }
tokio = { workspace = true } tokio = { workspace = true }
tracing = { workspace = true }
zbus = { version = "4", default-features = false, features = ["tokio"] } zbus = { version = "4", default-features = false, features = ["tokio"] }
+9 -22
View File
@@ -5,8 +5,6 @@
//! about Subsonic — they only ever speak the standard MPRIS interface. //! about Subsonic — they only ever speak the standard MPRIS interface.
//! Spec: <https://specifications.freedesktop.org/mpris-spec/latest/> //! Spec: <https://specifications.freedesktop.org/mpris-spec/latest/>
use riptune_types::AudioCommand;
use std::sync::mpsc::Sender;
use zbus::{interface, ConnectionBuilder}; use zbus::{interface, ConnectionBuilder};
pub struct MprisHandle { pub struct MprisHandle {
@@ -19,39 +17,28 @@ impl MprisHandle {
} }
} }
struct Player { // TODO: relay play_pause/next/previous to the audio thread's AudioCommand
// Commands are relayed to the audio thread over this channel; MPRIS itself // channel and reflect real playback state once that wiring exists.
// never touches audio state directly. struct Player;
#[allow(dead_code)]
audio_tx: Sender<AudioCommand>,
}
#[interface(name = "org.mpris.MediaPlayer2.Player")] #[interface(name = "org.mpris.MediaPlayer2.Player")]
impl Player { impl Player {
async fn play_pause(&self) { async fn play_pause(&self) {}
// TODO: relay to audio_tx based on current playback state
}
async fn next(&self) { async fn next(&self) {}
// TODO: emit AppEvent::NextTrack for app.rs to handle (advance in
// playlist, fetch new stream_url, send AudioCommand::Play)
}
async fn previous(&self) { async fn previous(&self) {}
// TODO
}
#[zbus(property)] #[zbus(property)]
async fn playback_status(&self) -> String { async fn playback_status(&self) -> String {
"Stopped".into() // TODO: reflect real state "Stopped".into()
} }
} }
pub async fn spawn(audio_tx: Sender<AudioCommand>) -> zbus::Result<MprisHandle> { pub async fn spawn() -> zbus::Result<MprisHandle> {
let player = Player { audio_tx };
let conn = ConnectionBuilder::session()? let conn = ConnectionBuilder::session()?
.name("org.mpris.MediaPlayer2.riptune")? .name("org.mpris.MediaPlayer2.riptune")?
.serve_at("/org/mpris/MediaPlayer2", player)? .serve_at("/org/mpris/MediaPlayer2", Player)?
.build() .build()
.await?; .await?;
Ok(MprisHandle { conn }) Ok(MprisHandle { conn })
-15
View File
@@ -1,15 +0,0 @@
[package]
name = "riptune-niri"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "niri IPC event-stream listener for workspace-aware behavior and custom bindings"
[dependencies]
riptune-types = { path = "../riptune-types" }
tokio = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tracing = { workspace = true }
thiserror = { workspace = true }
niri-ipc = "0.1"
-40
View File
@@ -1,40 +0,0 @@
//! niri IPC integration.
//!
//! Important distinction: `niri msg <cmd>` is for one-off commands (fine for
//! e.g. a keybind that shells out to `riptune favorite`). For *reactive*
//! behavior — adjusting the TUI or firing a notification when the focused
//! workspace changes — we want the persistent event-stream socket, not
//! polling `niri msg` in a loop. The `niri-ipc` crate exposes both; this
//! module only uses the event stream.
//!
//! NOTE: pin the `niri-ipc` crate version to match your installed niri
//! release — the IPC protocol has changed across niri versions and is not
//! guaranteed stable yet.
use riptune_types::AudioCommand;
use std::sync::mpsc::Sender;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum NiriError {
#[error("failed to connect to niri IPC socket: {0}")]
Connect(#[from] std::io::Error),
}
pub async fn listen(_audio_tx: Sender<AudioCommand>) -> Result<(), NiriError> {
// TODO:
// 1. Connect to the socket at $NIRI_SOCKET (niri sets this env var).
// 2. Send the `EventStream` request per niri-ipc's protocol.
// 3. Loop reading newline-delimited JSON events off the socket.
// 4. On `WorkspaceActivated` events, translate into an AppEvent and forward
// it to the TUI (e.g. via a broadcast channel) so it can show a toast
// ("Now playing on workspace 3") or adjust layout density.
//
// Custom bindings (e.g. "favorite current track" bound to a niri keybind)
// are simplest implemented as: niri config calls `riptune favorite`
// as a spawned command, and this binary's CLI has a `favorite` subcommand
// that talks to a local control socket riptune itself exposes — avoids
// needing niri to know anything about riptune's internals.
std::future::pending::<()>().await;
Ok(())
}
-24
View File
@@ -51,8 +51,6 @@ pub enum PlaybackStatus {
Buffering, Buffering,
Playing, Playing,
Paused, Paused,
Stopped,
Error(String),
} }
enum InputMode { enum InputMode {
@@ -260,7 +258,6 @@ impl App {
// Shuffle & Repeat Controls // Shuffle & Repeat Controls
KeyCode::Char('s') => { KeyCode::Char('s') => {
self.shuffle = !self.shuffle; self.shuffle = !self.shuffle;
self.notify_desktop_environment("Shuffle Toggled");
} }
KeyCode::Char('r') => { KeyCode::Char('r') => {
self.repeat = match self.repeat { self.repeat = match self.repeat {
@@ -268,7 +265,6 @@ impl App {
RepeatMode::All => RepeatMode::One, RepeatMode::All => RepeatMode::One,
RepeatMode::One => RepeatMode::Off, RepeatMode::One => RepeatMode::Off,
}; };
self.notify_desktop_environment("Repeat Mode Changed");
} }
KeyCode::Esc | KeyCode::Backspace => self.go_back().await, KeyCode::Esc | KeyCode::Backspace => self.go_back().await,
KeyCode::Down | KeyCode::Char('j') => self.move_selection(1), KeyCode::Down | KeyCode::Char('j') => self.move_selection(1),
@@ -315,21 +311,6 @@ impl App {
false false
} }
// --- Option 2 Integration Hooks (MPRIS & Niri IPC Sync Channels) ---
fn notify_desktop_environment(&self, action_context: &str) {
// These hooks act as communication bridges for your riptune-mpris and riptune-niri subcrates[cite: 1]
// Example: Emits state logs that zbus / mpris handlers catch via standard asynchronous orchestration[cite: 1, 7]
if let Some(np) = &self.now_playing {
let _status_indicator = match np.status {
PlaybackStatus::Playing => "Playing",
PlaybackStatus::Paused => "Paused",
_ => "Stopped",
};
// Under-the-hood bindings read this payload state to dynamically broadcast D-Bus properties[cite: 1, 7]
std::string::ToString::to_string(&format!("MPRIS/NIRI Sync Triggered: {} -> {} ({})", action_context, np.title, _status_indicator));
}
}
async fn execute_db_search(&mut self, query: &str) { async fn execute_db_search(&mut self, query: &str) {
self.selected_idx = 0; self.selected_idx = 0;
@@ -452,7 +433,6 @@ impl App {
playlist_context, playlist_context,
cover_art_id: track.cover_art.clone(), cover_art_id: track.cover_art.clone(),
}); });
self.notify_desktop_environment("Track Started");
} }
Err(e) => { Err(e) => {
self.status_line = Some(format!("failed to build stream url: {e}")); self.status_line = Some(format!("failed to build stream url: {e}"));
@@ -493,7 +473,6 @@ impl App {
} }
_ => {} _ => {}
} }
self.notify_desktop_environment("Play/Pause Toggled");
} else if let Some(track) = self.tracks.get(self.selected_idx).cloned() { } else if let Some(track) = self.tracks.get(self.selected_idx).cloned() {
self.play_track(track, self.tracks.clone()); self.play_track(track, self.tracks.clone());
} }
@@ -549,7 +528,6 @@ impl App {
} }
} }
self.now_playing = None; self.now_playing = None;
self.notify_desktop_environment("Track Stopped/Finished");
} }
AudioEvent::Error(message) => { AudioEvent::Error(message) => {
self.status_line = Some(format!("playback error: {message}")); self.status_line = Some(format!("playback error: {message}"));
@@ -948,7 +926,5 @@ fn playback_label(status: &PlaybackStatus) -> &str {
PlaybackStatus::Buffering => "[...] buffering...", PlaybackStatus::Buffering => "[...] buffering...",
PlaybackStatus::Playing => "[>] playing", PlaybackStatus::Playing => "[>] playing",
PlaybackStatus::Paused => "[||] paused", PlaybackStatus::Paused => "[||] paused",
PlaybackStatus::Stopped => "[.] stopped",
PlaybackStatus::Error(_) => "[X] error",
} }
} }
+5 -17
View File
@@ -1,11 +1,10 @@
//! Types shared across thread and crate boundaries. //! Types shared across thread and crate boundaries.
//! //!
//! `AudioCommand`/`AudioEvent` cross the audio-thread channel; `AppEvent` //! `AudioCommand`/`AudioEvent` cross the audio-thread channel. They live here
//! crosses between the TUI, MPRIS, and niri IPC layers. They live here — //! — deliberately dependency-free — so `riptune-mpris` and the `riptune`
//! deliberately dependency-free — so `riptune-mpris`, `riptune-niri`, and the //! binary share one definition instead of each inventing their own (which is
//! `riptune` binary all share one definition instead of each inventing their //! exactly the bug that broke the build previously: two different
//! own (which is exactly the bug that broke the build previously: two //! `AudioCommand` types that looked identical but weren't).
//! different `AudioCommand` types that looked identical but weren't).
use std::time::Duration; use std::time::Duration;
@@ -47,14 +46,3 @@ pub struct ThemeColors {
#[serde(default)] #[serde(default)]
pub selection_bg: Option<String>, pub selection_bg: Option<String>,
} }
#[derive(Debug, Clone)]
pub enum AppEvent {
/// User favorited/unfavorited a track (mirrors Subsonic star/unstar + can be
/// triggered from a niri keybind, MPRIS client, or the TUI itself).
ToggleFavorite { track_id: String },
/// Fired by the niri IPC listener when the focused workspace changes.
WorkspaceChanged { name: Option<String> },
NextTrack,
PreviousTrack,
}
+1 -17
View File
@@ -7,7 +7,6 @@ use std::sync::mpsc::{Receiver, Sender};
/// - riptune_core::SubsonicClient (REST API + streaming URLs) /// - riptune_core::SubsonicClient (REST API + streaming URLs)
/// - riptune_cache::Cache (SQLite index, warmed on startup) /// - riptune_cache::Cache (SQLite index, warmed on startup)
/// - riptune_mpris::Server (D-Bus MPRIS2 interface) /// - riptune_mpris::Server (D-Bus MPRIS2 interface)
/// - riptune_niri::EventListener (niri IPC event-stream socket)
/// - riptune_tui::App (ratatui render loop) /// - riptune_tui::App (ratatui render loop)
/// ///
/// The TUI is the only piece that technically needs to run on the main thread on /// The TUI is the only piece that technically needs to run on the main thread on
@@ -26,26 +25,11 @@ pub async fn run(cfg: Config, audio_tx: Sender<AudioCommand>, audio_events: Rece
} }
}); });
// Kept so we can abort this task once the TUI quits. let mpris_handle = riptune_mpris::spawn().await?;
let niri_task = if cfg.niri.enabled {
let audio_tx = audio_tx.clone();
Some(tokio::spawn(async move {
if let Err(e) = riptune_niri::listen(audio_tx).await {
tracing::warn!(error = %e, "niri IPC listener exited");
}
}))
} else {
None
};
let mpris_handle = riptune_mpris::spawn(audio_tx.clone()).await?;
riptune_tui::App::new(client, cache, audio_tx.clone(), audio_events, cfg.theme.clone()).run().await?; riptune_tui::App::new(client, cache, audio_tx.clone(), audio_events, cfg.theme.clone()).run().await?;
sync_task.abort(); sync_task.abort();
if let Some(task) = niri_task {
task.abort();
}
mpris_handle.shutdown().await; mpris_handle.shutdown().await;
let _ = audio_tx.send(AudioCommand::Stop); let _ = audio_tx.send(AudioCommand::Stop);
+2 -24
View File
@@ -6,8 +6,6 @@ use std::path::PathBuf;
pub struct Config { pub struct Config {
pub server: ServerConfig, pub server: ServerConfig,
#[serde(default)] #[serde(default)]
pub niri: NiriConfig,
#[serde(default)]
pub cache: CacheConfig, pub cache: CacheConfig,
#[serde(default)] #[serde(default)]
pub theme: riptune_types::ThemeColors, pub theme: riptune_types::ThemeColors,
@@ -20,21 +18,6 @@ pub struct ServerConfig {
pub password: String, pub password: String,
} }
#[derive(Debug, Clone, Deserialize)]
pub struct NiriConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[allow(dead_code)]
#[serde(default = "default_true")]
pub workspace_notifications: bool,
}
impl Default for NiriConfig {
fn default() -> Self {
Self { enabled: true, workspace_notifications: true }
}
}
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct CacheConfig { pub struct CacheConfig {
#[serde(default = "default_cache_path")] #[serde(default = "default_cache_path")]
@@ -47,10 +30,6 @@ impl Default for CacheConfig {
} }
} }
fn default_true() -> bool {
true
}
fn default_cache_path() -> PathBuf { fn default_cache_path() -> PathBuf {
dirs_cache_dir().join("riptune").join("library.sqlite") dirs_cache_dir().join("riptune").join("library.sqlite")
} }
@@ -114,11 +93,10 @@ mod tests {
"#; "#;
let cfg: Config = toml::from_str(raw).expect("minimal config should parse"); let cfg: Config = toml::from_str(raw).expect("minimal config should parse");
assert_eq!(cfg.server.url, "https://music.example.com"); assert_eq!(cfg.server.url, "https://music.example.com");
assert!(cfg.niri.enabled);
} }
#[test] #[test]
fn niri_and_cache_sections_are_optional() { fn cache_section_is_optional() {
let raw = r#" let raw = r#"
[server] [server]
url = "https://music.example.com" url = "https://music.example.com"
@@ -126,6 +104,6 @@ mod tests {
password = "secret" password = "secret"
"#; "#;
let cfg: Config = toml::from_str(raw).expect("minimal config should parse"); let cfg: Config = toml::from_str(raw).expect("minimal config should parse");
assert!(cfg.niri.enabled, "niri.enabled should default to true"); assert!(cfg.cache.path.ends_with("library.sqlite"));
} }
} }
-11
View File
@@ -1,11 +0,0 @@
//! Thin re-export so binary-crate code can `use crate::events::AppEvent`
//! without every call site needing to know it lives in `riptune-types`.
//! The type itself is defined once, in `riptune-types`, and shared with
//! `riptune-mpris` and `riptune-niri`.
// Not yet consumed — app.rs doesn't have an event loop wired up until the TUI
// is implemented. Kept here so mpris/niri call sites can start emitting
// AppEvents ahead of that without another refactor. Remove this allow once
// app::run() dispatches on it.
#[allow(unused_imports)]
pub use riptune_types::AppEvent;
-1
View File
@@ -3,7 +3,6 @@
mod app; mod app;
mod audio; mod audio;
mod config; mod config;
mod events;
use anyhow::Result; use anyhow::Result;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};