Merge branch 'master' into master

This commit is contained in:
Micha LaQua 2020-10-10 16:02:46 +02:00 committed by GitHub
commit e799b92ed6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 61 additions and 28 deletions

View file

@ -35,6 +35,7 @@ Any reported device helps the community to see which AVRs work properly and whic
* Denon CEOL piccolo N5 * Denon CEOL piccolo N5
* Denon CEOL N9 * Denon CEOL N9
* Denon DNP-720AE * Denon DNP-720AE
* Denon DNP-730AE
* Denon DRA-100 * Denon DRA-100
* Marantz Melody Media M-CR610 * Marantz Melody Media M-CR610
* Marantz NR1506 * Marantz NR1506
@ -49,6 +50,7 @@ Any reported device helps the community to see which AVRs work properly and whic
* Yamaha RX-A810 * Yamaha RX-A810
* Yamaha RX-A820 * Yamaha RX-A820
* Yamaha RX-A830 * Yamaha RX-A830
* Yamaha CRX-N560/MCR-N560
* Yamaha RX-V_71 series with network connectivity (RX-V671, RX-V771) * Yamaha RX-V_71 series with network connectivity (RX-V671, RX-V771)
* Yamaha RX-V_73 series with network connectivity (RX-V473, RX-V573, RX-V673, RX-V773) * Yamaha RX-V_73 series with network connectivity (RX-V473, RX-V573, RX-V673, RX-V773)
* Yamaha RX-V_75 series (RX-V375, RX-V475, RX-V575, RX-V675, RX-V775) * Yamaha RX-V_75 series (RX-V375, RX-V475, RX-V575, RX-V675, RX-V775)
@ -85,11 +87,13 @@ YCast really does not need much computing power nor bandwidth, i.e. you can run
### DNS entries ### DNS entries
You need to create a manual entry in your DNS server (read 'Router' for most home users). `vtuner.com` (more specifically `*.vtuner.com`) should point to the machine YCast is running on. Alternatively, in case you only want to forward specific vendors, the following entries may be configured: You need to create a manual entry in your DNS server (read 'Router' for most home users). The `*.vtuner.com` domain should point to the machine YCast is running on.
Specifically the following entries may be configured instead of a wildcard entry:
* Yamaha AVRs: `radioyamaha.vtuner.com` (and optionally `radioyamaha2.vtuner.com`) * Yamaha AVRs: `radioyamaha.vtuner.com` (and optionally `radioyamaha2.vtuner.com`)
* Onkyo AVRs: `onkyo.vtuner.com` (and optionally `onkyo2.vtuner.com`) * Onkyo AVRs: `onkyo.vtuner.com` (and optionally `onkyo2.vtuner.com`)
* Denon/Marantz AVRs: `denon.vtuner.com` (and optionally `denon2.vtuner.com`) * Denon/Marantz AVRs: `denon.vtuner.com` (and optionally `denon2.vtuner.com`)
* Grundig radios: `grundig.vtuner.com`, `grundig.radiosetup.com` (and optionally `grundig2.vtuner.com` and `grundig2.radiosetup.com`)
### Running the server ### Running the server

View file

@ -5,6 +5,7 @@ from ycast import __version__
import ycast.vtuner as vtuner import ycast.vtuner as vtuner
import ycast.generic as generic import ycast.generic as generic
API_ENDPOINT = "http://all.api.radio-browser.info"
MINIMUM_COUNT_GENRE = 5 MINIMUM_COUNT_GENRE = 5
MINIMUM_COUNT_COUNTRY = 5 MINIMUM_COUNT_COUNTRY = 5
MINIMUM_COUNT_LANGUAGE = 5 MINIMUM_COUNT_LANGUAGE = 5
@ -22,12 +23,12 @@ def get_json_attr(json, attr):
class Station: class Station:
def __init__(self, station_json): def __init__(self, station_json):
self.id = generic.generate_stationid_with_prefix(get_json_attr(station_json, 'id'), ID_PREFIX) self.id = generic.generate_stationid_with_prefix(get_json_attr(station_json, 'stationuuid'), ID_PREFIX)
self.name = get_json_attr(station_json, 'name') self.name = get_json_attr(station_json, 'name')
self.url = get_json_attr(station_json, 'url') self.url = get_json_attr(station_json, 'url')
self.icon = get_json_attr(station_json, 'favicon') self.icon = get_json_attr(station_json, 'favicon')
self.tags = get_json_attr(station_json, 'tags').split(',') self.tags = get_json_attr(station_json, 'tags').split(',')
self.country = get_json_attr(station_json, 'country') self.countrycode = get_json_attr(station_json, 'countrycode')
self.language = get_json_attr(station_json, 'language') self.language = get_json_attr(station_json, 'language')
self.votes = get_json_attr(station_json, 'votes') self.votes = get_json_attr(station_json, 'votes')
self.codec = get_json_attr(station_json, 'codec') self.codec = get_json_attr(station_json, 'codec')
@ -35,7 +36,7 @@ class Station:
def to_vtuner(self): def to_vtuner(self):
return vtuner.Station(self.id, self.name, ', '.join(self.tags), self.url, self.icon, return vtuner.Station(self.id, self.name, ', '.join(self.tags), self.url, self.icon,
self.tags[0], self.country, self.codec, self.bitrate, None) self.tags[0], self.countrycode, self.codec, self.bitrate, None)
def get_playable_url(self): def get_playable_url(self):
try: try:
@ -49,7 +50,7 @@ def request(url):
logging.debug("Radiobrowser API request: %s", url) logging.debug("Radiobrowser API request: %s", url)
headers = {'content-type': 'application/json', 'User-Agent': generic.USER_AGENT + '/' + __version__} headers = {'content-type': 'application/json', 'User-Agent': generic.USER_AGENT + '/' + __version__}
try: try:
response = requests.get('http://www.radio-browser.info/webservice/json/' + url, headers=headers) response = requests.get(API_ENDPOINT + '/json/' + url, headers=headers)
except requests.exceptions.ConnectionError as err: except requests.exceptions.ConnectionError as err:
logging.error("Connection to Radiobrowser API failed (%s)", err) logging.error("Connection to Radiobrowser API failed (%s)", err)
return {} return {}
@ -71,7 +72,7 @@ def search(name, limit=DEFAULT_STATION_LIMIT):
stations = [] stations = []
stations_json = request('stations/search?order=name&reverse=false&limit=' + str(limit) + '&name=' + str(name)) stations_json = request('stations/search?order=name&reverse=false&limit=' + str(limit) + '&name=' + str(name))
for station_json in stations_json: for station_json in stations_json:
if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == '1': if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == 1:
stations.append(Station(station_json)) stations.append(Station(station_json))
return stations return stations
@ -122,7 +123,7 @@ def get_stations_by_country(country):
stations = [] stations = []
stations_json = request('stations/search?order=name&reverse=false&countryExact=true&country=' + str(country)) stations_json = request('stations/search?order=name&reverse=false&countryExact=true&country=' + str(country))
for station_json in stations_json: for station_json in stations_json:
if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == '1': if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == 1:
stations.append(Station(station_json)) stations.append(Station(station_json))
return stations return stations
@ -131,7 +132,7 @@ def get_stations_by_language(language):
stations = [] stations = []
stations_json = request('stations/search?order=name&reverse=false&languageExact=true&language=' + str(language)) stations_json = request('stations/search?order=name&reverse=false&languageExact=true&language=' + str(language))
for station_json in stations_json: for station_json in stations_json:
if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == '1': if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == 1:
stations.append(Station(station_json)) stations.append(Station(station_json))
return stations return stations
@ -140,7 +141,7 @@ def get_stations_by_genre(genre):
stations = [] stations = []
stations_json = request('stations/search?order=name&reverse=false&tagExact=true&tag=' + str(genre)) stations_json = request('stations/search?order=name&reverse=false&tagExact=true&tag=' + str(genre))
for station_json in stations_json: for station_json in stations_json:
if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == '1': if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == 1:
stations.append(Station(station_json)) stations.append(Station(station_json))
return stations return stations
@ -149,6 +150,7 @@ def get_stations_by_votes(limit=DEFAULT_STATION_LIMIT):
stations = [] stations = []
stations_json = request('stations?order=votes&reverse=true&limit=' + str(limit)) stations_json = request('stations?order=votes&reverse=true&limit=' + str(limit))
for station_json in stations_json: for station_json in stations_json:
if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == '1': print(station_json)
if SHOW_BROKEN_STATIONS or get_json_attr(station_json, 'lastcheckok') == 1:
stations.append(Station(station_json)) stations.append(Station(station_json))
return stations return stations

View file

@ -73,6 +73,8 @@ def get_stations_page(stations, request):
def get_paged_elements(items, requestargs): def get_paged_elements(items, requestargs):
if requestargs.get('startitems'): if requestargs.get('startitems'):
offset = int(requestargs.get('startitems')) - 1 offset = int(requestargs.get('startitems')) - 1
elif requestargs.get('startItems'):
offset = int(requestargs.get('startItems')) - 1
elif requestargs.get('start'): elif requestargs.get('start'):
offset = int(requestargs.get('start')) - 1 offset = int(requestargs.get('start')) - 1
else: else:
@ -82,6 +84,8 @@ def get_paged_elements(items, requestargs):
return [] return []
if requestargs.get('enditems'): if requestargs.get('enditems'):
limit = int(requestargs.get('enditems')) limit = int(requestargs.get('enditems'))
elif requestargs.get('endItems'):
limit = int(requestargs.get('endItems'))
elif requestargs.get('start') and requestargs.get('howmany'): elif requestargs.get('start') and requestargs.get('howmany'):
limit = int(requestargs.get('start')) - 1 + int(requestargs.get('howmany')) limit = int(requestargs.get('start')) - 1 + int(requestargs.get('howmany'))
else: else:
@ -113,7 +117,8 @@ def vtuner_redirect(url):
return redirect(url, code=302) return redirect(url, code=302)
@app.route('/setupapp/<path:path>') @app.route('/setupapp/<path:path>',
methods=['GET', 'POST'])
def upstream(path): def upstream(path):
if request.args.get('token') == '0': if request.args.get('token') == '0':
return vtuner.get_init_token() return vtuner.get_init_token()
@ -121,14 +126,22 @@ def upstream(path):
return station_search() return station_search()
if 'statxml.asp' in path and request.args.get('id'): if 'statxml.asp' in path and request.args.get('id'):
return get_station_info() return get_station_info()
if 'navXML.asp' in path:
return radiobrowser_landing()
if 'FavXML.asp' in path:
return my_stations_landing()
if 'loginXML.asp' in path: if 'loginXML.asp' in path:
return landing() return landing()
logging.error("Unhandled upstream query (/setupapp/%s)", path) logging.error("Unhandled upstream query (/setupapp/%s)", path)
abort(404) abort(404)
@app.route('/', defaults={'path': ''}) @app.route('/',
@app.route('/' + PATH_ROOT + '/', defaults={'path': ''}) defaults={'path': ''},
methods=['GET', 'POST'])
@app.route('/' + PATH_ROOT + '/',
defaults={'path': ''},
methods=['GET', 'POST'])
def landing(path=''): def landing(path=''):
page = vtuner.Page() page = vtuner.Page()
page.add(vtuner.Directory('Radiobrowser', url_for('radiobrowser_landing', _external=True), 4)) page.add(vtuner.Directory('Radiobrowser', url_for('radiobrowser_landing', _external=True), 4))
@ -141,19 +154,22 @@ def landing(path=''):
return page.to_string() return page.to_string()
@app.route('/' + PATH_ROOT + '/' + PATH_MY_STATIONS + '/') @app.route('/' + PATH_ROOT + '/' + PATH_MY_STATIONS + '/',
methods=['GET', 'POST'])
def my_stations_landing(): def my_stations_landing():
directories = my_stations.get_category_directories() directories = my_stations.get_category_directories()
return get_directories_page('my_stations_category', directories, request).to_string() return get_directories_page('my_stations_category', directories, request).to_string()
@app.route('/' + PATH_ROOT + '/' + PATH_MY_STATIONS + '/<directory>') @app.route('/' + PATH_ROOT + '/' + PATH_MY_STATIONS + '/<directory>',
methods=['GET', 'POST'])
def my_stations_category(directory): def my_stations_category(directory):
stations = my_stations.get_stations_by_category(directory) stations = my_stations.get_stations_by_category(directory)
return get_stations_page(stations, request).to_string() return get_stations_page(stations, request).to_string()
@app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/') @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/',
methods=['GET', 'POST'])
def radiobrowser_landing(): def radiobrowser_landing():
page = vtuner.Page() page = vtuner.Page()
page.add(vtuner.Directory('Genres', url_for('radiobrowser_genres', _external=True), page.add(vtuner.Directory('Genres', url_for('radiobrowser_genres', _external=True),
@ -168,49 +184,57 @@ def radiobrowser_landing():
return page.to_string() return page.to_string()
@app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_COUNTRY + '/') @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_COUNTRY + '/',
methods=['GET', 'POST'])
def radiobrowser_countries(): def radiobrowser_countries():
directories = radiobrowser.get_country_directories() directories = radiobrowser.get_country_directories()
return get_directories_page('radiobrowser_country_stations', directories, request).to_string() return get_directories_page('radiobrowser_country_stations', directories, request).to_string()
@app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_COUNTRY + '/<directory>') @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_COUNTRY + '/<directory>',
methods=['GET', 'POST'])
def radiobrowser_country_stations(directory): def radiobrowser_country_stations(directory):
stations = radiobrowser.get_stations_by_country(directory) stations = radiobrowser.get_stations_by_country(directory)
return get_stations_page(stations, request).to_string() return get_stations_page(stations, request).to_string()
@app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_LANGUAGE + '/') @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_LANGUAGE + '/',
methods=['GET', 'POST'])
def radiobrowser_languages(): def radiobrowser_languages():
directories = radiobrowser.get_language_directories() directories = radiobrowser.get_language_directories()
return get_directories_page('radiobrowser_language_stations', directories, request).to_string() return get_directories_page('radiobrowser_language_stations', directories, request).to_string()
@app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_LANGUAGE + '/<directory>') @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_LANGUAGE + '/<directory>',
methods=['GET', 'POST'])
def radiobrowser_language_stations(directory): def radiobrowser_language_stations(directory):
stations = radiobrowser.get_stations_by_language(directory) stations = radiobrowser.get_stations_by_language(directory)
return get_stations_page(stations, request).to_string() return get_stations_page(stations, request).to_string()
@app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_GENRE + '/') @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_GENRE + '/',
methods=['GET', 'POST'])
def radiobrowser_genres(): def radiobrowser_genres():
directories = radiobrowser.get_genre_directories() directories = radiobrowser.get_genre_directories()
return get_directories_page('radiobrowser_genre_stations', directories, request).to_string() return get_directories_page('radiobrowser_genre_stations', directories, request).to_string()
@app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_GENRE + '/<directory>') @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_GENRE + '/<directory>',
methods=['GET', 'POST'])
def radiobrowser_genre_stations(directory): def radiobrowser_genre_stations(directory):
stations = radiobrowser.get_stations_by_genre(directory) stations = radiobrowser.get_stations_by_genre(directory)
return get_stations_page(stations, request).to_string() return get_stations_page(stations, request).to_string()
@app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_POPULAR + '/') @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_POPULAR + '/',
methods=['GET', 'POST'])
def radiobrowser_popular(): def radiobrowser_popular():
stations = radiobrowser.get_stations_by_votes() stations = radiobrowser.get_stations_by_votes()
return get_stations_page(stations, request).to_string() return get_stations_page(stations, request).to_string()
@app.route('/' + PATH_ROOT + '/' + PATH_SEARCH + '/') @app.route('/' + PATH_ROOT + '/' + PATH_SEARCH + '/',
methods=['GET', 'POST'])
def station_search(): def station_search():
query = request.args.get('search') query = request.args.get('search')
if not query or len(query) < 3: if not query or len(query) < 3:
@ -224,7 +248,8 @@ def station_search():
return get_stations_page(stations, request).to_string() return get_stations_page(stations, request).to_string()
@app.route('/' + PATH_ROOT + '/' + PATH_PLAY) @app.route('/' + PATH_ROOT + '/' + PATH_PLAY,
methods=['GET', 'POST'])
def get_stream_url(): def get_stream_url():
stationid = request.args.get('id') stationid = request.args.get('id')
if not stationid: if not stationid:
@ -238,7 +263,8 @@ def get_stream_url():
return vtuner_redirect(station.url) return vtuner_redirect(station.url)
@app.route('/' + PATH_ROOT + '/' + PATH_STATION) @app.route('/' + PATH_ROOT + '/' + PATH_STATION,
methods=['GET', 'POST'])
def get_station_info(): def get_station_info():
stationid = request.args.get('id') stationid = request.args.get('id')
if not stationid: if not stationid:
@ -261,7 +287,8 @@ def get_station_info():
return page.to_string() return page.to_string()
@app.route('/' + PATH_ROOT + '/' + PATH_ICON) @app.route('/' + PATH_ROOT + '/' + PATH_ICON,
methods=['GET', 'POST'])
def get_station_icon(): def get_station_icon():
stationid = request.args.get('id') stationid = request.args.get('id')
if not stationid: if not stationid:

View file

@ -137,7 +137,7 @@ class Station:
ET.SubElement(item, 'Logo').text = self.icon ET.SubElement(item, 'Logo').text = self.icon
ET.SubElement(item, 'StationFormat').text = self.genre ET.SubElement(item, 'StationFormat').text = self.genre
ET.SubElement(item, 'StationLocation').text = self.location ET.SubElement(item, 'StationLocation').text = self.location
ET.SubElement(item, 'StationBandWidth').text = self.bitrate ET.SubElement(item, 'StationBandWidth').text = str(self.bitrate)
ET.SubElement(item, 'StationMime').text = self.mime ET.SubElement(item, 'StationMime').text = self.mime
ET.SubElement(item, 'Relia').text = '3' ET.SubElement(item, 'Relia').text = '3'
ET.SubElement(item, 'Bookmark').text = self.bookmark ET.SubElement(item, 'Bookmark').text = self.bookmark