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