about summary refs log tree commit diff stats
path: root/pkgs/by-name/mp/mpdpopm/src/config.rs
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-01-24 23:51:04 +0100
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-01-24 23:51:19 +0100
commit38c6f95c94830ed8eb6c6678e303480257cf3cf5 (patch)
treee5bd55a6306c5f787ff49d528810c62b5c5dbb32 /pkgs/by-name/mp/mpdpopm/src/config.rs
parentmodule/mpd: Set-up a sticker file (diff)
downloadnixos-config-38c6f95c94830ed8eb6c6678e303480257cf3cf5.zip
pkgs/mpdpopm: Init
This is based on https://github.com/sp1ff/mpdpopm at
commit 178df8ad3a5c39281cfd8b3cec05394f4c9256fd.
Diffstat (limited to '')
-rw-r--r--pkgs/by-name/mp/mpdpopm/src/config.rs275
1 files changed, 275 insertions, 0 deletions
diff --git a/pkgs/by-name/mp/mpdpopm/src/config.rs b/pkgs/by-name/mp/mpdpopm/src/config.rs
new file mode 100644
index 00000000..da8e63be
--- /dev/null
+++ b/pkgs/by-name/mp/mpdpopm/src/config.rs
@@ -0,0 +1,275 @@
+// Copyright (C) 2021-2025 Michael herstine <sp1ff@pobox.com>
+//
+// This file is part of mpdpopm.
+//
+// mpdpopm is free software: you can redistribute it and/or modify it under the terms of the GNU
+// General Public License as published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// mpdpopm is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
+// Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with mpdpopm.  If not,
+// see <http://www.gnu.org/licenses/>.
+
+//! # mpdpopm Configuration
+//!
+//! ## Introduction
+//!
+//! This module defines the configuration struct & handles deserialization thereof.
+//!
+//! ## Discussion
+//!
+//! In the first releases of [mpdpopm](crate) I foolishly forgot to add a version field to the
+//! configuration structure. I am now paying for my sin by having to attempt serializing two
+//! versions until one succeeds.
+//!
+//! The idiomatic approach to versioning [serde](https://docs.serde.rs/serde/) structs seems to be
+//! using an
+//! [enumeration](https://www.reddit.com/r/rust/comments/44dds3/handling_multiple_file_versions_with_serde_or/). This
+//! implementation *now* uses that, but that leaves us with the problem of handling the initial,
+//! un-tagged version. I proceed as follows:
+//!
+//!    1. attempt to deserialize as a member of the modern enumeration
+//!    2. if that succeeds, with the most-recent version, we're good
+//!    3. if that succeeds with an archaic version, convert to the most recent and warn the user
+//!    4. if that fails, attempt to deserialize as the initial struct version
+//!    5. if that succeeds, convert to the most recent & warn the user
+//!    6. if that fails, I'm kind of stuck because I don't know what the user was trying to express;
+//!       bundle-up all the errors, report 'em & urge the user to use the most recent version
+use crate::vars::{LOCALSTATEDIR, PREFIX};
+
+use serde::{Deserialize, Serialize};
+
+use std::path::PathBuf;
+
+/// [mpdpopm](crate) can communicate with MPD over either a local Unix socket, or over regular TCP
+#[derive(Debug, Deserialize, PartialEq, Serialize)]
+pub enum Connection {
+    /// Local Unix socket-- payload is the path to the socket
+    Local { path: PathBuf },
+    /// TCP-- payload is the hostname & port number
+    TCP { host: String, port: u16 },
+}
+
+#[cfg(test)]
+mod test_connection {
+    use super::Connection;
+
+    #[test]
+    fn test_serde() {
+        use serde_lexpr::to_string;
+
+        use std::path::PathBuf;
+
+        let text = to_string(&Connection::Local {
+            path: PathBuf::from("/var/run/mpd.sock"),
+        })
+        .unwrap();
+        assert_eq!(
+            text,
+            String::from(r#"(Local (path . "/var/run/mpd.sock"))"#)
+        );
+        let text = to_string(&Connection::TCP {
+            host: String::from("localhost"),
+            port: 6600,
+        })
+        .unwrap();
+        assert_eq!(
+            text,
+            String::from(r#"(TCP (host . "localhost") (port . 6600))"#)
+        );
+    }
+}
+
+impl std::default::Default for Connection {
+    fn default() -> Self {
+        Connection::TCP {
+            host: String::from("localhost"),
+            port: 6600,
+        }
+    }
+}
+
+/// This is the most recent `mppopmd` configuration struct.
+#[derive(Deserialize, Debug, Serialize)]
+#[serde(default)]
+pub struct Config {
+    /// Configuration format version-- must be "1"
+    // Workaround to https://github.com/rotty/lexpr-rs/issues/77
+    // When this gets fixed, I can remove this element from the struct & deserialize as
+    // a Configurations element-- the on-disk format will be the same.
+    #[serde(rename = "version")]
+    _version: String,
+
+    /// Location of log file
+    pub log: PathBuf,
+
+    /// How to connect to mpd
+    pub conn: Connection,
+
+    /// The `mpd' root music directory, relative to the host on which *this* daemon is running
+    pub local_music_dir: PathBuf,
+
+    /// Percentage threshold, expressed as a number between zero & one, for considering a song to
+    /// have been played
+    pub played_thresh: f64,
+
+    /// The interval, in milliseconds, at which to poll `mpd' for the current state
+    pub poll_interval_ms: u64,
+
+    /// Channel to setup for assorted commands-- channel names must satisfy "[-a-zA-Z-9_.:]+"
+    pub commands_chan: String,
+}
+
+impl Default for Config {
+    fn default() -> Config {
+        Config {
+            _version: String::from("1"),
+            log: [LOCALSTATEDIR, "log", "mppopmd.log"].iter().collect(),
+            conn: Connection::default(),
+            local_music_dir: [PREFIX, "Music"].iter().collect(),
+            played_thresh: 0.6,
+            poll_interval_ms: 5000,
+            commands_chan: String::from("unwoundstack.com:commands"),
+        }
+    }
+}
+
+#[derive(Debug)]
+pub enum Error {
+    /// Failure to parse
+    ParseFail { err: Box<dyn std::error::Error> },
+}
+
+impl std::fmt::Display for Error {
+    #[allow(unreachable_patterns)] // the _ arm is *currently* unreachable
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        match self {
+            Error::ParseFail { err } => write!(f, "Parse failure: {}", err),
+            _ => write!(f, "Unknown configuration error"),
+        }
+    }
+}
+
+pub type Result<T> = std::result::Result<T, Error>;
+
+pub fn from_str(text: &str) -> Result<Config> {
+    let cfg: Config = match serde_lexpr::from_str(text) {
+        Ok(cfg) => cfg,
+        Err(err_outer) => {
+            return Err(Error::ParseFail {
+                err: Box::new(err_outer),
+            });
+        }
+    };
+    Ok(cfg)
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn test_from_str() {
+        let cfg = Config::default();
+        assert_eq!(cfg.commands_chan, String::from("unwoundstack.com:commands"));
+
+        assert_eq!(
+            serde_lexpr::to_string(&cfg).unwrap(),
+            format!(
+                r#"((version . "1") (log . "{}/log/mppopmd.log") (conn TCP (host . "localhost") (port . 6600)) (local_music_dir . "{}/Music") (playcount_sticker . "unwoundstack.com:playcount") (lastplayed_sticker . "unwoundstack.com:lastplayed") (played_thresh . 0.6) (poll_interval_ms . 5000) (commands_chan . "unwoundstack.com:commands") (playcount_command . "") (playcount_command_args) (rating_sticker . "unwoundstack.com:rating") (ratings_command . "") (ratings_command_args) (gen_cmds))"#,
+                LOCALSTATEDIR, PREFIX
+            )
+        );
+
+        let cfg: Config = serde_lexpr::from_str(
+            r#"
+((version . "1")
+ (log . "/usr/local/var/log/mppopmd.log")
+ (conn TCP (host . "localhost") (port . 6600))
+ (local_music_dir . "/usr/local/Music")
+ (playcount_sticker . "unwoundstack.com:playcount")
+ (lastplayed_sticker . "unwoundstack.com:lastplayed")
+ (played_thresh . 0.6)
+ (poll_interval_ms . 5000)
+ (commands_chan . "unwoundstack.com:commands")
+ (playcount_command . "")
+ (playcount_command_args)
+ (rating_sticker . "unwoundstack.com:rating")
+ (ratings_command . "")
+ (ratings_command_args)
+ (gen_cmds))
+"#,
+        )
+        .unwrap();
+        assert_eq!(cfg._version, String::from("1"));
+
+        let cfg: Config = serde_lexpr::from_str(
+            r#"
+((version . "1")
+ (log . "/usr/local/var/log/mppopmd.log")
+ (conn Local (path . "/home/mgh/var/run/mpd/mpd.sock"))
+ (local_music_dir . "/usr/local/Music")
+ (playcount_sticker . "unwoundstack.com:playcount")
+ (lastplayed_sticker . "unwoundstack.com:lastplayed")
+ (played_thresh . 0.6)
+ (poll_interval_ms . 5000)
+ (commands_chan . "unwoundstack.com:commands")
+ (playcount_command . "")
+ (playcount_command_args)
+ (rating_sticker . "unwoundstack.com:rating")
+ (ratings_command . "")
+ (ratings_command_args)
+ (gen_cmds))
+"#,
+        )
+        .unwrap();
+        assert_eq!(cfg._version, String::from("1"));
+        assert_eq!(
+            cfg.conn,
+            Connection::Local {
+                path: PathBuf::from("/home/mgh/var/run/mpd/mpd.sock")
+            }
+        );
+
+        // Test fallback to "v0" of the config struct
+        let cfg = from_str(r#"
+((log . "/home/mgh/var/log/mppopmd.log")
+ (host . "192.168.1.14")
+ (port . 6600)
+ (local_music_dir . "/space/mp3")
+ (playcount_sticker . "unwoundstack.com:playcount")
+ (lastplayed_sticker . "unwoundstack.com:lastplayed")
+ (played_thresh . 0.6)
+ (poll_interval_ms . 5000)
+ (playcount_command . "/usr/local/bin/scribbu")
+ (playcount_command_args . ("popm" "-v" "-a" "-f" "-o" "sp1ff@pobox.com" "-C" "%playcount" "%full-file"))
+ (commands_chan . "unwoundstack.com:commands")
+ (rating_sticker . "unwoundstack.com:rating")
+ (ratings_command . "/usr/local/bin/scribbu")
+ (ratings_command_args . ("popm" "-v" "-a" "-f" "-o" "sp1ff@pobox.com" "-r" "%rating" "%full-file"))
+ (gen_cmds .
+	   (((name . "set-genre")
+	     (formal_parameters . (Literal Track))
+	     (default_after . 1)
+	     (cmd . "/usr/local/bin/scribbu")
+	     (args . ("genre" "-a" "-C" "-g" "%1" "%full-file"))
+	     (update . TrackOnly))
+	    ((name . "set-xtag")
+	     (formal_parameters . (Literal Track))
+	     (default_after . 1)
+	     (cmd . "/usr/local/bin/scribbu")
+	     (args . ("xtag" "-A" "-o" "sp1ff@pobox.com" "-T" "%1" "%full-file"))
+	     (update . TrackOnly))
+	    ((name . "merge-xtag")
+	     (formal_parameters . (Literal Track))
+	     (default_after . 1)
+	     (cmd . "/usr/local/bin/scribbu")
+	     (args . ("xtag" "-m" "-o" "sp1ff@pobox.com" "-T" "%1" "%full-file"))
+	     (update . TrackOnly)))))
+"#).unwrap();
+        assert_eq!(cfg.log, PathBuf::from("/home/mgh/var/log/mppopmd.log"));
+    }
+}