// 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::{self, Config}, mpdpopm, }; use anyhow::{Context, Result, bail}; use clap::Parser; use tracing::{info, level_filters::LevelFilter}; use tracing_subscriber::{EnvFilter, Layer, Registry, layer::SubscriberExt}; use std::{io, path::PathBuf, sync::MutexGuard}; 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 `mpdopmd'. /// /// 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).with_context(|| { format!("Failed to parse config file at: `{}`", cfgpath.display()) })?, // 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: bail!( "No config file could be read at: `{}`, because: {err}", cfgpath.display() ) } } } 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() .context("Failed to construct env filter")?; 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)).context("Main mpdpopm failed") }