Compare commits
14 Commits
cb2c4f07b5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 02ba383ab0 | |||
| 20c341de98 | |||
| 5fbc51fba7 | |||
| a09a007787 | |||
| f3f2fbd42b | |||
|
|
62bf24b9b5 | ||
|
|
7b19c20269 | ||
|
|
9c2c78c373 | ||
|
|
55aab87f4e | ||
|
|
9382eb2ff4 | ||
|
|
5f40f6ccad | ||
|
|
34e4892975 | ||
|
|
0df2ad24b7 | ||
|
|
424173da27 |
49
README.md
Normal file
49
README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Mopidy Music Server Configuration
|
||||
|
||||
This repository contains the configuration and setup for the local Mopidy instance. Mopidy acts as the primary music logic server (Radio, Local Files, Extensions) feeding into the Snapcast multi-room audio system.
|
||||
|
||||
This system ses snapweb which needs to be set to MainAudio for Piper Voice play http://192.168.20.13:1780/
|
||||
|
||||
## 🎵 System Architecture
|
||||
|
||||
* **Core:** Mopidy (Python-based music server).
|
||||
* **Output:** Pipes audio to Snapserver via a Named Pipe (`/tmp/snapfifo`).
|
||||
* **Frontend:** Iris Web Interface (Port 6680).
|
||||
* **Extensions:** `mopidy-radionet`, `mopidy-local`, etc.
|
||||
|
||||
## 📂 Key Files
|
||||
|
||||
* **`mopidy.conf`**: The master configuration file. Controls enabled plugins, file paths, and GStreamer audio pipelines.
|
||||
* **`requirements.txt`** (Optional): Python pip dependencies for installed plugins.
|
||||
|
||||
## ⚙️ Configuration Highlights
|
||||
|
||||
### Audio Output (Snapcast Link)
|
||||
To ensure compatibility with Snapserver, the audio output is forced to **48kHz Stereo 16-bit** and written to the pipe:
|
||||
|
||||
```ini
|
||||
[audio]
|
||||
output = audioresample ! audioconvert ! audio/x-raw,rate=48000,channels=2,format=S16LE ! wavenc ! filesink location=/tmp/snapfifo
|
||||
```
|
||||
|
||||
## 🚀 Management Commands
|
||||
|
||||
**Restart Service:**
|
||||
```bash
|
||||
sudo systemctl restart mopidy
|
||||
```
|
||||
|
||||
**Check Status & Logs:**
|
||||
```bash
|
||||
sudo systemctl status mopidy
|
||||
sudo journalctl -u mopidy -f
|
||||
```
|
||||
|
||||
**Rescan Local Library:**
|
||||
```bash
|
||||
sudo mopidy local scan
|
||||
```
|
||||
|
||||
**Config Location:**
|
||||
* `/etc/mopidy/mopidy.conf` (System Service)
|
||||
* `~/.config/mopidy/mopidy.conf` (User Mode)
|
||||
@@ -1,8 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
import time
|
||||
|
||||
import pykka
|
||||
from mopidy import backend
|
||||
|
||||
@@ -29,8 +27,7 @@ class RadioNetBackend(pykka.ThreadingActor, backend.Backend):
|
||||
self.uri_schemes = ["radionet"]
|
||||
|
||||
self.radionet.min_bitrate = int(config["radionet"]["min_bitrate"])
|
||||
self.radionet.set_lang(str(config["radionet"]["language"]))
|
||||
self.radionet.set_apikey(str(config["radionet"]["api_key"]))
|
||||
self.radionet.set_lang(str(config["radionet"]["language"]).strip())
|
||||
self.radionet.set_favorites(
|
||||
tuple(
|
||||
file_ext.strip("'").lower() for file_ext in config["radionet"]["favorite_stations"]
|
||||
@@ -43,8 +40,9 @@ class RadioNetPlaybackProvider(backend.PlaybackProvider):
|
||||
return True
|
||||
|
||||
def translate_uri(self, uri):
|
||||
identifier = re.findall(r"^radionet:track:?([a-z0-9]+|\d+)?$", uri)
|
||||
identifier = re.findall(r"^radionet:track:?(.+)?$", uri)
|
||||
if identifier:
|
||||
print(identifier)
|
||||
return self.backend.radionet.get_stream_url(identifier[0])
|
||||
|
||||
return None
|
||||
|
||||
@@ -4,7 +4,8 @@ import logging
|
||||
import re
|
||||
|
||||
from mopidy import backend
|
||||
from mopidy.models import Album, Artist, Ref, SearchResult, Track
|
||||
from mopidy.models import Album, Artist, Ref, SearchResult, Track, Image
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -20,21 +21,18 @@ class RadioNetLibraryProvider(backend.LibraryProvider):
|
||||
if not uri.startswith("radionet:"):
|
||||
return None
|
||||
|
||||
variant, identifier, sorting, page = self.parse_uri(uri)
|
||||
variant, identifier, page = self.parse_uri(uri)
|
||||
|
||||
if variant == "station" or variant == "track":
|
||||
identifier = int(identifier)
|
||||
radio_data = self.backend.radionet.get_station_by_id(identifier)
|
||||
|
||||
artist = Artist(name=radio_data.name)
|
||||
|
||||
name = ""
|
||||
if radio_data.description is not None:
|
||||
name = radio_data.description + " / "
|
||||
name = radio_data.description
|
||||
name = (
|
||||
name
|
||||
+ radio_data.continent
|
||||
+ " / "
|
||||
+ radio_data.country
|
||||
+ " - "
|
||||
+ radio_data.city
|
||||
@@ -43,7 +41,7 @@ class RadioNetLibraryProvider(backend.LibraryProvider):
|
||||
album = Album(
|
||||
artists=[artist],
|
||||
name=name,
|
||||
uri="radionet:station:%s" % (identifier),
|
||||
uri="radionet:station:%s" % radio_data.id,
|
||||
)
|
||||
|
||||
track = Track(
|
||||
@@ -52,7 +50,7 @@ class RadioNetLibraryProvider(backend.LibraryProvider):
|
||||
name=radio_data.name,
|
||||
genre=radio_data.genres,
|
||||
comment=radio_data.description,
|
||||
uri="radionet:track:%s" % (identifier),
|
||||
uri="radionet:track:%s" % radio_data.id,
|
||||
)
|
||||
return [track]
|
||||
|
||||
@@ -60,22 +58,39 @@ class RadioNetLibraryProvider(backend.LibraryProvider):
|
||||
|
||||
def browse(self, uri):
|
||||
|
||||
category, page, value, sorting = self.parse_uri(uri)
|
||||
category, page, value = self.parse_uri(uri)
|
||||
|
||||
if category == "root":
|
||||
return self._browse_root()
|
||||
elif category in ["favorites", "topstations", "localstations"]:
|
||||
return self._browse_category(category, page)
|
||||
elif category in ["favorites", "local"]:
|
||||
return self._browse_simple_category(category, page)
|
||||
elif category in ["genres", "topics", "languages", "cities", "countries"]:
|
||||
return self._browse_sorted_category(category, value, sorting, page)
|
||||
result = self._browse_category(category, value, page)
|
||||
return result
|
||||
else:
|
||||
logger.debug("Unknown URI: %s", uri)
|
||||
return []
|
||||
|
||||
def get_images(self, uris):
|
||||
images = {}
|
||||
for uri in uris:
|
||||
variant, identifier, page = self.parse_uri(uri)
|
||||
station = self.backend.radionet.get_station_by_id(identifier)
|
||||
if station:
|
||||
images[uri] = []
|
||||
if station.image_tiny:
|
||||
images[uri].append(Image(uri=station.image_tiny, height=44, width=44))
|
||||
if station.image_small:
|
||||
images[uri].append(Image(uri=station.image_small, height=100, width=100))
|
||||
if station.image_medium:
|
||||
images[uri].append(Image(uri=station.image_medium, height=175, width=175))
|
||||
if station.image_large:
|
||||
images[uri].append(Image(uri=station.image_large, height=300, width=300))
|
||||
return images
|
||||
|
||||
def _browse_root(self):
|
||||
directories = [
|
||||
self.ref_directory("radionet:topstations", "Top stations"),
|
||||
self.ref_directory("radionet:localstations", "Local stations"),
|
||||
self.ref_directory("radionet:local", "Local stations"),
|
||||
self.ref_directory("radionet:genres", "Genres"),
|
||||
self.ref_directory("radionet:topics", "Topics"),
|
||||
self.ref_directory("radionet:languages", "Languages"),
|
||||
@@ -86,22 +101,22 @@ class RadioNetLibraryProvider(backend.LibraryProvider):
|
||||
directories.insert(0, self.ref_directory("radionet:favorites", "Favorites"))
|
||||
return directories
|
||||
|
||||
def _browse_category(self, category, page):
|
||||
def _browse_simple_category(self, category, page):
|
||||
result = []
|
||||
if category == "favorites":
|
||||
items = self._get_favorites()
|
||||
if items:
|
||||
for item in items:
|
||||
result.append(self.station_to_ref(item))
|
||||
elif category == "topstations":
|
||||
items = self._get_topstations()
|
||||
if items:
|
||||
for item in items:
|
||||
result.append(self.station_to_ref(item))
|
||||
# elif category == "topstations":
|
||||
# items = self._get_topstations()
|
||||
# if items:
|
||||
# for item in items:
|
||||
# result.append(self.station_to_ref(item))
|
||||
elif not page:
|
||||
pages = self._get_category_pages(category)
|
||||
pages = self._get_simple_category_pages(category)
|
||||
if pages == 1:
|
||||
items = self._get_category(category, 1)
|
||||
items = self._get_simple_category(category, 1)
|
||||
if items:
|
||||
for item in items:
|
||||
result.append(self.station_to_ref(item))
|
||||
@@ -120,7 +135,7 @@ class RadioNetLibraryProvider(backend.LibraryProvider):
|
||||
result.append(self.station_to_ref(item))
|
||||
return result
|
||||
|
||||
def _browse_sorted_category(self, category, value, sorting, page):
|
||||
def _browse_category(self, category, value, page):
|
||||
result = []
|
||||
|
||||
if not value:
|
||||
@@ -129,25 +144,14 @@ class RadioNetLibraryProvider(backend.LibraryProvider):
|
||||
for item in items:
|
||||
result.append(
|
||||
self.ref_directory(
|
||||
"radionet:{0}:{1}".format(category, item["systemEnglish"]),
|
||||
item["localized"],
|
||||
)
|
||||
)
|
||||
elif not sorting or sorting not in ["rank", "az"]:
|
||||
result.append(
|
||||
self.ref_directory(
|
||||
"radionet:{0}:{1}:rank".format(category, value), "By rank"
|
||||
)
|
||||
)
|
||||
result.append(
|
||||
self.ref_directory(
|
||||
"radionet:{0}:{1}:az".format(category, value), "Alphabetical"
|
||||
"radionet:{0}:{1}".format(category, item["slug"]),
|
||||
item["name"],
|
||||
)
|
||||
)
|
||||
elif not page:
|
||||
pages = self._get_sorted_category_pages(category, value)
|
||||
pages = self._get_category_pages(category, value)
|
||||
if pages == 1:
|
||||
items = self._get_sorted_category(category, value, sorting, 1)
|
||||
items = self._get_category(category, value, 1)
|
||||
if items:
|
||||
for item in items:
|
||||
result.append(self.station_to_ref(item))
|
||||
@@ -155,14 +159,14 @@ class RadioNetLibraryProvider(backend.LibraryProvider):
|
||||
for index in range(pages):
|
||||
result.append(
|
||||
self.ref_directory(
|
||||
"radionet:{0}:{1}:{2}:{3}".format(
|
||||
category, value, sorting, str(index + 1)
|
||||
"radionet:{0}:{1}:{2}".format(
|
||||
category, value, str(index + 1)
|
||||
),
|
||||
str(index + 1),
|
||||
)
|
||||
)
|
||||
else:
|
||||
items = self._get_sorted_category(category, value, sorting, page)
|
||||
items = self._get_category(category, value, int(page))
|
||||
if items:
|
||||
for item in items:
|
||||
result.append(self.station_to_ref(item))
|
||||
@@ -184,19 +188,19 @@ class RadioNetLibraryProvider(backend.LibraryProvider):
|
||||
return self.backend.radionet.get_countries()
|
||||
|
||||
def _get_topstations(self):
|
||||
return self.backend.radionet.get_category("topstations", 1)
|
||||
return self.backend.radionet.get_simple_category("local", 1)
|
||||
|
||||
def _get_sorted_category(self, category, name, sorting, page):
|
||||
return self.backend.radionet.get_sorted_category(category, name, sorting, page)
|
||||
def _get_category(self, category, name, page):
|
||||
return self.backend.radionet.get_category(category, name, page)
|
||||
|
||||
def _get_sorted_category_pages(self, category, name):
|
||||
return self.backend.radionet.get_sorted_category_pages(category, name)
|
||||
def _get_category_pages(self, category, name):
|
||||
return self.backend.radionet.get_category_pages(category, name)
|
||||
|
||||
def _get_category(self, category, page):
|
||||
return self.backend.radionet.get_category(category, page)
|
||||
def _get_simple_category(self, category, page):
|
||||
return self.backend.radionet.get_simple_category(category, page)
|
||||
|
||||
def _get_category_pages(self, category):
|
||||
return self.backend.radionet.get_category_pages(category)
|
||||
def _get_simple_category_pages(self, category):
|
||||
return self.backend.radionet.get_simple_category_pages(category)
|
||||
|
||||
def _get_favorites(self):
|
||||
return self.backend.radionet.get_favorites()
|
||||
@@ -214,7 +218,7 @@ class RadioNetLibraryProvider(backend.LibraryProvider):
|
||||
|
||||
def station_to_ref(self, station):
|
||||
return Ref.track(
|
||||
uri="radionet:station:%s" % (station.id),
|
||||
uri="radionet:station:%s" % station.id,
|
||||
name=station.name,
|
||||
)
|
||||
|
||||
@@ -237,22 +241,20 @@ class RadioNetLibraryProvider(backend.LibraryProvider):
|
||||
category = None
|
||||
value = None
|
||||
page = None
|
||||
sorting = None
|
||||
|
||||
result = re.findall(
|
||||
r"^radionet:(genres|topics|languages|cities|countries)(:([^:]+)(:(rank|az)(:([0-9]+))?)?)?$",
|
||||
r"^radionet:(genres|topics|languages|cities|countries)(:([^:]+)(:([0-9]+))?)?$",
|
||||
uri,
|
||||
)
|
||||
|
||||
if result:
|
||||
category = result[0][0]
|
||||
value = result[0][2]
|
||||
sorting = result[0][4]
|
||||
page = result[0][6]
|
||||
page = result[0][4]
|
||||
|
||||
else:
|
||||
result = re.findall(
|
||||
r"^radionet:(root|favorites|topstations|localstations|station|track)(:([0-9]+))?$",
|
||||
r"^radionet:(root|favorites|local|station|track)(:([0-9]+))?$",
|
||||
uri,
|
||||
)
|
||||
|
||||
@@ -260,4 +262,14 @@ class RadioNetLibraryProvider(backend.LibraryProvider):
|
||||
category = result[0][0]
|
||||
page = result[0][2]
|
||||
|
||||
return category, page, value, sorting
|
||||
else:
|
||||
result = re.findall(
|
||||
r"^radionet:(track|station):([^:]+)$",
|
||||
uri,
|
||||
)
|
||||
|
||||
if result:
|
||||
category = result[0][0]
|
||||
page = result[0][1]
|
||||
|
||||
return category, page, value
|
||||
|
||||
@@ -2,15 +2,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
import urllib
|
||||
|
||||
import requests
|
||||
from mopidy import httpclient
|
||||
from urllib.request import urlopen, Request
|
||||
|
||||
import math
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
REGIONS = {
|
||||
'at': 'de-AT',
|
||||
'au': 'en-AU',
|
||||
'br': 'pt-BR',
|
||||
'ca': 'en-CA',
|
||||
'co': 'es-CO',
|
||||
'de': 'de-DE',
|
||||
'dk': 'da-DK',
|
||||
'es': 'es-ES',
|
||||
'fr': 'fr-FR',
|
||||
'ie': 'en-IE',
|
||||
'it': 'it-IT',
|
||||
'mx': 'es-MX',
|
||||
'nl': 'nl-NL',
|
||||
'nz': 'en-NZ',
|
||||
'pl': 'pl-PL',
|
||||
'pt': 'pt-PT',
|
||||
'se': 'sv-SE',
|
||||
'uk': 'en-GB',
|
||||
'us': 'en-US',
|
||||
'za': 'en-ZA',
|
||||
}
|
||||
|
||||
|
||||
class Station(object):
|
||||
id = None
|
||||
@@ -20,16 +45,19 @@ class Station(object):
|
||||
genres = None
|
||||
name = None
|
||||
stream_url = None
|
||||
image = 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/"
|
||||
base_url = "https://prod.radio-api.net"
|
||||
language = REGIONS['us']
|
||||
user_agent = 'Radio.net - Web V5'
|
||||
|
||||
session = requests.Session()
|
||||
api_prefix = None
|
||||
min_bitrate = 96
|
||||
max_top_stations = 100
|
||||
station_bookmarks = None
|
||||
@@ -54,48 +82,26 @@ class RadioNetClient(object):
|
||||
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)
|
||||
if lang in REGIONS:
|
||||
self.language = REGIONS[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
|
||||
logging.warning("Radio.net not supported language: %s, defaulting to English", str(lang))
|
||||
|
||||
def do_get(self, api_suffix, url_params=None):
|
||||
if self.api_prefix is None:
|
||||
return None
|
||||
try:
|
||||
url = self.base_url + api_suffix
|
||||
if url_params is not None:
|
||||
url += "?" + urllib.parse.urlencode(url_params)
|
||||
req = Request(url)
|
||||
req.add_header('accept-language', self.language)
|
||||
req.add_header('user-agent', self.user_agent)
|
||||
response = urlopen(req).read()
|
||||
except Exception as err:
|
||||
logging.error(f'_open_url error: {err}')
|
||||
response = 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
|
||||
return json.loads(response)
|
||||
|
||||
def get_cache(self, key):
|
||||
if self.cache.get(key) is not None and self.cache[key].expired() is False:
|
||||
@@ -122,49 +128,25 @@ class RadioNetClient(object):
|
||||
if cache is not None:
|
||||
return cache
|
||||
|
||||
api_suffix = "/search/station"
|
||||
api_suffix = "/stations/details"
|
||||
|
||||
url_params = {
|
||||
"station": station_id,
|
||||
"stationIds": 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
|
||||
)
|
||||
if response is None or len(response) == 0:
|
||||
logger.error("Radio.net: Error on get station by id " + str(station_id))
|
||||
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 = json["logo100x100"]
|
||||
station.description = json["shortDescription"]
|
||||
if json["playable"] == "PLAYABLE":
|
||||
station.playable = True
|
||||
json = response[0]
|
||||
|
||||
self.stations_by_id[station.id] = station
|
||||
self.stations_by_slug[station.slug] = station
|
||||
station = self._get_station_from_search_result(json)
|
||||
|
||||
self.set_cache("station/" + str(station.id), station, 1440)
|
||||
self.set_cache("station/" + station.slug, station, 1440)
|
||||
self.set_cache(cache_key, station, 1440)
|
||||
return station
|
||||
|
||||
def _get_station_from_search_result(self, result):
|
||||
@@ -175,40 +157,56 @@ class RadioNetClient(object):
|
||||
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"]
|
||||
if "country" in result:
|
||||
station.country = result["country"]
|
||||
else:
|
||||
station.country = ""
|
||||
|
||||
if result["city"] is not None:
|
||||
station.city = result["city"]["value"]
|
||||
if "city" in result:
|
||||
station.city = result["city"]
|
||||
else:
|
||||
station.city = ""
|
||||
|
||||
if result["name"] is not None:
|
||||
station.name = result["name"]["value"]
|
||||
if "name" in result:
|
||||
station.name = result["name"]
|
||||
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"]
|
||||
if "shortDescription" in result:
|
||||
station.description = result["shortDescription"]
|
||||
else:
|
||||
station.description = ""
|
||||
|
||||
station.image = result["logo100x100"]
|
||||
if "genres" in result:
|
||||
station.genres = ", ".join(result["genres"])
|
||||
else:
|
||||
station.genres = ""
|
||||
|
||||
if "logo44x44" in result:
|
||||
station.image_tiny = result["logo44x44"]
|
||||
else:
|
||||
station.image_tiny = ""
|
||||
|
||||
if "logo100x100" in result:
|
||||
station.image_tiny = result["logo100x100"]
|
||||
else:
|
||||
station.image_tiny = ""
|
||||
|
||||
if "logo175x175" in result:
|
||||
station.image_medium = result["logo175x175"]
|
||||
else:
|
||||
station.image_medium = ""
|
||||
|
||||
if "logo300x300" in result:
|
||||
station.image_large = result["logo300x300"]
|
||||
else:
|
||||
station.image_large = ""
|
||||
|
||||
if "streams" in result:
|
||||
station.stream_url = self._get_stream_url(result["streams"], self.min_bitrate)
|
||||
|
||||
self.stations_by_id[station.id] = station
|
||||
self.stations_by_slug[station.slug] = station
|
||||
return station
|
||||
|
||||
def get_genres(self):
|
||||
@@ -231,106 +229,100 @@ class RadioNetClient(object):
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
api_suffix = "/search/get" + key
|
||||
api_suffix = "/stations/tags"
|
||||
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
|
||||
)
|
||||
if response is None:
|
||||
logger.error("Radio.net: Error on get item list")
|
||||
return False
|
||||
return self.set_cache(key, response.json(), 1440)
|
||||
|
||||
def get_sorted_category(self, category, name, sorting, page):
|
||||
return self.set_cache(key, self._filter_result(response, key, 0), 1440)
|
||||
|
||||
def _filter_result(self, data, tag, min_count_stations):
|
||||
api_result = data.get(tag, [])
|
||||
|
||||
if api_result and min_count_stations:
|
||||
# filter result by minimum count of stations
|
||||
result = []
|
||||
|
||||
for item in api_result:
|
||||
if item['count'] >= min_count_stations:
|
||||
result.append(item)
|
||||
|
||||
return result
|
||||
|
||||
return api_result
|
||||
|
||||
def get_category(self, category, name, page=1):
|
||||
results = []
|
||||
for result in self._get_sorted_category(category, name, sorting, page):
|
||||
for result in self._get_category(category, name, page):
|
||||
results.append(self._get_station_from_search_result(result))
|
||||
return results
|
||||
|
||||
def _get_sorted_category(self, category, name, sorting, page):
|
||||
def _get_category(self, category, name, page=1):
|
||||
|
||||
if sorting == "az":
|
||||
sorting = "STATION_NAME"
|
||||
else:
|
||||
sorting = "RANK"
|
||||
|
||||
cache_key = category + "/" + name + "/" + sorting + "/" + str(page)
|
||||
cache_key = category + "/" + name + "/" + str(page)
|
||||
cache = self.get_cache(cache_key)
|
||||
if cache is not None:
|
||||
return cache
|
||||
|
||||
api_suffix = "/search/stationsby" + self.category_param_map[category]
|
||||
api_suffix = "/stations/by-tag"
|
||||
url_params = {
|
||||
self.category_param_map[category]: name,
|
||||
"sorttype": sorting,
|
||||
"sizeperpage": 50,
|
||||
"pageindex": page,
|
||||
"tagType": category,
|
||||
"slug": name,
|
||||
"count": 50,
|
||||
"offset": (page - 1) * 50,
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
if response is None:
|
||||
logger.error("Radio.net: Error on get station by " + str(category))
|
||||
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)
|
||||
self.set_cache(category + "/" + name, int(math.ceil(response["totalCount"] / 50)), 10)
|
||||
return self.set_cache(cache_key, response["playables"], 10)
|
||||
|
||||
def get_category(self, category, page):
|
||||
def get_simple_category(self, category, page=1):
|
||||
results = []
|
||||
for result in self._get_category(category, page):
|
||||
for result in self._get_simple_category(category, page):
|
||||
results.append(self._get_station_from_search_result(result))
|
||||
return results
|
||||
|
||||
def _get_category(self, category, page):
|
||||
def _get_simple_category(self, category, page=1):
|
||||
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}
|
||||
api_suffix = "/stations/" + category
|
||||
url_params = {"count": 50, "offset": (page - 1) * 50}
|
||||
|
||||
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
|
||||
)
|
||||
if response is None:
|
||||
logger.error("Radio.net: Error on get station by " + str(category))
|
||||
return False
|
||||
|
||||
json = response.json()
|
||||
self.set_cache(category, int(json["numberPages"]), 10)
|
||||
return self.set_cache(cache_key, json["categories"][0]["matches"], 10)
|
||||
self.set_cache(category, int(math.ceil(response["totalCount"] / 50)), 10)
|
||||
return self.set_cache(cache_key, response["playables"], 10)
|
||||
|
||||
def get_sorted_category_pages(self, category, name):
|
||||
def get_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)
|
||||
self.get_category(category, name, 1)
|
||||
|
||||
return self.get_cache(cache_key)
|
||||
|
||||
def get_category_pages(self, category):
|
||||
def get_simple_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)
|
||||
self.get_simple_category(category, 1)
|
||||
|
||||
return self.get_cache(cache_key)
|
||||
|
||||
@@ -346,26 +338,27 @@ class RadioNetClient(object):
|
||||
favorite_stations = []
|
||||
for station_slug in self.favorites:
|
||||
|
||||
station = self.get_station_by_slug(station_slug)
|
||||
station = self.get_station_by_id(station_slug)
|
||||
|
||||
if station is False:
|
||||
api_suffix = "/search/stationsonly"
|
||||
api_suffix = "/stations/search"
|
||||
url_params = {
|
||||
"query": station_slug,
|
||||
"pageindex": 1,
|
||||
"count": 1,
|
||||
"offset": 0,
|
||||
}
|
||||
response = self.do_get(api_suffix, url_params)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error("Radio.net: Search error " + response.text)
|
||||
if response is None:
|
||||
logger.error("Radio.net: Search error")
|
||||
else:
|
||||
logger.debug("Radio.net: Done search")
|
||||
json = response.json()
|
||||
|
||||
if "playables" in response and len(response["playables"]) > 0:
|
||||
# take only the first match!
|
||||
station = self._get_station_from_search_result(
|
||||
json["categories"][0]["matches"][0]
|
||||
)
|
||||
station = self._get_station_from_search_result(response["playables"][0])
|
||||
else:
|
||||
logger.warning("Radio.net: No results for %s", station_slug)
|
||||
|
||||
if station and station.playable:
|
||||
favorite_stations.append(station)
|
||||
@@ -377,29 +370,30 @@ class RadioNetClient(object):
|
||||
|
||||
def do_search(self, query_string, page_index=1, search_results=None):
|
||||
|
||||
api_suffix = "/search/stationsonly"
|
||||
api_suffix = "/stations/search"
|
||||
url_params = {
|
||||
"query": query_string,
|
||||
"sizeperpage": 50,
|
||||
"pageindex": page_index,
|
||||
"count": 50,
|
||||
"offset": (page_index - 1) * 50,
|
||||
}
|
||||
|
||||
logger.info(url_params)
|
||||
response = self.do_get(api_suffix, url_params)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error("Radio.net: Search error " + response.text)
|
||||
if response is None:
|
||||
logger.error("Radio.net: Search error")
|
||||
else:
|
||||
logger.debug("Radio.net: Done search")
|
||||
logger.info("Radio.net: Done search")
|
||||
if search_results is None:
|
||||
search_results = []
|
||||
json = response.json()
|
||||
for match in json["categories"][0]["matches"]:
|
||||
for match in response["playables"]:
|
||||
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:
|
||||
number_pages = int(math.ceil(response["totalCount"] / 50))
|
||||
# Search is utterly broken, don't retrieve more than 10 pages
|
||||
logger.info(page_index)
|
||||
if number_pages > page_index and page_index < 10:
|
||||
self.do_search(query_string, page_index + 1, search_results)
|
||||
else:
|
||||
logger.info(
|
||||
@@ -417,12 +411,13 @@ class RadioNetClient(object):
|
||||
stream_url = None
|
||||
|
||||
for stream in stream_json:
|
||||
if int(stream["bitRate"]) >= bit_rate and stream["streamStatus"] == "VALID":
|
||||
stream_url = stream["streamUrl"]
|
||||
logger.info("Radio.net: Found stream URL " + stream["url"])
|
||||
if ("bitRate" in stream and int(stream["bitRate"]) >= bit_rate) and stream["status"] == "VALID":
|
||||
stream_url = stream["url"]
|
||||
break
|
||||
|
||||
if stream_url is None and len(stream_json) > 0:
|
||||
stream_url = stream_json[0]["streamUrl"]
|
||||
stream_url = stream_json[0]["url"]
|
||||
|
||||
return stream_url
|
||||
|
||||
@@ -430,7 +425,7 @@ class RadioNetClient(object):
|
||||
class CacheItem(object):
|
||||
def __init__(self, value, expires=10):
|
||||
self._value = value
|
||||
self._expires = expires = time.time() + expires * 60
|
||||
self._expires = time.time() + expires * 60
|
||||
|
||||
def expired(self):
|
||||
return self._expires < time.time()
|
||||
|
||||
2
setup.py
2
setup.py
@@ -7,7 +7,7 @@ from setuptools import find_packages, setup
|
||||
|
||||
def get_version(filename):
|
||||
with open(filename) as fh:
|
||||
metadata = dict(re.findall("__([a-z]+)__ = \"([^']+)\"", fh.read()))
|
||||
metadata = dict(re.findall("__([a-z]+)__ = \"([^\"]+)\"", fh.read()))
|
||||
return metadata['version']
|
||||
|
||||
|
||||
|
||||
238
snapserver.conf
Normal file
238
snapserver.conf
Normal file
@@ -0,0 +1,238 @@
|
||||
###############################################################################
|
||||
# ______ #
|
||||
# / _____) #
|
||||
# ( (____ ____ _____ ____ ___ _____ ____ _ _ _____ ____ #
|
||||
# \____ \ | _ \ (____ || _ \ /___)| ___ | / ___)| | | || ___ | / ___) #
|
||||
# _____) )| | | |/ ___ || |_| ||___ || ____|| | \ V / | ____|| | #
|
||||
# (______/ |_| |_|\_____|| __/ (___/ |_____)|_| \_/ |_____)|_| #
|
||||
# |_| #
|
||||
# #
|
||||
# Snapserver config file #
|
||||
# #
|
||||
###############################################################################
|
||||
|
||||
# default values are commented
|
||||
# uncomment and edit to change them
|
||||
|
||||
# Settings can be overwritten on command line with:
|
||||
# "--<section>.<name>=<value>", e.g. --server.threads=4
|
||||
|
||||
|
||||
# General server settings #####################################################
|
||||
#
|
||||
[server]
|
||||
# Number of additional worker threads to use
|
||||
# - For values < 0 the number of threads will be 2 (on single and dual cores)
|
||||
# or 4 (for quad and more cores)
|
||||
# - 0 will utilize just the processes main thread and might cause audio drops
|
||||
# in case there are a couple of longer running tasks, such as encoding
|
||||
# multiple audio streams
|
||||
#threads = -1
|
||||
|
||||
# the pid file when running as daemon (-d or --daemon)
|
||||
#pidfile = /var/run/snapserver/pid
|
||||
|
||||
# the user to run as when daemonized (-d or --daemon)
|
||||
#user = snapserver
|
||||
# the group to run as when daemonized (-d or --daemon)
|
||||
#group = snapserver
|
||||
|
||||
# directory where persistent data is stored (server.json)
|
||||
# if empty, data dir will be
|
||||
# - "/var/lib/snapserver/" when running as daemon
|
||||
# - "$HOME/.config/snapserver/" when not running as daemon
|
||||
#datadir =
|
||||
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
|
||||
# Secure Socket Layer #########################################################
|
||||
#
|
||||
[ssl]
|
||||
# Certificate files are either specified by their full or relative path. Certificates with
|
||||
# relative path are searched for in the current path and in "/etc/snapserver/certs"
|
||||
|
||||
# Certificate file in PEM format
|
||||
#certificate =
|
||||
|
||||
# Private key file in PEM format
|
||||
#certificate_key =
|
||||
|
||||
# Password for decryption of the certificate_key (only needed for encrypted certificate_key file)
|
||||
#key_password =
|
||||
|
||||
# Verify client certificates
|
||||
#verify_clients = false
|
||||
|
||||
# List of client CA certificate files, can be configured multiple times
|
||||
#client_cert =
|
||||
#client_cert =
|
||||
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
|
||||
# HTTP RPC ####################################################################
|
||||
#
|
||||
[http]
|
||||
# enable HTTP Json RPC (HTTP POST and websockets)
|
||||
#enabled = true
|
||||
|
||||
# address to listen on, can be specified multiple times
|
||||
# use "0.0.0.0" to bind to any IPv4 address or :: to bind to any IPv6 address
|
||||
# or "127.0.0.1" or "::1" to bind to localhost IPv4 or IPv6, respectively
|
||||
# use the address of a specific network interface to just listen to and accept
|
||||
# connections from that interface
|
||||
#bind_to_address = ::
|
||||
|
||||
# which port the server should listen to
|
||||
#port = 1780
|
||||
|
||||
# enable HTTPS Json RPC (HTTPS POST and ssl websockets)
|
||||
#ssl_enabled = false
|
||||
|
||||
# same as 'bind_to_address' but for SSL
|
||||
#ssl_bind_to_address = ::
|
||||
|
||||
# same as 'port' but for SSL
|
||||
#ssl_port = 1788
|
||||
|
||||
# serve a website from the doc_root location
|
||||
# disabled if commented or empty
|
||||
doc_root = /usr/share/snapserver/snapweb
|
||||
|
||||
# Hostname or IP under which clients can reach this host
|
||||
# used to serve cached cover art
|
||||
# use <hostname> as placeholder for your actual host name
|
||||
#host = <hostname>
|
||||
|
||||
# Optional custom URL prefix for generated URLs where clients can reach
|
||||
# cached album art, to e.g. match scheme behind a reverse proxy.
|
||||
#url_prefix = https://<hostname>
|
||||
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
|
||||
# TCP RPC #####################################################################
|
||||
#
|
||||
[tcp]
|
||||
# enable TCP Json RPC
|
||||
#enabled = true
|
||||
|
||||
# address to listen on, can be specified multiple times
|
||||
# use "0.0.0.0" to bind to any IPv4 address or :: to bind to any IPv6 address
|
||||
# or "127.0.0.1" or "::1" to bind to localhost IPv4 or IPv6, respectively
|
||||
# use the address of a specific network interface to just listen to and accept
|
||||
# connections from that interface
|
||||
#bind_to_address = ::
|
||||
|
||||
# which port the server should listen to
|
||||
#port = 1705
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
|
||||
# Stream settings #############################################################
|
||||
#
|
||||
[stream]
|
||||
# address to listen on, can be specified multiple times
|
||||
# use "0.0.0.0" to bind to any IPv4 address or :: to bind to any IPv6 address
|
||||
# or "127.0.0.1" or "::1" to bind to localhost IPv4 or IPv6, respectively
|
||||
# use the address of a specific network interface to just listen to and accept
|
||||
# connections from that interface
|
||||
#bind_to_address = ::
|
||||
|
||||
# which port the server should listen to
|
||||
#port = 1704
|
||||
|
||||
# source URI of the PCM input stream, can be configured multiple times
|
||||
# The following notation is used in this paragraph:
|
||||
# <angle brackets>: the whole expression must be replaced with your specific setting
|
||||
# [square brackets]: the whole expression is optional and can be left out
|
||||
# [key=value]: if you leave this option out, "value" will be the default for "key"
|
||||
#
|
||||
# Format: TYPE://host/path?name=<name>[&codec=<codec>][&sampleformat=<sampleformat>][&chunk_ms=<chunk ms>][&controlscript=<control script filename>[&controlscriptparams=<control script command line arguments>]]
|
||||
# parameters have the form "key=value", they are concatenated with an "&" character
|
||||
# parameter "name" is mandatory for all sources, while codec, sampleformat and chunk_ms are optional
|
||||
# and will override the default codec, sampleformat or chunk_ms settings
|
||||
# Available types are:
|
||||
# pipe: pipe:///<path/to/pipe>?name=<name>[&mode=create], mode can be "create" or "read"
|
||||
# librespot: librespot:///<path/to/librespot>?name=<name>[&username=<my username>&password=<my password>][&devicename=Snapcast][&bitrate=320][&wd_timeout=7800][&volume=100][&onevent=""][&normalize=false][&autoplay=false][¶ms=<generic librepsot process arguments>]
|
||||
# note that you need to have the librespot binary on your machine
|
||||
# sampleformat will be set to "44100:16:2"
|
||||
# file: file:///<path/to/PCM/file>?name=<name>
|
||||
# process: process:///<path/to/process>?name=<name>[&wd_timeout=0][&log_stderr=false][¶ms=<process arguments>]
|
||||
# airplay: airplay:///<path/to/airplay>?name=<name>[&port=5000]
|
||||
# note that you need to have the airplay binary on your machine
|
||||
# sampleformat will be set to "44100:16:2"
|
||||
# tcp server: tcp://<listen IP, e.g. 127.0.0.1>:<port>?name=<name>[&mode=server]
|
||||
# tcp client: tcp://<server IP, e.g. 127.0.0.1>:<port>?name=<name>&mode=client
|
||||
# alsa: alsa:///?name=<name>&device=<alsa device>[&send_silence=false][&idle_threshold=100][&silence_threshold_percent=0.0]
|
||||
# meta: meta:///<name of source#1>/<name of source#2>/.../<name of source#N>?name=<name>
|
||||
|
||||
|
||||
|
||||
#source = pipe:///tmp/snapfifo?name=default
|
||||
source = meta:///Announcements/default?name=MainAudio
|
||||
source = tcp://0.0.0.0:4953?name=Announcements&sampleformat=48000:16:2&codec=pcm
|
||||
source = pipe:///run/snapserver/snapfifo?name=default
|
||||
#source = spotify:///librespot?name=Spotify&username=guzzbinkles&password=Finn05092013&devicename=Snapcast&bitrate=320
|
||||
|
||||
|
||||
|
||||
#announcements
|
||||
#source = tcp://0.0.0.0:4953?name=Announcements&sampleformat=22050:16:1&codec=pcm
|
||||
#source = spotify:///librespot?name=Spotify&devicename=Snapcast&bitrate=320
|
||||
|
||||
|
||||
# Plugin directory, containing scripts, referred by "controlscript"
|
||||
#plugin_dir = /usr/share/snapserver/plug-ins
|
||||
|
||||
# Default sample format: <sample rate>:<bits per sample>:<channels>
|
||||
#sampleformat = 48000:16:2
|
||||
|
||||
# Default transport codec
|
||||
# (flac|ogg|opus|pcm)[:options]
|
||||
# Start Snapserver with "--stream:codec=<codec>:?" to get codec specific options
|
||||
#codec = flac
|
||||
|
||||
# Default source stream read chunk size [ms].
|
||||
# The server will continously read this number of milliseconds from the source into buffer and pass this buffer to the encoder.
|
||||
# The encoded buffer is sent to the clients. Some codecs have a higher latency and will need more data, e.g. Flac will need ~26ms chunks
|
||||
#chunk_ms = 20
|
||||
|
||||
# Buffer [ms]
|
||||
# The end-to-end latency, from capturing a sample on the server until the sample is played-out on the client
|
||||
#buffer = 1000
|
||||
|
||||
# Send audio to muted clients
|
||||
#send_to_muted = false
|
||||
#
|
||||
|
||||
|
||||
# Streaming client options ####################################################
|
||||
#
|
||||
[streaming_client]
|
||||
|
||||
# Volume assigned to new snapclients [percent]
|
||||
# Defaults to 100 if unset
|
||||
#initial_volume = 100
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
|
||||
# Logging options #############################################################
|
||||
#
|
||||
[logging]
|
||||
|
||||
# log sink [null,system,stdout,stderr,file:<filename>]
|
||||
# when left empty: if running as daemon "system" else "stdout"
|
||||
#sink =
|
||||
|
||||
# log filter <tag>:<level>[,<tag>:<level>]*
|
||||
# with tag = * or <log tag> and level = [trace,debug,info,notice,warning,error,fatal]
|
||||
#filter = *:info
|
||||
#
|
||||
###############################################################################
|
||||
@@ -6,19 +6,21 @@ from mopidy_radionet import backend
|
||||
from mopidy_radionet.radionet import RadioNetClient
|
||||
from mopidy_radionet.library import RadioNetLibraryProvider
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def backend_mock():
|
||||
backend_mock = mock.Mock(spec=backend.RadioNetBackend)
|
||||
backend_mock.radionet = RadioNetClient(proxy_config=None)
|
||||
backend_mock.library = RadioNetLibraryProvider(backend=backend_mock)
|
||||
backend_mock.radionet.set_apikey('test')
|
||||
backend_mock.radionet.set_favorites({'lush'})
|
||||
return backend_mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def library(backend_mock):
|
||||
return backend_mock.library
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def radionet(backend_mock):
|
||||
return backend_mock.radionet
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
from unittest import mock
|
||||
|
||||
|
||||
def test_browse_root(library):
|
||||
results = library.browse('radionet:root');
|
||||
assert 8 == len(results)
|
||||
results = library.browse('radionet:root')
|
||||
assert 7 == len(results)
|
||||
|
||||
|
||||
def test_browse_localstations(library):
|
||||
results = library.browse('radionet:localstations');
|
||||
results = library.browse('radionet:local')
|
||||
assert len(results) > 0
|
||||
|
||||
page_uri = results[0].uri if results is not None else None
|
||||
@@ -18,28 +15,23 @@ def test_browse_localstations(library):
|
||||
# assert len(results) > 0
|
||||
|
||||
|
||||
def test_browse_topstations(library):
|
||||
results = library.browse('radionet:topstations');
|
||||
assert len(results) > 0
|
||||
|
||||
|
||||
def test_browse_genres(library):
|
||||
results = library.browse('radionet:genres');
|
||||
assert len(results) > 0
|
||||
genres = library.browse('radionet:genres')
|
||||
assert len(genres) > 0
|
||||
|
||||
cat_uri = results[0].uri if results is not None else None
|
||||
cat_uri = genres[0].uri if genres is not None else None
|
||||
assert cat_uri is not None
|
||||
|
||||
results = library.browse(cat_uri)
|
||||
assert len(results) == 2
|
||||
pages = library.browse(cat_uri)
|
||||
assert len(pages) == 7
|
||||
|
||||
sort_uri = results[0].uri if results is not None else None
|
||||
assert sort_uri is not None
|
||||
page_uri = pages[0].uri if pages is not None else None
|
||||
assert page_uri is not None
|
||||
|
||||
results = library.browse(sort_uri)
|
||||
results = library.browse(page_uri)
|
||||
assert len(results) > 0
|
||||
|
||||
page_uri = results[0].uri if results is not None else None
|
||||
page_uri = pages[len(pages) - 1].uri if results is not None else None
|
||||
assert page_uri is not None
|
||||
|
||||
results = library.browse(page_uri)
|
||||
@@ -47,20 +39,14 @@ def test_browse_genres(library):
|
||||
|
||||
|
||||
def test_browse_topics(library):
|
||||
results = library.browse('radionet:topics');
|
||||
assert len(results) > 0
|
||||
results = library.browse('radionet:topics')
|
||||
assert len(results) > 2
|
||||
|
||||
cat_uri = results[0].uri if results is not None else None
|
||||
cat_uri = results[2].uri if results is not None else None
|
||||
assert cat_uri is not None
|
||||
|
||||
results = library.browse(cat_uri)
|
||||
assert len(results) == 2
|
||||
|
||||
sort_uri = results[0].uri if results is not None else None
|
||||
assert sort_uri is not None
|
||||
|
||||
results = library.browse(sort_uri)
|
||||
assert len(results) > 0
|
||||
assert len(results) > 2
|
||||
|
||||
page_uri = results[0].uri if results is not None else None
|
||||
assert page_uri is not None
|
||||
@@ -71,20 +57,14 @@ def test_browse_topics(library):
|
||||
|
||||
|
||||
def test_browse_languages(library):
|
||||
results = library.browse('radionet:languages');
|
||||
results = library.browse('radionet:languages')
|
||||
assert len(results) > 0
|
||||
|
||||
cat_uri = results[5].uri if results is not None else None
|
||||
assert cat_uri is not None
|
||||
|
||||
results = library.browse(cat_uri)
|
||||
assert len(results) == 2
|
||||
|
||||
sort_uri = results[0].uri if results is not None else None
|
||||
assert sort_uri is not None
|
||||
|
||||
results = library.browse(sort_uri)
|
||||
assert len(results) > 0
|
||||
assert len(results) == 11
|
||||
|
||||
page_uri = results[0].uri if results is not None else None
|
||||
assert page_uri is not None
|
||||
@@ -95,7 +75,7 @@ def test_browse_languages(library):
|
||||
|
||||
|
||||
def test_browse_cities(library):
|
||||
results = library.browse('radionet:cities');
|
||||
results = library.browse('radionet:cities')
|
||||
assert len(results) > 0
|
||||
|
||||
cat_uri = results[0].uri if results is not None else None
|
||||
@@ -104,12 +84,6 @@ def test_browse_cities(library):
|
||||
results = library.browse(cat_uri)
|
||||
assert len(results) == 2
|
||||
|
||||
sort_uri = results[0].uri if results is not None else None
|
||||
assert sort_uri is not None
|
||||
|
||||
results = library.browse(sort_uri)
|
||||
assert len(results) > 0
|
||||
|
||||
page_uri = results[0].uri if results is not None else None
|
||||
assert page_uri is not None
|
||||
|
||||
@@ -119,20 +93,14 @@ def test_browse_cities(library):
|
||||
|
||||
|
||||
def test_browse_countries(library):
|
||||
results = library.browse('radionet:countries');
|
||||
results = library.browse('radionet:countries')
|
||||
assert len(results) > 0
|
||||
|
||||
cat_uri = results[0].uri if results is not None else None
|
||||
assert cat_uri is not None
|
||||
|
||||
results = library.browse(cat_uri)
|
||||
assert len(results) == 2
|
||||
|
||||
sort_uri = results[0].uri if results is not None else None
|
||||
assert sort_uri is not None
|
||||
|
||||
results = library.browse(sort_uri)
|
||||
assert len(results) > 0
|
||||
assert len(results) == 4
|
||||
|
||||
page_uri = results[0].uri if results is not None else None
|
||||
assert page_uri is not None
|
||||
@@ -143,27 +111,31 @@ def test_browse_countries(library):
|
||||
|
||||
|
||||
def test_browse_favorites(library):
|
||||
results = library.browse('radionet:favorites');
|
||||
results = library.browse('radionet:favorites')
|
||||
assert 1 == len(results)
|
||||
|
||||
|
||||
|
||||
def test_search(library):
|
||||
result = library.search({'any': ['radio ram']})
|
||||
result = library.search({'any': ['katze']})
|
||||
|
||||
assert len(result.tracks) > 0
|
||||
|
||||
old_length = len(result.tracks)
|
||||
|
||||
result = library.search({'any': ['radio ram']})
|
||||
result = library.search({'any': ['katze']})
|
||||
|
||||
assert len(result.tracks) == old_length
|
||||
|
||||
|
||||
def test_lookup(library):
|
||||
|
||||
results = library.browse('radionet:favorites');
|
||||
results = library.browse('radionet:favorites')
|
||||
assert 1 == len(results)
|
||||
|
||||
for result in results:
|
||||
assert library.lookup(result.uri) is not None
|
||||
|
||||
|
||||
def test_track_by_slug(library):
|
||||
results = library.lookup('radionet:track:dancefm')
|
||||
assert 1 == len(results)
|
||||
assert results[0].uri == 'radionet:track:dancefm'
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
from unittest import mock
|
||||
|
||||
|
||||
def test_get_genres(radionet):
|
||||
genres = radionet.get_genres();
|
||||
genres = radionet.get_genres()
|
||||
assert len(genres) > 0
|
||||
|
||||
|
||||
def test_get_top_stations(radionet):
|
||||
result = radionet.get_category('topstations', 1)
|
||||
assert len(result) > 0
|
||||
|
||||
|
||||
def test_get_local_stations(radionet):
|
||||
result = radionet.get_category('localstations', 1)
|
||||
result = radionet.get_simple_category('local', 1)
|
||||
assert len(result) > 0
|
||||
|
||||
|
||||
def test_do_search(radionet):
|
||||
result = radionet.do_search("radio ram")
|
||||
result = radionet.do_search("\"radio ram\"")
|
||||
assert len(result) > 0
|
||||
|
||||
assert result[0].stream_url is None
|
||||
assert result[0].stream_url is not None
|
||||
|
||||
assert radionet.get_stream_url(result[0].id) is not None
|
||||
|
||||
|
||||
def test_get_favorites(radionet):
|
||||
test_favorites = ("Rock Antenne", "radio ram", "eska", "dancefm")
|
||||
radionet.cache = {}
|
||||
test_favorites = ["rockantenne", "eska", "dancefm"]
|
||||
radionet.set_favorites(test_favorites)
|
||||
result = radionet.get_favorites()
|
||||
assert len(result) == len(test_favorites)
|
||||
|
||||
assert result[2].name == 'Eska'
|
||||
assert result[1].name == 'Eska'
|
||||
|
||||
|
||||
def test_favorites_broken_slug(radionet):
|
||||
radionet.cache = {}
|
||||
test_favorites = ["radio 357"]
|
||||
radionet.set_favorites(test_favorites)
|
||||
result = radionet.get_favorites()
|
||||
assert len(result) == 1
|
||||
|
||||
Reference in New Issue
Block a user