diff options
| author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2026-01-24 23:51:04 +0100 |
|---|---|---|
| committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2026-01-24 23:51:19 +0100 |
| commit | 38c6f95c94830ed8eb6c6678e303480257cf3cf5 (patch) | |
| tree | e5bd55a6306c5f787ff49d528810c62b5c5dbb32 /pkgs/by-name/mp/mpdpopm/src/config.rs | |
| parent | module/mpd: Set-up a sticker file (diff) | |
| download | nixos-config-38c6f95c94830ed8eb6c6678e303480257cf3cf5.zip | |
pkgs/mpdpopm: Init
This is based on https://github.com/sp1ff/mpdpopm at commit 178df8ad3a5c39281cfd8b3cec05394f4c9256fd.
Diffstat (limited to 'pkgs/by-name/mp/mpdpopm/src/config.rs')
| -rw-r--r-- | pkgs/by-name/mp/mpdpopm/src/config.rs | 275 |
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")); + } +} |
