// 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 . //! Logic for rating MPD tracks. //! //! # Introduction //! //! This module contains types implementing a basic rating functionality for //! [MPD](http://www.musicpd.org). //! //! # Discussion //! //! Rating messages to the relevant channel take the form `RATING( TRACK)?` (the two components can //! be separated by any whitespace). The rating can be given by an integer between 0 & 255 //! (inclusive) represented in base ten, or as one-to-five asterisks (i.e. `\*{1,5}`). In the latter //! case, the rating will be mapped to 1-255 as per Winamp's //! [convention](http://forums.winamp.com/showpost.php?p=2903240&postcount=94): //! //! - 224-255: 5 stars when READ with windows explorer, writes 255 //! - 160-223: 4 stars when READ with windows explorer, writes 196 //! - 096-159: 3 stars when READ with windows explorer, writes 128 //! - 032-095: 2 stars when READ with windows explorer, writes 64 //! - 001-031: 1 stars when READ with windows explorer, writes 1 //! //! NB a rating of zero means "not rated". //! //! Everything after the first whitepace, if present, is taken to be the track to be rated (i.e. //! the track may contain whitespace). If omitted, the rating is taken to apply to the current //! track. use backtrace::Backtrace; use std::path::PathBuf; //////////////////////////////////////////////////////////////////////////////////////////////////// // Error // //////////////////////////////////////////////////////////////////////////////////////////////////// /// An enumeration of ratings errors #[derive(Debug)] pub enum Error { Rating { source: std::num::ParseIntError, text: String, }, PlayerStopped, NotImplemented { feature: String, }, BadPath { pth: PathBuf, back: Backtrace, }, Client { source: crate::clients::Error, back: Backtrace, }, } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Error::Rating { source, text } => write!( f, "Unable to interpret ``{}'' as a rating: {}", text, source ), Error::PlayerStopped => write!(f, "Player stopped"), Error::NotImplemented { feature } => write!(f, "{} not implemented", feature), Error::BadPath { pth, back: _ } => write!(f, "Bad path: {:?}", pth), Error::Client { source, back: _ } => write!(f, "Client error: {}", source), } } } impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match &self { Error::Rating { text: _, source } => Some(source), Error::Client { source, back: _ } => Some(source), _ => None, } } } pub type Result = std::result::Result; //////////////////////////////////////////////////////////////////////////////////////////////////// // RatingRequest message // //////////////////////////////////////////////////////////////////////////////////////////////////// /// The track to which a rating shall be applied. #[derive(Debug, PartialEq)] pub enum RatedTrack { Current, File(std::path::PathBuf), Relative(i8), } /// A request from a client to rate a track. #[derive(Debug)] pub struct RatingRequest { pub rating: u8, pub track: RatedTrack, } /// Produce a RatingRequest instance from a line of MPD output. impl std::convert::TryFrom<&str> for RatingRequest { type Error = Error; /// Attempt to produce a RatingRequest instance from a line of MPD response to a /// "readmessages" command. After the channel line, each subsequent line will be of the form /// "message: $MESSAGE"-- this method assumes that the "message: " prefix has been stripped off /// (i.e. we're dealing with a single line of text containing only our custom message format). /// /// For ratings, we expect a message of the form: "RATING (TRACK)?". fn try_from(text: &str) -> std::result::Result { // We expect a message of the form: "RATING (TRACK)?"; let us split `text' into those two // components for separate processing: let text = text.trim(); let (rating, track) = match text.find(char::is_whitespace) { Some(idx) => (&text[..idx], &text[idx + 1..]), None => (text, ""), }; // Rating first-- the desired rating can be specified in a few ways... let rating = if rating.is_empty() { // an empty string is interpreted as zero: 0u8 } else { // "*{1,5}" is interpreted as one-five stars, mapped to [0,255] as per Winamp: match rating { "*" => 1, "**" => 64, "***" => 128, "****" => 196, "*****" => 255, // failing that, we try just interperting `rating' as an unsigned integer: _ => rating.parse::().map_err(|err| Error::Rating { source: err, text: String::from(rating), })?, } }; // Next-- track. This, too, can be given in a few ways: let track = if track.is_empty() { // nothing at all just means "current track" RatedTrack::Current } else { // otherwise... match text.parse::() { // if we can interpret `track' as an i8, we take it as an offset... Ok(i) => RatedTrack::Relative(i), // else, we assume it's a path. If it's not, we'll figure that out downstream. Err(_) => RatedTrack::File(std::path::PathBuf::from(&track)), } }; Ok(RatingRequest { rating, track }) } } #[cfg(test)] mod rating_request_tests { use super::*; use std::convert::TryFrom; /// RatingRequest smoke tests #[test] fn rating_request_smoke() { let req = RatingRequest::try_from("*** foo bar splat.mp3").unwrap(); assert_eq!(req.rating, 128); assert_eq!( req.track, RatedTrack::File(PathBuf::from("foo bar splat.mp3")) ); let req = RatingRequest::try_from("255").unwrap(); assert_eq!(req.rating, 255); assert_eq!(req.track, RatedTrack::Current); let _req = RatingRequest::try_from("******").unwrap_err(); } }