# -*- coding: utf-8 -*-
import locale
import csv
import sys
import logging
import glob
import curses
import collections
import json
from pathlib import Path
# import socket
from os import path, getenv, makedirs, remove, rename, readlink, SEEK_END, SEEK_CUR, getpid, listdir, access, R_OK, environ, fspath, unlink
from time import ctime, sleep
from datetime import datetime
from shutil import which, copyfile, move, Error as shutil_Error, rmtree as remove_tree
import threading
from copy import deepcopy
from platform import system
from rich.console import Console
from rich.table import Table
from rich.align import Align
from rich import print
try:
    # Python ≥ 3.9
    from importlib.resources import files, as_file
    from importlib.resources.abc import Traversable
except ImportError:
    # Python 3.7 & 3.8 (backport)
    from importlib_resources import files, as_file
    from importlib_resources.abc import Traversable
from pyradio import version
from .common import validate_resource_opener_path, is_rasberrypi, Station, describe_playlist, CsvReadWrite, ProfileManager
from .keyboard import read_keyboard_shortcuts, read_localized_keyboard, set_lkbkey
from .browser import probeBrowsers
from .player import pywhich
from .server import IPsWithNumbers
from .xdg import XdgDirs, XdgMigrate, CheckDir
from .install import get_a_linux_resource_opener
from .html_help import is_graphical_environment_running
from .log import TIME_FORMATS
from .m3u import parse_m3u

try:
    from subprocess import Popen, DEVNULL
except ImportError:
    from subprocess import Popen
if system().lower() == 'windows':
    from os import startfile
else:
    pass
HAS_REQUESTS = True

try:
    import requests
except:
    HAS_REQUESTS = False

HAS_DNSPYTHON = True
try:
    from dns import resolver
except:
    HAS_DNSPYTHON = False
HAS_PSUTIL = True
try:
    import psutil
except:
    HAS_PSUTIL = False

logger = logging.getLogger(__name__)

locale.setlocale(locale.LC_ALL, "")

def to_ip_port(string):
    host = 'localhost'
    port = '11111'
    if string:
        sp = string.lower().split(':')
        if sp:
            if sp[0] == 'lan' or sp[0] == 'localhost':
                host = sp[0]
            else:
                x = IPsWithNumbers(
                        default_ip=sp[0],
                        fat=True
                )
                if x.current() == sp[0]:
                    host = sp[0]
                    # try:
                    #     x = socket.inet_pton(socket.AF_INET, sp[0])
                    #     host = sp[0]
                    # except:
                    #     return 'localhost', '11111'
                else:
                    return 'localhost', '11111'
            try:
                x = int(sp[1])
                if x > 1025:
                    port = sp[1]
                else:
                    host = 'localhost'
            except ValueError:
                host = 'localhost'
    return host, port

class PyRadioStations():
    ''' PyRadio stations file management '''

    ''' playlist_version:
            2: 4 columns (name,URL,encoding,online browser)
            1: 3 columns (name,URL,encoding)
            0: 2 columns (name,URL)
    '''
    PLAYLIST_HAS_NAME_URL = 0
    PLAYLIST_HAS_NAME_URL_ENCODING = 1
    PLAYLIST_HAS_NAME_URL_ENCODING_ICON = 2
    PLAYLIST_HAS_NAME_URL_ENCODING_ICON_VOL = 3
    PLAYLIST_HAS_NAME_URL_ENCODING_ICON_VOL_HTTP = 4
    PLAYLIST_HAS_NAME_URL_ENCODING_ICON_VOL_HTTP_REF = 5

    def __init__(self, stationFile='', user_config_dir=None):
        self.foreign_title = ''
        self.previous_station_path = ''
        self.stations_history = None
        self.foreign_copy_asked = False
        self.added_stations = 0
        self._is_register = False

        ''' True if playlist not in config dir '''
        self.foreign_file = False

        self.stations = []
        self._reading_stations = []
        self.playlists = []

        self.selected_playlist = -1
        self.number_of_stations = -1

        self._playlist_version = Station.url
        self._read_playlist_version = Station.url

        self.dirty_playlist = False

        self.playlist_recovery_result = 0

        self._open_string_id = 0

        self.jump_tag = -1

        ''' station directory service object '''
        self._online_browser = None

        self._register_to_open = None
        self._open_register_list = False

        self._registers_lock = threading.Lock()

        ''' set to True if a stations.csv is found in user's folder '''
        self._user_csv_found = False

        ''' the playlist saved as last playlist (name only) '''
        self._last_opened_playlist_name = ''

        ''' last opened playlist splitted on , '''
        self.last_playlist_to_open = []

        self.station_history = None
        self.play_from_history = False

        self.normal_stations_history = None

        self.show_no_themes_message = True

        self.renamed_stations = []

        self.favorites = None

        if sys.platform.startswith('win'):
            self._open_string_id = 1

        if user_config_dir is not None:
            self.stations_dir = user_config_dir
            self.xdg.build_paths()
        self.xdg.ensure_paths_exist()
        self.root_path = files("pyradio").joinpath("stations.csv")
        # logger.error(f'{self.root_path = }')
        self.themes_dir = path.join(self.stations_dir, 'themes')
        self.favorites_path = path.join(self.stations_dir, 'favorites.csv')
        try:
            makedirs(self.themes_dir, exist_ok = True)
        except:
            try:
                makedirs(self.themes_dir)
            except:
                pass

        self._ps = PyRadioPlaylistStack()

        if not self.locked and not self.headless:
            ''' If a station.csv file exitst, which is wrong,
                we rename it to stations.csv '''
            if path.exists(path.join(self.stations_dir, 'station.csv')):
                copyfile(path.join(self.stations_dir, 'station.csv'),
                         path.join(self.stations_dir, 'stations.csv'))
                remove(path.join(self.stations_dir, 'station.csv'))

            self._move_old_csv(self.stations_dir)
            self._check_stations_csv(self.stations_dir, self.root_path)
            self._move_to_data()
            ''' rename radio browser config '''
            rb_config = path.join(self.stations_dir, 'radio-browser-config')
            if path.exists(rb_config):
                new_rb_config = path.join(self.stations_dir, 'radio-browser.conf')
                rename(rb_config, new_rb_config)

    def add_to_favorites(self, an_item):
        if self.favorites is None:
            self.favorites = FavoritesManager(self.favorites_path)
        return self.favorites.add(an_item)

    def _move_to_data(self):
        if not path.exists(self.data_dir):
            makedirs(self.data_dir)
        files_to_move = [
            path.join(self.stations_dir, 'pyradio.png'),
        ]
        for n in ('.*.date', '.*.ver', '*.lock'):
            misc_files = glob.glob(path.join(self.stations_dir, n))
            if misc_files:
                files_to_move.extend(misc_files)
        for n in files_to_move:
            from_file = path.join(self.stations_dir, n)
            if path.exists(from_file):
                to_file = from_file.replace(self.stations_dir, self.state_dir)
                if path.exists(to_file):
                    try:
                        remove(from_file)
                    except (shutil_Error, IOError, OSError):
                        pass
                else:
                    try:
                        move(from_file, self.data_dir)
                    except (shutil_Error, IOError, OSError):
                        print('\n\nError moving data files!\nPlease close all PyRadio related files and try again...\n')
                        sys.exit(1)

    def _copy_icon(self):
        ''' if i still do not have the icons in the data dir
            copy it from the icons dir
        '''
        upd = path.join(self.state_dir, 'UPDATE_ICON')
        # # remove the file, to force icons update
        # # only do this if the packages's icons have been changed
        # # for example with gimp or inkscape
        # if path.exists(upd):
        #     os.remove(upd)

        for an_icon in ('pyradio.png', 'cover.png'):
            if (not path.exists(path.join(self.data_dir, an_icon)) or
                not path.exists(upd)):
                to_file = path.join(self.data_dir, an_icon)
                try:
                    icon_res = files('pyradio').joinpath('icons', an_icon)
                    with as_file(icon_res) as icon_path:
                        copyfile(icon_path, to_file)
                except Exception:
                    pass
        # create file so that icons will not be forced copied
        with open(upd, 'w', encoding='utf-8') as f:
            f.write('\n')

        ''' make sure that the icons are under ~/.config/pyradio/data
            (the previous section may install it to a different location,
            if --config-dir is used).
        '''
        default_icon_location = self.data_dir
        if default_icon_location != self.data_dir:
            for an_icon in ('pyradio.png', 'cover.png'):
                # target location
                to_file = path.join(default_icon_location, an_icon)

                try:
                    # package resource
                    icon_res = files('pyradio').joinpath('icons', an_icon)
                    if isinstance(icon_res, Traversable) and icon_res.is_file():
                        with as_file(icon_res) as tmp_path:
                            copyfile(tmp_path, to_file)
                    else:
                        # fallback to real path if available
                        real_path = str(icon_res)
                        if path.exists(real_path):
                            copyfile(real_path, to_file)
                except Exception:
                    pass

    @property
    def stations_dir(self):
        return self.xdg.stations_dir

    @property
    def registers_dir(self):
        return self.xdg.registers_dir

    @property
    def data_dir(self):
        return self.xdg.data_dir

    @property
    def logos_dir(self):
        return self.xdg.logos_dir

    @property
    def state_dir(self):
        return self.xdg.state_dir

    @property
    def cache_dir(self):
        return self.xdg.cache_dir

    @property
    def user_csv_found(self):
        return self._user_csv_found

    @user_csv_found.setter
    def user_csv_found(self, val):
        raise ValueError('parameter is read only')

    @property
    def is_local_playlist(self):
        return self._ps.is_local_playlist

    @is_local_playlist.setter
    def is_local_playlist(self, value):
        raise ValueError('property is read only')

    @property
    def is_register(self):
        return self._ps.is_register

    @is_register.setter
    def is_register(self, value):
        raise ValueError('property is read only')

    @property
    def internal_header_height(self):
        if self._online_browser:
            return self._online_browser.internal_header_height
        return 0

    @internal_header_height.setter
    def internal_header_height(self, value):
        raise ValueError('property is read only')

    @property
    def online_browser(self):
        return self._online_browser

    @online_browser.setter
    def online_browser(self, value):
        self._online_browser = value

    @property
    def playlist_version(self):
        return self._playlist_version

    @playlist_version.setter
    def playlist_version(self, value):
        raise ValueError('property is read only')

    #@property
    #def browsing_station_service(self):
    #    return self._ps.browsing_station_service

    #@browsing_station_service.setter
    #def browsing_station_service(self, value):
    #    self._ps.browsing_station_service = value

    @property
    def history_selection(self):
        return self._ps.selection

    @history_selection.setter
    def history_selection(self, value):
        self._ps.selection = value

    @property
    def history_startPos(self):
        return self._ps.startPos

    @history_startPos.setter
    def history_startPos(self, value):
        self._ps.startPos = value

    @property
    def history_playing(self):
        return self._ps.playing

    @history_playing.setter
    def history_playing(self, value):
        self._ps.playing = value

    @property
    def browsing_station_service(self):
        return self._ps.browsing_station_service

    @browsing_station_service.setter
    def browsing_station_service(self, value):
        self._ps.browsing_station_service = value

    @property
    def open_register_list(self):
        return self._open_register_list

    @open_register_list.setter
    def open_register_list(self, value):
        self._open_register_list = value

    @property
    def register_to_open(self):
        return self._register_to_open

    @register_to_open.setter
    def register_to_open(self, value):
        self._register_to_open = value

    @property
    def station_path(self):
        return self._ps.station_path

    @station_path.setter
    def station_path(self, value):
        self._ps.station_path = value

    @property
    def station_file_name(self):
        return self._ps.station_file_name

    @station_file_name.setter
    def station_file_name(self, value):
        self._ps.station_file_name = value

    @property
    def station_title(self):
        return self._ps.station_title

    @station_title.setter
    def station_title(self, value):
        self._ps.station_title = value

    @property
    def can_go_back_in_time(self):
        return len(self._ps._p) > 1

    @can_go_back_in_time.setter
    def can_go_cack_in_time(self, value):
        raise ValueError('property is read only')

    def set_station_history(self,
                            execute_funct,
                            pass_first_item_funct,
                            pass_last_item_funct,
                            no_items_funct):
        self.normal_stations_history = PyRadioStationsStack(
            execute_function=execute_funct,
            pass_first_item_function=pass_first_item_funct,
            pass_last_item_function=pass_last_item_funct,
            no_items_function=no_items_funct
        )
        self.stations_history = self.normal_stations_history

    def save_last_playlist(self, sel):
        lp = path.join(self.state_dir, 'last-playlist')
        llp = self._ps.last_local_playlist
        out_pl = [llp[2], llp[4], llp[5]]
        if llp[2]:
            # logger.error(f'llp = {llp} - saved = {self._last_opened_playlist_name}')
            try:
                with open(lp, 'w', encoding='utf-8') as f:
                    writter = csv.writer(f)
                    writter.writerow(out_pl)
            except PermissionError:
                pass

    def url(self, id_in_list):
        if self._ps.browsing_station_service:
            # TODO get browser url
            return self._online_browser.url(id_in_list)
            #return self.stations[id_in_list][1].strip()
        return self.stations[id_in_list][1].strip()

    def _move_old_csv(self, usr):
        ''' if a ~/.pyradio files exists, relocate it in user
            config folder and rename it to stations.csv, or if
            that exists, to pyradio.csv '''

        src = path.join(path.expanduser('~'), '.pyradio')
        dst = path.join(usr, 'pyradio.csv')
        dst1 = path.join(usr, 'stations.csv')
        if path.exists(src) and path.isfile(src):
            if path.exists(dst1):
                copyfile(src, dst)
            else:
                copyfile(src, dst1)
            try:
                remove(src)
            except:
                pass

    def _check_stations_csv(self, usr, root):
        ''' Reclocate a stations.csv copy in user home for easy manage.
            E.g. not need sudo when you add new station, etc '''

        if path.exists(path.join(usr, 'stations.csv')):
            self._user_csv_found = True
            return

        # root_traversable είναι το files(...).joinpath(...)
        with as_file(root) as root_real:
            copyfile(root_real, path.join(usr, 'stations.csv'))
        with open(path.join(self.state_dir, 'last-sync'), 'w', encoding='utf-8') as f:
            self.get_pyradio_version()
            v = self.current_pyradio_version.replace('.', ', ')
            f.write(v)

    def copy_playlist_to_config_dir(self):
        ''' Copy a foreign playlist in config dir
            Returns:
                -1: error copying file
                 0: success
                 1: playlist renamed
        '''
        ret = 0
        st = path.join(self.stations_dir, self.station_file_name)
        if path.exists(st):
            ret = 1
            st = datetime.now().strftime("%Y-%m-%d_%H-%M-%S_")
            st = path.join(self.stations_dir, st + self.station_file_name)
        try:
            copyfile(self.station_path, st)
        except:
            if logger.isEnabledFor(logging.ERROR):
                logger.error(f'Cannot copy playlist: "{self.station_path}"')
            ret = -1
            return
        self._set_playlist_elements(st)
        self.read_playlists()
        self.foreign_file = False
        if logger.isEnabledFor(logging.DEBUG):
            logger.debug(f'Playlist copied to: "{st}"')
        return ret

    def is_same_playlist(self, a_playlist):
        ''' Checks if a playlist is already loaded '''
        return a_playlist == self.station_path

    def is_playlist_reloaded(self):
        return self.is_same_playlist(self.previous_station_path)

    def _is_playlist_in_config_dir(self):
        ''' Check if a csv file is in the config dir '''
        if path.dirname(self.station_path) == self.stations_dir:
            self.foreign_file = False
            self.foreign_title = ''
        else:
            self.foreign_file = True
            self.foreign_title = self.station_title
        self.foreign_copy_asked = False

    def _get_register_filename_from_register(self):
        if self._register_to_open:
            p = path.join(self.registers_dir,
                    'register_' + self._register_to_open + '.csv')
            if path.exists(p) and path.isfile(p):
                return p, 0
        return p, -2

    def _get_playlist_abspath_from_data(self, stationFile=''):
        ''' Get playlist absolute path
            Returns: playlist path, result
              Result is:
                0  -  playlist found
               -2  -  playlist not found
               -3  -  negative number specified
               -4  -  number not found
               -8  -  file type not supported
               '''
        ret = -1
        orig_input = stationFile

        if stationFile:
            if stationFile.lower().endswith(('.csv', 'm3u')):
                ''' relative or absolute path '''
                stationFile = path.abspath(stationFile)
                if path.exists(stationFile) and path.isfile(stationFile):
                    return stationFile, 0
            else:
                ''' try to find it in config dir '''
                if path.exists(stationFile):
                    return '', -8
                for an_ext in ('.csv', '.m3u', '.CSV', '.M3U'):
                    test_file = path.join(self.stations_dir, stationFile + an_ext)
                    if path.exists(test_file) and path.isfile(test_file):
                        return test_file, 0
        else:
            for p in [path.join(self.stations_dir, 'pyradio.csv'),
                      path.join(self.stations_dir, 'stations.csv'),
                      self.root_path]:
                if path.exists(p) and path.isfile(p):
                    return p, 0

        if ret == -1:
            ''' Check if playlist number was specified '''
            if orig_input.replace('-', '').isdigit():
                sel = int(orig_input) - 1
                if sel == -1:
                    stationFile = path.join(self.stations_dir, 'stations.csv')
                    return stationFile, 0
                if sel < 0:
                    ''' negative playlist number '''
                    return '', -3
                n, _ = self.read_playlists()
                if sel <= n-1:
                    stationFile = self.playlists[sel][-1]
                    return stationFile, 0
                ''' playlist number sel does not exit '''
                return '', -4
            return '', -2

    def read_playlist_for_server(self, stationFile):
        """ read the station names only from a playlist """
        out = []
        in_file = self._construct_playlist_path(stationFile)
        try:
            with open(in_file, 'r', encoding='utf-8') as cfgfile:
                for row in csv.reader(filter(lambda row: row[0]!='#', cfgfile), skipinitialspace=True):
                    if not row:
                        continue
                    out.append('<b>' + row[0] + '</b>') if row[1] == '-' else out.append(row[0])
        except:
            return None, []
        return in_file, out

    def read_playlist_file(
        self,
        stationFile='',
        is_last_playlist=False,
        is_register=False):
        ''' Read a csv file
            Returns: number
                x  -  number of stations or
               -1  -  playlist is malformed
               -2  -  playlist not found (from _get_playlist_abspath_from_data)
               -3  -  negative number specified (from _get_playlist_abspath_from_data)
               -4  -  number not found (from _get_playlist_abspath_from_data)
               -7  -  playlist recovery failed
               -8  -  file not supported (from _get_playlist_abspath_from_data)
               '''

        ret = 0
        if self._register_to_open:
            stationFile, ret = self._get_register_filename_from_register()
            self._is_register = True
        else:
            stationFile, ret = self._get_playlist_abspath_from_data(stationFile=stationFile)
            self._is_register = False

        if ret == 0 and stationFile.lower().endswith('.m3u'):
            print('Converting [green]M3U[/green] to [green]CSV[/green]...')
            # Coming from -s command line parameter
            # Try to convert m3u to csv
            stations, error = parse_m3u(stationFile)
            if error:
                print(f'[bold red]Error:[/bold red] Cannot convert file: "{stationFile}" : "[red]{error}[/red]"')
                sys.exit(1)
            csv_file_to_save = stationFile[:-3] + 'csv'
            if path.exists(csv_file_to_save):
                print(f'[red]Error:[/red] File already exists: "{csv_file_to_save}"')
                sys.exit(1)
            out_csv = CsvReadWrite(csv_file_to_save)
            ret = out_csv.write(items=stations)
            out_csv = None
            # logger.error(f'{ret = }')
            if ret == 0:
                stationFile = csv_file_to_save
            else:
                print(f'[bold red]Error:[/bold red] Cannot save CSV file: "{csv_file_to_save}')
                if not path.exists(csv_file_to_save):
                    try:
                        unlink(csv_file_to_save)
                    except (FileNotFoundError, PermissionError, IsADirectoryError, OSError) as e:
                        pass
                sys.exit(1)

        read_file = True
        prev_file = ''
        if ret < 0:
            ''' returns -2, -3, -4 or -8 '''
            self.last_playlist_to_open = []
            if self._register_to_open:
                self._reading_stations = []
                prev_file = self.station_path
                prev_format = self._playlist_version
                self._read_playlist_version = self._playlist_version = Station.referer
                read_file = False
            else:
                self.stations = []
                return ret

        current_playlist_version = Station.url
        if read_file:
            if self._register_to_open:
                self.playlist_recovery_result = 0
            else:
                self.playlist_recovery_result = self._recover_backed_up_playlist(stationFile)
                if self.playlist_recovery_result > 0:
                    ''' playlist recovery failed
                        reason in cnf.playlist_recovery_result
                    '''
                    return -7
            prev_file = self.station_path
            prev_format = self._playlist_version
            self._read_playlist_version = self._playlist_version = Station.url
            self._reading_stations = []
            csv_in = CsvReadWrite(
                a_file=stationFile,
                encoding_to_remove=self.default_encoding
            )
            ret = csv_in.read()
            if not ret:
                self._reading_stations = []
                self._playlist_version = prev_format
                csv_in = None
                return -1
            self._reading_stations = csv_in.items
            current_playlist_version = csv_in.version
            csv_in = None
        self._read_playlist_version = self._playlist_version = current_playlist_version
        if logger.isEnabledFor(logging.DEBUG):
            logger.debug('===> {} = {}'.format(describe_playlist(self._playlist_version), self._playlist_version))

        self.stations = list(self._reading_stations)
        # logger.error('DE stations\n{}\n\n'.format(self.stations))
        self.set_playlist_data(stationFile, prev_file, is_register)
        self.number_of_stations = len(self.stations)

        # for n in self.stations:
        #     logger.info(n)
        return self.number_of_stations

    def set_playlist_data(self, stationFile, prev_file, is_register = False):
        ''' used to be part of read_playlist_file
            moved here so it can be used with station history
            when opening a playlit
        '''
        self._reading_stations = []
        self._ps.add(is_register=self._open_register_list or is_register)
        self._set_playlist_elements(stationFile)
        self.previous_station_path = prev_file
        self._is_playlist_in_config_dir()
        self.dirty_playlist = False
        if logger.isEnabledFor(logging.DEBUG):
            logger.debug('===> {} = {}'.format(describe_playlist(self._playlist_version), self._playlist_version))
        self.jump_tag = -1

    def _recover_backed_up_playlist(self, stationFile):
        ''' If a playlist backup file exists (.txt file), try to
            recover it (rename it to .csv)

            Return:
                -1: playlist recovered
                 0: no back up file found
                 1: remove (empty) csv file failed
                 2: rename txt to csv failed '''
        backup_stationFile = stationFile.replace('.csv', '.txt')
        if path.isfile(backup_stationFile):
            try:
                tmp = curses.COLORS
                if logger.isEnabledFor(logging.INFO):
                    logger.info(f'Trying to recover backed up playlist: "{backup_stationFile}"')
            except:
                print(f'Trying to recover backed up playlist:\n  "{backup_stationFile}"')


            if path.isfile(stationFile):
                try:
                    remove(stationFile)
                except:
                    ''' remove failed '''
                    if logger.isEnabledFor(logging.INFO):
                        logger.info('Playlist recovery failed: Cannot remove CSV file')
                    return 1
            try:
                rename(backup_stationFile, stationFile)
            except:
                ''' rename failed '''
                if logger.isEnabledFor(logging.INFO):
                    logger.info('Playlist recovery failed: Cannot rename TXT file to CSV')
                return 2
            ''' playlist recovered '''
            if logger.isEnabledFor(logging.INFO):
                logger.info('Playlist recovery successful!!!')
            return -1
        ''' no playlist back up found '''
        return 0

    def _playlist_format_changed(self):
        ''' Check if we have new or old format
            and report if format has changed
        '''
        playlist_version = Station.url  # Start with the assumption that the minimum version is 'url'
        max_value = max(station.value for station in Station)  # Get the maximum value from the Station enum

        found_max = False  # Flag to indicate if the maximum version has been found

        for n in self.stations:  # Iterate over each station in self.stations
            if found_max:  # If we've already found the maximum version, skip further processing
                break

            for a_version in range(max_value - 1, 0, -1):  # Iterate backward from max_value-1 down to 1
                if n[a_version]:  # Check if the value at index a_version is non-empty
                    if a_version > playlist_version:  # If this version is greater than the current playlist_version
                        playlist_version = a_version  # Update playlist_version to this higher version
                        if playlist_version == max_value - 1:  # If we just reached the highest possible version
                            found_max = True  # Set the flag to True
                            break  # Exit the inner loop immediately

        if self._playlist_version == playlist_version:
            ret = False
        else:
            self._playlist_version = playlist_version
            ret = True
        if logger.isEnabledFor(logging.DEBUG):
            logger.debug('===> {} = {}, {}'.format(
                self._playlist_version,
                describe_playlist(self._playlist_version),
                '(format changed)' if ret else '(same format)'
            ))
        return ret

    def save_playlist_file(self, stationFile=''):
        ''' Save a playlist
        Create a txt file and write stations in it.
        Then rename it to final target

        return    0: All ok
                 -1: Error writing file
                 -2: Error renaming file
        '''
        if self._playlist_format_changed():
            self.dirty_playlist = True

        if stationFile:
            st_file = stationFile
        else:
            st_file = self.station_path
        if logger.isEnabledFor(logging.DEBUG):
            logger.debug(f'Saving playlist: "{st_file}"')

        if not self.dirty_playlist:
            if logger.isEnabledFor(logging.DEBUG):
                logger.debug('Playlist not modified...')
            return 0

        out_csv = CsvReadWrite(
            a_file=st_file,
            encoding_to_remove=self.default_encoding
        )
        ret = out_csv.write(items=self.stations)
        if ret == 0:
            self.dirty_playlist = False
        out_csv = None
        return ret

    def _set_playlist_elements(self, a_playlist, a_title=''):
        self.station_path = path.abspath(a_playlist)
        self.station_file_name = path.basename(self.station_path)
        if a_title:
            self.station_title = a_title
        else:
            self.station_title = '.'.join(self.station_file_name.split('.')[:-1])
        if self.is_register and self.station_title.startswith('register_'):
            self.station_title = self.station_title.replace('register_', 'Register: ')
        self._ps.remove_duplicates()
        #for n in self._ps._p:
        #    logger.error('DE ------ {}'.format(n))

    def give_me_a_new_playlist_history_item(self):
        return ['', '', '', -1, -1, -1, False,False]

    def playlist_history_len(self):
        return len(self._ps)

    def get_playlist_history_item(self, item_id=-1):
        return self._ps.item(item_id)

    def dup_to_playlist_history(self):
        self._ps.duplicate()

    def add_to_playlist_history(self, station_path='',
            station_file_name='',
            station_title='',
            startPos=0, selection=0, playing=-1,
            is_register=False,
            browsing_station_service=False):
        self._ps.add(
                station_path=station_path,
                station_file_name=station_file_name,
                station_title=station_title,
                startPos=startPos,
                selection=selection,
                playing=playing,
                is_register=is_register,
                browsing_station_service=browsing_station_service
                )

    def reset_playlist_history(self):
        self._ps.reset()

    def remove_from_playlist_history(self):
        item = self._ps.pop()
        rem_item = self.clean_playlist_history()
        return item if rem_item is None else rem_item

    def clean_playlist_history(self):
        '''Remove all register items from
           the end of history'''
        item = None
        while self._ps._p[-1][6]:
            item = self._ps.pop()
        return item

    def copy_playlist_history(self):
        return self._ps.copy()

    def replace_playlist_history_items(self, a_search_path, new_item):
        ''' Find a_search_path in history and replace
            the item found with new_item '''
        return self._ps.replace(a_search_path, new_item)

    def _bytes_to_human(self, B):
        ''' Return the given bytes as a human friendly KB, MB, GB, or TB string '''
        KB = float(1024)
        MB = float(KB ** 2) # 1,048,576
        GB = float(KB ** 3) # 1,073,741,824
        TB = float(KB ** 4) # 1,099,511,627,776

        if B < KB:
            return f'{B} B'
        B = float(B)
        if KB <= B < MB:
            return f'{B / KB:.2f} KB'
        if MB <= B < GB:
            return f'{B / MB:.2f} MB'
        if GB <= B < TB:
            return f'{B / GB:.2f} GB'
        if TB <= B:
            return f'{B / TB:.2f} TB'

    def append_station(self, params, stationFile=''):
        ''' Append a station to csv file

        return    0: All ok
                 -2  -  playlist not found
                 -3  -  negative number specified
                 -4  -  number not found
                 -5: Error writing file
                 -6: Error renaming file
        '''
        if stationFile:
            st_file = stationFile
        else:
            st_file = self.station_path

        st_file, ret = self._get_playlist_abspath_from_data(st_file)
        if ret < -1:
            return ret
        param_len = len(params) - 2
        self._playlist_format_changed()
        if param_len == self._playlist_version:
            if logger.isEnabledFor(logging.DEBUG):
                logger.debug(f'Appending station to playlist: "{stationFile}"')
            try:
                #with open(st_file, 'a') as cfgfile:
                with open(st_file, 'a', encoding='utf-8') as cfgfile:
                    writter = csv.writer(cfgfile)
                    writter.writerow(params)
                return 0
            except:
                return -5
        else:
            #self.stations.append([ params[0], params[1], params[2] ])
            self.stations.append(params[:])
            self.dirty_playlist = True
            ret = self.save_playlist_file(st_file)
            if ret < 0:
                ret -= 4
            return ret

    def paste_station_to_named_playlist(self, a_station, a_playlist):
        ''' Appends a station to a playlist or register
        which is not opened in PyRadio.

        return    0: All ok
                 -2  -  playlist not found
                 -3  -  negative number specified
                 -4  -  number not found
                 -5: Error writing file
                 -6: Error renaming file
        '''
        if logger.isEnabledFor(logging.DEBUG):
            logger.debug(f'pasting to "{a_playlist}"')
        if path.exists(a_playlist):
            m_station = a_station[:]
            ch = ('  ', ',')
            for a_ch in ch:
                if a_ch in m_station[0]:
                    m_station[0] = '"' + m_station[0] + '"'
                    break

            w_str = ','.join(m_station)
            while w_str.endswith(','):
                w_str = w_str[:-1]
            try:
                with open(a_playlist, 'a', encoding='utf-8') as f:
                    f.write('\n' + w_str)
                return 0
            except:
                return -5
        else:
            return -2

    def remove_station(self, target):
        self.dirty_playlist = True
        d = collections.deque(self.stations)
        d.rotate(-target)
        ret = d.popleft()
        d.rotate(target)
        self.stations = list(d)
        #ret = self.stations.pop(target)
        self.number_of_stations = len(self.stations)
        return ret, self.number_of_stations

    def insert_station(self, station, target):
        ''' Insert a station in the list at index target
        It is inserted ABOVE old target (old target becomes old target + 1)'''
        # logger.error('DE target= {0}, number_of_stations = {1}'.format(target, self.number_of_stations))
        if target < 0 or \
                target > self.number_of_stations or \
                self.number_of_stations == 0:
            return False, self.number_of_stations
        if station[2] == 'utf-8':
            station[2] = ''
        if target == self.number_of_stations:
            self.stations.append(station)
        else:
            d = collections.deque(self.stations)
            d.rotate(-target)
            d.appendleft(station)
            d.rotate(target)
            self.stations = list(d)
        self.dirty_playlist = True
        self.number_of_stations = len(self.stations)
        # logger.error('DE number_of_stations = {}'.format(self.number_of_stations))
        return True, self.number_of_stations

    def move_station(self, source, target):
        ''' Moves a station in the list from index source to index target
        It is moved ABOVE old target (old target becomes old target + 1)'''
        # logger.error('DE source = {0}, target = {1}'.format(source, target))
        # logger.error('DE number_of_stations = {}'.format(self.number_of_stations))
        if source == target or \
                source < 0 or \
                target < 0 or \
                source >= self.number_of_stations or \
                target >= self.number_of_stations or \
                self.number_of_stations == 0:
            # logger.error('DE \n\nreturning False\n\n')
            return False
        d = collections.deque(self.stations)
        d.rotate(-source)
        source_item = d.popleft()
        # logger.error('DE source_item = "{}"'.format(source_item))
        d.rotate(source)
        d.rotate(-target)
        d.appendleft(source_item)
        d.rotate(target)
        self.stations = list(d)
        self.number_of_stations = len(self.stations)
        self.dirty_playlist = True
        return True

    def switch_stations(self, source, target):
        if source == target or \
                source < 0 or \
                target < 0 or \
                source >= self.number_of_stations or \
                target >= self.number_of_stations or \
                self.number_of_stations == 0:
            return False, self.number_of_stations
        target_item = self.stations[target]
        d = collections.deque(self.stations)
        self.stations.clear()
        d.rotate(-source)
        source_item = d.popleft()
        d.appendleft(target_item)
        d.rotate(source)
        d.rotate(-target)
        d.popleft()
        d.appendleft(source_item)
        d.rotate(target)
        self.stations = list(d)
        self.number_of_stations = len(self.stations)
        return True, self.number_of_stations

    def registers_exist(self):
        return glob.glob(path.join(self.registers_dir, '*.[Cc][Ss][Vv]'))

    def just_read_playlists(self):
        self.playlists = [
            f for f in glob.glob(path.join(self.stations_dir, '*.[Cc][Ss][Vv]'))
            if path.isfile(f)  # Exclude directories accidentally named *.CSV
        ]

    def read_playlists(self):
        self.playlists = []
        self.selected_playlist = -1
        csv_files = []
        m3u_files = []
        if self._open_register_list:
            csv_files = glob.glob(path.join(self.registers_dir, '*.[Cc][Ss][Vv]'))
        else:
            csv_files = glob.glob(path.join(self.stations_dir, '*.[Cc][Ss][Vv]'))
            m3u_files = glob.glob(path.join(self.stations_dir, '*.[Mm][3][Uu]'))
            # Filter M3U files:
            m3u_files = [f for f in m3u_files
                        if path.getsize(f) > 0
                        and access(f, R_OK)
                        and not any(path.splitext(path.basename(f))[0] == path.splitext(path.basename(csv))[0] for csv in csv_files)]

        if len(csv_files) == 0 and len(m3u_files) == 0:
            return 0, -1
        # logger.error('\n\n\n------------------')
        for a_file in csv_files:
            # logger.error(f'{a_file = }')
            a_file_name = '.'.join(path.basename(a_file).split('.')[:-1])
            # logger.error(f'{a_file_name = }')
            a_file_size = self._bytes_to_human(path.getsize(a_file))
            a_file_time = ctime(path.getmtime(a_file))
            self.playlists.append([a_file_name, a_file_time, a_file_size, a_file])

        if m3u_files:
            for a_file in m3u_files:
                a_file_name = '.'.join(path.basename(a_file).split('.')[:-1])
                a_file_size = self._bytes_to_human(path.getsize(a_file))
                a_file_time = ctime(path.getmtime(a_file))
                self.playlists.append([a_file_name + ' (m3u)', a_file_time, a_file_size, a_file])
        self.playlists.sort()

        # logger.error('\n\n')
        # for n in self.playlists:
        #     logger.error(f'{n}')
        # logger.error('\n\n')

        ''' get already loaded playlist id '''
        for i, a_playlist in enumerate(self.playlists):
            if a_playlist[-1] == self.station_path:
                self.selected_playlist = i
                break
        return len(self.playlists), self.selected_playlist

    def list_playlists(self):
        self.read_playlists()
        console = Console()

        table = Table(show_header=True, header_style="bold magenta")
        #table.title = 'Playlist: [bold magenta]{}[/bold magenta]'.format(pyradio_config.station_title)
        table.title_justify = "left"
        table.row_styles = ['', 'plum4']
        centered_table = Align.center(table)
        table.title = f'Playlists found in "[magenta]{self.stations_dir}[/magenta]"'
        table.title_justify = "left"
        table.add_column("#", justify="right")
        table.add_column("Name")
        table.add_column("Size", justify="right")
        table.add_column("Date")
        for i, n in enumerate(self.playlists):
            table.add_row(
                str(i+1),
                n[0],
                n[2],
                n[1],
            )
        console.print(centered_table)

    def current_playlist_index(self):
        if not self.playlists:
            self.read_playlists()
        for i, a_playlist in enumerate(self.playlists):
            #if a_playlist[0] == self.station_title:
            if a_playlist[3] == self.station_path:
                return i
        return -1

    def open_browser(self, url, search_return_function, message_function, cannot_delete_function):
        self._online_browser = probeBrowsers(url)(
            self,
            search_return_function=search_return_function,
            message_function=message_function,
            cannot_delete_function=cannot_delete_function
        )

    def save_station_position(self, startPos, selection, playing):
        # logger.error('DE startPos = {0}, selection = {1}'.format(startPos, selection))
        self._ps.startPos = startPos
        self._ps.selection = selection
        self._ps.playing = playing
        # logger.error('DE  self._ps._p\n\n{}\n\n'.format(self._ps._p))

    def append_to_register(self, register, station):
        reg_file = path.join(self.registers_dir, 'register_' + register + '.csv')
        a_station = station[:]
        name = a_station[0].replace('"', '""')
        if ',' in name:
            a_station[0] = '"' + name + '"'
        else:
            a_station[0] = name
        while len(a_station) < Station.referer:
            a_station.append('')
        string_to_write = ','.join(a_station) + '\n'
        with self._registers_lock:
            try:
                with open(reg_file, 'a', encoding='utf-8') as f:
                    f.write(string_to_write)
            except:
                if logger.isEnabledFor(logging.DEBUG):
                    logger.debug('Failed to save to register: ' + register)
                ret = reg_file
        ret = ''
        return ret

    def pop_to_first_real_playlist(self):
        self._ps.pop_to_first_real_playlist()

    def remove_playlist_history_duplicates(self):
        self._ps.remove_duplicates()

    def history_item(self, an_item=-1):
        # logger.error('DE /// history_item = {}'.format(self._ps._p[an_item]))
        return self._ps._p[an_item][:]

    def find_history_by_station_path(self, a_path):
        return self._ps._find_history_by_id(a_path, 'path')

    def find_history_by_station_name(self, a_name):
        return self._ps._find_history_by_id(a_name, 'file_name')

    def find_history_by_station_title(self, a_title):
        ret, ret_index, rev_ret_index = self._ps._find_history_by_id(a_title, 'title')
        if not ret:
            ret, ret_index, rev_ret_index = self._ps._find_history_by_id(a_title.replace('_', ' '), 'title')
        return ret, ret_index, rev_ret_index

class PyRadioConfig(PyRadioStations):
    ''' PyRadio Config Class '''

    internal_themes = (
        'dark', 'light', 'dark_16_colors',
        'light_16_colors', 'black_on_white', 'bow',
        'white_on_black', 'wob'
    )

    def _init_vars(self):
        ''' if debug is on, this will tell the logger to
                0:  not log input from the player
                1:  input accepted input from the player
                2:  input raw input from the player

            It applies to the updateStatus, updateMPVStatus and
                updateWinVLCStatus functions
        '''
        self.debug_log_player_input = 0

        self.localize = None
        self._old_localize = None

        self.EXTERNAL_PLAYER_OPTS = None
        self.USE_EXTERNAL_PLAYER = False


        ''' I will get this when a player is selected
            It will be used when command line parameters are evaluated
        '''
        self.SUPPORTED_PLAYERS = ('mpv', 'mplayer', 'vlc')
        self.AVAILABLE_PLAYERS = None

        self.PLAYER_NAME = None

        self.fallback_theme = ''
        self.use_themes = True
        self.terminal_is_blacklisted = False
        self.no_themes_notification_shown = False
        self.no_themes_from_command_line = False

        self.theme_not_supported = False
        self.theme_has_error = False
        self.theme_download_failed = False
        self.theme_not_supported_notification_shown = False

        self.log_debug = False

        ''' Title logging '''
        self._current_log_title = self._current_log_station = ''
        self._old_log_title = self._old_log_station = ''
        self._last_liked_title = ''
        self._current_notification_message = ''
        self._notification_command = None

        self.show_recording_start_message = True

        ''' True if lock file exists '''
        self.locked = False

        ''' this is used to inhibit opening the search window
            currently used by:
                - PyRadioKeyboardConfig when editing
        '''
        self.inhibit_search = False

        self._distro = 'None'
        self.opts = collections.OrderedDict()
        self.opts['general_title'] = ['General Options', '']
        self.opts['player'] = ['Player: ', '']
        self.opts['open_last_playlist'] = ['Open last playlist: ', False]
        self.opts['default_playlist'] = ['Def. playlist: ', 'stations']
        self.opts['default_station'] = ['Def. station: ', 'False']
        self.opts['default_encoding'] = ['Def. encoding: ', 'utf-8']
        self.opts['continuous_playback'] = ['Continuous playback: ', False]
        self.opts['recording_dir'] = ['Recordings dir: ', '']
        self.opts['resource_opener'] = ['Resource Opener: ', 'auto']
        self.opts['log_titles'] = ['Log titles: ', False]
        self.opts['use_os_media_controls'] = ['Enable OS Media Controls: ', False]
        self.opts['playlist_manngement_title'] = ['Playlist Management Options', '']
        self.opts['confirm_station_deletion'] = ['Confirm station deletion: ', True]
        self.opts['confirm_playlist_reload'] = ['Confirm playlist reload: ', True]
        self.opts['auto_save_playlist'] = ['Auto save playlist: ', False]
        self.opts['conn_title'] = ['Connection Options', '']
        self.opts['connection_timeout'] = ['Connection timeout: ', '10']
        self.opts['force_http'] = ['Force http connections: ', False]
        self.opts['buffering'] = ['Buffering (seconds): ', '20']
        self.opts['mplayer_save_br'] = ['MPlayer auto save br: ', False]
        self.opts['notification'] = ['Notifications', '']
        self.opts['enable_notifications'] = ['Enable notifications: ', '-1']
        self.opts['use_station_icon'] = ['  Use station icon: ', True]
        self.opts['remove_station_icons'] = ['  Remove cached icons: ', True]
        self.opts['update_window_title'] = ['Update Window Title: ', True]
        self.opts['tts_title'] = ['TTS', '']
        self.opts['enable_tts'] = ['Enable TTS: ', False]
        self.opts['tts_volume'] = ['  Volume: ', '50']
        self.opts['tts_rate'] = ['  Rate: ', '0']
        self.opts['tts_pitch'] = ['  Pitch: ', '0']
        self.opts['tts_verbosity'] = ['Verbosity: ', 'default']
        self.opts['tts_context'] = ['Context: ', 'all']
        self.opts['tts_speak_volume_change'] = ['Speak volume change: ', '0']
        self.opts['tts_speak_volume_change_start'] = ['  Speak start too: ', False]
        self.opts['clock_title'] = ['Clock', '']
        self.opts['enable_clock'] = ['Display on startup: ', False]
        self.opts['time_format'] = ['Time format: ', '1']
        self.opts['theme_title'] = ['Theme Options', '']
        self.opts['theme'] = ['Theme: ', 'dark']
        self.opts['use_transparency'] = ['Use transparency: ', False]
        self.opts['force_transparency'] = ['  Force transparency: ', False]
        self.opts['calculated_color_factor'] = ['Calculated color: ', '0']
        self.opts['console_theme'] = ['Console theme: ', 'dark']
        self.opts['mouse_options'] = ['Mouse Support', '']
        self.opts['enable_mouse'] = ['Enable mouse support: ', False]
        self.opts['wheel_adjusts_volume'] = ['  Reverse wheel: ', False]
        self.opts['remote'] = ['Remote Control Server', '']
        self.opts['remote_control_server_ip'] = ['Server IP: ', 'localhost']
        self.opts['remote_control_server_port'] = ['Server Port: ', '9998']
        self.opts['remote_control_server_auto_start'] = ['Auto-start Server: ', False]
        self.opts['shortcuts'] = ['Keyboard Shortcuts', '']
        self.opts['shortcuts_keys'] = ['Shortcuts', '-']
        self.opts['localized_keys'] = ['Localized Shortcuts', '-']
        self.opts['online_header'] = ['Online services', '']
        self.opts['auto_update_stations'] = ['Auto update stations: ', True]
        self.opts['radiobrowser'] = ['RadioBrowser', '-']
        self.opts['requested_player'] = ['', '']
        self.opts['dirty_config'] = ['', False]

        '''
        Keep several config options when no themes mode is enabled
        '''
        self.bck_opts = {}

        if sys.platform == 'win32':
            self.th_path = path.join(getenv('APPDATA'), 'pyradio', 'themes', 'auto.pyradio-themes')
        else:
            self.th_path = path.join(path.expanduser('~'), '.config', 'pyradio', 'themes', 'auto.pyradio-themes')
        self.opts['auto_update_theme'] = ['',  False]

        self.original_mousemask = (0, 0)

        ''' parameters used by the program
            may get modified by "Z" command
            but will not be saved to file
        '''
        self.params = {
            'mpv': [1, 'profile:pyradio'],
            'mplayer': [1, 'profile:pyradio'],
            'vlc': [1, 'Do not use any extra player parameters']
        }
        ''' parameters read from config file
            can only be modified from config window
        '''
        self.saved_params = deepcopy(self.params)

        self.params_changed = False

        ''' number of user specified (-pp) extra
            player parameter parameter id
        '''
        self.user_param_id = 0

        self.PROGRAM_UPDATE = False
        self.current_pyradio_version = None

        ''' Windows manage players trigger '''
        self.WIN_MANAGE_PLAYERS = False

        ''' Windows print EXE location trigger '''
        self.WIN_PRINT_PATHS = False

        ''' Windows Uninstall trigger '''
        self.WIN_UNINSTALL = False

        self.enable_calculated_colors = True
        self.has_border_background  = False

        self.start_colors_at = 0

        self.buffering_data = []
        self.buffering_enabled = True

        self._fixed_recording_dir = None

        self._linux_resource_opener = None

        self.need_to_fix_desktop_file_icon = False

        self.notification_image_file = None

        self._last_station_checked = None
        self._last_station_checked_id = -1
        self._check_output_folder = None
        self._check_output_file = None

        self.profile_manager = None
        self.use_calculated_colors = False

    def __init__(self, user_config_dir=None, headless=False):
        self.localize = None
        self._old_localize = None
        self.terminal_is_blacklisted = False
        self.no_themes_from_command_line = False
        self.show_recording_start_message = False
        self.locked = False
        self.params = None
        self.saved_params = None
        self.params_changed = False
        self.current_pyradio_version = None
        self._fixed_recording_dir = None
        self._linux_resource_opener = None
        self.need_to_fix_desktop_file_icon = False
        self.use_calculated_colors = False
        self.user_agent_string = None
        self.info = None
        self._show_colors_cannot_change = None
        self.config_opts = None
        self.__dirty_config = False
        self.player_params_file = None
        self.schedule_file = None
        self.keyboard_file = None
        self.active_enable_clock = False
        self.log_debug = False

        self._user_config_dir = user_config_dir
        self._headless = headless
        self.xdg = XdgDirs(
                a_dir_fix_function=self._save_config_from_fixed_rec_dir
                )
        self._init_vars()
        super().__init__()
        # keep old recording / new recording dir
        self.rec_dirs = ()
        self._first_read = True
        self.backup_player_params = None
        self.player = ''
        self.requested_player = ''
        self.confirm_station_deletion = True
        self.confirm_playlist_reload = True
        self.auto_save_playlist = False
        self.default_playlist = 'stations'
        self.default_station = 'False'
        self.force_http = False
        self.default_encoding = 'utf-8'
        self.connection_timeout = '10'
        self.theme = 'dark'
        self.active_transparency = False
        self._distro = 'None'
        self.dirty_config = self.params_changed
        ''' True if player changed by config window '''
        self.player_changed = False
        ''' [ old player, new player ] '''
        self.player_values = []

        self._session_lock_file = ''
        self._get_lock_file()
        if user_config_dir is None:
            self.xdg.migrate(self.locked or self.headless)

        PyRadioStations.__init__(self, user_config_dir=user_config_dir)

        self._check_config_file(self.stations_dir)
        self.config_file = path.join(self.stations_dir, 'config')

        self.force_to_remove_lock_file = False
        self.titles_log = PyRadioLog(self)
        #self.titles_log = PyRadioLog(self.stations_dir)

        ''' auto update programs
            Currently, base16 only
        '''
        self.base16_themes = PyRadioBase16Themes(self)
        self.pywal_themes = PyRadioPyWalThemes(self)
        self.theme_sh_themes = PyRadioThemesShThemes(self)
        self.auto_update_frameworks = ( self.base16_themes, self.pywal_themes, self.theme_sh_themes)

        self._read_notification_command()
        self.profile_manager = ProfileManager()

    @property
    def update_window_title(self):
        return self.opts['update_window_title'][1]

    @update_window_title.setter
    def update_window_title(self, val):
        self.opts['update_window_title'][1] = val
        self.opts['dirty_config'][1] = True

    @property
    def use_os_media_controls(self):
        return self.opts['use_os_media_controls'][1]

    @use_os_media_controls.setter
    def use_os_media_controls(self, val):
        self.opts['use_os_media_controls'][1] = val
        self.opts['dirty_config'][1] = True

    @property
    def continuous_playback(self):
        return self.opts['continuous_playback'][1]

    @continuous_playback.setter
    def continuous_playback(self, val):
        self.opts['continuous_playback'][1] = val
        self.opts['dirty_config'][1] = True

    @property
    def mplayer_save_br(self):
        return self.opts['mplayer_save_br'][1]

    @mplayer_save_br.setter
    def mplayer_save_br(self, val):
        self.opts['mplayer_save_br'][1] = val
        self.opts['dirty_config'][1] = True

    @property
    def buffering(self):
        return self.opts['buffering'][1]

    @buffering.setter
    def buffering(self, val):
        str_val = str(val)
        old_val = self.opts['buffering'][1]
        try:
            if str_val == '0' or \
                    5 <= int(str_val) <= 60:
                self.opts['buffering'][1] = str(int(str_val))
            else:
                self.opts['buffering'][1] = '0'
        except ValueError:
            self.opts['buffering'][1] = '0'
        if old_val != self.opts['buffering'][1]:
            self.dirty_config = True

    @property
    def auto_update_stations(self):
        return self.opts['auto_update_stations'][1]

    @auto_update_stations.setter
    def auto_update_stations(self, val):
        old_val = self.opts['auto_update_stations'][1]
        self.opts['auto_update_stations'][1] = val
        if old_val != val:
            self.dirty_config = True

    @property
    def log_titles(self):
        return self.opts['log_titles'][1]

    @log_titles.setter
    def log_titles(self, val):
        old_val = self.opts['log_titles'][1]
        self.opts['log_titles'][1] = val
        if old_val != val:
            self.dirty_config = True

    @property
    def linux_resource_opener(self):
        return self._linux_resource_opener

    @property
    def xdg_compliant(self):
        return self.xdg.xdg_compliant

    @xdg_compliant.setter
    def xdg_compliant(self, val):
        self.xdg.xdg_compliant = val

    @property
    def headless(self):
        if self._headless is None:
            return False
        return True

    @property
    def remote_control_server_report_file(self):
        if self.headless:
            return path.join(self.state_dir, 'server-headless.txt')
        return path.join(self.state_dir, 'server.txt')

    @property
    def enable_clock(self):
        return self.opts['enable_clock'][1]

    @enable_clock.setter
    def enable_clock(self, val):
        old_val = self.opts['enable_clock'][1]
        self.opts['enable_clock'][1] = val
        if old_val != val:
            self.dirty_config = True

    @property
    def wheel_adjusts_volume(self):
        return self.opts['wheel_adjusts_volume'][1]

    @property
    def time_format(self):
        return self.opts['time_format'][1]

    @time_format.setter
    def time_format(self, val):
        old_val = self.opts['time_format'][1]
        self.opts['time_format'][1] = val
        if old_val != val:
            self.dirty_config = True

    @property
    def open_last_playlist(self):
        return self.opts['open_last_playlist'][1]

    @open_last_playlist.setter
    def open_last_playlist(self, val):
        old_val = self.opts['open_last_playlist'][1]
        self.opts['open_last_playlist'][1] = val
        if old_val != val:
            self.dirty_config = True

    @property
    def distro(self):
        return self._distro

    @distro.setter
    def distro(self, val):
        raise ValueError('parameter is read only')

    @property
    def requested_player(self):
        return self.opts['requested_player'][1]

    @requested_player.setter
    def requested_player(self, val):
        self.opts['requested_player'][1] = val.replace(' ', '')
        if self.opts['player'][1] != self.opts['requested_player'][1]:
            self.opts['player'][1] = self.requested_player
            self.opts['dirty_config'][1] = True

    @property
    def enable_mouse(self):
        return self.opts['enable_mouse'][1]

    @enable_mouse.setter
    def enable_mouse(self, val):
        self.opts['enable_mouse'][1] = val
        self.opts['dirty_config'][1] = True

    @property
    def enable_notifications(self):
        if is_graphical_environment_running():
            return self.opts['enable_notifications'][1]
        return '-1'

    @enable_notifications.setter
    def enable_notifications(self, val):
        self.opts['enable_notifications'][1] = val
        self.opts['dirty_config'][1] = True

    @property
    def use_station_icon(self):
        return self.opts['use_station_icon'][1]

    @use_station_icon.setter
    def use_station_icon(self, val):
        self.opts['use_station_icon'][1] = val

    @property
    def remove_station_icons(self):
        return self.opts['remove_station_icons'][1]

    @remove_station_icons.setter
    def remove_station_icons(self, val):
        self.opts['remove_station_icons'][1] = val
        self.opts['dirty_config'][1] = True

    @property
    def enable_tts(self):
        return self.opts['enable_tts'][1]

    @enable_tts.setter
    def enable_tts(self, val):
        self.opts['enable_tts'][1] = val
        self.opts['dirty_config'][1] = True

    @property
    def tts_volume(self):
        ''' connection timeout as string '''
        return self.opts['tts_volume'][1]

    @tts_volume.setter
    def tts_volume(self, val):
        self.opts['tts_volume'][1] = val
        self.opts['dirty_config'][1] = True

    @property
    def tts_rate(self):
        ''' connection timeout as string '''
        return self.opts['tts_rate'][1]

    @tts_rate.setter
    def tts_rate(self, val):
        self.opts['tts_rate'][1] = val
        self.opts['dirty_config'][1] = True

    @property
    def tts_pitch(self):
        ''' connection timeout as string '''
        return self.opts['tts_pitch'][1]

    @tts_pitch.setter
    def tts_pitch(self, val):
        self.opts['tts_pitch'][1] = val
        self.opts['dirty_config'][1] = True

    @property
    def tts_verbosity(self):
        ''' connection timeout as string '''
        return self.opts['tts_verbosity'][1]

    @tts_verbosity.setter
    def tts_verbosity(self, val):
        if val in ('default', 'punctuation'):
            set_val = val
        else:
            set_val = 'default'
        self.opts['tts_verbosity'][1] = set_val
        self.opts['dirty_config'][1] = True

    @property
    def tts_context(self):
        ''' connection timeout as string '''
        return self.opts['tts_context'][1]

    @tts_context.setter
    def tts_context(self, val):
        if val in ('limited', 'window', 'all'):
            set_val = val
        else:
            set_val = 'limited'
        self.opts['tts_context'][1] = set_val
        self.opts['dirty_config'][1] = True

    @property
    def tts_speak_volume_change(self):
        return self.opts['tts_speak_volume_change'][1]

    @tts_speak_volume_change.setter
    def tts_speak_volume_change(self, val):
        self.opts['tts_speak_volume_change'][1] = val

    @property
    def tts_speak_volume_change_start(self):
        ''' connection timeout as string '''
        return self.opts['tts_speak_volume_change_start'][1]

    @tts_speak_volume_change_start.setter
    def tts_speak_volume_change_start(self, val):
        self.opts['tts_speak_volume_change_start'][1] = val
        self.opts['dirty_config'][1] = True

    @property
    def player(self):
        return self.opts['player'][1]

    @player.setter
    def player(self, val):
        self.opts['player'][1] = val
        self.opts['dirty_config'][1] = True

    @property
    def force_http(self):
        return self.opts['force_http'][1]

    @force_http.setter
    def force_http(self, val):
        self.opts['force_http'][1] = val
        self.opts['dirty_config'][1] = True

    @property
    def use_transparency(self):
        return self.opts['use_transparency'][1]

    @use_transparency.setter
    def use_transparency(self, val):
        self.opts['use_transparency'][1] = val
        self.opts['dirty_config'][1] = True

    @property
    def force_transparency(self):
        return self.opts['force_transparency'][1]

    @force_transparency.setter
    def force_transparency(self, val):
        self.opts['force_transparency'][1] = val
        self.opts['dirty_config'][1] = True

    @property
    def calculated_color_factor(self):
        return float(self.opts['calculated_color_factor'][1])

    @calculated_color_factor.setter
    def calculated_color_factor(self, value):
        try:
            float(str(value))
            self.opts['calculated_color_factor'][1] = str(value)
        except (ValueError, TypeError, NameError):
            self.opts['calculated_color_factor'][1] = '0'
        self.opts['dirty_config'][1] = True

    @property
    def default_encoding(self):
        return self.opts['default_encoding'][1]

    @default_encoding.setter
    def default_encoding(self, val):
        self.opts['default_encoding'][1] = val
        self.opts['dirty_config'][1] = True

    @property
    def default_playlist(self):
        return self.opts['default_playlist'][1]

    @default_playlist.setter
    def default_playlist(self, val):
        self.opts['default_playlist'][1] = val
        self.opts['dirty_config'][1] = True

    @property
    def default_station(self):
        return self.opts['default_station'][1]

    @default_station.setter
    def default_station(self, val):
        self.opts['default_station'][1] = val
        self.opts['dirty_config'][1] = True

    @property
    def confirm_station_deletion(self):
        return self.opts['confirm_station_deletion'][1]

    @confirm_station_deletion.setter
    def confirm_station_deletion(self, val):
        self.opts['confirm_station_deletion'][1] = val
        self.opts['dirty_config'][1] = True

    @property
    def confirm_playlist_reload(self):
        return self.opts['confirm_playlist_reload'][1]

    @confirm_playlist_reload.setter
    def confirm_playlist_reload(self, val):
        self.opts['confirm_playlist_reload'][1] = val
        self.opts['dirty_config'][1] = True

    @property
    def auto_save_playlist(self):
        return self.opts['auto_save_playlist'][1]

    @auto_save_playlist.setter
    def auto_save_playlist(self, val):
        self.opts['auto_save_playlist'][1] = val
        self.opts['dirty_config'][1] = True

    @property
    def connection_timeout(self):
        ''' connection timeout as string '''
        return self.opts['connection_timeout'][1]

    @connection_timeout.setter
    def connection_timeout(self, val):
        self.opts['connection_timeout'][1] = val
        self.opts['dirty_config'][1] = True

    @property
    def connection_timeout_int(self):
        ''' connection timeout as integer
            if < 5 or > 60, set to 10
            On error set to 10
            Read only
        '''
        try:
            ret = int(self.opts['connection_timeout'][1])
            if ret != 0:
                if not 5 <= ret <= 60:
                    ret = 10
        except ValueError:
            ret = 10
        self.opts['connection_timeout'][1] = str(ret)
        return ret

    @connection_timeout_int.setter
    def connection_timeout_int(self, val):
        return

    @property
    def theme(self):
        return self.opts['theme'][1]

    @theme.setter
    def theme(self, val):
        if val.startswith('*'):
            self.opts['theme'][1] = val[1:]
            self.opts['auto_update_theme'][1] = True
        else:
            self.opts['theme'][1] = val
            self.opts['auto_update_theme'][1] = False
        self.opts['dirty_config'][1] = True

    @property
    def console_theme(self):
        return self.opts['console_theme'][1]

    @console_theme.setter
    def console_theme(self, val):
        if val in ('dark', 'light'):
            if val != self.opts['console_theme'][1]:
                self.opts['dirty_config'][1] = True
            self.opts['console_theme'][1] = val

    @property
    def theme_path(self):
        return path.join(self.stations_dir, 'themes', self.opts['theme'][1] + '.pyradio-theme')

    @property
    def auto_update_theme(self):
        return self.opts['auto_update_theme'][1]

    @auto_update_theme.setter
    def auto_update_theme(self, val):
        self.opts['auto_update_theme'][1] = val
        self.opts['dirty_config'][1] = True

    @property
    def dirty_config(self):
        return self.opts['dirty_config'][1]

    @dirty_config.setter
    def dirty_config(self, val):
        self.opts['dirty_config'][1] = val

    @property
    def session_lock_file(self):
        return self._session_lock_file

    @session_lock_file.setter
    def session_lock_file(self, val):
        return

    @property
    def system_themes(self):
        """Return a tuple of theme names shipped with the package."""
        try:
            theme_dir = files('pyradio').joinpath('themes')
            if isinstance(theme_dir, Traversable):
                # inside a wheel/zip → Traversable
                return tuple(sorted(
                    res.name.replace('.pyradio-theme', '')
                    for res in theme_dir.iterdir()
                    if res.name.endswith('.pyradio-theme')
                ))
            # normal filesystem path
            return tuple(sorted(
                path.basename(x).replace('.pyradio-theme', '')
                for x in glob.glob(path.join(theme_dir, '*.pyradio-theme'), recursive=False)
            ))
        except Exception:
            return tuple()

    def is_isolated_install(self):
        file_path = path.join(self.data_dir, "INSTALLATION_TYPE")

        # If the file does not exist, return False immediately
        if not path.exists(file_path):
            return False

        try:
            # Open and read the file, stripping any surrounding whitespace
            with open(file_path, 'r') as f:
                content = f.read().strip()
            # Return True only if the stripped content is exactly "2"
            return content == "2"
        except:
            # In case of any error (e.g., permission, read error), return False
            return False

    def is_project_theme(self, a_theme_name):
        ''' Check if a theme name is in auto_update_frameworks
            If it is, return
                classe's instance, the index in THEME
            Else, return
                None, -1
        '''
        for n in self.auto_update_frameworks:
            if a_theme_name in n.THEME:
                if logger.isEnabledFor(logging.DEBUG):
                    logger.debug('Checking theme: ' + a_theme_name)
                return n, n.THEME.index(a_theme_name)
        return None, -1

    @property
    def remote_control_server_ip(self):
        return self.opts['remote_control_server_ip'][1]

    @property
    def remote_control_server_port(self):
        return self.opts['remote_control_server_port'][1]

    @property
    def remote_control_server_auto_start(self):
        return self.opts['remote_control_server_auto_start'][1]

    @property
    def resource_opener(self):
        return self.opts['resource_opener'][1]

    @property
    def recording_dir(self):
        return self.opts['recording_dir'][1]

    @recording_dir.setter
    def recording_dir(self, val):
        self.opts['recording_dir'][1] = val
        # self.xdg.recording_dir = val
        self.dirty_config = True

    @property
    def last_station_checked(self):
        return self._last_station_checked

    @last_station_checked.setter
    def last_station_checked(self, value):
        self._last_station_checked = value

    @property
    def last_station_checked_id(self):
        return self._last_station_checked_id

    @last_station_checked_id.setter
    def last_station_checked_id(self, value):
        self._last_station_checked_id = value

    @property
    def check_output_folder(self):
        return self._check_output_folder

    @check_output_folder.setter
    def check_output_folder(self, value):
        self._check_output_folder = value

    @property
    def check_output_file(self):
        return self._check_output_file

    @check_output_file.setter
    def check_output_file(self, value):
        self._check_output_file = value

    def is_default_file(self, a_theme_name):
        for n in self.auto_update_frameworks:
            if a_theme_name == n.default_filename_only:
                return True
        return False

    def _construct_playlist_path(self, a_playlist):
        return path.join(self.stations_dir, a_playlist + '.csv')

    def _read_notification_command(self):
        self._notification_command = []
        if sys.platform == 'win32':
            return

        # first look for the user notification file
        user_file = path.join(self.stations_dir, 'notification')
        if path.exists(user_file):
            try:
                with open(user_file, 'r', encoding='utf-8') as f:
                    self._notification_command = [
                        line.strip() for line in f if line.strip()
                    ]
                return
            except Exception:
                self._notification_command = []

        if not self._notification_command:
            # then look for package resource (filesystem ή zip)
            try:
                notif_res = files('pyradio').joinpath('notification')
                if isinstance(notif_res, Traversable) and notif_res.is_file():
                    # zip/wheel → Traversable
                    with notif_res.open('r', encoding='utf-8') as f:
                        self._notification_command = [
                            line.strip() for line in f if line.strip()
                        ]
                else:
                    # fallback: real path
                    real_path = str(notif_res)
                    if path.exists(real_path):
                        with open(real_path, 'r', encoding='utf-8') as f:
                            self._notification_command = [
                                line.strip() for line in f if line.strip()
                            ]
            except Exception:
                self._notification_command = []

        if not self._notification_command:
            # set default commands
            if sys.platform.lower().startswith('darwin'):
                notif = which('terminal-notifier')
                if notif:
                    self._notification_command = [
                        notif, '-title', 'TITLE',
                        '-message', 'MSG',
                        '-sender', 'org.pyradio.pyradio',
                        '-contentImage', 'ICON'
                    ]
                else:
                    self._notification_command = [
                        'osascript', '-e',
                        'display notification "MSG" with title "TITLE"'
                    ]
            else:
                self._notification_command = [
                    'notify-send', '-i',
                    'ICON', 'TITLE', 'MSG'
                ]

    def get_pyradio_version(self):
        ''' reads pyradio version from installed files

            Retrurns
                self.info
                    The string to display at left top corner of main window
                self.get_current_pyradio_version
                    The version to use when checking for updates
        '''
        ret = None
        self.info = f" PyRadio {version} "
        self.user_agent_string = r'PyRadio/{}'.format(version)
        ''' git_description can be set at build time
            if so, revision will be shown along with the version
        '''
        # if revision is not 0
        git_description = ''
        if git_description:
            git_info = git_description.split('-')
            if logger.isEnabledFor(logging.DEBUG):
                logger.debug(f'versrion = {version} - git_info = {git_info}')
            if git_info[0] == version:
                if git_description.endswith('-git') or \
                        git_description.endswith('-sng') or \
                        git_description.endswith('-dev'):
                    if 'rdev' in git_description:
                        ''' failed to get it on linux '''
                        git_description = 'PyRadio-git'
                    self.info = ' ' + git_description
                    ret = self.info + " (development version)"
                else:
                    try:
                        if git_info[1] == '0':
                            self.info = f" PyRadio {git_info[0]}"
                            ret = 'PyRadio built from git master branch'
                        else:
                            self.info = f" PyRadio {git_info[0]}-r{git_info[1]}"
                            ret = f"PyRadio built from git: https://github.com/coderholic/pyradio/commit/{git_info[-1]} (rev. {git_info[1]})"
                    except:
                        pass
            else:
                self.info = f" PyRadio {version}"
                ret = ''
        self.current_pyradio_version = self.info.replace(' PyRadio ', '').replace(' ', '')
        # if self._distro != 'None':
        #     self.info += '({})'.format(self._distro)
        return ret

    def remove_remote_control_server_report_file(self):
        if path.exists(self.remote_control_server_report_file):
            try:
                remove(self.remote_control_server_report_file)
            except:
                pass

    def setup_mouse(self):
        curses.mousemask(curses.ALL_MOUSE_EVENTS
                         | curses.REPORT_MOUSE_POSITION)
        #curses.mouseinterval(0)

    def open_a_dir(self, a_dir):
        str_dir = fspath(a_dir)
        if system().lower() == 'windows':
            startfile(str_dir)
        elif system().lower() == 'darwin':
            Popen([which('open'), str_dir])
        else:
            xdg_open_path = self._linux_resource_opener if self._linux_resource_opener else get_a_linux_resource_opener()
            if isinstance(xdg_open_path, str):
                xdg_open_path = xdg_open_path.split(' ')
            if xdg_open_path:
                try:
                    Popen(
                        [*xdg_open_path, str_dir],
                        stderr=DEVNULL,
                        stdout=DEVNULL
                    )
                except (FileNotFoundError, PermissionError):
                    pass

    def open_config_dir(self, recording=0):
        a_dir = fspath(self.stations_dir if recording == 0 else self.recording_dir)
        if system().lower() == 'windows':
            startfile(a_dir)
        elif system().lower() == 'darwin':
            Popen([which('open'), a_dir])
        else:
            xdg_open_path = self._linux_resource_opener if self._linux_resource_opener else get_a_linux_resource_opener()
            if xdg_open_path:
                try:
                    Popen(
                        [*xdg_open_path, a_dir],
                        stderr=DEVNULL,
                        stdout=DEVNULL
                    )
                except (FileNotFoundError, PermissionError):
                    pass

    def _get_lock_file(self):
        ''' Populate self._session_lock_file
            If it exists, locked becomes True
            Otherwise, the file is created
        '''
        self._i_created_the_lock_file = False
        self.locked = False
        if sys.platform == 'win32':
            self._session_lock_file = path.join(self.state_dir, 'pyradio.lock')
            win_lock = path.join(self.state_dir, 'data', '_windows.lock')
            if path.exists(win_lock):
                ''' pyradio lock file was probably not deleted the last
                    time Windows terminated. It should be safe to use it
                '''
                try:
                    remove(win_lock)
                except:
                    pass
        else:
            xdg_runtime_dir = getenv('XDG_RUNTIME_DIR')
            if xdg_runtime_dir:
                self._session_lock_file = path.join(xdg_runtime_dir, 'pyradio.lock')
            elif path.exists('/run/user'):
                from os import geteuid
                self._session_lock_file = path.join('/run/user', str(geteuid()), 'pyradio.lock')
                if not path.exists(self._session_lock_file):
                    self._session_lock_file = None
            if self._session_lock_file is None:
                self._session_lock_file = path.join(self.state_dir, '.pyradio.lock')
        try:
            makedirs(self.state_dir, exist_ok=True)
        except:
            print(f'[red]Error:[/red] Cannot create dir: "{self.state_dir}"')
            sys.exit(1)

        ''' remove old style session lock file (if it exists) '''
        if path.exists(path.join(self.stations_dir, '.lock')):
            try:
                remove(path.join(self.stations_dir, '.lock'))
            except:
                pass
        if path.exists(self._session_lock_file):
            self.locked = True
        else:
            if not self.headless:
                try:
                    with open(self._session_lock_file, 'w', encoding='utf-8'):
                        pass
                    self._i_created_the_lock_file = True
                except:
                    pass

    def remove_session_lock_file(self):
        ''' remove session lock file, if session is not locked '''
        if self.locked:
            if logger.isEnabledFor(logging.INFO):
                logger.info('Not removing lock file; session is locked...')
        else:
            if path.exists(self._session_lock_file):
                try:
                    remove(self._session_lock_file)
                    if logger.isEnabledFor(logging.INFO):
                        logger.info('Lock file removed...')
                    return 0, self._session_lock_file
                except:
                    if logger.isEnabledFor(logging.INFO):
                        logger.info('Failed to remove Lock file...')
                    return 1, self._session_lock_file
            else:
                if logger.isEnabledFor(logging.INFO):
                    logger.info('Lock file not found...')
                return -1, self._session_lock_file

    def change_to_no_theme_mode(self, show_colors_cannot_change):
        if self.console_theme == 'light':
            curses.init_pair(1, curses.COLOR_MAGENTA, curses.COLOR_WHITE )
            curses.init_pair(2, curses.COLOR_BLUE, curses.COLOR_WHITE)
            curses.init_pair(3, curses.COLOR_RED, curses.COLOR_WHITE)
            curses.init_pair(4, curses.COLOR_RED, curses.COLOR_WHITE)
            curses.init_pair(5, curses.COLOR_BLACK, curses.COLOR_WHITE)
            curses.init_pair(6, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
            curses.init_pair(7, curses.COLOR_WHITE, curses.COLOR_BLUE)
            curses.init_pair(8, curses.COLOR_BLACK, curses.COLOR_WHITE)
            curses.init_pair(9, curses.COLOR_WHITE, curses.COLOR_BLUE)
            curses.init_pair(10, curses.COLOR_BLACK, curses.COLOR_WHITE)
            curses.init_pair(11, curses.COLOR_RED, curses.COLOR_WHITE)
            curses.init_pair(12, curses.COLOR_BLACK, curses.COLOR_WHITE)
            curses.init_pair(13, curses.COLOR_BLACK, curses.COLOR_WHITE)
            curses.init_pair(14, curses.COLOR_BLACK, curses.COLOR_WHITE)
            curses.init_pair(15, curses.COLOR_BLACK, curses.COLOR_WHITE)
        else:
            curses.init_pair(1, curses.COLOR_CYAN, curses.COLOR_BLACK)
            curses.init_pair(2, curses.COLOR_BLUE, curses.COLOR_BLACK)
            curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK)
            curses.init_pair(4, curses.COLOR_GREEN, curses.COLOR_BLACK)
            curses.init_pair(5, curses.COLOR_WHITE, curses.COLOR_BLACK)
            curses.init_pair(6, curses.COLOR_BLACK, curses.COLOR_MAGENTA)
            curses.init_pair(7, curses.COLOR_BLACK, curses.COLOR_GREEN)
            curses.init_pair(8, curses.COLOR_WHITE, curses.COLOR_BLACK)
            curses.init_pair(9, curses.COLOR_BLACK, curses.COLOR_GREEN)
            curses.init_pair(10, curses.COLOR_WHITE, curses.COLOR_BLACK)
            curses.init_pair(11, curses.COLOR_GREEN, curses.COLOR_BLACK)
            curses.init_pair(12, curses.COLOR_WHITE, curses.COLOR_BLACK)
            curses.init_pair(13, curses.COLOR_WHITE, curses.COLOR_BLACK)
            curses.init_pair(14, curses.COLOR_WHITE, curses.COLOR_BLACK)
            curses.init_pair(15, curses.COLOR_WHITE, curses.COLOR_BLACK)
        ''' Theme values backup '''
        self.bck_opts['use_transparency'] = self.opts['use_transparency'][1]
        self.bck_opts['force_transparency'] = self.opts['force_transparency'][1]
        self.bck_opts['theme'] = self.opts['theme'][1]
        self.bck_opts['auto_update_theme'] = self.opts['auto_update_theme'][1]
        self.bck_opts['calculated_color_factor'] = self.opts['calculated_color_factor'][1]
        ''' No theme values '''
        self.opts['use_transparency'][1] = False
        self.opts['force_transparency'][1] = False
        self.opts['theme'][1] = self.console_theme
        self.opts['auto_update_theme'][1] = False
        self.opts['calculated_color_factor'][1] = "0"
        self._show_colors_cannot_change = show_colors_cannot_change
        # logger.error('bck_opts = {}'.format(self.bck_opts))

    def _check_config_file(self, usr):
        ''' Make sure a config file exists in the config dir '''
        user_config_file = path.join(usr, 'config')

        ''' restore config from bck file '''
        if path.exists(user_config_file + '.restore'):
            try:
                copyfile(user_config_file + '.restore', user_config_file)
                remove(user_config_file + '.restore')
            except:
                pass

        ''' update pi config file '''
        if not path.exists(user_config_file) and \
                is_rasberrypi():
            self._convert_config_for_rasberrypi(user_config_file)

    def _convert_config_for_rasberrypi(self, user_config_file):
        lines = []

        # read package config (filesystem or wheel/zip)
        pkg_res = files('pyradio').joinpath('config')
        try:
            if isinstance(pkg_res, Traversable) and pkg_res.is_file():
                with pkg_res.open('r', encoding='utf-8') as f:
                    lines = [line.strip() for line in f]
            else:
                real_path = str(pkg_res)
                if path.exists(real_path):
                    with open(real_path, 'r', encoding='utf-8') as f:
                        lines = [line.strip() for line in f]
        except Exception:
            lines = []

        # modify player line
        for i, _ in enumerate(lines):
            if lines[i].startswith('player'):
                lines[i] = 'player = mplayer,vlc,mpv'

        # write to user config
        with open(user_config_file, 'w', encoding='utf-8') as f:
            f.write('\n'.join(lines) + '\n')

    def _validate_remote_control_server_ip(self, val):
        '''
        validate a config remote_control_server string
        Return
            input    if valid
            default  if invalid
        '''
        hosts = ('localhost', 'LAN', 'lan')
        default_remote_control_server = 'localhost:9998'
        if ':' in val:
            sp = val.split(':')
            ''' is server valid '''
            if sp[0].startswith('*'):
                sp[0] = sp[0][1:]
            x = [r for r in hosts if r == sp[0]]
            if not x:
                return default_remote_control_server
            ''' server is valid, is port valid? '''
            try:
                x = int(sp[1])
            except (ValueError, IndexError):
                return default_remote_control_server
        else:
            return default_remote_control_server
        return val

    def migrate_xdg(self):
        mXdg = XdgMigrate(config=self)
        mXdg.rename_files()
        self._copy_icon()
        if self.xdg_compliant:
            # I need to have ~/.config/pyradio/data hardcoded here
            dpath = path.join(path.expanduser('~'), '.config', 'pyradio', 'data')
            if path.exists(dpath):
                flist = listdir(dpath)
                if len(flist) == 0:
                    try:
                        remove_tree(dpath)
                    except:
                        pass

    def _read_localized_shortcuts(self, name=None):
        """
        Read the localized keyboard shortcuts.

        Parameters
        ----------
        name : str, optional
            The name of the localization (e.g., 'greek'). Defaults to self.localize.

        Behavior
        --------
        1. Tries to read from ~/.config/pyradio/{name}.json
        2. If not found, tries to read from package resources (may be zip/wheel)
        3. If from package, copies it to cache to make a real file
        4. Reverses the dictionary (value -> key) and sets it via set_lkbkey()
        """
        if name is None:
            name = self.localize
        if not name or name == 'english':
            set_lkbkey({})
            return

        # Paths
        user_path = Path(self.data_dir) / f"{name}.json"
        script_res = files('pyradio').joinpath('keyboard', f"{name}.json")

        target_path = None

        # 1. User path exists?
        if user_path.exists():
            target_path = str(user_path)
        else:
            # 2. Package resource
            try:
                if isinstance(script_res, Traversable) and script_res.is_file():
                    # Copy to cache to have a real file
                    cache_dir = Path(self._cnf.cache_dir) / "keyboard"
                    cache_dir.mkdir(parents=True, exist_ok=True)
                    real_path = cache_dir / f"{name}.json"
                    with as_file(script_res) as tmp_path:
                        copyfile(tmp_path, real_path)
                    target_path = str(real_path)
                else:
                    # fallback: try as string path
                    real_path = str(script_res)
                    if Path(real_path).exists():
                        target_path = real_path
            except Exception:
                target_path = None

        # 3. Read JSON
        reversed_dict = {}
        if target_path:
            try:
                with open(target_path, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                reversed_dict = {v: k for k, v in data.items()}
            except Exception:
                reversed_dict = {}

        # 4. Apply
        set_lkbkey(reversed_dict)

    def read_config(self, distro_config=False):
        self._read_config(distro_config=True)
        self.config_opts = deepcopy(self.opts)
        # for n in self.config_opts.items():
        #     logger.error('  {}: {}'.format(*n))
        self.re_read_config()

    def re_read_config(self):
        self._read_config()
        self.xdg.ensure_paths_exist()

    def _read_config(self, distro_config=False):
        xdg_compliant_read_from_file = False
        lines = []

        try:
            if distro_config:
                # config file in package (system / wheel)
                cfg_res = files('pyradio').joinpath('config')
                with as_file(cfg_res) as cfg_path:
                    with open(cfg_path, 'r', encoding='utf-8') as cfgfile:
                        lines = [line.strip() for line in cfgfile
                                 if line.strip() and not line.startswith('#')]
            else:
                file_to_read = self.config_file
                if not path.exists(file_to_read):
                    # user config does not exist
                    self._make_sure_dirs_exist()
                    self._first_read = False
                    return
                with open(file_to_read, 'r', encoding='utf-8') as cfgfile:
                    lines = [line.strip() for line in cfgfile
                             if line.strip() and not line.startswith('#')]
        except Exception:
            self.__dirty_config = False
            return -1

        self.params = {
            'mpv': [1, 'profile:pyradio'],
            'mplayer': [1, 'profile:pyradio'],
            'vlc': [1, 'Do not use any extra player parameters']
        }
        for line in lines:
            sp = line.split('=')
            if len(sp) < 2:
                return -2
            if sp[1] == '':
                return -2
            for i, _ in enumerate(sp):
                sp[i] = sp[i].strip()
            if sp[0] == 'mplayer_save_br':
                self.mplayer_save_br = True
                st = sp[1].strip()
                if st.lower() == 'false':
                    self.mplayer_save_br = False
            elif sp[0] == 'show_no_themes_message':
                self.show_no_themes_message = True
                st = sp[1].strip()
                if st.lower() == 'false':
                    self.show_no_themes_message = False
            elif sp[0] == 'show_recording_message':
                self.show_recording_start_message = True
                st = sp[1].strip()
                if st.lower() == 'false':
                    self.show_recording_start_message = False
            elif sp[0] == 'player':
                self.opts['player'][1] = sp[1].lower().replace(' ', '')
                # if sys.platform.startswith('win'):
                #     self.opts['player'][1] = self.opts['player'][1].replace('mpv,', '')
            elif sp[0] == 'connection_timeout':
                self.opts['connection_timeout'][1] = sp[1].strip()
                ''' check integer number and set to 10 if error
                    x is a dummy parameter
                '''
                x = self.connection_timeout_int
            elif sp[0] == 'default_encoding':
                self.opts['default_encoding'][1] = sp[1].strip()
            elif sp[0] == 'theme':
                self.opts['theme'][1] = sp[1].strip()
                if self.opts['theme'][1].startswith('*'):
                    self.opts['theme'][1] = self.opts['theme'][1][1:]
                    self.opts['auto_update_theme'][1] = True
                else:
                    self.opts['auto_update_theme'][1] = False
            elif sp[0] == 'console_theme':
                tmp = sp[1].strip()
                if tmp not in ('dark', 'light'):
                    self.opts['console_theme'][1] = 'dark'
                else:
                    self.opts['console_theme'][1] = tmp
            elif sp[0] == 'default_playlist':
                self.opts['default_playlist'][1] = sp[1].strip()
            elif sp[0] == 'update_window_title':
                if sp[1].lower() == 'false':
                    self.opts['update_window_title'][1] = False
                else:
                    self.opts['update_window_title'][1] = True
            elif sp[0] == 'default_station':
                st = sp[1].strip()
                if st == '-1' or st.lower() == 'false':
                    self.opts['default_station'][1] = 'False'
                elif st in ("0", 'random'):
                    self.opts['default_station'][1] = None
                else:
                    self.opts['default_station'][1] = st
            elif sp[0] == 'use_os_media_controls':
                if sp[1].lower() == 'true':
                    self.opts['use_os_media_controls'][1] = True
                else:
                    self.opts['use_os_media_controls'][1] = False
            elif sp[0] == 'continuous_playback':
                if sp[1].lower() == 'false':
                    self.opts['continuous_playback'][1] = False
                else:
                    self.opts['continuous_playback'][1] = True
            elif sp[0] == 'open_last_playlist':
                if sp[1].lower() == 'false':
                    self.opts['open_last_playlist'][1] = False
                else:
                    self.opts['open_last_playlist'][1] = True
            elif sp[0] == 'enable_mouse':
                if sp[1].lower() == 'false':
                    self.opts['enable_mouse'][1] = False
                else:
                    self.opts['enable_mouse'][1] = True
            elif sp[0] == 'wheel_adjusts_volume':
                if sp[1].lower() == 'false':
                    self.opts['wheel_adjusts_volume'][1] = False
                else:
                    self.opts['wheel_adjusts_volume'][1] = True
            elif sp[0] == 'enable_notifications':
                self.opts['enable_notifications'][1] = sp[1]
                if sp[1] not in ('0', '-1'):
                    try:
                        t = int(int(sp[1]) / 30)
                        self.opts['enable_notifications'][1] = str(t * 30)
                    except (ValueError, TypeError):
                        self.opts['enable_notifications'][1] = '-1'
            elif sp[0] == 'use_station_icon':
                if sp[1].lower() == 'false':
                    self.opts['use_station_icon'][1] = False
                else:
                    self.opts['use_station_icon'][1] = True
            elif sp[0] == 'remove_station_icons':
                if sp[1].lower() == 'false':
                    self.opts['remove_station_icons'][1] = False
                else:
                    self.opts['remove_station_icons'][1] = True
            elif sp[0] == 'enable_tts':
                if sp[1].lower() == 'false':
                    self.opts['enable_tts'][1] = False
                else:
                    self.opts['enable_tts'][1] = True
            elif sp[0] == 'tts_volume':
                try:
                    if 0 <= int(sp[1])  <= 100:
                        self.opts['tts_volume'][1] = sp[1]
                    else:
                        self.opts['tts_volume'][1] = '50'
                except ValueError:
                    self.opts['tts_volume'][1] = '50'
            elif sp[0] == 'tts_rate':
                self.opts['tts_rate'][1] = sp[1]
            elif sp[0] == 'tts_pitch':
                self.opts['tts_pitch'][1] = sp[1]
            elif sp[0] == 'tts_verbosity':
                self.opts['tts_verbosity'][1] = 'punctuation' if sp[1].lower() == 'punctuation' else 'default'
            elif sp[0] == 'tts_context':
                cont = sp[1].lower()
                self.opts['tts_context'][1] = 'all'
                if cont == 'window':
                    self.opts['tts_context'][1] = 'window'
                elif cont == 'limited':
                    self.opts['tts_context'][1] = 'limited'
            elif sp[0] == 'tts_speak_volume_change':
                try:
                    speak_vol = int(sp[1])
                except ValueError:
                    speak_vol = 0
                if not 300 <= speak_vol <= 3000:
                    speak_vol = 0
                self.opts['tts_speak_volume_change'][1] = str(speak_vol)
            elif sp[0] == 'tts_speak_volume_change_start':
                if sp[1].lower() == 'true':
                    self.opts['tts_speak_volume_change_start'][1] = True
                else:
                    self.opts['tts_speak_volume_change_start'][1] = False
            elif sp[0] == 'confirm_station_deletion':
                if sp[1].lower() == 'false':
                    self.opts['confirm_station_deletion'][1] = False
                else:
                    self.opts['confirm_station_deletion'][1] = True
            elif sp[0] == 'confirm_playlist_reload':
                if sp[1].lower() == 'false':
                    self.opts['confirm_playlist_reload'][1] = False
                else:
                    self.opts['confirm_playlist_reload'][1] = True
            elif sp[0] == 'auto_save_playlist':
                if sp[1].lower() == 'true':
                    self.opts['auto_save_playlist'][1] = True
                else:
                    self.opts['auto_save_playlist'][1] = False
            elif sp[0] == 'use_transparency':
                if sp[1].lower() == 'true':
                    self.opts['use_transparency'][1] = True
                else:
                    self.opts['use_transparency'][1] = False
            elif sp[0] == 'force_transparency':
                if sp[1].lower() == 'true':
                    self.opts['force_transparency'][1] = True
                else:
                    self.opts['force_transparency'][1] = False
            elif sp[0] == 'log_titles':
                if sp[1].lower() == 'true':
                    self.opts['log_titles'][1] = True
                else:
                    self.opts['log_titles'][1] = False
            elif sp[0] == 'calculated_color_factor':
                try:
                    t = round(float(sp[1]), 2)
                    s_t = str(t)[:4]
                    if s_t == '0.0':
                        s_t = '0'
                    self.opts['calculated_color_factor'][1] = s_t
                except (ValueError, TypeError):
                    self.opts['calculated_color_factor'][1] = '0'
                self.use_calculated_colors = not self.opts['calculated_color_factor'][1] == '0'
            elif sp[0] == 'force_http':
                if sp[1].lower() == 'true':
                    self.opts['force_http'][1] = True
                else:
                    self.opts['force_http'][1] = False
            elif sp[0] in ('mpv_parameter',
                           'mplayer_parameter',
                           'vlc_parameter'):
                self._config_to_params(sp)
            elif sp[0] == 'remote_control_server_ip':
                # hosts = ('localhost', 'LAN', 'lan')
                sp[1] = sp[1].strip()
                nip = IPsWithNumbers(
                        default_ip=sp[1]
                )
                self.opts['remote_control_server_ip'][1] = nip.current()
                nip = None
                # x = [r for r in hosts if r == sp[1]]
                # if x:
                #     self.opts['remote_control_server_ip'][1] = x[0]
                # else:
                #     self.opts['remote_control_server_ip'][1] = 'localhost'
            elif sp[0] == 'remote_control_server_port':
                try:
                    x = int(sp[1])
                except (ValueError, TypeError):
                    x = 9998
                if 1025 <= x <= 65535:
                    self.opts['remote_control_server_port'][1] = str(x)
                else:
                    self.opts['remote_control_server_port'][1] = '9998'
            elif sp[0] == 'remote_control_server_auto_start':
                if sp[1].lower() == 'true':
                    self.opts['remote_control_server_auto_start'][1] = True
                else:
                    self.opts['remote_control_server_auto_start'][1] = False
            elif sp[0] == 'recording_dir':
                self.opts['recording_dir'][1] = sp[1].strip()
                if self.opts['recording_dir'][1] == 'default':
                    self.opts['recording_dir'][1] = ''
                if self.opts['recording_dir'][1].startswith('~'):
                    self.opts['recording_dir'][1] = path.expanduser('~') + self.opts['recording_dir'][1][1:]
                elif self.opts['recording_dir'][1].startswith('%homepath%') or \
                        self.opts['recording_dir'][1].startswith('%HOMEPATH%'):
                    self.opts['recording_dir'][1] = path.expanduser('~') + self.opts['recording_dir'][1][len('%homepath%'):]
            elif sp[0] == 'distro' and \
                    distro_config:
                ''' mark as dirty to force saving config to remove setting '''
                # self.dirty_config = True
                self._distro = sp[1].strip()
            elif sp[0] == 'xdg_compliant' and \
                    not sys.platform.startswith('win') and \
                    self._user_config_dir is None and \
                    sp[1].lower() == 'true':
                self.xdg_compliant = True
                xdg_compliant_read_from_file = True
            elif sp[0] == 'resource_opener' and \
                    not (sys.platform.startswith('win') or \
                        sys.platform.startswith('dar')):
                if sp[1] != 'auto':
                    tmp = sp[1].split(' ')
                    prog = validate_resource_opener_path(tmp[0])
                    if prog is not None:
                        tmp[0] = prog
                        self._linux_resource_opener = ' '.join(tmp)
                        self.opts['resource_opener'][1] = sp[1]
            elif sp[0] == 'enable_clock':
                if sp[1].lower() == 'false':
                    self.opts['enable_clock'][1] = False
                else:
                    self.opts['enable_clock'][1] = True
            elif sp[0] == 'time_format':
                tmp = sp[1].split(' ')[0]
                try:
                    x = int(tmp)
                    if not 0 <= x < len(TIME_FORMATS):
                        tmp = '0'
                except (ValueError, TypeError):
                    tmp = '0'
                self.opts['time_format'][1] = tmp
            elif sp[0] == 'buffering':
                try:
                    b = int(sp[1].strip())
                except ValueError:
                    b = 0
                if not (b == 0 or 5 <= b <= 60):
                    b = 0
                self.opts['buffering'][1] = str(b)
            elif sp[0] == 'auto_update_stations':
                if sp[1].lower() == 'false':
                    self.opts['auto_update_stations'][1] = False
                else:
                    self.opts['auto_update_stations'][1] = True

            elif sp[0] == 'localized_keys':
                # logger.error(f'{sp[1] = }')
                self.localize = None if sp[1].strip().lower() == 'none' else sp[1].strip().lower()
                # logger.error(f'{self.localize = }')
                self._old_localize = self.localize

        # logger.error('\n\nself.params{}\n\n'.format(self.params))
        ''' read distro from package config file '''
        try:
            # The file may be real, Traversable even in z zip/wheel
            config_res = files('pyradio').joinpath('config')
            with as_file(config_res) as config_path:
                with open(config_path, 'r', encoding='utf-8') as pkg_config:
                    lines = [line.strip() for line in pkg_config
                             if line.strip() and not line.startswith('#')]
            for line in lines:
                sp = line.split('=')
                if len(sp) == 2 and sp[0].strip() == 'distro':
                    self._distro = sp[1].strip() or 'None'
                    break
        except Exception:
            self._distro = 'None'

        self.opts['dirty_config'][1] = False
        self.saved_params = deepcopy(self.params)

        if self.headless:
            self.opts['remote_control_server_ip'][1], self.opts['remote_control_server_port'][1] = to_ip_port(self._headless)
            self.opts['remote_control_server_auto_start'][1] = True
            self.opts['theme'][1] = 'dark'
            self.opts['auto_update_theme'][1] = False
            self.opts['use_transparency'][1] = False
            self.opts['force_transparency'][1] = False
            self.opts['enable_mouse'][1] = False
            self.opts['calculated_color_factor'][1] = '0'
            self.opts['enable_clock'][1] = False
            self.opts['enable_tts'][1] = False

        ''' check if default playlist exists '''
        if self.opts['default_playlist'][1] != 'stations':
            ch = path.join(self.stations_dir, self.opts['default_playlist'][1] + '.csv')
            if not path.exists(ch):
                if logger.isEnabledFor(logging.INFO):
                    logger.info(f"Default playlist \"({self.opts['default_station'][1]}\") does not exist; reverting to \"stations\"")
                self.opts['default_playlist'][1] = 'stations'
                self.opts['default_station'][1] = 'False'
        # for n in self.opts:
        #     print('{0}: {1}'.format(n, self.opts[n]))

        if not distro_config and self._fixed_recording_dir is not None:
            self.opts['recording_dir'][1] = self._fixed_recording_dir
            self._fixed_recording_dir = None
            self.opts['dirty_config'][1] = True

        self._make_sure_dirs_exist()
        ''' detect previous XDG Base installation '''
        if not sys.platform.startswith('win')  and \
                self._user_config_dir is None and \
                not self.xdg_compliant and \
                distro_config:
            # d_dir = path.join(XdgDirs.get_xdg_dir('XDG_DATA_HOME'), 'pyradio')
            # s_dir = path.join(XdgDirs.get_xdg_dir('XDG_STATE_HOME'), 'pyradio')
            d_dir = XdgDirs.get_xdg_dir('XDG_DATA_HOME')
            s_dir = XdgDirs.get_xdg_dir('XDG_STATE_HOME')
            if path.exists(d_dir) and path.exists(s_dir):
                # print('[magenta]XDG Dirs[/magenta] found; enabling [magenta]XDG Base compliant[/magenta] operation')
                self.xdg_compliant = True
                self.need_to_fix_desktop_file_icon = True

        # do this here to get proper extra parameters config filepath if XDG is on
        self.player_params_file = path.join(self.data_dir, 'player-params.json')
        if not distro_config:
            # read localized shortcuts
            self._read_localized_shortcuts(name=None)

            if path.exists(self.player_params_file + '.restore'):
                try:
                    copyfile(self.player_params_file + '.restore', self.player_params_file)
                except:
                    pass
            if path.exists(self.player_params_file):
                try:
                    with open(self.player_params_file, 'r', encoding='utf-8') as jf:
                        self.params = json.load(jf)
                except:
                    pass
            self._first_read = False
            # logger.error('\n\nfile = {0}\nplayer extra params = {1}\n\n'.format(self.player_params_file, self.params))

            # do this here to get proper schedule and keyboard config filepath if XDG is on
            self.schedule_file = path.join(self.data_dir, 'schedule.json')
            self.keyboard_file = path.join(self.data_dir, 'keyboard.json')
            # logger.error(f'{self.keyboard_file = }')
        if not self.headless and not distro_config:
            read_keyboard_shortcuts(self.keyboard_file)
            read_localized_keyboard(
                self.localize,
                self.data_dir
            )
        self.active_enable_clock = self.enable_clock

    def _make_sure_dirs_exist(self):
        home_rec_dir = path.join(path.expanduser('~'), 'pyradio-recordings')
        if self.opts['recording_dir'][1] == '':
            self.opts['recording_dir'][1] = path.join(path.expanduser('~'), 'pyradio-recordings')
        ch_dir = CheckDir(
            self.opts['recording_dir'][1],
            home_rec_dir,
            remove_after_validation=True
        )
        if not ch_dir.can_be_created:
            print(f"Error: Recordings directory is for a folder: \"{self.opts['recording_dir'][1]}\"")
            sys.exit(1)
        elif not ch_dir.can_be_writable:
            print(f"Error: Recordings directory is not writable: \"{self.opts['recording_dir'][1]}\"")
            sys.exit(1)

        if not path.exists(self.opts['recording_dir'][1]):
            try:
                makedirs(self.opts['recording_dir'][1])
            except:
                print(f"Error: Cannot create recordings directory: \"{self.opts['recording_dir'][1]}\"")
                sys.exit(1)
        # logger.error('self.opts["recording_dir"][1] = "{}"'.format(self.opts['recording_dir'][1]))
        if path.exists(path.join(self.stations_dir, 'data', 'recordings')) and \
                    self._first_read:
            print('++ Need to migrate')
            ''' On startup only move recordings dir
                from  [[STATIONS DIR]]data/recordings
                to    ~/pyradio_recordings
                        (also on Windows)
            '''
            self.xdg.set_recording_dir(
                    new_dir=self.opts['recording_dir'][1],
                    print_to_console=True,
                    migrate=True,
                    first_read=path.join(self.stations_dir, 'data', 'recordings')
            )

        # remove recordings dir from home dir if it is empty, as per #253
        if path.exists(self.opts['recording_dir'][1]) and \
                self.opts['recording_dir'][1] ==  home_rec_dir and  \
                len(listdir(self.opts['recording_dir'][1])) == 0:
            # fix for #255
            try:
                remove_tree(self.opts['recording_dir'][1])
            except:
                pass

    def get_last_playlist(self):
        ''' read last opened playlist
                reads:     ~/pyradio/last_playlist
                returns:   last playlist name
                sets:      self._last_opened_playlist_name (if successful)

            CAUTION:
                To be used by main.py only
        '''
        playlist = ''
        lps = (
            path.join(self.state_dir, 'last-playlist'),
            path.join(self.stations_dir, 'last_playlist'),
            path.join(self.stations_dir, 'last-playlist')
        )
        for lp in lps:
            # print('lp = "{}"'.format(lp))
            if path.exists(lp):
                with open(lp, 'r', encoding='utf-8') as f:
                    for row in csv.reader(filter(lambda row: row[0]!='#', f), skipinitialspace=True):
                        if not row:
                            continue
                        self.last_playlist_to_open = row

                if len(self.last_playlist_to_open) == 0:
                    self.last_playlist_to_open = []
                    return None

                if len(self.last_playlist_to_open) > 3:
                    self.last_playlist_to_open = self.last_playlist_to_open[:3]

                while len(self.last_playlist_to_open) < 3:
                    self.last_playlist_to_open.append(0)

                for i in range(-1, -3, -1):
                    try:
                        x = int(self.last_playlist_to_open[i])
                    except ValueError:
                        x = -1 if i == -1 else 0
                    self.last_playlist_to_open[i] = x


                playlist = self.last_playlist_to_open[0]
                if playlist != '':
                    if path.exists(path.join(self.stations_dir, playlist + '.csv')):
                        print('=> Opening last playlist: "' + playlist + '"')
                        self._last_opened_playlist_name = playlist
                        return playlist
                    print('=> Last playlist does not exist: "' + playlist + '"')
                else:
                    print('=> Last playlist name is invalid!')
                try:
                    remove(lp)
                except:
                    pass
                break
        return None

    def init_backup_player_params(self):
        # logger.error('DE ====  init_backup_player_params ====')
        if self.params:
            self.backup_player_params = [self.params[self.PLAYER_NAME][:],
                                         self.params[self.PLAYER_NAME][:]]
            # logger.error('DE backup_player_params = {}'.format(self.backup_player_params))

    def set_backup_params_from_session(self):
        # logger.error('DE ==== set_backup_params_from_session  ====')
        # logger.error('DE backup params before = {}'.format(self.backup_player_params))
        if self.backup_player_params is not None:
            self.backup_player_params[1] = self.params[self.PLAYER_NAME][:]
        # logger.error('DE backup params  after = {}'.format(self.backup_player_params))

    def get_player_params_from_backup(self, param_type=0):
        # logger.error('DE ==== get_player_params_from_backup  ====')
        the_param_type = 0
        if param_type in (0, 'config'):
            the_param_type = 0
        elif param_type in (1, 'session'):
            the_param_type = 1
        # logger.error('DE param_type = "{0}", {1}'.format(param_type, the_param_type))
        # logger.error('DE params before = {}'.format(self.params))
        if self.backup_player_params is not None:
            self.params[self.PLAYER_NAME] = self.backup_player_params[the_param_type][:]
        # logger.error('DE params  after = {}'.format(self.params))
        # logger.error('DE backup_player_params = {}'.format(self.backup_player_params))

    def _read_player_params(self):
        pass

    def _save_player_params(self):
        pass

    def _config_to_params(self, a_param):
        player = a_param[0].split('_')[0]
        default = False
        if a_param[1].startswith('*'):
            default = True
            a_param[1] = a_param[1][1:]
        self.params[player].append('='.join(a_param[1:]))
        if default:
            self.params[player][0] = len(self.params[player]) - 1

    def check_parameters(self):
        ''' Config parameters check '''
        # logger.error('DE check_params: params = {}'.format(self.params))

        for a_key in self.saved_params.keys():
            if self.saved_params[a_key] != self.params[a_key]:
                self.dirty_config = True
                return True
        if self.localize != self._old_localize:
            return True
        return False

    def _validate_config_key(
            self, a_key, theme, trnsp,
            f_trnsp, calcf, auto, rec_dir
            ):
        ''' check if a config parameter is different to the one in the config file
            if it has return "key = value", otherwise return None
        '''
        comment = ''
        if a_key == 'theme':
            return None if self.config_opts[a_key][-1] == theme else a_key + ' = ' + str(theme)
        if a_key == 'use_transparency':
            return None if self.config_opts[a_key][-1] == trnsp else a_key + ' = ' + str(trnsp)
        if a_key == 'force_transparency':
            return None if self.config_opts[a_key][-1] == f_trnsp else a_key + ' = ' + str(f_trnsp)
        if a_key == 'calculated_color_factor':
            return None if self.config_opts[a_key][-1] == calcf else a_key + ' = ' + str(calcf)
        if a_key == 'auto_update_theme':
            return None if self.config_opts[a_key][-1] == auto else a_key + ' = ' + str(auto)
        if a_key == 'recording_dir':
            comment = r'''# Please do not change the recording_dir paramter manually
# Use the following program option instead:
#     Config / General Options / Recordings dir
'''
            # logger.error('\n\nself.config_opts[a_key][-1]: {} == rec_dir: {}\n\n'.format(self.config_opts[a_key][-1], rec_dir))
            if self._dir_to_shorthand(self.config_opts[a_key][-1]) == rec_dir or \
                    rec_dir == 'default':
                # logger.error('returning None')
                return None
            # logger.error('returning {}'.format(comment + a_key + ' = ' + rec_dir))
            return comment + a_key + ' = ' + rec_dir
        if self.config_opts[a_key][-1] == self.opts[a_key][-1]:
            return None
        return comment + a_key + ' = ' + str(self.opts[a_key][-1])

    def _get_sting_to_save(
            self, theme, trnsp,
            f_trnsp, calcf, auto, rec_dir
            ):
        out = []
        prm = []
        for n in self.config_opts.keys():
            if self.config_opts[n][0] != '' and \
                    self.config_opts[n][-1] != '-' and \
                    self.config_opts[n][-1] != '':
                prm.append(n)
        for n in prm:
            chk = self._validate_config_key(
                    n, theme, trnsp, f_trnsp,
                    calcf, auto, rec_dir
                    )
            if chk:
                out.append(chk)

        if self.localize is not None:
            out.append(f'localized_keys = {self.localize}')

        if not self.show_no_themes_message:
            out.append('')
            out.append('#')
            out.append('# User option (response to a message window)')
            out.append('# Show a message if themes are disabled')
            out.append('#')
            out.append('# Default value: True')
            out.append('show_no_themes_message = False')
        if not self.show_recording_start_message:
            out.append('')
            out.append('#')
            out.append('# User option (response to a message window)')
            out.append('# Show a message when recording is enabled')
            out.append('#')
            out.append('# Default value: True')
            out.append('show_recording_message = False')

        if out:
            # it is ok, no need to compensate for zip/wheel
            config_path = path.join(path.dirname(__file__), 'config')
            out.reverse()
            out.append('#')
            if path.isfile(config_path):
                out.append(f'# or examine the file: {config_path}')
            out.append('# To get a full list of options execute: pyradio -pc')
            out.append('# PyRadio User Configuration File')
            out.reverse()

        # for i, n in enumerate(out):
        #     logger.error(f'out[{i}] : {n}')
        # logger.error(out)
        return out

    def _dir_to_shorthand(self, a_dir):
        ret = a_dir.replace(
                path.expanduser('~'),
                '%HOMEPATH%' if sys.platform.startswith('win') else '~'
                )
        return ret

    def _save_config_from_fixed_rec_dir(self, a_path):
        self._fixed_recording_dir = a_path

    def save_config(self, from_command_line=False):
        ''' Save config file

            Creates config.restore (back up file)
            Returns:
                -1: Error saving config
                 0: Config saved successfully
                 1: Config not saved (not modified)
                 TODO: 2: Config not saved (session locked) '''
        if self.locked:
            if not from_command_line and \
                    logger.isEnabledFor(logging.INFO):
                logger.info('Config not saved (session locked)')
            return 1

        if self.headless:
            if not from_command_line and \
                    logger.isEnabledFor(logging.INFO):
                logger.info('Config not saved (session is headless)')
            return 1

        ''' Check if parameters are changed
            Do it this way (not using is_ditry) to capture
            parameter changes due to 'Z' also
        '''
        # logger.error('DE save_conifg: saved params = {}'.format(self.saved_params))
        if not from_command_line:
            self.get_player_params_from_backup()
        if self.check_parameters():
            self.saved_params = deepcopy(self.params)
            # if logger.isEnabledFor(logging.DEBUG):
            #     logger.info('* self.backup_player_params {}'.format(self.backup_player_params))
            if self.backup_player_params is not None:
                self.backup_player_params[1] = self.backup_player_params[0][:]
            # if logger.isEnabledFor(logging.DEBUG):
            #    logger.info('* self.backup_player_params {}'.format(self.backup_player_params))
        if not from_command_line and \
                logger.isEnabledFor(logging.DEBUG):
            logger.debug(f'saved params = {self.saved_params}')

        # logger.info('\nsaved_params\n{}\n\n'.format(self.saved_params))
        if not self.opts['dirty_config'][1]:
            if not from_command_line and \
                    logger.isEnabledFor(logging.INFO):
                logger.info('Config not saved (not modified)')
            return 1
        if path.exists(self.config_file):
            copyfile(self.config_file, self.config_file + '.restore')
        if self.opts['default_station'][1] is None:
            self.opts['default_station'][1] = '-1'

        if self.use_themes:
            theme = self.opts['theme'][1] if not self.opts['auto_update_theme'][1] else '*' + self.opts['theme'][1]
            trnsp = self.opts['use_transparency'][1]
            f_trnsp = self.opts['force_transparency'][1]
            calcf = self.opts['calculated_color_factor'][1]
            auto = self.opts['auto_update_theme'][1]
        else:
            theme = self.bck_opts['theme'] if not self.bck_opts['auto_update_theme'] else '*' + self.bck_opts['theme']
            trnsp = self.bck_opts['use_transparency']
            f_trnsp = self.bck_opts['force_transparency']
            calcf = self.bck_opts['calculated_color_factor']
            auto = self.bck_opts['auto_update_theme']

        if self.opts['recording_dir'][1] == path.join(path.expanduser('~'), 'pyradio-recordings'):
            rec_dir = 'default'
        else:
            rec_dir = self._dir_to_shorthand(self.opts['recording_dir'][1])
        self.xdg.set_recording_dir(
                new_dir=self.opts['recording_dir'][1],
                print_to_console=False,
                migrate=False
                )

        # TODO: migrate recordings dir
        # self.xdg.set_recording_dir(
        #         new_dir=rec_dir,
        #         print_to_console=False,
        #         migrate=False
        # )
        try:
            out = self._get_sting_to_save(theme, trnsp, f_trnsp, calcf, auto, rec_dir)
            with open(self.config_file, 'w', encoding='utf-8') as cfgfile:
                if out:
                    cfgfile.write('\n'.join(out) + '\n')

            ''' write extra player parameters to file '''
            if path.exists(self.player_params_file):
                try:
                    copyfile(self.player_params_file, self.player_params_file + '.restore')
                except:
                    pass
            # fix self.saved_params (remove profiles)
            profiles_params_changed = False
            for a_key in self.saved_params.keys():
                # the_id = self.saved_params[a_key][0]
                the_profile = self.saved_params[a_key][self.saved_params[a_key][0]]
                # logger.error('\n\na_key = {0}\nthe_id = {1}\nthe_profile = {2}\n\n'.format(a_key, the_id, the_profile))
                for i in range(len(self.saved_params[a_key])-1, 0, -1):
                    if self.saved_params[a_key][i].startswith('profile:'):
                        if self.saved_params[a_key][i] != the_profile:
                            del self.saved_params[a_key][i]
                            profiles_params_changed = True
                    self.saved_params[a_key][0] = self.saved_params[a_key].index(the_profile)
            if profiles_params_changed and logger.isEnabledFor(logging.DEBUG):
                logger.debug(f'reduced saved params = {self.saved_params}')
            try:
                with open(self.player_params_file, 'w', encoding='utf-8') as jf:
                    jf.write(json.dumps(self.saved_params))
                    if path.exists(self.player_params_file + '.restore'):
                        try:
                            remove(self.player_params_file + '.restore')
                        except:
                            pass
            except:
                pass
        except:
            if not from_command_line and \
                    logger.isEnabledFor(logging.ERROR):
                logger.error('Error saving config')
            return -1
        if not out:
            remove(self.config_file)

        # if self.open_last_playlist:
        #     self.save_last_playlist()
        try:
            remove(self.config_file + '.restore')
        except:
            pass
        if logger.isEnabledFor(logging.INFO):
            logger.info('Config saved')
        self.dirty_config = False
        self.params_changed = False
        self._linux_resource_opener = self.resource_opener
        return 0

    def read_playlist_file(
            self, stationFile='',
            is_last_playlist=False,
            is_register=False
    ):
        if stationFile.strip() == '':
            stationFile = self.default_playlist
        return super().read_playlist_file(
            stationFile=stationFile,
            is_last_playlist=is_last_playlist,
            is_register=is_register)

    def can_like_a_station(self):
        return self._current_log_title != self._last_liked_title

    def is_blacklisted_terminal(self):
        self.terminal_is_blacklisted = False
        if HAS_PSUTIL:
            pid = getpid()
            try:
                parents = psutil.Process(pid).parents()
            except AttributeError:
                parents = self._get_parents(pid)

            logger.error('\n\nparents = {}\n\n'.format(parents))
            if parents is not None:
                '''
                read ~/.config/pyradio/no-themes-terminals
                '''
                term_file = path.join(self.stations_dir, 'no-themes-terminals')
                if path.exists(term_file):
                    try:
                        with open(term_file, 'r', encoding='utf-8') as term:
                            user_terminals = term.read().splitlines()
                    except:
                        pass
                    logger.error(f'\n\nuser terminals: {user_terminals}\n\n')
                    if user_terminals:
                        for parent in parents:
                            if parent.name() in user_terminals:
                                '''
                                set this to not display notification because
                                user has customized no-themes-terminals
                                '''
                                self.no_themes_from_command_line = True
                                self.terminal_is_blacklisted = True
                                return True

                ''' blacklisted terminals '''
                terminals = [
                    'yakuake',
                    'konsole',
                    'qterminal',
                    'terminology',
                    'deepin-terminal',
                    'pangoterm'
                ]
                for parent in parents:
                    if parent.name() in terminals:
                        ''' auto detected terminal; display notification '''
                        self.terminal_is_blacklisted = True
                        return True
        return False

    def _get_parents(self, pid):
        '''
        get pid parents, when psutil.Process.parents() does not exist
        '''
        procs = []
        out = []
        ppid = None
        for proc in psutil.process_iter(['pid', 'ppid', 'name', 'username']):
            procs.append(proc)
            if proc.pid == pid:
                par = proc.parent()
                if par:
                    ppid = par.pid
        old_ppid = 0
        while ppid > 100 or old_ppid == ppid:
            for proc in procs:
                if proc.pid == ppid:
                    out.append(proc)
                    par = proc.parent()
                    if par:
                        old_ppid = ppid
                        ppid = par.pid
        return out


class PyRadioPlaylistStack():

    def __init__(self):
        self._p = []

        self._id = {'station_path': 0,
               'path': 0,
               'station_file_name': 1,
               'file_name': 1,
               'filename': 1,
               'station_title': 2,
               'title': 2,
               'startPos': 3,
               'selection': 4,
               'playing' : 5,
               'is_register': 6,
               'browsing_station_service': 7,
               }

    def __len__(self):
        return len(self._p)

    @property
    def is_local_playlist(self):
        if self._p:
            return not self._p[-1][self._id['is_register']] and \
                not self._p[-1][self._id['browsing_station_service']]
        return True

    @is_local_playlist.setter
    def is_local_playlist(self, value):
        raise ValueError('parameter is read only')

    @property
    def is_register(self):
        if self._p:
            return self._p[-1][self._id['is_register']]
        return False

    @is_register.setter
    def is_register(self, value):
        if self._p:
            self._p[-1][self._id['is_register']] = value

    @property
    def browsing_station_service(self):
        if self._p:
            return self._p[-1][self._id['browsing_station_service']]
        return False

    @browsing_station_service.setter
    def browsing_station_service(self, value):
        if self._p:
            self._p[-1][self._id['browsing_station_service']] = value

    @property
    def station_path(self):
        if self._p:
            return self._p[-1][self._id['station_path']]
        return ''

    @station_path.setter
    def station_path(self, value):
        if self._p:
            self._p[-1][self._id['station_path']] = value

    @property
    def station_file_name(self):
        if self._p:
            return self._p[-1][self._id['station_file_name']]
        return ''

    @station_file_name.setter
    def station_file_name(self, value):
        if self._p:
            self._p[-1][self._id['station_file_name']] = value

    @property
    def station_title(self):
        if self._p:
            return self._p[-1][self._id['station_title']]
        return ''

    @station_title.setter
    def station_title(self, value):
        if self._p:
            self._p[-1][self._id['station_title']] = value

    @property
    def selection(self):
        if self._p:
            return self._p[-1][self._id['selection']]
        return 0

    @selection.setter
    def selection(self, value):
        if self._p:
            self._p[-1][self._id['selection']] = value

    @property
    def startPos(self):
        if self._p:
            return self._p[-1][self._id['startPos']]
        return 0

    @startPos.setter
    def startPos(self, value):
        if self._p:
            self._p[-1][self._id['startPos']] = value

    @property
    def playing(self):
        if self._p:
            return self._p[-1][self._id['playing']]
        return -1

    @playing.setter
    def playing(self, value):
        if self._p:
            self._p[-1][self._id['playing']] = value

    @property
    def last_local_playlist(self):
        for n in range(len(self._p) -1, -1, -1):
            if not self._p[n][-1] and not self._p[n][-2]:
                return self._p[n]

    @last_local_playlist.setter
    def last_local_playlist(self, value):
        raise ValueError('parameter is read only')

    def item(self, item_id=-1):
        try:
            return self._p[item_id]
        except:
            return []

    def remove_duplicates(self):
        if len(self._p) > 1:
            val1 = self._p[-1][self._id['station_file_name']]
            val2 = self._p[-2][self._id['station_file_name']]
            if val1 == val2:
                self._p.pop()
                if logger.isEnabledFor(logging.DEBUG):
                    logger.debug('PyRadioPlaylistStack: Removing duplicate entry...')
                return True
        return False

    def duplicate(self):
        it = self._p[-1]
        self._p.append(it)

    def add(self, station_path='',
            station_file_name='',
            station_title='',
            startPos=0, selection=0, playing=-1,
            is_register=False,
            browsing_station_service=False):
        if len(self._p) > 1 and station_path:
            if self._p[-1][self._id['station_path']] == station_path:
                if logger.isEnabledFor(logging.DEBUG):
                    logger.debug(f'PyRadioPlaylistStack.add(): Refusing to add duplicate entry: "{station_path}"\nUpdating selections instead')
                    logger.debug('                            Updating selections instead')
                self._p[-1][3:6] = [startPos, selection, playing]
                return
        if is_register:
            while self._p[-1][self._id['is_register']]:
                self._p.pop()
        self._p.append([station_path,
            station_file_name,
            station_title,
            startPos, selection, playing,
            is_register,
            browsing_station_service])
        # logger.error('DE playlist history\n{}\n'.format(self._p))

    def get_item_member(self, member, item_id=-1):
        if member in self._id:
            return self._p[item_id][self._id[member]]
        raise ValueError(f'member "{member}" does not exist')

    def _find_history_by_id(self, a_search, it_id, start=0):
        ''' Find a history item

            Parameters
            ==========
            a_search search term
            it_id    one of the _id strings
            start    return id if >0 start

            Returns
            =======
            history item,
            index,
            reversed index (len - id - 1)
        '''
        logger.error('DE looking for: ' + a_search + ' with id: ' + it_id)
        for i, n in enumerate(self._p):
            if (n[self._id[it_id]] == a_search) and (i >= start):
                return n, i, len(self._p) - i - 1
        return None, -1, -1

    def pop(self):
        if len(self._p) > 1:
            return self._p.pop()
        if logger.isEnabledFor(logging.DEBUG):
            logger.debug('Refusing to remove first entry')
        return self._p[0]

    def set(self, a_list):
        if a_list:
            self._p = a_list[:]
        else:
            raise ValueError('playlist history cannot be empty')

    def reset(self):
        if self._p:
            self._p = self._p[:1]

    def copy(self):
        return self._p[:]

    def pop_to_first_real_playlist(self):
        if not self.get_item_member('is_register'):
            return
        while self.get_item_member('is_register'):
            self.pop()

    def replace(self, a_search_path, new_item):
        ''' Find a_search_path in history and replace
            the item found with new_item '''
        if not isinstance(new_item, list) and \
                not isinstance(new_item, tuple):
            return -2
        if len(new_item) != 8:
            return -1
        ret = 0
        for i,n in enumerate(self._p):
            if n[0] == a_search_path:
                self._p[i] = list(new_item[:])
                ret += 1
        return ret


class PyRadioStationsStack():

    def __init__(
        self,
        execute_function,
        pass_first_item_function=None,
        pass_last_item_function=None,
        no_items_function=None
    ):
        self.play_from_history = False

        ''' items: list of lists
            [
                [playlist name, station name, station id],
                ...
            ]
        '''
        self.items = []
        self.item = -1

        # ######## DEBUG START
        # self.items = [
        #     ['reversed', 'Lounge (Illinois Street Lounge - SomaFM)', 10],
        #     ['reversed', 'Folk (Folk Forward - SomaFM)', 17],
        #     ['Κέρκυρα', 'Ράδιο Επτάνησα 98,8', 11]
        # ]
        # self.item = 0
        # self.play_from_history = True
        # self.clear()
        # ######## DEBUG END

        self.execute_func = execute_function
        self.pass_first_item_func = pass_first_item_function
        self.pass_last_item_func = pass_last_item_function
        self.no_items_func = no_items_function

    def _show_station_history_debug(self):
        if logger.isEnabledFor(logging.DEBUG):
            logger.debug('>>> Stations history')
            if self.items:
                for n in self.items:
                    logger.debug(f'   {n}')
                logger.debug(f'   item was = {self.item}')
            else:
                logger.debug('   No items in list')
                logger.debug(f'   item = {self.item}')

    def add(self, a_playlist, a_station, a_station_id):
        if a_playlist and a_station:
            if a_playlist == a_station:
                return
            if self.item == -1:
                self.items.append([a_playlist, a_station, a_station_id])
                self.item = 0
                self._show_station_history_debug()
            else:
                if not a_station.startswith('register_') and \
                        (not self.play_from_history) and \
                        (not a_playlist.startswith('register_')) and \
                        (self.items[-1][0] != a_playlist \
                        or self.items[-1][1] != a_station) and \
                        (self.items[self.item][0] != a_playlist \
                         or self.items[self.item][1] != a_station):
                    if logger.isEnabledFor(logging.DEBUG):
                        logger.debug('Adding station history item...')
                    self.items.append([a_playlist, a_station, a_station_id])
                    self.item = len(self.items) - 1
                    self._show_station_history_debug()
            self.play_from_history = False

    def clear(self):
        self.items = []
        self.item = -1
        self.play_from_history = False

    def remove_station(self, a_station):
        for i in range(len(self.items) - 1, -1, -1):
            if self.items[i][1] == a_station:
                self.items.pop(i)
        if self.item >= len(self.items):
            self.item = len(self.items) - 1
        self._show_station_history_debug()

    def rename_station(self, playlist, orig_station, new_station):
        # logger.error('playlist = "{}"'.format(playlist))
        # logger.error('orig_station = "{}"'.format(orig_station))
        # logger.error('new_station = "{}"'.format(new_station))
        self._show_station_history_debug()
        for i in range(len(self.items) - 1, -1, -1):
            if self.items[i][0] == playlist and \
                    self.items[i][1] == orig_station:
                logger.error(f'item = {self.items[i]}')
                self.items[i][1] = new_station
                logger.error(f'item = {self.items[i]}')
        self._show_station_history_debug()

    def rename_playlist(self, orig, new):
        self._show_station_history_debug()
        for i, _ in enumerate(self.items):
            if self.items[i][0] == orig:
                self.items[i][0] = new
        self._show_station_history_debug()

    def _get(self):
        if self.item == -1:
            if self.no_items_func is not None:
                self.no_items_func()
        return tuple(self.items[self.item])

    def play_previous(self):
        self._show_station_history_debug()
        if self.item == -1:
            if self.no_items_func is not None:
                self.no_items_func()
        elif self.item == 0:
            if logger.isEnabledFor(logging.DEBUG):
                logger.debug('   Already on first item')
            if self.pass_first_item_func is not None:
                self.pass_first_item_func()
        else:
            self.item -= 1
            if logger.isEnabledFor(logging.DEBUG):
                logger.debug(f'   item is  = {self.item}')
            self.execute_func(self._get(), self.play_previous)

    def play_next(self):
        self._show_station_history_debug()
        if self.item == -1:
            if self.no_items_func is not None:
                self.no_items_func()
        elif self.item == len(self.items) - 1:
            if logger.isEnabledFor(logging.DEBUG):
                logger.debug('   Already on last item')
            if self.pass_last_item_func is not None:
                self.pass_last_item_func()
        else:
            self.item += 1
            if logger.isEnabledFor(logging.DEBUG):
                logger.debug(f'   item is  = {self.item}')
            self.execute_func(self._get(), self.play_next)

    def restore_index(self, a_func):
        if a_func == self.play_next:
            self.item -= 1
        else:
            self.item += 1
        if logger.isEnabledFor(logging.DEBUG):
            logger.debug(f'   item restored  = {self.item}')

class PyRadioLog():

    PATTERN = '%(asctime)s - %(name)s:%(funcName)s():%(lineno)d - %(levelname)s: %(message)s'
    PATTERN_TITLE = '%(asctime)s | %(message)s'

    def __init__(self, pyradio_config):
        self.log_titles = False
        self.log_debug = False
        self.titles_handler = False
        self.debug_handler = None
        self._cnf = pyradio_config
        self._stations_dir = pyradio_config.stations_dir

    def configure_logger(self, recording_dir=None, debug=None, titles=None):
        logger = logging.getLogger('pyradio')
        logger.setLevel(logging.DEBUG)
        if debug or titles:
            if debug and not self.log_debug:
                # Handler
                log_file = path.join(path.expanduser('~'), 'pyradio.log')
                self.debug_handler = logging.FileHandler(log_file)
                self.debug_handler.setLevel(logging.DEBUG)

                # create formatter
                formatter = logging.Formatter(self.PATTERN)

                # add formatter to ch
                self.debug_handler.setFormatter(formatter)

                # add ch to logger
                #l = logging.getLogger()
                logger.addHandler(self.debug_handler)

                # inform config
                self.log_debug = True

            if titles and not self.log_titles:
                if logger.isEnabledFor(logging.INFO):
                    logger.info('setting up pyradio-titles.log')
                if not path.exists(recording_dir):
                    try:
                        makedirs(recording_dir)
                    except:
                        pass
                if not path.exists(recording_dir):
                    self.log_titles = False
                    self.titles_handler = None
                    if logger.isEnabledFor(logging.ERROR):
                        logger.error(f'cannot start titles log on: "{recording_dir}"; directory does not exist')
                    return False

                self.titles_handler = logging.handlers.RotatingFileHandler(
                    path.join(
                        recording_dir,
                        'pyradio-titles.log'),
                    maxBytes=50000,
                    backupCount=5)
                self.titles_handler.setFormatter(logging.Formatter(
                    fmt=self.PATTERN_TITLE,
                    datefmt='%b %d (%a) %H:%M')
                )
                self.titles_handler.setLevel(logging.CRITICAL)
                #l = logging.getLogger()
                logger.addHandler(self.titles_handler)
                self.log_titles = True
                logger.critical('=== Logging started')
                if logger.isEnabledFor(logging.INFO):
                    logger.info(f'starting titles log on: "{recording_dir}"')

        if (not titles) and self.log_titles:
            if self.titles_handler:
                logger.critical('=== Logging stopped')
                logger.removeHandler(self.titles_handler)
                self.log_titles = False
                self.titles_handler = None

        logger.propagate = False
        logging.raiseExceptions = False
        logging.lastResort = None
        # logger.info('self.log_titles = {}'.format(self.log_titles))
        return True

    def _create_check_output_folder(self):
        # Generate the timestamped folder name using os.strftime
        timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
        folder_name = f"{timestamp}-playlist-check"

        # Join with state directory
        self._cnf.check_output_folder = path.join(self._cnf.state_dir, folder_name)

        # print('self._cnf.check_output_folder = "{}"'.format(self._cnf.check_output_folder))
        # Check if the directory exists, if not, create it
        try:
            if not path.exists(self._cnf.check_output_folder):
                makedirs(self._cnf.check_output_folder, exist_ok=True)

            # Double-check if the folder was created successfully
            if path.isdir(self._cnf.check_output_folder):
                return True
            return False
        except Exception:
            # print(f"Error creating directory: {e}")
            return False

    def tag_title(self, the_log):
        ''' tags a title

            Returns:
                0: All ok
                1: Error
                2: Already tagged
        '''
        if self._cnf.can_like_a_station():
            if logger.isEnabledFor(logging.CRITICAL):
                try:
                    the_log._write_title_to_log()
                except:
                    return 1
                return 0
        return 2

class PyRadioBase16Themes():

    NAME = 'Base16 Project'
    ''' theme name to be found in themes window '''
    THEME = (
        'base16-pyradio-default',
        'base16-pyradio-default-alt',
        'base16-pyradio-variation',
        'base16-pyradio-variation-alt'
    )
    ''' To be used in get_url (along with theme_id) '''
    URL_PART = (
        'default',
        'default-alt',
        'variation',
        'variation-alt'
    )
    '''  link to last used base16 theme '''
    _ln = path.join(path.expanduser('~'), '.config/base16-project/base16_shell_theme')

    def __init__(self, config):
        self.theme = None
        self.status = None

        ''' last used theme name (no "base16-", no extension) '''
        self._last_used_theme = None
        ''' pyradio base16 file for auto download '''
        self._default_theme_file = None
        ''' the default base16 file, without path and extension '''
        self.default_filename_only = 'base16-pyradio'
        ''' pyradio base16 them for download '''
        self._custom_theme_file = None

        ''' working parameters'''
        ''' applied name
            to be used by download()
        '''
        self.theme_id = 0
        ''' applied theme filename (if autoloaed, = _default_theme_file)
            to be used by download()
        '''
        self.theme_file_name = self._default_theme_file
        # applied theme url
        self.theme_url = None

        self._cnf = config
        self._themes_dir = config.themes_dir
        ''' base16 autoload filename '''
        self._custom_theme_file = path.join(self._themes_dir, self.THEME[self.theme_id] + '.pyradio-theme')
        self._default_theme_file = path.join(self._themes_dir, 'base16-pyradio.pyradio-theme')

    @property
    def check_file(self):
        return self._ln

    @property
    def default_theme_path(self):
        return self._default_theme_file

    @property
    def theme_path(self):
        return self._custom_theme_file

    @property
    def can_auto_update(self):
        if sys.platform.startswith('win'):
            ''' base16 does not work on windows '''
            return False
        return path.exists(self._ln)

    @property
    def last_used_theme(self):
        if path.exists(self._ln):
            try:
                self._last_used_theme = path.basename(readlink(self._ln)[7:-3])
            except:
                self._last_used_theme = None
        else:
            self._last_used_theme = None
        return self._last_used_theme

    def get_url(self, a_theme=None):
        base_url = 'https://raw.githubusercontent.com/edunfelt/base16-pyradio/master/themes'
        w_theme = self.last_used_theme if a_theme is None else a_theme
        # if self._last_used_theme is None:
        #     return None
        if not w_theme.startswith('base16-'):
            w_theme = 'base16-' + w_theme
        if not w_theme.endswith('.pyradio-theme'):
            w_theme += '.pyradio-theme'
        # logger.error('get_url(): url = {}'.format(base_url + '/' + self.URL_PART[self.theme_id] + '/' + w_theme))
        return base_url + '/' + self.URL_PART[self.theme_id] + '/' + w_theme

    def download(self, a_theme=None, a_path=None, print_errors=None):
        ''' download a theme
            return False if failed...
            delete downloaded file if failed

            Parameters
            ==========
            a_theme
                the theme name (no path, no extension)
                if None, use self.check_file content
            a_path
                full path to save the theme to
                if None, use self._custom_theme_file
        '''
        w_path = self._default_theme_file if a_path is None else a_path
        if self._cnf.locked:
            if logger.isEnabledFor(logging.DEBUG):
                logger.debug(' Theme creation inhibited (session if locked)... Waiting for 2 seconds...')
            ret = True
            sleep(2)
        else:
            if a_theme is None:
                try:
                    if self.can_auto_update:
                        # w_theme = path.basename(readlink(self._ln)[:-3]) + '.pyradio-theme'
                        w_theme = path.basename(readlink(self._ln))[7:-3]
                    else:
                        return False, None
                except:
                    return False, None
            else:
                w_theme = a_theme

            url = self.get_url(w_theme)
            if logger.isEnabledFor(logging.DEBUG):
                logger.debug(f'Project theme URL: "{url}"')

            ret = False
            # logger.error('w_path = {}'.format(w_path))
            requests_response = None
            written = False
            for n in range(0,5):
                requests_response = None
                try:
                    requests_response = requests.get(url, timeout=1)
                    requests_response.raise_for_status()
                    try:
                        with open(w_path, 'w', encoding='utf-8') as f:
                            f.write(requests_response.text)
                        written = True
                    except:
                        logger.error('cannot write file')
                        if print_errors is not None:
                            print_errors.addstr(n + 1, 0, '  download failed, retrying...', curses.color_pair(0))
                            print_errors.refresh()
                except requests.exceptions.RequestException:
                    if print_errors is not None:
                        print_errors.addstr(n + 1, 0, '  download failed, retrying...', curses.color_pair(0))
                        print_errors.refresh()
                    logger.error('requests through an exception')
                    sleep(.15)
                if requests_response is not None:
                    if requests_response.status_code == 200 and written:
                        break

            self.theme = self._last_used_theme
            self.theme_file_name = w_path
            self.theme_url = url
            try:
                self.status = requests_response.status_code
            except AttributeError:
                self.status = 404

            ret = self.status == 200 and written
            if not ret:
                try:
                    remove(w_path)
                except:
                    pass
        return ret, w_path


class PyRadioPyWalThemes(PyRadioBase16Themes):
    NAME = 'PyWal Project'
    ''' theme name to be found in themes window '''
    THEME = (
        'pywal-pyradio-default',
        'pywal-pyradio-default-alt',
    )
    '''  link to last used base16 theme '''
    _ln = path.join(path.expanduser('~'), '.cache', 'wal', 'colors.json')

    def __init__(self, config):
        super().__init__(config)
        self._cnf = config
        self._themes_dir = config.themes_dir
        self._custom_theme_file = path.join(self._themes_dir, self.THEME[self.theme_id] + '.pyradio-theme')
        self._default_theme_file = path.join(self._themes_dir, 'pywal-pyradio.pyradio-theme')
        ''' the default pywal file, without path and extension '''
        self.default_filename_only = 'pywal-pyradio'

    def download(self, a_theme=None, a_path=None, print_errors=None):
        ''' download a theme
            return False if failed...
            delete downloaded file if failed

            Parameters
            ==========
            a_theme
                the theme name (no path, no extension)
                if None, use self.check_file content
            a_path
                full path to save the theme to
                if None, use self._custom_theme_file
        '''
        w_path = self._default_theme_file if a_path is None else a_path
        if self._cnf.locked:
            if logger.isEnabledFor(logging.DEBUG):
                logger.debug(' Theme creation inhibited (session if locked)... Waiting for 1 second...')
            ret = True
            sleep(1)
        else:
            templates = ('''# Main foreground and background
Stations            {color15} {color0}

# Playing station text color
# (background color will come from Stations)
Active Station      {color1}

# Status bar foreground and background
Status Bar          {color0} {color3}

# Normal cursor foreground and background
Normal Cursor       {color0} {color1}

# Cursor foreground and background
# when cursor on playing station
Active Cursor       {color0} {color3}

# Cursor foreground and background
# This is the Line Editor cursor
Edit Cursor         {color15} {color5}

# Text color for extra function indication
# and jump numbers within the status bar
# (background color will come from Stations)
Extra Func          {color7}

# Text color for URL
# (background color will come from Stations)
PyRadio URL         {color2}

# Message window border foreground and background.
# The background color can be left unset.
# Please refer to the following link for more info
# https://github.com/coderholic/pyradio#secondary-windows-background
#
Messages Border     {color4}

# Theme Transparency
# Values are:
#   0: No transparency (default)
#   1: Theme is transparent
#   2: Obey config setting
transparency        0
''', '''# Main foreground and background
Stations            {color15} {color0}

# Playing station text color
# (background color will come from Stations)
Active Station      {color3}

# Status bar foreground and background
Status Bar          {color0} {color6}

# Normal cursor foreground and background
Normal Cursor       {color0} {color3}

# Cursor foreground and background
# when cursor on playing station
Active Cursor       {color0} {color6}

# Cursor foreground and background
# This is the Line Editor cursor
Edit Cursor         {color15} {color5}

# Text color for extra function indication
# and jump numbers within the status bar
# (background color will come from Stations)
Extra Func          {color7}

# Text color for URL
# (background color will come from Stations)
PyRadio URL         {color4}

# Message window border foreground and background.
# The background color can be left unset.
# Please refer to the following link for more info
# https://github.com/coderholic/pyradio#secondary-windows-background
#
Messages Border     {color2}

# Theme Transparency
# Values are:
#   0: No transparency (default)
#   1: Theme is transparent
#   2: Obey config setting
transparency        0
'''
            )
            if a_theme is None:
                try:
                    if not self.can_auto_update:
                        return False, None
                except:
                    return False, None
            else:
                w_theme = a_theme

            with open(self._ln, 'r', encoding='utf-8') as jfile:
                jdata = json.load(jfile)

            lines = templates[self.theme_id].split('\n')
            for k in jdata['colors'].keys():
                for i, _ in enumerate(lines):
                    lines[i] = lines[i].replace('{' + k + '}', jdata['colors'][k])

            ret = True
            try:
                with open(w_path, 'w', encoding='utf-8') as out_file:
                    for n in lines:
                        out_file.write(n + '\n')
            except:
                ret = False

        if ret:
            return True, w_path

        try:
            remove(w_path)
        except:
            pass
        return False, None


class PyRadioThemesShThemes(PyRadioBase16Themes):
    NAME = 'Theme.sh Project'
    ''' theme name to be found in themes window '''
    THEME = (
        'theme-sh-pyradio-default',
        'theme-sh-pyradio-default-alt',
        'theme-sh-pyradio-variation',
        'theme-sh-pyradio-variation-alt',
    )
    ''' To be used in get_url (along with theme_id) '''
    '''  link to last used base16 theme '''

    def __init__(self, config):
        super().__init__(config)
        self._cnf = config
        self._themes_dir = config.themes_dir
        self._custom_theme_file = path.join(self._themes_dir, self.THEME[self.theme_id] + '.pyradio-theme')
        self._default_theme_file = path.join(self._themes_dir, 'theme-sh-pyradio.pyradio-theme')
        self._theme_sh_executable = pywhich('theme.sh')
        ''' the default theme.sh file, without path and extension '''
        self.default_filename_only = 'theme-sh-pyradio'

        xdg = getenv('XDG_CONFIG_HOME')
        if xdg:
            self._ln = path.join(xdg, '.theme_history')
        else:
            self._ln = path.join(path.expanduser('~'), '.theme_history')
        new_ln = path.join(self._cnf.state_dir, 'theme_history')
        if path.exists(self._ln) and \
                path.exists(self._cnf.state_dir):
            move(self._ln, new_ln)
        self._ln = new_ln

    @property
    def can_auto_update(self):
        if sys.platform.startswith('win'):
            ''' theme.sh does not work on windows '''
            return False

        if not path.exists(self._ln):
            return False

        if self._theme_sh_executable is None:
            return False
        return True

    def download(self, a_theme=None, a_path=None, print_errors=None):
        ''' read theme name from self._ln
            read theme data from self._theme_sh_executable
            return False if failed...
            delete downloaded file if failed

            Parameters
            ==========
            a_theme
                the theme name (no path, no extension)
                if None, use self.check_file content
            a_path
                full path to save the theme to
                if None, use self._custom_theme_file
        '''
        w_path = self._default_theme_file if a_path is None else a_path
        if self._cnf.locked:
            if logger.isEnabledFor(logging.DEBUG):
                logger.debug(' Theme creation inhibited (session if locked)... Waiting for 1 second...')
            ret = True
            sleep(1)
        else:
            theme_name = self._read_last_line_from_ln()
            if logger.isEnabledFor(logging.DEBUG):
                logger.debug('Actual theme name: ' + theme_name)
            theme_data = self._read_theme_sh(theme_name)
            templates = ('''# Main foreground and background
Stations            {foreground} {background}

# Playing station text color
# (background color will come from Stations)
Active Station      {color1}

# Status bar foreground and background
Status Bar          {background} {color4}

# Normal cursor foreground and background
Normal Cursor       {background} {color1}

# Cursor foreground and background
# when cursor on playing station
Active Cursor       {background} {color4}

# Cursor foreground and background
# This is the Line Editor cursor
Edit Cursor         {background} {foreground}

# Text color for extra function indication
# and jump numbers within the status bar
# (background color will come from Stations)
Extra Func          {color1}

# Text color for URL
# (background color will come from Stations)
PyRadio URL         {color2}

# Message window border foreground and background.
# The background color can be left unset.
# Please refer to the following link for more info
# https://github.com/coderholic/pyradio#secondary-windows-background
#
Messages Border     {color8}

# Theme Transparency
# Values are:
#   0: No transparency (default)
#   1: Theme is transparent
#   2: Obey config setting
transparency        0
''', '''# Main foreground and background
Stations            {foreground} {background}

# Playing station text color
# (background color will come from Stations)
Active Station      {color4}

# Status bar foreground and background
Status Bar          {background} {color1}

# Normal cursor foreground and background
Normal Cursor       {background} {color4}

# Cursor foreground and background
# when cursor on playing station
Active Cursor       {background} {color1}

# Cursor foreground and background
# This is the Line Editor cursor
Edit Cursor         {background} {foreground}

# Text color for extra function indication
# and jump numbers within the status bar
# (background color will come from Stations)
Extra Func          {color4}

# Text color for URL
# (background color will come from Stations)
PyRadio URL         {color2}

# Message window border foreground and background.
# The background color can be left unset.
# Please refer to the following link for more info
# https://github.com/coderholic/pyradio#secondary-windows-background
#
Messages Border     {color8}

# Theme Transparency
# Values are:
#   0: No transparency (default)
#   1: Theme is transparent
#   2: Obey config setting
transparency       0
''', '''# Main foreground and background
Stations            {foreground} {background}

# Playing station text color
# (background color will come from Stations)
Active Station      {color1}

# Status bar foreground and background
Status Bar          {background} {color2}

# Normal cursor foreground and background
Normal Cursor       {background} {color1}

# Cursor foreground and background
# when cursor on playing station
Active Cursor       {background} {color2}

# Cursor foreground and background
# This is the Line Editor cursor
Edit Cursor         {background} {foreground}

# Text color for extra function indication
# and jump numbers within the status bar
# (background color will come from Stations)
Extra Func          {color1}

# Text color for URL
# (background color will come from Stations)
PyRadio URL         {foreground}

# Message window border foreground and background.
# The background color can be left unset.
# Please refer to the following link for more info
# https://github.com/coderholic/pyradio#secondary-windows-background
#
Messages Border     {color8}

# Theme Transparency
# Values are:
#   0: No transparency (default)
#   1: Theme is transparent
#   2: Obey config setting
transparency        0
''', '''# Main foreground and background
Stations            {foreground} {background}

# Playing station text color
# (background color will come from Stations)
Active Station      {color2}

# Status bar foreground and background
Status Bar          {background} {color1}

# Normal cursor foreground and background
Normal Cursor       {background} {color2}

# Cursor foreground and background
# when cursor on playing station
Active Cursor       {background} {color1}

# Cursor foreground and background
# This is the Line Editor cursor
Edit Cursor         {background} {foreground}

# Text color for extra function indication
# and jump numbers within the status bar
# (background color will come from Stations)
Extra Func          {color2}

# Text color for URL
# (background color will come from Stations)
PyRadio URL         {foreground}

# Message window border foreground and background.
# The background color can be left unset.
# Please refer to the following link for more info
# https://github.com/coderholic/pyradio#secondary-windows-background
#
Messages Border     {color8}

# Theme Transparency
# Values are:
#   0: No transparency (default)
#   1: Theme is transparent
#   2: Obey config setting
transparency        0
'''
            )

            lines = templates[self.theme_id].split('\n')
            for k in 'foreground', 'background':
                for i, _ in enumerate(lines):
                    lines[i] = lines[i].replace('{' + k + '}', theme_data[k])

            for k in range(15, -1, -1):
                if k == 8 and \
                        theme_data[str(8)] == theme_data['background']:
                    ''' fix border color when it's equal to background '''
                    color_data = theme_data[str(5 + (self.theme_id % 2))]
                else:
                    color_data = theme_data[str(k)]
                token = '{color' + str(k) + '}'
                for i, _ in enumerate(lines):
                    lines[i] = lines[i].replace(token, color_data)
            ret = True
            # enable this to see the contents of the theme
            # for n in lines:
            #     logger.error(n)
            try:
                with open(w_path, 'w', encoding='utf-8') as out_file:
                    for n in lines:
                        out_file.write(n + '\n')
            except:
                ret = False

        if ret:
            return True, w_path

        try:
            remove(w_path)
        except:
            pass
        return False, None

    def _read_last_line_from_ln(self):
        last_line = ''
        with open(self._ln, "rb") as file:
            try:
                file.seek(-2, SEEK_END)
                while file.read(1) != b'\n':
                    file.seek(-2, SEEK_CUR)
            except:
                file.seek(0)
            last_line = file.readline().decode()
        return last_line.replace('\n', '')

    def _read_theme_sh(self, theme_name):
        lines = {}
        in_theme = False
        with open(self._theme_sh_executable, 'r', encoding='utf-8') as f:
            for line in f:
                if in_theme:
                    l = line.replace('\n', '').split(': ')
                    lines[l[0]] = l[1]
                    if l[0].startswith('cursor'):
                        break
                if line.startswith(theme_name):
                    in_theme = True
        return lines


class FavoritesManager:
    def __init__(self, a_file):
        self.file_path = a_file

    def add(self, an_item):
        ''' Returns:
                -2 : Error saving file
                -1 : Invalid item
                 0 : Item added
                 1 : Item already in favorites
        '''
        if an_item[0] == '' or \
                an_item[1] == '':
            return -1, '___Station is invalid!___'

        items = self._read_csv()
        updated = False
        write_it = True

        this_item = an_item[:]
        if this_item[Station.buffering] == '0@128':
            this_item[Station.buffering] = ''
        while this_item[-1] == '':
            this_item.pop()

        msg = None
        for i, item in enumerate(items):
            if item[1] == this_item[1]:
                if item == this_item:
                    return 1, '___Already in favorites!___'
                items[i] = this_item
                msg = '___Station updated!___'
                updated = True
                break
        if not updated:
            items.append(this_item)
            updated = True
        if updated:
            ret = self._write_csv(items)
            return ret[0], msg if msg else ret[1]
        return 1, '___Already in favorites!___'

    def _read_csv(self):
        csv_in = CsvReadWrite(a_file=self.file_path)
        ret = csv_in.read()
        if ret:
            items = csv_in.items
        else:
            items = []
        csv_in = None
        return items

    def _write_csv(self, items):
        ''' Returns:
                -2 : Error saving file
                 0 : Item added
        '''
        csv_out = CsvReadWrite(a_file=self.file_path)
        ret = csv_out.write(items=items)
        csv_out = None
        if ret == 0:
            return 0, '___Added to favorites!___'
        return -2, '___Error writing favorites!___'

