summaryrefslogtreecommitdiff
path: root/searx/engines/radio_browser.py
blob: a8f07a6380791519afd36688986df161c7f6e220 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Search radio stations from RadioBrowser by `Advanced station search API`_.

.. _Advanced station search API:
   https://de1.api.radio-browser.info/#Advanced_station_search

"""

from urllib.parse import urlencode
import babel
from flask_babel import gettext

from searx.network import get
from searx.enginelib.traits import EngineTraits
from searx.locales import language_tag

traits: EngineTraits

about = {
    "website": 'https://www.radio-browser.info/',
    "wikidata_id": 'Q111664849',
    "official_api_documentation": 'https://de1.api.radio-browser.info/',
    "use_official_api": True,
    "require_api_key": False,
    "results": 'JSON',
}
paging = True
categories = ['music', 'radio']

base_url = "https://de1.api.radio-browser.info"  # see https://api.radio-browser.info/ for all nodes
number_of_results = 10

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.

"""


def request(query, params):
    args = {
        'name': query,
        'order': 'votes',
        'offset': (params['pageno'] - 1) * number_of_results,
        'limit': number_of_results,
        'hidebroken': 'true',
        'reverse': 'true',
    }

    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

    params['url'] = f"{base_url}/json/stations/search?{urlencode(args)}"
    return params


def response(resp):
    results = []

    json_resp = resp.json()

    for result in json_resp:
        url = result['homepage']
        if not url:
            url = result['url_resolved']

        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}")
        results.append(
            {
                'url': url,
                'title': result['name'],
                'thumbnail': result.get('favicon', '').replace("http://", "https://"),
                'content': ' | '.join(content),
                'metadata': ' | '.join(metadata),
                'iframe_src': result['url_resolved'].replace("http://", "https://"),
            }
        )

    return results


def fetch_traits(engine_traits: EngineTraits):
    """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()

    language_list = get(f'{base_url}/json/languages').json()  # type: ignore
    country_list = get(f'{base_url}/json/countries').json()  # type: ignore

    for lang in language_list:

        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}")
            continue
        try:
            sxng_tag = language_tag(babel.Locale.parse(babel_lang, sep="-"))
        except babel.UnknownLocaleError:
            # print(f"ERROR: language tag {babel_lang} is unknown by babel")
            continue

        eng_tag = lang['name']
        conflict = engine_traits.languages.get(sxng_tag)
        if conflict:
            if conflict != eng_tag:
                print("CONFLICT: babel %s --> %s, %s" % (sxng_tag, conflict, eng_tag))
            continue
        engine_traits.languages[sxng_tag] = eng_tag

    countrycodes = set()
    for region in country_list:
        # country_list contains duplicates that differ only in upper/lower case
        _reg = region['iso_3166_1'].upper()
        if _reg not in babel_reg_list:
            print(f"ERROR: region tag {region['iso_3166_1']} is unknown by babel")
            continue
        countrycodes.add(_reg)

    countrycodes = list(countrycodes)
    countrycodes.sort()
    engine_traits.custom['countrycodes'] = countrycodes