From 9d903a1a1d0bf15290b8c7fd4e6fdc5325753a54 Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Sun, 23 Jan 2022 18:37:39 +0100 Subject: [PATCH 01/51] Allow favicons in favourites --- ycast/my_stations.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/ycast/my_stations.py b/ycast/my_stations.py index 64df667..d330f79 100644 --- a/ycast/my_stations.py +++ b/ycast/my_stations.py @@ -12,12 +12,12 @@ class Station: - def __init__(self, uid, name, url, category): + def __init__(self, uid, name, url, category, icon): self.id = generic.generate_stationid_with_prefix(uid, ID_PREFIX) self.name = name self.url = url self.tag = category - self.icon = None + self.icon = icon def to_vtuner(self): return vtuner.Station(self.id, self.name, self.tag, self.url, self.icon, self.tag, None, None, None, None) @@ -70,9 +70,14 @@ def get_stations_by_category(category): stations = [] if my_stations_yaml and category in my_stations_yaml: for station_name in my_stations_yaml[category]: - station_url = my_stations_yaml[category][station_name] + station_urls = my_stations_yaml[category][station_name] + url_list = station_urls.split('|') + station_url = url_list[0] + station_icon = None + if len(url_list) > 1: + station_icon = url_list[1] station_id = str(get_checksum(station_name + station_url)).upper() - stations.append(Station(station_id, station_name, station_url, category)) + stations.append(Station(station_id, station_name, station_url, category, station_icon)) return stations From 107ce5c0fa558313ff4b470178e4c10ae73e341a Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Mon, 24 Jan 2022 17:14:12 +0100 Subject: [PATCH 02/51] add recently stations in favourites --- ycast/my_lastheard.py | 71 +++++++++++++++++++++++++++++++++++++++++++ ycast/my_stations.py | 13 ++++++-- ycast/server.py | 3 +- 3 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 ycast/my_lastheard.py diff --git a/ycast/my_lastheard.py b/ycast/my_lastheard.py new file mode 100644 index 0000000..9c527bd --- /dev/null +++ b/ycast/my_lastheard.py @@ -0,0 +1,71 @@ +import logging +import os + +import yaml + +VAR_PATH = os.path.expanduser("~") + '/.ycast' + +config_file = VAR_PATH + '/lastheared.yml' + +def signalStationSelected(name,url,icon): + logging.debug(" %s:%s|%s",name,url,icon) + list_heared_stations = get_stations_list() + if len(list_heared_stations) == 0 : + list_heared_stations.append("recently used:\n") + + for line in list_heared_stations: + elements = line.split(':') + if elements[0] == ' '+name: + list_heared_stations.remove(line) + logging.debug("Name '%s' exists and deleted",name) + piped_icon = '' + if icon and len(icon) > 0: + piped_icon = '|' + icon + + list_heared_stations.insert(1,' '+name+': '+url+piped_icon+'\n') + if len(list_heared_stations) > 11: + # remove last + list_heared_stations.pop() + + set_stations_yaml(list_heared_stations) + +def set_stations_yaml(heared_stations): + try: + os.makedirs(VAR_PATH) + except FileExistsError: + pass + except PermissionError: + logging.error("Could not create folders (%s) because of access permissions", VAR_PATH) + return None + + try: + with open(config_file, 'w') as f: + f.writelines(heared_stations) + logging.info("File written '%s'", config_file) + + except Exception as ex: + logging.error("File not written '%s': %s", config_file, ex) + +def get_stations_list(): + try: + with open(config_file, 'r') as f: + heared_stations = f.readlines() + except FileNotFoundError: + logging.warning("File not found '%s' not found", config_file) + return [] + except yaml.YAMLError as e: + logging.error("Station configuration format error: %s", e) + return [] + return heared_stations + +def get_last_stations_yaml(): + try: + with open(config_file, 'r') as f: + my_stations = yaml.safe_load(f) + except FileNotFoundError: + logging.error("Station configuration '%s' not found", config_file) + return None + except yaml.YAMLError as e: + logging.error("Station configuration format error: %s", e) + return None + return my_stations diff --git a/ycast/my_stations.py b/ycast/my_stations.py index d330f79..2d8328d 100644 --- a/ycast/my_stations.py +++ b/ycast/my_stations.py @@ -44,17 +44,24 @@ def get_station_by_id(uid): def get_stations_yaml(): + from ycast.my_lastheard import get_last_stations_yaml + my_last_station = get_last_stations_yaml() + my_stations = None try: with open(config_file, 'r') as f: my_stations = yaml.safe_load(f) except FileNotFoundError: logging.error("Station configuration '%s' not found", config_file) - return None + except yaml.YAMLError as e: logging.error("Station configuration format error: %s", e) - return None - return my_stations + if my_stations: + if my_last_station: + my_stations.append(my_last_station) + else: + return my_last_station + return my_stations def get_category_directories(): my_stations_yaml = get_stations_yaml() diff --git a/ycast/server.py b/ycast/server.py index 6d963da..f46b2ac 100644 --- a/ycast/server.py +++ b/ycast/server.py @@ -8,7 +8,7 @@ import ycast.my_stations as my_stations import ycast.generic as generic import ycast.station_icons as station_icons - +from ycast.my_lastheard import signalStationSelected PATH_ROOT = 'ycast' PATH_PLAY = 'play' @@ -296,6 +296,7 @@ def get_station_icon(): logging.error("Station icon without station ID requested") abort(400) station = get_station_by_id(stationid) + signalStationSelected(station.name,station.url,station.icon) if not station: logging.error("Could not get station with id '%s'", stationid) abort(404) From a0cba1449ebc089b61fdbf07d6c925707addc750 Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Mon, 24 Jan 2022 20:29:22 +0100 Subject: [PATCH 03/51] refactor: add recently stations in favourites --- ycast/{my_lastheard.py => my_recentlystation.py} | 9 +++++---- ycast/my_stations.py | 10 +++++----- ycast/server.py | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) rename ycast/{my_lastheard.py => my_recentlystation.py} (91%) diff --git a/ycast/my_lastheard.py b/ycast/my_recentlystation.py similarity index 91% rename from ycast/my_lastheard.py rename to ycast/my_recentlystation.py index 9c527bd..7b05755 100644 --- a/ycast/my_lastheard.py +++ b/ycast/my_recentlystation.py @@ -4,8 +4,9 @@ import yaml VAR_PATH = os.path.expanduser("~") + '/.ycast' +MAX_ENTRIES = 15 -config_file = VAR_PATH + '/lastheared.yml' +config_file = VAR_PATH + '/recently.yml' def signalStationSelected(name,url,icon): logging.debug(" %s:%s|%s",name,url,icon) @@ -23,8 +24,8 @@ def signalStationSelected(name,url,icon): piped_icon = '|' + icon list_heared_stations.insert(1,' '+name+': '+url+piped_icon+'\n') - if len(list_heared_stations) > 11: - # remove last + if len(list_heared_stations) > MAX_ENTRIES+1: + # remove last (oldest) entry list_heared_stations.pop() set_stations_yaml(list_heared_stations) @@ -58,7 +59,7 @@ def get_stations_list(): return [] return heared_stations -def get_last_stations_yaml(): +def get_recently_stations_yaml(): try: with open(config_file, 'r') as f: my_stations = yaml.safe_load(f) diff --git a/ycast/my_stations.py b/ycast/my_stations.py index 2d8328d..5bed56c 100644 --- a/ycast/my_stations.py +++ b/ycast/my_stations.py @@ -44,8 +44,8 @@ def get_station_by_id(uid): def get_stations_yaml(): - from ycast.my_lastheard import get_last_stations_yaml - my_last_station = get_last_stations_yaml() + from ycast.my_recentlystation import get_recently_stations_yaml + my_recently_station = get_recently_stations_yaml() my_stations = None try: with open(config_file, 'r') as f: @@ -57,10 +57,10 @@ def get_stations_yaml(): logging.error("Station configuration format error: %s", e) if my_stations: - if my_last_station: - my_stations.append(my_last_station) + if my_recently_station: + my_stations.append(my_recently_station) else: - return my_last_station + return my_recently_station return my_stations def get_category_directories(): diff --git a/ycast/server.py b/ycast/server.py index f46b2ac..3838c08 100644 --- a/ycast/server.py +++ b/ycast/server.py @@ -8,7 +8,7 @@ import ycast.my_stations as my_stations import ycast.generic as generic import ycast.station_icons as station_icons -from ycast.my_lastheard import signalStationSelected +from ycast.my_recentlystation import signalStationSelected PATH_ROOT = 'ycast' PATH_PLAY = 'play' From bda5f1515566725f4d308ab190416047c7b50eeb Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Mon, 24 Jan 2022 20:38:34 +0100 Subject: [PATCH 04/51] bug --- ycast/my_stations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ycast/my_stations.py b/ycast/my_stations.py index 5bed56c..e4afe3a 100644 --- a/ycast/my_stations.py +++ b/ycast/my_stations.py @@ -58,7 +58,7 @@ def get_stations_yaml(): if my_stations: if my_recently_station: - my_stations.append(my_recently_station) + my_stations.update(my_recently_station) else: return my_recently_station return my_stations From 53d88a4e3630f425118f190ef7d13601fb019b05 Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Mon, 24 Jan 2022 22:07:39 +0100 Subject: [PATCH 05/51] init jonnieZG/YCast --- .gitignore | 1 + setup.py | 2 +- ycast/generic.py | 11 +++++++++++ ycast/my_stations.py | 18 +++--------------- ycast/radiobrowser.py | 28 +++++++++++++++++++++------- ycast/server.py | 2 +- ycast/station_icons.py | 4 ++-- 7 files changed, 40 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index 0855b89..741cc2c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ build dist *.egg-info .idea +.vscode *.iml *.pyc diff --git a/setup.py b/setup.py index 9d70bb7..f18a3ce 100644 --- a/setup.py +++ b/setup.py @@ -40,6 +40,6 @@ 'onkyo', 'denon' ], - install_requires=['requests', 'flask', 'PyYAML', 'Pillow'], + install_requires=['requests', 'flask', 'PyYAML', 'Pillow', 'oyaml'], packages=find_packages(exclude=['contrib', 'docs', 'tests']) ) diff --git a/ycast/generic.py b/ycast/generic.py index 0ef42a1..dcbe209 100644 --- a/ycast/generic.py +++ b/ycast/generic.py @@ -1,5 +1,6 @@ import logging import os +import hashlib USER_AGENT = 'YCast' VAR_PATH = os.path.expanduser("~") + '/.ycast' @@ -50,3 +51,13 @@ def get_cache_path(cache_name): logging.error("Could not create cache folders (%s) because of access permissions", cache_path) return None return cache_path + +def get_checksum(feed, charlimit=12): + hash_feed = feed.encode() + hash_object = hashlib.md5(hash_feed) + digest = hash_object.digest() + xor_fold = bytearray(digest[:8]) + for i, b in enumerate(digest[8:]): + xor_fold[i] ^= b + digest_xor_fold = ''.join(format(x, '02x') for x in bytes(xor_fold)) + return digest_xor_fold[:charlimit] diff --git a/ycast/my_stations.py b/ycast/my_stations.py index e4afe3a..13983f7 100644 --- a/ycast/my_stations.py +++ b/ycast/my_stations.py @@ -1,7 +1,5 @@ import logging -import hashlib - -import yaml +import oyaml as yaml import ycast.vtuner as vtuner import ycast.generic as generic @@ -63,6 +61,7 @@ def get_stations_yaml(): return my_recently_station return my_stations + def get_category_directories(): my_stations_yaml = get_stations_yaml() categories = [] @@ -83,17 +82,6 @@ def get_stations_by_category(category): station_icon = None if len(url_list) > 1: station_icon = url_list[1] - station_id = str(get_checksum(station_name + station_url)).upper() + station_id = str(generic.get_checksum(station_name + station_url)).upper() stations.append(Station(station_id, station_name, station_url, category, station_icon)) return stations - - -def get_checksum(feed, charlimit=12): - hash_feed = feed.encode() - hash_object = hashlib.md5(hash_feed) - digest = hash_object.digest() - xor_fold = bytearray(digest[:8]) - for i, b in enumerate(digest[8:]): - xor_fold[i] ^= b - digest_xor_fold = ''.join(format(x, '02x') for x in bytes(xor_fold)) - return digest_xor_fold[:charlimit] diff --git a/ycast/radiobrowser.py b/ycast/radiobrowser.py index fa50541..87c1a2f 100644 --- a/ycast/radiobrowser.py +++ b/ycast/radiobrowser.py @@ -13,6 +13,7 @@ SHOW_BROKEN_STATIONS = False ID_PREFIX = "RB" +id_registry = {} def get_json_attr(json, attr): try: @@ -23,7 +24,7 @@ def get_json_attr(json, attr): class Station: def __init__(self, station_json): - self.id = generic.generate_stationid_with_prefix(get_json_attr(station_json, 'stationuuid'), ID_PREFIX) + self.id = get_json_attr(station_json, 'stationuuid') self.name = get_json_attr(station_json, 'name') self.url = get_json_attr(station_json, 'url') self.icon = get_json_attr(station_json, 'favicon') @@ -35,12 +36,14 @@ def __init__(self, station_json): self.bitrate = get_json_attr(station_json, 'bitrate') def to_vtuner(self): - return vtuner.Station(self.id, self.name, ', '.join(self.tags), self.url, self.icon, + tid = generic.get_checksum(self.id) + id_registry[tid] = self.id + return vtuner.Station(generic.generate_stationid_with_prefix(tid, ID_PREFIX), self.name, ', '.join(self.tags), self.url, self.icon, self.tags[0], self.countrycode, self.codec, self.bitrate, None) def get_playable_url(self): try: - playable_url_json = request('url/' + generic.get_stationid_without_prefix(self.id))[0] + playable_url_json = request('url/' + str(self.id))[0] self.url = playable_url_json['url'] except (IndexError, KeyError): logging.error("Could not retrieve first playlist item for station with id '%s'", self.id) @@ -61,14 +64,18 @@ def request(url): def get_station_by_id(uid): - station_json = request('stations/byuuid/' + str(uid)) - if station_json and len(station_json): - return Station(station_json[0]) - else: + try: + station_json = request('stations/byuuid/' + str(id_registry[uid])) + if station_json and len(station_json): + return Station(station_json[0]) + else: + return None + except KeyError: return None def search(name, limit=DEFAULT_STATION_LIMIT): + id_registry.clear() stations = [] stations_json = request('stations/search?order=name&reverse=false&limit=' + str(limit) + '&name=' + str(name)) for station_json in stations_json: @@ -78,6 +85,7 @@ def search(name, limit=DEFAULT_STATION_LIMIT): def get_country_directories(): + id_registry.clear() country_directories = [] apicall = 'countries' if not SHOW_BROKEN_STATIONS: @@ -92,6 +100,7 @@ def get_country_directories(): def get_language_directories(): + id_registry.clear() language_directories = [] apicall = 'languages' if not SHOW_BROKEN_STATIONS: @@ -107,6 +116,7 @@ def get_language_directories(): def get_genre_directories(): + id_registry.clear() genre_directories = [] apicall = 'tags' if not SHOW_BROKEN_STATIONS: @@ -122,6 +132,7 @@ def get_genre_directories(): def get_stations_by_country(country): + id_registry.clear() stations = [] stations_json = request('stations/search?order=name&reverse=false&countryExact=true&country=' + str(country)) for station_json in stations_json: @@ -131,6 +142,7 @@ def get_stations_by_country(country): def get_stations_by_language(language): + id_registry.clear() stations = [] stations_json = request('stations/search?order=name&reverse=false&languageExact=true&language=' + str(language)) for station_json in stations_json: @@ -140,6 +152,7 @@ def get_stations_by_language(language): def get_stations_by_genre(genre): + id_registry.clear() stations = [] stations_json = request('stations/search?order=name&reverse=false&tagExact=true&tag=' + str(genre)) for station_json in stations_json: @@ -149,6 +162,7 @@ def get_stations_by_genre(genre): def get_stations_by_votes(limit=DEFAULT_STATION_LIMIT): + id_registry.clear() stations = [] stations_json = request('stations?order=votes&reverse=true&limit=' + str(limit)) for station_json in stations_json: diff --git a/ycast/server.py b/ycast/server.py index 3838c08..b7596eb 100644 --- a/ycast/server.py +++ b/ycast/server.py @@ -305,7 +305,7 @@ def get_station_icon(): abort(404) station_icon = station_icons.get_icon(station) if not station_icon: - logging.error("Could not get station icon for station with id '%s'", stationid) + logging.warning("Could not get station icon for station with id '%s'", stationid) abort(404) response = make_response(station_icon) response.headers.set('Content-Type', 'image/jpeg') diff --git a/ycast/station_icons.py b/ycast/station_icons.py index 3c79ce7..e072024 100644 --- a/ycast/station_icons.py +++ b/ycast/station_icons.py @@ -23,10 +23,10 @@ def get_icon(station): try: response = requests.get(station.icon, headers=headers) except requests.exceptions.ConnectionError as err: - logging.error("Connection to station icon URL failed (%s)", err) + logging.debug("Connection to station icon URL failed (%s)", err) return None if response.status_code != 200: - logging.error("Could not get station icon data from %s (HTML status %s)", station.icon, response.status_code) + logging.debug("Could not get station icon data from %s (HTML status %s)", station.icon, response.status_code) return None try: image = Image.open(io.BytesIO(response.content)) From b0bb8c519fa1b29e914a84f4d5c92ffb7ae6b3f0 Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Tue, 25 Jan 2022 07:37:11 +0100 Subject: [PATCH 06/51] correcturen --- ycast/radiobrowser.py | 59 ++++++++++++++++++++----------------------- ycast/server.py | 2 +- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/ycast/radiobrowser.py b/ycast/radiobrowser.py index 87c1a2f..59a09d7 100644 --- a/ycast/radiobrowser.py +++ b/ycast/radiobrowser.py @@ -13,7 +13,7 @@ SHOW_BROKEN_STATIONS = False ID_PREFIX = "RB" -id_registry = {} +station_cache = {} def get_json_attr(json, attr): try: @@ -24,7 +24,8 @@ def get_json_attr(json, attr): class Station: def __init__(self, station_json): - self.id = get_json_attr(station_json, 'stationuuid') + self.stationuuid = get_json_attr(station_json, 'stationuuid') + self.id = generic.get_checksum(self.stationuuid) self.name = get_json_attr(station_json, 'name') self.url = get_json_attr(station_json, 'url') self.icon = get_json_attr(station_json, 'favicon') @@ -36,17 +37,15 @@ def __init__(self, station_json): self.bitrate = get_json_attr(station_json, 'bitrate') def to_vtuner(self): - tid = generic.get_checksum(self.id) - id_registry[tid] = self.id - return vtuner.Station(generic.generate_stationid_with_prefix(tid, ID_PREFIX), self.name, ', '.join(self.tags), self.url, self.icon, + return vtuner.Station(generic.generate_stationid_with_prefix(self.id, ID_PREFIX), self.name, ', '.join(self.tags), self.url, self.icon, self.tags[0], self.countrycode, self.codec, self.bitrate, None) def get_playable_url(self): try: - playable_url_json = request('url/' + str(self.id))[0] + playable_url_json = request('url/' + str(self.stationuuid))[0] self.url = playable_url_json['url'] except (IndexError, KeyError): - logging.error("Could not retrieve first playlist item for station with id '%s'", self.id) + logging.error("Could not retrieve first playlist item for station with id '%s'", self.stationuuid) def request(url): @@ -63,29 +62,21 @@ def request(url): return response.json() -def get_station_by_id(uid): - try: - station_json = request('stations/byuuid/' + str(id_registry[uid])) - if station_json and len(station_json): - return Station(station_json[0]) - else: - return None - except KeyError: - return None - +def get_station_by_id(id): + return station_cache[id] def search(name, limit=DEFAULT_STATION_LIMIT): - id_registry.clear() + station_cache.clear() stations = [] stations_json = request('stations/search?order=name&reverse=false&limit=' + str(limit) + '&name=' + str(name)) for station_json in stations_json: if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == 1: - stations.append(Station(station_json)) + curStation = Station(station_json) + station_cache[curStation.id] = curStation + stations.append(curStation) return stations - def get_country_directories(): - id_registry.clear() country_directories = [] apicall = 'countries' if not SHOW_BROKEN_STATIONS: @@ -100,7 +91,6 @@ def get_country_directories(): def get_language_directories(): - id_registry.clear() language_directories = [] apicall = 'languages' if not SHOW_BROKEN_STATIONS: @@ -116,7 +106,6 @@ def get_language_directories(): def get_genre_directories(): - id_registry.clear() genre_directories = [] apicall = 'tags' if not SHOW_BROKEN_STATIONS: @@ -132,40 +121,48 @@ def get_genre_directories(): def get_stations_by_country(country): - id_registry.clear() + station_cache.clear() stations = [] stations_json = request('stations/search?order=name&reverse=false&countryExact=true&country=' + str(country)) for station_json in stations_json: if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == 1: - stations.append(Station(station_json)) + curStation = Station(station_json) + station_cache[curStation.id] = curStation + stations.append(curStation) return stations def get_stations_by_language(language): - id_registry.clear() + station_cache.clear() stations = [] stations_json = request('stations/search?order=name&reverse=false&languageExact=true&language=' + str(language)) for station_json in stations_json: if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == 1: - stations.append(Station(station_json)) + curStation = Station(station_json) + station_cache[curStation.id] = curStation + stations.append(curStation) return stations def get_stations_by_genre(genre): - id_registry.clear() + station_cache.clear() stations = [] stations_json = request('stations/search?order=name&reverse=false&tagExact=true&tag=' + str(genre)) for station_json in stations_json: if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == 1: - stations.append(Station(station_json)) + curStation = Station(station_json) + station_cache[curStation.id] = curStation + stations.append(curStation) return stations def get_stations_by_votes(limit=DEFAULT_STATION_LIMIT): - id_registry.clear() + station_cache.clear() stations = [] stations_json = request('stations?order=votes&reverse=true&limit=' + str(limit)) for station_json in stations_json: if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == 1: - stations.append(Station(station_json)) + curStation = Station(station_json) + station_cache[curStation.id] = curStation + stations.append(curStation) return stations diff --git a/ycast/server.py b/ycast/server.py index b7596eb..bc701f0 100644 --- a/ycast/server.py +++ b/ycast/server.py @@ -296,10 +296,10 @@ def get_station_icon(): logging.error("Station icon without station ID requested") abort(400) station = get_station_by_id(stationid) - signalStationSelected(station.name,station.url,station.icon) if not station: logging.error("Could not get station with id '%s'", stationid) abort(404) + signalStationSelected(station.name,station.url,station.icon) if not hasattr(station, 'icon') or not station.icon: logging.warning("No icon information found for station with id '%s'", stationid) abort(404) From 36b7f69539c8de5c4b3379d145c326fb2b023826 Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Tue, 25 Jan 2022 08:10:55 +0100 Subject: [PATCH 07/51] only station with FAVICON --- ycast/radiobrowser.py | 49 ++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/ycast/radiobrowser.py b/ycast/radiobrowser.py index 59a09d7..9044a15 100644 --- a/ycast/radiobrowser.py +++ b/ycast/radiobrowser.py @@ -11,14 +11,17 @@ MINIMUM_COUNT_LANGUAGE = 5 DEFAULT_STATION_LIMIT = 200 SHOW_BROKEN_STATIONS = False +SHOW_WITHOUT_FAVICON = False ID_PREFIX = "RB" station_cache = {} + def get_json_attr(json, attr): try: return json[attr] - except: + except Exception as ex: + logging.error("json: attr '%s' not found: %s", attr, ex) return None @@ -37,7 +40,8 @@ def __init__(self, station_json): self.bitrate = get_json_attr(station_json, 'bitrate') def to_vtuner(self): - return vtuner.Station(generic.generate_stationid_with_prefix(self.id, ID_PREFIX), self.name, ', '.join(self.tags), self.url, self.icon, + return vtuner.Station(generic.generate_stationid_with_prefix(self.id, ID_PREFIX), self.name, + ', '.join(self.tags), self.url, self.icon, self.tags[0], self.countrycode, self.codec, self.bitrate, None) def get_playable_url(self): @@ -62,8 +66,9 @@ def request(url): return response.json() -def get_station_by_id(id): - return station_cache[id] +def get_station_by_id(vtune_id): + return station_cache[vtune_id] + def search(name, limit=DEFAULT_STATION_LIMIT): station_cache.clear() @@ -71,11 +76,13 @@ def search(name, limit=DEFAULT_STATION_LIMIT): stations_json = request('stations/search?order=name&reverse=false&limit=' + str(limit) + '&name=' + str(name)) for station_json in stations_json: if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == 1: - curStation = Station(station_json) - station_cache[curStation.id] = curStation - stations.append(curStation) + cur_station = Station(station_json) + if SHOW_WITHOUT_FAVICON or cur_station.icon: + station_cache[cur_station.id] = cur_station + stations.append(cur_station) return stations + def get_country_directories(): country_directories = [] apicall = 'countries' @@ -126,9 +133,10 @@ def get_stations_by_country(country): stations_json = request('stations/search?order=name&reverse=false&countryExact=true&country=' + str(country)) for station_json in stations_json: if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == 1: - curStation = Station(station_json) - station_cache[curStation.id] = curStation - stations.append(curStation) + cur_station = Station(station_json) + if SHOW_WITHOUT_FAVICON or cur_station.icon: + station_cache[cur_station.id] = cur_station + stations.append(cur_station) return stations @@ -138,9 +146,10 @@ def get_stations_by_language(language): stations_json = request('stations/search?order=name&reverse=false&languageExact=true&language=' + str(language)) for station_json in stations_json: if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == 1: - curStation = Station(station_json) - station_cache[curStation.id] = curStation - stations.append(curStation) + cur_station = Station(station_json) + if SHOW_WITHOUT_FAVICON or cur_station.icon: + station_cache[cur_station.id] = cur_station + stations.append(cur_station) return stations @@ -150,9 +159,10 @@ def get_stations_by_genre(genre): stations_json = request('stations/search?order=name&reverse=false&tagExact=true&tag=' + str(genre)) for station_json in stations_json: if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == 1: - curStation = Station(station_json) - station_cache[curStation.id] = curStation - stations.append(curStation) + cur_station = Station(station_json) + if SHOW_WITHOUT_FAVICON or cur_station.icon: + station_cache[cur_station.id] = cur_station + stations.append(cur_station) return stations @@ -162,7 +172,8 @@ def get_stations_by_votes(limit=DEFAULT_STATION_LIMIT): stations_json = request('stations?order=votes&reverse=true&limit=' + str(limit)) for station_json in stations_json: if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == 1: - curStation = Station(station_json) - station_cache[curStation.id] = curStation - stations.append(curStation) + cur_station = Station(station_json) + if SHOW_WITHOUT_FAVICON or cur_station.icon: + station_cache[cur_station.id] = cur_station + stations.append(cur_station) return stations From a490e13b99485a71825c698c1db886572428486c Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Tue, 25 Jan 2022 13:23:54 +0100 Subject: [PATCH 08/51] refactor --- ycast/generic.py | 10 ++-------- ycast/my_recentlystation.py | 18 ++++++++++++------ ycast/my_stations.py | 6 +++--- ycast/radiobrowser.py | 4 ++-- ycast/server.py | 8 ++++---- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/ycast/generic.py b/ycast/generic.py index dcbe209..8a130e1 100644 --- a/ycast/generic.py +++ b/ycast/generic.py @@ -34,13 +34,6 @@ def get_stationid_prefix(uid): return uid[:2] -def get_stationid_without_prefix(uid): - if len(uid) < 4: - logging.error("Could not extract stationid (Invalid station id length)") - return None - return uid[3:] - - def get_cache_path(cache_name): cache_path = CACHE_PATH + '/' + cache_name try: @@ -52,6 +45,7 @@ def get_cache_path(cache_name): return None return cache_path + def get_checksum(feed, charlimit=12): hash_feed = feed.encode() hash_object = hashlib.md5(hash_feed) @@ -60,4 +54,4 @@ def get_checksum(feed, charlimit=12): for i, b in enumerate(digest[8:]): xor_fold[i] ^= b digest_xor_fold = ''.join(format(x, '02x') for x in bytes(xor_fold)) - return digest_xor_fold[:charlimit] + return str(digest_xor_fold[:charlimit]).upper() diff --git a/ycast/my_recentlystation.py b/ycast/my_recentlystation.py index 7b05755..4214c3d 100644 --- a/ycast/my_recentlystation.py +++ b/ycast/my_recentlystation.py @@ -8,28 +8,32 @@ config_file = VAR_PATH + '/recently.yml' -def signalStationSelected(name,url,icon): - logging.debug(" %s:%s|%s",name,url,icon) + +def signal_station_selected(name, url, icon): + logging.debug(" %s:%s|%s", name, url, icon) list_heared_stations = get_stations_list() - if len(list_heared_stations) == 0 : + if len(list_heared_stations) == 0: list_heared_stations.append("recently used:\n") + # make name yaml - like + name = name.replace(":", " -") for line in list_heared_stations: - elements = line.split(':') + elements = line.split(': ') if elements[0] == ' '+name: list_heared_stations.remove(line) - logging.debug("Name '%s' exists and deleted",name) + logging.debug("Name '%s' exists and deleted", name) piped_icon = '' if icon and len(icon) > 0: piped_icon = '|' + icon - list_heared_stations.insert(1,' '+name+': '+url+piped_icon+'\n') + list_heared_stations.insert(1, ' '+name+': '+url+piped_icon+'\n') if len(list_heared_stations) > MAX_ENTRIES+1: # remove last (oldest) entry list_heared_stations.pop() set_stations_yaml(list_heared_stations) + def set_stations_yaml(heared_stations): try: os.makedirs(VAR_PATH) @@ -47,6 +51,7 @@ def set_stations_yaml(heared_stations): except Exception as ex: logging.error("File not written '%s': %s", config_file, ex) + def get_stations_list(): try: with open(config_file, 'r') as f: @@ -59,6 +64,7 @@ def get_stations_list(): return [] return heared_stations + def get_recently_stations_yaml(): try: with open(config_file, 'r') as f: diff --git a/ycast/my_stations.py b/ycast/my_stations.py index 13983f7..8a36b80 100644 --- a/ycast/my_stations.py +++ b/ycast/my_stations.py @@ -31,12 +31,12 @@ def set_config(config): return False -def get_station_by_id(uid): +def get_station_by_id(vtune_id): my_stations_yaml = get_stations_yaml() if my_stations_yaml: for category in my_stations_yaml: for station in get_stations_by_category(category): - if uid == generic.get_stationid_without_prefix(station.id): + if vtune_id == station.id: return station return None @@ -82,6 +82,6 @@ def get_stations_by_category(category): station_icon = None if len(url_list) > 1: station_icon = url_list[1] - station_id = str(generic.get_checksum(station_name + station_url)).upper() + station_id = generic.get_checksum(station_name + station_url) stations.append(Station(station_id, station_name, station_url, category, station_icon)) return stations diff --git a/ycast/radiobrowser.py b/ycast/radiobrowser.py index 9044a15..eb495f6 100644 --- a/ycast/radiobrowser.py +++ b/ycast/radiobrowser.py @@ -28,7 +28,7 @@ def get_json_attr(json, attr): class Station: def __init__(self, station_json): self.stationuuid = get_json_attr(station_json, 'stationuuid') - self.id = generic.get_checksum(self.stationuuid) + self.id = generic.generate_stationid_with_prefix(generic.get_checksum(self.stationuuid), ID_PREFIX) self.name = get_json_attr(station_json, 'name') self.url = get_json_attr(station_json, 'url') self.icon = get_json_attr(station_json, 'favicon') @@ -40,7 +40,7 @@ def __init__(self, station_json): self.bitrate = get_json_attr(station_json, 'bitrate') def to_vtuner(self): - return vtuner.Station(generic.generate_stationid_with_prefix(self.id, ID_PREFIX), self.name, + return vtuner.Station(self.id, self.name, ', '.join(self.tags), self.url, self.icon, self.tags[0], self.countrycode, self.codec, self.bitrate, None) diff --git a/ycast/server.py b/ycast/server.py index bc701f0..9101428 100644 --- a/ycast/server.py +++ b/ycast/server.py @@ -8,7 +8,7 @@ import ycast.my_stations as my_stations import ycast.generic as generic import ycast.station_icons as station_icons -from ycast.my_recentlystation import signalStationSelected +from ycast.my_recentlystation import signal_station_selected PATH_ROOT = 'ycast' PATH_PLAY = 'play' @@ -102,9 +102,9 @@ def get_paged_elements(items, requestargs): def get_station_by_id(stationid, additional_info=False): station_id_prefix = generic.get_stationid_prefix(stationid) if station_id_prefix == my_stations.ID_PREFIX: - return my_stations.get_station_by_id(generic.get_stationid_without_prefix(stationid)) + return my_stations.get_station_by_id(stationid) elif station_id_prefix == radiobrowser.ID_PREFIX: - station = radiobrowser.get_station_by_id(generic.get_stationid_without_prefix(stationid)) + station = radiobrowser.get_station_by_id(stationid) if additional_info: station.get_playable_url() return station @@ -299,7 +299,7 @@ def get_station_icon(): if not station: logging.error("Could not get station with id '%s'", stationid) abort(404) - signalStationSelected(station.name,station.url,station.icon) + signal_station_selected(station.name, station.url, station.icon) if not hasattr(station, 'icon') or not station.icon: logging.warning("No icon information found for station with id '%s'", stationid) abort(404) From 7f76a50b3636b06a8fed6f5f6eb7942251e3cbdd Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Tue, 25 Jan 2022 17:39:29 +0100 Subject: [PATCH 09/51] refactor --- ycast/generic.py | 25 +++++++++++++++++++++++-- ycast/my_recentlystation.py | 18 +++++++++--------- ycast/my_stations.py | 2 +- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/ycast/generic.py b/ycast/generic.py index 8a130e1..8801172 100644 --- a/ycast/generic.py +++ b/ycast/generic.py @@ -5,7 +5,7 @@ USER_AGENT = 'YCast' VAR_PATH = os.path.expanduser("~") + '/.ycast' CACHE_PATH = VAR_PATH + '/cache' - +FILTER_PATH = VAR_PATH + '/filter' class Directory: def __init__(self, name, item_count, displayname=None): @@ -35,7 +35,9 @@ def get_stationid_prefix(uid): def get_cache_path(cache_name): - cache_path = CACHE_PATH + '/' + cache_name + cache_path = CACHE_PATH + if cache_name: + cache_path = CACHE_PATH + '/' + cache_name try: os.makedirs(cache_path) except FileExistsError: @@ -45,6 +47,25 @@ def get_cache_path(cache_name): return None return cache_path +def get_filter_path(): + try: + os.makedirs(FILTER_PATH) + except FileExistsError: + pass + except PermissionError: + logging.error("Could not create cache folders (%s) because of access permissions", cache_path) + return None + return FILTER_PATH + +def get_var_path(): + try: + os.makedirs(VAR_PATH) + except FileExistsError: + pass + except PermissionError: + logging.error("Could not create cache folders (%s) because of access permissions", cache_path) + return None + return VAR_PATH def get_checksum(feed, charlimit=12): hash_feed = feed.encode() diff --git a/ycast/my_recentlystation.py b/ycast/my_recentlystation.py index 4214c3d..7fccb12 100644 --- a/ycast/my_recentlystation.py +++ b/ycast/my_recentlystation.py @@ -2,11 +2,11 @@ import os import yaml +from ycast import generic -VAR_PATH = os.path.expanduser("~") + '/.ycast' MAX_ENTRIES = 15 -config_file = VAR_PATH + '/recently.yml' +recently_file = generic.get_var_path() + '/recently.yml' def signal_station_selected(name, url, icon): @@ -44,20 +44,20 @@ def set_stations_yaml(heared_stations): return None try: - with open(config_file, 'w') as f: + with open(recently_file, 'w') as f: f.writelines(heared_stations) - logging.info("File written '%s'", config_file) + logging.info("File written '%s'", recently_file) except Exception as ex: - logging.error("File not written '%s': %s", config_file, ex) + logging.error("File not written '%s': %s", recently_file, ex) def get_stations_list(): try: - with open(config_file, 'r') as f: + with open(recently_file, 'r') as f: heared_stations = f.readlines() except FileNotFoundError: - logging.warning("File not found '%s' not found", config_file) + logging.warning("File not found '%s' not found", recently_file) return [] except yaml.YAMLError as e: logging.error("Station configuration format error: %s", e) @@ -67,10 +67,10 @@ def get_stations_list(): def get_recently_stations_yaml(): try: - with open(config_file, 'r') as f: + with open(recently_file, 'r') as f: my_stations = yaml.safe_load(f) except FileNotFoundError: - logging.error("Station configuration '%s' not found", config_file) + logging.error("Station configuration '%s' not found", recently_file) return None except yaml.YAMLError as e: logging.error("Station configuration format error: %s", e) diff --git a/ycast/my_stations.py b/ycast/my_stations.py index 8a36b80..2a820d3 100644 --- a/ycast/my_stations.py +++ b/ycast/my_stations.py @@ -6,7 +6,7 @@ ID_PREFIX = "MY" -config_file = 'stations.yml' +config_file = generic.get_var_path() + '/stations.yml' class Station: From e50c93e72a3a645ca1a31467e87408e43c22facc Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Tue, 25 Jan 2022 17:57:23 +0100 Subject: [PATCH 10/51] refactor --- ycast/my_recentlystation.py | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/ycast/my_recentlystation.py b/ycast/my_recentlystation.py index 7fccb12..a78a200 100644 --- a/ycast/my_recentlystation.py +++ b/ycast/my_recentlystation.py @@ -1,5 +1,4 @@ import logging -import os import yaml from ycast import generic @@ -11,41 +10,33 @@ def signal_station_selected(name, url, icon): logging.debug(" %s:%s|%s", name, url, icon) - list_heared_stations = get_stations_list() - if len(list_heared_stations) == 0: - list_heared_stations.append("recently used:\n") + list_heard_stations = get_stations_list() + if len(list_heard_stations) == 0: + list_heard_stations.append("recently used:\n") # make name yaml - like name = name.replace(":", " -") - for line in list_heared_stations: + for line in list_heard_stations: elements = line.split(': ') if elements[0] == ' '+name: - list_heared_stations.remove(line) + list_heard_stations.remove(line) logging.debug("Name '%s' exists and deleted", name) piped_icon = '' if icon and len(icon) > 0: piped_icon = '|' + icon - list_heared_stations.insert(1, ' '+name+': '+url+piped_icon+'\n') - if len(list_heared_stations) > MAX_ENTRIES+1: + list_heard_stations.insert(1, ' '+name+': '+url+piped_icon+'\n') + if len(list_heard_stations) > MAX_ENTRIES+1: # remove last (oldest) entry - list_heared_stations.pop() + list_heard_stations.pop() - set_stations_yaml(list_heared_stations) + set_stations_yaml(list_heard_stations) -def set_stations_yaml(heared_stations): - try: - os.makedirs(VAR_PATH) - except FileExistsError: - pass - except PermissionError: - logging.error("Could not create folders (%s) because of access permissions", VAR_PATH) - return None - +def set_stations_yaml(heard_stations): try: with open(recently_file, 'w') as f: - f.writelines(heared_stations) + f.writelines(heard_stations) logging.info("File written '%s'", recently_file) except Exception as ex: @@ -55,14 +46,14 @@ def set_stations_yaml(heared_stations): def get_stations_list(): try: with open(recently_file, 'r') as f: - heared_stations = f.readlines() + heard_stations = f.readlines() except FileNotFoundError: logging.warning("File not found '%s' not found", recently_file) return [] except yaml.YAMLError as e: logging.error("Station configuration format error: %s", e) return [] - return heared_stations + return heard_stations def get_recently_stations_yaml(): From 4bc3bed4872bfa4ca36d8c94b5baabcdc66c4795 Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Wed, 26 Jan 2022 20:40:05 +0100 Subject: [PATCH 11/51] filter.yml get global filter on station_lists --- ycast/filter.py | 98 +++++++++++++++++++++++++++++ ycast/generic.py | 64 +++++++++++++++---- ycast/my_recentlystation.py | 33 +--------- ycast/my_stations.py | 12 +--- ycast/radiobrowser.py | 122 +++++++++++++++++++----------------- ycast/test_filter.py | 44 +++++++++++++ 6 files changed, 265 insertions(+), 108 deletions(-) create mode 100644 ycast/filter.py create mode 100644 ycast/test_filter.py diff --git a/ycast/filter.py b/ycast/filter.py new file mode 100644 index 0000000..2c9df36 --- /dev/null +++ b/ycast/filter.py @@ -0,0 +1,98 @@ +import logging + +from ycast import generic +from ycast.generic import get_json_attr + +white_list = {} +black_list = {} +filter_dir = {} +parameter_failed_list = {} + +def init_filter(): + global white_list + global black_list + global parameter_failed_list + global filter_dir + filter_dir = generic.read_yaml_file(generic.get_var_path() + '/filter.yml') + if filter_dir: + white_list = filter_dir['whitelist'] + black_list = filter_dir['blacklist'] + else: + white_list = { 'lastcheckok': 1 } + black_list = {} + filter_dir = {} + filter_dir['whitelist'] = white_list + filter_dir['blacklist'] = black_list + generic.write_yaml_file(generic.get_var_path() + '/filter.yml', filter_dir) + + filter_ex = {} + filter_ex['whitelist'] = {'lastcheckok': 1, 'countrycode': 'DE,US,NO,GB', 'languagecodes': 'en,no'} + filter_ex['blacklist'] = {'favicon': ''} + generic.write_yaml_file(generic.get_var_path() + '/filter.yml_example', filter_ex) + + parameter_failed_list.clear() + return + +def end_filter(): + if parameter_failed_list: + logging.info("Used filter parameter: %s", parameter_failed_list) + else: + logging.info("Used filter parameter: ") + +def parameter_hit(param_name): + count = 1 + old = None + if parameter_failed_list: + old = parameter_failed_list.get(param_name) + if old: + count = old + 1 + parameter_failed_list[param_name] = count + +def check_station(station_json): + station_name = get_json_attr(station_json, 'name') + if not station_name: + # müll response + logging.debug(station_json) + return False +# oder verknüpft + if black_list: + black_list_hit = False + for param_name in black_list: + unvalid_elements = black_list[param_name] + val = get_json_attr(station_json, param_name) + if not val == None: + # attribut in json vorhanden + if unvalid_elements: + if val: + pos = unvalid_elements.find(val) + black_list_hit = pos >= 0 + else: + if not val: + black_list_hit = True + if black_list_hit: + parameter_hit(param_name) + logging.debug("FAIL '%s' blacklist hit on '%s' '%s' == '%s'", + station_name, param_name, unvalid_elements, val) + return False + +# und verknüpft + if white_list: + white_list_hit = True + for param_name in white_list: + val = get_json_attr(station_json, param_name) + if not val == None: + # attribut in json vorhanden + valid_elements = white_list[param_name] + if type(val) is int: + white_list_hit = val == valid_elements + else: + if val: + pos = valid_elements.find(val) + white_list_hit = pos >=0 + if not white_list_hit: + parameter_hit(param_name) + logging.debug("FAIL '%s' whitelist failed on '%s' '%s' == '%s'", + station_name, param_name, valid_elements, val) + return False + logging.debug("OK '%s' passed", station_name) + return True diff --git a/ycast/generic.py b/ycast/generic.py index 8801172..023ee47 100644 --- a/ycast/generic.py +++ b/ycast/generic.py @@ -1,11 +1,12 @@ import logging import os import hashlib +import yaml USER_AGENT = 'YCast' VAR_PATH = os.path.expanduser("~") + '/.ycast' CACHE_PATH = VAR_PATH + '/cache' -FILTER_PATH = VAR_PATH + '/filter' + class Directory: def __init__(self, name, item_count, displayname=None): @@ -47,15 +48,6 @@ def get_cache_path(cache_name): return None return cache_path -def get_filter_path(): - try: - os.makedirs(FILTER_PATH) - except FileExistsError: - pass - except PermissionError: - logging.error("Could not create cache folders (%s) because of access permissions", cache_path) - return None - return FILTER_PATH def get_var_path(): try: @@ -63,10 +55,11 @@ def get_var_path(): except FileExistsError: pass except PermissionError: - logging.error("Could not create cache folders (%s) because of access permissions", cache_path) + logging.error("Could not create cache folders (%s) because of access permissions", VAR_PATH) return None return VAR_PATH + def get_checksum(feed, charlimit=12): hash_feed = feed.encode() hash_object = hashlib.md5(hash_feed) @@ -76,3 +69,52 @@ def get_checksum(feed, charlimit=12): xor_fold[i] ^= b digest_xor_fold = ''.join(format(x, '02x') for x in bytes(xor_fold)) return str(digest_xor_fold[:charlimit]).upper() + + +def read_yaml_file(file_name): + try: + with open(file_name, 'r') as f: + return yaml.safe_load(f) + except FileNotFoundError: + logging.error("YAML file '%s' not found", file_name) + except yaml.YAMLError as e: + logging.error("YAML format error in '%':\n %s", file_name, e) + return None + + +def write_yaml_file(file_name, dictionary): + try: + with open(file_name, 'w') as f: + yaml.dump(dictionary, f) + except yaml.YAMLError as e: + logging.error("YAML format error in '%':\n %s", file_name, e) + except Exception as ex: + logging.error("File not written '%s':\n %s", file_name, ex) + + +def readlns_txt_file(file_name): + try: + with open(file_name, 'r') as f: + return f.readlines() + except FileNotFoundError: + logging.error("YAML file '%s' not found", file_name) + except yaml.YAMLError as e: + logging.error("YAML format error in '%':\n %s", file_name, e) + return None + + +def writelns_txt_file(file_name, line_list): + try: + with open(file_name, 'w') as f: + f.writelines(line_list) + logging.info("File written '%s'", file_name) + except Exception as ex: + logging.error("File not written '%s':\n %s", file_name, ex) + + +def get_json_attr(json, attr): + try: + return json[attr] + except Exception as ex: + logging.debug("json: attr '%s' not found: %s", attr, ex) + return None diff --git a/ycast/my_recentlystation.py b/ycast/my_recentlystation.py index a78a200..5ed61c0 100644 --- a/ycast/my_recentlystation.py +++ b/ycast/my_recentlystation.py @@ -1,10 +1,7 @@ import logging - -import yaml from ycast import generic MAX_ENTRIES = 15 - recently_file = generic.get_var_path() + '/recently.yml' @@ -34,36 +31,12 @@ def signal_station_selected(name, url, icon): def set_stations_yaml(heard_stations): - try: - with open(recently_file, 'w') as f: - f.writelines(heard_stations) - logging.info("File written '%s'", recently_file) - - except Exception as ex: - logging.error("File not written '%s': %s", recently_file, ex) + generic.writelns_txt_file(recently_file, heard_stations) def get_stations_list(): - try: - with open(recently_file, 'r') as f: - heard_stations = f.readlines() - except FileNotFoundError: - logging.warning("File not found '%s' not found", recently_file) - return [] - except yaml.YAMLError as e: - logging.error("Station configuration format error: %s", e) - return [] - return heard_stations + return generic.readlns_txt_file(recently_file) def get_recently_stations_yaml(): - try: - with open(recently_file, 'r') as f: - my_stations = yaml.safe_load(f) - except FileNotFoundError: - logging.error("Station configuration '%s' not found", recently_file) - return None - except yaml.YAMLError as e: - logging.error("Station configuration format error: %s", e) - return None - return my_stations + return generic.read_yaml_file(recently_file) diff --git a/ycast/my_stations.py b/ycast/my_stations.py index 2a820d3..cba5dd1 100644 --- a/ycast/my_stations.py +++ b/ycast/my_stations.py @@ -1,5 +1,4 @@ import logging -import oyaml as yaml import ycast.vtuner as vtuner import ycast.generic as generic @@ -44,16 +43,7 @@ def get_station_by_id(vtune_id): def get_stations_yaml(): from ycast.my_recentlystation import get_recently_stations_yaml my_recently_station = get_recently_stations_yaml() - my_stations = None - try: - with open(config_file, 'r') as f: - my_stations = yaml.safe_load(f) - except FileNotFoundError: - logging.error("Station configuration '%s' not found", config_file) - - except yaml.YAMLError as e: - logging.error("Station configuration format error: %s", e) - + my_stations = generic.read_yaml_file(config_file) if my_stations: if my_recently_station: my_stations.update(my_recently_station) diff --git a/ycast/radiobrowser.py b/ycast/radiobrowser.py index eb495f6..36e3a3b 100644 --- a/ycast/radiobrowser.py +++ b/ycast/radiobrowser.py @@ -4,6 +4,8 @@ from ycast import __version__ import ycast.vtuner as vtuner import ycast.generic as generic +from ycast.filter import check_station, init_filter, end_filter +from ycast.generic import get_json_attr API_ENDPOINT = "http://all.api.radio-browser.info" MINIMUM_COUNT_GENRE = 5 @@ -17,27 +19,20 @@ station_cache = {} -def get_json_attr(json, attr): - try: - return json[attr] - except Exception as ex: - logging.error("json: attr '%s' not found: %s", attr, ex) - return None - - class Station: def __init__(self, station_json): - self.stationuuid = get_json_attr(station_json, 'stationuuid') + self.stationuuid = generic.get_json_attr(station_json, 'stationuuid') self.id = generic.generate_stationid_with_prefix(generic.get_checksum(self.stationuuid), ID_PREFIX) - self.name = get_json_attr(station_json, 'name') - self.url = get_json_attr(station_json, 'url') - self.icon = get_json_attr(station_json, 'favicon') - self.tags = get_json_attr(station_json, 'tags').split(',') - self.countrycode = get_json_attr(station_json, 'countrycode') - self.language = get_json_attr(station_json, 'language') - self.votes = get_json_attr(station_json, 'votes') - self.codec = get_json_attr(station_json, 'codec') - self.bitrate = get_json_attr(station_json, 'bitrate') + self.name = generic.get_json_attr(station_json, 'name') + self.url = generic.get_json_attr(station_json, 'url') + self.icon = generic.get_json_attr(station_json, 'favicon') + self.tags = generic.get_json_attr(station_json, 'tags').split(',') + self.countrycode = generic.get_json_attr(station_json, 'countrycode') + self.language = generic.get_json_attr(station_json, 'language') + self.languagecodes = generic.get_json_attr(station_json, 'languagecodes') + self.votes = generic.get_json_attr(station_json, 'votes') + self.codec = generic.get_json_attr(station_json, 'codec') + self.bitrate = generic.get_json_attr(station_json, 'bitrate') def to_vtuner(self): return vtuner.Station(self.id, self.name, @@ -67,23 +62,13 @@ def request(url): def get_station_by_id(vtune_id): - return station_cache[vtune_id] - - -def search(name, limit=DEFAULT_STATION_LIMIT): - station_cache.clear() - stations = [] - stations_json = request('stations/search?order=name&reverse=false&limit=' + str(limit) + '&name=' + str(name)) - for station_json in stations_json: - if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == 1: - cur_station = Station(station_json) - if SHOW_WITHOUT_FAVICON or cur_station.icon: - station_cache[cur_station.id] = cur_station - stations.append(cur_station) - return stations + if station_cache: + return station_cache[vtune_id] + return None def get_country_directories(): + init_filter() country_directories = [] apicall = 'countries' if not SHOW_BROKEN_STATIONS: @@ -98,6 +83,7 @@ def get_country_directories(): def get_language_directories(): + init_filter() language_directories = [] apicall = 'languages' if not SHOW_BROKEN_STATIONS: @@ -128,52 +114,76 @@ def get_genre_directories(): def get_stations_by_country(country): + init_filter() station_cache.clear() stations = [] - stations_json = request('stations/search?order=name&reverse=false&countryExact=true&country=' + str(country)) - for station_json in stations_json: - if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == 1: + stations_list_json = request('stations/search?order=name&reverse=false&countryExact=true&country=' + str(country)) + for station_json in stations_list_json: + if check_station(station_json): cur_station = Station(station_json) - if SHOW_WITHOUT_FAVICON or cur_station.icon: - station_cache[cur_station.id] = cur_station - stations.append(cur_station) + station_cache[cur_station.id] = cur_station + stations.append(cur_station) + logging.info("Stations (%d/%d)", len(stations), len(stations_list_json)) + end_filter() return stations def get_stations_by_language(language): + init_filter() station_cache.clear() stations = [] - stations_json = request('stations/search?order=name&reverse=false&languageExact=true&language=' + str(language)) - for station_json in stations_json: - if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == 1: + stations_list_json = \ + request('stations/search?order=name&reverse=false&languageExact=true&language=' + str(language)) + for station_json in stations_list_json: + if check_station(station_json): cur_station = Station(station_json) - if SHOW_WITHOUT_FAVICON or cur_station.icon: - station_cache[cur_station.id] = cur_station - stations.append(cur_station) + station_cache[cur_station.id] = cur_station + stations.append(cur_station) + logging.info("Stations (%d/%d)", len(stations), len(stations_list_json)) + end_filter() return stations def get_stations_by_genre(genre): + init_filter() station_cache.clear() stations = [] - stations_json = request('stations/search?order=name&reverse=false&tagExact=true&tag=' + str(genre)) - for station_json in stations_json: - if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == 1: + stations_list_json = request('stations/search?order=name&reverse=false&tagExact=true&tag=' + str(genre)) + for station_json in stations_list_json: + if check_station(station_json): cur_station = Station(station_json) - if SHOW_WITHOUT_FAVICON or cur_station.icon: - station_cache[cur_station.id] = cur_station - stations.append(cur_station) + station_cache[cur_station.id] = cur_station + stations.append(cur_station) + logging.info("Stations (%d/%d)", len(stations), len(stations_list_json)) + end_filter() return stations def get_stations_by_votes(limit=DEFAULT_STATION_LIMIT): + init_filter() + station_cache.clear() + stations = [] + stations_list_json = request('stations?order=votes&reverse=true&limit=' + str(limit)) + for station_json in stations_list_json: + if check_station(station_json): + cur_station = Station(station_json) + station_cache[cur_station.id] = cur_station + stations.append(cur_station) + logging.info("Stations (%d/%d)", len(stations), len(stations_list_json)) + end_filter() + return stations + + +def search(name, limit=DEFAULT_STATION_LIMIT): + init_filter() station_cache.clear() stations = [] - stations_json = request('stations?order=votes&reverse=true&limit=' + str(limit)) - for station_json in stations_json: - if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == 1: + stations_list_json = request('stations/search?order=name&reverse=false&limit=' + str(limit) + '&name=' + str(name)) + for station_json in stations_list_json: + if check_station(station_json): cur_station = Station(station_json) - if SHOW_WITHOUT_FAVICON or cur_station.icon: - station_cache[cur_station.id] = cur_station - stations.append(cur_station) + station_cache[cur_station.id] = cur_station + stations.append(cur_station) + logging.info("Stations (%d/%d)", len(stations), len(stations_list_json)) + end_filter() return stations diff --git a/ycast/test_filter.py b/ycast/test_filter.py new file mode 100644 index 0000000..78564b7 --- /dev/null +++ b/ycast/test_filter.py @@ -0,0 +1,44 @@ +import json +import logging +import unittest +from io import StringIO + +import filter +import generic +from ycast import radiobrowser + +class MyTestCase(unittest.TestCase): + + logging.getLogger().setLevel(logging.DEBUG) + + def test_init_filter(self): + filter.init_filter() + + for elem in filter.filter_dir: + logging.warning("Name filtertype: %s", elem) + filter_param = filter.filter_dir[elem] + if filter_param: + for par in filter_param: + logging.warning(" Name paramter: %s",par) + else: + logging.warning(" ") + + + def test_valid_station(self): + filter.init_filter() + test_lines = generic.readlns_txt_file(generic.get_var_path()+"/test.json") + io = StringIO(test_lines[0]) + stations_json = json.load(io) + count = 0 + for station_json in stations_json: + if filter.check_station(station_json): + station = radiobrowser.Station(station_json) + count = count + 1 + + logging.info("Stations (%d/%d)" , count, len(stations_json) ) + logging.info("Used filter parameter", filter.parameter_failed_list) + + +if __name__ == '__main__': + unittest.main() + From 4d2a8b219fbaa9a2518b6d714f697a7bb1877b33 Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Thu, 27 Jan 2022 15:39:01 +0100 Subject: [PATCH 12/51] using bas64 coded uuid, favicon refernced as favicon-url --- examples/filter.yml.example | 6 ++++++ ycast/filter.py | 31 ++++++++++++++----------------- ycast/generic.py | 7 +++++++ ycast/radiobrowser.py | 22 ++++++++++++++++++++-- ycast/station_icons.py | 7 +++++-- 5 files changed, 52 insertions(+), 21 deletions(-) create mode 100644 examples/filter.yml.example diff --git a/examples/filter.yml.example b/examples/filter.yml.example new file mode 100644 index 0000000..ce4fd1f --- /dev/null +++ b/examples/filter.yml.example @@ -0,0 +1,6 @@ +blacklist: + favicon: '' +whitelist: + countrycode: DE,US,NO,GB + languagecodes: en,no + lastcheckok: 1 diff --git a/ycast/filter.py b/ycast/filter.py index 2c9df36..b5fba1e 100644 --- a/ycast/filter.py +++ b/ycast/filter.py @@ -8,6 +8,7 @@ filter_dir = {} parameter_failed_list = {} + def init_filter(): global white_list global black_list @@ -18,27 +19,22 @@ def init_filter(): white_list = filter_dir['whitelist'] black_list = filter_dir['blacklist'] else: - white_list = { 'lastcheckok': 1 } + white_list = {'lastcheckok': 1} black_list = {} - filter_dir = {} - filter_dir['whitelist'] = white_list - filter_dir['blacklist'] = black_list + filter_dir = {'whitelist': white_list, 'blacklist': black_list} generic.write_yaml_file(generic.get_var_path() + '/filter.yml', filter_dir) - filter_ex = {} - filter_ex['whitelist'] = {'lastcheckok': 1, 'countrycode': 'DE,US,NO,GB', 'languagecodes': 'en,no'} - filter_ex['blacklist'] = {'favicon': ''} - generic.write_yaml_file(generic.get_var_path() + '/filter.yml_example', filter_ex) - parameter_failed_list.clear() return + def end_filter(): if parameter_failed_list: logging.info("Used filter parameter: %s", parameter_failed_list) else: logging.info("Used filter parameter: ") + def parameter_hit(param_name): count = 1 old = None @@ -48,6 +44,7 @@ def parameter_hit(param_name): count = old + 1 parameter_failed_list[param_name] = count + def check_station(station_json): station_name = get_json_attr(station_json, 'name') if not station_name: @@ -59,7 +56,7 @@ def check_station(station_json): black_list_hit = False for param_name in black_list: unvalid_elements = black_list[param_name] - val = get_json_attr(station_json, param_name) + val = get_json_attr(station_json, param_name) if not val == None: # attribut in json vorhanden if unvalid_elements: @@ -71,15 +68,15 @@ def check_station(station_json): black_list_hit = True if black_list_hit: parameter_hit(param_name) - logging.debug("FAIL '%s' blacklist hit on '%s' '%s' == '%s'", - station_name, param_name, unvalid_elements, val) +# logging.debug("FAIL '%s' blacklist hit on '%s' '%s' == '%s'", +# station_name, param_name, unvalid_elements, val) return False # und verknüpft if white_list: white_list_hit = True for param_name in white_list: - val = get_json_attr(station_json, param_name) + val = get_json_attr(station_json, param_name) if not val == None: # attribut in json vorhanden valid_elements = white_list[param_name] @@ -88,11 +85,11 @@ def check_station(station_json): else: if val: pos = valid_elements.find(val) - white_list_hit = pos >=0 + white_list_hit = pos >= 0 if not white_list_hit: parameter_hit(param_name) - logging.debug("FAIL '%s' whitelist failed on '%s' '%s' == '%s'", - station_name, param_name, valid_elements, val) +# logging.debug("FAIL '%s' whitelist failed on '%s' '%s' == '%s'", +# station_name, param_name, valid_elements, val) return False - logging.debug("OK '%s' passed", station_name) +# logging.debug("OK '%s' passed", station_name) return True diff --git a/ycast/generic.py b/ycast/generic.py index 023ee47..bc126fe 100644 --- a/ycast/generic.py +++ b/ycast/generic.py @@ -35,6 +35,13 @@ def get_stationid_prefix(uid): return uid[:2] +def get_stationid_without_prefix(uid): + if len(uid) < 4: + logging.error("Could not extract stationid (Invalid station id length)") + return None + return uid[3:] + + def get_cache_path(cache_name): cache_path = CACHE_PATH if cache_name: diff --git a/ycast/radiobrowser.py b/ycast/radiobrowser.py index 36e3a3b..45f1adb 100644 --- a/ycast/radiobrowser.py +++ b/ycast/radiobrowser.py @@ -1,3 +1,6 @@ +import base64 +import uuid + import requests import logging @@ -22,7 +25,8 @@ class Station: def __init__(self, station_json): self.stationuuid = generic.get_json_attr(station_json, 'stationuuid') - self.id = generic.generate_stationid_with_prefix(generic.get_checksum(self.stationuuid), ID_PREFIX) + self.id = generic.generate_stationid_with_prefix( + base64.urlsafe_b64encode(uuid.UUID(self.stationuuid).bytes).decode(), ID_PREFIX) self.name = generic.get_json_attr(station_json, 'name') self.url = generic.get_json_attr(station_json, 'url') self.icon = generic.get_json_attr(station_json, 'favicon') @@ -62,8 +66,22 @@ def request(url): def get_station_by_id(vtune_id): + global station_cache +# decode + uidbase64 = generic.get_stationid_without_prefix(vtune_id) + uid = str(uuid.UUID(base64.urlsafe_b64decode(uidbase64).hex())) if station_cache: - return station_cache[vtune_id] + station = station_cache[vtune_id] + if station: + logging.debug('verify %s:%s', station.stationuuid, uid) + return station +# no item in cache, do request + station_json = request('stations/byuuid?uuids=' + uid) + if station_json and len(station_json): + station = Station(station_json[0]) + if station: + station_cache[station.id] = station + return station return None diff --git a/ycast/station_icons.py b/ycast/station_icons.py index e072024..20ff6c3 100644 --- a/ycast/station_icons.py +++ b/ycast/station_icons.py @@ -16,7 +16,9 @@ def get_icon(station): cache_path = generic.get_cache_path(CACHE_NAME) if not cache_path: return None - station_icon_file = cache_path + '/' + station.id + +# make icon filename from favicon-adress + station_icon_file = cache_path + '/' + generic.get_checksum(station.icon) + '.jpg' if not os.path.exists(station_icon_file): logging.debug("Station icon cache miss. Fetching and converting station icon for station id '%s'", station.id) headers = {'User-Agent': generic.USER_AGENT + '/' + __version__} @@ -26,7 +28,8 @@ def get_icon(station): logging.debug("Connection to station icon URL failed (%s)", err) return None if response.status_code != 200: - logging.debug("Could not get station icon data from %s (HTML status %s)", station.icon, response.status_code) + logging.debug("Could not get station icon data from %s (HTML status %s)", + station.icon, response.status_code) return None try: image = Image.open(io.BytesIO(response.content)) From 2179c48d5ae1750ba4371fd0de52f79e47a95c09 Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Fri, 28 Jan 2022 10:23:25 +0100 Subject: [PATCH 13/51] used url_resolved for yamaha app --- setup.py | 2 +- ycast/{filter.py => my_filter.py} | 15 ++++++++++++--- ycast/my_stations.py | 1 - ycast/radiobrowser.py | 16 ++++++++-------- ycast/server.py | 31 ++++++++++++++++++++++++------- ycast/test_filter.py | 6 +++++- 6 files changed, 50 insertions(+), 21 deletions(-) rename ycast/{filter.py => my_filter.py} (88%) diff --git a/setup.py b/setup.py index f18a3ce..9d70bb7 100644 --- a/setup.py +++ b/setup.py @@ -40,6 +40,6 @@ 'onkyo', 'denon' ], - install_requires=['requests', 'flask', 'PyYAML', 'Pillow', 'oyaml'], + install_requires=['requests', 'flask', 'PyYAML', 'Pillow'], packages=find_packages(exclude=['contrib', 'docs', 'tests']) ) diff --git a/ycast/filter.py b/ycast/my_filter.py similarity index 88% rename from ycast/filter.py rename to ycast/my_filter.py index b5fba1e..44ba728 100644 --- a/ycast/filter.py +++ b/ycast/my_filter.py @@ -7,13 +7,18 @@ black_list = {} filter_dir = {} parameter_failed_list = {} - +count_used = 0 +count_hit = 0 def init_filter(): global white_list global black_list global parameter_failed_list global filter_dir + global count_used + global count_hit + count_used = 0 + count_hit = 0 filter_dir = generic.read_yaml_file(generic.get_var_path() + '/filter.yml') if filter_dir: white_list = filter_dir['whitelist'] @@ -30,9 +35,9 @@ def init_filter(): def end_filter(): if parameter_failed_list: - logging.info("Used filter parameter: %s", parameter_failed_list) + logging.info("(%d/%d) stations filtered by: %s", count_hit, count_used, parameter_failed_list) else: - logging.info("Used filter parameter: ") + logging.info("(%d/%d) stations filtered by: ") def parameter_hit(param_name): @@ -46,6 +51,9 @@ def parameter_hit(param_name): def check_station(station_json): + global count_used + global count_hit + count_used = count_used + 1 station_name = get_json_attr(station_json, 'name') if not station_name: # müll response @@ -92,4 +100,5 @@ def check_station(station_json): # station_name, param_name, valid_elements, val) return False # logging.debug("OK '%s' passed", station_name) + count_hit = count_hit + 1 return True diff --git a/ycast/my_stations.py b/ycast/my_stations.py index cba5dd1..7aca54e 100644 --- a/ycast/my_stations.py +++ b/ycast/my_stations.py @@ -1,4 +1,3 @@ -import logging import ycast.vtuner as vtuner import ycast.generic as generic diff --git a/ycast/radiobrowser.py b/ycast/radiobrowser.py index 45f1adb..6b83522 100644 --- a/ycast/radiobrowser.py +++ b/ycast/radiobrowser.py @@ -7,7 +7,7 @@ from ycast import __version__ import ycast.vtuner as vtuner import ycast.generic as generic -from ycast.filter import check_station, init_filter, end_filter +from ycast.my_filter import check_station, init_filter, end_filter from ycast.generic import get_json_attr API_ENDPOINT = "http://all.api.radio-browser.info" @@ -28,7 +28,11 @@ def __init__(self, station_json): self.id = generic.generate_stationid_with_prefix( base64.urlsafe_b64encode(uuid.UUID(self.stationuuid).bytes).decode(), ID_PREFIX) self.name = generic.get_json_attr(station_json, 'name') - self.url = generic.get_json_attr(station_json, 'url') + + self.url = generic.get_json_attr(station_json, 'url_resolved') + if not self.url: + self.url = generic.get_json_attr(station_json, 'url') + self.icon = generic.get_json_attr(station_json, 'favicon') self.tags = generic.get_json_attr(station_json, 'tags').split(',') self.countrycode = generic.get_json_attr(station_json, 'countrycode') @@ -45,8 +49,9 @@ def to_vtuner(self): def get_playable_url(self): try: - playable_url_json = request('url/' + str(self.stationuuid))[0] + playable_url_json = request('url/' + str(self.stationuuid)) self.url = playable_url_json['url'] + except (IndexError, KeyError): logging.error("Could not retrieve first playlist item for station with id '%s'", self.stationuuid) @@ -141,7 +146,6 @@ def get_stations_by_country(country): cur_station = Station(station_json) station_cache[cur_station.id] = cur_station stations.append(cur_station) - logging.info("Stations (%d/%d)", len(stations), len(stations_list_json)) end_filter() return stations @@ -157,7 +161,6 @@ def get_stations_by_language(language): cur_station = Station(station_json) station_cache[cur_station.id] = cur_station stations.append(cur_station) - logging.info("Stations (%d/%d)", len(stations), len(stations_list_json)) end_filter() return stations @@ -172,7 +175,6 @@ def get_stations_by_genre(genre): cur_station = Station(station_json) station_cache[cur_station.id] = cur_station stations.append(cur_station) - logging.info("Stations (%d/%d)", len(stations), len(stations_list_json)) end_filter() return stations @@ -187,7 +189,6 @@ def get_stations_by_votes(limit=DEFAULT_STATION_LIMIT): cur_station = Station(station_json) station_cache[cur_station.id] = cur_station stations.append(cur_station) - logging.info("Stations (%d/%d)", len(stations), len(stations_list_json)) end_filter() return stations @@ -202,6 +203,5 @@ def search(name, limit=DEFAULT_STATION_LIMIT): cur_station = Station(station_json) station_cache[cur_station.id] = cur_station stations.append(cur_station) - logging.info("Stations (%d/%d)", len(stations), len(stations_list_json)) end_filter() return stations diff --git a/ycast/server.py b/ycast/server.py index 9101428..b808eda 100644 --- a/ycast/server.py +++ b/ycast/server.py @@ -40,13 +40,13 @@ def check_my_stations_feature(config): my_stations_enabled = my_stations.set_config(config) -def get_directories_page(subdir, directories, request): +def get_directories_page(subdir, directories, request_obj): page = vtuner.Page() if len(directories) == 0: page.add(vtuner.Display("No entries found")) page.set_count(1) return page - for directory in get_paged_elements(directories, request.args): + for directory in get_paged_elements(directories, request_obj.args): vtuner_directory = vtuner.Directory(directory.displayname, url_for(subdir, _external=True, directory=directory.name), directory.item_count) @@ -55,17 +55,18 @@ def get_directories_page(subdir, directories, request): return page -def get_stations_page(stations, request): +def get_stations_page(stations, request_obj): page = vtuner.Page() if len(stations) == 0: page.add(vtuner.Display("No stations found")) page.set_count(1) return page - for station in get_paged_elements(stations, request.args): + for station in get_paged_elements(stations, request_obj.args): vtuner_station = station.to_vtuner() if station_tracking: - vtuner_station.set_trackurl(request.host_url + PATH_ROOT + '/' + PATH_PLAY + '?id=' + vtuner_station.uid) - vtuner_station.icon = request.host_url + PATH_ROOT + '/' + PATH_ICON + '?id=' + vtuner_station.uid + vtuner_station.set_trackurl( + request_obj.host_url + PATH_ROOT + '/' + PATH_PLAY + '?id=' + vtuner_station.uid) + vtuner_station.icon = request_obj.host_url + PATH_ROOT + '/' + PATH_ICON + '?id=' + vtuner_station.uid page.add(vtuner_station) page.set_count(len(stations)) return page @@ -112,7 +113,7 @@ def get_station_by_id(stationid, additional_info=False): def vtuner_redirect(url): - if request and request.host and not re.search("^[A-Za-z0-9]+\.vtuner\.com$", request.host): + if request and request.host and not re.search(r"^[A-Za-z0-9]+\.vtuner\.com$", request.host): logging.warning("You are not accessing a YCast redirect with a whitelisted host url (*.vtuner.com). " "Some AVRs have problems with this. The requested host was: %s", request.host) return redirect(url, code=302) @@ -121,6 +122,7 @@ def vtuner_redirect(url): @app.route('/setupapp/', methods=['GET', 'POST']) def upstream(path): + logging.debug('**********************: %s', request.url) if request.args.get('token') == '0': return vtuner.get_init_token() if request.args.get('search'): @@ -144,6 +146,7 @@ def upstream(path): defaults={'path': ''}, methods=['GET', 'POST']) def landing(path=''): + logging.debug('**********************: %s', request.url) page = vtuner.Page() page.add(vtuner.Directory('Radiobrowser', url_for('radiobrowser_landing', _external=True), 4)) if my_stations_enabled: @@ -158,6 +161,7 @@ def landing(path=''): @app.route('/' + PATH_ROOT + '/' + PATH_MY_STATIONS + '/', methods=['GET', 'POST']) def my_stations_landing(): + logging.debug('**********************: %s', request.url) directories = my_stations.get_category_directories() return get_directories_page('my_stations_category', directories, request).to_string() @@ -165,6 +169,7 @@ def my_stations_landing(): @app.route('/' + PATH_ROOT + '/' + PATH_MY_STATIONS + '/', methods=['GET', 'POST']) def my_stations_category(directory): + logging.debug('**********************: %s', request.url) stations = my_stations.get_stations_by_category(directory) return get_stations_page(stations, request).to_string() @@ -172,6 +177,7 @@ def my_stations_category(directory): @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/', methods=['GET', 'POST']) def radiobrowser_landing(): + logging.debug('**********************: %s', request.url) page = vtuner.Page() page.add(vtuner.Directory('Genres', url_for('radiobrowser_genres', _external=True), len(radiobrowser.get_genre_directories()))) @@ -188,6 +194,7 @@ def radiobrowser_landing(): @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_COUNTRY + '/', methods=['GET', 'POST']) def radiobrowser_countries(): + logging.debug('**********************: %s', request.url) directories = radiobrowser.get_country_directories() return get_directories_page('radiobrowser_country_stations', directories, request).to_string() @@ -195,6 +202,7 @@ def radiobrowser_countries(): @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_COUNTRY + '/', methods=['GET', 'POST']) def radiobrowser_country_stations(directory): + logging.debug('**********************: %s', request.url) stations = radiobrowser.get_stations_by_country(directory) return get_stations_page(stations, request).to_string() @@ -202,6 +210,7 @@ def radiobrowser_country_stations(directory): @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_LANGUAGE + '/', methods=['GET', 'POST']) def radiobrowser_languages(): + logging.debug('**********************: %s', request.url) directories = radiobrowser.get_language_directories() return get_directories_page('radiobrowser_language_stations', directories, request).to_string() @@ -209,6 +218,7 @@ def radiobrowser_languages(): @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_LANGUAGE + '/', methods=['GET', 'POST']) def radiobrowser_language_stations(directory): + logging.debug('**********************: %s', request.url) stations = radiobrowser.get_stations_by_language(directory) return get_stations_page(stations, request).to_string() @@ -216,6 +226,7 @@ def radiobrowser_language_stations(directory): @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_GENRE + '/', methods=['GET', 'POST']) def radiobrowser_genres(): + logging.debug('**********************: %s', request.url) directories = radiobrowser.get_genre_directories() return get_directories_page('radiobrowser_genre_stations', directories, request).to_string() @@ -223,6 +234,7 @@ def radiobrowser_genres(): @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_GENRE + '/', methods=['GET', 'POST']) def radiobrowser_genre_stations(directory): + logging.debug('**********************: %s', request.url) stations = radiobrowser.get_stations_by_genre(directory) return get_stations_page(stations, request).to_string() @@ -230,6 +242,7 @@ def radiobrowser_genre_stations(directory): @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_POPULAR + '/', methods=['GET', 'POST']) def radiobrowser_popular(): + logging.debug('**********************: %s', request.url) stations = radiobrowser.get_stations_by_votes() return get_stations_page(stations, request).to_string() @@ -237,6 +250,7 @@ def radiobrowser_popular(): @app.route('/' + PATH_ROOT + '/' + PATH_SEARCH + '/', methods=['GET', 'POST']) def station_search(): + logging.debug('**********************: %s', request.url) query = request.args.get('search') if not query or len(query) < 3: page = vtuner.Page() @@ -252,6 +266,7 @@ def station_search(): @app.route('/' + PATH_ROOT + '/' + PATH_PLAY, methods=['GET', 'POST']) def get_stream_url(): + logging.debug('**********************: %s', request.url) stationid = request.args.get('id') if not stationid: logging.error("Stream URL without station ID requested") @@ -267,6 +282,7 @@ def get_stream_url(): @app.route('/' + PATH_ROOT + '/' + PATH_STATION, methods=['GET', 'POST']) def get_station_info(): + logging.debug('**********************: %s', request.url) stationid = request.args.get('id') if not stationid: logging.error("Station info without station ID requested") @@ -291,6 +307,7 @@ def get_station_info(): @app.route('/' + PATH_ROOT + '/' + PATH_ICON, methods=['GET', 'POST']) def get_station_icon(): + logging.debug('**********************: %s', request.url) stationid = request.args.get('id') if not stationid: logging.error("Station icon without station ID requested") diff --git a/ycast/test_filter.py b/ycast/test_filter.py index 78564b7..61d840c 100644 --- a/ycast/test_filter.py +++ b/ycast/test_filter.py @@ -3,7 +3,7 @@ import unittest from io import StringIO -import filter +import my_filter import generic from ycast import radiobrowser @@ -39,6 +39,10 @@ def test_valid_station(self): logging.info("Used filter parameter", filter.parameter_failed_list) + def test_popular_station(self): + stations = radiobrowser.get_stations_by_votes() + logging.info("Stations (%d)", len(stations)) + if __name__ == '__main__': unittest.main() From 838d2f3639433e10c62d7c7f34ff52f36d49eff2 Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Fri, 28 Jan 2022 12:49:38 +0100 Subject: [PATCH 14/51] manual --- README.md | 11 +++++++---- ycast/__init__.py | 2 +- ycast/radiobrowser.py | 1 - 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c3c30d1..cad8c5c 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,17 @@ -# YCast +# YCast (advanced) [![PyPI latest version](https://img.shields.io/pypi/v/ycast?color=success)](https://pypi.org/project/ycast/) [![GitHub latest version](https://img.shields.io/github/v/release/milaq/YCast?color=success&label=github&sort=semver)](https://github.com/milaq/YCast/releases) [![Python version](https://img.shields.io/pypi/pyversions/ycast)](https://www.python.org/downloads/) [![License](https://img.shields.io/pypi/l/ycast)](https://www.gnu.org/licenses/gpl-3.0.en.html) [![GitHub issues](https://img.shields.io/github/issues/milaq/ycast)](https://github.com/milaq/YCast/issues) -[Get it via PyPI](https://pypi.org/project/ycast/) +[Download from GitHub](https://github.com/THanika/YCast/releases) -[Download from GitHub](https://github.com/milaq/YCast/releases) +[Issue tracker](https://github.com/THanika/YCast/issues) -[Issue tracker](https://github.com/milaq/YCast/issues) +### The advanced feature: + * icons in my_stations stations.yml + * recently visited ./ycast/recently.yml + * global filter by ./ycast/filter.yml YCast is a self hosted replacement for the vTuner internet radio service which many AVRs use. It emulates a vTuner backend to provide your AVR with the necessary information to play self defined categorized internet radio stations and listen to Radio stations listed in the [Community Radio Browser index](http://www.radio-browser.info). diff --git a/ycast/__init__.py b/ycast/__init__.py index 1a72d32..58d478a 100644 --- a/ycast/__init__.py +++ b/ycast/__init__.py @@ -1 +1 @@ -__version__ = '1.1.0' +__version__ = '1.2.0' diff --git a/ycast/radiobrowser.py b/ycast/radiobrowser.py index 6b83522..afb8649 100644 --- a/ycast/radiobrowser.py +++ b/ycast/radiobrowser.py @@ -78,7 +78,6 @@ def get_station_by_id(vtune_id): if station_cache: station = station_cache[vtune_id] if station: - logging.debug('verify %s:%s', station.stationuuid, uid) return station # no item in cache, do request station_json = request('stations/byuuid?uuids=' + uid) From cdb1c932a6127bd480dd1ffd2785aacdc0b55706 Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Fri, 28 Jan 2022 18:15:40 +0100 Subject: [PATCH 15/51] 5 recently stations added in first page (experimental) --- ycast/generic.py | 1 - ycast/my_recentlystation.py | 11 ++++++++--- ycast/server.py | 23 ++++++++++++++++++++++- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/ycast/generic.py b/ycast/generic.py index bc126fe..c485041 100644 --- a/ycast/generic.py +++ b/ycast/generic.py @@ -114,7 +114,6 @@ def writelns_txt_file(file_name, line_list): try: with open(file_name, 'w') as f: f.writelines(line_list) - logging.info("File written '%s'", file_name) except Exception as ex: logging.error("File not written '%s':\n %s", file_name, ex) diff --git a/ycast/my_recentlystation.py b/ycast/my_recentlystation.py index 5ed61c0..acb4dc6 100644 --- a/ycast/my_recentlystation.py +++ b/ycast/my_recentlystation.py @@ -2,14 +2,15 @@ from ycast import generic MAX_ENTRIES = 15 +DIRECTORY_NAME = "recently used" + recently_file = generic.get_var_path() + '/recently.yml' def signal_station_selected(name, url, icon): - logging.debug(" %s:%s|%s", name, url, icon) list_heard_stations = get_stations_list() if len(list_heard_stations) == 0: - list_heard_stations.append("recently used:\n") + list_heard_stations.append(DIRECTORY_NAME + ":\n") # make name yaml - like name = name.replace(":", " -") @@ -17,7 +18,6 @@ def signal_station_selected(name, url, icon): elements = line.split(': ') if elements[0] == ' '+name: list_heard_stations.remove(line) - logging.debug("Name '%s' exists and deleted", name) piped_icon = '' if icon and len(icon) > 0: piped_icon = '|' + icon @@ -40,3 +40,8 @@ def get_stations_list(): def get_recently_stations_yaml(): return generic.read_yaml_file(recently_file) + + +def directory_name(): + dir = generic.read_yaml_file(recently_file) + return list(dir.keys())[0] diff --git a/ycast/server.py b/ycast/server.py index b808eda..04aeb01 100644 --- a/ycast/server.py +++ b/ycast/server.py @@ -8,6 +8,7 @@ import ycast.my_stations as my_stations import ycast.generic as generic import ycast.station_icons as station_icons +from ycast import my_recentlystation from ycast.my_recentlystation import signal_station_selected PATH_ROOT = 'ycast' @@ -122,7 +123,7 @@ def vtuner_redirect(url): @app.route('/setupapp/', methods=['GET', 'POST']) def upstream(path): - logging.debug('**********************: %s', request.url) + logging.debug('upstream **********************: %s', request.url) if request.args.get('token') == '0': return vtuner.get_init_token() if request.args.get('search'): @@ -152,6 +153,26 @@ def landing(path=''): if my_stations_enabled: page.add(vtuner.Directory('My Stations', url_for('my_stations_landing', _external=True), len(my_stations.get_category_directories()))) + + stations = my_stations.get_stations_by_category(my_recentlystation.directory_name()) + if stations: +# emulate Sp + page.add(vtuner.Directory(' ', url_for('my_stations_landing', _external=True), + len(my_stations.get_category_directories()))) + count = 0 + for station in stations: + vtuner_station = station.to_vtuner() + if station_tracking: + vtuner_station.set_trackurl( + request.host_url + PATH_ROOT + '/' + PATH_PLAY + '?id=' + vtuner_station.uid) + vtuner_station.icon = request.host_url + PATH_ROOT + '/' + PATH_ICON + '?id=' + vtuner_station.uid + page.add(vtuner_station) + count = count + 1 + if count > 4: + break + + page.set_count(3) + else: page.add(vtuner.Display("'My Stations' feature not configured.")) page.set_count(1) From f38b1f566c0e483bc6d542414e9acd50c600fdf5 Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Fri, 28 Jan 2022 23:31:29 +0100 Subject: [PATCH 16/51] 5 recently stations added in first page (experimental) bugfix missing recently.yml --- ycast/generic.py | 8 +++++--- ycast/my_recentlystation.py | 20 +++++++++++++++----- ycast/server.py | 4 ++-- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/ycast/generic.py b/ycast/generic.py index c485041..f483f1f 100644 --- a/ycast/generic.py +++ b/ycast/generic.py @@ -93,10 +93,12 @@ def write_yaml_file(file_name, dictionary): try: with open(file_name, 'w') as f: yaml.dump(dictionary, f) + return True except yaml.YAMLError as e: logging.error("YAML format error in '%':\n %s", file_name, e) except Exception as ex: logging.error("File not written '%s':\n %s", file_name, ex) + return False def readlns_txt_file(file_name): @@ -104,9 +106,7 @@ def readlns_txt_file(file_name): with open(file_name, 'r') as f: return f.readlines() except FileNotFoundError: - logging.error("YAML file '%s' not found", file_name) - except yaml.YAMLError as e: - logging.error("YAML format error in '%':\n %s", file_name, e) + logging.error("TXT file '%s' not found", file_name) return None @@ -114,8 +114,10 @@ def writelns_txt_file(file_name, line_list): try: with open(file_name, 'w') as f: f.writelines(line_list) + return True except Exception as ex: logging.error("File not written '%s':\n %s", file_name, ex) + return False def get_json_attr(json, attr): diff --git a/ycast/my_recentlystation.py b/ycast/my_recentlystation.py index acb4dc6..aede610 100644 --- a/ycast/my_recentlystation.py +++ b/ycast/my_recentlystation.py @@ -1,15 +1,16 @@ -import logging from ycast import generic MAX_ENTRIES = 15 DIRECTORY_NAME = "recently used" recently_file = generic.get_var_path() + '/recently.yml' +is_yml_file_loadable = True def signal_station_selected(name, url, icon): list_heard_stations = get_stations_list() - if len(list_heard_stations) == 0: + if not list_heard_stations: + list_heard_stations = [] list_heard_stations.append(DIRECTORY_NAME + ":\n") # make name yaml - like name = name.replace(":", " -") @@ -31,7 +32,8 @@ def signal_station_selected(name, url, icon): def set_stations_yaml(heard_stations): - generic.writelns_txt_file(recently_file, heard_stations) + global is_yml_file_loadable + is_yml_file_loadable = generic.writelns_txt_file(recently_file, heard_stations) def get_stations_list(): @@ -39,9 +41,17 @@ def get_stations_list(): def get_recently_stations_yaml(): - return generic.read_yaml_file(recently_file) + global is_yml_file_loadable + dict_stations = None + if is_yml_file_loadable: + dict_stations = generic.read_yaml_file(recently_file) + if not dict_stations: + is_yml_file_loadable = False + return dict_stations def directory_name(): dir = generic.read_yaml_file(recently_file) - return list(dir.keys())[0] + if dir: + return list(dir.keys())[0] + return None diff --git a/ycast/server.py b/ycast/server.py index 04aeb01..f630300 100644 --- a/ycast/server.py +++ b/ycast/server.py @@ -155,7 +155,7 @@ def landing(path=''): len(my_stations.get_category_directories()))) stations = my_stations.get_stations_by_category(my_recentlystation.directory_name()) - if stations: + if stations and len(stations) > 0: # emulate Sp page.add(vtuner.Directory(' ', url_for('my_stations_landing', _external=True), len(my_stations.get_category_directories()))) @@ -171,7 +171,7 @@ def landing(path=''): if count > 4: break - page.set_count(3) + page.set_count(1) else: page.add(vtuner.Display("'My Stations' feature not configured.")) From a80d6f15f9c90f770985794cdff8bc4925548ee4 Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Sat, 29 Jan 2022 12:03:25 +0100 Subject: [PATCH 17/51] refactor my_recentlystation --- ycast/my_recentlystation.py | 93 ++++++++++++++++++++++++------------- ycast/my_stations.py | 16 +++---- ycast/server.py | 2 +- 3 files changed, 71 insertions(+), 40 deletions(-) diff --git a/ycast/my_recentlystation.py b/ycast/my_recentlystation.py index aede610..5bfbdba 100644 --- a/ycast/my_recentlystation.py +++ b/ycast/my_recentlystation.py @@ -1,57 +1,88 @@ from ycast import generic MAX_ENTRIES = 15 +# define a max, so after 5 hits, an other station is get better votes +MAX_VOTES = 5 DIRECTORY_NAME = "recently used" recently_file = generic.get_var_path() + '/recently.yml' -is_yml_file_loadable = True +recently_station_dictionary = None + + +class StationVote: + def __init__(self, name, params_txt): + self.name = name + params = params_txt.split('|') + self.url = params[0] + self.icon = '' + self.vote = 0 + if len(params) > 0: + self.icon = params[1] + if len(params) > 1: + self.vote = int(params[2]) + + def to_params_txt(self): + text_line = self.url + '|' + self.icon + '|' + str(self.vote) + '\n' + return text_line def signal_station_selected(name, url, icon): - list_heard_stations = get_stations_list() - if not list_heard_stations: - list_heard_stations = [] - list_heard_stations.append(DIRECTORY_NAME + ":\n") # make name yaml - like name = name.replace(":", " -") - for line in list_heard_stations: - elements = line.split(': ') - if elements[0] == ' '+name: - list_heard_stations.remove(line) - piped_icon = '' - if icon and len(icon) > 0: - piped_icon = '|' + icon + recently_station_list = get_stations_list() + station_hit = StationVote(name, url + '|' + icon) + for recently_station in recently_station_list: + if name == recently_station.name: + station_hit.vote = recently_station.vote + 1 + recently_station_list.remove(recently_station) + break + recently_station_list.insert(0, station_hit) - list_heard_stations.insert(1, ' '+name+': '+url+piped_icon+'\n') - if len(list_heard_stations) > MAX_ENTRIES+1: + if station_hit.vote > MAX_VOTES: + for recently_station in recently_station_list: + if recently_station.vote > 0: + recently_station.vote = recently_station.vote - 1 + + if len(recently_station_list) > MAX_ENTRIES: # remove last (oldest) entry - list_heard_stations.pop() + recently_station_list.pop() + + set_station_dictionary(directory_name(), recently_station_list) - set_stations_yaml(list_heard_stations) +def set_station_dictionary(cathegory, station_list): + global recently_station_dictionary + new_cathegory_dictionary = {} + station_dictionary = {} + for station in station_list: + station_dictionary[station.name] = station.to_params_txt() + new_cathegory_dictionary[cathegory] = station_dictionary -def set_stations_yaml(heard_stations): - global is_yml_file_loadable - is_yml_file_loadable = generic.writelns_txt_file(recently_file, heard_stations) + recently_station_dictionary = new_cathegory_dictionary + generic.write_yaml_file(recently_file, recently_station_dictionary) def get_stations_list(): - return generic.readlns_txt_file(recently_file) + stations_list = [] + cathegory_dict = get_recently_stations_yaml() + if cathegory_dict: + for cat_key in cathegory_dict: + station_dict = cathegory_dict[cat_key] + for station_key in station_dict: + stations_list.append(StationVote(station_key, station_dict[station_key])) + return stations_list def get_recently_stations_yaml(): - global is_yml_file_loadable - dict_stations = None - if is_yml_file_loadable: - dict_stations = generic.read_yaml_file(recently_file) - if not dict_stations: - is_yml_file_loadable = False - return dict_stations + # cached recently + global recently_station_dictionary + if not recently_station_dictionary: + recently_station_dictionary = generic.read_yaml_file(recently_file) + if not recently_station_dictionary: + recently_station_dictionary[DIRECTORY_NAME] = None + return recently_station_dictionary def directory_name(): - dir = generic.read_yaml_file(recently_file) - if dir: - return list(dir.keys())[0] - return None + return list(get_recently_stations_yaml().keys())[0] diff --git a/ycast/my_stations.py b/ycast/my_stations.py index 7aca54e..2248a1e 100644 --- a/ycast/my_stations.py +++ b/ycast/my_stations.py @@ -8,8 +8,9 @@ class Station: - def __init__(self, uid, name, url, category, icon): - self.id = generic.generate_stationid_with_prefix(uid, ID_PREFIX) + def __init__(self, name, url, category, icon): + self.id = generic.generate_stationid_with_prefix( + generic.get_checksum(name + url), ID_PREFIX) self.name = name self.url = url self.tag = category @@ -66,11 +67,10 @@ def get_stations_by_category(category): if my_stations_yaml and category in my_stations_yaml: for station_name in my_stations_yaml[category]: station_urls = my_stations_yaml[category][station_name] - url_list = station_urls.split('|') - station_url = url_list[0] + param_list = station_urls.split('|') + station_url = param_list[0] station_icon = None - if len(url_list) > 1: - station_icon = url_list[1] - station_id = generic.get_checksum(station_name + station_url) - stations.append(Station(station_id, station_name, station_url, category, station_icon)) + if len(param_list) > 1: + station_icon = param_list[1] + stations.append(Station(station_name, station_url, category, station_icon)) return stations diff --git a/ycast/server.py b/ycast/server.py index f630300..9e22109 100644 --- a/ycast/server.py +++ b/ycast/server.py @@ -156,7 +156,7 @@ def landing(path=''): stations = my_stations.get_stations_by_category(my_recentlystation.directory_name()) if stations and len(stations) > 0: -# emulate Sp + # make blank line (display is not shown) page.add(vtuner.Directory(' ', url_for('my_stations_landing', _external=True), len(my_stations.get_category_directories()))) count = 0 From 415562c7e7323415e20eef0be61688a73ecf3a6e Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Sat, 29 Jan 2022 16:48:54 +0100 Subject: [PATCH 18/51] display max 5 of my voted stations by clicks in landing page --- ycast/generic.py | 3 +- ycast/my_recentlystation.py | 58 +++++++++++++++------- ycast/my_stations.py | 4 +- ycast/server.py | 43 ++++++++-------- ycast/test_YCast.py | 97 +++++++++++++++++++++++++++++++++++++ ycast/test_filter.py | 48 ------------------ 6 files changed, 163 insertions(+), 90 deletions(-) create mode 100644 ycast/test_YCast.py delete mode 100644 ycast/test_filter.py diff --git a/ycast/generic.py b/ycast/generic.py index f483f1f..379053b 100644 --- a/ycast/generic.py +++ b/ycast/generic.py @@ -92,7 +92,8 @@ def read_yaml_file(file_name): def write_yaml_file(file_name, dictionary): try: with open(file_name, 'w') as f: - yaml.dump(dictionary, f) + # no sort please + yaml.dump(dictionary, f, sort_keys=False) return True except yaml.YAMLError as e: logging.error("YAML format error in '%':\n %s", file_name, e) diff --git a/ycast/my_recentlystation.py b/ycast/my_recentlystation.py index 5bfbdba..8a5c36e 100644 --- a/ycast/my_recentlystation.py +++ b/ycast/my_recentlystation.py @@ -1,4 +1,4 @@ -from ycast import generic +from ycast import generic, my_stations MAX_ENTRIES = 15 # define a max, so after 5 hits, an other station is get better votes @@ -7,6 +7,7 @@ recently_file = generic.get_var_path() + '/recently.yml' recently_station_dictionary = None +voted5_station_dictinary = None class StationVote: @@ -16,20 +17,19 @@ def __init__(self, name, params_txt): self.url = params[0] self.icon = '' self.vote = 0 - if len(params) > 0: + if len(params) > 1: self.icon = params[1] - if len(params) > 1: + if len(params) > 2: self.vote = int(params[2]) def to_params_txt(self): - text_line = self.url + '|' + self.icon + '|' + str(self.vote) + '\n' + text_line = self.url + '|' + self.icon + '|' + str(self.vote) return text_line + def to_server_station(self,cathegory): + return my_stations.Station(self.name, self.url, cathegory, self.icon) def signal_station_selected(name, url, icon): - # make name yaml - like - name = name.replace(":", " -") - recently_station_list = get_stations_list() station_hit = StationVote(name, url + '|' + icon) for recently_station in recently_station_list: @@ -37,6 +37,7 @@ def signal_station_selected(name, url, icon): station_hit.vote = recently_station.vote + 1 recently_station_list.remove(recently_station) break + recently_station_list.insert(0, station_hit) if station_hit.vote > MAX_VOTES: @@ -48,24 +49,28 @@ def signal_station_selected(name, url, icon): # remove last (oldest) entry recently_station_list.pop() - set_station_dictionary(directory_name(), recently_station_list) + set_recently_station_dictionary(mk_station_dictionary(directory_name(), recently_station_list)) -def set_station_dictionary(cathegory, station_list): +def set_recently_station_dictionary(station_dict): global recently_station_dictionary + recently_station_dictionary = station_dict + generic.write_yaml_file(recently_file, recently_station_dictionary) + + +def mk_station_dictionary(cathegory, station_list): new_cathegory_dictionary = {} station_dictionary = {} for station in station_list: station_dictionary[station.name] = station.to_params_txt() - new_cathegory_dictionary[cathegory] = station_dictionary - recently_station_dictionary = new_cathegory_dictionary - generic.write_yaml_file(recently_file, recently_station_dictionary) + new_cathegory_dictionary[cathegory] = station_dictionary + return new_cathegory_dictionary def get_stations_list(): stations_list = [] - cathegory_dict = get_recently_stations_yaml() + cathegory_dict = get_recently_stations_dictionary() if cathegory_dict: for cat_key in cathegory_dict: station_dict = cathegory_dict[cat_key] @@ -74,15 +79,34 @@ def get_stations_list(): return stations_list -def get_recently_stations_yaml(): +def get_recently_stations_dictionary(): # cached recently global recently_station_dictionary if not recently_station_dictionary: recently_station_dictionary = generic.read_yaml_file(recently_file) - if not recently_station_dictionary: - recently_station_dictionary[DIRECTORY_NAME] = None return recently_station_dictionary def directory_name(): - return list(get_recently_stations_yaml().keys())[0] + station_dictionary = get_recently_stations_dictionary() + if station_dictionary: + return list(get_recently_stations_dictionary().keys())[0] + return DIRECTORY_NAME + +# used in landing page +def get_stations_by_vote(): + station_list = get_stations_list() + station_list.sort(key=lambda station: station.vote, reverse=True) + station_list = station_list[:5] + stations = [] + for item in station_list: + stations.append(item.to_server_station('voted')) + return stations + + +def get_stations_by_recently(): + station_list = get_stations_list() + stations = [] + for item in station_list: + stations.append(item.to_server_station(directory_name())) + return stations diff --git a/ycast/my_stations.py b/ycast/my_stations.py index 2248a1e..0648b5e 100644 --- a/ycast/my_stations.py +++ b/ycast/my_stations.py @@ -41,8 +41,8 @@ def get_station_by_id(vtune_id): def get_stations_yaml(): - from ycast.my_recentlystation import get_recently_stations_yaml - my_recently_station = get_recently_stations_yaml() + from ycast.my_recentlystation import get_recently_stations_dictionary + my_recently_station = get_recently_stations_dictionary() my_stations = generic.read_yaml_file(config_file) if my_stations: if my_recently_station: diff --git a/ycast/server.py b/ycast/server.py index 9e22109..8ffdb3d 100644 --- a/ycast/server.py +++ b/ycast/server.py @@ -150,32 +150,31 @@ def landing(path=''): logging.debug('**********************: %s', request.url) page = vtuner.Page() page.add(vtuner.Directory('Radiobrowser', url_for('radiobrowser_landing', _external=True), 4)) - if my_stations_enabled: - page.add(vtuner.Directory('My Stations', url_for('my_stations_landing', _external=True), - len(my_stations.get_category_directories()))) - - stations = my_stations.get_stations_by_category(my_recentlystation.directory_name()) - if stations and len(stations) > 0: - # make blank line (display is not shown) - page.add(vtuner.Directory(' ', url_for('my_stations_landing', _external=True), - len(my_stations.get_category_directories()))) - count = 0 - for station in stations: - vtuner_station = station.to_vtuner() - if station_tracking: - vtuner_station.set_trackurl( - request.host_url + PATH_ROOT + '/' + PATH_PLAY + '?id=' + vtuner_station.uid) - vtuner_station.icon = request.host_url + PATH_ROOT + '/' + PATH_ICON + '?id=' + vtuner_station.uid - page.add(vtuner_station) - count = count + 1 - if count > 4: - break - page.set_count(1) + page.add(vtuner.Directory('My Stations', url_for('my_stations_landing', _external=True), + len(my_stations.get_category_directories()))) + + stations = my_recentlystation.get_stations_by_vote() + if stations and len(stations) > 0: + # make blank line (display is not shown) + # page.add(vtuner.Directory(' ', url_for('my_stations_landing', _external=True), + # len(my_stations.get_category_directories()))) + + vtuner_station = stations[0].to_vtuner() + vtuner_station.icon = request.host_url + PATH_ROOT + '/' + PATH_ICON + '?id=' + vtuner_station.uid + vtuner_station.name = ' ' + page.add(vtuner_station) + for station in stations: + vtuner_station = station.to_vtuner() + if station_tracking: + vtuner_station.set_trackurl( + request.host_url + PATH_ROOT + '/' + PATH_PLAY + '?id=' + vtuner_station.uid) + vtuner_station.icon = request.host_url + PATH_ROOT + '/' + PATH_ICON + '?id=' + vtuner_station.uid + page.add(vtuner_station) else: page.add(vtuner.Display("'My Stations' feature not configured.")) - page.set_count(1) + page.set_count(1) return page.to_string() diff --git a/ycast/test_YCast.py b/ycast/test_YCast.py new file mode 100644 index 0000000..8932b62 --- /dev/null +++ b/ycast/test_YCast.py @@ -0,0 +1,97 @@ +import json +import logging +import os +import unittest +from io import StringIO + +import my_filter +import generic +from ycast import radiobrowser, my_recentlystation + + +class MyTestCase(unittest.TestCase): + + logging.getLogger().setLevel(logging.DEBUG) + + def test_init_filter(self): + + filter.init_filter() + + for elem in filter.filter_dir: + logging.warning("Name filtertype: %s", elem) + filter_param = filter.filter_dir[elem] + if filter_param: + for par in filter_param: + logging.warning(" Name paramter: %s",par) + else: + logging.warning(" ") + + + def test_valid_station(self): + filter.init_filter() + test_lines = generic.readlns_txt_file(generic.get_var_path()+"/test.json") + io = StringIO(test_lines[0]) + stations_json = json.load(io) + count = 0 + for station_json in stations_json: + if filter.check_station(station_json): + station = radiobrowser.Station(station_json) + count = count + 1 + + logging.info("Stations (%d/%d)" , count, len(stations_json) ) + logging.info("Used filter parameter", filter.parameter_failed_list) + + + def test_popular_station(self): + stations = radiobrowser.get_stations_by_votes() + logging.info("Stations (%d)", len(stations)) + + def test_recently_hit(self): + + try: + os.remove(my_recentlystation.recently_file) + except Exception: + pass + + result = my_recentlystation.get_stations_by_vote() + assert len(result) == 0 + + result = my_recentlystation.get_recently_stations_dictionary() + assert result == None + + i = 0 + while i < 10: + my_recentlystation.signal_station_selected('NAME '+ str(i),'http://dummy/'+ str(i), 'http://icon'+ str(i)) + i = i+1 + + result = my_recentlystation.get_recently_stations_dictionary() + assert my_recentlystation.directory_name() + assert result[my_recentlystation.directory_name()] + + my_recentlystation.signal_station_selected('Konverenz: Sport' , 'http://dummy/' + str(i), 'http://icon' + str(i)) + my_recentlystation.signal_station_selected('Konverenz: Sport' , 'http://dummy/' + str(i), 'http://icon' + str(i)) + my_recentlystation.signal_station_selected('Konverenz: Sport' , 'http://dummy/' + str(i), 'http://icon' + str(i)) + + i = 6 + while i < 17: + my_recentlystation.signal_station_selected('NAME '+ str(i),'http://dummy/'+ str(i), 'http://icon'+ str(i)) + i = i+1 + + result = my_recentlystation.get_recently_stations_dictionary() + assert result[my_recentlystation.directory_name()] + + result = my_recentlystation.get_stations_by_vote() + assert len(result) == 5 + + j = 0 + while j < 6: + i = 6 + while i < 9: + my_recentlystation.signal_station_selected('NAME '+ str(i),'http://dummy/'+ str(i), 'http://icon'+ str(i)) + i = i+1 + j = j+1 + result = my_recentlystation.get_stations_by_vote() + assert len(result) == 5 + +if __name__ == '__main__': + unittest.main() diff --git a/ycast/test_filter.py b/ycast/test_filter.py deleted file mode 100644 index 61d840c..0000000 --- a/ycast/test_filter.py +++ /dev/null @@ -1,48 +0,0 @@ -import json -import logging -import unittest -from io import StringIO - -import my_filter -import generic -from ycast import radiobrowser - -class MyTestCase(unittest.TestCase): - - logging.getLogger().setLevel(logging.DEBUG) - - def test_init_filter(self): - filter.init_filter() - - for elem in filter.filter_dir: - logging.warning("Name filtertype: %s", elem) - filter_param = filter.filter_dir[elem] - if filter_param: - for par in filter_param: - logging.warning(" Name paramter: %s",par) - else: - logging.warning(" ") - - - def test_valid_station(self): - filter.init_filter() - test_lines = generic.readlns_txt_file(generic.get_var_path()+"/test.json") - io = StringIO(test_lines[0]) - stations_json = json.load(io) - count = 0 - for station_json in stations_json: - if filter.check_station(station_json): - station = radiobrowser.Station(station_json) - count = count + 1 - - logging.info("Stations (%d/%d)" , count, len(stations_json) ) - logging.info("Used filter parameter", filter.parameter_failed_list) - - - def test_popular_station(self): - stations = radiobrowser.get_stations_by_votes() - logging.info("Stations (%d)", len(stations)) - -if __name__ == '__main__': - unittest.main() - From 478f78359624e0ade3c7b6e66b16fd2b529785ae Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Sat, 29 Jan 2022 19:11:34 +0100 Subject: [PATCH 19/51] refactor filter modul --- ycast/my_filter.py | 67 ++++++++++++++++++++----------------- ycast/my_recentlystation.py | 4 ++- ycast/test_YCast.py | 33 ++++++++++++++---- 3 files changed, 65 insertions(+), 39 deletions(-) diff --git a/ycast/my_filter.py b/ycast/my_filter.py index 44ba728..08a26dd 100644 --- a/ycast/my_filter.py +++ b/ycast/my_filter.py @@ -10,6 +10,7 @@ count_used = 0 count_hit = 0 + def init_filter(): global white_list global black_list @@ -40,7 +41,7 @@ def end_filter(): logging.info("(%d/%d) stations filtered by: ") -def parameter_hit(param_name): +def parameter_failed_evt(param_name): count = 1 old = None if parameter_failed_list: @@ -50,6 +51,29 @@ def parameter_hit(param_name): parameter_failed_list[param_name] = count +def verify_value(ref_val, val): + if ref_val == val: + return True + if ref_val is None: + return len(val) == 0 + if type(val) is int: + return val == int(ref_val) + if val: + return ref_val.find(val) >= 0 + return False + + +def chk_paramter(parameter_name, val): + if black_list: + if parameter_name in black_list: + if verify_value(black_list[parameter_name], val): + return False + if white_list: + if parameter_name in white_list: + return verify_value(white_list[parameter_name], val) + return True + + def check_station(station_json): global count_used global count_hit @@ -61,43 +85,24 @@ def check_station(station_json): return False # oder verknüpft if black_list: - black_list_hit = False for param_name in black_list: - unvalid_elements = black_list[param_name] val = get_json_attr(station_json, param_name) - if not val == None: - # attribut in json vorhanden - if unvalid_elements: - if val: - pos = unvalid_elements.find(val) - black_list_hit = pos >= 0 - else: - if not val: - black_list_hit = True - if black_list_hit: - parameter_hit(param_name) -# logging.debug("FAIL '%s' blacklist hit on '%s' '%s' == '%s'", -# station_name, param_name, unvalid_elements, val) - return False + if verify_value(black_list[param_name], val): + parameter_failed_evt(param_name) + logging.debug("FAIL '%s' blacklist failed on '%s' '%s' == '%s'", + station_name, param_name, black_list[param_name], val) + return False -# und verknüpft + # und verknüpft if white_list: - white_list_hit = True for param_name in white_list: val = get_json_attr(station_json, param_name) - if not val == None: + if val is not None: # attribut in json vorhanden - valid_elements = white_list[param_name] - if type(val) is int: - white_list_hit = val == valid_elements - else: - if val: - pos = valid_elements.find(val) - white_list_hit = pos >= 0 - if not white_list_hit: - parameter_hit(param_name) -# logging.debug("FAIL '%s' whitelist failed on '%s' '%s' == '%s'", -# station_name, param_name, valid_elements, val) + if not verify_value(white_list[param_name], val): + parameter_failed_evt(param_name) + logging.debug("FAIL '%s' whitelist failed on '%s' '%s' == '%s'", + station_name, param_name, white_list[param_name], val) return False # logging.debug("OK '%s' passed", station_name) count_hit = count_hit + 1 diff --git a/ycast/my_recentlystation.py b/ycast/my_recentlystation.py index 8a5c36e..8944443 100644 --- a/ycast/my_recentlystation.py +++ b/ycast/my_recentlystation.py @@ -26,9 +26,10 @@ def to_params_txt(self): text_line = self.url + '|' + self.icon + '|' + str(self.vote) return text_line - def to_server_station(self,cathegory): + def to_server_station(self, cathegory): return my_stations.Station(self.name, self.url, cathegory, self.icon) + def signal_station_selected(name, url, icon): recently_station_list = get_stations_list() station_hit = StationVote(name, url + '|' + icon) @@ -93,6 +94,7 @@ def directory_name(): return list(get_recently_stations_dictionary().keys())[0] return DIRECTORY_NAME + # used in landing page def get_stations_by_vote(): station_list = get_stations_list() diff --git a/ycast/test_YCast.py b/ycast/test_YCast.py index 8932b62..2ef027e 100644 --- a/ycast/test_YCast.py +++ b/ycast/test_YCast.py @@ -13,6 +13,23 @@ class MyTestCase(unittest.TestCase): logging.getLogger().setLevel(logging.DEBUG) + def test_verify_values(self): + assert my_filter.verify_value( None, None ) + assert my_filter.verify_value( '', '' ) + assert my_filter.verify_value( None, '' ) + assert my_filter.verify_value( 3, 3 ) + assert my_filter.verify_value( '3', 3 ) + assert my_filter.verify_value( '3', '3' ) + assert my_filter.verify_value( '3,4,5,6', '5' ) + + assert not my_filter.verify_value( '', None ) + assert not my_filter.verify_value( '', '3' ) + assert not my_filter.verify_value( 3, 4 ) + assert not my_filter.verify_value( '3', 4 ) + assert not my_filter.verify_value( '4', '3' ) + assert not my_filter.verify_value( '3,4,5,6', '9' ) + + def test_init_filter(self): filter.init_filter() @@ -28,22 +45,24 @@ def test_init_filter(self): def test_valid_station(self): - filter.init_filter() + my_filter.init_filter() test_lines = generic.readlns_txt_file(generic.get_var_path()+"/test.json") + + test_lines = radiobrowser.get_stations_by_votes() + io = StringIO(test_lines[0]) stations_json = json.load(io) count = 0 for station_json in stations_json: - if filter.check_station(station_json): + if my_filter.check_station(station_json): station = radiobrowser.Station(station_json) count = count + 1 - logging.info("Stations (%d/%d)" , count, len(stations_json) ) - logging.info("Used filter parameter", filter.parameter_failed_list) - + my_filter.end_filter() - def test_popular_station(self): - stations = radiobrowser.get_stations_by_votes() + def test_life_popular_station(self): + #hard test for filter + stations = radiobrowser.get_stations_by_votes(10000000) logging.info("Stations (%d)", len(stations)) def test_recently_hit(self): From 3601528ad30ae6bb658f63b0199ab3a775e8fc8f Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Sat, 29 Jan 2022 19:14:27 +0100 Subject: [PATCH 20/51] logging reduced --- ycast/my_filter.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ycast/my_filter.py b/ycast/my_filter.py index 08a26dd..ade7566 100644 --- a/ycast/my_filter.py +++ b/ycast/my_filter.py @@ -89,8 +89,8 @@ def check_station(station_json): val = get_json_attr(station_json, param_name) if verify_value(black_list[param_name], val): parameter_failed_evt(param_name) - logging.debug("FAIL '%s' blacklist failed on '%s' '%s' == '%s'", - station_name, param_name, black_list[param_name], val) +# logging.debug("FAIL '%s' blacklist failed on '%s' '%s' == '%s'", +# station_name, param_name, black_list[param_name], val) return False # und verknüpft @@ -101,9 +101,8 @@ def check_station(station_json): # attribut in json vorhanden if not verify_value(white_list[param_name], val): parameter_failed_evt(param_name) - logging.debug("FAIL '%s' whitelist failed on '%s' '%s' == '%s'", - station_name, param_name, white_list[param_name], val) +# logging.debug("FAIL '%s' whitelist failed on '%s' '%s' == '%s'", +# station_name, param_name, white_list[param_name], val) return False -# logging.debug("OK '%s' passed", station_name) count_hit = count_hit + 1 return True From c818934a69fceb2f6bf2f0378b00c50bb507a542 Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Sat, 29 Jan 2022 20:23:54 +0100 Subject: [PATCH 21/51] filter languages country directories and reduce genre by higher level of station-counts --- ycast/my_filter.py | 2 +- ycast/radiobrowser.py | 20 ++++++------ ycast/test_YCast.py | 74 ++++++++++++++++++++++++++----------------- 3 files changed, 57 insertions(+), 39 deletions(-) diff --git a/ycast/my_filter.py b/ycast/my_filter.py index ade7566..54a1cf2 100644 --- a/ycast/my_filter.py +++ b/ycast/my_filter.py @@ -63,7 +63,7 @@ def verify_value(ref_val, val): return False -def chk_paramter(parameter_name, val): +def chk_parameter(parameter_name, val): if black_list: if parameter_name in black_list: if verify_value(black_list[parameter_name], val): diff --git a/ycast/radiobrowser.py b/ycast/radiobrowser.py index afb8649..20a836f 100644 --- a/ycast/radiobrowser.py +++ b/ycast/radiobrowser.py @@ -4,14 +4,14 @@ import requests import logging -from ycast import __version__ +from ycast import __version__, my_filter import ycast.vtuner as vtuner import ycast.generic as generic from ycast.my_filter import check_station, init_filter, end_filter from ycast.generic import get_json_attr API_ENDPOINT = "http://all.api.radio-browser.info" -MINIMUM_COUNT_GENRE = 5 +MINIMUM_COUNT_GENRE = 40 MINIMUM_COUNT_COUNTRY = 5 MINIMUM_COUNT_LANGUAGE = 5 DEFAULT_STATION_LIMIT = 200 @@ -72,14 +72,14 @@ def request(url): def get_station_by_id(vtune_id): global station_cache -# decode + # decode uidbase64 = generic.get_stationid_without_prefix(vtune_id) uid = str(uuid.UUID(base64.urlsafe_b64decode(uidbase64).hex())) if station_cache: station = station_cache[vtune_id] if station: return station -# no item in cache, do request + # no item in cache, do request station_json = request('stations/byuuid?uuids=' + uid) if station_json and len(station_json): station = Station(station_json[0]) @@ -99,8 +99,9 @@ def get_country_directories(): for country_raw in countries_raw: if get_json_attr(country_raw, 'name') and get_json_attr(country_raw, 'stationcount') and \ int(get_json_attr(country_raw, 'stationcount')) > MINIMUM_COUNT_COUNTRY: - country_directories.append(generic.Directory(get_json_attr(country_raw, 'name'), - get_json_attr(country_raw, 'stationcount'))) + if my_filter.chk_parameter('country', get_json_attr(country_raw, 'name')): + country_directories.append(generic.Directory(get_json_attr(country_raw, 'name'), + get_json_attr(country_raw, 'stationcount'))) return country_directories @@ -114,9 +115,10 @@ def get_language_directories(): for language_raw in languages_raw: if get_json_attr(language_raw, 'name') and get_json_attr(language_raw, 'stationcount') and \ int(get_json_attr(language_raw, 'stationcount')) > MINIMUM_COUNT_LANGUAGE: - language_directories.append(generic.Directory(get_json_attr(language_raw, 'name'), - get_json_attr(language_raw, 'stationcount'), - get_json_attr(language_raw, 'name').title())) + if my_filter.chk_parameter('languagecodes', get_json_attr(language_raw, 'iso_639')): + language_directories.append(generic.Directory(get_json_attr(language_raw, 'name'), + get_json_attr(language_raw, 'stationcount'), + get_json_attr(language_raw, 'name').title())) return language_directories diff --git a/ycast/test_YCast.py b/ycast/test_YCast.py index 2ef027e..c0ba879 100644 --- a/ycast/test_YCast.py +++ b/ycast/test_YCast.py @@ -14,36 +14,33 @@ class MyTestCase(unittest.TestCase): logging.getLogger().setLevel(logging.DEBUG) def test_verify_values(self): - assert my_filter.verify_value( None, None ) - assert my_filter.verify_value( '', '' ) - assert my_filter.verify_value( None, '' ) - assert my_filter.verify_value( 3, 3 ) - assert my_filter.verify_value( '3', 3 ) - assert my_filter.verify_value( '3', '3' ) - assert my_filter.verify_value( '3,4,5,6', '5' ) - - assert not my_filter.verify_value( '', None ) - assert not my_filter.verify_value( '', '3' ) - assert not my_filter.verify_value( 3, 4 ) - assert not my_filter.verify_value( '3', 4 ) - assert not my_filter.verify_value( '4', '3' ) - assert not my_filter.verify_value( '3,4,5,6', '9' ) - + assert my_filter.verify_value(None, None) + assert my_filter.verify_value('', '') + assert my_filter.verify_value(None, '') + assert my_filter.verify_value(3, 3) + assert my_filter.verify_value('3', 3) + assert my_filter.verify_value('3', '3') + assert my_filter.verify_value('3,4,5,6', '5') + + assert not my_filter.verify_value('', None) + assert not my_filter.verify_value('', '3') + assert not my_filter.verify_value(3, 4) + assert not my_filter.verify_value('3', 4) + assert not my_filter.verify_value('4', '3') + assert not my_filter.verify_value('3,4,5,6', '9') def test_init_filter(self): + my_filter.init_filter() - filter.init_filter() - - for elem in filter.filter_dir: + for elem in my_filter.filter_dir: logging.warning("Name filtertype: %s", elem) - filter_param = filter.filter_dir[elem] + filter_param = my_filter.filter_dir[elem] if filter_param: for par in filter_param: - logging.warning(" Name paramter: %s",par) + logging.warning(" Name paramter: %s", par) else: logging.warning(" ") - def test_valid_station(self): my_filter.init_filter() test_lines = generic.readlns_txt_file(generic.get_var_path()+"/test.json") @@ -61,10 +58,22 @@ def test_valid_station(self): my_filter.end_filter() def test_life_popular_station(self): - #hard test for filter + # hard test for filter stations = radiobrowser.get_stations_by_votes(10000000) logging.info("Stations (%d)", len(stations)) + def test_get_languages(self): + result = radiobrowser.get_language_directories() + assert len(result) == 3 + + def test_get_countries(self): + result = radiobrowser.get_country_directories() + assert len(result) == 4 + + def test_get_genre(self): + result = radiobrowser.get_genre_directories() + assert len(result) < 300 + def test_recently_hit(self): try: @@ -76,24 +85,29 @@ def test_recently_hit(self): assert len(result) == 0 result = my_recentlystation.get_recently_stations_dictionary() - assert result == None + assert result is None i = 0 while i < 10: - my_recentlystation.signal_station_selected('NAME '+ str(i),'http://dummy/'+ str(i), 'http://icon'+ str(i)) + my_recentlystation.signal_station_selected('NAME ' + str(i), 'http://dummy/' + str(i), + 'http://icon' + str(i)) i = i+1 result = my_recentlystation.get_recently_stations_dictionary() assert my_recentlystation.directory_name() assert result[my_recentlystation.directory_name()] - my_recentlystation.signal_station_selected('Konverenz: Sport' , 'http://dummy/' + str(i), 'http://icon' + str(i)) - my_recentlystation.signal_station_selected('Konverenz: Sport' , 'http://dummy/' + str(i), 'http://icon' + str(i)) - my_recentlystation.signal_station_selected('Konverenz: Sport' , 'http://dummy/' + str(i), 'http://icon' + str(i)) + my_recentlystation.signal_station_selected('Konverenz: Sport', 'http://dummy/' + str(i), + 'http://icon' + str(i)) + my_recentlystation.signal_station_selected('Konverenz: Sport', 'http://dummy/' + str(i), + 'http://icon' + str(i)) + my_recentlystation.signal_station_selected('Konverenz: Sport', 'http://dummy/' + str(i), + 'http://icon' + str(i)) i = 6 while i < 17: - my_recentlystation.signal_station_selected('NAME '+ str(i),'http://dummy/'+ str(i), 'http://icon'+ str(i)) + my_recentlystation.signal_station_selected('NAME ' + str(i), 'http://dummy/' + str(i), + 'http://icon' + str(i)) i = i+1 result = my_recentlystation.get_recently_stations_dictionary() @@ -106,11 +120,13 @@ def test_recently_hit(self): while j < 6: i = 6 while i < 9: - my_recentlystation.signal_station_selected('NAME '+ str(i),'http://dummy/'+ str(i), 'http://icon'+ str(i)) + my_recentlystation.signal_station_selected('NAME ' + str(i), 'http://dummy/' + str(i), + 'http://icon' + str(i)) i = i+1 j = j+1 result = my_recentlystation.get_stations_by_vote() assert len(result) == 5 + if __name__ == '__main__': unittest.main() From 87661436189b588282a041513ab3c752d233040f Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Sat, 29 Jan 2022 21:08:10 +0100 Subject: [PATCH 22/51] explanations in README.md --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cad8c5c..9cda033 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,10 @@ [Issue tracker](https://github.com/THanika/YCast/issues) ### The advanced feature: - * icons in my_stations stations.yml - * recently visited ./ycast/recently.yml - * global filter by ./ycast/filter.yml +* Icons in my favorites list 'stations.yml' (the icon URL can be appended after the pipe character '|') +* recently visited radio stations are stored in /.yast/resently.yml (compatible with stations.yml, for easy editing of your favorites and pasting into stations.yml) +* global filter configurable file ./ycast/filter.yml (with this you can globally reduce the radio stations according to your interests) +* 5 frequently used radio stations can be selected on the target page (self-learning algorithm based on frequency of station selection) YCast is a self hosted replacement for the vTuner internet radio service which many AVRs use. It emulates a vTuner backend to provide your AVR with the necessary information to play self defined categorized internet radio stations and listen to Radio stations listed in the [Community Radio Browser index](http://www.radio-browser.info). From f9c1c7ea750169638646aeb9a686034919039fdd Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Sun, 30 Jan 2022 06:32:21 +0100 Subject: [PATCH 23/51] radiobrowser limits configurable in filter.yml --- ycast/__init__.py | 2 +- ycast/my_filter.py | 31 +++++++++++++++++++++++-------- ycast/radiobrowser.py | 12 ++++++------ ycast/test_YCast.py | 9 +++++++-- 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/ycast/__init__.py b/ycast/__init__.py index 58d478a..923b987 100644 --- a/ycast/__init__.py +++ b/ycast/__init__.py @@ -1 +1 @@ -__version__ = '1.2.0' +__version__ = '1.2.2' diff --git a/ycast/my_filter.py b/ycast/my_filter.py index 54a1cf2..0fb0327 100644 --- a/ycast/my_filter.py +++ b/ycast/my_filter.py @@ -5,30 +5,31 @@ white_list = {} black_list = {} -filter_dir = {} +filter_dictionary = {} parameter_failed_list = {} count_used = 0 count_hit = 0 +LIMITS_NAME = 'limits' def init_filter(): global white_list global black_list global parameter_failed_list - global filter_dir + global filter_dictionary global count_used global count_hit count_used = 0 count_hit = 0 - filter_dir = generic.read_yaml_file(generic.get_var_path() + '/filter.yml') - if filter_dir: - white_list = filter_dir['whitelist'] - black_list = filter_dir['blacklist'] + filter_dictionary = generic.read_yaml_file(generic.get_var_path() + '/filter.yml') + if filter_dictionary: + white_list = filter_dictionary['whitelist'] + black_list = filter_dictionary['blacklist'] else: white_list = {'lastcheckok': 1} black_list = {} - filter_dir = {'whitelist': white_list, 'blacklist': black_list} - generic.write_yaml_file(generic.get_var_path() + '/filter.yml', filter_dir) + filter_dictionary = {'whitelist': white_list, 'blacklist': black_list} + generic.write_yaml_file(generic.get_var_path() + '/filter.yml', filter_dictionary) parameter_failed_list.clear() return @@ -106,3 +107,17 @@ def check_station(station_json): return False count_hit = count_hit + 1 return True + + +def get_limit(param_name, default): + global filter_dictionary + filter_dictionary = generic.read_yaml_file(generic.get_var_path() + '/filter.yml') + limits_dict = {} + if LIMITS_NAME in filter_dictionary: + limits_dict = filter_dictionary[LIMITS_NAME] + if param_name in limits_dict: + return limits_dict[param_name] + limits_dict[param_name] = default + filter_dictionary[LIMITS_NAME] = limits_dict + generic.write_yaml_file(generic.get_var_path() + '/filter.yml', filter_dictionary) + return default diff --git a/ycast/radiobrowser.py b/ycast/radiobrowser.py index 20a836f..85e094b 100644 --- a/ycast/radiobrowser.py +++ b/ycast/radiobrowser.py @@ -11,17 +11,17 @@ from ycast.generic import get_json_attr API_ENDPOINT = "http://all.api.radio-browser.info" -MINIMUM_COUNT_GENRE = 40 -MINIMUM_COUNT_COUNTRY = 5 -MINIMUM_COUNT_LANGUAGE = 5 -DEFAULT_STATION_LIMIT = 200 -SHOW_BROKEN_STATIONS = False -SHOW_WITHOUT_FAVICON = False +MINIMUM_COUNT_GENRE = my_filter.get_limit('MINIMUM_COUNT_GENRE', 40) +MINIMUM_COUNT_COUNTRY = my_filter.get_limit('MINIMUM_COUNT_COUNTRY', 5) +MINIMUM_COUNT_LANGUAGE = my_filter.get_limit('MINIMUM_COUNT_LANGUAGE', 5) +DEFAULT_STATION_LIMIT = my_filter.get_limit('DEFAULT_STATION_LIMIT', 200) +SHOW_BROKEN_STATIONS = my_filter.get_limit('SHOW_BROKEN_STATIONS', False) ID_PREFIX = "RB" station_cache = {} + class Station: def __init__(self, station_json): self.stationuuid = generic.get_json_attr(station_json, 'stationuuid') diff --git a/ycast/test_YCast.py b/ycast/test_YCast.py index c0ba879..0d6da73 100644 --- a/ycast/test_YCast.py +++ b/ycast/test_YCast.py @@ -32,9 +32,9 @@ def test_verify_values(self): def test_init_filter(self): my_filter.init_filter() - for elem in my_filter.filter_dir: + for elem in my_filter.filter_dictionary: logging.warning("Name filtertype: %s", elem) - filter_param = my_filter.filter_dir[elem] + filter_param = my_filter.filter_dictionary[elem] if filter_param: for par in filter_param: logging.warning(" Name paramter: %s", par) @@ -74,6 +74,11 @@ def test_get_genre(self): result = radiobrowser.get_genre_directories() assert len(result) < 300 + def test_get_limits(self): + result = my_filter.get_limit('irgendwas',20) + assert result == 20 + + def test_recently_hit(self): try: From 9eefaa01e6ee9fef995bbe1840e1cbea48b9d946 Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Sun, 30 Jan 2022 06:41:22 +0100 Subject: [PATCH 24/51] radiobrowser limits configurable in filter.yml --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9cda033..b616500 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ [Issue tracker](https://github.com/THanika/YCast/issues) +#### pip3 install git+https://github.com/ThHanika/YCast + ### The advanced feature: * Icons in my favorites list 'stations.yml' (the icon URL can be appended after the pipe character '|') * recently visited radio stations are stored in /.yast/resently.yml (compatible with stations.yml, for easy editing of your favorites and pasting into stations.yml) From 1338430f271eea813499efd83ba65e1533a96497 Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Mon, 31 Jan 2022 22:27:16 +0100 Subject: [PATCH 25/51] refaktor to use workingdirectory --- README.md | 2 +- examples/ycast.service.example | 12 ------------ examples/ycast.service.example_root | 15 +++++++++++++++ examples/ycast.service.example_ycast | 15 +++++++++++++++ ycast/generic.py | 2 +- ycast/my_filter.py | 18 ++++++++++++------ 6 files changed, 44 insertions(+), 20 deletions(-) delete mode 100644 examples/ycast.service.example create mode 100644 examples/ycast.service.example_root create mode 100644 examples/ycast.service.example_ycast diff --git a/README.md b/README.md index b616500..f3c34cc 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ You can redirect all traffic destined for the original request URL (e.g. `radioy __Attention__: Do not rewrite the requests transparently. YCast expects the complete URL (i.e. including `/ycast` or `/setupapp`). It also need an intact `Host` header; so if you're proxying YCast you need to pass the original header on. For Nginx, this can be accomplished with `proxy_set_header Host $host;`. In case you are using (or plan on using) Nginx to proxy requests, have a look at [this example](examples/nginx-ycast.conf.example). -This can be used together with [this systemd service example](examples/ycast.service.example) for a fully functional deployment. +This can be used together with [this systemd service example](examples/ycast.service.example_ycast) for a fully functional deployment. #### With WSGI diff --git a/examples/ycast.service.example b/examples/ycast.service.example deleted file mode 100644 index cefa7cb..0000000 --- a/examples/ycast.service.example +++ /dev/null @@ -1,12 +0,0 @@ -[Unit] -Description=YCast internet radio service -After=network.target - -[Service] -Type=simple -User=ycast -Group=ycast -ExecStart=/usr/bin/python3 -m ycast -l 127.0.0.1 -p 8010 - -[Install] -WantedBy=multi-user.target diff --git a/examples/ycast.service.example_root b/examples/ycast.service.example_root new file mode 100644 index 0000000..412521d --- /dev/null +++ b/examples/ycast.service.example_root @@ -0,0 +1,15 @@ +[Unit] +Description=YCast internet radio service +After=network.target + +[Service] +Type=simple +WorkingDirectory=/var/www/ycast +StandardOutput=file:/var/www/ycast/service.log +StandardError=file:/var/www/ycast/ycast.log +Restart=always +RestartSec=130 +ExecStart=/usr/bin/python3 -m ycast -c /var/www/ycast/stations.yml -d + +[Install] +WantedBy=multi-user.target diff --git a/examples/ycast.service.example_ycast b/examples/ycast.service.example_ycast new file mode 100644 index 0000000..a7267d1 --- /dev/null +++ b/examples/ycast.service.example_ycast @@ -0,0 +1,15 @@ +[Unit] +Description=YCast internet radio service +After=network.target + +[Service] +Type=simple +User=ycast +Group=ycast +WorkingDirectory=/home/ycast +StandardOutput=file:/home/ycast/service.log +StandardError=file:/home/ycast/ycast.log +ExecStart=/usr/bin/python3 -m ycast -l 127.0.0.1 -p 8010 -d -c /home/ycast/.ycast/stations.yml + +[Install] +WantedBy=multi-user.target diff --git a/ycast/generic.py b/ycast/generic.py index 379053b..78aa3f3 100644 --- a/ycast/generic.py +++ b/ycast/generic.py @@ -4,7 +4,7 @@ import yaml USER_AGENT = 'YCast' -VAR_PATH = os.path.expanduser("~") + '/.ycast' +VAR_PATH = '.ycast' CACHE_PATH = VAR_PATH + '/cache' diff --git a/ycast/my_filter.py b/ycast/my_filter.py index 0fb0327..a4a765d 100644 --- a/ycast/my_filter.py +++ b/ycast/my_filter.py @@ -23,11 +23,15 @@ def init_filter(): count_hit = 0 filter_dictionary = generic.read_yaml_file(generic.get_var_path() + '/filter.yml') if filter_dictionary: - white_list = filter_dictionary['whitelist'] - black_list = filter_dictionary['blacklist'] - else: - white_list = {'lastcheckok': 1} - black_list = {} + if 'whitelist' in filter_dictionary: + white_list = filter_dictionary['whitelist'] + else: + white_list = {'lastcheckok': 1} + + if 'blacklist' in filter_dictionary: + black_list = filter_dictionary['blacklist'] + else: + black_list = {} filter_dictionary = {'whitelist': white_list, 'blacklist': black_list} generic.write_yaml_file(generic.get_var_path() + '/filter.yml', filter_dictionary) @@ -111,7 +115,9 @@ def check_station(station_json): def get_limit(param_name, default): global filter_dictionary - filter_dictionary = generic.read_yaml_file(generic.get_var_path() + '/filter.yml') + tempdict = generic.read_yaml_file(generic.get_var_path() + '/filter.yml') + if tempdict is not None: + filter_dictionary = tempdict limits_dict = {} if LIMITS_NAME in filter_dictionary: limits_dict = filter_dictionary[LIMITS_NAME] From 48072f9d7d0d6548ab5c83e710a3c7b91f89ace0 Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Tue, 1 Feb 2022 12:34:21 +0100 Subject: [PATCH 26/51] optimize for using as root-service with confitured workingdirectory --- examples/ycast.service.example_root | 10 +++- ycast/__main__.py | 7 +++ ycast/generic.py | 75 +++++++++++++++++++++++++++-- ycast/my_filter.py | 59 ++++++++++++++++------- ycast/my_recentlystation.py | 8 +-- ycast/my_stations.py | 14 +----- ycast/radiobrowser.py | 23 ++++----- ycast/server.py | 8 +-- ycast/test_YCast.py | 4 +- 9 files changed, 145 insertions(+), 63 deletions(-) diff --git a/examples/ycast.service.example_root b/examples/ycast.service.example_root index 412521d..030a6c9 100644 --- a/examples/ycast.service.example_root +++ b/examples/ycast.service.example_root @@ -5,8 +5,14 @@ After=network.target [Service] Type=simple WorkingDirectory=/var/www/ycast -StandardOutput=file:/var/www/ycast/service.log -StandardError=file:/var/www/ycast/ycast.log + +# StandardOutput=file:/var/www/ycast/service.log +# StandardError=file:/var/www/ycast/ycast.log + +StandardOutput=syslog +StandardError=syslog +SyslogIdentifier=ycast + Restart=always RestartSec=130 ExecStart=/usr/bin/python3 -m ycast -c /var/www/ycast/stations.yml -d diff --git a/ycast/__main__.py b/ycast/__main__.py index 9b77024..8826c79 100755 --- a/ycast/__main__.py +++ b/ycast/__main__.py @@ -23,6 +23,13 @@ def launch_server(): logging.debug("Debug logging enabled") else: logging.getLogger('werkzeug').setLevel(logging.WARNING) + + # initialize important ycast parameters + from ycast.generic import init_base_dir + init_base_dir('/.ycast') + from ycast.my_filter import init_limits_and_filters + init_limits_and_filters() + server.run(arguments.config, arguments.address, arguments.port) diff --git a/ycast/generic.py b/ycast/generic.py index 78aa3f3..3b20ba8 100644 --- a/ycast/generic.py +++ b/ycast/generic.py @@ -1,11 +1,17 @@ import logging import os import hashlib +import sys + import yaml + USER_AGENT = 'YCast' -VAR_PATH = '.ycast' -CACHE_PATH = VAR_PATH + '/cache' + +# initialize it start +VAR_PATH = '' +CACHE_PATH = '' +stations_file_by_config = '' class Directory: @@ -18,6 +24,46 @@ def __init__(self, name, item_count, displayname=None): self.displayname = name +def mk_writeable_dir(path): + try: + os.makedirs(path) + except FileExistsError: + pass + except Exception as ex: + logging.error("Could not create base folder (%s) because of access permissions: %s", path, ex) + return None + return path + + +def init_base_dir(path_element): + global VAR_PATH, CACHE_PATH + logging.info('Initialize base directory %s', path_element) + logging.debug(' HOME: %s',os.path.expanduser("~")) + logging.debug(' PWD: %s',os.getcwd()) + var_dir = None + + if not os.getcwd().endswith('/ycast'): + # specified working dir with /ycast has prio + try_path = os.path.expanduser("~") + path_element + logging.info(' try Home-Dir: %s', try_path) + var_dir = mk_writeable_dir(try_path) + + if var_dir is None: + # avoid using root '/' and it's subdir + if len(os.getcwd()) < 6: + logging.error(" len(PWD) < 6 (PWD is too small) < 6: '%s'", os.getcwd()) + else: + try_path = os.getcwd() + path_element + logging.info(' try Work-Dir: %s', try_path) + var_dir = mk_writeable_dir(os.getcwd() + path_element) + if var_dir is None: + sys.exit('YCast: ###### No usable directory found #######, I give up....') + logging.info('using var directory: %s', var_dir) + VAR_PATH = var_dir + CACHE_PATH = var_dir + '/cache' + return + + def generate_stationid_with_prefix(uid, prefix): if not prefix or len(prefix) != 2: logging.error("Invalid station prefix length (must be 2)") @@ -67,6 +113,27 @@ def get_var_path(): return VAR_PATH +def get_recently_file(): + return get_var_path() + '/recently.yml' + + +def get_filter_file(): + return get_var_path() + '/filter.yml' + + +def get_stations_file(): + global stations_file_by_config + if stations_file_by_config: + return stations_file_by_config + return get_var_path() + '/stations.yml' + + +def set_stations_file(stations_file): + global stations_file_by_config + if stations_file: + stations_file_by_config = stations_file + + def get_checksum(feed, charlimit=12): hash_feed = feed.encode() hash_object = hashlib.md5(hash_feed) @@ -83,7 +150,7 @@ def read_yaml_file(file_name): with open(file_name, 'r') as f: return yaml.safe_load(f) except FileNotFoundError: - logging.error("YAML file '%s' not found", file_name) + logging.warning("YAML file '%s' not found", file_name) except yaml.YAMLError as e: logging.error("YAML format error in '%':\n %s", file_name, e) return None @@ -107,7 +174,7 @@ def readlns_txt_file(file_name): with open(file_name, 'r') as f: return f.readlines() except FileNotFoundError: - logging.error("TXT file '%s' not found", file_name) + logging.warning("TXT file '%s' not found", file_name) return None diff --git a/ycast/my_filter.py b/ycast/my_filter.py index a4a765d..d612830 100644 --- a/ycast/my_filter.py +++ b/ycast/my_filter.py @@ -9,31 +9,56 @@ parameter_failed_list = {} count_used = 0 count_hit = 0 + LIMITS_NAME = 'limits' +MINIMUM_COUNT_GENRE = 40 +MINIMUM_COUNT_COUNTRY = 5 +MINIMUM_COUNT_LANGUAGE = 5 +DEFAULT_STATION_LIMIT = 200 +SHOW_BROKEN_STATIONS = False + + +def init_limits_and_filters(): + global MINIMUM_COUNT_GENRE, MINIMUM_COUNT_LANGUAGE, MINIMUM_COUNT_COUNTRY, DEFAULT_STATION_LIMIT, SHOW_BROKEN_STATIONS + logging.info('Initialize Limits and Filters') + init_filter_file() + MINIMUM_COUNT_GENRE = get_limit('MINIMUM_COUNT_GENRE', 40) + MINIMUM_COUNT_COUNTRY = get_limit('MINIMUM_COUNT_COUNTRY', 5) + MINIMUM_COUNT_LANGUAGE = get_limit('MINIMUM_COUNT_LANGUAGE', 5) + DEFAULT_STATION_LIMIT = get_limit('DEFAULT_STATION_LIMIT', 200) + SHOW_BROKEN_STATIONS = get_limit('SHOW_BROKEN_STATIONS', False) + +def init_filter_file(): + global filter_dictionary, white_list, black_list + filter_dictionary = generic.read_yaml_file(generic.get_filter_file()) + is_updated = False + if filter_dictionary is None: + filter_dictionary = {} + is_updated = True + if 'whitelist' in filter_dictionary: + white_list = filter_dictionary['whitelist'] + else: + white_list = {'lastcheckok': 1} + is_updated = True + if 'blacklist' in filter_dictionary: + black_list = filter_dictionary['blacklist'] + else: + black_list = {} + is_updated = True + + if is_updated: + filter_dictionary = {'whitelist': white_list, 'blacklist': black_list} + generic.write_yaml_file(generic.get_var_path() + '/filter.yml', filter_dictionary) -def init_filter(): - global white_list - global black_list +def begin_filter(): global parameter_failed_list - global filter_dictionary global count_used global count_hit count_used = 0 count_hit = 0 - filter_dictionary = generic.read_yaml_file(generic.get_var_path() + '/filter.yml') - if filter_dictionary: - if 'whitelist' in filter_dictionary: - white_list = filter_dictionary['whitelist'] - else: - white_list = {'lastcheckok': 1} - - if 'blacklist' in filter_dictionary: - black_list = filter_dictionary['blacklist'] - else: - black_list = {} - filter_dictionary = {'whitelist': white_list, 'blacklist': black_list} - generic.write_yaml_file(generic.get_var_path() + '/filter.yml', filter_dictionary) + + init_filter_file() parameter_failed_list.clear() return diff --git a/ycast/my_recentlystation.py b/ycast/my_recentlystation.py index 8944443..c410ff9 100644 --- a/ycast/my_recentlystation.py +++ b/ycast/my_recentlystation.py @@ -1,11 +1,11 @@ from ycast import generic, my_stations +from ycast.generic import get_recently_file MAX_ENTRIES = 15 -# define a max, so after 5 hits, an other station is get better votes +# define a max, so after 5 hits, another station is get better votes MAX_VOTES = 5 DIRECTORY_NAME = "recently used" -recently_file = generic.get_var_path() + '/recently.yml' recently_station_dictionary = None voted5_station_dictinary = None @@ -56,7 +56,7 @@ def signal_station_selected(name, url, icon): def set_recently_station_dictionary(station_dict): global recently_station_dictionary recently_station_dictionary = station_dict - generic.write_yaml_file(recently_file, recently_station_dictionary) + generic.write_yaml_file(get_recently_file(), recently_station_dictionary) def mk_station_dictionary(cathegory, station_list): @@ -84,7 +84,7 @@ def get_recently_stations_dictionary(): # cached recently global recently_station_dictionary if not recently_station_dictionary: - recently_station_dictionary = generic.read_yaml_file(recently_file) + recently_station_dictionary = generic.read_yaml_file(get_recently_file()) return recently_station_dictionary diff --git a/ycast/my_stations.py b/ycast/my_stations.py index 0648b5e..2afad46 100644 --- a/ycast/my_stations.py +++ b/ycast/my_stations.py @@ -4,8 +4,6 @@ ID_PREFIX = "MY" -config_file = generic.get_var_path() + '/stations.yml' - class Station: def __init__(self, name, url, category, icon): @@ -20,16 +18,6 @@ def to_vtuner(self): return vtuner.Station(self.id, self.name, self.tag, self.url, self.icon, self.tag, None, None, None, None) -def set_config(config): - global config_file - if config: - config_file = config - if get_stations_yaml(): - return True - else: - return False - - def get_station_by_id(vtune_id): my_stations_yaml = get_stations_yaml() if my_stations_yaml: @@ -43,7 +31,7 @@ def get_station_by_id(vtune_id): def get_stations_yaml(): from ycast.my_recentlystation import get_recently_stations_dictionary my_recently_station = get_recently_stations_dictionary() - my_stations = generic.read_yaml_file(config_file) + my_stations = generic.read_yaml_file(generic.get_stations_file()) if my_stations: if my_recently_station: my_stations.update(my_recently_station) diff --git a/ycast/radiobrowser.py b/ycast/radiobrowser.py index 85e094b..764f56b 100644 --- a/ycast/radiobrowser.py +++ b/ycast/radiobrowser.py @@ -7,21 +7,16 @@ from ycast import __version__, my_filter import ycast.vtuner as vtuner import ycast.generic as generic -from ycast.my_filter import check_station, init_filter, end_filter +from ycast.my_filter import check_station, begin_filter, end_filter, SHOW_BROKEN_STATIONS, MINIMUM_COUNT_COUNTRY, \ + MINIMUM_COUNT_LANGUAGE, MINIMUM_COUNT_GENRE, DEFAULT_STATION_LIMIT from ycast.generic import get_json_attr API_ENDPOINT = "http://all.api.radio-browser.info" -MINIMUM_COUNT_GENRE = my_filter.get_limit('MINIMUM_COUNT_GENRE', 40) -MINIMUM_COUNT_COUNTRY = my_filter.get_limit('MINIMUM_COUNT_COUNTRY', 5) -MINIMUM_COUNT_LANGUAGE = my_filter.get_limit('MINIMUM_COUNT_LANGUAGE', 5) -DEFAULT_STATION_LIMIT = my_filter.get_limit('DEFAULT_STATION_LIMIT', 200) -SHOW_BROKEN_STATIONS = my_filter.get_limit('SHOW_BROKEN_STATIONS', False) ID_PREFIX = "RB" station_cache = {} - class Station: def __init__(self, station_json): self.stationuuid = generic.get_json_attr(station_json, 'stationuuid') @@ -90,7 +85,7 @@ def get_station_by_id(vtune_id): def get_country_directories(): - init_filter() + begin_filter() country_directories = [] apicall = 'countries' if not SHOW_BROKEN_STATIONS: @@ -106,7 +101,7 @@ def get_country_directories(): def get_language_directories(): - init_filter() + begin_filter() language_directories = [] apicall = 'languages' if not SHOW_BROKEN_STATIONS: @@ -138,7 +133,7 @@ def get_genre_directories(): def get_stations_by_country(country): - init_filter() + begin_filter() station_cache.clear() stations = [] stations_list_json = request('stations/search?order=name&reverse=false&countryExact=true&country=' + str(country)) @@ -152,7 +147,7 @@ def get_stations_by_country(country): def get_stations_by_language(language): - init_filter() + begin_filter() station_cache.clear() stations = [] stations_list_json = \ @@ -167,7 +162,7 @@ def get_stations_by_language(language): def get_stations_by_genre(genre): - init_filter() + begin_filter() station_cache.clear() stations = [] stations_list_json = request('stations/search?order=name&reverse=false&tagExact=true&tag=' + str(genre)) @@ -181,7 +176,7 @@ def get_stations_by_genre(genre): def get_stations_by_votes(limit=DEFAULT_STATION_LIMIT): - init_filter() + begin_filter() station_cache.clear() stations = [] stations_list_json = request('stations?order=votes&reverse=true&limit=' + str(limit)) @@ -195,7 +190,7 @@ def get_stations_by_votes(limit=DEFAULT_STATION_LIMIT): def search(name, limit=DEFAULT_STATION_LIMIT): - init_filter() + begin_filter() station_cache.clear() stations = [] stations_list_json = request('stations/search?order=name&reverse=false&limit=' + str(limit) + '&name=' + str(name)) diff --git a/ycast/server.py b/ycast/server.py index 8ffdb3d..ac97651 100644 --- a/ycast/server.py +++ b/ycast/server.py @@ -24,23 +24,17 @@ PATH_RADIOBROWSER_POPULAR = 'popular' station_tracking = False -my_stations_enabled = False app = Flask(__name__) def run(config, address='0.0.0.0', port=8010): try: - check_my_stations_feature(config) + generic.set_stations_file(config) app.run(host=address, port=port) except PermissionError: logging.error("No permission to create socket. Are you trying to use ports below 1024 without elevated rights?") -def check_my_stations_feature(config): - global my_stations_enabled - my_stations_enabled = my_stations.set_config(config) - - def get_directories_page(subdir, directories, request_obj): page = vtuner.Page() if len(directories) == 0: diff --git a/ycast/test_YCast.py b/ycast/test_YCast.py index 0d6da73..3b1c515 100644 --- a/ycast/test_YCast.py +++ b/ycast/test_YCast.py @@ -30,7 +30,7 @@ def test_verify_values(self): assert not my_filter.verify_value('3,4,5,6', '9') def test_init_filter(self): - my_filter.init_filter() + my_filter.begin_filter() for elem in my_filter.filter_dictionary: logging.warning("Name filtertype: %s", elem) @@ -42,7 +42,7 @@ def test_init_filter(self): logging.warning(" ") def test_valid_station(self): - my_filter.init_filter() + my_filter.begin_filter() test_lines = generic.readlns_txt_file(generic.get_var_path()+"/test.json") test_lines = radiobrowser.get_stations_by_votes() From eea9f87b9520bfe7a67f124cb99bf792ea6fb1cd Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Tue, 1 Feb 2022 12:35:28 +0100 Subject: [PATCH 27/51] optimize for using as root-service with confitured workingdirectory --- ycast/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ycast/__init__.py b/ycast/__init__.py index 923b987..5a5df3b 100644 --- a/ycast/__init__.py +++ b/ycast/__init__.py @@ -1 +1 @@ -__version__ = '1.2.2' +__version__ = '1.2.3' From 9b4a5f4f1b3b9b42334456a1103bdea6e966df13 Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Wed, 2 Feb 2022 19:29:46 +0100 Subject: [PATCH 28/51] refactor and add a spacer (undefined type) for items (workaround for yamaha) --- ycast/server.py | 81 ++++++++++++++++++++++++------------------------- ycast/vtuner.py | 10 +++++- 2 files changed, 48 insertions(+), 43 deletions(-) diff --git a/ycast/server.py b/ycast/server.py index ac97651..14d8332 100644 --- a/ycast/server.py +++ b/ycast/server.py @@ -38,22 +38,23 @@ def run(config, address='0.0.0.0', port=8010): def get_directories_page(subdir, directories, request_obj): page = vtuner.Page() if len(directories) == 0: - page.add(vtuner.Display("No entries found")) + page.add_item(vtuner.Display("No entries found")) page.set_count(1) return page for directory in get_paged_elements(directories, request_obj.args): vtuner_directory = vtuner.Directory(directory.displayname, url_for(subdir, _external=True, directory=directory.name), directory.item_count) - page.add(vtuner_directory) + page.add_item(vtuner_directory) page.set_count(len(directories)) return page def get_stations_page(stations, request_obj): page = vtuner.Page() + page.add_item(vtuner.Previous(url_for('landing', _external=True))) if len(stations) == 0: - page.add(vtuner.Display("No stations found")) + page.add_item(vtuner.Display("No stations found")) page.set_count(1) return page for station in get_paged_elements(stations, request_obj.args): @@ -62,7 +63,7 @@ def get_stations_page(stations, request_obj): vtuner_station.set_trackurl( request_obj.host_url + PATH_ROOT + '/' + PATH_PLAY + '?id=' + vtuner_station.uid) vtuner_station.icon = request_obj.host_url + PATH_ROOT + '/' + PATH_ICON + '?id=' + vtuner_station.uid - page.add(vtuner_station) + page.add_item(vtuner_station) page.set_count(len(stations)) return page @@ -117,7 +118,7 @@ def vtuner_redirect(url): @app.route('/setupapp/', methods=['GET', 'POST']) def upstream(path): - logging.debug('upstream **********************: %s', request.url) + logging.debug('upstream **********************') if request.args.get('token') == '0': return vtuner.get_init_token() if request.args.get('search'): @@ -141,41 +142,37 @@ def upstream(path): defaults={'path': ''}, methods=['GET', 'POST']) def landing(path=''): - logging.debug('**********************: %s', request.url) + logging.debug('===============================================================') page = vtuner.Page() - page.add(vtuner.Directory('Radiobrowser', url_for('radiobrowser_landing', _external=True), 4)) - page.add(vtuner.Directory('My Stations', url_for('my_stations_landing', _external=True), - len(my_stations.get_category_directories()))) + page.add_item(vtuner.Directory('Radiobrowser', url_for('radiobrowser_landing', _external=True), 4)) + + page.add_item(vtuner.Directory('My Stations', url_for('my_stations_landing', _external=True), + len(my_stations.get_category_directories()))) stations = my_recentlystation.get_stations_by_vote() if stations and len(stations) > 0: # make blank line (display is not shown) - # page.add(vtuner.Directory(' ', url_for('my_stations_landing', _external=True), - # len(my_stations.get_category_directories()))) + page.add_item(vtuner.Spacer()) - vtuner_station = stations[0].to_vtuner() - vtuner_station.icon = request.host_url + PATH_ROOT + '/' + PATH_ICON + '?id=' + vtuner_station.uid - vtuner_station.name = ' ' - page.add(vtuner_station) for station in stations: vtuner_station = station.to_vtuner() if station_tracking: vtuner_station.set_trackurl( request.host_url + PATH_ROOT + '/' + PATH_PLAY + '?id=' + vtuner_station.uid) vtuner_station.icon = request.host_url + PATH_ROOT + '/' + PATH_ICON + '?id=' + vtuner_station.uid - page.add(vtuner_station) + page.add_item(vtuner_station) else: - page.add(vtuner.Display("'My Stations' feature not configured.")) - page.set_count(1) + page.add_item(vtuner.Display("'My Stations' feature not configured.")) + page.set_count(-1) return page.to_string() @app.route('/' + PATH_ROOT + '/' + PATH_MY_STATIONS + '/', methods=['GET', 'POST']) def my_stations_landing(): - logging.debug('**********************: %s', request.url) + logging.debug('===============================================================') directories = my_stations.get_category_directories() return get_directories_page('my_stations_category', directories, request).to_string() @@ -183,7 +180,7 @@ def my_stations_landing(): @app.route('/' + PATH_ROOT + '/' + PATH_MY_STATIONS + '/', methods=['GET', 'POST']) def my_stations_category(directory): - logging.debug('**********************: %s', request.url) + logging.debug('===============================================================') stations = my_stations.get_stations_by_category(directory) return get_stations_page(stations, request).to_string() @@ -191,16 +188,16 @@ def my_stations_category(directory): @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/', methods=['GET', 'POST']) def radiobrowser_landing(): - logging.debug('**********************: %s', request.url) + logging.debug('===============================================================') page = vtuner.Page() - page.add(vtuner.Directory('Genres', url_for('radiobrowser_genres', _external=True), - len(radiobrowser.get_genre_directories()))) - page.add(vtuner.Directory('Countries', url_for('radiobrowser_countries', _external=True), - len(radiobrowser.get_country_directories()))) - page.add(vtuner.Directory('Languages', url_for('radiobrowser_languages', _external=True), - len(radiobrowser.get_language_directories()))) - page.add(vtuner.Directory('Most Popular', url_for('radiobrowser_popular', _external=True), - len(radiobrowser.get_stations_by_votes()))) + page.add_item(vtuner.Directory('Genres', url_for('radiobrowser_genres', _external=True), + len(radiobrowser.get_genre_directories()))) + page.add_item(vtuner.Directory('Countries', url_for('radiobrowser_countries', _external=True), + len(radiobrowser.get_country_directories()))) + page.add_item(vtuner.Directory('Languages', url_for('radiobrowser_languages', _external=True), + len(radiobrowser.get_language_directories()))) + page.add_item(vtuner.Directory('Most Popular', url_for('radiobrowser_popular', _external=True), + len(radiobrowser.get_stations_by_votes()))) page.set_count(4) return page.to_string() @@ -208,7 +205,7 @@ def radiobrowser_landing(): @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_COUNTRY + '/', methods=['GET', 'POST']) def radiobrowser_countries(): - logging.debug('**********************: %s', request.url) + logging.debug('===============================================================') directories = radiobrowser.get_country_directories() return get_directories_page('radiobrowser_country_stations', directories, request).to_string() @@ -216,7 +213,7 @@ def radiobrowser_countries(): @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_COUNTRY + '/', methods=['GET', 'POST']) def radiobrowser_country_stations(directory): - logging.debug('**********************: %s', request.url) + logging.debug('===============================================================') stations = radiobrowser.get_stations_by_country(directory) return get_stations_page(stations, request).to_string() @@ -224,7 +221,7 @@ def radiobrowser_country_stations(directory): @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_LANGUAGE + '/', methods=['GET', 'POST']) def radiobrowser_languages(): - logging.debug('**********************: %s', request.url) + logging.debug('===============================================================') directories = radiobrowser.get_language_directories() return get_directories_page('radiobrowser_language_stations', directories, request).to_string() @@ -232,7 +229,7 @@ def radiobrowser_languages(): @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_LANGUAGE + '/', methods=['GET', 'POST']) def radiobrowser_language_stations(directory): - logging.debug('**********************: %s', request.url) + logging.debug('===============================================================') stations = radiobrowser.get_stations_by_language(directory) return get_stations_page(stations, request).to_string() @@ -240,7 +237,7 @@ def radiobrowser_language_stations(directory): @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_GENRE + '/', methods=['GET', 'POST']) def radiobrowser_genres(): - logging.debug('**********************: %s', request.url) + logging.debug('===============================================================') directories = radiobrowser.get_genre_directories() return get_directories_page('radiobrowser_genre_stations', directories, request).to_string() @@ -248,7 +245,7 @@ def radiobrowser_genres(): @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_GENRE + '/', methods=['GET', 'POST']) def radiobrowser_genre_stations(directory): - logging.debug('**********************: %s', request.url) + logging.debug('===============================================================') stations = radiobrowser.get_stations_by_genre(directory) return get_stations_page(stations, request).to_string() @@ -256,7 +253,7 @@ def radiobrowser_genre_stations(directory): @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_POPULAR + '/', methods=['GET', 'POST']) def radiobrowser_popular(): - logging.debug('**********************: %s', request.url) + logging.debug('===============================================================') stations = radiobrowser.get_stations_by_votes() return get_stations_page(stations, request).to_string() @@ -264,11 +261,11 @@ def radiobrowser_popular(): @app.route('/' + PATH_ROOT + '/' + PATH_SEARCH + '/', methods=['GET', 'POST']) def station_search(): - logging.debug('**********************: %s', request.url) + logging.debug('===============================================================') query = request.args.get('search') if not query or len(query) < 3: page = vtuner.Page() - page.add(vtuner.Display("Search query too short")) + page.add_item(vtuner.Display("Search query too short")) page.set_count(1) return page.to_string() else: @@ -280,7 +277,7 @@ def station_search(): @app.route('/' + PATH_ROOT + '/' + PATH_PLAY, methods=['GET', 'POST']) def get_stream_url(): - logging.debug('**********************: %s', request.url) + logging.debug('===============================================================') stationid = request.args.get('id') if not stationid: logging.error("Stream URL without station ID requested") @@ -296,7 +293,7 @@ def get_stream_url(): @app.route('/' + PATH_ROOT + '/' + PATH_STATION, methods=['GET', 'POST']) def get_station_info(): - logging.debug('**********************: %s', request.url) + logging.debug('===============================================================') stationid = request.args.get('id') if not stationid: logging.error("Station info without station ID requested") @@ -305,7 +302,7 @@ def get_station_info(): if not station: logging.error("Could not get station with id '%s'", stationid) page = vtuner.Page() - page.add(vtuner.Display("Station not found")) + page.add_item(vtuner.Display("Station not found")) page.set_count(1) return page.to_string() vtuner_station = station.to_vtuner() @@ -313,7 +310,7 @@ def get_station_info(): vtuner_station.set_trackurl(request.host_url + PATH_ROOT + '/' + PATH_PLAY + '?id=' + vtuner_station.uid) vtuner_station.icon = request.host_url + PATH_ROOT + '/' + PATH_ICON + '?id=' + vtuner_station.uid page = vtuner.Page() - page.add(vtuner_station) + page.add_item(vtuner_station) page.set_count(1) return page.to_string() diff --git a/ycast/vtuner.py b/ycast/vtuner.py index e4f20cd..5724da0 100644 --- a/ycast/vtuner.py +++ b/ycast/vtuner.py @@ -29,7 +29,7 @@ def __init__(self): self.count = -1 self.dontcache = False - def add(self, item): + def add_item(self, item): self.items.append(item) def set_count(self, count): @@ -71,6 +71,14 @@ def to_xml(self): return item +class Spacer: + + def to_xml(self): + item = ET.Element('Item') + ET.SubElement(item, 'ItemType').text = 'Spacer' + return item + + class Search: def __init__(self, caption, url): self.caption = caption From 3222c2d77f6bc3ea6a6777369d57b56d2b7d7cd8 Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Mon, 7 Feb 2022 13:09:23 +0100 Subject: [PATCH 29/51] zwischenstand --- ycast/my_stations.py | 3 ++ ycast/radiobrowser.py | 6 ++- ycast/server.py | 39 +++++++++++++- ycast/static/script.js | 36 +++++++++++++ ycast/static/style.css | 102 +++++++++++++++++++++++++++++++++++++ ycast/templates/index.html | 56 ++++++++++++++++++++ ycast/test_YCast.py | 18 +++++-- 7 files changed, 254 insertions(+), 6 deletions(-) create mode 100644 ycast/static/script.js create mode 100644 ycast/static/style.css create mode 100644 ycast/templates/index.html diff --git a/ycast/my_stations.py b/ycast/my_stations.py index 2afad46..a0fe30b 100644 --- a/ycast/my_stations.py +++ b/ycast/my_stations.py @@ -17,6 +17,9 @@ def __init__(self, name, url, category, icon): def to_vtuner(self): return vtuner.Station(self.id, self.name, self.tag, self.url, self.icon, self.tag, None, None, None, None) + def to_dict(self): + return {'name': self.name , 'url': self.url, 'icon': self.icon, 'description': self.tag } + def get_station_by_id(vtune_id): my_stations_yaml = get_stations_yaml() diff --git a/ycast/radiobrowser.py b/ycast/radiobrowser.py index 764f56b..eff4370 100644 --- a/ycast/radiobrowser.py +++ b/ycast/radiobrowser.py @@ -1,4 +1,5 @@ import base64 +import json import uuid import requests @@ -29,6 +30,7 @@ def __init__(self, station_json): self.url = generic.get_json_attr(station_json, 'url') self.icon = generic.get_json_attr(station_json, 'favicon') + self.description = generic.get_json_attr(station_json, 'tags') self.tags = generic.get_json_attr(station_json, 'tags').split(',') self.countrycode = generic.get_json_attr(station_json, 'countrycode') self.language = generic.get_json_attr(station_json, 'language') @@ -39,8 +41,10 @@ def __init__(self, station_json): def to_vtuner(self): return vtuner.Station(self.id, self.name, - ', '.join(self.tags), self.url, self.icon, + self.description, self.url, self.icon, self.tags[0], self.countrycode, self.codec, self.bitrate, None) + def to_dict(self): + return {'name': self.name , 'url': self.url, 'icon': self.icon, 'description': self.description } def get_playable_url(self): try: diff --git a/ycast/server.py b/ycast/server.py index 14d8332..cda8d08 100644 --- a/ycast/server.py +++ b/ycast/server.py @@ -1,7 +1,8 @@ import logging import re -from flask import Flask, request, url_for, redirect, abort, make_response +import flask +from flask import Flask, request, url_for, redirect, abort, make_response, render_template import ycast.vtuner as vtuner import ycast.radiobrowser as radiobrowser @@ -135,9 +136,45 @@ def upstream(path): abort(404) +@app.route('/api/', + methods=['GET', 'POST']) +def landing_api(path): + if path.endswith('stations'): + category = request.args.get('category') + stations = None + if category.endswith('recently'): + stations = my_recentlystation.get_stations_by_recently() + if category.endswith('voted'): + stations = radiobrowser.get_stations_by_votes() + if category.endswith('language'): + language = request.args.get('language','german') + stations = radiobrowser.get_stations_by_language(language) + if category.endswith('countrycode'): + country = request.args.get('country','Germany') + stations = radiobrowser.get_stations_by_country(country) + + if stations is not None: + stations_dict = [] + for station in stations: + stations_dict.append(station.to_dict()) + + return flask.jsonify(stations_dict) + + if path.endswith('bookmarks'): + category = request.args.get('category') + stations = my_stations.get_stations_by_category(category) + return flask.jsonify({'stations': stations}) + return abort(400,'Not implemented: ' + path) + + @app.route('/', defaults={'path': ''}, methods=['GET', 'POST']) +def landing_root(path=''): + return render_template("index.html") + + + @app.route('/' + PATH_ROOT + '/', defaults={'path': ''}, methods=['GET', 'POST']) diff --git a/ycast/static/script.js b/ycast/static/script.js new file mode 100644 index 0000000..5c04e93 --- /dev/null +++ b/ycast/static/script.js @@ -0,0 +1,36 @@ +function createItem(name, icon, description) { + + var itemElem = document.createElement("div"); + itemElem.className = "item"; + + var itemicon = document.createElement("div"); + itemicon.className = "itemicon"; + var itemiconimg = document.createElement("img"); + itemiconimg.src = icon; + + var itemtext = document.createElement("div"); + itemtext.className = "itemtext"; + var h4text = document.createElement("h4"); + h4text.textContent = name; + var desc = document.createElement("p"); + desc.textContent = description; + + itemicon.appendChild(itemiconimg); + + itemtext.appendChild(h4text); + itemtext.appendChild(desc); + + itemElem.appendChild(itemicon); + itemElem.appendChild(itemtext); + + return itemElem; +} + +function stationsAddItem() { + var listElemet = document.createElement("li"); + listElemet.className = "item"; + listElemet.appendChild(createItem(" Halle self created","http://www.klassikradio.de/_nuxt/icons/icon_64.a00w80w0000.png","classic, poppi")); + + document.getElementById("stationList").appendChild(listElemet); +} + diff --git a/ycast/static/style.css b/ycast/static/style.css new file mode 100644 index 0000000..49a31c0 --- /dev/null +++ b/ycast/static/style.css @@ -0,0 +1,102 @@ +body { + font-family: sans-serif; + height: 100%; +} +.header { + height = 30rem; + width: 90%; + background-color: blue; + color: white; + text-align: center; + border: 4px #eee solid; + min-width: 20rem; +} +.container { + display: block; + width: 100%; + height: 100%; + max-height: auto; + background-color: aquamarine; +} +.content { + position: relative; + border: 2px #eee solid; + width: 45%; + min-width: 20rem; + float: left; + margin: 0rem; + height: 20rem; +} + +.contentheader { + background-color: blue; + color: white; + align-items: center; + margin: 0rem; + padding: 0.2rem; +} + +.contentitems { + position: absolute; + overflow-y: scroll; + overflow-x: hidden; + width: 100%; + height: 100%; + display: block; +} + +.item { + border: 2px #eee solid; + margin: 0rem; + display: flex; + height: 2.4rem; + align-items: center; + +} +.item:hover { + background-color: beige; + } + +.itemicon { + height: 2.4rem; + width: 2.4rem; +} + +.itemtext { + +} + +ul { + padding: 0.2rem; + margin: 0rem; + height: 100%; + width: 100%; + display: contents; +} +li { + width: 95%; + padding: 0rem; + margin: 0rem; +} +h3 { + border: 2px #eee solid; + text-align: center; + padding: 10px; + margin: 0rem; +} + +h4 { + text-align: left; + padding-left: 1rem; + margin: auto +} +p { + padding-left: 1.1rem; + margin: auto; + font-size: 0.8rem; + +} +img { + height: 100%; + width: 100%; +} \ No newline at end of file diff --git a/ycast/templates/index.html b/ycast/templates/index.html new file mode 100644 index 0000000..7702cc5 --- /dev/null +++ b/ycast/templates/index.html @@ -0,0 +1,56 @@ + + + + + + YCast + + +
+

Hallo YCast

+
+
+
+

Stations

+
+
+
    +
  • +
    + +
    +
    +

    Radio

    +

    ard,actuell

    +
    +
  • +
  • +
    + +
    +
    +

    Radio

    +

    ard,actuell

    +
    +
  • +
+
+
+
+

Bookmarks

+
    +
  • +
    + +
    +
    +

    Radio

    +

    ard,actuell

    +
    +
  • +
+
+ + \ No newline at end of file diff --git a/ycast/test_YCast.py b/ycast/test_YCast.py index 3b1c515..2099bbd 100644 --- a/ycast/test_YCast.py +++ b/ycast/test_YCast.py @@ -4,9 +4,9 @@ import unittest from io import StringIO -import my_filter -import generic -from ycast import radiobrowser, my_recentlystation +import flask + +from ycast import my_filter, generic, radiobrowser, my_recentlystation class MyTestCase(unittest.TestCase): @@ -78,11 +78,21 @@ def test_get_limits(self): result = my_filter.get_limit('irgendwas',20) assert result == 20 + def test_jsonable_classes(self): + generic.init_base_dir('.ycast') + my_filter.init_filter_file() + stations = radiobrowser.get_stations_by_country('Germany') + station = stations[0] + text = station.to_vtuner() + response = station.toJson() + response = json.dumps(station, skipkeys= True) + + assert response is not None def test_recently_hit(self): try: - os.remove(my_recentlystation.recently_file) + os.remove(my_recentlystation.get_recently_file()) except Exception: pass From ac3674555ca8c653d676f40328e6f47ac62389ae Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Tue, 8 Feb 2022 22:24:20 +0100 Subject: [PATCH 30/51] zwischenstand --- ycast/generic.py | 4 ++ ycast/server.py | 23 ++++++- ycast/static/script.js | 133 ++++++++++++++++++++++++++++++++++--- ycast/static/style.css | 77 ++++++++++++++------- ycast/templates/index.html | 96 +++++++++++++++----------- ycast/test_YCast.py | 5 +- 6 files changed, 261 insertions(+), 77 deletions(-) diff --git a/ycast/generic.py b/ycast/generic.py index 3b20ba8..32d53a2 100644 --- a/ycast/generic.py +++ b/ycast/generic.py @@ -23,6 +23,10 @@ def __init__(self, name, item_count, displayname=None): else: self.displayname = name + def to_dict(self): + return {'name': self.name , 'displayname': self.displayname, 'count': self.item_count } + + def mk_writeable_dir(path): try: diff --git a/ycast/server.py b/ycast/server.py index cda8d08..1ba5125 100644 --- a/ycast/server.py +++ b/ycast/server.py @@ -149,7 +149,7 @@ def landing_api(path): if category.endswith('language'): language = request.args.get('language','german') stations = radiobrowser.get_stations_by_language(language) - if category.endswith('countrycode'): + if category.endswith('country'): country = request.args.get('country','Germany') stations = radiobrowser.get_stations_by_country(country) @@ -163,7 +163,26 @@ def landing_api(path): if path.endswith('bookmarks'): category = request.args.get('category') stations = my_stations.get_stations_by_category(category) - return flask.jsonify({'stations': stations}) + if stations is not None: + stations_dict = [] + for station in stations: + stations_dict.append(station.to_dict()) + return flask.jsonify(stations_dict) + + if path.endswith('paramlist'): + category = request.args.get('category') + directories = None + if category.endswith('language'): + directories = radiobrowser.get_language_directories(); + if category.endswith('country'): + directories = radiobrowser.get_country_directories(); + if directories is not None: + directories_dict = [] + for directory in directories: + directories_dict.append(directory.to_dict()) + return flask.jsonify(directories_dict) + + return abort(400,'Not implemented: ' + path) diff --git a/ycast/static/script.js b/ycast/static/script.js index 5c04e93..95d82ef 100644 --- a/ycast/static/script.js +++ b/ycast/static/script.js @@ -1,5 +1,28 @@ -function createItem(name, icon, description) { +window.onload = function () { + category = document.getElementById('id_category').value; + param = document.getElementById('id_param').value; + requestStationList(category, param) +} + +function initSearch() { + var stationsearch = document.getElementById('stationsearch'); + stationsearch.value = ''; + stationsearch.onkeyup = function () { + var filter = stationsearch.value.toUpperCase(); + var lis = document.getElementsByTagName('li'); + for (var i = 0; i < lis.length; i++) { + var searchval = lis[i].dataset.search; + if (searchval.indexOf(filter) > -1) + lis[i].style.display = 'flex'; + else + lis[i].style.display = 'none'; + } + } +} + +function createItem(name, icon, description) { + var itemElem = document.createElement("div"); itemElem.className = "item"; @@ -7,16 +30,17 @@ function createItem(name, icon, description) { itemicon.className = "itemicon"; var itemiconimg = document.createElement("img"); itemiconimg.src = icon; + itemiconimg.className = "itemicon"; var itemtext = document.createElement("div"); itemtext.className = "itemtext"; - var h4text = document.createElement("h4"); + var h4text = document.createElement("h4"); h4text.textContent = name; var desc = document.createElement("p"); desc.textContent = description; - + itemicon.appendChild(itemiconimg); - + itemtext.appendChild(h4text); itemtext.appendChild(desc); @@ -26,11 +50,102 @@ function createItem(name, icon, description) { return itemElem; } -function stationsAddItem() { - var listElemet = document.createElement("li"); - listElemet.className = "item"; - listElemet.appendChild(createItem(" Halle self created","http://www.klassikradio.de/_nuxt/icons/icon_64.a00w80w0000.png","classic, poppi")); +function requestStationList(category, param) { + var url = 'api/stations?category=' + category; - document.getElementById("stationList").appendChild(listElemet); + if (category.indexOf('language') > -1) { + url = url + '&language=' + param.toLowerCase(); + } + if (category.indexOf('country') > -1) { + url = url + '&country=' + param; + } + var myRequest = new Request(url); + var myOldList = document.getElementById("stationList"); + + var myList = myOldList.cloneNode(false); + myOldList.parentNode.replaceChild(myList, myOldList); + + fetch(myRequest) + .then(response => response.json()) + .then(data => { + for (const station of data) { + let listItem = document.createElement('li'); + listItem.appendChild( + createItem(station.name, station.icon, station.description) + ); + listItem.dataset.json = JSON.stringify(station); + listItem.dataset.search = (station.name + '#' + station.description).toUpperCase(); + myList.appendChild(listItem); + } + }) + .catch(console.error); + initSearch(); +} + +function onInputSelect(e, objElem) { + + if (objElem.id == 'id_category') { + paramElem = document.getElementById('id_param') + param = paramElem.value + category = objElem.value + switch (category) { + case 'language': + setParamlist(); + try {paramElem.fokus();} catch(e) {}; + return; + case 'country': + setParamlist(); + try {paramElem.fokus();} catch(e) {}; + return; + default: + paramElem.disabled = true; + break; + } + requestStationList(category, param); + } } +function setParamlist() { + var category = document.getElementById('id_category').value + var url = 'api/paramlist?category=' + category; + document.getElementById('id_param').value = ''; + var myRequest = new Request(url); + var myOldList = document.getElementById('paramlist'); + + var myList = myOldList.cloneNode(false); + myOldList.parentNode.replaceChild(myList, myOldList); + + + fetch(myRequest) + .then(response => response.json()) + .then(data => { + for (const param of data) { + var option = document.createElement('option'); + option.value = param.name; + myList.appendChild(option); + } + }) + .catch(console.error); + document.getElementById('id_param').disabled = false; +} + +function keyUpEvent(e, objElem) { + switch (objElem.id) { + case 'id_param': + param = objElem.value; + category = document.getElementById('id_category').value; + if (e instanceof KeyboardEvent) { + // it is a keyboard event! + if (e.code == 'Enter') { + requestStationList(category, param); + } else if (e.code == 'Backspace') + this.value = ''; + } else if (e instanceof Event) { + // one Element from selection is selected + requestStationList(category, param); + } + break; + default: + break; + } +} diff --git a/ycast/static/style.css b/ycast/static/style.css index 49a31c0..1ed7c7d 100644 --- a/ycast/static/style.css +++ b/ycast/static/style.css @@ -1,61 +1,78 @@ body { font-family: sans-serif; height: 100%; + align-items: center; } + +.page { + align-items: center; + height: 100%; + width: 100%; +} + .header { - height = 30rem; - width: 90%; + height=30rem; + width: 38.9rem; + margin: 0rem; background-color: blue; color: white; text-align: center; - border: 4px #eee solid; min-width: 20rem; + max-width: 40rem; + padding: 0.5rem } + .container { display: block; width: 100%; - height: 100%; - max-height: auto; + bottom: 0rem; background-color: aquamarine; } + .content { - position: relative; - border: 2px #eee solid; - width: 45%; + display: block; + width: 20rem; min-width: 20rem; float: left; margin: 0rem; - height: 20rem; + height: calc(100% - 13rem); + max-height: 30rem; } .contentheader { + display: block; background-color: blue; color: white; align-items: center; margin: 0rem; padding: 0.2rem; + border: 2px #eee solid; + height: 5rem; + width: 100% } .contentitems { - position: absolute; - overflow-y: scroll; + display: block; + overflow-y: auto; overflow-x: hidden; + top: 6rem; width: 100%; - height: 100%; - display: block; + height: calc(100% - 20rem); + max-height: 30rem; + border: 2px #eee solid ; } .item { + display: flex; border: 2px #eee solid; margin: 0rem; - display: flex; height: 2.4rem; align-items: center; - } + .item:hover { - background-color: beige; - } + background-color: beige; +} .itemicon { height: 2.4rem; @@ -63,7 +80,9 @@ body { } .itemtext { - + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } ul { @@ -73,16 +92,23 @@ ul { width: 100%; display: contents; } + li { width: 95%; padding: 0rem; margin: 0rem; } + +h2 { + padding: 0rem; + margin: 0.5rem; +} + h3 { - border: 2px #eee solid; text-align: center; padding: 10px; margin: 0rem; + padding: 0.5rem; } h4 { @@ -90,13 +116,16 @@ h4 { padding-left: 1rem; margin: auto } + p { padding-left: 1.1rem; margin: auto; font-size: 0.8rem; - + } -img { - height: 100%; - width: 100%; -} \ No newline at end of file + +label { + padding-left: 1.1rem; +} + +input diff --git a/ycast/templates/index.html b/ycast/templates/index.html index 7702cc5..76250d8 100644 --- a/ycast/templates/index.html +++ b/ycast/templates/index.html @@ -1,56 +1,72 @@ - + + - - YCast + + + YCast + -
-

Hallo YCast

-
-
-
-

Stations

-
-
-
    -
  • -
    - +
    +
    +

    YCast advanced

    +
    + + +
    +
    + + + + +
    -
    -

    Radio

    -

    ard,actuell

    +
    +
    +

    Stations

    + + +
    +
    +
      +
    +
    -
  • -
  • +
    +
    +

    Bookmarks

    + + + + + +
    +
    +
      +
    • - +

      Radio

      ard,actuell

    • -
    -
    -
    -
    -

    Bookmarks

    -
      -
    • -
      - + +
    +
-
-

Radio

-

ard,actuell

-
- - -
+ - \ No newline at end of file + + diff --git a/ycast/test_YCast.py b/ycast/test_YCast.py index 2099bbd..10c802a 100644 --- a/ycast/test_YCast.py +++ b/ycast/test_YCast.py @@ -67,6 +67,9 @@ def test_get_languages(self): assert len(result) == 3 def test_get_countries(self): + generic.init_base_dir('.ycast') + my_filter.init_filter_file() + result = radiobrowser.get_country_directories() assert len(result) == 4 @@ -79,8 +82,6 @@ def test_get_limits(self): assert result == 20 def test_jsonable_classes(self): - generic.init_base_dir('.ycast') - my_filter.init_filter_file() stations = radiobrowser.get_stations_by_country('Germany') station = stations[0] text = station.to_vtuner() From c18ebca9bbf3145e1091e28106e3ce15186fa1d0 Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Wed, 9 Feb 2022 21:07:04 +0100 Subject: [PATCH 31/51] FE prototyping --- ycast/my_stations.py | 15 ++++++ ycast/server.py | 2 +- ycast/static/script.js | 103 +++++++++++++++++++++++++++++-------- ycast/static/style.css | 83 +++++++++++++++++------------- ycast/templates/index.html | 14 +---- ycast/test_YCast.py | 31 ++--------- 6 files changed, 149 insertions(+), 99 deletions(-) diff --git a/ycast/my_stations.py b/ycast/my_stations.py index a0fe30b..349a29f 100644 --- a/ycast/my_stations.py +++ b/ycast/my_stations.py @@ -65,3 +65,18 @@ def get_stations_by_category(category): station_icon = param_list[1] stations.append(Station(station_name, station_url, category, station_icon)) return stations + +def get_all_bookmarks_stations(): + bm_stations_category = generic.read_yaml_file(generic.get_stations_file()) + stations = [] + if bm_stations_category : + for category in bm_stations_category: + for station_name in bm_stations_category[category]: + station_urls = bm_stations_category[category][station_name] + param_list = station_urls.split('|') + station_url = param_list[0] + station_icon = None + if len(param_list) > 1: + station_icon = param_list[1] + stations.append(Station(station_name, station_url, category, station_icon)) + return stations diff --git a/ycast/server.py b/ycast/server.py index 1ba5125..3eda1bf 100644 --- a/ycast/server.py +++ b/ycast/server.py @@ -162,7 +162,7 @@ def landing_api(path): if path.endswith('bookmarks'): category = request.args.get('category') - stations = my_stations.get_stations_by_category(category) + stations = my_stations.get_all_bookmarks_stations() if stations is not None: stations_dict = [] for station in stations: diff --git a/ycast/static/script.js b/ycast/static/script.js index 95d82ef..7c36172 100644 --- a/ycast/static/script.js +++ b/ycast/static/script.js @@ -1,23 +1,27 @@ window.onload = function () { category = document.getElementById('id_category').value; param = document.getElementById('id_param').value; - requestStationList(category, param) - + requestStationList(category, param); + requestStationList('', '', true); } -function initSearch() { +function initSearchStation() { var stationsearch = document.getElementById('stationsearch'); stationsearch.value = ''; stationsearch.onkeyup = function () { var filter = stationsearch.value.toUpperCase(); - var lis = document.getElementsByTagName('li'); - for (var i = 0; i < lis.length; i++) { - var searchval = lis[i].dataset.search; - if (searchval.indexOf(filter) > -1) - lis[i].style.display = 'flex'; - else - lis[i].style.display = 'none'; - } + var stationList = Array.from(document.getElementById("stationList").childNodes); + stationList.forEach(function (listItem) { + try { + var searchval = listItem.dataset.search; + if (searchval.indexOf(filter) > -1) + listItem.style.display = 'flex'; + else + listItem.style.display = 'none'; + } catch (e) { + console.error(listItem, e) + } + }) } } @@ -50,17 +54,23 @@ function createItem(name, icon, description) { return itemElem; } -function requestStationList(category, param) { +function requestStationList(category, param, isbookmarklist = false) { var url = 'api/stations?category=' + category; - - if (category.indexOf('language') > -1) { - url = url + '&language=' + param.toLowerCase(); + var id_listnode = "stationList"; + if (isbookmarklist) { + var url = 'api/bookmarks?category=' + category; + var id_listnode = "bookmarkList"; } - if (category.indexOf('country') > -1) { - url = url + '&country=' + param; + if (param.length > 0) { + if (category.indexOf('language') > -1) { + url = url + '&language=' + param.toLowerCase(); + } + if (category.indexOf('country') > -1) { + url = url + '&country=' + param; + } } var myRequest = new Request(url); - var myOldList = document.getElementById("stationList"); + var myOldList = document.getElementById(id_listnode); var myList = myOldList.cloneNode(false); myOldList.parentNode.replaceChild(myList, myOldList); @@ -75,11 +85,15 @@ function requestStationList(category, param) { ); listItem.dataset.json = JSON.stringify(station); listItem.dataset.search = (station.name + '#' + station.description).toUpperCase(); + listItem.dataset.category = station.description; myList.appendChild(listItem); } + if(isbookmarklist) { + setBookmarkCategoryList(); + } }) .catch(console.error); - initSearch(); + initSearchStation(); } function onInputSelect(e, objElem) { @@ -91,11 +105,15 @@ function onInputSelect(e, objElem) { switch (category) { case 'language': setParamlist(); - try {paramElem.fokus();} catch(e) {}; + try { + paramElem.fokus(); + } catch (e) {}; return; case 'country': setParamlist(); - try {paramElem.fokus();} catch(e) {}; + try { + paramElem.fokus(); + } catch (e) {}; return; default: paramElem.disabled = true; @@ -105,13 +123,54 @@ function onInputSelect(e, objElem) { } } +function setBookmarkCategoryList() { + var categoryList = []; + var bookmarkList = Array.from(document.getElementById("bookmarkList").childNodes); + bookmarkList.forEach(function (listItem) { + try { + var category = listItem.dataset.category; + if (!categoryList.find(function(arElem) { return (category == arElem);})) { + console.log(category); + categoryList.push(category); + } + } catch (e) { + console.error(listItem, e) + } + }) + console.log(categoryList); + if (categoryList.length >0) { + var myOldList = document.getElementById('categorylist'); + var myList = myOldList.cloneNode(false); + myOldList.parentNode.replaceChild(myList, myOldList); + + for (const categ of categoryList) { + var option = document.createElement('option'); + option.value = categ; + myList.appendChild(option); + } + } +} + +function filterBookmarkCategoryList(category) { + var bookmarkList = Array.from(document.getElementById("bookmarkList").childNodes); + bookmarkList.forEach(function (listItem) { + try { + if (listItem.dataset.category.indexOf(category) > -1) + listItem.style.display = 'flex'; + else + listItem.style.display = 'none'; + } catch (e) { + console.error(listItem, e) + } + }) +} + function setParamlist() { var category = document.getElementById('id_category').value var url = 'api/paramlist?category=' + category; document.getElementById('id_param').value = ''; var myRequest = new Request(url); var myOldList = document.getElementById('paramlist'); - var myList = myOldList.cloneNode(false); myOldList.parentNode.replaceChild(myList, myOldList); diff --git a/ycast/static/style.css b/ycast/static/style.css index 1ed7c7d..4005970 100644 --- a/ycast/static/style.css +++ b/ycast/static/style.css @@ -1,46 +1,45 @@ body { font-family: sans-serif; - height: 100%; - align-items: center; } .page { - align-items: center; - height: 100%; - width: 100%; + display: block; + padding-left: calc(100% / 2 - 20rem); } .header { + position: relative; height=30rem; - width: 38.9rem; + max-width: 19.0rem; + min-width: 19.0rem; margin: 0rem; background-color: blue; color: white; text-align: center; - min-width: 20rem; - max-width: 40rem; - padding: 0.5rem -} - -.container { - display: block; - width: 100%; - bottom: 0rem; - background-color: aquamarine; + padding: 0.5rem; } .content { - display: block; - width: 20rem; - min-width: 20rem; - float: left; + position: relative; + display: inline-block; + box-sizing: border-box; margin: 0rem; - height: calc(100% - 13rem); - max-height: 30rem; + max-height: 60rem; + max-width: 20rem; + min-width: 20rem; +} + +@media (min-width: 42.5rem) { + .header { + max-width: 39.2rem; + min-width: 39.2rem; + } } .contentheader { - display: block; + box-sizing: border-box; + display: inline-table; + position: relative; background-color: blue; color: white; align-items: center; @@ -48,25 +47,28 @@ body { padding: 0.2rem; border: 2px #eee solid; height: 5rem; - width: 100% + width: 100%; + min-width: 20rem; } .contentitems { - display: block; + box-sizing: border-box; + position: relative; + display: flex; overflow-y: auto; overflow-x: hidden; - top: 6rem; - width: 100%; - height: calc(100% - 20rem); max-height: 30rem; - border: 2px #eee solid ; + min-width: 20rem; + border: 2px #eee solid; } .item { - display: flex; + position: relative; + display: inline-flex; border: 2px #eee solid; - margin: 0rem; - height: 2.4rem; + margin: 0.1rem; + min-height: 2.6rem; + min-width: 19rem; align-items: center; } @@ -75,12 +77,15 @@ body { } .itemicon { + display: flex; + box-sizing: border-box; + margin: 1px; height: 2.4rem; width: 2.4rem; } .itemtext { - white-space: nowrap; + display: flex white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } @@ -90,7 +95,6 @@ ul { margin: 0rem; height: 100%; width: 100%; - display: contents; } li { @@ -114,18 +118,23 @@ h3 { h4 { text-align: left; padding-left: 1rem; - margin: auto + margin: auto; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } p { padding-left: 1.1rem; margin: auto; font-size: 0.8rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } label { padding-left: 1.1rem; } - -input diff --git a/ycast/templates/index.html b/ycast/templates/index.html index 76250d8..3b4ff04 100644 --- a/ycast/templates/index.html +++ b/ycast/templates/index.html @@ -43,8 +43,8 @@

Stations

Bookmarks

- - + +
    -
  • -
    - -
    -
    -

    Radio

    -

    ard,actuell

    -
    -
  • -
diff --git a/ycast/test_YCast.py b/ycast/test_YCast.py index 10c802a..88e1375 100644 --- a/ycast/test_YCast.py +++ b/ycast/test_YCast.py @@ -12,6 +12,9 @@ class MyTestCase(unittest.TestCase): logging.getLogger().setLevel(logging.DEBUG) + generic.init_base_dir("../../.test_ycast") + my_filter.init_filter_file() + def test_verify_values(self): assert my_filter.verify_value(None, None) @@ -41,22 +44,6 @@ def test_init_filter(self): else: logging.warning(" ") - def test_valid_station(self): - my_filter.begin_filter() - test_lines = generic.readlns_txt_file(generic.get_var_path()+"/test.json") - - test_lines = radiobrowser.get_stations_by_votes() - - io = StringIO(test_lines[0]) - stations_json = json.load(io) - count = 0 - for station_json in stations_json: - if my_filter.check_station(station_json): - station = radiobrowser.Station(station_json) - count = count + 1 - - my_filter.end_filter() - def test_life_popular_station(self): # hard test for filter stations = radiobrowser.get_stations_by_votes(10000000) @@ -67,11 +54,10 @@ def test_get_languages(self): assert len(result) == 3 def test_get_countries(self): - generic.init_base_dir('.ycast') my_filter.init_filter_file() result = radiobrowser.get_country_directories() - assert len(result) == 4 + assert len(result) == 137 def test_get_genre(self): result = radiobrowser.get_genre_directories() @@ -81,15 +67,6 @@ def test_get_limits(self): result = my_filter.get_limit('irgendwas',20) assert result == 20 - def test_jsonable_classes(self): - stations = radiobrowser.get_stations_by_country('Germany') - station = stations[0] - text = station.to_vtuner() - response = station.toJson() - response = json.dumps(station, skipkeys= True) - - assert response is not None - def test_recently_hit(self): try: From d4a221b5a4e184c3f42cfc8b451597329135e768 Mon Sep 17 00:00:00 2001 From: Thomas Hanika Date: Thu, 10 Feb 2022 17:30:56 +0100 Subject: [PATCH 32/51] FE prototyping --- ycast/static/script.js | 143 ++++++++++++++++++++++++++++--------- ycast/templates/index.html | 5 +- 2 files changed, 111 insertions(+), 37 deletions(-) diff --git a/ycast/static/script.js b/ycast/static/script.js index 7c36172..8177abb 100644 --- a/ycast/static/script.js +++ b/ycast/static/script.js @@ -1,3 +1,4 @@ + window.onload = function () { category = document.getElementById('id_category').value; param = document.getElementById('id_param').value; @@ -8,23 +9,26 @@ window.onload = function () { function initSearchStation() { var stationsearch = document.getElementById('stationsearch'); stationsearch.value = ''; - stationsearch.onkeyup = function () { + stationsearch.onkeyup = function (event) { + if(event.code == 'Backspace') + stationsearch.value = ''; var filter = stationsearch.value.toUpperCase(); - var stationList = Array.from(document.getElementById("stationList").childNodes); - stationList.forEach(function (listItem) { - try { - var searchval = listItem.dataset.search; - if (searchval.indexOf(filter) > -1) - listItem.style.display = 'flex'; - else - listItem.style.display = 'none'; - } catch (e) { - console.error(listItem, e) - } - }) + refreshFilteredList( + document.getElementById('stationList'), filter, false ); + } +} + +function initSearchBookmark() { + bookmarksearch = document.getElementById('idCategory'); + bookmarksearch.value = ''; + bookmarksearch.onkeyup = function (event) { + if(event.code == 'Backspace') + document.getElementById('idCategory').value = ''; + refreshFilteredList(document.getElementById("bookmarkList"), document.getElementById('idCategory').value, true); } } + function createItem(name, icon, description) { var itemElem = document.createElement("div"); @@ -32,9 +36,13 @@ function createItem(name, icon, description) { var itemicon = document.createElement("div"); itemicon.className = "itemicon"; - var itemiconimg = document.createElement("img"); - itemiconimg.src = icon; - itemiconimg.className = "itemicon"; + + if (icon.length > 0){ + var itemiconimg = document.createElement("img"); + itemiconimg.src = icon; + itemiconimg.className = "itemicon"; + itemicon.appendChild(itemiconimg); + } var itemtext = document.createElement("div"); itemtext.className = "itemtext"; @@ -43,7 +51,6 @@ function createItem(name, icon, description) { var desc = document.createElement("p"); desc.textContent = description; - itemicon.appendChild(itemiconimg); itemtext.appendChild(h4text); itemtext.appendChild(desc); @@ -73,6 +80,12 @@ function requestStationList(category, param, isbookmarklist = false) { var myOldList = document.getElementById(id_listnode); var myList = myOldList.cloneNode(false); + // First Elemet is empty (workaround