Skip to content

Commit 24350ba

Browse files
authored
V2.0.0 - Big changes! (#8)
* New and improved config file with room for adding more optional settings in the future * New `version` field in the config, to display warnings when breaking changes are introduced * New `max_lines` optional field in the config, to change the maximum number of options shown at once * Optimized parsing, now it's blazing fast! * Optimized (a lot) the station searching algorithm * Added the CLI option `-c` or `--config` to specify the config file to use * Added the CLI option `-d` or `--debug` to separate verbose and debug messages * Reorganized a bit the project structure, for more easy development (although there is some tidying needed in the main function) * Fixed exit error codes being wrong (#7 ) * Changed from `--no-video` to `--show-video` (so now the default is to **not** show the video) * And more!
1 parent 891d223 commit 24350ba

File tree

9 files changed

+374
-134
lines changed

9 files changed

+374
-134
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
authors = ["Marcos Gutiérrez Alonso <margual56@gmail.com>"]
33
description = "A simple radio cli for listening to your favourite streams from the console"
44
name = "radio-cli"
5-
version = "1.0.1"
5+
version = "2.0.0"
66
edition = "2021"
77
homepage = "https://github.com/margual56/radio-cli"
88
repository = "https://github.com/margual56/radio-cli"

config.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
{
2+
"config_version": "2.0.0",
3+
"max_lines": 7,
24
"data": [
35
{
46
"station": "Los 40 Principales",
@@ -25,4 +27,4 @@
2527
"url": "https://www.youtube.com/watch?v=5qap5aO4i9A"
2628
}
2729
]
28-
}
30+
}

src/lib/config.rs

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
extern crate xdg;
2+
3+
use crate::station::Station;
4+
use crate::version::Version;
5+
use crate::errors::{ConfigError, ConfigErrorCode};
6+
use crate::perror;
7+
8+
use serde::{Deserialize};
9+
use serde::de::{Deserializer, Visitor, Error as SeError};
10+
use std::fs::File;
11+
use std::io::{Write, Read};
12+
use std::path::PathBuf;
13+
use inquire::{error::InquireError, Select};
14+
use std::fmt::{Formatter, Result as ResultFmt};
15+
use colored::*;
16+
17+
const _CONFIG_URL: &str = "https://raw.githubusercontent.com/margual56/radio-cli/main/config.json";
18+
19+
#[derive(Deserialize, Debug, Clone)]
20+
pub struct Config {
21+
#[serde(deserialize_with = "deserialize_version")]
22+
pub config_version: Version,
23+
pub max_lines: Option<usize>,
24+
pub data: Vec<Station>
25+
}
26+
27+
impl Config {
28+
pub fn load_default() -> Result<Config, ConfigError> {
29+
// Load config.json from $XDG_CONFIG_HOME/radio-cli
30+
let xdg_dirs = xdg::BaseDirectories::with_prefix("radio-cli").unwrap();
31+
let config_file = Config::load_config(xdg_dirs);
32+
33+
Config::load(config_file)
34+
}
35+
36+
pub fn load_from_file(path: PathBuf) -> Result<Config, ConfigError> {
37+
Config::load(path)
38+
}
39+
40+
fn load(file: PathBuf) -> Result<Config, ConfigError> {
41+
let mut config_file = match File::open(file.to_path_buf()) {
42+
Ok(x) => x,
43+
Err(error) =>
44+
return Err(ConfigError {
45+
code: ConfigErrorCode::OpenError,
46+
message: format!("Could not open the file {:?}", file),
47+
extra: format!("{:?}", error),
48+
})
49+
};
50+
51+
// Read and parse the config into the `cfg` variable
52+
let mut config: String = String::new();
53+
match config_file.read_to_string(&mut config) {
54+
Ok(_) => {},
55+
Err(error) =>
56+
return Err(ConfigError {
57+
code: ConfigErrorCode::ReadError,
58+
message: format!("Couldn't read the file {:?}", file),
59+
extra: format!("{:?}", error),
60+
})
61+
}
62+
63+
let data: Config = match serde_json::from_str(&config) {
64+
Ok(x) => x,
65+
Err(error) =>
66+
return Err(ConfigError {
67+
code: ConfigErrorCode::ParseError,
68+
message: format!("Couldn't parse config"),
69+
extra: format!("{:?}", error),
70+
})
71+
};
72+
73+
Ok(data)
74+
}
75+
76+
fn load_config(dir: xdg::BaseDirectories) -> PathBuf {
77+
match dir.find_config_file("config.json") {
78+
None => {
79+
// Get the name of the directory
80+
let tmp = dir.get_config_file("");
81+
let dir_name: &str = match tmp.to_str() {
82+
Some(x) => x,
83+
None => "??"
84+
};
85+
86+
// Print an error message
87+
let msg = format!("The config file does not exist in \"{}\"", dir_name);
88+
perror(msg.as_str());
89+
90+
// Download the file
91+
println!("\tLoading file from {}...", _CONFIG_URL.italic());
92+
let resp = reqwest::blocking::get(_CONFIG_URL).expect("Request failed");
93+
let body = resp.text().expect("Body invalid");
94+
95+
// Create the new config file
96+
let file_ref = dir.place_config_file("config.json").expect("Could not create config file");
97+
98+
println!("\tDone loading!");
99+
100+
println!("\tTrying to open {} to write the config...", file_ref.to_str().expect("msg: &str").bold());
101+
102+
let mut file = File::create(file_ref.clone()).unwrap(); // This is write-only!!
103+
file.write_all(body.as_bytes()).expect("Could not write to config");
104+
105+
drop(file); // So we close the file to be able to read it
106+
107+
println!("\tFinished writing config. Enjoy! :)\n\n");
108+
109+
file_ref
110+
},
111+
Some(x) => x
112+
}
113+
}
114+
115+
pub fn get_url_for(self, station_name: &str) -> Option<String> {
116+
for s in self.data.iter() {
117+
if s.station.eq(station_name) {
118+
return Some(s.url.clone());
119+
}
120+
}
121+
122+
None
123+
}
124+
125+
pub fn get_all_stations(self) -> Vec<String> {
126+
let mut stations: Vec<String> = Vec::new();
127+
128+
for s in self.data.iter() {
129+
stations.push(s.station.clone());
130+
}
131+
132+
return stations;
133+
}
134+
135+
pub fn prompt(self) -> Result<Station, InquireError> {
136+
if let Some(lines) = self.max_lines {
137+
Select
138+
::new(&"Select a station to play:".bold(), self.data)
139+
.with_page_size(lines)
140+
.prompt()
141+
}else{
142+
Select
143+
::new(&"Select a station to play:".bold(), self.data)
144+
.prompt()
145+
}
146+
}
147+
}
148+
149+
fn deserialize_version<'de, D>(deserializer: D) -> Result<Version, D::Error> where D: Deserializer<'de>, {
150+
struct JsonStringVisitor;
151+
152+
impl<'de> Visitor<'de> for JsonStringVisitor {
153+
type Value = Version;
154+
155+
fn expecting(&self, formatter: &mut Formatter) -> ResultFmt {
156+
formatter.write_str("a string")
157+
}
158+
159+
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
160+
where
161+
E: SeError,
162+
{
163+
// unfortunately we lose some typed information
164+
// from errors deserializing the json string
165+
match Version::from(String::from(v)) {
166+
Some(x) => Ok(x),
167+
None => Err(SeError::custom("Could not parse version"))
168+
}
169+
}
170+
}
171+
172+
// use our visitor to deserialize an `ActualValue`
173+
deserializer.deserialize_any(JsonStringVisitor)
174+
}

src/lib/errors.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use std::fmt::{Display, Debug, Formatter, Result};
2+
3+
#[derive(Debug)]
4+
pub enum ConfigErrorCode {
5+
OpenError,
6+
ReadError,
7+
CloseError,
8+
ParseError,
9+
}
10+
11+
pub struct ConfigError {
12+
pub code: ConfigErrorCode,
13+
pub message: String,
14+
pub extra: String,
15+
}
16+
17+
// Implement std::fmt::Display for ConfigError
18+
impl Display for ConfigError {
19+
fn fmt(&self, f: &mut Formatter) -> Result {
20+
write!(f, "{}", self.message) // user-facing output
21+
}
22+
}
23+
24+
// Implement std::fmt::Debug for ConfigError
25+
impl Debug for ConfigError {
26+
fn fmt(&self, f: &mut Formatter) -> Result {
27+
write!(f, "{{ code: {:?}, message: \"{}\", info: {} }}", self.code, self.message, self.extra) // programmer-facing output
28+
}
29+
}

src/lib/lib.rs

Lines changed: 9 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,108 +1,14 @@
1-
extern crate xdg;
1+
mod errors;
2+
mod station;
3+
mod version;
4+
mod config;
25

3-
use serde::{Deserialize};
4-
use std::fs::File;
5-
use std::io::Write;
6-
use std::io::Read;
7-
use inquire::{error::InquireError, Select};
8-
use colored::*;
9-
10-
pub const CONFIG_URL: &str = "https://raw.githubusercontent.com/margual56/radio-cli/main/config.json";
11-
12-
#[derive(Deserialize, Debug, Clone)]
13-
pub struct Station {
14-
pub station: String,
15-
pub url: String
16-
}
17-
18-
#[derive(Deserialize, Debug, Clone)]
19-
pub struct Config {
20-
data: Vec<Station>
21-
}
22-
23-
impl Config {
24-
pub fn load() -> Config {
25-
// Load config.json from $XDG_CONFIG_HOME/radio-cli
26-
let xdg_dirs = xdg::BaseDirectories::with_prefix("radio-cli").unwrap();
27-
let mut config_file = Config::load_config(xdg_dirs);
28-
29-
// Read and parse the config into the `cfg` variable
30-
let mut config: String = String::new();
31-
config_file.read_to_string(&mut config).expect("Couldn't read config");
32-
33-
Config {
34-
data: serde_json::from_str::<Config>(&config).expect("Couldn't parse config").data,
35-
}
36-
}
37-
38-
fn load_config(dir: xdg::BaseDirectories) -> std::fs::File {
39-
match dir.find_config_file("config.json") {
40-
None => {
41-
// Get the name of the directory
42-
let tmp = dir.get_config_file("");
43-
let dir_name: &str = match tmp.to_str() {
44-
Some(x) => x,
45-
None => "??"
46-
};
47-
48-
// Print an error message
49-
let msg = format!("The config file does not exist in \"{}\"", dir_name);
50-
perror(msg.as_str());
51-
52-
// Download the file
53-
println!("\tLoading file from {}...", CONFIG_URL.italic());
54-
let resp = reqwest::blocking::get(CONFIG_URL).expect("Request failed");
55-
let body = resp.text().expect("Body invalid");
56-
57-
// Create the new config file
58-
let file_ref = dir.place_config_file("config.json").expect("Could not create config file");
59-
60-
println!("\tDone loading!");
6+
pub use station::Station;
7+
pub use version::Version;
8+
pub use errors::{ConfigError, ConfigErrorCode};
9+
pub use config::Config;
6110

62-
println!("\tTrying to open {} to write the config...", file_ref.to_str().expect("msg: &str").bold());
63-
64-
let mut file = File::create(file_ref.clone()).unwrap(); // This is write-only!!
65-
file.write_all(body.as_bytes()).expect("Could not write to config");
66-
67-
drop(file); // So we close the file to be able to read it
68-
69-
println!("\tFinished writing config. Enjoy! :)\n\n");
70-
71-
File::open(file_ref).unwrap() // This is read-only
72-
},
73-
Some(x) => File::open(x).expect("Could not open config")
74-
}
75-
}
76-
77-
pub fn get_url_for(self, station_name: &str) -> Result<String, ()> {
78-
for s in self.data.iter() {
79-
if s.station.eq(station_name) {
80-
return Ok(s.url.clone());
81-
}
82-
}
83-
84-
Err(())
85-
}
86-
87-
pub fn get_all_stations(self) -> Vec<String> {
88-
let mut stations: Vec<String> = Vec::new();
89-
90-
for s in self.data.iter() {
91-
stations.push(s.station.clone());
92-
}
93-
94-
return stations;
95-
}
96-
97-
pub fn prompt(self, options: Vec<String>) -> Result<String, InquireError> {
98-
let res: Result<&str, InquireError> = Select::new(&"Select a station to play:".bold(), options.iter().map(|s| s.as_ref()).collect()).prompt();
99-
100-
match res {
101-
Ok(s) => Ok(s.to_string()),
102-
Err(error) => Err(error)
103-
}
104-
}
105-
}
11+
use colored::*;
10612

10713
pub fn perror(msg: &str) {
10814
println!("{} {}", "Error:".red().bold(), msg);

src/lib/station.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use serde::Deserialize;
2+
#[derive(Deserialize, Debug, Clone)]
3+
pub struct Station {
4+
pub station: String,
5+
pub url: String
6+
}
7+
8+
impl std::fmt::Display for Station {
9+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
10+
write!(f, "{}", self.station)
11+
}
12+
}

0 commit comments

Comments
 (0)