Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed Radiobrowser - Access Error issue on some models #79

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ build
dist
*.egg-info
.idea
.vscode
*.iml
*.pyc
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
)
11 changes: 11 additions & 0 deletions ycast/generic.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import os
import hashlib

USER_AGENT = 'YCast'
VAR_PATH = os.path.expanduser("~") + '/.ycast'
Expand Down Expand Up @@ -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]
17 changes: 2 additions & 15 deletions ycast/my_stations.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import logging
import hashlib

import yaml
import oyaml as yaml

import ycast.vtuner as vtuner
import ycast.generic as generic
Expand Down Expand Up @@ -71,17 +69,6 @@ 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_url = my_stations_yaml[category][station_name]
station_id = str(get_checksum(station_name + station_url)).upper()
station_id = str(generic.get_checksum(station_name + station_url)).upper()
milaq marked this conversation as resolved.
Show resolved Hide resolved
stations.append(Station(station_id, station_name, station_url, category))
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]
29 changes: 21 additions & 8 deletions ycast/radiobrowser.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
SHOW_BROKEN_STATIONS = False
ID_PREFIX = "RB"

id_registry = {}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's that for? We can use self.id

Copy link
Author

@jonnieZG jonnieZG Dec 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No we can't because the Access Error issue was caused by ID size limit to 15 chars in some radios. That's why we have to translate UUID (36 chars) to a fairly unique ID that will fit the limitation. Currently it is 12 chars, because with the prefix "RB_" it comes to exactly 15 chars.

I am using this dictionary in memory to avoid reading fetching the stations all over again from the remote service. While my_stations reads the stations all over again from the file, calculates ID and matches it on the fly, there are usually much more stations returned by the radiobrowser and going there twice just to find a matching UUID would be really slow.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand.
Can we split this cache speed improvement off of the functionality fix? Would be nice for testing and traceability.

Copy link
Author

@jonnieZG jonnieZG Dec 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am sorry, I am not quite sure what you meant by that. There is no "cache speed improvement" here. Just keeping the ID-UUID dictionary of the last search response. How else would you be able to find the original station when the radio returns an ID that is nothing but a digest value that you calculated from the original station UUID?

The id_registry is absolutely necessary for this fix to work.


def get_json_attr(json, attr):
try:
Expand All @@ -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')
Expand All @@ -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)
Expand All @@ -61,14 +64,18 @@ def request(url):


def get_station_by_id(uid):
station_json = request('stations/byid/' + 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:
Expand All @@ -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:
Expand All @@ -92,6 +100,7 @@ def get_country_directories():


def get_language_directories():
id_registry.clear()
language_directories = []
apicall = 'languages'
if not SHOW_BROKEN_STATIONS:
Expand All @@ -107,6 +116,7 @@ def get_language_directories():


def get_genre_directories():
id_registry.clear()
genre_directories = []
apicall = 'tags'
if not SHOW_BROKEN_STATIONS:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -149,10 +162,10 @@ 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:
print(station_json)
if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == 1:
stations.append(Station(station_json))
return stations
2 changes: 1 addition & 1 deletion ycast/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,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')
Expand Down
4 changes: 2 additions & 2 deletions ycast/station_icons.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down