1
0
Fork 0
mirror of https://github.com/librespot-org/librespot.git synced 2025-10-04 02:09:26 +02:00

Various code improvements (#777)

* Remove deprecated use of std::u16::MAX
* Use `FromStr` for fallible `&str` conversions
* DRY up strings into constants
* Change `as_ref().map()` into `as_deref()`
* Use `Duration` for time constants and functions
* Optimize `Vec` with response times
* Move comments for `rustdoc` to parse
This commit is contained in:
Roderick van Domburg 2021-05-31 22:32:39 +02:00 committed by GitHub
parent bae1834988
commit ad19b69bfb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 433 additions and 309 deletions

View file

@ -17,6 +17,8 @@ use librespot::playback::config::{
AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, VolumeCtrl,
};
use librespot::playback::dither;
#[cfg(feature = "alsa-backend")]
use librespot::playback::mixer::alsamixer::AlsaMixer;
use librespot::playback::mixer::mappings::MappedCtrl;
use librespot::playback::mixer::{self, MixerConfig, MixerFn};
use librespot::playback::player::{db_to_ratio, Player};
@ -24,17 +26,14 @@ use librespot::playback::player::{db_to_ratio, Player};
mod player_event_handler;
use player_event_handler::{emit_sink_event, run_program_on_events};
use std::convert::TryFrom;
use std::env;
use std::io::{stderr, Write};
use std::path::Path;
use std::pin::Pin;
use std::process::exit;
use std::str::FromStr;
use std::{env, time::Instant};
use std::{
io::{stderr, Write},
pin::Pin,
};
const MILLIS: f64 = 1000.0;
use std::time::Duration;
use std::time::Instant;
fn device_id(name: &str) -> String {
hex::encode(Sha1::digest(name.as_bytes()))
@ -189,176 +188,216 @@ struct Setup {
}
fn get_setup(args: &[String]) -> Setup {
const AP_PORT: &str = "ap-port";
const AUTOPLAY: &str = "autoplay";
const BACKEND: &str = "backend";
const BITRATE: &str = "b";
const CACHE: &str = "c";
const CACHE_SIZE_LIMIT: &str = "cache-size-limit";
const DEVICE: &str = "device";
const DEVICE_TYPE: &str = "device-type";
const DISABLE_AUDIO_CACHE: &str = "disable-audio-cache";
const DISABLE_DISCOVERY: &str = "disable-discovery";
const DISABLE_GAPLESS: &str = "disable-gapless";
const DITHER: &str = "dither";
const EMIT_SINK_EVENTS: &str = "emit-sink-events";
const ENABLE_VOLUME_NORMALISATION: &str = "enable-volume-normalisation";
const FORMAT: &str = "format";
const HELP: &str = "h";
const INITIAL_VOLUME: &str = "initial-volume";
const MIXER_CARD: &str = "mixer-card";
const MIXER_INDEX: &str = "mixer-index";
const MIXER_NAME: &str = "mixer-name";
const NAME: &str = "name";
const NORMALISATION_ATTACK: &str = "normalisation-attack";
const NORMALISATION_GAIN_TYPE: &str = "normalisation-gain-type";
const NORMALISATION_KNEE: &str = "normalisation-knee";
const NORMALISATION_METHOD: &str = "normalisation-method";
const NORMALISATION_PREGAIN: &str = "normalisation-pregain";
const NORMALISATION_RELEASE: &str = "normalisation-release";
const NORMALISATION_THRESHOLD: &str = "normalisation-threshold";
const ONEVENT: &str = "onevent";
const PASSTHROUGH: &str = "passthrough";
const PASSWORD: &str = "password";
const PROXY: &str = "proxy";
const SYSTEM_CACHE: &str = "system-cache";
const USERNAME: &str = "username";
const VERBOSE: &str = "verbose";
const VERSION: &str = "version";
const VOLUME_CTRL: &str = "volume-ctrl";
const VOLUME_RANGE: &str = "volume-range";
const ZEROCONF_PORT: &str = "zeroconf-port";
let mut opts = getopts::Options::new();
opts.optflag(
"h",
HELP,
"help",
"Print this help menu.",
).optopt(
"c",
CACHE,
"cache",
"Path to a directory where files will be cached.",
"PATH",
).optopt(
"",
"system-cache",
SYSTEM_CACHE,
"Path to a directory where system files (credentials, volume) will be cached. Can be different from cache option value.",
"PATH",
).optopt(
"",
"cache-size-limit",
CACHE_SIZE_LIMIT,
"Limits the size of the cache for audio files.",
"SIZE"
).optflag("", "disable-audio-cache", "Disable caching of the audio data.")
.optopt("n", "name", "Device name.", "NAME")
.optopt("", "device-type", "Displayed device type.", "TYPE")
).optflag("", DISABLE_AUDIO_CACHE, "Disable caching of the audio data.")
.optopt("n", NAME, "Device name.", "NAME")
.optopt("", DEVICE_TYPE, "Displayed device type.", "TYPE")
.optopt(
"b",
BITRATE,
"bitrate",
"Bitrate (kbps) {96|160|320}. Defaults to 160.",
"BITRATE",
)
.optopt(
"",
"onevent",
ONEVENT,
"Run PROGRAM when a playback event occurs.",
"PROGRAM",
)
.optflag("", "emit-sink-events", "Run program set by --onevent before sink is opened and after it is closed.")
.optflag("v", "verbose", "Enable verbose output.")
.optflag("V", "version", "Display librespot version string.")
.optopt("u", "username", "Username to sign in with.", "USERNAME")
.optopt("p", "password", "Password", "PASSWORD")
.optopt("", "proxy", "HTTP proxy to use when connecting.", "URL")
.optopt("", "ap-port", "Connect to AP with specified port. If no AP with that port are present fallback AP will be used. Available ports are usually 80, 443 and 4070.", "PORT")
.optflag("", "disable-discovery", "Disable discovery mode.")
.optflag("", EMIT_SINK_EVENTS, "Run program set by --onevent before sink is opened and after it is closed.")
.optflag("v", VERBOSE, "Enable verbose output.")
.optflag("V", VERSION, "Display librespot version string.")
.optopt("u", USERNAME, "Username to sign in with.", "USERNAME")
.optopt("p", PASSWORD, "Password", "PASSWORD")
.optopt("", PROXY, "HTTP proxy to use when connecting.", "URL")
.optopt("", AP_PORT, "Connect to AP with specified port. If no AP with that port are present fallback AP will be used. Available ports are usually 80, 443 and 4070.", "PORT")
.optflag("", DISABLE_DISCOVERY, "Disable discovery mode.")
.optopt(
"",
"backend",
BACKEND,
"Audio backend to use. Use '?' to list options.",
"NAME",
)
.optopt(
"",
"device",
DEVICE,
"Audio device to use. Use '?' to list options if using alsa, portaudio or rodio.",
"NAME",
)
.optopt(
"",
"format",
FORMAT,
"Output format {F64|F32|S32|S24|S24_3|S16}. Defaults to S16.",
"FORMAT",
)
.optopt(
"",
"dither",
DITHER,
"Specify the dither algorithm to use - [none, gpdf, tpdf, tpdf_hp]. Defaults to 'tpdf' for formats S16, S24, S24_3 and 'none' for other formats.",
"DITHER",
)
.optopt("", "mixer", "Mixer to use {alsa|softvol}.", "MIXER")
.optopt(
"m",
"mixer-name",
MIXER_NAME,
"Alsa mixer control, e.g. 'PCM' or 'Master'. Defaults to 'PCM'.",
"NAME",
)
.optopt(
"",
"mixer-card",
MIXER_CARD,
"Alsa mixer card, e.g 'hw:0' or similar from `aplay -l`. Defaults to DEVICE if specified, 'default' otherwise.",
"MIXER_CARD",
)
.optopt(
"",
"mixer-index",
MIXER_INDEX,
"Alsa index of the cards mixer. Defaults to 0.",
"INDEX",
)
.optopt(
"",
"initial-volume",
INITIAL_VOLUME,
"Initial volume in % from 0-100. Default for softvol: '50'. For the Alsa mixer: the current volume.",
"VOLUME",
)
.optopt(
"",
"zeroconf-port",
ZEROCONF_PORT,
"The port the internal server advertised over zeroconf uses.",
"PORT",
)
.optflag(
"",
"enable-volume-normalisation",
ENABLE_VOLUME_NORMALISATION,
"Play all tracks at the same volume.",
)
.optopt(
"",
"normalisation-method",
NORMALISATION_METHOD,
"Specify the normalisation method to use {basic|dynamic}. Defaults to dynamic.",
"METHOD",
)
.optopt(
"",
"normalisation-gain-type",
NORMALISATION_GAIN_TYPE,
"Specify the normalisation gain type to use {track|album}. Defaults to album.",
"TYPE",
)
.optopt(
"",
"normalisation-pregain",
NORMALISATION_PREGAIN,
"Pregain (dB) applied by volume normalisation. Defaults to 0.",
"PREGAIN",
)
.optopt(
"",
"normalisation-threshold",
NORMALISATION_THRESHOLD,
"Threshold (dBFS) to prevent clipping. Defaults to -1.0.",
"THRESHOLD",
)
.optopt(
"",
"normalisation-attack",
NORMALISATION_ATTACK,
"Attack time (ms) in which the dynamic limiter is reducing gain. Defaults to 5.",
"TIME",
)
.optopt(
"",
"normalisation-release",
NORMALISATION_RELEASE,
"Release or decay time (ms) in which the dynamic limiter is restoring gain. Defaults to 100.",
"TIME",
)
.optopt(
"",
"normalisation-knee",
NORMALISATION_KNEE,
"Knee steepness of the dynamic limiter. Defaults to 1.0.",
"KNEE",
)
.optopt(
"",
"volume-ctrl",
VOLUME_CTRL,
"Volume control type {cubic|fixed|linear|log}. Defaults to log.",
"VOLUME_CTRL"
)
.optopt(
"",
"volume-range",
VOLUME_RANGE,
"Range of the volume control (dB). Default for softvol: 60. For the Alsa mixer: what the control supports.",
"RANGE",
)
.optflag(
"",
"autoplay",
AUTOPLAY,
"Automatically play similar songs when your music ends.",
)
.optflag(
"",
"disable-gapless",
DISABLE_GAPLESS,
"Disable gapless playback.",
)
.optflag(
"",
"passthrough",
PASSTHROUGH,
"Pass raw stream to output, only works for pipe and subprocess.",
);
@ -374,17 +413,17 @@ fn get_setup(args: &[String]) -> Setup {
}
};
if matches.opt_present("h") {
if matches.opt_present(HELP) {
println!("{}", usage(&args[0], &opts));
exit(0);
}
if matches.opt_present("version") {
if matches.opt_present(VERSION) {
print_version();
exit(0);
}
let verbose = matches.opt_present("verbose");
let verbose = matches.opt_present(VERBOSE);
setup_logging(verbose);
info!(
@ -395,7 +434,7 @@ fn get_setup(args: &[String]) -> Setup {
build_id = version::BUILD_ID
);
let backend_name = matches.opt_str("backend");
let backend_name = matches.opt_str(BACKEND);
if backend_name == Some("?".into()) {
list_backends();
exit(0);
@ -404,40 +443,41 @@ fn get_setup(args: &[String]) -> Setup {
let backend = audio_backend::find(backend_name).expect("Invalid backend");
let format = matches
.opt_str("format")
.as_ref()
.map(|format| AudioFormat::try_from(format).expect("Invalid output format"))
.opt_str(FORMAT)
.as_deref()
.map(|format| AudioFormat::from_str(format).expect("Invalid output format"))
.unwrap_or_default();
let device = matches.opt_str("device");
let device = matches.opt_str(DEVICE);
if device == Some("?".into()) {
backend(device, format);
exit(0);
}
let mixer_name = matches.opt_str("mixer");
let mixer = mixer::find(mixer_name.as_ref()).expect("Invalid mixer");
let mixer_name = matches.opt_str(MIXER_NAME);
let mixer = mixer::find(mixer_name.as_deref()).expect("Invalid mixer");
let mixer_config = {
let card = matches.opt_str("mixer-card").unwrap_or_else(|| {
let card = matches.opt_str(MIXER_CARD).unwrap_or_else(|| {
if let Some(ref device_name) = device {
device_name.to_string()
} else {
String::from("default")
MixerConfig::default().card
}
});
let index = matches
.opt_str("mixer-index")
.opt_str(MIXER_INDEX)
.map(|index| index.parse::<u32>().unwrap())
.unwrap_or(0);
let control = matches
.opt_str("mixer-name")
.unwrap_or_else(|| String::from("PCM"));
.opt_str(MIXER_NAME)
.unwrap_or_else(|| MixerConfig::default().control);
let mut volume_range = matches
.opt_str("volume-range")
.opt_str(VOLUME_RANGE)
.map(|range| range.parse::<f64>().unwrap())
.unwrap_or_else(|| match mixer_name.as_ref().map(AsRef::as_ref) {
Some("alsa") => 0.0, // let Alsa query the control
.unwrap_or_else(|| match mixer_name.as_deref() {
#[cfg(feature = "alsa-backend")]
Some(AlsaMixer::NAME) => 0.0, // let Alsa query the control
_ => VolumeCtrl::DEFAULT_DB_RANGE,
});
if volume_range < 0.0 {
@ -449,8 +489,8 @@ fn get_setup(args: &[String]) -> Setup {
);
}
let volume_ctrl = matches
.opt_str("volume-ctrl")
.as_ref()
.opt_str(VOLUME_CTRL)
.as_deref()
.map(|volume_ctrl| {
VolumeCtrl::from_str_with_range(volume_ctrl, volume_range)
.expect("Invalid volume control type")
@ -472,26 +512,26 @@ fn get_setup(args: &[String]) -> Setup {
let cache = {
let audio_dir;
let system_dir;
if matches.opt_present("disable-audio-cache") {
if matches.opt_present(DISABLE_AUDIO_CACHE) {
audio_dir = None;
system_dir = matches
.opt_str("system-cache")
.or_else(|| matches.opt_str("c"))
.opt_str(SYSTEM_CACHE)
.or_else(|| matches.opt_str(CACHE))
.map(|p| p.into());
} else {
let cache_dir = matches.opt_str("c");
let cache_dir = matches.opt_str(CACHE);
audio_dir = cache_dir
.as_ref()
.map(|p| AsRef::<Path>::as_ref(p).join("files"));
system_dir = matches
.opt_str("system-cache")
.opt_str(SYSTEM_CACHE)
.or(cache_dir)
.map(|p| p.into());
}
let limit = if audio_dir.is_some() {
matches
.opt_str("cache-size-limit")
.opt_str(CACHE_SIZE_LIMIT)
.as_deref()
.map(parse_file_size)
.map(|e| {
@ -514,7 +554,7 @@ fn get_setup(args: &[String]) -> Setup {
};
let initial_volume = matches
.opt_str("initial-volume")
.opt_str(INITIAL_VOLUME)
.map(|initial_volume| {
let volume = initial_volume.parse::<u16>().unwrap();
if volume > 100 {
@ -523,18 +563,19 @@ fn get_setup(args: &[String]) -> Setup {
}
(volume as f32 / 100.0 * VolumeCtrl::MAX_VOLUME as f32) as u16
})
.or_else(|| match mixer_name.as_ref().map(AsRef::as_ref) {
Some("alsa") => None,
.or_else(|| match mixer_name.as_deref() {
#[cfg(feature = "alsa-backend")]
Some(AlsaMixer::NAME) => None,
_ => cache.as_ref().and_then(Cache::volume),
});
let zeroconf_port = matches
.opt_str("zeroconf-port")
.opt_str(ZEROCONF_PORT)
.map(|port| port.parse::<u16>().unwrap())
.unwrap_or(0);
let name = matches
.opt_str("name")
.opt_str(NAME)
.unwrap_or_else(|| "Librespot".to_string());
let credentials = {
@ -547,8 +588,8 @@ fn get_setup(args: &[String]) -> Setup {
};
get_credentials(
matches.opt_str("username"),
matches.opt_str("password"),
matches.opt_str(USERNAME),
matches.opt_str(PASSWORD),
cached_credentials,
password,
)
@ -560,7 +601,7 @@ fn get_setup(args: &[String]) -> Setup {
SessionConfig {
user_agent: version::VERSION_STRING.to_string(),
device_id,
proxy: matches.opt_str("proxy").or_else(|| std::env::var("http_proxy").ok()).map(
proxy: matches.opt_str(PROXY).or_else(|| std::env::var("http_proxy").ok()).map(
|s| {
match Url::parse(&s) {
Ok(url) => {
@ -578,41 +619,41 @@ fn get_setup(args: &[String]) -> Setup {
},
),
ap_port: matches
.opt_str("ap-port")
.opt_str(AP_PORT)
.map(|port| port.parse::<u16>().expect("Invalid port")),
}
};
let player_config = {
let bitrate = matches
.opt_str("b")
.as_ref()
.opt_str(BITRATE)
.as_deref()
.map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate"))
.unwrap_or_default();
let gapless = !matches.opt_present("disable-gapless");
let gapless = !matches.opt_present(DISABLE_GAPLESS);
let normalisation = matches.opt_present("enable-volume-normalisation");
let normalisation = matches.opt_present(ENABLE_VOLUME_NORMALISATION);
let normalisation_method = matches
.opt_str("normalisation-method")
.as_ref()
.opt_str(NORMALISATION_METHOD)
.as_deref()
.map(|method| {
NormalisationMethod::from_str(method).expect("Invalid normalisation method")
})
.unwrap_or_default();
let normalisation_type = matches
.opt_str("normalisation-gain-type")
.as_ref()
.opt_str(NORMALISATION_GAIN_TYPE)
.as_deref()
.map(|gain_type| {
NormalisationType::from_str(gain_type).expect("Invalid normalisation type")
})
.unwrap_or_default();
let normalisation_pregain = matches
.opt_str("normalisation-pregain")
.opt_str(NORMALISATION_PREGAIN)
.map(|pregain| pregain.parse::<f64>().expect("Invalid pregain float value"))
.unwrap_or(PlayerConfig::default().normalisation_pregain);
let normalisation_threshold = matches
.opt_str("normalisation-threshold")
.opt_str(NORMALISATION_THRESHOLD)
.map(|threshold| {
db_to_ratio(
threshold
@ -622,19 +663,23 @@ fn get_setup(args: &[String]) -> Setup {
})
.unwrap_or(PlayerConfig::default().normalisation_threshold);
let normalisation_attack = matches
.opt_str("normalisation-attack")
.map(|attack| attack.parse::<f64>().expect("Invalid attack float value") / MILLIS)
.opt_str(NORMALISATION_ATTACK)
.map(|attack| {
Duration::from_millis(attack.parse::<u64>().expect("Invalid attack value"))
})
.unwrap_or(PlayerConfig::default().normalisation_attack);
let normalisation_release = matches
.opt_str("normalisation-release")
.map(|release| release.parse::<f64>().expect("Invalid release float value") / MILLIS)
.opt_str(NORMALISATION_RELEASE)
.map(|release| {
Duration::from_millis(release.parse::<u64>().expect("Invalid release value"))
})
.unwrap_or(PlayerConfig::default().normalisation_release);
let normalisation_knee = matches
.opt_str("normalisation-knee")
.opt_str(NORMALISATION_KNEE)
.map(|knee| knee.parse::<f64>().expect("Invalid knee float value"))
.unwrap_or(PlayerConfig::default().normalisation_knee);
let ditherer_name = matches.opt_str("dither");
let ditherer_name = matches.opt_str(DITHER);
let ditherer = match ditherer_name.as_deref() {
// explicitly disabled on command line
Some("none") => None,
@ -654,7 +699,7 @@ fn get_setup(args: &[String]) -> Setup {
},
};
let passthrough = matches.opt_present("passthrough");
let passthrough = matches.opt_present(PASSTHROUGH);
PlayerConfig {
bitrate,
@ -674,12 +719,12 @@ fn get_setup(args: &[String]) -> Setup {
let connect_config = {
let device_type = matches
.opt_str("device-type")
.as_ref()
.opt_str(DEVICE_TYPE)
.as_deref()
.map(|device_type| DeviceType::from_str(device_type).expect("Invalid device type"))
.unwrap_or_default();
let has_volume_ctrl = !matches!(mixer_config.volume_ctrl, VolumeCtrl::Fixed);
let autoplay = matches.opt_present("autoplay");
let autoplay = matches.opt_present(AUTOPLAY);
ConnectConfig {
name,
@ -690,9 +735,9 @@ fn get_setup(args: &[String]) -> Setup {
}
};
let enable_discovery = !matches.opt_present("disable-discovery");
let player_event_program = matches.opt_str("onevent");
let emit_sink_events = matches.opt_present("emit-sink-events");
let enable_discovery = !matches.opt_present(DISABLE_DISCOVERY);
let player_event_program = matches.opt_str(ONEVENT);
let emit_sink_events = matches.opt_present(EMIT_SINK_EVENTS);
Setup {
format,
@ -714,8 +759,9 @@ fn get_setup(args: &[String]) -> Setup {
#[tokio::main(flavor = "current_thread")]
async fn main() {
if env::var("RUST_BACKTRACE").is_err() {
env::set_var("RUST_BACKTRACE", "full")
const RUST_BACKTRACE: &str = "RUST_BACKTRACE";
if env::var(RUST_BACKTRACE).is_err() {
env::set_var(RUST_BACKTRACE, "full")
}
let args: Vec<String> = std::env::args().collect();