aboutsummaryrefslogtreecommitdiffstats
path: root/pkgs/by-name/mp/mpdpopm/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--pkgs/by-name/mp/mpdpopm/src/lib.rs185
1 files changed, 185 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..6d04a527
--- /dev/null
+++ b/pkgs/by-name/mp/mpdpopm/src/lib.rs
@@ -0,0 +1,185 @@
+// 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 dj;
+pub mod filters_ast;
+pub mod messanges;
+pub mod playcounts;
+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"), "/filters.rs"));
+}
+
+use crate::{
+ clients::{Client, IdleClient, IdleSubSystem},
+ config::{Config, Connection},
+ messanges::{COMMAND_CHANNEL, MessageQueue},
+ playcounts::PlayState,
+};
+
+use anyhow::{Context, Error};
+use futures::{future::FutureExt, pin_mut, select};
+use tokio::{
+ signal,
+ signal::unix::{SignalKind, signal},
+ time::{Duration, sleep},
+};
+use tracing::{debug, error, info};
+
+/// Core `mppopmd' logic
+pub async fn mpdpopm(cfg: Config) -> std::result::Result<(), Error> {
+ info!("mpdpopm {} beginning.", vars::VERSION);
+
+ let mut client =
+ match cfg.conn {
+ Connection::Local { ref path } => Client::open(path)
+ .await
+ .with_context(|| format!("Failed to open socket at `{}`", path.display()))?,
+ Connection::TCP { ref host, port } => Client::connect(format!("{}:{}", host, port))
+ .await
+ .with_context(|| format!("Failed to connect to client at `{}:{}`", host, port))?,
+ };
+
+ let mut state = PlayState::new(&mut client, cfg.played_thresh)
+ .await
+ .context("Failed to construct PlayState")?;
+
+ let mut idle_client = match cfg.conn {
+ Connection::Local { ref path } => IdleClient::open(path)
+ .await
+ .context("Failed to open idle client")?,
+ Connection::TCP { ref host, port } => IdleClient::connect(format!("{}:{}", host, port))
+ .await
+ .context("Failed to connect to TCP idle client")?,
+ };
+
+ let mut mqueue = MessageQueue::new(cfg.mode);
+
+ idle_client
+ .subscribe(COMMAND_CHANNEL)
+ .await
+ .context("Failed to subscribe to idle_client")?;
+
+ 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 mut done = false;
+ let mut msg_check_needed = false;
+ while !done {
+ debug!("selecting...");
+ {
+ // `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
+ .context("PlayState update failed")?;
+ },
+ res = idle => match res {
+ Ok(subsys) => {
+ debug!("subsystem {} changed", subsys);
+ if subsys == IdleSubSystem::Player {
+ if state.update(&mut client)
+ .await
+ .context("PlayState update failed")? {
+ mqueue
+ .advance_dj(&mut client)
+ .await
+ .context("MessageQueue tick failed")?;
+ }
+ } else if subsys == IdleSubSystem::Message {
+ msg_check_needed = true;
+ }
+ break;
+ },
+ Err(err) => {
+ debug!("error {err:#?} on idle");
+ done = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if msg_check_needed {
+ msg_check_needed = false;
+
+ // 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) = mqueue.check_messages(&mut client, &mut idle_client).await {
+ error!("Error while processing messages: {err:#?}");
+ }
+ }
+ }
+
+ info!("mpdpopm exiting.");
+
+ Ok(())
+}