// Copyright (C) 2020-2025 Michael herstine // // 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 . //! # 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 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"), "/src/filters.rs")); } use crate::{ clients::{Client, IdleClient, IdleSubSystem}, config::{Config, Connection}, filters_ast::FilterStickerNames, 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 filter_stickers = FilterStickerNames::new(); 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")?, }; idle_client .subscribe(&cfg.commands_chan) .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; 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 { state.update(&mut client) .await .context("PlayState update failed")? } else if subsys == IdleSubSystem::Message { error!("Message handling is not supported!"); } break; }, Err(err) => { debug!("error {err:#?} on idle"); done = true; break; } } } } } } info!("mpdpopm exiting."); Ok(()) }