diff --git a/Cargo.lock b/Cargo.lock index ccaab27..7038500 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1297,7 +1297,7 @@ dependencies = [ [[package]] name = "radio-cli" -version = "2.2.1" +version = "2.3.0" dependencies = [ "clap", "clap-verbosity-flag", diff --git a/Cargo.toml b/Cargo.toml index e191aa5..a53e7e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ authors = ["Marcos GutiƩrrez Alonso "] description = "A simple radio cli for listening to your favourite streams from the console" name = "radio-cli" -version = "2.2.1" +version = "2.3.0" edition = "2021" homepage = "https://github.com/margual56/radio-cli" repository = "https://github.com/margual56/radio-cli" diff --git a/README.md b/README.md index 4ece23f..ef3db9d 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ The license is GPLv2 Don't be surprised if these are not implemented in the end hehe (if there is no interest in the project, certainly not) - [x] ~Audio (mpv) controls when not in verbose mode~ -- [ ] Loop to selection list when pressing `q` while playing +- [x] Loop to selection list when pressing `q` while playing - [x] ~Some kind of online updating of the list of stations~ _(kind of)_ - [x] ~Code optimizations/beautification~ - [x] ~Search international online radios~ diff --git a/src/lib/browser.rs b/src/lib/browser.rs index 35d08b9..b0c56af 100644 --- a/src/lib/browser.rs +++ b/src/lib/browser.rs @@ -1,12 +1,15 @@ use std::error::Error; +use std::rc::Rc; use crate::{station::Station, Config}; use inquire::{error::InquireError, Autocomplete, Text}; use radiobrowser::{blocking::RadioBrowserAPI, ApiCountry, ApiStation, StationOrder}; +pub type StationCache = Rc>; + #[derive(Debug, Clone)] pub struct Stations { - stations: Vec, + stations: StationCache, } impl Autocomplete for Stations { @@ -39,39 +42,47 @@ impl Autocomplete for Stations { pub struct Browser { api: RadioBrowserAPI, - config: Config, - stations: Vec, + config: Rc, + stations: StationCache, } impl Browser { - pub fn new(config: Config) -> Result> { + pub fn new( + config: Rc, + cached_stations: Option, + ) -> Result<(Browser, StationCache), Box> { let api = match RadioBrowserAPI::new() { Ok(r) => r, Err(e) => return Err(e), }; - let stations = if let Some(code) = &config.country_code { - match api - .get_stations() - .countrycode(code) - .order(StationOrder::Clickcount) - .send() - { - Ok(s) => s, - Err(_e) => Vec::new(), - } - } else { - match api.get_stations().order(StationOrder::Clickcount).send() { - Ok(s) => s, - Err(_e) => Vec::new(), - } - }; + let stations = cached_stations.unwrap_or_else(|| { + Rc::new(if let Some(code) = &config.country_code { + match api + .get_stations() + .countrycode(code) + .order(StationOrder::Clickcount) + .send() + { + Ok(s) => s, + Err(_e) => Vec::new(), + } + } else { + match api.get_stations().order(StationOrder::Clickcount).send() { + Ok(s) => s, + Err(_e) => Vec::new(), + } + }) + }); - Ok(Browser { - api, - config, + Ok(( + Browser { + api, + config, + stations: stations.clone(), + }, stations, - }) + )) } pub fn get_countries() -> Result, Box> { diff --git a/src/lib/cli_args.rs b/src/lib/cli_args.rs index c913b4e..3844ba7 100644 --- a/src/lib/cli_args.rs +++ b/src/lib/cli_args.rs @@ -51,6 +51,13 @@ pub struct Cli { )] pub list_countries: bool, + /// Flag: --no-station-cache: Don't cache the station list loaded from the internet. + #[clap( + long = "no-station-cache", + help = "Don't cache the station list loaded from the internet." + )] + pub no_station_cache: bool, + /// Show extra info #[clap(flatten)] pub verbose: clap_verbosity_flag::Verbosity, diff --git a/src/lib/config.rs b/src/lib/config.rs index 357d48b..644be6b 100644 --- a/src/lib/config.rs +++ b/src/lib/config.rs @@ -6,7 +6,6 @@ use crate::station::Station; use crate::version::Version; use colored::*; -use inquire::{error::InquireError, Select}; use serde::de::{Deserializer, Error as SeError, Visitor}; use serde::Deserialize; use std::fmt::{Formatter, Result as ResultFmt}; @@ -14,8 +13,6 @@ use std::fs::File; use std::io::{Read, Write}; use std::path::PathBuf; -use crate::browser::Browser; - const _CONFIG_URL: &str = "https://raw.githubusercontent.com/margual56/radio-cli/main/config.json"; #[derive(Deserialize, Debug, Clone)] @@ -134,7 +131,7 @@ impl Config { } } - pub fn get_url_for(self, station_name: &str) -> Option { + pub fn get_url_for(&self, station_name: &str) -> Option { for s in self.data.iter() { if s.station.eq(station_name) { return Some(s.url.clone()); @@ -153,45 +150,6 @@ impl Config { stations } - - /// Prompts the user to select a station. - /// Returns a station and if the station was taken from the internet. - pub fn prompt(self) -> Result<(Station, bool), InquireError> { - let max_lines: usize = match self.max_lines { - Some(x) => x, - None => Select::::DEFAULT_PAGE_SIZE, - }; - - let res = Select::new(&"Select a station to play:".bold(), self.data.clone()) - .with_page_size(max_lines) - .prompt(); - - let internet: bool; - let station: Station = match res { - Ok(s) => { - if s.station.eq("Other") { - internet = true; - let result = Browser::new(self); - - let brow = match result { - Ok(b) => b, - Err(_e) => return Err(InquireError::OperationInterrupted), - }; - - match brow.prompt() { - Ok(r) => r, - Err(e) => return Err(e), - } - } else { - internet = false; - s - } - } - Err(e) => return Err(e), - }; - - Ok((station, internet)) - } } fn deserialize_version<'de, D>(deserializer: D) -> Result diff --git a/src/main.rs b/src/main.rs index f2759aa..2ce75b8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,14 @@ use clap::Parser; use colored::*; +use inquire::{InquireError, Select}; use log::{debug, error, info, log_enabled, warn}; -use radio_libs::{browser::Browser, perror, Cli, Config, ConfigError, Station, Version}; +use radio_libs::{ + browser::{Browser, StationCache}, + perror, Cli, Config, ConfigError, Station, Version, +}; use std::io::Write; use std::process::{Command, Stdio}; +use std::rc::Rc; fn main() { let version = match Version::from(String::from(env!("CARGO_PKG_VERSION"))) { @@ -49,7 +54,7 @@ fn main() { Err(error) => { debug!("{:?}", error); error!("{}", error); - info!("{}", "Try pasing the debug flag (-vvv). ".yellow()); + info!("{}", "Try passing the debug flag (-vvv). ".yellow()); info!( "{}", @@ -61,6 +66,7 @@ fn main() { std::process::exit(1); } }; + let config = Rc::new(config); debug!( "{} {}", @@ -95,47 +101,64 @@ fn main() { ); } - let station = match args.url { - None => { - let (station, internet) = get_station(args.station, config); + let mut url = args.url; + let mut station_arg = args.station; + let mut cached_stations = None; + loop { + let station = match url { + None => { + let (station, internet, updated_cached_stations) = + get_station(station_arg, config.clone(), cached_stations.clone()); + if !args.no_station_cache { + cached_stations = updated_cached_stations; + } - print!("Playing {}", station.station.green()); + print!("Playing {}", station.station.green()); - if internet { - println!(" ({})", station.url.yellow().italic()); - } else { - println!(); - } + if internet { + println!(" ({})", station.url.yellow().italic()); + } else { + println!(); + } - station - } + station + } - Some(x) => { - println!("Playing url '{}'", x.blue()); + Some(x) => { + println!("Playing url '{}'", x.blue()); - Station { - station: String::from("URL"), - url: x, + Station { + station: String::from("URL"), + url: x, + } } - } - }; + }; - println!("{}", "Info: press 'q' to exit".italic().bright_black()); + // Don't play the same station again when returning to the browser + url = None; + station_arg = None; - let output_status = run_mpv(station, args.show_video); + println!( + "{}", + "Info: press 'q' to stop playing this station" + .italic() + .bright_black() + ); - if !output_status.success() { - perror(format!("mpv {}", output_status).as_str()); + let output_status = run_mpv(station, args.show_video); + if !output_status.success() { + perror(format!("mpv {}", output_status).as_str()); - if !log_enabled!(log::Level::Info) { - println!( - "{}: {}", - "Hint".italic().bold(), - "Try running radio-cli with the verbose flag (-vv or -vvv)".italic() - ); - } + if !log_enabled!(log::Level::Info) { + println!( + "{}: {}", + "Hint".italic().bold(), + "Try running radio-cli with the verbose flag (-vv or -vvv)".italic() + ); + } - std::process::exit(2); + std::process::exit(2); + } } } @@ -168,14 +191,18 @@ fn run_mpv(station: Station, show_video: bool) -> std::process::ExitStatus { output.status } -fn get_station(station: Option, config: Config) -> (Station, bool) { +fn get_station( + station: Option, + config: Rc, + cached_stations: Option, +) -> (Station, bool, Option) { let mut internet = false; match station { // If the station name is passed as an argument: Some(x) => { - let url = match config.clone().get_url_for(&x) { - Some(u) => u, + let (url, updated_cached_stations) = match config.get_url_for(&x) { + Some(u) => (u, None), None => { println!( "{}", @@ -186,19 +213,20 @@ fn get_station(station: Option, config: Config) -> (Station, bool) { internet = true; - let brows = match Browser::new(config) { - Ok(b) => b, - Err(e) => { - error!("Could not connect with the API"); + let (brows, updated_cached_stations) = + match Browser::new(config, cached_stations) { + Ok(b) => b, + Err(e) => { + error!("Could not connect with the API"); - debug!("{}", e); + debug!("{}", e); - std::process::exit(1); - } - }; + std::process::exit(1); + } + }; match brows.get_station(x.clone()) { - Ok(s) => s.url, + Ok(s) => (s.url, Some(updated_cached_stations)), Err(e) => { error!("This station was not found :("); debug!("{}", e); @@ -209,14 +237,18 @@ fn get_station(station: Option, config: Config) -> (Station, bool) { } }; - (Station { station: x, url }, internet) + ( + Station { station: x, url }, + internet, + updated_cached_stations, + ) } // Otherwise None => { // And let the user choose one - match config.clone().prompt() { - Ok((s, b)) => (s, b), + match prompt(config, cached_stations) { + Ok((s, b, cached)) => (s, b, cached), Err(error) => { println!("\n\t{}", "Bye!".bold().green()); @@ -228,3 +260,45 @@ fn get_station(station: Option, config: Config) -> (Station, bool) { } } } + +/// Prompts the user to select a station. +/// Returns a station and if the station was taken from the internet. +pub fn prompt( + config: Rc, + cached_stations: Option, +) -> Result<(Station, bool, Option), InquireError> { + let max_lines: usize = match config.max_lines { + Some(x) => x, + None => Select::::DEFAULT_PAGE_SIZE, + }; + + let res = Select::new(&"Select a station to play:".bold(), config.data.clone()) + .with_page_size(max_lines) + .prompt(); + + let internet: bool; + let (station, updated_cached_stations) = match res { + Ok(s) => { + if s.station.eq("Other") { + internet = true; + let result = Browser::new(config, cached_stations); + + let (brow, updated_cached_stations) = match result { + Ok((b, updated_cached_stations)) => (b, updated_cached_stations), + Err(_e) => return Err(InquireError::OperationInterrupted), + }; + + match brow.prompt() { + Ok(r) => (r, Some(updated_cached_stations)), + Err(e) => return Err(e), + } + } else { + internet = false; + (s, None) + } + } + Err(e) => return Err(e), + }; + + Ok((station, internet, updated_cached_stations)) +}