diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e653e4..c7a305e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## 2.5.0 + +1. Added a selection menu while no station information is provided. This will include the last played station and the favorite list. +2. Added `--volume` option to the player. Now you can can pass volume level to the player. +3. ffplay initialization errors handled. Better logic to stop the PID of ffplay +4. Some unhandled errors are now handled +5. Minor typos fixed +6. sentry-sdk added to gater errors (will be removed on next major release) +7. About section updated to show donation link +8. Upgrade message will now point to this changelog file +9. Updated documentation + ## 2.4.0 1. Crashes on Windows fixed diff --git a/README.md b/README.md index c5a4088..52634f5 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,8 @@ Run with `radioactive --station [STATION_NAME]` or as simply `radio -U [UUID] ` ### Tips 1. Use a modern terminal emulator, otherwise the UI might break! (gets too ugly sometimes) -2. On windows, instead of default Command Prompt, use the new Windows Terminal or web-based emulators like hyper,Cmdr,Terminus etc. for better UI -3. Let the app run for atleast 5 seconds (not a serious issue though, for better performance) +2. On Windows, instead of default Command Prompt, use the new Windows Terminal or web-based emulators like hyper,Cmdr,Terminus etc. for better UI +3. Let the app run for at least 5 seconds (not a serious issue though, for better performance) ### Demo @@ -94,18 +94,18 @@ Run with `radioactive --station [STATION_NAME]` or as simply `radio -U [UUID] ` | Argument | Note | Description | Default | | ---------------------------- | ------------------------------------ | -------------------------------------------- | ------- | -| `--station`, `-S` | Required ( Optional from second run) | Station name | None | +| `--station`, `-S` | Required (Optional from second run) | Station name | None | | `--uuid`, `-U` | Optional | ID of the station | None | -| `--log-level`, `-L` | Optional | Log level of the program | info | +| `--log-level`, `-L` | Optional | Log level of the program | Info | | `--add-station` , `-A` | Optional | Add an entry to fav list | False | -| `--show-favourite-list`,`-W` | Optional | Show fav list | False | -| `--add-to-favourite`,`-F` | Optional | Add current station to fav list | False | +| `--show-favorite-list`,`-W` | Optional | Show fav list | False | +| `--add-to-favorite`,`-F` | Optional | Add current station to fav list | False | | `--flush` | Optional | Remove all the entries from fav list | False | -| `--discover-by-country`,`-D` | Optional | Discover stations by country code | false | -| `--discover-by-state` | Optioanl | Discover stations by country state | false | -| `--discover-by-tag` | Optional | Discover stations by tags/genre | fasle | -| `--discover-by-language` | optional | Discover stations by | false | -| `--limit` | Optional | Limit the # of results in the discover table | 100 | +| `--discover-by-country`,`-D` | Optional | Discover stations by country code | False | +| `--discover-by-state` | Optional | Discover stations by country state | False | +| `--discover-by-tag` | Optional | Discover stations by tags/genre | False | +| `--discover-by-language` | optional | Discover stations by | False | +| `--limit` | Optional | Limit the # of results in the Discover table | 100 | | `--volume` | Optional | Change the volume passed into ffplay | 50 | diff --git a/radioactive/__main__.py b/radioactive/__main__.py index 40c9337..88cdd73 100755 --- a/radioactive/__main__.py +++ b/radioactive/__main__.py @@ -10,6 +10,7 @@ from rich.table import Table from rich.text import Text from zenlog import log +from pick import pick from radioactive.alias import Alias from radioactive.app import App @@ -19,6 +20,20 @@ from radioactive.last_station import Last_station from radioactive.player import Player + +# using sentry to gather unhandled errors at production and will be removed on next major update. +# I respect your concerns but need this to improve radioactive. +import sentry_sdk +sentry_sdk.init( + dsn="https://e3c430f3b03f49b6bd9e9d61e7b3dc37@o615507.ingest.sentry.io/5749950", + + # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + traces_sample_rate=1.0, +) + + # globally needed as signal handler needs it # to terminate main() properly player = None @@ -43,8 +58,8 @@ def main(): limit = args.limit add_station = args.new_station - add_to_favourite = args.add_to_favourite - show_favourite_list = args.show_favourite_list + add_to_favorite = args.add_to_favorite + show_favorite_list = args.show_favorite_list flush_fav_list = args.flush ######################################## @@ -64,9 +79,6 @@ def main(): "Correct log levels are: error,warning,info(default),debug") handler = Handler() - alias = Alias() - alias.generate_map() - last_station = Last_station() mode_of_search = "" direct_play = False @@ -81,9 +93,10 @@ def main(): """ :radio: Play any radios around the globe right from this Terminal [yellow][blink]:zap:[/blink][/yellow]! :smile: Author: Dipankar Pal - :question: Type '--help' for more details on avaliable commands. + :question: Type '--help' for more details on available commands. :bug: Visit https://github.com/deep5050/radio-active to submit issues :star: Show some love by starring the project on GitHub [red][blink]:heart:[/blink][/red] + :dollar: You can donate me at https://deep5050.github.io/payme/ :x: Press Ctrl+C to quit """, title="[b][rgb(250,0,0)]RADIO[rgb(0,255,0)]ACTIVE[/b]", @@ -91,10 +104,14 @@ def main(): ) print(welcome) + alias = Alias() + alias.generate_map() + last_station = Last_station() + if app.is_update_available(): update_msg = ( "\t[blink]An update available, run [green][italic]pip install radio-active==" - + app.get_remote_version() + "[/italic][/green][/blink]") + + app.get_remote_version() + "[/italic][/green][/blink]\n See the changes: https://github.com/deep5050/radio-active/blob/main/CHANGELOG.md") update_panel = Panel( update_msg, width=85, @@ -106,8 +123,8 @@ def main(): if flush_fav_list: alias.flush() - if show_favourite_list: - log.info("Your favourite station list is below") + if show_favorite_list: + log.info("Your favorite station list is below") table = Table(show_header=True, header_style="bold magenta") table.add_column("Station", justify="left") table.add_column("URL / UUID", justify="center") @@ -116,7 +133,7 @@ def main(): table.add_row(entry["name"], entry["uuid_or_url"]) console.print(table) else: - log.info("You have no favourite station list") + log.info("You have no favorite station list") sys.exit(0) if add_station: @@ -149,40 +166,81 @@ def main(): # if neither of --station and --uuid provided , look in last_station file if station_name is None and station_uuid is None: + # Add a selection list here. first entry must be the last played station # try to fetch the last played station's information - log.warn( - "No station information provided, trying to play the last station") - - last_station_info = last_station.get_info() - + # log.warn( + # "No station information provided, trying to play the last station") try: - if last_station_info["alias"]: - is_alias = True + last_station_info = last_station.get_info() except: + # no last station?? pass + # print(last_station_info) + log.info("You can search for a station on internet using the --station option") + title = 'Please select a station from your favorite list:' + station_selection_names = [] + station_selection_urls = [] + - if is_alias: - alias.found = True # save on last_play as an alias too! - # last station was an alias, don't save it again - skip_saving_current_station = True - station_uuid_or_url = last_station_info["uuid_or_url"] - # here we are setting the name but will not be used for API call - station_name = last_station_info["name"] - if station_uuid_or_url.find("://") != -1: - # Its a URL - log.debug( - "Last station was an alias and contains a URL, Direct play set to True" - ) - direct_play = True - direct_play_url = station_uuid_or_url - log.info("Current station: {}".format( - last_station_info["name"])) - else: - # an UUID - station_uuid = last_station_info["uuid_or_url"] + # add last played station first + if last_station_info: + station_selection_names.append(f"{last_station_info['name']} (last played station)") + try: + station_selection_urls.append(last_station_info["stationuuid"]) + except: + station_selection_urls.append(last_station_info["uuid_or_url"]) + + fav_stations = alias.alias_map + for entry in fav_stations: + station_selection_names.append(entry["name"]) + station_selection_urls.append(entry["uuid_or_url"]) + + options = station_selection_names + option, index = pick(options, title,indicator="-->") + + # check if there is direct URL or just UUID + station_option_url = station_selection_urls[index] + station_name = station_selection_names[index] + if station_option_url.find("://") != -1: + # set direct play to TRUE + direct_play = True + direct_play_url = station_option_url else: - # was not an alias - station_uuid = last_station_info["stationuuid"] + # UUID + station_uuid = station_option_url + +################################## + + # try: + # if last_station_info["alias"]: + # is_alias = True + # except: + # pass + + # if is_alias: + # alias.found = True # save on last_play as an alias too! + # # last station was an alias, don't save it again + # skip_saving_current_station = True + # station_uuid_or_url = last_station_info["uuid_or_url"] + # # here we are setting the name but will not be used for API call + # station_name = last_station_info["name"] + # if station_uuid_or_url.find("://") != -1: + # # Its a URL + # log.debug( + # "Last station was an alias and contains a URL, Direct play set to True" + # ) + # direct_play = True + # direct_play_url = station_uuid_or_url + # log.info("Current station: {}".format( + # last_station_info["name"])) + # else: + # # an UUID + # station_uuid = last_station_info["uuid_or_url"] + # else: + # # was not an alias + # station_uuid = last_station_info["stationuuid"] +############################################ + # --------------------ONLY UUID PROVIDED --------------------- # # if --uuid provided call directly @@ -204,7 +262,7 @@ def main(): # its a URL log.debug("Entry contains a URL") log.debug("Direct play set to True ") - log.info("current station: {}".format(result["name"])) + log.info("Current station: {}".format(result["name"])) direct_play = True # assigning url and name directly direct_play_url = result["uuid_or_url"] @@ -215,7 +273,7 @@ def main(): except: log.warning( - "Station found in favourite list but seems to be invalid") + "Station found in favorite list but seems to be invalid") log.warning("Looking on the web instead") alias.found = False @@ -259,17 +317,22 @@ def main(): last_station.save_info(last_played_station) # TODO: handle error when favouring last played (aliased) station (BUG) (LOW PRIORITY) - if add_to_favourite: - alias.add_entry(add_to_favourite, handler.target_station["url"]) + if add_to_favorite: + alias.add_entry(add_to_favorite, handler.target_station["url"]) + + curr_station_name = station_name - curr_station_name = station_name if alias.found else handler.target_station[ - "name"] - panel_station_name = Text(curr_station_name, justify="center") + try: + # TODO fix this. when aliasing a station with an existing name curr_station_name is being None + panel_station_name = Text(curr_station_name, justify="center") - station_panel = Panel(panel_station_name, - title="[blink]:radio:[/blink]", - width=85) - console.print(station_panel) + station_panel = Panel(panel_station_name, + title="[blink]:radio:[/blink]", + width=85) + console.print(station_panel) + except: + # TODO handle exception + pass if os.name == "nt": while True: @@ -285,7 +348,7 @@ def signal_handler(sig, frame): global player log.debug("You pressed Ctrl+C!") log.debug("Stopping the radio") - if player.is_playing: + if player and player.is_playing: player.stop() log.info("Exiting now") sys.exit(0) diff --git a/radioactive/alias.py b/radioactive/alias.py index 496cced..0f41b08 100644 --- a/radioactive/alias.py +++ b/radioactive/alias.py @@ -26,15 +26,15 @@ def generate_map(self): # log.debug(json.dumps(alias_map, indent=3)) except Exception as e: - log.warning("could not get / parse alias data") + log.debug("could not get / parse alias data") # log.debug(json.dumps(self.alias_map)) else: - log.warning("Alias file does not exist") + log.debug("Alias file does not exist") # log.debug(json.dumps(self.alias_map, indent=3)) def search(self, entry): - """searchs for an entry in the fav list with the name + """searches for an entry in the fav list with the name the right side may contain both url or uuid , need to check properly """ if len(self.alias_map) > 0: @@ -58,13 +58,15 @@ def add_entry(self, left, right): """Adds a new entry to the fav list""" if self.search(left) is not None: log.warning("An entry with same name already exists, try another name") + return False else: with open(self.alias_path, "a+") as f: f.write("{}=={}\n".format(left.strip(), right.strip())) - log.info("Current station added to your favourite list") + log.info("Current station added to your favorite list") + return True def flush(self): """deletes all the entries in the fav list""" with open(self.alias_path, "w") as f: f.flush() - log.info("All entries deleted in your favourite list") + log.info("All entries deleted in your favorite list") diff --git a/radioactive/app.py b/radioactive/app.py index f244cf3..6cc3f7f 100644 --- a/radioactive/app.py +++ b/radioactive/app.py @@ -1,16 +1,16 @@ """ - Version of the current program, (in development mode it needs to be updated in every realease) + Version of the current program, (in development mode it needs to be updated in every release) and to check if an updated version available for the app or not """ import json import requests -from zenlog import log + class App: def __init__(self): - self.__VERSION__ = "2.4.0" # change this on every update # + self.__VERSION__ = "2.5.0" # change this on every update # self.pypi_api = "https://pypi.org/pypi/radio-active/json" self.remote_version = "" @@ -41,8 +41,5 @@ def is_update_available(self): return False except: - log.debug("Could not fetch remote version number") - + print("Could not fetch remote version number") -# if __name__ == "__main__": -# get_version() diff --git a/radioactive/args.py b/radioactive/args.py index 6378113..7ae970a 100644 --- a/radioactive/args.py +++ b/radioactive/args.py @@ -5,7 +5,7 @@ class Parser: - """Parse the command-line args and retrun result to the __main__""" + """Parse the command-line args and return result to the __main__""" def __init__(self): self.parser = None @@ -96,24 +96,24 @@ def __init__(self): action="store_true", default=False, dest="new_station", - help="Add an entry to your favourite station", + help="Add an entry to your favorite station", ) self.parser.add_argument( - "--add-to-favourite", + "--add-to-favorite", "-F", action="store", - dest="add_to_favourite", - help="Save current station to your favourite list", + dest="add_to_favorite", + help="Save current station to your favorite list", ) self.parser.add_argument( - "--show-favourite-list", + "--show-favorite-list", "-W", action="store_true", - dest="show_favourite_list", + dest="show_favorite_list", default=False, - help="Show your favourite list in table format", + help="Show your favorite list in table format", ) self.parser.add_argument( @@ -122,7 +122,7 @@ def __init__(self): action="store_true", dest="random", default=False, - help="Play a random station from your favourite list", + help="Play a random station from your favorite list", ) self.parser.add_argument( @@ -130,7 +130,7 @@ def __init__(self): action="store_true", dest="flush", default=False, - help="Flush your favourite list", + help="Flush your favorite list", ) self.parser.add_argument( diff --git a/radioactive/last_station.py b/radioactive/last_station.py index 82b8927..c53ab9f 100644 --- a/radioactive/last_station.py +++ b/radioactive/last_station.py @@ -36,8 +36,9 @@ def get_info(self): # return last_station['uuid_or_url'] # return last_station["stationuuid"] except Exception: - log.critical("Need a station name or UUID to play the radio, see help") - sys.exit(0) + return "" + # log.critical("Need a station name or UUID to play the radio, see help") + # sys.exit(0) def save_info(self, station): """dumps the current station information as a json file""" diff --git a/radioactive/player.py b/radioactive/player.py index 9f96ebe..eb8ced1 100644 --- a/radioactive/player.py +++ b/radioactive/player.py @@ -1,10 +1,9 @@ -""" FFplay proess handler """ +""" FFplay process handler """ import os import sys from shutil import which -from signal import SIGTERM -from subprocess import Popen +import subprocess from time import sleep import psutil @@ -14,7 +13,7 @@ class Player: """FFPlayer handler, it holds all the attributes to properly execute ffplay - FFmepg required to be installed seperately + FFmepg required to be installed separately """ def __init__(self, URL, volume): @@ -34,41 +33,77 @@ def __init__(self, URL, volume): log.critical("FFplay not found, install it first please") sys.exit(1) - self.process = Popen( - [self.exe_path, "-nodisp", "-nostats", "-loglevel", "0", "-volume", f"{self.volume}", self.url], - shell=False, - ) + try: + self.process = subprocess.Popen( + [self.exe_path, "-nodisp", "-nostats", "-loglevel", "0", "-volume", f"{self.volume}", self.url], + shell=False, + ) - log.debug("player: ffplay => PID {} initiated".format(self.process.pid)) + log.debug("player: ffplay => PID {} initiated".format(self.process.pid)) - #sleep(3) # sleeping for 3 seconds wainting for ffplay to start properly + #sleep(3) # sleeping for 3 seconds waiting for ffplay to start properly - if self.is_active(): - self.is_playing = True - log.info("Radio started successfully") - else: - log.error("Radio could not be stared, may be a dead station") - sys.exit(0) + if self.is_active(): + self.is_playing = True + log.info("Radio started successfully") + else: + log.error("Radio could not be stared, may be a dead station") + raise RuntimeError("Radio startup failed") + + except subprocess.CalledProcessError as e: + log.error("Error while starting radio: {}".format(e)) def is_active(self): - """checks for if the ffplay is still active or not, - will be used to terminate FFPLAY when the radioactive terminates""" + """Check if the ffplay process is still active.""" + if not self.process: + log.warning("Process is not initialized") + return False + try: + proc = psutil.Process(self.process.pid) + if proc.status() == psutil.STATUS_ZOMBIE: + log.debug("Process is a zombie") + return False + + if proc.status() == psutil.STATUS_RUNNING: + return True + + if proc.status() == psutil.STATUS_SLEEPING: + log.debug("Process is sleeping") + return True # Sleeping is considered active for our purpose - proc = psutil.Process(self.process.pid) - if proc.status() == psutil.STATUS_ZOMBIE: + # Handle other process states if needed + + log.warning("Process is not in an expected state") + return False + except psutil.NoSuchProcess: + log.debug("Process not found") return False - return True + except Exception as e: + log.error("Error while checking process status: {}".format(e)) + return False + def play(self): - """Nothing""" + """Play a station""" if not self.is_playing: pass # call the init function again ? def stop(self): - """sends a SIGTERM to the process id of the current FFPLAY""" + """stop the ffplayer """ if self.is_playing: - log.debug("Killing ffplay PID: {}".format(self.process.pid)) - os.kill(self.process.pid, SIGTERM) + try: + self.process.terminate() # Terminate the process gracefully + self.process.wait(timeout=5) # Wait for process to finish + log.info("Radio playback stopped successfully") + except subprocess.TimeoutExpired: + log.warning("Radio process did not terminate, killing...") + self.process.kill() # Kill the process forcefully + except Exception as e: + log.error("Error while stopping radio: {}".format(e)) + raise + finally: + self.is_playing = False + self.process = None else: - log.warn("Player: radio is not playing") + log.warning("Radio is not currently playing") \ No newline at end of file diff --git a/requirements b/requirements index cf53b60..a97ac87 100644 --- a/requirements +++ b/requirements @@ -1,11 +1,8 @@ -appdirs==1.4.4 -certifi==2020.12.5 -chardet==4.0.0 -colorlog==5.0.1 -idna==2.10 -psutil==5.8.0 +psutil pyradios==0.0.22 -requests==2.25.1 -urllib3==1.26.4 -zenlog==1.1 -rich==10.1.0 \ No newline at end of file +requests +urllib3 +zenlog +rich +pick +sentry-sdk \ No newline at end of file