// 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 . //! # mppopmd //! //! 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)). use mpdpopm::config; use mpdpopm::config::Config; use mpdpopm::mpdpopm; use backtrace::Backtrace; use clap::Parser; use tracing::{info, level_filters::LevelFilter}; use tracing_subscriber::{EnvFilter, Layer, Registry, layer::SubscriberExt}; use std::{fmt, io, path::PathBuf, sync::MutexGuard}; //////////////////////////////////////////////////////////////////////////////////////////////////// // mppopmd application Error type // //////////////////////////////////////////////////////////////////////////////////////////////////// #[non_exhaustive] pub enum Error { NoConfigArg, NoConfig { config: std::path::PathBuf, cause: std::io::Error, }, Filter { source: tracing_subscriber::filter::FromEnvError, back: Backtrace, }, Fork { errno: errno::Errno, back: Backtrace, }, PathContainsNull { back: Backtrace, }, OpenLockFile { errno: errno::Errno, back: Backtrace, }, LockFile { errno: errno::Errno, back: Backtrace, }, WritePid { errno: errno::Errno, back: Backtrace, }, Config { source: crate::config::Error, back: Backtrace, }, MpdPopm { source: Box, 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::NoConfigArg => write!(f, "No configuration file given"), Error::NoConfig { config, cause } => { write!(f, "Configuration error ({:?}): {}", config, cause) } Error::Fork { errno, back: _ } => write!(f, "When forking, got errno {}", errno), Error::PathContainsNull { back: _ } => write!(f, "Path contains a null character"), Error::OpenLockFile { errno, back: _ } => { write!(f, "While opening lock file, got errno {}", errno) } Error::LockFile { errno, back: _ } => { write!(f, "While locking the lock file, got errno {}", errno) } Error::WritePid { errno, back: _ } => { write!(f, "While writing pid file, got errno {}", errno) } Error::Config { source, back: _ } => write!(f, "Configuration error: {}", source), Error::MpdPopm { source, back: _ } => write!(f, "mpdpopm error: {}", source), _ => write!(f, "Unknown mppopmd error"), } } } impl fmt::Debug for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self) } } type Result = std::result::Result<(), Error>; pub struct MyMutexGuardWriter<'a>(MutexGuard<'a, std::fs::File>); impl io::Write for MyMutexGuardWriter<'_> { #[inline] fn write(&mut self, buf: &[u8]) -> io::Result { self.0.write(buf) } #[inline] fn flush(&mut self) -> io::Result<()> { self.0.flush() } #[inline] fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result { self.0.write_vectored(bufs) } #[inline] fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { self.0.write_all(buf) } #[inline] fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> io::Result<()> { self.0.write_fmt(fmt) } } /// mpd + POPM /// /// `mppopmd' is a companion daemon for `mpd' that maintains playcounts & ratings, /// as well as implementing some handy functions. It maintains ratings & playcounts in the sticker /// database, but it allows you to keep that information in your tags, as well, by invoking external /// commands to keep your tags up-to-date. #[derive(Parser)] struct Args { /// path to configuration file #[arg(short, long)] config: Option, /// enable verbose logging #[arg(short, long)] verbose: bool, /// enable debug loggin (implies --verbose) #[arg(short, long)] debug: bool, } /// Entry point for `mppopmd'. /// /// Do *not* use the #[tokio::main] attribute here! If this program is asked to daemonize (the usual /// case), we will fork after tokio has started its thread pool, with disastrous consequences. /// Instead, stay synchronous until we've daemonized (or figured out that we don't need to), and /// only then fire-up the tokio runtime. fn main() -> Result { use mpdpopm::vars::VERSION; let args = Args::parse(); let config = if let Some(cfgpath) = &args.config { match std::fs::read_to_string(cfgpath) { Ok(text) => config::from_str(&text).map_err(|err| Error::Config { source: err, back: Backtrace::new(), })?, // The config file (defaulted or not) either didn't exist, or we were unable to read its // contents... Err(err) => { // Either they did _not_, in which case they probably want to know that the config // file they explicitly asked for does not exist, or there was some other problem, // in which case we're out of options, anyway. Either way: return Err(Error::NoConfig { config: PathBuf::from(cfgpath), cause: err, }); } } } else { Config::default() }; // `--verbose' & `--debug' work as follows: if `--debug' is present, log at level Trace, no // matter what. Else, if `--verbose' is present, log at level Debug. Else, log at level Info. let lf = match (args.verbose, args.debug) { (_, true) => LevelFilter::TRACE, (true, false) => LevelFilter::DEBUG, _ => LevelFilter::INFO, }; let filter = EnvFilter::builder() .with_default_directive(lf.into()) .from_env() .map_err(|err| Error::Filter { source: err, back: Backtrace::new(), })?; let formatter: Box + Send + Sync> = { Box::new( tracing_subscriber::fmt::Layer::default() .compact() .with_writer(io::stdout), ) }; tracing::subscriber::set_global_default(Registry::default().with(formatter).with(filter)) .unwrap(); info!("mppopmd {VERSION} logging at level {lf:#?}."); let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(mpdpopm(config)).map_err(|err| Error::MpdPopm { source: Box::new(err), back: Backtrace::new(), }) }