about summary refs log tree commit diff stats
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.rs207
1 files changed, 207 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..4fe523ea
--- /dev/null
+++ b/pkgs/by-name/mp/mpdpopm/src/lib.rs
@@ -0,0 +1,207 @@
+// 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 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,
+    messages::MessageProcessor,
+    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 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
+                                .context("PlayState update failed")?
+                    },
+                    // 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
+                                    .context("PlayState update 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 {
+            // 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(())
+}