Skip to content

Commit 56d4721

Browse files
committed
bin: New --token arg for using Spotify access token
Provide a token with sufficient scopes or empty string to obtain new token. When obtaining a new token, use --token-port argument to specify the redirect port. Specify 0 to manually enter the auth code (headless). Re-arranged setup function so have session_config earlier for use with get_access_token().
1 parent 24fadda commit 56d4721

File tree

2 files changed

+169
-125
lines changed

2 files changed

+169
-125
lines changed

src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ pub use librespot_connect as connect;
55
pub use librespot_core as core;
66
pub use librespot_discovery as discovery;
77
pub use librespot_metadata as metadata;
8+
pub use librespot_oauth as oauth;
89
pub use librespot_playback as playback;
910
pub use librespot_protocol as protocol;

src/main.rs

+168-125
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,8 @@ fn get_setup() -> Setup {
234234
const QUIET: &str = "quiet";
235235
const SYSTEM_CACHE: &str = "system-cache";
236236
const TEMP_DIR: &str = "tmp";
237+
const TOKEN: &str = "token";
238+
const TOKEN_PORT: &str = "token-port";
237239
const USERNAME: &str = "username";
238240
const VERBOSE: &str = "verbose";
239241
const VERSION: &str = "version";
@@ -276,6 +278,8 @@ fn get_setup() -> Setup {
276278
const ALSA_MIXER_INDEX_SHORT: &str = "s";
277279
const ALSA_MIXER_CONTROL_SHORT: &str = "T";
278280
const TEMP_DIR_SHORT: &str = "t";
281+
const TOKEN_SHORT: &str = "k";
282+
const TOKEN_PORT_SHORT: &str = "K";
279283
const NORMALISATION_ATTACK_SHORT: &str = "U";
280284
const USERNAME_SHORT: &str = "u";
281285
const VERSION_SHORT: &str = "V";
@@ -415,6 +419,18 @@ fn get_setup() -> Setup {
415419
DEVICE_IS_GROUP,
416420
"Whether the device represents a group. Defaults to false.",
417421
)
422+
.optopt(
423+
TOKEN_SHORT,
424+
TOKEN,
425+
"Spotify access token to sign in with. Use empty string to obtain token.",
426+
"TOKEN",
427+
)
428+
.optopt(
429+
TOKEN_PORT_SHORT,
430+
TOKEN_PORT,
431+
"The port the oauth redirect server uses 1 - 65535. Ports <= 1024 may require root privileges.",
432+
"PORT",
433+
)
418434
.optopt(
419435
TEMP_DIR_SHORT,
420436
TEMP_DIR,
@@ -670,7 +686,10 @@ fn get_setup() -> Setup {
670686
trace!("Environment variable(s):");
671687

672688
for (k, v) in &env_vars {
673-
if matches!(k.as_str(), "LIBRESPOT_PASSWORD" | "LIBRESPOT_USERNAME") {
689+
if matches!(
690+
k.as_str(),
691+
"LIBRESPOT_PASSWORD" | "LIBRESPOT_USERNAME" | "LIBRESPOT_TOKEN"
692+
) {
674693
trace!("\t\t{k}=\"XXXXXXXX\"");
675694
} else if v.is_empty() {
676695
trace!("\t\t{k}=");
@@ -702,7 +721,10 @@ fn get_setup() -> Setup {
702721
&& matches.opt_defined(opt)
703722
&& matches.opt_present(opt)
704723
{
705-
if matches!(opt, PASSWORD | PASSWORD_SHORT | USERNAME | USERNAME_SHORT) {
724+
if matches!(
725+
opt,
726+
PASSWORD | PASSWORD_SHORT | USERNAME | USERNAME_SHORT | TOKEN | TOKEN_SHORT
727+
) {
706728
// Don't log creds.
707729
trace!("\t\t{opt} \"XXXXXXXX\"");
708730
} else {
@@ -1081,129 +1103,6 @@ fn get_setup() -> Setup {
10811103
}
10821104
};
10831105

1084-
let credentials = {
1085-
let cached_creds = cache.as_ref().and_then(Cache::credentials);
1086-
1087-
if let Some(username) = opt_str(USERNAME) {
1088-
if username.is_empty() {
1089-
empty_string_error_msg(USERNAME, USERNAME_SHORT);
1090-
}
1091-
if let Some(password) = opt_str(PASSWORD) {
1092-
if password.is_empty() {
1093-
empty_string_error_msg(PASSWORD, PASSWORD_SHORT);
1094-
}
1095-
Some(Credentials::with_password(username, password))
1096-
} else {
1097-
match cached_creds {
1098-
Some(creds) if Some(&username) == creds.username.as_ref() => Some(creds),
1099-
_ => {
1100-
let prompt = &format!("Password for {username}: ");
1101-
match rpassword::prompt_password(prompt) {
1102-
Ok(password) => {
1103-
if !password.is_empty() {
1104-
Some(Credentials::with_password(username, password))
1105-
} else {
1106-
trace!("Password was empty.");
1107-
if cached_creds.is_some() {
1108-
trace!("Using cached credentials.");
1109-
}
1110-
cached_creds
1111-
}
1112-
}
1113-
Err(e) => {
1114-
warn!("Cannot parse password: {}", e);
1115-
if cached_creds.is_some() {
1116-
trace!("Using cached credentials.");
1117-
}
1118-
cached_creds
1119-
}
1120-
}
1121-
}
1122-
}
1123-
}
1124-
} else {
1125-
if cached_creds.is_some() {
1126-
trace!("Using cached credentials.");
1127-
}
1128-
cached_creds
1129-
}
1130-
};
1131-
1132-
let enable_discovery = !opt_present(DISABLE_DISCOVERY);
1133-
1134-
if credentials.is_none() && !enable_discovery {
1135-
error!("Credentials are required if discovery is disabled.");
1136-
exit(1);
1137-
}
1138-
1139-
if !enable_discovery && opt_present(ZEROCONF_PORT) {
1140-
warn!(
1141-
"With the `--{}` / `-{}` flag set `--{}` / `-{}` has no effect.",
1142-
DISABLE_DISCOVERY, DISABLE_DISCOVERY_SHORT, ZEROCONF_PORT, ZEROCONF_PORT_SHORT
1143-
);
1144-
}
1145-
1146-
let zeroconf_port = if enable_discovery {
1147-
opt_str(ZEROCONF_PORT)
1148-
.map(|port| match port.parse::<u16>() {
1149-
Ok(value) if value != 0 => value,
1150-
_ => {
1151-
let valid_values = &format!("1 - {}", u16::MAX);
1152-
invalid_error_msg(ZEROCONF_PORT, ZEROCONF_PORT_SHORT, &port, valid_values, "");
1153-
1154-
exit(1);
1155-
}
1156-
})
1157-
.unwrap_or(0)
1158-
} else {
1159-
0
1160-
};
1161-
1162-
// #1046: not all connections are supplied an `autoplay` user attribute to run statelessly.
1163-
// This knob allows for a manual override.
1164-
let autoplay = match opt_str(AUTOPLAY) {
1165-
Some(value) => match value.as_ref() {
1166-
"on" => Some(true),
1167-
"off" => Some(false),
1168-
_ => {
1169-
invalid_error_msg(
1170-
AUTOPLAY,
1171-
AUTOPLAY_SHORT,
1172-
&opt_str(AUTOPLAY).unwrap_or_default(),
1173-
"on, off",
1174-
"",
1175-
);
1176-
exit(1);
1177-
}
1178-
},
1179-
None => SessionConfig::default().autoplay,
1180-
};
1181-
1182-
let zeroconf_ip: Vec<std::net::IpAddr> = if opt_present(ZEROCONF_INTERFACE) {
1183-
if let Some(zeroconf_ip) = opt_str(ZEROCONF_INTERFACE) {
1184-
zeroconf_ip
1185-
.split(',')
1186-
.map(|s| {
1187-
s.trim().parse::<std::net::IpAddr>().unwrap_or_else(|_| {
1188-
invalid_error_msg(
1189-
ZEROCONF_INTERFACE,
1190-
ZEROCONF_INTERFACE_SHORT,
1191-
s,
1192-
"IPv4 and IPv6 addresses",
1193-
"",
1194-
);
1195-
exit(1);
1196-
})
1197-
})
1198-
.collect()
1199-
} else {
1200-
warn!("Unable to use zeroconf-interface option, default to all interfaces.");
1201-
vec![]
1202-
}
1203-
} else {
1204-
vec![]
1205-
};
1206-
12071106
let connect_config = {
12081107
let connect_default_config = ConnectConfig::default();
12091108

@@ -1330,6 +1229,26 @@ fn get_setup() -> Setup {
13301229
}
13311230
};
13321231

1232+
// #1046: not all connections are supplied an `autoplay` user attribute to run statelessly.
1233+
// This knob allows for a manual override.
1234+
let autoplay = match opt_str(AUTOPLAY) {
1235+
Some(value) => match value.as_ref() {
1236+
"on" => Some(true),
1237+
"off" => Some(false),
1238+
_ => {
1239+
invalid_error_msg(
1240+
AUTOPLAY,
1241+
AUTOPLAY_SHORT,
1242+
&opt_str(AUTOPLAY).unwrap_or_default(),
1243+
"on, off",
1244+
"",
1245+
);
1246+
exit(1);
1247+
}
1248+
},
1249+
None => SessionConfig::default().autoplay,
1250+
};
1251+
13331252
let session_config = SessionConfig {
13341253
device_id: device_id(&connect_config.name),
13351254
proxy: opt_str(PROXY).or_else(|| std::env::var("http_proxy").ok()).map(
@@ -1364,6 +1283,130 @@ fn get_setup() -> Setup {
13641283
..SessionConfig::default()
13651284
};
13661285

1286+
let credentials = {
1287+
let cached_creds = cache.as_ref().and_then(Cache::credentials);
1288+
1289+
let token_port = if opt_present(TOKEN_PORT) {
1290+
opt_str(TOKEN_PORT)
1291+
.map(|port| match port.parse::<u16>() {
1292+
Ok(value) => value,
1293+
_ => {
1294+
let valid_values = &format!("1 - {}", u16::MAX);
1295+
invalid_error_msg(TOKEN_PORT, TOKEN_PORT_SHORT, &port, valid_values, "");
1296+
1297+
exit(1);
1298+
}
1299+
})
1300+
.unwrap_or(0)
1301+
} else {
1302+
5588
1303+
};
1304+
if let Some(mut access_token) = opt_str(TOKEN) {
1305+
if access_token.is_empty() {
1306+
access_token =
1307+
librespot::oauth::get_access_token(&session_config.client_id, token_port);
1308+
}
1309+
Some(Credentials::with_access_token(access_token))
1310+
} else if let Some(username) = opt_str(USERNAME) {
1311+
if username.is_empty() {
1312+
empty_string_error_msg(USERNAME, USERNAME_SHORT);
1313+
}
1314+
if let Some(password) = opt_str(PASSWORD) {
1315+
if password.is_empty() {
1316+
empty_string_error_msg(PASSWORD, PASSWORD_SHORT);
1317+
}
1318+
Some(Credentials::with_password(username, password))
1319+
} else {
1320+
match cached_creds {
1321+
Some(creds) if Some(&username) == creds.username.as_ref() => Some(creds),
1322+
_ => {
1323+
let prompt = &format!("Password for {username}: ");
1324+
match rpassword::prompt_password(prompt) {
1325+
Ok(password) => {
1326+
if !password.is_empty() {
1327+
Some(Credentials::with_password(username, password))
1328+
} else {
1329+
trace!("Password was empty.");
1330+
if cached_creds.is_some() {
1331+
trace!("Using cached credentials.");
1332+
}
1333+
cached_creds
1334+
}
1335+
}
1336+
Err(e) => {
1337+
warn!("Cannot parse password: {}", e);
1338+
if cached_creds.is_some() {
1339+
trace!("Using cached credentials.");
1340+
}
1341+
cached_creds
1342+
}
1343+
}
1344+
}
1345+
}
1346+
}
1347+
} else {
1348+
if cached_creds.is_some() {
1349+
trace!("Using cached credentials.");
1350+
}
1351+
cached_creds
1352+
}
1353+
};
1354+
1355+
let enable_discovery = !opt_present(DISABLE_DISCOVERY);
1356+
1357+
if credentials.is_none() && !enable_discovery {
1358+
error!("Credentials are required if discovery is disabled.");
1359+
exit(1);
1360+
}
1361+
1362+
if !enable_discovery && opt_present(ZEROCONF_PORT) {
1363+
warn!(
1364+
"With the `--{}` / `-{}` flag set `--{}` / `-{}` has no effect.",
1365+
DISABLE_DISCOVERY, DISABLE_DISCOVERY_SHORT, ZEROCONF_PORT, ZEROCONF_PORT_SHORT
1366+
);
1367+
}
1368+
1369+
let zeroconf_port = if enable_discovery {
1370+
opt_str(ZEROCONF_PORT)
1371+
.map(|port| match port.parse::<u16>() {
1372+
Ok(value) if value != 0 => value,
1373+
_ => {
1374+
let valid_values = &format!("1 - {}", u16::MAX);
1375+
invalid_error_msg(ZEROCONF_PORT, ZEROCONF_PORT_SHORT, &port, valid_values, "");
1376+
1377+
exit(1);
1378+
}
1379+
})
1380+
.unwrap_or(0)
1381+
} else {
1382+
0
1383+
};
1384+
1385+
let zeroconf_ip: Vec<std::net::IpAddr> = if opt_present(ZEROCONF_INTERFACE) {
1386+
if let Some(zeroconf_ip) = opt_str(ZEROCONF_INTERFACE) {
1387+
zeroconf_ip
1388+
.split(',')
1389+
.map(|s| {
1390+
s.trim().parse::<std::net::IpAddr>().unwrap_or_else(|_| {
1391+
invalid_error_msg(
1392+
ZEROCONF_INTERFACE,
1393+
ZEROCONF_INTERFACE_SHORT,
1394+
s,
1395+
"IPv4 and IPv6 addresses",
1396+
"",
1397+
);
1398+
exit(1);
1399+
})
1400+
})
1401+
.collect()
1402+
} else {
1403+
warn!("Unable to use zeroconf-interface option, default to all interfaces.");
1404+
vec![]
1405+
}
1406+
} else {
1407+
vec![]
1408+
};
1409+
13671410
let player_config = {
13681411
let player_default_config = PlayerConfig::default();
13691412

0 commit comments

Comments
 (0)