diff --git a/.vscode/settings.json b/.vscode/settings.json index 356a09c..6d48e0f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,7 +17,7 @@ ], "files.autoSave": "off", "editor.wordWrap": "wordWrapColumn", - "workbench.colorTheme": "Quiet Light", + "workbench.colorTheme": "GitHub Light High Contrast", "editor.minimap.autohide": true, "editor.minimap.renderCharacters": false, "editor.experimentalWhitespaceRendering": "font", diff --git a/CHANGELOG.md b/CHANGELOG.md index 59b0e36..9ff2d0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.9.1 + +1. Play a random station from favorite list `--random` +2. Multiple media player support ( MPV, VLC, FFplay) `--player` +3. Default config file support added +4. Fixed minor bugs while giving runtime commands + + ## 2.9.0 1. Fetch current playing track info from runtime commands 🎶 ⚡ diff --git a/README.md b/README.md index fe8ecf2..57c3c8d 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,9 @@ - [x] Finds nearby stations - [x] Discovers stations by genre - [x] Discovers stations by language +- [x] VLC, MPV player support +- [x] Default config file - [ ] I'm feeling lucky! Play Random stations -- [ ] VLC, MPV player support > See my progress ➡️ [here](https://github.com/users/deep5050/projects/5) @@ -120,30 +121,33 @@ Search a station with `radio --search [STATION_NAME]` or simply `radio` :zap: to ### Options -| Argument | Note | Description | Default | -| ------------------ | -------- | ---------------------------------------------- | ------------- | -| `--search`, `-S` | Optional | Station name | None | -| `--play`, `-P` | Optional | A station from fav list or url for direct play | None | -| `--country`, `-C` | Optional | Discover stations by country code | False | -| `--state` | Optional | Discover stations by country state | False | -| `--language` | optional | Discover stations by | False | -| `--tag` | Optional | Discover stations by tags/genre | False | -| `--uuid`, `-U` | Optional | ID of the station | None | -| `--record` , `-R` | Optional | Record a station and save to file | False | -| `--filename`, `-N` | Optional | Filename to used to save the recorded audio | None | -| `--filepath` | Optional | Path to save the recordings | | -| `--filetype`, `-T` | Optional | Format of the recording (mp3/auto) | mp3 | -| `--last` | Optional | Play last played station | False | -| `--sort` | Optional | Sort the result page | name | -| `--limit` | Optional | Limit the # of results in the Discover table | 100 | -| `--volume` , `-V` | Optional | Change the volume passed into ffplay | 80 | -| `--favorite`, `-F` | Optional | Add current station to fav list | False | -| `--add` , `-A` | Optional | Add an entry to fav list | False | -| `--list`, `-W` | Optional | Show fav list | False | -| `--remove` | Optional | Remove entries from favorite list | False | -| `--flush` | Optional | Remove all the entries from fav list | False | -| `--kill` , `-K` | Optional | Kill background radios. | False | -| `--loglevel` | Optional | Log level of the program | Info | +| Options | Note | Description | Default | Values | +| ------------------ | -------- | ---------------------------------------------- | ------------- | ---------------------- | +| (No Option) | Optional | Select a station from menu to play | False | | +| `--search`, `-S` | Optional | Station name | None | | +| `--play`, `-P` | Optional | A station from fav list or url for direct play | None | | +| `--country`, `-C` | Optional | Discover stations by country code | False | | +| `--state` | Optional | Discover stations by country state | False | | +| `--language` | optional | Discover stations by | False | | +| `--tag` | Optional | Discover stations by tags/genre | False | | +| `--uuid`, `-U` | Optional | ID of the station | None | | +| `--record` , `-R` | Optional | Record a station and save to file | False | | +| `--filename`, `-N` | Optional | Filename to used to save the recorded audio | None | | +| `--filepath` | Optional | Path to save the recordings | | | +| `--filetype`, `-T` | Optional | Format of the recording | mp3 | `mp3`,`auto` | +| `--last` | Optional | Play last played station | False | | +| `--random` | Optional | Play a random station from favorite list | False | | +| `--sort` | Optional | Sort the result page | name | | +| `--limit` | Optional | Limit the # of results in the Discover table | 100 | | +| `--volume` , `-V` | Optional | Change the volume passed into ffplay | 80 | [0-100] | +| `--favorite`, `-F` | Optional | Add current station to fav list | False | | +| `--add` , `-A` | Optional | Add an entry to fav list | False | | +| `--list`, `-W` | Optional | Show fav list | False | | +| `--remove` | Optional | Remove entries from favorite list | False | | +| `--flush` | Optional | Remove all the entries from fav list | False | | +| `--kill` , `-K` | Optional | Kill background radios. | False | | +| `--loglevel` | Optional | Log level of the program | Info | `info`, `warning`, `error`, `debug` | +| `--player` | Optional | Media player to use | ffplay | `vlc`, `mpv`, `ffplay` |
@@ -198,6 +202,24 @@ you can sort the result page with these parameters: - `clicktrend` (currently trending stations) - `random` + +### Default configs + +Default configuration file is added into your home directory as `.radio-active-configs.ini` + +```bash +[AppConfig] +loglevel = info +limit = 100 +sort = votes +volume = 80 +filepath = /home/{user}/recordings/radioactive/ +filetype = mp3 +player = ffplay +``` + +Do NOT modify the keys, only change the values. you can give any absolute or relative path as filepath. + ### Bonus Tips 1. when using `rf`: you can force the recording to be in mp3 format by adding an extension to the file name. Example "talk-show.mp3". If you don't specify any extension it should auto-detect. Example "new_show" diff --git a/radioactive/__main__.py b/radioactive/__main__.py index a5cc308..3ccd47d 100755 --- a/radioactive/__main__.py +++ b/radioactive/__main__.py @@ -8,11 +8,11 @@ from radioactive.alias import Alias from radioactive.app import App +from radioactive.ffplay import Ffplay, kill_background_ffplays from radioactive.handler import Handler from radioactive.help import show_help from radioactive.last_station import Last_station from radioactive.parser import parse_options -from radioactive.player import Player, kill_background_ffplays from radioactive.utilities import ( check_sort_by_parameter, handle_add_station, @@ -22,6 +22,7 @@ handle_favorite_table, handle_listen_keypress, handle_play_last_station, + handle_play_random_station, handle_record, handle_save_last_station, handle_search_stations, @@ -34,21 +35,39 @@ # globally needed as signal handler needs it # to terminate main() properly -player = None +ffplay = None def final_step(options, last_station, alias, handler): - global player + global ffplay # always needed + # check target URL for the last time if options["target_url"].strip() == "": log.error("something is wrong with the url") sys.exit(1) + if options["audio_player"] == "vlc": + from radioactive.vlc import VLC + + vlc = VLC() + vlc.start(options["target_url"]) + + elif options["audio_player"] == "mpv": + from radioactive.mpv import MPV + + mpv = MPV() + mpv.start(options["target_url"]) + + elif options["audio_player"] == "ffplay": + ffplay = Ffplay(options["target_url"], options["volume"], options["loglevel"]) + + else: + log.error("Unsupported media player selected") + sys.exit(1) + if options["curr_station_name"].strip() == "": options["curr_station_name"] = "N/A" - player = Player(options["target_url"], options["volume"], options["loglevel"]) - handle_save_last_station( last_station, options["curr_station_name"], options["target_url"] ) @@ -89,8 +108,6 @@ def main(): options = parse_options() - handle_welcome_screen() - VERSION = app.get_version() handler = Handler() @@ -104,6 +121,8 @@ def main(): log.info("RADIO-ACTIVE : version {}".format(VERSION)) sys.exit(0) + handle_welcome_screen() + if options["show_help_table"]: show_help() sys.exit(0) @@ -193,6 +212,7 @@ def main(): and options["search_station_uuid"] is None and options["direct_play"] is None and not options["play_last_station"] + and not options["play_random"] ): ( options["curr_station_name"], @@ -239,6 +259,13 @@ def main(): ) final_step(options, last_station, alias, handler) + if options["play_random"]: + ( + options["curr_station_name"], + options["target_url"], + ) = handle_play_random_station(alias) + final_step(options, last_station, alias, handler) + if options["play_last_station"]: options["curr_station_name"], options["target_url"] = handle_play_last_station( last_station @@ -259,11 +286,11 @@ def main(): def signal_handler(sig, frame): - global player + global ffplay log.debug("You pressed Ctrl+C!") log.debug("Stopping the radio") - if player and player.is_playing: - player.stop() + if ffplay and ffplay.is_playing: + ffplay.stop() log.info("Exiting now") sys.exit(0) diff --git a/radioactive/alias.py b/radioactive/alias.py index a6a63fd..8dd4b84 100644 --- a/radioactive/alias.py +++ b/radioactive/alias.py @@ -76,6 +76,7 @@ def search(self, entry): def add_entry(self, left, right): """Adds a new entry to the fav list""" + self.generate_map() if self.search(left) is not None: log.warning("An entry with same name already exists, try another name") return False diff --git a/radioactive/app.py b/radioactive/app.py index 736a0de..60a3842 100644 --- a/radioactive/app.py +++ b/radioactive/app.py @@ -9,7 +9,7 @@ class App: def __init__(self): - self.__VERSION__ = "2.9.0" # change this on every update # + self.__VERSION__ = "2.9.1" # change this on every update # self.pypi_api = "https://pypi.org/pypi/radio-active/json" self.remote_version = "" diff --git a/radioactive/args.py b/radioactive/args.py index f9af9a9..f17bcfa 100644 --- a/radioactive/args.py +++ b/radioactive/args.py @@ -3,6 +3,16 @@ from zenlog import log +from radioactive.config import Configs + + +# load default configs +def load_default_configs(): + # load config file and apply configs + configs = Configs() + default_configs = configs.load() + return default_configs + class Parser: @@ -11,6 +21,7 @@ class Parser: def __init__(self): self.parser = None self.result = None + self.defaults = load_default_configs() self.parser = argparse.ArgumentParser( description="Play any radio around the globe right from the CLI ", @@ -54,6 +65,14 @@ def __init__(self): help="Play last played station.", ) + self.parser.add_argument( + "--random", + action="store_true", + default=False, + dest="play_random_station", + help="Play random station from fav list.", + ) + self.parser.add_argument( "--uuid", "-U", @@ -65,7 +84,7 @@ def __init__(self): self.parser.add_argument( "--loglevel", action="store", - default="info", + default=self.defaults["loglevel"], dest="log_level", help="Specify log level", ) @@ -103,7 +122,7 @@ def __init__(self): "-L", action="store", dest="limit", - default=100, + default=self.defaults["limit"], help="Limit of entries in discover table", ) @@ -111,7 +130,7 @@ def __init__(self): "--sort", action="store", dest="stations_sort_by", - default="name", + default=self.defaults["sort"], help="Sort stations", ) @@ -161,7 +180,7 @@ def __init__(self): "-V", action="store", dest="volume", - default=80, + default=self.defaults["volume"], type=int, choices=range(0, 101, 10), help="Volume to pass down to ffplay", @@ -189,7 +208,7 @@ def __init__(self): "--filepath", action="store", dest="record_file_path", - default="", + default=self.defaults["filepath"], help="specify the audio format for recording", ) @@ -207,10 +226,18 @@ def __init__(self): "-T", action="store", dest="record_file_format", - default="mp3", + default=self.defaults["filetype"], help="specify the audio format for recording. auto/mp3", ) + self.parser.add_argument( + "--player", + action="store", + dest="audio_player", + default=self.defaults["player"], + help="specify the audio player to use. ffplay/vlc/mpv", + ) + def parse(self): self.result = self.parser.parse_args() if self.result is None: diff --git a/radioactive/config.py b/radioactive/config.py new file mode 100644 index 0000000..6720127 --- /dev/null +++ b/radioactive/config.py @@ -0,0 +1,75 @@ +# load configs from a file and apply. +# If any options are given on command line it will override the configs +import configparser +import getpass +import os +import sys + +from zenlog import log + + +def write_a_sample_config_file(): + # Create a ConfigParser object + config = configparser.ConfigParser() + + # Add sections and key-value pairs + config["AppConfig"] = { + "loglevel": "info", + "limit": "100", + "sort": "votes", + "volume": "80", + "filepath": "/home/{user}/recordings/radioactive/", + "filetype": "mp3", + "player": "ffplay", + } + + # Get the user's home directory + home_directory = os.path.expanduser("~") + + # Specify the file path + file_path = os.path.join(home_directory, ".radio-active-configs.ini") + + try: + # Write the configuration to the file + with open(file_path, "w") as config_file: + config.write(config_file) + + log.info(f"A sample default configuration file added at: {file_path}") + + except Exception as e: + print(f"Error writing the configuration file: {e}") + + +class Configs: + def __init__(self): + self.config_path = os.path.join( + os.path.expanduser("~"), ".radio-active-configs.ini" + ) + + def load(self): + self.config = configparser.ConfigParser() + + try: + self.config.read(self.config_path) + options = {} + options["volume"] = self.config.get("AppConfig", "volume") + options["loglevel"] = self.config.get("AppConfig", "loglevel") + options["sort"] = self.config.get("AppConfig", "sort") + options["limit"] = self.config.get("AppConfig", "limit") + options["filepath"] = self.config.get("AppConfig", "filepath") + # if filepath has any placeholder, replace + # {user} to actual user map + options["filepath"] = options["filepath"].replace( + "{user}", getpass.getuser() + ) + options["filetype"] = self.config.get("AppConfig", "filetype") + options["player"] = self.config.get("AppConfig", "player") + + return options + + except Exception as e: + log.error(f"Something went wrong while parsing the config file: {e}") + # write the example config file + write_a_sample_config_file() + log.info("Re-run radioactive") + sys.exit(1) diff --git a/radioactive/player.py b/radioactive/ffplay.py similarity index 99% rename from radioactive/player.py rename to radioactive/ffplay.py index 82205f5..1915ff0 100644 --- a/radioactive/player.py +++ b/radioactive/ffplay.py @@ -34,7 +34,7 @@ def kill_background_ffplays(): log.info("No background radios are running!") -class Player: +class Ffplay: """FFPlayer handler, it holds all the attributes to properly execute ffplay FFmepg required to be installed separately diff --git a/radioactive/help.py b/radioactive/help.py index ac48b8f..6a25412 100644 --- a/radioactive/help.py +++ b/radioactive/help.py @@ -61,6 +61,11 @@ def show_help(): "Play last played station", "False", ) + table.add_row( + "--random", + "Play a random station from favorite list", + "False", + ) table.add_row( "--add , -A", @@ -145,6 +150,12 @@ def show_help(): "info", ) + table.add_row( + "--player", + "Media player to use. vlc/mpv/ffplay", + "ffplay", + ) + console.print(table) print( "For more details : https://github.com/deep5050/radio-active/blob/main/README.md" diff --git a/radioactive/mpv.py b/radioactive/mpv.py new file mode 100644 index 0000000..c0f583b --- /dev/null +++ b/radioactive/mpv.py @@ -0,0 +1,40 @@ +# mpv player +import subprocess +import sys +from shutil import which + +from zenlog import log + + +class MPV: + def __init__(self): + # check if mpv is installed + self.program_name = "mpv" + self.exe_path = which(self.program_name) + log.debug("mpv: {}".format(self.exe_path)) + + if self.exe_path is None: + log.critical("MPV not found, install it first please") + sys.exit(1) + + def start(self, url): + # call mpv with URL + self.mpv_commands = [ + self.exe_path, + url, + ] + + try: + self.process = subprocess.Popen( + self.mpv_commands, + shell=False, + stdout=subprocess.PIPE, # Capture standard output + stderr=subprocess.PIPE, # Capture standard error + text=True, # Use text mode to capture strings + ) + self.is_running = True + log.debug("player: MPV => PID {} initiated".format(self.process.pid)) + + except Exception as e: + # Handle exceptions that might occur during process setup + log.error("Error while starting radio: {}".format(e)) diff --git a/radioactive/parser.py b/radioactive/parser.py index 00a19de..d7b7cbd 100644 --- a/radioactive/parser.py +++ b/radioactive/parser.py @@ -29,6 +29,7 @@ def parse_options(): options["play_last_station"] = args.play_last_station options["direct_play"] = args.direct_play + options["play_random"] = args.play_random_station options["sort_by"] = args.stations_sort_by @@ -53,5 +54,6 @@ def parse_options(): options["target_url"] = "" options["volume"] = args.volume + options["audio_player"] = args.audio_player return options diff --git a/radioactive/utilities.py b/radioactive/utilities.py index 8bd3e9b..1d3bf85 100644 --- a/radioactive/utilities.py +++ b/radioactive/utilities.py @@ -16,8 +16,8 @@ from rich.text import Text from zenlog import log +from radioactive.ffplay import kill_background_ffplays from radioactive.last_station import Last_station -from radioactive.player import kill_background_ffplays from radioactive.recorder import record_audio_auto_codec, record_audio_from_url RED_COLOR = "\033[91m" @@ -96,7 +96,9 @@ def handle_record( elif not record_file_path: log.debug("filepath: fallback to default path") - record_file_path = os.path.join(os.path.expanduser("~"), "Music/radioactive") + record_file_path = os.path.join( + os.path.expanduser("~"), "Music/radioactive" + ) # fallback path try: os.makedirs(record_file_path, exist_ok=True) except Exception as e: @@ -465,7 +467,7 @@ def handle_user_choice_from_search_result(handler, response): print() sys.exit(0) - if user_input == ("y" or "Y"): + if user_input in ["y", "Y"]: log.debug("Playing UUID from single response") global_current_station_info = response[0] @@ -491,6 +493,9 @@ def handle_user_choice_from_search_result(handler, response): # pick a random integer withing range user_input = randint(1, len(response) - 1) log.debug(f"Radom station id: {user_input}") + # elif user_input in ["f", "F", "fuzzy"]: + # fuzzy find all the stations, and return the selected station id + # user_input = fuzzy_find(response) user_input = int(user_input) - 1 # because ID starts from 1 if user_input in range(0, len(response)): @@ -599,3 +604,12 @@ def handle_station_name_from_headers(url): ) ) return station_name + + +def handle_play_random_station(alias): + """Select a random station from favorite menu""" + log.debug("playing a random station") + alias_map = alias.alias_map + index = randint(0, len(alias_map) - 1) + station = alias_map[index] + return station["name"], station["uuid_or_url"] diff --git a/radioactive/vlc.py b/radioactive/vlc.py new file mode 100644 index 0000000..3c0a846 --- /dev/null +++ b/radioactive/vlc.py @@ -0,0 +1,40 @@ +# VLC player +import subprocess +import sys +from shutil import which + +from zenlog import log + + +class VLC: + def __init__(self): + # check if vlc is installed + self.program_name = "vlc" + self.exe_path = which(self.program_name) + log.debug("VLC: {}".format(self.exe_path)) + + if self.exe_path is None: + log.critical("VLC not found, install it first please") + sys.exit(1) + + def start(self, url): + # call vlc with URL + self.vlc_commands = [ + self.exe_path, + url, + ] + + try: + self.process = subprocess.Popen( + self.vlc_commands, + shell=False, + stdout=subprocess.PIPE, # Capture standard output + stderr=subprocess.PIPE, # Capture standard error + text=True, # Use text mode to capture strings + ) + self.is_running = True + log.debug("player: VLC => PID {} initiated".format(self.process.pid)) + + except Exception as e: + # Handle exceptions that might occur during process setup + log.error("Error while starting radio: {}".format(e)) diff --git a/requirements.txt b/requirements.txt index bdad370..1ccde19 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +setuptools requests urllib3 psutil