#!/usr/bin/env python3 # -*- coding: utf-8 -*- from __future__ import unicode_literals import logging import re import time import requests from mopidy import httpclient logger = logging.getLogger(__name__) class Station(object): id = None continent = None country = None city = None genres = None name = None stream_url = None image_tiny = None image_small = None image_medium = None image_large = None description = None playable = False class RadioNetClient(object): base_url = "https://radio.net/" session = requests.Session() api_prefix = None min_bitrate = 96 max_top_stations = 100 station_bookmarks = None api_key = None stations_images = [] favorites = [] cache = {} stations_by_id = {} stations_by_slug = {} category_param_map = { "genres": "genre", "topics": "topic", "languages": "language", "cities": "city", "countries": "country", } def __init__(self, proxy_config=None, user_agent=None): super(RadioNetClient, self).__init__() self.session = requests.Session() if proxy_config is not None: proxy = httpclient.format_proxy(proxy_config) self.session.proxies.update({"http": proxy, "https": proxy}) full_user_agent = httpclient.format_user_agent(user_agent) self.session.headers.update({"user-agent": full_user_agent}) self.session.headers.update({"cache-control": "no-cache"}) self.update_prefix() def __del__(self): self.session.close() def set_lang(self, lang): langs = ["net", "de", "at", "fr", "pt", "es", "dk", "se", "it", "pl"] if lang in langs: self.base_url = self.base_url.replace(".net", "." + lang) else: logging.error("Radio.net not supported language: %s", str(lang)) self.update_prefix() def update_prefix(self): tmp_str = self.session.get(self.base_url) lang = self.base_url.split(".")[-1].replace("/", "") self.api_prefix = "https://api.radio." + lang + "/info/v2" def set_apikey(self, api_key): self.api_key = api_key def do_get(self, api_suffix, url_params=None): if self.api_prefix is None: return None if url_params is None: url_params = {} url_params["apikey"] = self.api_key response = self.session.get(self.api_prefix + api_suffix, params=url_params) return response def get_cache(self, key): if self.cache.get(key) is not None and self.cache[key].expired() is False: return self.cache[key].value() return None def set_cache(self, key, value, expires): self.cache[key] = CacheItem(value, expires) return value def get_station_by_id(self, station_id): if not self.stations_by_id.get(station_id): return self._get_station_by_id(station_id) return self.stations_by_id.get(station_id) def get_station_by_slug(self, station_slug): if not self.stations_by_slug.get(station_slug): return self._get_station_by_id(station_slug) return self.stations_by_slug.get(station_slug) def _get_station_by_id(self, station_id): cache_key = "station/" + str(station_id) cache = self.get_cache(cache_key) if cache is not None: return cache api_suffix = "/search/station" url_params = { "station": station_id, } response = self.do_get(api_suffix, url_params) if response.status_code != 200: logger.error( "Radio.net: Error on get station by id " + str(station_id) + ". Error: " + response.text ) return False logger.debug("Radio.net: Done get top stations list") json = response.json() if not self.stations_by_id.get(json["id"]): station = Station() station.playable = True else: station = self.stations_by_id[json["id"]] station.id = json["id"] station.continent = json["continent"] station.country = json["country"] station.city = json["city"] station.genres = ", ".join(json["genres"]) station.name = json["name"] station.slug = json["subdomain"] station.stream_url = self._get_stream_url(json["streamUrls"], self.min_bitrate) station.image_tiny = json["logo44x44"] station.image_small = json["logo100x100"] station.image_medium= json["logo175x175"] station.image_large = json["logo300x300"] station.description = json["shortDescription"] if json["playable"] == "PLAYABLE": station.playable = True self.stations_by_id[station.id] = station self.stations_by_slug[station.slug] = station self.set_cache("station/" + str(station.id), station, 1440) self.set_cache("station/" + station.slug, station, 1440) return station def _get_station_from_search_result(self, result): if not self.stations_by_id.get(result["id"]): station = Station() station.playable = True else: station = self.stations_by_id[result["id"]] station.id = result["id"] if result["continent"] is not None: station.continent = result["continent"]["value"] else: station.continent = "" if result["country"] is not None: station.country = result["country"]["value"] else: station.country = "" if result["city"] is not None: station.city = result["city"]["value"] else: station.city = "" if result["name"] is not None: station.name = result["name"]["value"] else: station.name = "" if result["subdomain"] is not None: station.slug = result["subdomain"]["value"] else: station.slug = "" if result["shortDescription"] is not None: station.description = result["shortDescription"]["value"] else: station.description = "" station.image_tiny = result["logo44x44"] station.image_small = result["logo100x100"] station.image_medium = result["logo175x175"] self.stations_by_id[station.id] = station self.stations_by_slug[station.slug] = station return station def get_genres(self): return self._get_items("genres") def get_topics(self): return self._get_items("topics") def get_languages(self): return self._get_items("languages") def get_cities(self): return self._get_items("cities") def get_countries(self): return self._get_items("countries") def _get_items(self, key): cached = self.get_cache(key) if cached is not None: return cached api_suffix = "/search/get" + key response = self.do_get(api_suffix) if response.status_code != 200: logger.error( "Radio.net: Error on get item list " + str(api_suffix) + ". Error: " + response.text ) return False return self.set_cache(key, response.json(), 1440) def get_sorted_category(self, category, name, sorting, page): results = [] for result in self._get_sorted_category(category, name, sorting, page): results.append(self._get_station_from_search_result(result)) return results def _get_sorted_category(self, category, name, sorting, page): if sorting == "az": sorting = "STATION_NAME" else: sorting = "RANK" cache_key = category + "/" + name + "/" + sorting + "/" + str(page) cache = self.get_cache(cache_key) if cache is not None: return cache api_suffix = "/search/stationsby" + self.category_param_map[category] url_params = { self.category_param_map[category]: name, "sorttype": sorting, "sizeperpage": 50, "pageindex": page, } response = self.do_get(api_suffix, url_params) if response.status_code != 200: logger.error( "Radio.net: Error on get station by " + str(category) + ". Error: " + response.text ) return False json = response.json() self.set_cache(category + "/" + name, int(json["numberPages"]), 10) return self.set_cache(cache_key, json["categories"][0]["matches"], 10) def get_category(self, category, page): results = [] for result in self._get_category(category, page): results.append(self._get_station_from_search_result(result)) return results def _get_category(self, category, page): cache_key = category + "/" + str(page) cache = self.get_cache(cache_key) if cache is not None: return cache api_suffix = "/search/" + category url_params = {"sizeperpage": 50, "pageindex": page} response = self.do_get(api_suffix, url_params) if response.status_code != 200: logger.error( "Radio.net: Error on get station by " + str(category) + ". Error: " + response.text ) return False json = response.json() self.set_cache(category, int(json["numberPages"]), 10) return self.set_cache(cache_key, json["categories"][0]["matches"], 10) def get_sorted_category_pages(self, category, name): cache_key = category + "/" + name cache = self.get_cache(cache_key) if cache is not None: return cache self.get_sorted_category(category, name, "rank", 1) return self.get_cache(cache_key) def get_category_pages(self, category): cache_key = category cache = self.get_cache(cache_key) if cache is not None: return cache self.get_category(category, 1) return self.get_cache(cache_key) def set_favorites(self, favorites): self.favorites = favorites def get_favorites(self): cache_key = "favorites" cache = self.get_cache(cache_key) if cache is not None: return cache favorite_stations = [] for station_slug in self.favorites: station = self.get_station_by_slug(station_slug) if station is False: api_suffix = "/search/stationsonly" url_params = { "query": station_slug, "pageindex": 1, } response = self.do_get(api_suffix, url_params) if response.status_code != 200: logger.error("Radio.net: Search error " + response.text) else: logger.debug("Radio.net: Done search") json = response.json() number_pages = int(json["numberPages"]) logger.error(json) if number_pages != 0: # take only the first match! station = self._get_station_from_search_result( json["categories"][0]["matches"][0] ) else: logger.warning("Radio.net: No results for %s", station_slug) if station and station.playable: favorite_stations.append(station) logger.info( "Radio.net: Loaded " + str(len(favorite_stations)) + " favorite stations." ) return self.set_cache(cache_key, favorite_stations, 1440) def do_search(self, query_string, page_index=1, search_results=None): api_suffix = "/search/stationsonly" url_params = { "query": query_string, "sizeperpage": 50, "pageindex": page_index, } response = self.do_get(api_suffix, url_params) if response.status_code != 200: logger.error("Radio.net: Search error " + response.text) else: logger.debug("Radio.net: Done search") if search_results is None: search_results = [] json = response.json() for match in json["categories"][0]["matches"]: station = self._get_station_from_search_result(match) if station and station.playable: search_results.append(station) number_pages = int(json["numberPages"]) if number_pages >= page_index: self.do_search(query_string, page_index + 1, search_results) else: logger.info( "Radio.net: Found " + str(len(search_results)) + " stations." ) return search_results def get_stream_url(self, station_id): station = self.get_station_by_id(station_id) if not station.stream_url: station = self._get_station_by_id(station.id) return station.stream_url def _get_stream_url(self, stream_json, bit_rate): stream_url = None for stream in stream_json: if int(stream["bitRate"]) >= bit_rate and stream["streamStatus"] == "VALID": stream_url = stream["streamUrl"] break if stream_url is None and len(stream_json) > 0: stream_url = stream_json[0]["streamUrl"] return stream_url class CacheItem(object): def __init__(self, value, expires=10): self._value = value self._expires = expires = time.time() + expires * 60 def expired(self): return self._expires < time.time() def value(self): return self._value