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",
]
[[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]]
name = "nix"
version = "0.29.0"
@@ -2365,7 +2355,6 @@ dependencies = [
"riptune-cache",
"riptune-core",
"riptune-mpris",
"riptune-niri",
"riptune-tui",
"riptune-types",
"rodio",
@@ -2407,25 +2396,10 @@ dependencies = [
name = "riptune-mpris"
version = "0.1.0"
dependencies = [
"riptune-types",
"tokio",
"tracing",
"zbus",
]
[[package]]
name = "riptune-niri"
version = "0.1.0"
dependencies = [
"niri-ipc",
"riptune-types",
"serde",
"serde_json",
"thiserror 1.0.69",
"tokio",
"tracing",
]
[[package]]
name = "riptune-tui"
version = "0.1.0"
-2
View File
@@ -4,7 +4,6 @@ members = [
"crates/riptune-core",
"crates/riptune-cache",
"crates/riptune-mpris",
"crates/riptune-niri",
"crates/riptune-tui",
"crates/riptune-types",
]
@@ -42,7 +41,6 @@ path = "src/main.rs"
riptune-core = { path = "crates/riptune-core" }
riptune-cache = { path = "crates/riptune-cache" }
riptune-mpris = { path = "crates/riptune-mpris" }
riptune-niri = { path = "crates/riptune-niri" }
riptune-tui = { path = "crates/riptune-tui" }
riptune-types = { path = "crates/riptune-types" }
tokio = { workspace = true }
-4
View File
@@ -3,10 +3,6 @@ url = "https://your-navidrome-server.com"
username = "your_username"
password = "your_password_or_api_key"
[niri]
enabled = true
workspace_notifications = true
[cache]
path = "~/.cache/riptune/library.sqlite"
+24 -35
View File
@@ -102,18 +102,7 @@ impl Cache {
.fetch_all(&self.pool)
.await?;
let records = rows.into_iter().map(|row| {
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)
Ok(rows.into_iter().map(row_to_album).collect())
}
/// Tracklist for an album, in soundtrack order (disc, then track number)
@@ -144,17 +133,7 @@ impl Cache {
.fetch_all(&self.pool)
.await?;
let records = rows.into_iter().map(|row| {
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)
Ok(rows.into_iter().map(row_to_artist).collect())
}
pub async fn search_local_albums(&self, query: &str) -> Result<Vec<AlbumSummary>, CacheError> {
@@ -166,18 +145,7 @@ impl Cache {
.fetch_all(&self.pool)
.await?;
let records = rows.into_iter().map(|row| {
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)
Ok(rows.into_iter().map(row_to_album).collect())
}
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 {
let duration: Option<i64> = row.get("duration_secs");
let track_number: Option<i64> = row.get("track_number");
+16 -26
View File
@@ -166,34 +166,24 @@ impl SubsonicClient {
// regardless of whether the server uses a flat index or nested structure.
let response: serde_json::Value = self.call("getArtists", &[]).await?;
let mut artists = Vec::new();
// Some servers nest the index under 'artists', others put it at the
// root -- try 'artists.index' first, then fall back to root 'index'.
let index_array = response
.get("artists")
.and_then(|a| a.get("index"))
.or_else(|| response.get("index"))
.and_then(|i| i.as_array());
// Navigate safely through 'artists' -> 'index' array
if let Some(index_array) = response.get("artists").and_then(|a| a.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);
}
}
}
}
}
// 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);
}
}
}
}
}
let Some(index_array) = index_array else {
return Ok(Vec::new());
};
Ok(artists)
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> {
-2
View File
@@ -6,7 +6,5 @@ license.workspace = true
description = "MPRIS2 D-Bus interface so waybar/notification daemons can control riptune"
[dependencies]
riptune-types = { path = "../riptune-types" }
tokio = { workspace = true }
tracing = { workspace = true }
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.
//! Spec: <https://specifications.freedesktop.org/mpris-spec/latest/>
use riptune_types::AudioCommand;
use std::sync::mpsc::Sender;
use zbus::{interface, ConnectionBuilder};
pub struct MprisHandle {
@@ -19,39 +17,28 @@ impl MprisHandle {
}
}
struct Player {
// Commands are relayed to the audio thread over this channel; MPRIS itself
// never touches audio state directly.
#[allow(dead_code)]
audio_tx: Sender<AudioCommand>,
}
// TODO: relay play_pause/next/previous to the audio thread's AudioCommand
// channel and reflect real playback state once that wiring exists.
struct Player;
#[interface(name = "org.mpris.MediaPlayer2.Player")]
impl Player {
async fn play_pause(&self) {
// TODO: relay to audio_tx based on current playback state
}
async fn play_pause(&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 next(&self) {}
async fn previous(&self) {
// TODO
}
async fn previous(&self) {}
#[zbus(property)]
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> {
let player = Player { audio_tx };
pub async fn spawn() -> zbus::Result<MprisHandle> {
let conn = ConnectionBuilder::session()?
.name("org.mpris.MediaPlayer2.riptune")?
.serve_at("/org/mpris/MediaPlayer2", player)?
.serve_at("/org/mpris/MediaPlayer2", Player)?
.build()
.await?;
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,
Playing,
Paused,
Stopped,
Error(String),
}
enum InputMode {
@@ -260,7 +258,6 @@ impl App {
// Shuffle & Repeat Controls
KeyCode::Char('s') => {
self.shuffle = !self.shuffle;
self.notify_desktop_environment("Shuffle Toggled");
}
KeyCode::Char('r') => {
self.repeat = match self.repeat {
@@ -268,7 +265,6 @@ impl App {
RepeatMode::All => RepeatMode::One,
RepeatMode::One => RepeatMode::Off,
};
self.notify_desktop_environment("Repeat Mode Changed");
}
KeyCode::Esc | KeyCode::Backspace => self.go_back().await,
KeyCode::Down | KeyCode::Char('j') => self.move_selection(1),
@@ -315,21 +311,6 @@ impl App {
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) {
self.selected_idx = 0;
@@ -452,7 +433,6 @@ impl App {
playlist_context,
cover_art_id: track.cover_art.clone(),
});
self.notify_desktop_environment("Track Started");
}
Err(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() {
self.play_track(track, self.tracks.clone());
}
@@ -549,7 +528,6 @@ impl App {
}
}
self.now_playing = None;
self.notify_desktop_environment("Track Stopped/Finished");
}
AudioEvent::Error(message) => {
self.status_line = Some(format!("playback error: {message}"));
@@ -948,7 +926,5 @@ fn playback_label(status: &PlaybackStatus) -> &str {
PlaybackStatus::Buffering => "[...] buffering...",
PlaybackStatus::Playing => "[>] playing",
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.
//!
//! `AudioCommand`/`AudioEvent` cross the audio-thread channel; `AppEvent`
//! crosses between the TUI, MPRIS, and niri IPC layers. They live here —
//! deliberately dependency-free — so `riptune-mpris`, `riptune-niri`, and the
//! `riptune` binary all share one definition instead of each inventing their
//! own (which is exactly the bug that broke the build previously: two
//! different `AudioCommand` types that looked identical but weren't).
//! `AudioCommand`/`AudioEvent` cross the audio-thread channel. They live here
//! — deliberately dependency-free — so `riptune-mpris` and the `riptune`
//! binary share one definition instead of each inventing their own (which is
//! exactly the bug that broke the build previously: two different
//! `AudioCommand` types that looked identical but weren't).
use std::time::Duration;
@@ -47,14 +46,3 @@ pub struct ThemeColors {
#[serde(default)]
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_cache::Cache (SQLite index, warmed on startup)
/// - riptune_mpris::Server (D-Bus MPRIS2 interface)
/// - riptune_niri::EventListener (niri IPC event-stream socket)
/// - riptune_tui::App (ratatui render loop)
///
/// 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 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?;
let mpris_handle = riptune_mpris::spawn().await?;
riptune_tui::App::new(client, cache, audio_tx.clone(), audio_events, cfg.theme.clone()).run().await?;
sync_task.abort();
if let Some(task) = niri_task {
task.abort();
}
mpris_handle.shutdown().await;
let _ = audio_tx.send(AudioCommand::Stop);
+2 -24
View File
@@ -6,8 +6,6 @@ use std::path::PathBuf;
pub struct Config {
pub server: ServerConfig,
#[serde(default)]
pub niri: NiriConfig,
#[serde(default)]
pub cache: CacheConfig,
#[serde(default)]
pub theme: riptune_types::ThemeColors,
@@ -20,21 +18,6 @@ pub struct ServerConfig {
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)]
pub struct CacheConfig {
#[serde(default = "default_cache_path")]
@@ -47,10 +30,6 @@ impl Default for CacheConfig {
}
}
fn default_true() -> bool {
true
}
fn default_cache_path() -> PathBuf {
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");
assert_eq!(cfg.server.url, "https://music.example.com");
assert!(cfg.niri.enabled);
}
#[test]
fn niri_and_cache_sections_are_optional() {
fn cache_section_is_optional() {
let raw = r#"
[server]
url = "https://music.example.com"
@@ -126,6 +104,6 @@ mod tests {
password = "secret"
"#;
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 audio;
mod config;
mod events;
use anyhow::Result;
use clap::{Parser, Subcommand};