cutting 160 dead/duplicated lines and one unused dependency
This commit is contained in:
Generated
-26
@@ -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"
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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"] }
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
|||||||
@@ -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"
|
|
||||||
@@ -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(())
|
|
||||||
}
|
|
||||||
@@ -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",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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
@@ -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"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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};
|
||||||
|
|||||||
Reference in New Issue
Block a user