From 07521204b8d5116de28610a3465314be144e2452 Mon Sep 17 00:00:00 2001 From: Dipankar Pal Date: Wed, 4 Oct 2023 15:51:05 +0530 Subject: [PATCH] 2.8.0 (#74) * 2.8.0 * feat: :sparkles: debug stats added for ffplay and FFmpeg * docs: :sparkles: add doc for debug message and selection menu * feat(recording): :sparkles: detect codec automatically from stream save recorded audio with autodetected codec information. but by default set to mp3 * feat: :sparkles: 2.8.0 * fix: :bug: disable video playback in debug mode * docs: tip updated * fix: :ambulance: fix directplay url related bug * docs: :art: updated * Update README.md * docs(typos): fix minor typos * feat: show default record path on runtime --- CHANGELOG.md | 9 ++ README.md | 115 ++++++++------- radioactive/__main__.py | 282 ++++++++++++++++++++++-------------- radioactive/app.py | 2 +- radioactive/args.py | 4 +- radioactive/handler.py | 112 +++++++++----- radioactive/help.py | 12 +- radioactive/last_station.py | 9 -- radioactive/player.py | 32 ++-- radioactive/recorder.py | 63 ++++++-- radioactive/utilities.py | 102 ++++++++----- setup.py | 4 +- 12 files changed, 472 insertions(+), 274 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d35ee7..f76b635 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 2.8.0 + +1. Selection menu added for `--country` and `--tag` results. Play directly from result page. +2. `ffplay` and `ffmpeg` will show debug info while started with `--loglevel debug` +3. Autodetect the codec information and set the file extension of the recorded file. +4. Force a recording to be in mp3 format only. +5. Simpler command help message + + ## 2.7.0 1. Recording support added 🎉 . save recording as mp3 or wav 🎶 `--record` diff --git a/README.md b/README.md index 10c2199..4dfd18e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@

-

RADIO-ACTIVE

+

RADIOACTIVE

+

SEARCH - PLAY - RECORD - REPEAT

-

Play any radios around the globe right from your terminal

+

Play and record any radio stations around the globe right from your terminal

radio-active - Play more than 30K radio stations from your terminal | Product Hunt @@ -34,108 +35,112 @@ - [x] Selection menu for favorite stations - [x] Supports user-added stations :wrench: - [x] Looks minimal and user-friendly -- [x] Runs on Raspberry-Pi +- [x] Runs on Raspberry Pi - [x] Finds nearby stations - [x] Discovers stations by genre - [x] Discovers stations by language - [x] Record audio from live radio on demand :zap: -- [ ] I'm feeling lucky ! Play Random stations +- [ ] I'm feeling lucky! Play Random stations > See my progress [here](https://github.com/deep5050/radio-active/projects/1) +### Install + +Just run: `pip3 install --upgrade radio-active` + ### External Dependency It needs [FFmpeg](https://ffmpeg.org/download.html) to be installed on your system in order to play the audio -on Ubuntu based system >= 20.04 Run +on Ubuntu-based system >= 20.04 Run ``` sudo apt update sudo apt install ffmpeg ``` -For other systems including windows see the above link +For other systems including Windows see the above link #### Installing FFmpeg -FFmpeg is required for this program to work correctly. Install FFmpeg by following these steps :- +FFmpeg is required for this program to work correctly. Install FFmpeg by following these steps:- - On Linux - - On Windows - -### Install - -Just run: `pip3 install --upgrade radio-active` - -I encourage you to install with pipx: `pipx install radio-active` - ### Run -Run with `radioactive --search [STATION_NAME]` or as simply `radio -U [UUID] ` :zap: +Search a station with `radio --search [STATION_NAME]` or simply `radio` :zap: to select from the favorite menu. ### 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 +2. On Windows, instead of the 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 - -
- + + ### Options -| Argument | Note | Description | Default | -| ----------------------------- | ----------------------------------- | ---------------------------------------------- | ----------------------- | -| `--search`, `-S` | Required (Optional from second run) | Station name | None | -| `--play`, `-P` | Optional | A station from fav list or url for direct play | None | -| `--last` | Optional | Play last played station | False | -| `--uuid`, `-U` | Optional | ID of the station | None | -| `--loglevel` | Optional | Log level of the program | Info | -| `--add` , `-A` | Optional | Add an entry to fav list | False | -| `--list`, `-W` | Optional | Show fav list | False | -| `--favorite`, `-F` | Optional | Add current station to fav list | False | -| `--flush` | Optional | Remove all the entries from fav list | False | -| `--country`, `-C` | Optional | Discover stations by country code | False | -| `--state` | Optional | Discover stations by country state | False | -| `--tag` | Optional | Discover stations by tags/genre | False | -| `--language` | optional | Discover stations by | False | -| `--limit` | Optional | Limit the # of results in the Discover table | 100 | -| `--volume` , `-V` | Optional | Change the volume passed into ffplay | 80 | -| `--kill` , `-K` | Optional | Kill background radios. | False | -| `--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 | /User/Music/radioactive | -| `--filetype`, `-T` | Optional | Format of the recording (mp3/wav) | mp3 | -| | | | | +| Argument | Note | Description | Default | +| ------------------ | ----------------------------------- | ---------------------------------------------- | ------------- | +| `--search`, `-S` | Required (Optional from second run) | Station name | None | +| `--play`, `-P` | Optional | A station from fav list or url for direct play | None | +| `--last` | Optional | Play last played station | False | +| `--uuid`, `-U` | Optional | ID of the station | None | +| `--loglevel` | Optional | Log level of the program | Info | +| `--add` , `-A` | Optional | Add an entry to fav list | False | +| `--list`, `-W` | Optional | Show fav list | False | +| `--favorite`, `-F` | Optional | Add current station to fav list | False | +| `--flush` | Optional | Remove all the entries from fav list | False | +| `--country`, `-C` | Optional | Discover stations by country code | False | +| `--state` | Optional | Discover stations by country state | False | +| `--tag` | Optional | Discover stations by tags/genre | False | +| `--language` | optional | Discover stations by | False | +| `--limit` | Optional | Limit the # of results in the Discover table | 100 | +| `--volume` , `-V` | Optional | Change the volume passed into ffplay | 80 | +| `--kill` , `-K` | Optional | Kill background radios. | False | +| `--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 | +
-> `--search`, `-S` : Expects a station name to be played . Example: "pehla nasha" , -> pehla_nasha, bbc_radio -> `--play`, `-P`: You can pass an exact name from your favorite stations or alternatively pass any direct stream url. This would bypass any user slection menu (useful when running from another srcipt) +> `--search`, `-S`: Search for a station online. + +> `--play`, `-P`: You can pass an exact name from your favorite stations or alternatively pass any direct stream URL. This would bypass any user selection menu (useful when running from another script) -> `--uuid`,`-U` : When station names are too long or confusing (or multiple -> results for the same name) use the station's uuid to play . --uuid gets the -> greater priority than `--search`. Example: 96444e20-0601-11e8-ae97-52543be04c81 +> `--uuid`,`-U`: When station names are too long or confusing (or multiple +> results for the same name) use the station's uuid to play. --uuid gets the +> greater priority than `--search`. Example: 96444e20-0601-11e8-ae97-52543be04c81. type `u` on the runtime command to get the UUID of a station. -> `--loglevel`, : Don't need to specify unless you are developing it. `info` , `warning` , `error` , `debug` +> `--loglevel`,: Don't need to specify unless you are developing it. `info`, `warning`, `error`, `debug` -> `-F` : Add current station to your favorite list. Example: `-F my_fav_1` +> `-F`: Add the current station to your favorite list. Example: `-F my_fav_1` > `-A`: Add any stations to your list. You can add stations that are not currently available on our API. When adding a new station enter a name and direct URL to the audio stream. +> `--limit`: Specify how many search results should be displayed. + +> `--filetype`: Specify the extension of the final recording file. default is `mp3`. you can provide `-T auto` to autodetect the codec and set file extension accordingly (in original form). + +> DEFAULT_DIR: is `/home/user/Music/radioactive` + ### Runtime Commands Input a command during the radio playback to perform an action. Available commands are: @@ -143,15 +148,21 @@ Input a command during the radio playback to perform an action. Available comman ``` Enter a command to perform an action: ? -q/Q/x/quit: Quit radioactive +q/Q/quit: Quit radioactive h/H/help/?: Show this help message r/R/record: Record a station f/F/fav: Add station to favorite list -rf/RF/recordfile: Speficy a filename for the recording +rf/RF/recordfile: Specify a filename for the recording. ``` -> **TIP**: when using `rf`: specify the format of the output using the name. for example: "new-show.mp3" or "new-show.wav" +### 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" + +2. You don't have to pass the exact option name, a portion of it will also work. for example `--sea` for `--search`, `--coun` for `--country`, `--lim` for `--limit` + +3. It's better to leave the `--filetype` as mp3 when you need to record something quickly. The autocodec takes a few milliseconds extra to determine the codec. ### Changes diff --git a/radioactive/__main__.py b/radioactive/__main__.py index 7af4e70..ced3a74 100755 --- a/radioactive/__main__.py +++ b/radioactive/__main__.py @@ -4,7 +4,6 @@ import sys from time import sleep -from rich.console import Console from zenlog import log from radioactive.alias import Alias @@ -14,66 +13,107 @@ from radioactive.help import show_help from radioactive.last_station import Last_station from radioactive.player import Player, kill_background_ffplays -from radioactive.utilities import ( - handle_add_station, - handle_add_to_favorite, - handle_current_play_panel, - handle_direct_play, - handle_favorite_table, - handle_listen_keypress, - handle_log_level, - handle_play_last_station, - handle_record, - handle_save_last_station, - handle_search_stations, - handle_station_selection_menu, - handle_station_uuid_play, - handle_update_screen, - handle_user_choice_from_search_result, - handle_welcome_screen, -) +from radioactive.utilities import (handle_add_station, handle_add_to_favorite, + handle_current_play_panel, + handle_direct_play, handle_favorite_table, + handle_listen_keypress, handle_log_level, + handle_play_last_station, handle_record, + handle_save_last_station, + handle_search_stations, + handle_station_selection_menu, + handle_station_uuid_play, + handle_update_screen, + handle_user_choice_from_search_result, + handle_welcome_screen) # globally needed as signal handler needs it # to terminate main() properly player = None +def final_step(options, last_station, alias, handler): + global player + # 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["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"] + ) + + if options["add_to_favorite"]: + handle_add_to_favorite( + alias, options["curr_station_name"], options["target_url"] + ) + + handle_current_play_panel(options["curr_station_name"]) + + if options["record_stream"]: + handle_record( + options["target_url"], + options["curr_station_name"], + options["record_file_path"], + options["record_file"], + options["record_file_format"], + options["loglevel"], + ) + + handle_listen_keypress( + alias, + target_url=options["target_url"], + station_name=options["curr_station_name"], + station_url=options["target_url"], + record_file_path=options["record_file_path"], + record_file=options["record_file"], + record_file_format=options["record_file_format"], + loglevel=options["loglevel"], + ) + + def main(): log.level("info") parser = Parser() app = App() args = parser.parse() + options = {} # ----------------- all the args ------------- # - show_help_table = args.help - search_station_name = args.search_station_name - direct_play = args.direct_play - play_last_station = args.play_last_station + options["show_help_table"] = args.help + options["search_station_name"] = args.search_station_name + options["direct_play"] = args.direct_play + options["play_last_station"] = args.play_last_station - search_station_uuid = args.search_station_uuid + options["search_station_uuid"] = args.search_station_uuid - discover_country_code = args.discover_country_code - discover_state = args.discover_state - discover_language = args.discover_language - discover_tag = args.discover_tag + options["discover_country_code"] = args.discover_country_code + options["discover_state"] = args.discover_state + options["discover_language"] = args.discover_language + options["discover_tag"] = args.discover_tag limit = args.limit - limit = int(limit) if limit else 100 + options["limit"] = int(limit) if limit else 100 log.debug("limit is set to: {}".format(limit)) - add_station = args.new_station - add_to_favorite = args.add_to_favorite - show_favorite_list = args.show_favorite_list + options["add_station"] = args.new_station + options["add_to_favorite"] = args.add_to_favorite + options["show_favorite_list"] = args.show_favorite_list - flush_fav_list = args.flush - kill_ffplays = args.kill_ffplays + options["flush_fav_list"] = args.flush + options["kill_ffplays"] = args.kill_ffplays - record_stream = args.record_stream - record_file = args.record_file - record_file_format = args.record_file_format - record_file_path = args.record_file_path + options["record_stream"] = args.record_stream + options["record_file"] = args.record_file + options["record_file_format"] = args.record_file_format + options["record_file_path"] = args.record_file_path - target_url = "" + options["target_url"] = "" + options["volume"] = args.volume VERSION = app.get_version() @@ -89,116 +129,138 @@ def main(): log.info("RADIO-ACTIVE : version {}".format(VERSION)) sys.exit(0) - if show_help_table: + if options["show_help_table"]: show_help() sys.exit(0) - handle_log_level(args) - if flush_fav_list: + options["loglevel"] = handle_log_level(args) + + if options["flush_fav_list"]: sys.exit(alias.flush()) - if kill_ffplays: + if options["kill_ffplays"]: kill_background_ffplays() sys.exit(0) - if show_favorite_list: + if options["show_favorite_list"]: handle_favorite_table(alias) sys.exit(0) - if add_station: + if options["add_station"]: handle_add_station(alias) handle_update_screen(app) - if discover_country_code: - handler.discover_by_country(discover_country_code, limit) + # ----------- country ----------- # + if options["discover_country_code"]: + response = handler.discover_by_country( + options["discover_country_code"], options["limit"] + ) + if response is not None: + ( + options["curr_station_name"], + options["target_url"], + ) = handle_user_choice_from_search_result(handler, response) + final_step(options, last_station, alias, handler) + else: + sys.exit(0) - if discover_state: - handler.discover_by_state(discover_state, limit) + # -------------- state ------------- # + if options["discover_state"]: + response = handler.discover_by_state( + options["discover_state"], options["limit"] + ) + if response is not None: + ( + options["curr_station_name"], + options["target_url"], + ) = handle_user_choice_from_search_result(handler, response) + final_step(options, last_station, alias, handler) + else: + sys.exit(0) - if discover_language: - handler.discover_by_language(discover_language, limit) + # ----------- language ------------ # + if options["discover_language"]: + response = handler.discover_by_language( + options["discover_language"], options["limit"] + ) + if response is not None: + ( + options["curr_station_name"], + options["target_url"], + ) = handle_user_choice_from_search_result(handler, response) + final_step(options, last_station, alias, handler) + else: + sys.exit(0) - if discover_tag: - handler.discover_by_tag(discover_tag, limit) + # -------------- tag ------------- # + if options["discover_tag"]: + response = handler.discover_by_tag(options["discover_tag"], options["limit"]) + if response is not None: + ( + options["curr_station_name"], + options["target_url"], + ) = handle_user_choice_from_search_result(handler, response) + final_step(options, last_station, alias, handler) + else: + sys.exit(0) # -------------------- NOTHING PROVIDED --------------------- # - # if neither of --search and --uuid provided if ( - search_station_name is None - and search_station_uuid is None - and direct_play is None - and not play_last_station + options["search_station_name"] is None + and options["search_station_uuid"] is None + and options["direct_play"] is None + and not options["play_last_station"] ): - curr_station_name, target_url = handle_station_selection_menu( - handler, last_station, alias - ) + ( + options["curr_station_name"], + options["target_url"], + ) = handle_station_selection_menu(handler, last_station, alias) + final_step(options, last_station, alias, handler) # --------------------ONLY UUID PROVIDED --------------------- # - if search_station_uuid is not None: - curr_station_name, target_url = handle_station_uuid_play( - handler, search_station_uuid + if options["search_station_uuid"] is not None: + options["curr_station_name"], options["target_url"] = handle_station_uuid_play( + handler, options["search_station_uuid"] ) + final_step(options, last_station, alias, handler) # ------------------- ONLY STATION PROVIDED ------------------ # elif ( - search_station_name is not None - and search_station_uuid is None - and direct_play is None + options["search_station_name"] is not None + and options["search_station_uuid"] is None + and options["direct_play"] is None ): response = [{}] - response = handle_search_stations(handler, search_station_name, limit) + response = handle_search_stations( + handler, options["search_station_name"], options["limit"] + ) if response is not None: - curr_station_name, target_url = handle_user_choice_from_search_result( - handler, response - ) + ( + options["curr_station_name"], + options["target_url"], + ) = handle_user_choice_from_search_result(handler, response) + # options["codec"] = response["codec"] + # print(response) + final_step(options, last_station, alias, handler) else: sys.exit(0) # ------------------------- direct play ------------------------# - if direct_play is not None: - curr_station_name, target_url = handle_direct_play(alias, direct_play) - - if play_last_station: - curr_station_name, target_url = handle_play_last_station(last_station) - # ---------------------- player ------------------------ # - # check target URL for the last time - if target_url.strip() == "": - log.error("something is wrong with the url") - sys.exit(1) - - if curr_station_name.strip() == "": - curr_station_name = "N/A" - - global player - player = Player(target_url, args.volume) - - handle_save_last_station(last_station, curr_station_name, target_url) - - if add_to_favorite: - handle_add_to_favorite(alias, curr_station_name, target_url) - - handle_current_play_panel(curr_station_name) + if options["direct_play"] is not None: + options["curr_station_name"], options["target_url"] = handle_direct_play( + alias, options["direct_play"] + ) + final_step(options, last_station, alias, handler) - if record_stream: - handle_record( - target_url, - curr_station_name, - record_file_path, - record_file, - record_file_format, + if options["play_last_station"]: + options["curr_station_name"], options["target_url"] = handle_play_last_station( + last_station ) + final_step(options, last_station, alias, handler) - handle_listen_keypress( - alias=alias, - target_url=target_url, - station_name=curr_station_name, - station_url=target_url, - record_file_path=record_file_path, - record_file=record_file, - record_file_format=record_file_format, - ) + # final_step() if os.name == "nt": while True: diff --git a/radioactive/app.py b/radioactive/app.py index 4f0c571..badad9c 100644 --- a/radioactive/app.py +++ b/radioactive/app.py @@ -9,7 +9,7 @@ class App: def __init__(self): - self.__VERSION__ = "2.7.0" # change this on every update # + self.__VERSION__ = "2.8.0" # 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 8a8541e..65f390c 100644 --- a/radioactive/args.py +++ b/radioactive/args.py @@ -190,8 +190,8 @@ def __init__(self): "-T", action="store", dest="record_file_format", - default="", - help="specify the audio format for recording", + default="mp3", + help="specify the audio format for recording. auto/mp3", ) def parse(self): diff --git a/radioactive/handler.py b/radioactive/handler.py index 411f572..e78ed17 100644 --- a/radioactive/handler.py +++ b/radioactive/handler.py @@ -44,6 +44,13 @@ def __init__(self): log.critical("Something is wrong with your internet connection") sys.exit(1) + def get_country_code(self, name): + self.countries = self.API.countries() + for country in self.countries: + if country["name"].lower() == name.lower(): + return country["iso_3166_1"] + return None + def station_validator(self): """Validates a response from the API and takes appropriate decision""" @@ -51,8 +58,6 @@ def station_validator(self): if not self.response: log.error("No stations found by the name") return [] - # TODO: remove sys exit - # sys.exit(0) # considering it as not an error # when multiple results found if len(self.response) > 1: @@ -74,7 +79,7 @@ def station_validator(self): station["countrycode"], trim_string( station["tags"] - ), # trimming tags to make the table shortrer + ), # trimming tags to make the table shorter ) console.print(table) @@ -83,8 +88,6 @@ def station_validator(self): \ntry to maximize the window , decrease the font by a bit and retry" ) return self.response - # TODO: remove sys exit - # sys.exit(0) # when exactly one response found if len(self.response) == 1: @@ -100,7 +103,6 @@ def station_validator(self): # ---------------------------- NAME -------------------------------- # def search_by_station_name(self, _name=None, limit=100): """search and play a station by its name""" - # TODO: handle exact error try: self.response = self.API.search(name=_name, name_exact=False, limit=limit) return self.station_validator() @@ -109,10 +111,9 @@ def search_by_station_name(self, _name=None, limit=100): log.error("Something went wrong. please try again.") sys.exit(1) - # ------------------------------- UUID ------------------------------ # + # ------------------------- UUID ------------------------ # def play_by_station_uuid(self, _uuid): """search and play station by its stationuuid""" - # TODO: handle exact error try: self.response = self.API.station_by_uuid(_uuid) return self.station_validator() # should return a station name also @@ -121,31 +122,56 @@ def play_by_station_uuid(self, _uuid): log.error("Something went wrong. please try again.") sys.exit(1) - # ----------------------- ------- COUNTRY -------------------------# - def discover_by_country(self, country_code, limit): - try: - discover_result = self.API.search(countrycode=country_code, limit=limit) - except Exception as e: - log.debug("Error: {}".format(e)) - log.error("Something went wrong. please try again.") - sys.exit(1) - - if len(discover_result) > 1: - log.info("Result for country: {}".format(discover_result[0]["country"])) + # -------------------------- COUNTRY ----------------------# + def discover_by_country(self, country_code_or_name, limit): + # check if it is a code or name + if len(country_code_or_name.strip()) == 2: + # it's a code + log.debug("Country code {} provided".format(country_code_or_name)) + try: + response = self.API.search( + countrycode=country_code_or_name, limit=limit + ) + except Exception as e: + log.debug("Error: {}".format(e)) + log.error("Something went wrong. please try again.") + sys.exit(1) + else: + # it's name + log.debug("Country name {} provided".format(country_code_or_name)) + code = self.get_country_code(country_code_or_name) + if code: + try: + response = self.API.search( + countrycode=code, limit=limit, country_exact=True + ) + except Exception as e: + log.debug("Error: {}".format(e)) + log.error("Something went wrong. please try again.") + sys.exit(1) + else: + log.error("Not a valid country name") + sys.exit(1) + + if len(response) > 1: + log.info("Result for country: {}".format(response[0]["country"])) table = Table(show_header=True, header_style="bold magenta") + table.add_column("ID", justify="center") table.add_column("Station", justify="left") # table.add_column("UUID", justify="center") table.add_column("State", justify="center") table.add_column("Tags", justify="center") table.add_column("Language", justify="center") - for res in discover_result: + for i in range(0, len(response)): + current_response = response[i] table.add_row( - trim_string(res["name"], max_length=30), + str(i + 1), + trim_string(current_response["name"], max_length=30), # res["stationuuid"], - res["state"], - trim_string(res["tags"], max_length=20), - trim_string(res["language"], max_length=20), + current_response["state"], + trim_string(current_response["tags"], max_length=20), + trim_string(current_response["language"], max_length=20), ) console.print(table) log.info( @@ -153,9 +179,9 @@ def discover_by_country(self, country_code, limit): \ntry to maximize the window , decrease the font by a bit and retry" ) - sys.exit(0) + return response else: - log.error("No stations found for the country code, recheck it") + log.error("No stations found for the country code/name, recheck it") sys.exit(1) # ------------------- by state --------------------- @@ -169,17 +195,20 @@ def discover_by_state(self, state, limit): if len(discover_result) > 1: table = Table(show_header=True, header_style="bold magenta") + table.add_column("ID", justify="center") table.add_column("Station", justify="left") # table.add_column("UUID", justify="center") table.add_column("Country", justify="center") table.add_column("Tags", justify="center") table.add_column("Language", justify="center") - for res in discover_result: + for i in range(0, len(discover_result)): + res = discover_result[i] table.add_row( + str(i + 1), trim_string(res["name"], max_length=30), # res["stationuuid"], - res["country"], + trim_string(res["country"], max_length=20), trim_string(res["tags"], max_length=20), trim_string(res["language"], max_length=20), ) @@ -188,7 +217,7 @@ def discover_by_state(self, state, limit): "If the table does not fit into your screen, \ntry to maximize the window , decrease the font by a bit and retry" ) - sys.exit(0) + return discover_result else: log.error("No stations found for the state, recheck it") sys.exit(1) @@ -205,16 +234,19 @@ def discover_by_language(self, language, limit): if len(discover_result) > 1: table = Table(show_header=True, header_style="bold magenta") + table.add_column("ID", justify="center") table.add_column("Station", justify="left") # table.add_column("UUID", justify="center") table.add_column("Country", justify="center") table.add_column("Tags", justify="center") - for res in discover_result: + for i in range(0, len(discover_result)): + res = discover_result[i] table.add_row( + str(i + 1), trim_string(res["name"], max_length=30), # res["stationuuid"], - res["country"], + trim_string(res["country"], max_length=20), trim_string(res["tags"], max_length=30), ) console.print(table) @@ -222,7 +254,7 @@ def discover_by_language(self, language, limit): "If the table does not fit into your screen, \ntry to maximize the window, decrease the font by a bit and retry" ) - sys.exit(0) + return discover_result else: log.error("No stations found for the language, recheck it") sys.exit(1) @@ -239,25 +271,27 @@ def discover_by_tag(self, tag, limit): if len(discover_result) > 1: table = Table(show_header=True, header_style="bold magenta") + table.add_column("ID", justify="center") table.add_column("Station", justify="left") - table.add_column("UUID", justify="center") + # table.add_column("UUID", justify="center") table.add_column("country", justify="center") table.add_column("Language", justify="center") - for res in discover_result: + for i in range(0, len(discover_result)): + res = discover_result[i] table.add_row( + str(i + 1), trim_string(res["name"], max_length=30), - res["stationuuid"], - res["country"], - res["language"], + # res["stationuuid"], + trim_string(res["country"], max_length=20), + trim_string(res["language"], max_length=20), ) console.print(table) log.info( "If the table does not fit into your screen, \ \ntry to maximize the window , decrease the font by a bit and retry" ) - - sys.exit(0) + return discover_result else: log.error("No stations found for the tag, recheck it") sys.exit(1) diff --git a/radioactive/help.py b/radioactive/help.py index 78e8a6e..a556658 100644 --- a/radioactive/help.py +++ b/radioactive/help.py @@ -1,7 +1,10 @@ +from os import path + from rich.console import Console -from rich.panel import Panel from rich.table import Table +user = path.expanduser("~") + def show_help(): """Show help message as table""" @@ -104,7 +107,7 @@ def show_help(): table.add_row( "--filepath", "Path to save the recorded audio", - "/User/Music/radioactive", + f"{user}/Music/radioactive", ) table.add_row( @@ -114,7 +117,7 @@ def show_help(): ) table.add_row( "--filetype, -T", - "Type / format of target recording", + "Type/codec of target recording. (mp3/auto)", "mp3", ) @@ -125,3 +128,6 @@ def show_help(): ) console.print(table) + print( + "For more details : https://github.com/deep5050/radio-active/blob/main/README.md" + ) diff --git a/radioactive/last_station.py b/radioactive/last_station.py index bc01bad..18325a0 100644 --- a/radioactive/last_station.py +++ b/radioactive/last_station.py @@ -28,17 +28,8 @@ def get_info(self): with open(self.last_station_path, "r") as f: last_station = json.load(f) return last_station - - # log.info("Playing last station: {}".format( - # last_station["name"])) - # if last_station['alias'] == True: - # # if station was an alias - # return last_station['uuid_or_url'] - # return last_station["stationuuid"] except Exception: 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 6a1c670..82205f5 100644 --- a/radioactive/player.py +++ b/radioactive/player.py @@ -40,13 +40,14 @@ class Player: FFmepg required to be installed separately """ - def __init__(self, URL, volume): + def __init__(self, URL, volume, loglevel): self.url = URL self.volume = volume self.is_playing = False self.process = None self.exe_path = None self.program_name = "ffplay" # constant value + self.loglevel = loglevel log.debug("player: url => {}".format(self.url)) # check if FFplay is installed @@ -60,18 +61,26 @@ def __init__(self, URL, volume): self.start_process() def start_process(self): + ffplay_commands = [ + self.exe_path, + "-volume", + f"{self.volume}", + "-vn", # no video playback + self.url, + ] + + if self.loglevel == "debug": + # don't add no disp and + ffplay_commands.append("-loglevel") + ffplay_commands.append("error") + + else: + ffplay_commands.append("-loglevel") + ffplay_commands.append("error") + ffplay_commands.append("-nodisp") try: self.process = subprocess.Popen( - [ - self.exe_path, - "-nodisp", - "-nostats", - "-loglevel", - "error", - "-volume", - f"{self.volume}", - self.url, - ], + ffplay_commands, shell=False, stdout=subprocess.PIPE, # Capture standard output stderr=subprocess.PIPE, # Capture standard error @@ -92,6 +101,7 @@ def check_error_output(self): while self.is_running: stderr_result = self.process.stderr.readline() if stderr_result: + print() # pass a blank line to command for better log messages log.error("Could not connect to the station") try: # try to show the debug info diff --git a/radioactive/recorder.py b/radioactive/recorder.py index f5f7a5c..c461451 100644 --- a/radioactive/recorder.py +++ b/radioactive/recorder.py @@ -3,25 +3,66 @@ from zenlog import log -def record_audio_from_url(input_url, output_file): +def record_audio_auto_codec(input_stream_url): + try: + # Run FFprobe to get the audio codec information + ffprobe_command = [ + "ffprobe", + "-v", + "error", + "-select_streams", + "a:0", + "-show_entries", + "stream=codec_name", + "-of", + "default=noprint_wrappers=1:nokey=1", + input_stream_url, + ] + + codec_info = subprocess.check_output(ffprobe_command, text=True) + + # Determine the file extension based on the audio codec + audio_codec = codec_info.strip() + audio_codec = audio_codec.split("\n")[0] + return audio_codec + + except subprocess.CalledProcessError as e: + log.error(f"Error: could not fetch codec {e}") + return None + + +def record_audio_from_url(input_url, output_file, force_mp3, loglevel): try: # Construct the FFmpeg command ffmpeg_command = [ "ffmpeg", "-i", - input_url, # Input URL - "-c:a", - "copy", # Codec (copy) audio - "-vn", # Disable video recording - # "-n", # no overwrite file, possible on foreground only - "-loglevel", - "error", # stop showing build and metadata info - "-hide_banner", + input_url, # input URL + "-vn", # disable video recording "-stats", # show stats - output_file, # Output file path ] - # Run FFmpeg command on frouground to catch 'q' without + # codec for audio stream + ffmpeg_command.append("-c:a") + if force_mp3: + ffmpeg_command.append("libmp3lame") + log.debug("Record: force libmp3lame") + else: + # file will be saved as as provided. this is more error prone + # file extension must match the actual stream codec + ffmpeg_command.append("copy") + + ffmpeg_command.append("-loglevel") + if loglevel == "debug": + ffmpeg_command.append("info") + else: + ffmpeg_command.append("error"), + ffmpeg_command.append("-hide_banner") + + # output file + ffmpeg_command.append(output_file) + + # Run FFmpeg command on foreground to catch 'q' without # any complex thread for now subprocess.run(ffmpeg_command, check=True) diff --git a/radioactive/utilities.py b/radioactive/utilities.py index 5b51d4e..dca70af 100644 --- a/radioactive/utilities.py +++ b/radioactive/utilities.py @@ -14,7 +14,7 @@ from radioactive.last_station import Last_station from radioactive.player import kill_background_ffplays -from radioactive.recorder import record_audio_from_url +from radioactive.recorder import record_audio_auto_codec, record_audio_from_url RED_COLOR = "\033[91m" END_COLOR = "\033[0m" @@ -24,16 +24,41 @@ def handle_log_level(args): log_level = args.log_level if log_level in ["info", "error", "warning", "debug"]: log.level(log_level) + return args.log_level else: log.warning("Correct log levels are: error,warning,info(default),debug") def handle_record( - target_url, curr_station_name, record_file_path, record_file, record_file_format + target_url, + curr_station_name, + record_file_path, + record_file, + record_file_format, # auto/mp3 + loglevel, ): log.info("Press 'q' to stop recording") + force_mp3 = False + + if record_file_format != "mp3" and record_file_format != "auto": + record_file_format = "mp3" # default to mp3 + log.debug("Error: wrong codec supplied!. falling back to mp3") + force_mp3 = True + elif record_file_format == "auto": + log.debug("Codec: fetching stream codec") + codec = record_audio_auto_codec(target_url) + if codec is None: + record_file_format = "mp3" # default to mp3 + force_mp3 = True + log.debug("Error: could not detect codec. falling back to mp3") + else: + record_file_format = codec + log.debug("Codec: found {}".format(codec)) + elif record_file_format == "mp3": + # always save to mp3 to eliminate any runtime issues + # it is better to leave it on libmp3lame + force_mp3 = True - # check record path if record_file_path and not os.path.exists(record_file_path): log.debug("filepath: {}".format(record_file_path)) os.makedirs(record_file_path, exist_ok=True) @@ -45,6 +70,7 @@ def handle_record( os.makedirs(record_file_path, exist_ok=True) except Exception as e: log.debug("{}".format(e)) + log.error("Could not make default directory") sys.exit(1) now = datetime.datetime.now() @@ -52,16 +78,10 @@ def handle_record( # Format AM/PM as 'AM' or 'PM' am_pm = now.strftime("%p") + # format is : day-monthname-year@hour-minute-second-(AM/PM) formatted_date_time = now.strftime(f"%d-{month_name}-%Y@%I-%M-%S-{am_pm}") - # formatted_date_time = now.strftime("%y-%m-%d-%H:%M:%S") - # check file format type. currently wav and mp3 supported - if record_file_format != ("mp3" and "wav"): - log.debug( - "Filetype: unknown type '{}'. falling back to mp3".format( - record_file_format - ) - ) + if not record_file_format.strip(): record_file_format = "mp3" if not record_file: @@ -74,7 +94,7 @@ def handle_record( log.info(f"Recording will be saved as: \n{outfile_path}") - record_audio_from_url(target_url, outfile_path) + record_audio_from_url(target_url, outfile_path, force_mp3, loglevel) def handle_welcome_screen(): @@ -114,7 +134,7 @@ def handle_favorite_table(alias): 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") + table.add_column("URL / UUID", justify="left") if len(alias.alias_map) > 0: for entry in alias.alias_map: table.add_row(entry["name"], entry["uuid_or_url"]) @@ -160,7 +180,7 @@ def handle_station_uuid_play(handler, station_uuid): station_url = handler.target_station["url"] except Exception as e: log.debug("{}".format(e)) - log.error("Somethig went wrong") + log.error("Something went wrong") sys.exit(1) return station_name, station_url @@ -170,7 +190,6 @@ def handle_search_stations(handler, station_name, limit): log.debug("Searching API for: {}".format(station_name)) return handler.search_by_station_name(station_name, limit) - # TODO: ask user to play using a # number of the result def handle_station_selection_menu(handler, last_station, alias): @@ -248,7 +267,9 @@ def handle_listen_keypress( record_file_path, record_file, record_file_format, + loglevel, ): + log.info("Press '?' to see available commands\n") while True: user_input = input("Enter a command to perform an action: ") if user_input == "r" or user_input == "R" or user_input == "record": @@ -258,30 +279,42 @@ def handle_listen_keypress( record_file_path, record_file, record_file_format, + loglevel, ) elif user_input == "rf" or user_input == "RF" or user_input == "recordfile": + # if no filename is provided try to auto detect + # else if ".mp3" is provided, use libmp3lame to force write to mp3 + user_input = input("Enter output filename: ") # try to get extension from filename try: file_name, file_ext = user_input.split(".") + if file_ext == "mp3": + log.debug("codec: force mp3") + # overwrite original codec with "mp3" + record_file_format = "mp3" + else: + log.warning("You can only specify mp3 as file extension.\n") + log.warning( + "Do not provide any extension to autodetect the codec.\n" + ) except: file_name = user_input - file_ext = "" # set default if user_input.strip() != "": handle_record( - target_url, station_name, record_file_path, file_name, file_ext + target_url, + station_name, + record_file_path, + file_name, + record_file_format, + loglevel, ) elif user_input == "f" or user_input == "F" or user_input == "fav": handle_add_to_favorite(alias, station_name, station_url) - elif ( - user_input == "q" - or user_input == "Q" - or user_input == "x" - or user_input == "quit" - ): + elif user_input == "q" or user_input == "Q" or user_input == "quit": kill_background_ffplays() sys.exit(0) elif user_input == "w" or user_input == "W" or user_input == "list": @@ -294,13 +327,12 @@ def handle_listen_keypress( or user_input == "?" or user_input == "help" ): - print() - print("q/Q/x/quit: Quit radioactive") - print("h/H/help/?: Show this help message") - print("r/R/record: Record a station") - print("f/F/fav: Add station to favorite list") - print("rf/RF/recordfile: Speficy a filename for the recording") - print() + log.info("h/help/?: Show this help message") + log.info("q/quit: Quit radioactive") + log.info("r/record: Record a station") + log.info("f/fav: Add station to favorite list") + log.info("rf/recordfile: Specify a filename for the recording") + # TODO: u for uuid, link for url, p for setting path def handle_current_play_panel(curr_station_name=""): @@ -324,7 +356,7 @@ def handle_user_choice_from_search_result(handler, response): log.debug("Playing UUID from single response") return handle_station_uuid_play(handler, response[0]["stationuuid"]) else: - log.debug("Quiting") + log.debug("Quitting") sys.exit(0) else: # multiple station @@ -357,13 +389,13 @@ def handle_direct_play(alias, station_name_or_url=""): # station name from fav list # search for the station in fav list and return name and url - respone = alias.search(station_name_or_url) - if not respone: + response = alias.search(station_name_or_url) + if not response: log.error("No station found on your favorite list with the name") sys.exit(1) else: - log.debug("Direct play: {}".format(respone)) - return respone["name"], respone["uuid_or_url"] + log.debug("Direct play: {}".format(response)) + return response["name"], response["uuid_or_url"] def handle_play_last_station(last_station): diff --git a/setup.py b/setup.py index 8dad7f0..71b4b1c 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,9 @@ app = App() -DESCRIPTION = "Play any radio around the globe right from the terminal" +DESCRIPTION = ( + "Play and record any radio stations around the globe right from the terminal" +) VERSION = app.get_version()