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/lib.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/lib.rs')
| -rw-r--r-- | pkgs/by-name/mp/mpdpopm/src/lib.rs | 274 |
1 files changed, 274 insertions, 0 deletions
diff --git a/pkgs/by-name/mp/mpdpopm/src/lib.rs b/pkgs/by-name/mp/mpdpopm/src/lib.rs new file mode 100644 index 00000000..26645228 --- /dev/null +++ b/pkgs/by-name/mp/mpdpopm/src/lib.rs @@ -0,0 +1,274 @@ +// Copyright (C) 2020-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 +//! +//! Maintain ratings & playcounts for your mpd server. +//! +//! # Introduction +//! +//! This is a companion daemon for [mpd](https://www.musicpd.org/) that maintains play counts & +//! ratings. Similar to [mpdfav](https://github.com/vincent-petithory/mpdfav), but written in Rust +//! (which I prefer to Go), it will allow you to maintain that information in your tags, as well as +//! the sticker database, by invoking external commands to keep your tags up-to-date (something +//! along the lines of [mpdcron](https://alip.github.io/mpdcron)). +//! +//! # Commands +//! +//! I'm currently sending all commands over one (configurable) channel. +//! + +#![recursion_limit = "512"] // for the `select!' macro + +pub mod clients; +pub mod config; +pub mod filters_ast; +pub mod messages; +pub mod playcounts; +pub mod ratings; +pub mod storage; +pub mod vars; + +#[rustfmt::skip] +#[allow(clippy::extra_unused_lifetimes)] +#[allow(clippy::needless_lifetimes)] +#[allow(clippy::let_unit_value)] +#[allow(clippy::just_underscores_and_digits)] +pub mod filters { + include!(concat!(env!("OUT_DIR"), "/src/filters.rs")); +} + +use clients::{Client, IdleClient, IdleSubSystem}; +use config::Config; +use config::Connection; +use filters_ast::FilterStickerNames; +use messages::MessageProcessor; +use playcounts::PlayState; + +use backtrace::Backtrace; +use futures::{future::FutureExt, pin_mut, select}; +use tokio::{ + signal, + signal::unix::{SignalKind, signal}, + time::{Duration, sleep}, +}; +use tracing::{debug, error, info}; + +use std::path::PathBuf; + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +#[derive(Debug)] +#[non_exhaustive] +pub enum Error { + BadPath { + pth: PathBuf, + }, + Client { + source: crate::clients::Error, + back: Backtrace, + }, + Playcounts { + source: crate::playcounts::Error, + back: Backtrace, + }, +} + +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::BadPath { pth } => write!(f, "Bad path: {:?}", pth), + Error::Client { source, back: _ } => write!(f, "Client error: {}", source), + Error::Playcounts { source, back: _ } => write!(f, "Playcount error: {}", source), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match &self { + Error::Client { source, back: _ } => Some(source), + _ => None, + } + } +} + +pub type Result<T> = std::result::Result<T, Error>; + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Core `mppopmd' logic +pub async fn mpdpopm(cfg: Config) -> std::result::Result<(), Error> { + info!("mpdpopm {} beginning.", vars::VERSION); + + let filter_stickers = FilterStickerNames::new(); + + let mut client = match cfg.conn { + Connection::Local { ref path } => { + Client::open(path).await.map_err(|err| Error::Client { + source: err, + back: Backtrace::new(), + })? + } + Connection::TCP { ref host, port } => Client::connect(format!("{}:{}", host, port)) + .await + .map_err(|err| Error::Client { + source: err, + back: Backtrace::new(), + })?, + }; + + let mut state = PlayState::new(&mut client, cfg.played_thresh) + .await + .map_err(|err| Error::Client { + source: err, + back: Backtrace::new(), + })?; + + let mut idle_client = match cfg.conn { + Connection::Local { ref path } => { + IdleClient::open(path).await.map_err(|err| Error::Client { + source: err, + back: Backtrace::new(), + })? + } + Connection::TCP { ref host, port } => IdleClient::connect(format!("{}:{}", host, port)) + .await + .map_err(|err| Error::Client { + source: err, + back: Backtrace::new(), + })?, + }; + + idle_client + .subscribe(&cfg.commands_chan) + .await + .map_err(|err| Error::Client { + source: err, + back: Backtrace::new(), + })?; + + let mut hup = signal(SignalKind::hangup()).unwrap(); + let mut kill = signal(SignalKind::terminate()).unwrap(); + let ctrl_c = signal::ctrl_c().fuse(); + + let sighup = hup.recv().fuse(); + let sigkill = kill.recv().fuse(); + + let tick = sleep(Duration::from_millis(cfg.poll_interval_ms)).fuse(); + pin_mut!(ctrl_c, sighup, sigkill, tick); + + let mproc = MessageProcessor::new(); + + let mut done = false; + while !done { + debug!("selecting..."); + let mut msg_check_needed = false; + { + // `idle_client' mutably borrowed here + let mut idle = Box::pin(idle_client.idle().fuse()); + loop { + select! { + _ = ctrl_c => { + info!("got ctrl-C"); + done = true; + break; + }, + _ = sighup => { + info!("got SIGHUP"); + done = true; + break; + }, + _ = sigkill => { + info!("got SIGKILL"); + done = true; + break; + }, + _ = tick => { + tick.set(sleep(Duration::from_millis(cfg.poll_interval_ms)).fuse()); + state.update(&mut client) + .await + .map_err(|err| Error::Playcounts { + source: err, + back: Backtrace::new() + })? + }, + // next = cmds.next() => match next { + // Some(out) => { + // debug!("output status is {:#?}", out.out); + // match out.upd { + // Some(uri) => { + // debug!("{} needs to be updated", uri); + // client.update(&uri).await.map_err(|err| Error::Client { + // source: err, + // back: Backtrace::new(), + // })?; + // }, + // None => debug!("No database update needed"), + // } + // }, + // None => { + // debug!("No more commands to process."); + // } + // }, + res = idle => match res { + Ok(subsys) => { + debug!("subsystem {} changed", subsys); + if subsys == IdleSubSystem::Player { + state.update(&mut client) + .await + .map_err(|err| Error::Playcounts { + source: err, + back: Backtrace::new() + })? + } else if subsys == IdleSubSystem::Message { + msg_check_needed = true; + } + break; + }, + Err(err) => { + debug!("error {err:#?} on idle"); + done = true; + break; + } + } + } + } + } + + if msg_check_needed { + // Check for any messages that have come in; if there's an error there's not a lot we + // can do about it (suppose some client fat-fingers a command name, e.g.)-- just log it + // & move on. + if let Err(err) = mproc + .check_messages( + &mut client, + &mut idle_client, + state.last_status(), + &cfg.commands_chan, + &filter_stickers, + ) + .await + { + error!("Error while processing messages: {err:#?}"); + } + } + } + + info!("mpdpopm exiting."); + + Ok(()) +} |
