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
@@ -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()