radio_browser.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. # SPDX-License-Identifier: AGPL-3.0-or-later
  2. """Search radio stations from RadioBrowser by `Advanced station search API`_.
  3. .. _Advanced station search API:
  4. https://de1.api.radio-browser.info/#Advanced_station_search
  5. """
  6. from urllib.parse import urlencode
  7. import babel
  8. from flask_babel import gettext
  9. from searx.network import get
  10. from searx.enginelib.traits import EngineTraits
  11. from searx.locales import language_tag
  12. traits: EngineTraits
  13. about = {
  14. "website": 'https://www.radio-browser.info/',
  15. "wikidata_id": 'Q111664849',
  16. "official_api_documentation": 'https://de1.api.radio-browser.info/',
  17. "use_official_api": True,
  18. "require_api_key": False,
  19. "results": 'JSON',
  20. }
  21. paging = True
  22. categories = ['music', 'radio']
  23. base_url = "https://de1.api.radio-browser.info" # see https://api.radio-browser.info/ for all nodes
  24. number_of_results = 10
  25. station_filters = [] # ['countrycode', 'language']
  26. """A list of filters to be applied to the search of radio stations. By default
  27. none filters are applied. Valid filters are:
  28. ``language``
  29. Filter stations by selected language. For instance the ``de`` from ``:de-AU``
  30. will be translated to `german` and used in the argument ``language=``.
  31. ``countrycode``
  32. Filter stations by selected country. The 2-digit countrycode of the station
  33. comes from the region the user selected. For instance ``:de-AU`` will filter
  34. out all stations not in ``AU``.
  35. .. note::
  36. RadioBrowser has registered a lot of languages and countrycodes unknown to
  37. :py:obj:`babel` and note that when searching for radio stations, users are
  38. more likely to search by name than by region or language.
  39. """
  40. def request(query, params):
  41. args = {
  42. 'name': query,
  43. 'order': 'votes',
  44. 'offset': (params['pageno'] - 1) * number_of_results,
  45. 'limit': number_of_results,
  46. 'hidebroken': 'true',
  47. 'reverse': 'true',
  48. }
  49. if 'language' in station_filters:
  50. lang = traits.get_language(params['searxng_locale']) # type: ignore
  51. if lang:
  52. args['language'] = lang
  53. if 'countrycode' in station_filters:
  54. if len(params['searxng_locale'].split('-')) > 1:
  55. countrycode = params['searxng_locale'].split('-')[-1].upper()
  56. if countrycode in traits.custom['countrycodes']: # type: ignore
  57. args['countrycode'] = countrycode
  58. params['url'] = f"{base_url}/json/stations/search?{urlencode(args)}"
  59. return params
  60. def response(resp):
  61. results = []
  62. json_resp = resp.json()
  63. for result in json_resp:
  64. url = result['homepage']
  65. if not url:
  66. url = result['url_resolved']
  67. content = []
  68. tags = ', '.join(result.get('tags', '').split(','))
  69. if tags:
  70. content.append(tags)
  71. for x in ['state', 'country']:
  72. v = result.get(x)
  73. if v:
  74. v = str(v).strip()
  75. content.append(v)
  76. metadata = []
  77. codec = result.get('codec')
  78. if codec and codec.lower() != 'unknown':
  79. metadata.append(f'{codec} ' + gettext('radio'))
  80. for x, y in [
  81. (gettext('bitrate'), 'bitrate'),
  82. (gettext('votes'), 'votes'),
  83. (gettext('clicks'), 'clickcount'),
  84. ]:
  85. v = result.get(y)
  86. if v:
  87. v = str(v).strip()
  88. metadata.append(f"{x} {v}")
  89. results.append(
  90. {
  91. 'url': url,
  92. 'title': result['name'],
  93. 'thumbnail': result.get('favicon', '').replace("http://", "https://"),
  94. 'content': ' | '.join(content),
  95. 'metadata': ' | '.join(metadata),
  96. 'iframe_src': result['url_resolved'].replace("http://", "https://"),
  97. }
  98. )
  99. return results
  100. def fetch_traits(engine_traits: EngineTraits):
  101. """Fetch languages and countrycodes from RadioBrowser
  102. - ``traits.languages``: `list of languages API`_
  103. - ``traits.custom['countrycodes']``: `list of countries API`_
  104. .. _list of countries API: https://de1.api.radio-browser.info/#List_of_countries
  105. .. _list of languages API: https://de1.api.radio-browser.info/#List_of_languages
  106. """
  107. # pylint: disable=import-outside-toplevel
  108. from babel.core import get_global
  109. babel_reg_list = get_global("territory_languages").keys()
  110. language_list = get(f'{base_url}/json/languages').json() # type: ignore
  111. country_list = get(f'{base_url}/json/countries').json() # type: ignore
  112. for lang in language_list:
  113. babel_lang = lang.get('iso_639')
  114. if not babel_lang:
  115. # the language doesn't have any iso code, and hence can't be parsed
  116. # print(f"ERROR: lang - no iso code in {lang}")
  117. continue
  118. try:
  119. sxng_tag = language_tag(babel.Locale.parse(babel_lang, sep="-"))
  120. except babel.UnknownLocaleError:
  121. # print(f"ERROR: language tag {babel_lang} is unknown by babel")
  122. continue
  123. eng_tag = lang['name']
  124. conflict = engine_traits.languages.get(sxng_tag)
  125. if conflict:
  126. if conflict != eng_tag:
  127. print("CONFLICT: babel %s --> %s, %s" % (sxng_tag, conflict, eng_tag))
  128. continue
  129. engine_traits.languages[sxng_tag] = eng_tag
  130. countrycodes = set()
  131. for region in country_list:
  132. # country_list contains duplicates that differ only in upper/lower case
  133. _reg = region['iso_3166_1'].upper()
  134. if _reg not in babel_reg_list:
  135. print(f"ERROR: region tag {region['iso_3166_1']} is unknown by babel")
  136. continue
  137. countrycodes.add(_reg)
  138. countrycodes = list(countrycodes)
  139. countrycodes.sort()
  140. engine_traits.custom['countrycodes'] = countrycodes