1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
|
// 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/>.
//! 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<T> = std::result::Result<T, Error>;
////////////////////////////////////////////////////////////////////////////////////////////////////
// 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<Self, Self::Error> {
// 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::<u8>().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::<i8>() {
// 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();
}
}
|