| 
									
										
										
										
											2023-09-27 14:01:28 +02:00
										 |  |  | # SPDX-License-Identifier: AGPL-3.0-or-later | 
					
						
							| 
									
										
										
										
											2023-09-30 18:41:13 +02:00
										 |  |  | """Search radio stations from RadioBrowser by `Advanced station search API`_.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | .. _Advanced station search API: | 
					
						
							|  |  |  |    https://de1.api.radio-browser.info/#Advanced_station_search | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-27 14:01:28 +02:00
										 |  |  | """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from urllib.parse import urlencode | 
					
						
							|  |  |  | import babel | 
					
						
							| 
									
										
										
										
											2023-09-30 18:41:13 +02:00
										 |  |  | from flask_babel import gettext | 
					
						
							| 
									
										
										
										
											2023-09-27 14:01:28 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | from searx.network import get | 
					
						
							|  |  |  | from searx.enginelib.traits import EngineTraits | 
					
						
							| 
									
										
										
										
											2023-09-30 18:41:13 +02:00
										 |  |  | from searx.locales import language_tag | 
					
						
							| 
									
										
										
										
											2023-09-27 14:01:28 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | traits: EngineTraits | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | about = { | 
					
						
							|  |  |  |     "website": 'https://www.radio-browser.info/', | 
					
						
							| 
									
										
										
										
											2023-09-30 18:41:13 +02:00
										 |  |  |     "wikidata_id": 'Q111664849', | 
					
						
							| 
									
										
										
										
											2023-09-27 14:01:28 +02:00
										 |  |  |     "official_api_documentation": 'https://de1.api.radio-browser.info/', | 
					
						
							|  |  |  |     "use_official_api": True, | 
					
						
							|  |  |  |     "require_api_key": False, | 
					
						
							|  |  |  |     "results": 'JSON', | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | paging = True | 
					
						
							| 
									
										
										
										
											2023-09-30 18:41:13 +02:00
										 |  |  | categories = ['music', 'radio'] | 
					
						
							| 
									
										
										
										
											2023-09-27 14:01:28 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | base_url = "https://de1.api.radio-browser.info"  # see https://api.radio-browser.info/ for all nodes | 
					
						
							|  |  |  | number_of_results = 10 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-30 18:41:13 +02:00
										 |  |  | station_filters = []  # ['countrycode', 'language'] | 
					
						
							|  |  |  | """A list of filters to be applied to the search of radio stations.  By default
 | 
					
						
							|  |  |  | none filters are applied. Valid filters are: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ``language`` | 
					
						
							|  |  |  |   Filter stations by selected language.  For instance the ``de`` from ``:de-AU`` | 
					
						
							|  |  |  |   will be translated to `german` and used in the argument ``language=``. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ``countrycode`` | 
					
						
							|  |  |  |   Filter stations by selected country.  The 2-digit countrycode of the station | 
					
						
							|  |  |  |   comes from the region the user selected.  For instance ``:de-AU`` will filter | 
					
						
							|  |  |  |   out all stations not in ``AU``. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | .. note:: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |    RadioBrowser has registered a lot of languages and countrycodes unknown to | 
					
						
							|  |  |  |    :py:obj:`babel` and note that when searching for radio stations, users are | 
					
						
							|  |  |  |    more likely to search by name than by region or language. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | """
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-27 14:01:28 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | def request(query, params): | 
					
						
							|  |  |  |     args = { | 
					
						
							|  |  |  |         'name': query, | 
					
						
							|  |  |  |         'order': 'votes', | 
					
						
							|  |  |  |         'offset': (params['pageno'] - 1) * number_of_results, | 
					
						
							|  |  |  |         'limit': number_of_results, | 
					
						
							|  |  |  |         'hidebroken': 'true', | 
					
						
							|  |  |  |         'reverse': 'true', | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-30 18:41:13 +02:00
										 |  |  |     if 'language' in station_filters: | 
					
						
							|  |  |  |         lang = traits.get_language(params['searxng_locale'])  # type: ignore | 
					
						
							|  |  |  |         if lang: | 
					
						
							|  |  |  |             args['language'] = lang | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if 'countrycode' in station_filters: | 
					
						
							|  |  |  |         if len(params['searxng_locale'].split('-')) > 1: | 
					
						
							|  |  |  |             countrycode = params['searxng_locale'].split('-')[-1].upper() | 
					
						
							|  |  |  |             if countrycode in traits.custom['countrycodes']:  # type: ignore | 
					
						
							|  |  |  |                 args['countrycode'] = countrycode | 
					
						
							| 
									
										
										
										
											2023-09-27 14:01:28 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     params['url'] = f"{base_url}/json/stations/search?{urlencode(args)}" | 
					
						
							|  |  |  |     return params | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def response(resp): | 
					
						
							|  |  |  |     results = [] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-30 18:41:13 +02:00
										 |  |  |     json_resp = resp.json() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     for result in json_resp: | 
					
						
							| 
									
										
										
										
											2023-09-27 14:01:28 +02:00
										 |  |  |         url = result['homepage'] | 
					
						
							|  |  |  |         if not url: | 
					
						
							|  |  |  |             url = result['url_resolved'] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-30 18:41:13 +02:00
										 |  |  |         content = [] | 
					
						
							|  |  |  |         tags = ', '.join(result.get('tags', '').split(',')) | 
					
						
							|  |  |  |         if tags: | 
					
						
							|  |  |  |             content.append(tags) | 
					
						
							|  |  |  |         for x in ['state', 'country']: | 
					
						
							|  |  |  |             v = result.get(x) | 
					
						
							|  |  |  |             if v: | 
					
						
							|  |  |  |                 v = str(v).strip() | 
					
						
							|  |  |  |                 content.append(v) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         metadata = [] | 
					
						
							|  |  |  |         codec = result.get('codec') | 
					
						
							|  |  |  |         if codec and codec.lower() != 'unknown': | 
					
						
							|  |  |  |             metadata.append(f'{codec} ' + gettext('radio')) | 
					
						
							|  |  |  |         for x, y in [ | 
					
						
							|  |  |  |             (gettext('bitrate'), 'bitrate'), | 
					
						
							|  |  |  |             (gettext('votes'), 'votes'), | 
					
						
							|  |  |  |             (gettext('clicks'), 'clickcount'), | 
					
						
							|  |  |  |         ]: | 
					
						
							|  |  |  |             v = result.get(y) | 
					
						
							|  |  |  |             if v: | 
					
						
							|  |  |  |                 v = str(v).strip() | 
					
						
							|  |  |  |                 metadata.append(f"{x} {v}") | 
					
						
							| 
									
										
										
										
											2023-09-27 14:01:28 +02:00
										 |  |  |         results.append( | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 'url': url, | 
					
						
							|  |  |  |                 'title': result['name'], | 
					
						
							| 
									
										
										
										
											2023-09-30 18:41:13 +02:00
										 |  |  |                 'img_src': result.get('favicon', '').replace("http://", "https://"), | 
					
						
							|  |  |  |                 'content': ' | '.join(content), | 
					
						
							|  |  |  |                 'metadata': ' | '.join(metadata), | 
					
						
							| 
									
										
										
										
											2023-09-27 14:01:28 +02:00
										 |  |  |                 'iframe_src': result['url_resolved'].replace("http://", "https://"), | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return results | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def fetch_traits(engine_traits: EngineTraits): | 
					
						
							| 
									
										
										
										
											2023-09-30 18:41:13 +02:00
										 |  |  |     """Fetch languages and countrycodes from RadioBrowser
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     - ``traits.languages``: `list of languages API`_ | 
					
						
							|  |  |  |     - ``traits.custom['countrycodes']``: `list of countries API`_ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     .. _list of countries API: https://de1.api.radio-browser.info/#List_of_countries | 
					
						
							|  |  |  |     .. _list of languages API: https://de1.api.radio-browser.info/#List_of_languages | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     # pylint: disable=import-outside-toplevel | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     from babel.core import get_global | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     babel_reg_list = get_global("territory_languages").keys() | 
					
						
							| 
									
										
										
										
											2023-09-27 14:01:28 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-30 18:41:13 +02:00
										 |  |  |     language_list = get(f'{base_url}/json/languages').json()  # type: ignore | 
					
						
							|  |  |  |     country_list = get(f'{base_url}/json/countries').json()  # type: ignore | 
					
						
							| 
									
										
										
										
											2023-09-27 14:01:28 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     for lang in language_list: | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-30 18:41:13 +02:00
										 |  |  |         babel_lang = lang.get('iso_639') | 
					
						
							|  |  |  |         if not babel_lang: | 
					
						
							|  |  |  |             # the language doesn't have any iso code, and hence can't be parsed | 
					
						
							|  |  |  |             # print(f"ERROR: lang - no iso code in {lang}") | 
					
						
							| 
									
										
										
										
											2023-09-27 14:01:28 +02:00
										 |  |  |             continue | 
					
						
							|  |  |  |         try: | 
					
						
							| 
									
										
										
										
											2023-09-30 18:41:13 +02:00
										 |  |  |             sxng_tag = language_tag(babel.Locale.parse(babel_lang, sep="-")) | 
					
						
							| 
									
										
										
										
											2023-09-27 14:01:28 +02:00
										 |  |  |         except babel.UnknownLocaleError: | 
					
						
							| 
									
										
										
										
											2023-09-30 18:41:13 +02:00
										 |  |  |             # print(f"ERROR: language tag {babel_lang} is unknown by babel") | 
					
						
							| 
									
										
										
										
											2023-09-27 14:01:28 +02:00
										 |  |  |             continue | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-30 18:41:13 +02:00
										 |  |  |         eng_tag = lang['name'] | 
					
						
							| 
									
										
										
										
											2023-09-27 14:01:28 +02:00
										 |  |  |         conflict = engine_traits.languages.get(sxng_tag) | 
					
						
							|  |  |  |         if conflict: | 
					
						
							| 
									
										
										
										
											2023-09-30 18:41:13 +02:00
										 |  |  |             if conflict != eng_tag: | 
					
						
							|  |  |  |                 print("CONFLICT: babel %s --> %s, %s" % (sxng_tag, conflict, eng_tag)) | 
					
						
							| 
									
										
										
										
											2023-09-27 14:01:28 +02:00
										 |  |  |             continue | 
					
						
							| 
									
										
										
										
											2023-09-30 18:41:13 +02:00
										 |  |  |         engine_traits.languages[sxng_tag] = eng_tag | 
					
						
							| 
									
										
										
										
											2023-09-27 14:01:28 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-30 18:41:13 +02:00
										 |  |  |     countrycodes = set() | 
					
						
							|  |  |  |     for region in country_list: | 
					
						
							|  |  |  |         if region['iso_3166_1'] not in babel_reg_list: | 
					
						
							|  |  |  |             print(f"ERROR: region tag {region['iso_3166_1']} is unknown by babel") | 
					
						
							|  |  |  |             continue | 
					
						
							|  |  |  |         countrycodes.add(region['iso_3166_1']) | 
					
						
							| 
									
										
										
										
											2023-09-27 14:01:28 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-30 18:41:13 +02:00
										 |  |  |     countrycodes = list(countrycodes) | 
					
						
							|  |  |  |     countrycodes.sort() | 
					
						
							|  |  |  |     engine_traits.custom['countrycodes'] = countrycodes |