From c1dd4503ce5e60927b2a5eb096d938cb03de9de2 Mon Sep 17 00:00:00 2001 From: Lars Wegner Date: Wed, 18 Oct 2023 17:50:59 +0200 Subject: [PATCH 01/13] Add type annotation to global player variable Signed-off-by: Lars Wegner --- radioactive/__main__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/radioactive/__main__.py b/radioactive/__main__.py index ced3a74..f084f79 100755 --- a/radioactive/__main__.py +++ b/radioactive/__main__.py @@ -3,6 +3,7 @@ import signal import sys from time import sleep +from typing import Optional from zenlog import log @@ -28,7 +29,7 @@ # globally needed as signal handler needs it # to terminate main() properly -player = None +player: Optional[Player] = None def final_step(options, last_station, alias, handler): From 0d7fb1df885fc0eca27106c73232b574a77f74b6 Mon Sep 17 00:00:00 2001 From: Lars Wegner Date: Wed, 18 Oct 2023 17:52:19 +0200 Subject: [PATCH 02/13] Make player.start_process private Signed-off-by: Lars Wegner --- radioactive/player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/radioactive/player.py b/radioactive/player.py index 82205f5..1ab4b7b 100644 --- a/radioactive/player.py +++ b/radioactive/player.py @@ -58,9 +58,9 @@ def __init__(self, URL, volume, loglevel): log.critical("FFplay not found, install it first please") sys.exit(1) - self.start_process() + self._start_process() - def start_process(self): + def _start_process(self): ffplay_commands = [ self.exe_path, "-volume", From c8a050db63e8c27afd5898d58700a9867741645e Mon Sep 17 00:00:00 2001 From: Lars Wegner Date: Wed, 18 Oct 2023 18:01:43 +0200 Subject: [PATCH 03/13] Prefer format strings Signed-off-by: Lars Wegner --- radioactive/player.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/radioactive/player.py b/radioactive/player.py index 1ab4b7b..ed6e930 100644 --- a/radioactive/player.py +++ b/radioactive/player.py @@ -35,7 +35,6 @@ def kill_background_ffplays(): class Player: - """FFPlayer handler, it holds all the attributes to properly execute ffplay FFmepg required to be installed separately """ @@ -49,14 +48,15 @@ def __init__(self, URL, volume, loglevel): self.program_name = "ffplay" # constant value self.loglevel = loglevel - log.debug("player: url => {}".format(self.url)) + log.debug(f"player: url => {self.url}") # check if FFplay is installed self.exe_path = which(self.program_name) - log.debug("FFplay: {}".format(self.exe_path)) - if self.exe_path is None: - log.critical("FFplay not found, install it first please") + log.critical(f"{self.program_name} not found, install it first please") sys.exit(1) + else: + log.debug(f"{self.program_name}: {self.exe_path}") + self._start_process() @@ -87,7 +87,7 @@ def _start_process(self): text=True, # Use text mode to capture strings ) self.is_running = True - log.debug("player: ffplay => PID {} initiated".format(self.process.pid)) + log.debug(f"player: {self.program_name} => PID {self.process.pid} initiated") # Create a thread to continuously capture and check error output error_thread = threading.Thread(target=self.check_error_output) error_thread.daemon = True @@ -95,7 +95,7 @@ def _start_process(self): except Exception as e: # Handle exceptions that might occur during process setup - log.error("Error while starting radio: {}".format(e)) + log.error(f"Error while starting radio: {e}") def check_error_output(self): while self.is_running: @@ -109,7 +109,7 @@ def check_error_output(self): # only showing the server response log.error(stderr_result.split(": ")[1]) except Exception as e: - log.debug("Error: {}".format(e)) + log.debug(f"Error: {e}") pass self.is_running = False @@ -147,7 +147,7 @@ def is_active(self): log.debug("Process not found") return False except Exception as e: - log.error("Error while checking process status: {}".format(e)) + log.error(f"Error while checking process status: {e}") return False def play(self): @@ -167,7 +167,7 @@ def stop(self): 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)) + log.error(f"Error while stopping radio: {e}") raise finally: self.is_playing = False From fbdb9eb0f6789df3de433992110c393befb975fa Mon Sep 17 00:00:00 2001 From: Lars Wegner Date: Wed, 18 Oct 2023 18:09:25 +0200 Subject: [PATCH 04/13] Infer player.is_playing() by existing ffplay process - player.stop() sets the process fast to None Signed-off-by: Lars Wegner --- radioactive/__main__.py | 2 +- radioactive/player.py | 27 ++++++++++++--------------- radioactive/utilities.py | 1 - 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/radioactive/__main__.py b/radioactive/__main__.py index f084f79..f03cdf3 100755 --- a/radioactive/__main__.py +++ b/radioactive/__main__.py @@ -278,7 +278,7 @@ def signal_handler(sig, frame): global player log.debug("You pressed Ctrl+C!") log.debug("Stopping the radio") - if player and player.is_playing: + if player and player.is_playing(): player.stop() log.info("Exiting now") sys.exit(0) diff --git a/radioactive/player.py b/radioactive/player.py index ed6e930..c1cd982 100644 --- a/radioactive/player.py +++ b/radioactive/player.py @@ -42,7 +42,6 @@ class Player: 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 @@ -86,7 +85,6 @@ def _start_process(self): stderr=subprocess.PIPE, # Capture standard error text=True, # Use text mode to capture strings ) - self.is_running = True log.debug(f"player: {self.program_name} => PID {self.process.pid} initiated") # Create a thread to continuously capture and check error output error_thread = threading.Thread(target=self.check_error_output) @@ -98,7 +96,7 @@ def _start_process(self): log.error(f"Error while starting radio: {e}") def check_error_output(self): - while self.is_running: + while self.is_playing(): stderr_result = self.process.stderr.readline() if stderr_result: print() # pass a blank line to command for better log messages @@ -111,8 +109,6 @@ def check_error_output(self): except Exception as e: log.debug(f"Error: {e}") pass - - self.is_running = False self.stop() sleep(2) @@ -152,26 +148,27 @@ def is_active(self): def play(self): """Play a station""" - if not self.is_playing: - pass # call the init function again ? + if not self.is_playing(): + self._start_process() + + def is_playing(self): + return self.process def stop(self): """stop the ffplayer""" - - if self.is_playing: + if self.is_playing(): + ffplay_proc = self.process + self.process = None try: - self.process.terminate() # Terminate the process gracefully - self.process.wait(timeout=5) # Wait for process to finish + ffplay_proc.terminate() # Terminate the process gracefully + ffplay_proc.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 + ffplay_proc.kill() # Kill the process forcefully except Exception as e: log.error(f"Error while stopping radio: {e}") raise - finally: - self.is_playing = False - self.process = None else: log.debug("Radio is not currently playing") current_pid = os.getpid() diff --git a/radioactive/utilities.py b/radioactive/utilities.py index dca70af..f799cad 100644 --- a/radioactive/utilities.py +++ b/radioactive/utilities.py @@ -320,7 +320,6 @@ def handle_listen_keypress( elif user_input == "w" or user_input == "W" or user_input == "list": alias.generate_map() handle_favorite_table(alias) - elif ( user_input == "h" or user_input == "H" From 909c59b35a501061564aede85a95ff978f1c1d7f Mon Sep 17 00:00:00 2001 From: Lars Wegner Date: Wed, 18 Oct 2023 18:48:08 +0200 Subject: [PATCH 05/13] Make player.is_playing a property Signed-off-by: Lars Wegner --- radioactive/__main__.py | 2 +- radioactive/player.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/radioactive/__main__.py b/radioactive/__main__.py index f03cdf3..f084f79 100755 --- a/radioactive/__main__.py +++ b/radioactive/__main__.py @@ -278,7 +278,7 @@ def signal_handler(sig, frame): global player log.debug("You pressed Ctrl+C!") log.debug("Stopping the radio") - if player and player.is_playing(): + if player and player.is_playing: player.stop() log.info("Exiting now") sys.exit(0) diff --git a/radioactive/player.py b/radioactive/player.py index c1cd982..d5b14bd 100644 --- a/radioactive/player.py +++ b/radioactive/player.py @@ -96,7 +96,7 @@ def _start_process(self): log.error(f"Error while starting radio: {e}") def check_error_output(self): - while self.is_playing(): + while self.is_playing: stderr_result = self.process.stderr.readline() if stderr_result: print() # pass a blank line to command for better log messages @@ -148,15 +148,16 @@ def is_active(self): def play(self): """Play a station""" - if not self.is_playing(): + if not self.is_playing: self._start_process() + @property def is_playing(self): return self.process def stop(self): """stop the ffplayer""" - if self.is_playing(): + if self.is_playing: ffplay_proc = self.process self.process = None try: From 8814e1e95df1e7d0e6d6a2f46e96728cf118abd0 Mon Sep 17 00:00:00 2001 From: Lars Wegner Date: Wed, 18 Oct 2023 18:11:35 +0200 Subject: [PATCH 06/13] Add ffprobe to read stream info Signed-off-by: Lars Wegner --- radioactive/player.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/radioactive/player.py b/radioactive/player.py index d5b14bd..b9174b1 100644 --- a/radioactive/player.py +++ b/radioactive/player.py @@ -45,6 +45,8 @@ def __init__(self, URL, volume, loglevel): self.process = None self.exe_path = None self.program_name = "ffplay" # constant value + self._metadata_program = "ffprobe" # constant value + self._exe_metadata_path = None self.loglevel = loglevel log.debug(f"player: url => {self.url}") @@ -56,7 +58,12 @@ def __init__(self, URL, volume, loglevel): else: log.debug(f"{self.program_name}: {self.exe_path}") - + self._exe_metadata_path = which(self._metadata_program) + if self._exe_metadata_path is None: + log.critical(f"{self._metadata_program} not found, install it first please") + sys.exit(1) + else: + log.debug(f"{self.program_name}: {self._exe_metadata_path}") self._start_process() def _start_process(self): From 07b455a1269635f96276b3d5cda52240ec3d53d4 Mon Sep 17 00:00:00 2001 From: Lars Wegner Date: Wed, 18 Oct 2023 18:37:59 +0200 Subject: [PATCH 07/13] Read stream info every 1s Signed-off-by: Lars Wegner --- radioactive/player.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/radioactive/player.py b/radioactive/player.py index b9174b1..a72f4e8 100644 --- a/radioactive/player.py +++ b/radioactive/player.py @@ -44,6 +44,7 @@ def __init__(self, URL, volume, loglevel): self.volume = volume self.process = None self.exe_path = None + self._stream_title: str = "" self.program_name = "ffplay" # constant value self._metadata_program = "ffprobe" # constant value self._exe_metadata_path = None @@ -66,6 +67,33 @@ def __init__(self, URL, volume, loglevel): log.debug(f"{self.program_name}: {self._exe_metadata_path}") self._start_process() + def read_title(self): + ffprobe_command = [ + self._exe_metadata_path, + "-v", "error", + "-select_streams", "a:0", + # TODO maybe this tag is different on different stations + "-show_entries", "format_tags=StreamTitle", + "-of", "default=noprint_wrappers=1:nokey=1", + self.url + ] + try: + ffprobe_proc = subprocess.Popen( + ffprobe_command, + shell=False, + stdout=subprocess.PIPE, # Capture standard output + stderr=subprocess.PIPE, # Capture standard error + text=True, # Use text mode to capture strings + ) + title = "" + for line in ffprobe_proc.stdout: + title += line + return title + except Exception as e: + # Handle exceptions that might occur during process setup + log.error(f"Error while reading current track: {e}") + return f"Error while reading current track: {e}" + def _start_process(self): ffplay_commands = [ self.exe_path, @@ -97,11 +125,21 @@ def _start_process(self): error_thread = threading.Thread(target=self.check_error_output) error_thread.daemon = True error_thread.start() + # Create a thread to update the stream title information + title_update_thread = threading.Thread(target=self.update_title) + title_update_thread.daemon = True + title_update_thread.start() except Exception as e: # Handle exceptions that might occur during process setup log.error(f"Error while starting radio: {e}") + def update_title(self): + while self.is_playing: + self._stream_title = self.read_title() + sleep(1) + self._stream_title = "" + def check_error_output(self): while self.is_playing: stderr_result = self.process.stderr.readline() From 3c1f0721e386af458c5433e78978b1e81e8e66c0 Mon Sep 17 00:00:00 2001 From: Lars Wegner Date: Wed, 18 Oct 2023 18:39:09 +0200 Subject: [PATCH 08/13] Allow the player to switch the url Signed-off-by: Lars Wegner --- radioactive/player.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/radioactive/player.py b/radioactive/player.py index a72f4e8..985ce4d 100644 --- a/radioactive/player.py +++ b/radioactive/player.py @@ -219,3 +219,8 @@ def stop(self): log.debug("Radio is not currently playing") current_pid = os.getpid() os.kill(current_pid, signal.SIGINT) + + def switch_url(self, url: str): + self.stop() + self.url = url + self.play() From b5e235f955a0d985688b13d65028be2f22263268 Mon Sep 17 00:00:00 2001 From: Lars Wegner Date: Wed, 18 Oct 2023 20:01:26 +0200 Subject: [PATCH 09/13] Add 's' and 'S' for start/stop replay - also reworking handle_listen_keypress Signed-off-by: Lars Wegner --- radioactive/__main__.py | 1 + radioactive/utilities.py | 28 ++++++++++++++++------------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/radioactive/__main__.py b/radioactive/__main__.py index f084f79..0c3f1c6 100755 --- a/radioactive/__main__.py +++ b/radioactive/__main__.py @@ -66,6 +66,7 @@ def final_step(options, last_station, alias, handler): ) handle_listen_keypress( + player, alias, target_url=options["target_url"], station_name=options["curr_station_name"], diff --git a/radioactive/utilities.py b/radioactive/utilities.py index f799cad..79efeaa 100644 --- a/radioactive/utilities.py +++ b/radioactive/utilities.py @@ -3,6 +3,7 @@ import datetime import os import sys +from typing import Optional from pick import pick from rich import print @@ -13,7 +14,7 @@ from zenlog import log from radioactive.last_station import Last_station -from radioactive.player import kill_background_ffplays +from radioactive.player import kill_background_ffplays, Player from radioactive.recorder import record_audio_auto_codec, record_audio_from_url RED_COLOR = "\033[91m" @@ -260,6 +261,7 @@ def handle_save_last_station(last_station, station_name, station_url): def handle_listen_keypress( + player: Optional[Player], alias, target_url, station_name, @@ -272,7 +274,7 @@ def handle_listen_keypress( 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": + if user_input in ["r", "R", "record"]: handle_record( target_url, station_name, @@ -281,7 +283,7 @@ def handle_listen_keypress( record_file_format, loglevel, ) - elif user_input == "rf" or user_input == "RF" or user_input == "recordfile": + elif user_input in ["rf", "RF", "recordfile"]: # if no filename is provided try to auto detect # else if ".mp3" is provided, use libmp3lame to force write to mp3 @@ -311,26 +313,28 @@ def handle_listen_keypress( loglevel, ) - elif user_input == "f" or user_input == "F" or user_input == "fav": + elif user_input in ["f", "F", "fav"]: handle_add_to_favorite(alias, station_name, station_url) - elif user_input == "q" or user_input == "Q" or user_input == "quit": + elif user_input in ["q", "Q", "quit"]: kill_background_ffplays() sys.exit(0) - elif user_input == "w" or user_input == "W" or user_input == "list": + elif user_input in ["w", "W", "list"]: alias.generate_map() handle_favorite_table(alias) - elif ( - user_input == "h" - or user_input == "H" - or user_input == "?" - or user_input == "help" - ): + elif user_input in ["s", "S"]: + if player: + if player.is_playing: + player.stop() + else: + player.play() + elif user_input in ["h", "H", "?", "help"]: 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") + log.info("s: Start/stop playing the station") # TODO: u for uuid, link for url, p for setting path From 80d7d431e86cf26e7972393aec2cbe1db1cd9c82 Mon Sep 17 00:00:00 2001 From: Lars Wegner Date: Wed, 18 Oct 2023 20:10:39 +0200 Subject: [PATCH 10/13] Stop player on exception Signed-off-by: Lars Wegner --- radioactive/__main__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/radioactive/__main__.py b/radioactive/__main__.py index 0c3f1c6..1cdb02c 100755 --- a/radioactive/__main__.py +++ b/radioactive/__main__.py @@ -288,4 +288,8 @@ def signal_handler(sig, frame): signal.signal(signal.SIGINT, signal_handler) if __name__ == "__main__": - main() + try: + main() + finally: + if player: + player.stop() From 910fb5aa1ab1d1e2a041390556536c2ac31fd919 Mon Sep 17 00:00:00 2001 From: Lars Wegner Date: Wed, 18 Oct 2023 20:18:26 +0200 Subject: [PATCH 11/13] Set current song on startup Signed-off-by: Lars Wegner --- radioactive/__main__.py | 2 +- radioactive/player.py | 7 ++++++- radioactive/utilities.py | 7 +++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/radioactive/__main__.py b/radioactive/__main__.py index 1cdb02c..6aba7b1 100755 --- a/radioactive/__main__.py +++ b/radioactive/__main__.py @@ -53,7 +53,7 @@ def final_step(options, last_station, alias, handler): alias, options["curr_station_name"], options["target_url"] ) - handle_current_play_panel(options["curr_station_name"]) + handle_current_play_panel(player, options["curr_station_name"]) if options["record_stream"]: handle_record( diff --git a/radioactive/player.py b/radioactive/player.py index 985ce4d..3924aad 100644 --- a/radioactive/player.py +++ b/radioactive/player.py @@ -88,7 +88,8 @@ def read_title(self): title = "" for line in ffprobe_proc.stdout: title += line - return title + # truncate trailing \n + return title[:-1] if title[-1] == "\n" else title except Exception as e: # Handle exceptions that might occur during process setup log.error(f"Error while reading current track: {e}") @@ -200,6 +201,10 @@ def play(self): def is_playing(self): return self.process + @property + def stream_title(self): + return self._stream_title + def stop(self): """stop the ffplayer""" if self.is_playing: diff --git a/radioactive/utilities.py b/radioactive/utilities.py index 79efeaa..4de3529 100644 --- a/radioactive/utilities.py +++ b/radioactive/utilities.py @@ -338,8 +338,11 @@ def handle_listen_keypress( # TODO: u for uuid, link for url, p for setting path -def handle_current_play_panel(curr_station_name=""): - panel_station_name = Text(curr_station_name, justify="center") +def handle_current_play_panel(player: Optional[Player], curr_station_name=""): + now_playing = curr_station_name + if player: + now_playing += f"\n{player.read_title()}" + panel_station_name = Text(now_playing, justify="center") station_panel = Panel(panel_station_name, title="[blink]:radio:[/blink]", width=85) console = Console() From 787aef143c88f74a3de70fdc3a83f011b6cbe8cb Mon Sep 17 00:00:00 2001 From: Lars Wegner Date: Wed, 18 Oct 2023 21:06:41 +0200 Subject: [PATCH 12/13] Introduce callback to notify, if player state changes The current callback needs to be improved,this needs a type for stations Signed-off-by: Lars Wegner --- radioactive/__main__.py | 9 ++++++--- radioactive/player.py | 28 ++++++++++++++++++++++++---- radioactive/utilities.py | 1 + 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/radioactive/__main__.py b/radioactive/__main__.py index 6aba7b1..7373ac0 100755 --- a/radioactive/__main__.py +++ b/radioactive/__main__.py @@ -42,7 +42,12 @@ def final_step(options, last_station, alias, handler): if options["curr_station_name"].strip() == "": options["curr_station_name"] = "N/A" - player = Player(options["target_url"], options["volume"], options["loglevel"]) + def curry_current_station_name_state_changed(callback_player: Optional[Player]): + # TODO clean this up, as soon as there's a type for stations, + # which holds at least the station and url + handle_current_play_panel(callback_player, options["curr_station_name"]) + + player = Player(options["target_url"], options["volume"], options["loglevel"],[curry_current_station_name_state_changed]) handle_save_last_station( last_station, options["curr_station_name"], options["target_url"] @@ -53,8 +58,6 @@ def final_step(options, last_station, alias, handler): alias, options["curr_station_name"], options["target_url"] ) - handle_current_play_panel(player, options["curr_station_name"]) - if options["record_stream"]: handle_record( options["target_url"], diff --git a/radioactive/player.py b/radioactive/player.py index 3924aad..6f0f7c4 100644 --- a/radioactive/player.py +++ b/radioactive/player.py @@ -1,4 +1,5 @@ """ FFplay process handler """ +from __future__ import annotations import os import signal @@ -7,6 +8,7 @@ import threading from shutil import which from time import sleep +from typing import Callable, List import psutil from zenlog import log @@ -39,8 +41,14 @@ class Player: FFmepg required to be installed separately """ - def __init__(self, URL, volume, loglevel): - self.url = URL + def __init__( + self, + url: str, # TODO pass the station in the future + volume: int, # [0..100] everything else is interpreted as 100 by ffplay + loglevel: str, + state_changed: List[Callable[[Player], None]] = None + ): + self.url = url self.volume = volume self.process = None self.exe_path = None @@ -49,6 +57,7 @@ def __init__(self, URL, volume, loglevel): self._metadata_program = "ffprobe" # constant value self._exe_metadata_path = None self.loglevel = loglevel + self._state_changed = state_changed log.debug(f"player: url => {self.url}") # check if FFplay is installed @@ -121,7 +130,8 @@ def _start_process(self): stderr=subprocess.PIPE, # Capture standard error text=True, # Use text mode to capture strings ) - log.debug(f"player: {self.program_name} => PID {self.process.pid} initiated") + log.debug( + f"player: {self.program_name} => PID {self.process.pid} initiated") # Create a thread to continuously capture and check error output error_thread = threading.Thread(target=self.check_error_output) error_thread.daemon = True @@ -137,7 +147,10 @@ def _start_process(self): def update_title(self): while self.is_playing: - self._stream_title = self.read_title() + new_title = self.read_title() + if self._stream_title != new_title: + self._stream_title = new_title + self._informAboutChange() sleep(1) self._stream_title = "" @@ -210,6 +223,8 @@ def stop(self): if self.is_playing: ffplay_proc = self.process self.process = None + self._stream_title = "" + self._informAboutChange() try: ffplay_proc.terminate() # Terminate the process gracefully ffplay_proc.wait(timeout=5) # Wait for process to finish @@ -225,6 +240,11 @@ def stop(self): current_pid = os.getpid() os.kill(current_pid, signal.SIGINT) + def _informAboutChange(self): + for changed in self._state_changed: + changed(self) + + # TODO pass the station in the future def switch_url(self, url: str): self.stop() self.url = url diff --git a/radioactive/utilities.py b/radioactive/utilities.py index 4de3529..03725c9 100644 --- a/radioactive/utilities.py +++ b/radioactive/utilities.py @@ -339,6 +339,7 @@ def handle_listen_keypress( def handle_current_play_panel(player: Optional[Player], curr_station_name=""): + print() now_playing = curr_station_name if player: now_playing += f"\n{player.read_title()}" From c2fde617a01e67e6b0c66c6242c92a9dbff7e49c Mon Sep 17 00:00:00 2001 From: Lars Wegner Date: Wed, 18 Oct 2023 21:16:17 +0200 Subject: [PATCH 13/13] Update documentation Signed-off-by: Lars Wegner --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4dfd18e..3e1ba2e 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,7 @@ h/H/help/?: Show this help message r/R/record: Record a station f/F/fav: Add station to favorite list rf/RF/recordfile: Specify a filename for the recording. +s: Start/stop playing the station ```