cutting 160 dead/duplicated lines and one unused dependency
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -165,35 +165,25 @@ impl SubsonicClient {
|
||||
// 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.
|
||||
let response: serde_json::Value = self.call("getArtists", &[]).await?;
|
||||
|
||||
let mut artists = Vec::new();
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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());
|
||||
|
||||
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> {
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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,
|
||||
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",
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user