Remove dead code and #![allow(dead_code)] blanket suppressions

- Delete src/audio/queue.rs (321 lines, PlayQueue never used)
- Remove #![allow(dead_code)] from audio, subsonic, and mpris module roots
- Remove unused MpvEvent2 enum, playlist_clear, get_volume, get_path,
  is_eof, observe_property from mpv.rs
- Remove unused DEFAULT_DEVICE_ID, is_available, get_effective_rate
  from pipewire.rs (and associated dead test)
- Remove unused search(), get_cover_art_url() from subsonic client
- Remove unused SearchResult3Data, SearchResult3 model structs
- Move parse_song_id_from_url into #[cfg(test)] block (only used by tests)
- Add targeted #[allow(dead_code)] on deserialization-only fields
  (MpvEvent, SubsonicResponseInner.version, ArtistIndex.name)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-27 23:56:52 +00:00
parent 7582937439
commit 766614f5e9
13 changed files with 48 additions and 620 deletions

View File

@@ -1,12 +1,8 @@
//! Application actions and message passing
use crate::subsonic::models::{Album, Artist, Child, Playlist};
/// Actions that can be sent to the audio backend
#[derive(Debug, Clone)]
pub enum AudioAction {
/// Play a specific song by URL
Play { url: String, song: Child },
/// Pause playback
Pause,
/// Resume playback
@@ -26,86 +22,3 @@ pub enum AudioAction {
/// Set volume (0-100)
SetVolume(i32),
}
/// Actions that can be sent to update the UI
#[derive(Debug, Clone)]
pub enum UiAction {
/// Update playback position
UpdatePosition { position: f64, duration: f64 },
/// Update playback state
UpdatePlaybackState(PlaybackStateUpdate),
/// Update audio properties
UpdateAudioProperties {
sample_rate: Option<u32>,
bit_depth: Option<u32>,
format: Option<String>,
},
/// Track ended (EOF from MPV)
TrackEnded,
/// Show notification
Notify { message: String, is_error: bool },
/// Artists loaded from server
ArtistsLoaded(Vec<Artist>),
/// Albums loaded for an artist
AlbumsLoaded {
artist_id: String,
albums: Vec<Album>,
},
/// Songs loaded for an album
SongsLoaded { album_id: String, songs: Vec<Child> },
/// Playlists loaded from server
PlaylistsLoaded(Vec<Playlist>),
/// Playlist songs loaded
PlaylistSongsLoaded {
playlist_id: String,
songs: Vec<Child>,
},
/// Server connection test result
ConnectionTestResult { success: bool, message: String },
/// Force redraw
Redraw,
}
/// Playback state update
#[derive(Debug, Clone, Copy)]
pub enum PlaybackStateUpdate {
Playing,
Paused,
Stopped,
}
/// Actions for the Subsonic client
#[derive(Debug, Clone)]
pub enum SubsonicAction {
/// Fetch all artists
FetchArtists,
/// Fetch albums for an artist
FetchAlbums { artist_id: String },
/// Fetch songs for an album
FetchAlbum { album_id: String },
/// Fetch all playlists
FetchPlaylists,
/// Fetch songs in a playlist
FetchPlaylist { playlist_id: String },
/// Test server connection
TestConnection,
}
/// Queue manipulation actions
#[derive(Debug, Clone)]
pub enum QueueAction {
/// Append songs to queue
Append(Vec<Child>),
/// Insert songs after current position
InsertNext(Vec<Child>),
/// Clear the queue
Clear,
/// Remove song at index
Remove(usize),
/// Move song from one index to another
Move { from: usize, to: usize },
/// Shuffle the queue (keeping current song in place)
Shuffle,
/// Play song at queue index
PlayIndex(usize),
}

View File

@@ -32,16 +32,6 @@ impl Page {
}
}
pub fn from_index(index: usize) -> Self {
match index {
0 => Page::Artists,
1 => Page::Queue,
2 => Page::Playlists,
3 => Page::Server,
4 => Page::Settings,
_ => Page::Artists,
}
}
pub fn label(&self) -> &'static str {
match self {
@@ -205,6 +195,8 @@ pub struct SettingsState {
pub theme_index: usize,
/// Cava visualizer enabled
pub cava_enabled: bool,
/// Cava visualizer height percentage (10-80, step 5)
pub cava_size: u8,
}
impl Default for SettingsState {
@@ -214,6 +206,7 @@ impl Default for SettingsState {
themes: vec![ThemeData::default_theme()],
theme_index: 0,
cava_enabled: false,
cava_size: 40,
}
}
}
@@ -269,10 +262,8 @@ pub struct Notification {
#[derive(Debug, Clone, Default)]
pub struct LayoutAreas {
pub header: Rect,
pub cava: Option<Rect>,
pub content: Rect,
pub now_playing: Rect,
pub footer: Rect,
/// Left pane for dual-pane pages (Artists tree, Playlists list)
pub content_left: Option<Rect>,
/// Right pane for dual-pane pages (Songs list)
@@ -349,6 +340,7 @@ impl AppState {
state.server_state.password = config.password.clone();
// Initialize cava from config
state.settings_state.cava_enabled = config.cava;
state.settings_state.cava_size = config.cava_size.clamp(10, 80);
state
}

View File

@@ -1,7 +1,4 @@
//! Audio playback module
#![allow(dead_code)]
pub mod mpv;
pub mod pipewire;
pub mod queue;

View File

@@ -32,8 +32,9 @@ struct MpvResponse {
error: String,
}
/// MPV event
/// MPV event (used for deserialization and debug tracing)
#[derive(Debug, Deserialize)]
#[allow(dead_code)] // Fields populated by deserialization, read via Debug
struct MpvEvent {
event: String,
#[serde(default)]
@@ -42,27 +43,6 @@ struct MpvEvent {
data: Option<Value>,
}
/// Events emitted by MPV
#[derive(Debug, Clone)]
pub enum MpvEvent2 {
/// Track reached end of file
EndFile,
/// Playback paused
Pause,
/// Playback resumed
Unpause,
/// Position changed (time in seconds)
TimePos(f64),
/// Audio properties changed
AudioProperties {
sample_rate: Option<u32>,
bit_depth: Option<u32>,
format: Option<String>,
},
/// MPV shut down
Shutdown,
}
/// MPV controller
pub struct MpvController {
/// Path to the IPC socket
@@ -217,13 +197,6 @@ impl MpvController {
Ok(())
}
/// Clear the playlist except current track
pub fn playlist_clear(&mut self) -> Result<(), AudioError> {
debug!("Clearing playlist");
self.send_command(vec![json!("playlist-clear")])?;
Ok(())
}
/// Remove a specific entry from the playlist by index
pub fn playlist_remove(&mut self, index: usize) -> Result<(), AudioError> {
debug!("Removing playlist entry {}", index);
@@ -307,15 +280,6 @@ impl MpvController {
Ok(data.and_then(|v| v.as_f64()).unwrap_or(0.0))
}
/// Get volume (0-100)
pub fn get_volume(&mut self) -> Result<i32, AudioError> {
let data = self.send_command(vec![json!("get_property"), json!("volume")])?;
Ok(data
.and_then(|v| v.as_f64())
.map(|v| v as i32)
.unwrap_or(100))
}
/// Set volume (0-100)
pub fn set_volume(&mut self, volume: i32) -> Result<(), AudioError> {
debug!("Setting volume to {}", volume);
@@ -378,24 +342,12 @@ impl MpvController {
}))
}
/// Get current filename/URL
pub fn get_path(&mut self) -> Result<Option<String>, AudioError> {
let data = self.send_command(vec![json!("get_property"), json!("path")])?;
Ok(data.and_then(|v| v.as_str().map(String::from)))
}
/// Check if anything is loaded
pub fn is_idle(&mut self) -> Result<bool, AudioError> {
let data = self.send_command(vec![json!("get_property"), json!("idle-active")])?;
Ok(data.and_then(|v| v.as_bool()).unwrap_or(true))
}
/// Check if current file has reached EOF
pub fn is_eof(&mut self) -> Result<bool, AudioError> {
let data = self.send_command(vec![json!("get_property"), json!("eof-reached")])?;
Ok(data.and_then(|v| v.as_bool()).unwrap_or(false))
}
/// Quit MPV
pub fn quit(&mut self) -> Result<(), AudioError> {
if self.socket.is_some() {
@@ -414,11 +366,6 @@ impl MpvController {
Ok(())
}
/// Observe a property for changes
pub fn observe_property(&mut self, id: u64, name: &str) -> Result<(), AudioError> {
self.send_command(vec![json!("observe_property"), json!(id), json!(name)])?;
Ok(())
}
}
impl Drop for MpvController {

View File

@@ -5,9 +5,6 @@ use tracing::{debug, error, info};
use crate::error::AudioError;
/// Default audio device ID for PipeWire
const DEFAULT_DEVICE_ID: u32 = 0;
/// PipeWire sample rate controller
pub struct PipeWireController {
/// Original sample rate before ferrosonic started
@@ -133,47 +130,6 @@ impl PipeWireController {
Ok(())
}
/// Check if PipeWire is available
pub fn is_available() -> bool {
Command::new("pw-metadata")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
/// Get the effective sample rate (from pw-metadata or system default)
pub fn get_effective_rate() -> Option<u32> {
// Try to get from PipeWire
let output = Command::new("pw-metadata")
.arg("-n")
.arg("settings")
.output()
.ok()?;
let stdout = String::from_utf8_lossy(&output.stdout);
// Look for clock.rate or clock.force-rate
for line in stdout.lines() {
if (line.contains("clock.rate") || line.contains("clock.force-rate"))
&& line.contains("value:")
{
if let Some(start) = line.find("value:'") {
let rest = &line[start + 7..];
if let Some(end) = rest.find('\'') {
let rate_str = &rest[..end];
if let Ok(rate) = rate_str.parse::<u32>() {
if rate > 0 {
return Some(rate);
}
}
}
}
}
}
None
}
}
impl Default for PipeWireController {
@@ -190,13 +146,3 @@ impl Drop for PipeWireController {
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_available() {
// This test just checks the function doesn't panic
let _ = PipeWireController::is_available();
}
}

View File

@@ -1,321 +0,0 @@
//! Play queue management
use rand::seq::SliceRandom;
use tracing::debug;
use crate::subsonic::models::Child;
use crate::subsonic::SubsonicClient;
/// Play queue
#[derive(Debug, Clone, Default)]
pub struct PlayQueue {
/// Songs in the queue
songs: Vec<Child>,
/// Current position in the queue (None = stopped)
position: Option<usize>,
}
impl PlayQueue {
/// Create a new empty queue
pub fn new() -> Self {
Self::default()
}
/// Get the songs in the queue
pub fn songs(&self) -> &[Child] {
&self.songs
}
/// Get the current position
pub fn position(&self) -> Option<usize> {
self.position
}
/// Get the current song
pub fn current(&self) -> Option<&Child> {
self.position.and_then(|pos| self.songs.get(pos))
}
/// Get number of songs in queue
pub fn len(&self) -> usize {
self.songs.len()
}
/// Check if queue is empty
pub fn is_empty(&self) -> bool {
self.songs.is_empty()
}
/// Add songs to the end of the queue
pub fn append(&mut self, songs: impl IntoIterator<Item = Child>) {
self.songs.extend(songs);
debug!("Queue now has {} songs", self.songs.len());
}
/// Insert songs after the current position
pub fn insert_next(&mut self, songs: impl IntoIterator<Item = Child>) {
let insert_pos = self.position.map(|p| p + 1).unwrap_or(0);
let new_songs: Vec<_> = songs.into_iter().collect();
let count = new_songs.len();
for (i, song) in new_songs.into_iter().enumerate() {
self.songs.insert(insert_pos + i, song);
}
debug!("Inserted {} songs at position {}", count, insert_pos);
}
/// Clear the queue
pub fn clear(&mut self) {
self.songs.clear();
self.position = None;
debug!("Queue cleared");
}
/// Remove song at index
pub fn remove(&mut self, index: usize) -> Option<Child> {
if index >= self.songs.len() {
return None;
}
let song = self.songs.remove(index);
// Adjust position if needed
if let Some(pos) = self.position {
if index < pos {
self.position = Some(pos - 1);
} else if index == pos {
// Removed current song
if self.songs.is_empty() {
self.position = None;
} else if pos >= self.songs.len() {
self.position = Some(self.songs.len() - 1);
}
}
}
debug!("Removed song at index {}", index);
Some(song)
}
/// Move song from one position to another
pub fn move_song(&mut self, from: usize, to: usize) {
if from >= self.songs.len() || to >= self.songs.len() {
return;
}
let song = self.songs.remove(from);
self.songs.insert(to, song);
// Adjust position if needed
if let Some(pos) = self.position {
if from == pos {
self.position = Some(to);
} else if from < pos && to >= pos {
self.position = Some(pos - 1);
} else if from > pos && to <= pos {
self.position = Some(pos + 1);
}
}
debug!("Moved song from {} to {}", from, to);
}
/// Shuffle the queue, keeping current song in place
pub fn shuffle(&mut self) {
if self.songs.len() <= 1 {
return;
}
let mut rng = rand::thread_rng();
if let Some(pos) = self.position {
// Keep current song, shuffle the rest
let current = self.songs.remove(pos);
// Shuffle remaining songs
self.songs.shuffle(&mut rng);
// Put current song at the front
self.songs.insert(0, current);
self.position = Some(0);
} else {
// No current song, shuffle everything
self.songs.shuffle(&mut rng);
}
debug!("Queue shuffled");
}
/// Set current position
pub fn set_position(&mut self, position: Option<usize>) {
if let Some(pos) = position {
if pos < self.songs.len() {
self.position = Some(pos);
debug!("Position set to {}", pos);
}
} else {
self.position = None;
debug!("Position cleared");
}
}
/// Advance to next song
/// Returns true if there was a next song
pub fn next(&mut self) -> bool {
match self.position {
Some(pos) if pos + 1 < self.songs.len() => {
self.position = Some(pos + 1);
debug!("Advanced to position {}", pos + 1);
true
}
_ => {
self.position = None;
debug!("Reached end of queue");
false
}
}
}
/// Go to previous song
/// Returns true if there was a previous song
pub fn previous(&mut self) -> bool {
match self.position {
Some(pos) if pos > 0 => {
self.position = Some(pos - 1);
debug!("Went back to position {}", pos - 1);
true
}
_ => {
if !self.songs.is_empty() {
self.position = Some(0);
}
debug!("At start of queue");
false
}
}
}
/// Get stream URL for current song
pub fn current_stream_url(&self, client: &SubsonicClient) -> Option<String> {
self.current()
.and_then(|song| client.get_stream_url(&song.id).ok())
}
/// Get stream URL for song at index
pub fn stream_url_at(&self, index: usize, client: &SubsonicClient) -> Option<String> {
self.songs
.get(index)
.and_then(|song| client.get_stream_url(&song.id).ok())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_song(id: &str, title: &str) -> Child {
Child {
id: id.to_string(),
title: title.to_string(),
parent: None,
is_dir: false,
album: None,
artist: None,
track: None,
year: None,
genre: None,
cover_art: None,
size: None,
content_type: None,
suffix: None,
duration: None,
bit_rate: None,
path: None,
disc_number: None,
}
}
#[test]
fn test_append_and_len() {
let mut queue = PlayQueue::new();
assert!(queue.is_empty());
queue.append(vec![make_song("1", "Song 1"), make_song("2", "Song 2")]);
assert_eq!(queue.len(), 2);
}
#[test]
fn test_position_and_navigation() {
let mut queue = PlayQueue::new();
queue.append(vec![
make_song("1", "Song 1"),
make_song("2", "Song 2"),
make_song("3", "Song 3"),
]);
assert!(queue.current().is_none());
queue.set_position(Some(0));
assert_eq!(queue.current().unwrap().id, "1");
assert!(queue.next());
assert_eq!(queue.current().unwrap().id, "2");
assert!(queue.next());
assert_eq!(queue.current().unwrap().id, "3");
assert!(!queue.next());
assert!(queue.current().is_none());
}
#[test]
fn test_remove() {
let mut queue = PlayQueue::new();
queue.append(vec![
make_song("1", "Song 1"),
make_song("2", "Song 2"),
make_song("3", "Song 3"),
]);
queue.set_position(Some(1));
// Remove song before current
queue.remove(0);
assert_eq!(queue.position(), Some(0));
assert_eq!(queue.current().unwrap().id, "2");
// Remove current song
queue.remove(0);
assert_eq!(queue.current().unwrap().id, "3");
}
#[test]
fn test_insert_next() {
let mut queue = PlayQueue::new();
queue.append(vec![make_song("1", "Song 1"), make_song("3", "Song 3")]);
queue.set_position(Some(0));
queue.insert_next(vec![make_song("2", "Song 2")]);
assert_eq!(queue.songs[0].id, "1");
assert_eq!(queue.songs[1].id, "2");
assert_eq!(queue.songs[2].id, "3");
}
#[test]
fn test_move_song() {
let mut queue = PlayQueue::new();
queue.append(vec![
make_song("1", "Song 1"),
make_song("2", "Song 2"),
make_song("3", "Song 3"),
]);
queue.set_position(Some(0));
queue.move_song(0, 2);
assert_eq!(queue.songs[0].id, "2");
assert_eq!(queue.songs[1].id, "3");
assert_eq!(queue.songs[2].id, "1");
assert_eq!(queue.position(), Some(2));
}
}

View File

@@ -30,9 +30,17 @@ pub struct Config {
/// Enable cava audio visualizer
#[serde(rename = "Cava", default)]
pub cava: bool,
/// Cava visualizer height percentage (10-80, step 5)
#[serde(rename = "CavaSize", default = "Config::default_cava_size")]
pub cava_size: u8,
}
impl Config {
fn default_cava_size() -> u8 {
40
}
/// Create a new empty config
pub fn new() -> Self {
Self::default()

View File

@@ -1,5 +1,3 @@
//! MPRIS2 D-Bus integration module
#![allow(dead_code)]
pub mod server;

View File

@@ -297,77 +297,23 @@ impl SubsonicClient {
Ok(url.to_string())
}
/// Parse song ID from a stream URL
///
/// Useful for session restoration
pub fn parse_song_id_from_url(url: &str) -> Option<String> {
let parsed = Url::parse(url).ok()?;
parsed
.query_pairs()
.find(|(k, _)| k == "id")
.map(|(_, v)| v.to_string())
}
/// Get cover art URL for a given cover art ID
pub fn get_cover_art_url(&self, cover_art_id: &str) -> Result<String, SubsonicError> {
let (salt, token) = generate_auth_params(&self.password);
let mut url = Url::parse(&format!("{}/rest/getCoverArt", self.base_url))?;
url.query_pairs_mut()
.append_pair("id", cover_art_id)
.append_pair("u", &self.username)
.append_pair("t", &token)
.append_pair("s", &salt)
.append_pair("v", API_VERSION)
.append_pair("c", CLIENT_NAME);
Ok(url.to_string())
}
/// Search for artists, albums, and songs
pub async fn search(
&self,
query: &str,
) -> Result<(Vec<Artist>, Vec<Album>, Vec<Child>), SubsonicError> {
let url = self.build_url(&format!("search3?query={}", urlencoding::encode(query)))?;
debug!("Searching: {}", query);
let response = self.http.get(url).send().await?;
let text = response.text().await?;
let parsed: SubsonicResponse<SearchResult3Data> = serde_json::from_str(&text)
.map_err(|e| SubsonicError::Parse(format!("Failed to parse search response: {}", e)))?;
if parsed.subsonic_response.status != "ok" {
if let Some(error) = parsed.subsonic_response.error {
return Err(SubsonicError::Api {
code: error.code,
message: error.message,
});
}
}
let result = parsed
.subsonic_response
.data
.ok_or_else(|| SubsonicError::Parse("Empty search data".to_string()))?
.search_result3;
debug!(
"Search found {} artists, {} albums, {} songs",
result.artist.len(),
result.album.len(),
result.song.len()
);
Ok((result.artist, result.album, result.song))
}
}
#[cfg(test)]
mod tests {
use super::*;
impl SubsonicClient {
/// Parse song ID from a stream URL
fn parse_song_id_from_url(url: &str) -> Option<String> {
let parsed = Url::parse(url).ok()?;
parsed
.query_pairs()
.find(|(k, _)| k == "id")
.map(|(_, v)| v.to_string())
}
}
#[test]
fn test_parse_song_id() {
let url = "https://example.com/rest/stream?id=12345&u=user&t=token&s=salt&v=1.16.1&c=test";

View File

@@ -1,7 +1,5 @@
//! Subsonic API client module
#![allow(dead_code)]
pub mod auth;
pub mod client;
pub mod models;

View File

@@ -12,6 +12,7 @@ pub struct SubsonicResponse<T> {
#[derive(Debug, Deserialize)]
pub struct SubsonicResponseInner<T> {
pub status: String,
#[allow(dead_code)] // Present in API response, needed for deserialization
pub version: String,
#[serde(default)]
pub error: Option<ApiError>,
@@ -40,6 +41,7 @@ pub struct ArtistsIndex {
#[derive(Debug, Deserialize)]
pub struct ArtistIndex {
#[allow(dead_code)] // Present in API response, needed for deserialization
pub name: String,
#[serde(default)]
pub artist: Vec<Artist>,
@@ -217,19 +219,3 @@ pub struct PlaylistDetail {
#[derive(Debug, Deserialize)]
pub struct PingData {}
/// Search result
#[derive(Debug, Deserialize)]
pub struct SearchResult3Data {
#[serde(rename = "searchResult3")]
pub search_result3: SearchResult3,
}
#[derive(Debug, Deserialize)]
pub struct SearchResult3 {
#[serde(default)]
pub artist: Vec<Artist>,
#[serde(default)]
pub album: Vec<Album>,
#[serde(default)]
pub song: Vec<Child>,
}

View File

@@ -28,7 +28,7 @@ pub fn draw(frame: &mut Frame, state: &mut AppState) {
let (header_area, cava_area, content_area, now_playing_area, footer_area) = if cava_active {
let chunks = Layout::vertical([
Constraint::Length(1), // Header
Constraint::Percentage(40), // Cava visualizer — top half-ish
Constraint::Percentage(state.settings_state.cava_size as u16), // Cava visualizer
Constraint::Min(10), // Page content
Constraint::Length(7), // Now playing
Constraint::Length(1), // Footer
@@ -62,10 +62,8 @@ pub fn draw(frame: &mut Frame, state: &mut AppState) {
// Store layout areas for mouse hit-testing
state.layout = LayoutAreas {
header: header_area,
cava: cava_area,
content: content_area,
now_playing: now_playing_area,
footer: footer_area,
content_left,
content_right,
};

View File

@@ -34,6 +34,8 @@ pub fn render(frame: &mut Frame, area: Rect, state: &AppState) {
Constraint::Length(2), // Theme selector
Constraint::Length(1), // Spacing
Constraint::Length(2), // Cava toggle
Constraint::Length(1), // Spacing
Constraint::Length(2), // Cava size
Constraint::Min(1), // Remaining space
])
.split(inner);
@@ -66,11 +68,29 @@ pub fn render(frame: &mut Frame, area: Rect, state: &AppState) {
&colors,
);
// Cava size (field 2)
let cava_size_value = if !state.cava_available {
"N/A (cava not found)".to_string()
} else {
format!("{}%", settings.cava_size)
};
render_option(
frame,
chunks[5],
"Cava Size",
&cava_size_value,
settings.selected_field == 2,
&colors,
);
// Help text at bottom
let help_text = match settings.selected_field {
0 => "← → or Enter to change theme (auto-saves)",
1 if state.cava_available => "← → or Enter to toggle cava visualizer (auto-saves)",
1 => "cava is not installed on this system",
2 if state.cava_available => "← → to adjust cava size (10%-80%, auto-saves)",
2 => "cava is not installed on this system",
_ => "",
};
let help = Paragraph::new(help_text).style(Style::default().fg(colors.muted));