import asyncio
import os
from utils import run_periodic, LOGGER, get_sort_name, try_parse_int
-from models import MediaType, Artist, Album, Track, Playlist
+from models import MediaType, Artist, Album, Track, Playlist, Radio
from typing import List
import aiosqlite
import operator
await db.execute('CREATE TABLE IF NOT EXISTS playlists(playlist_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, owner TEXT NOT NULL, is_editable BOOLEAN NOT NULL, UNIQUE(name, owner));')
await db.execute('CREATE TABLE IF NOT EXISTS playlist_tracks(playlist_id INTEGER NOT NULL, track_id INTEGER NOT NULL, position INTEGER, UNIQUE(playlist_id, track_id));')
+ await db.execute('CREATE TABLE IF NOT EXISTS radios(radio_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE);')
+
await db.commit()
await db.execute('VACUUM;')
self.db_ready = True
playlists.append(playlist)
return playlists
+ async def radios(self, filter_query=None, provider=None, limit=100000, offset=0, orderby='name') -> List[Radio]:
+ ''' fetch all radio records from table'''
+ items = []
+ sql_query = 'SELECT * FROM radios'
+ if filter_query:
+ sql_query += filter_query
+ elif provider != None:
+ sql_query += ' WHERE radio_id in (SELECT item_id FROM provider_mappings WHERE provider = "%s" AND media_type = %d)' % (provider,MediaType.Radio)
+ sql_query += ' ORDER BY %s' % orderby
+ if limit:
+ sql_query += ' LIMIT %d OFFSET %d' %(limit, offset)
+ async with aiosqlite.connect(self.dbfile) as db:
+ async with db.execute(sql_query) as cursor:
+ db_rows = await cursor.fetchall()
+ for db_row in db_rows:
+ radio = Radio()
+ radio.item_id = db_row[0]
+ radio.name = db_row[1]
+ radio.metadata = await self.__get_metadata(radio.item_id, MediaType.Radio, db)
+ radio.provider_ids = await self.__get_prov_ids(radio.item_id, MediaType.Radio, db)
+ radio.in_library = await self.__get_library_providers(radio.item_id, MediaType.Radio, db)
+ items.append(radio)
+ return items
+
async def playlist(self, playlist_id:int) -> Playlist:
''' get playlist record by id '''
playlist_id = try_parse_int(playlist_id)
return None
return playlists[0]
+ async def radio(self, radio_id:int) -> Playlist:
+ ''' get radio record by id '''
+ radio_id = try_parse_int(radio_id)
+ radios = await self.radios(' WHERE radio_id = %s' % radio_id)
+ if not radios:
+ return None
+ return radios[0]
+
async def add_playlist(self, playlist:Playlist):
''' add a new playlist record into table'''
assert(playlist.name)
await db.commit()
return playlist_id
+ async def add_radio(self, radio:Radio):
+ ''' add a new radio record into table'''
+ assert(radio.name)
+ async with aiosqlite.connect(self.dbfile, timeout=20) as db:
+ async with db.execute('SELECT (radio_id) FROM radios WHERE name=?;', (radio.name,)) as cursor:
+ result = await cursor.fetchone()
+ if result:
+ radio_id = result[0]
+ else:
+ # insert radio
+ sql_query = 'INSERT OR REPLACE INTO radios (name) VALUES(?);'
+ await db.execute(sql_query, (radio.name,))
+ # get id from newly created item (the safe way)
+ async with db.execute('SELECT (radio_id) FROM radios WHERE name=?;', (radio.name,)) as cursor:
+ radio_id = await cursor.fetchone()
+ radio_id = radio_id[0]
+ LOGGER.info('added radio station %s to database: %s' %(radio.name, radio_id))
+ # add/update metadata
+ await self.__add_prov_ids(radio_id, MediaType.Radio, radio.provider_ids, db)
+ await self.__add_metadata(radio_id, MediaType.Radio, radio.metadata, db)
+ # save
+ await db.commit()
+ return radio_id
+
async def add_to_library(self, item_id:int, media_type:MediaType, provider:str):
''' add an item to the library (item must already be present in the db!) '''
item_id = try_parse_int(item_id)
Album = 2
Track = 3
Playlist = 4
+ Radio = 5
def media_type_from_string(media_type_str):
media_type_str = media_type_str.lower()
return MediaType.Track
elif 'playlist' in media_type_str or media_type_str == '4':
return MediaType.Playlist
+ elif 'radio' in media_type_str or media_type_str == '5':
+ return MediaType.Radio
else:
return None
self.in_library = []
self.is_editable = False
+class Radio(Track):
+ ''' representation of a radio station '''
+ def __init__(self):
+ super().__init__()
+ self.item_id = None
+ self.provider = 'database'
+ self.name = ''
+ self.provider_ids = []
+ self.metadata = {}
+ self.media_type = MediaType.Radio
+ self.in_library = []
+ self.is_editable = False
+ self.duration = 0
+
class MusicProvider():
'''
Model for a Musicprovider
else:
return await self.get_playlist(prov_playlist_id)
+ async def radio(self, prov_radio_id) -> Radio:
+ ''' return radio details for the given provider playlist id '''
+ db_id = await self.mass.db.get_database_id(self.prov_id, prov_radio_id, MediaType.Radio)
+ if db_id:
+ # synced radio, return database details
+ return await self.mass.db.radio(db_id)
+ else:
+ return await self.get_radio(prov_radio_id)
+
async def album_tracks(self, prov_album_id) -> List[Track]:
''' return album tracks for the given provider album id'''
items = []
''' retrieve library/subscribed playlists from the provider '''
raise NotImplementedError
+ async def get_radios(self) -> List[Radio]:
+ ''' retrieve library/subscribed radio stations from the provider '''
+ raise NotImplementedError
+
async def get_artist(self, prov_item_id) -> Artist:
''' get full artist details by id '''
raise NotImplementedError
''' get full playlist details by id '''
raise NotImplementedError
+ async def get_radio(self, prov_item_id) -> Radio:
+ ''' get full radio details by id '''
+ raise NotImplementedError
+
async def get_album_tracks(self, prov_item_id, limit=100, offset=0) -> List[Track]:
''' get album tracks for given album id '''
raise NotImplementedError
import asyncio
import os
from utils import LOGGER, try_parse_int, get_ip, run_async_background_task, run_periodic, get_folder_size
-from models import TrackQuality
+from models import TrackQuality, MediaType
import shutil
import xml.etree.ElementTree as ET
import random
import base64
+import operator
+from aiohttp import web
+import threading
AUDIO_TEMP_DIR = "/tmp/audio_tmp"
AUDIO_CACHE_DIR = "/tmp/audio_cache"
if not key in self.mass.config['base']['http_streamer']:
self.mass.config['base']['http_streamer'][key] = def_value
- async def get_audio_stream(self, audioqueue, track_id, provider, player_id=None):
+ async def stream_track(self, http_request):
+ ''' start streaming track from provider '''
+ player_id = http_request.query.get('player_id')
+ track_id = http_request.query.get('track_id')
+ provider = http_request.query.get('provider')
+ resp = web.StreamResponse(status=200,
+ reason='OK',
+ headers={'Content-Type': 'audio/flac'})
+ await resp.prepare(http_request)
+ if http_request.method.upper() != 'HEAD':
+ # stream audio
+ queue = asyncio.Queue()
+ cancelled = threading.Event()
+ task = run_async_background_task(
+ self.mass.bg_executor,
+ self.__get_audio_stream, queue, track_id, provider, player_id, cancelled)
+ try:
+ while True:
+ chunk = await queue.get()
+ if not chunk:
+ queue.task_done()
+ break
+ await resp.write(chunk)
+ queue.task_done()
+ LOGGER.info("Finished streaming %s" % track_id)
+ except asyncio.CancelledError:
+ cancelled.set()
+ LOGGER.info("Streaming interrupted for %s" % track_id)
+ raise asyncio.CancelledError()
+ return resp
+
+ async def stream_radio(self, http_request):
+ ''' start streaming radio from provider '''
+ player_id = http_request.query.get('player_id')
+ radio_id = http_request.query.get('radio_id')
+ provider = http_request.query.get('provider')
+ resp = web.StreamResponse(status=200,
+ reason='OK',
+ headers={'Content-Type': 'audio/flac'})
+ await resp.prepare(http_request)
+ if http_request.method.upper() != 'HEAD':
+ # stream audio with sox
+ sox_effects = await self.__get_player_sox_options(radio_id, provider, player_id, True)
+ media_item = await self.mass.music.item(radio_id, MediaType.Radio, provider)
+ stream = sorted(media_item.provider_ids, key=operator.itemgetter('quality'), reverse=True)[0]
+ stream_url = stream["details"]
+ if stream["quality"] == TrackQuality.LOSSY_AAC:
+ input_content_type = "aac"
+ elif stream["quality"] == TrackQuality.LOSSY_OGG:
+ input_content_type = "ogg"
+ else:
+ input_content_type = "mp3"
+ if input_content_type == "aac":
+ args = 'ffmpeg -i "%s" -f flac - | sox -t flac -t flac -C 0 - %s' % (stream_url, sox_effects)
+ else:
+ args = 'sox -t %s "%s" -t flac -C 0 - %s' % (input_content_type, stream_url, sox_effects)
+ LOGGER.info("Running sox with args: %s" % args)
+ process = await asyncio.create_subprocess_shell(args, stdout=asyncio.subprocess.PIPE)
+ try:
+ while not process.stdout.at_eof():
+ chunk = await process.stdout.read(128000)
+ if not chunk:
+ break
+ await resp.write(chunk)
+ await process.wait()
+ LOGGER.info("streaming of radio_id %s completed" % radio_id)
+ except asyncio.CancelledError:
+ process.terminate()
+ await process.wait()
+ LOGGER.info("streaming of radio_id %s interrupted" % radio_id)
+ raise asyncio.CancelledError()
+ return resp
+
+ async def __get_audio_stream(self, audioqueue, track_id, provider, player_id=None, cancelled=None):
''' get audio stream from provider and apply additional effects/processing where/if needed'''
- input_content_type = await self.mass.music.providers[provider].get_stream_content_type(track_id)
cachefile = self.__get_track_cache_filename(track_id, provider)
- sox_effects = ''
- # sox settings
- if self.mass.config['base']['http_streamer']['volume_normalisation']:
- gain_correct = await self.__get_track_gain_correct(track_id, provider)
- LOGGER.info("apply gain correction of %s" % gain_correct)
- sox_effects += ' vol %s dB ' % gain_correct
- sox_effects += await self.__get_player_sox_options(track_id, provider, player_id)
+ sox_effects = await self.__get_player_sox_options(track_id, provider, player_id, False)
if os.path.isfile(cachefile):
# we have a cache file for this track which we can use
args = 'sox -t flac %s -t flac -C 0 - %s' % (cachefile, sox_effects)
buffer_task = None
else:
# stream from provider
+ input_content_type = await self.mass.music.providers[provider].get_stream_content_type(track_id)
+ assert(input_content_type)
args = 'sox -t %s - -t flac -C 0 - %s' % (input_content_type, sox_effects)
LOGGER.info("Running sox with args: %s" % args)
process = await asyncio.create_subprocess_shell(args,
chunk = await process.stdout.read(256000)
if not chunk:
break
- await audioqueue.put(chunk)
- if audioqueue.qsize() > 10:
- await asyncio.sleep(0.1) # cooldown a bit
+ if not cancelled.is_set():
+ await audioqueue.put(chunk)
+ if audioqueue.qsize() > 10:
+ await asyncio.sleep(0.1) # cooldown a bit
await process.wait()
await audioqueue.put('') # indicate EOF
- LOGGER.info("streaming of track_id %s completed" % track_id)
+ if cancelled.is_set():
+ LOGGER.info("streaming of track_id %s interrupted" % track_id)
+ else:
+ LOGGER.info("streaming of track_id %s completed" % track_id)
- async def __get_player_sox_options(self, track_id, provider, player_id):
+ async def __get_player_sox_options(self, track_id, provider, player_id, is_radio):
''' get player specific sox options '''
- sox_effects = ' '
- if not player_id:
- return ''
- if self.mass.config['player_settings'][player_id]['max_sample_rate']:
+ sox_effects = ''
+ if player_id and not is_radio and self.mass.config['player_settings'][player_id]['max_sample_rate']:
# downsample if needed
max_sample_rate = try_parse_int(self.mass.config['player_settings'][player_id]['max_sample_rate'])
if max_sample_rate:
sox_effects += 'rate -v 96000'
elif quality > TrackQuality.FLAC_LOSSLESS_HI_RES_1 and max_sample_rate == 48000:
sox_effects += 'rate -v 48000'
- if self.mass.config['player_settings'][player_id]['sox_effects']:
- sox_effects += self.mass.config['player_settings'][player_id]['sox_effects']
- return sox_effects + ' '
+ if player_id and self.mass.config['player_settings'][player_id]['sox_effects']:
+ sox_effects += ' ' + self.mass.config['player_settings'][player_id]['sox_effects']
+ if self.mass.config['base']['http_streamer']['volume_normalisation']:
+ gain_correct = await self.__get_track_gain_correct(track_id, provider)
+ sox_effects += ' vol %s dB ' % gain_correct
+ return sox_effects
async def __analyze_audio(self, tmpfile, track_id, provider, content_type):
''' analyze track audio, for now we only calculate EBU R128 loudness '''
import asyncio
import os
-from utils import run_periodic, run_async_background_task, LOGGER, try_parse_int
+from utils import run_periodic, run_async_background_task, LOGGER, try_parse_int, try_supported
import aiohttp
from difflib import SequenceMatcher as Matcher
-from models import MediaType, Track, Artist, Album, Playlist
+from models import MediaType, Track, Artist, Album, Playlist, Radio
from typing import List
import toolz
import operator
return await self.track(item_id, provider, lazy=lazy)
elif media_type == MediaType.Playlist:
return await self.playlist(item_id, provider)
+ elif media_type == MediaType.Radio:
+ return await self.radio(item_id, provider)
else:
return None
''' return all library playlists, optionally filtered by provider '''
return await self.mass.db.playlists(provider=provider_filter, limit=limit, offset=offset, orderby=orderby)
+ async def radios(self, limit=0, offset=0, orderby='name', provider_filter=None) -> List[Playlist]:
+ ''' return all library radios, optionally filtered by provider '''
+ return await self.mass.db.radios(provider=provider_filter, limit=limit, offset=offset, orderby=orderby)
+
async def library_items(self, media_type:MediaType, limit=0, offset=0, orderby='name', provider_filter=None) -> List[object]:
''' get multiple music items in library'''
if media_type == MediaType.Artist:
return await self.library_tracks(limit=limit, offset=offset, orderby=orderby, provider_filter=provider_filter)
elif media_type == MediaType.Playlist:
return await self.playlists(limit=limit, offset=offset, orderby=orderby, provider_filter=provider_filter)
+ elif media_type == MediaType.Radio:
+ return await self.radios(limit=limit, offset=offset, orderby=orderby, provider_filter=provider_filter)
async def artist(self, item_id, provider='database', lazy=True) -> Artist:
''' get artist by id '''
return await self.mass.db.playlist(item_id)
return await self.providers[provider].playlist(item_id)
+ async def radio(self, item_id, provider='database') -> Radio:
+ ''' get radio by id '''
+ if not provider or provider == 'database':
+ return await self.mass.db.radio(item_id)
+ return await self.providers[provider].radio(item_id)
+
async def playlist_by_name(self, name) -> Playlist:
''' get playlist by name '''
for playlist in await self.playlists():
if playlist.name == name:
return playlist
return None
+
+ async def radio_by_name(self, name) -> Radio:
+ ''' get radio by name '''
+ for radio in await self.radios():
+ if radio.name == name:
+ return radio
+ return None
async def artist_toptracks(self, artist_id, provider='database') -> List[Track]:
''' get top tracks for given artist '''
self.sync_running = True
for prov_id in self.providers.keys():
# sync library artists
- await self.sync_library_artists(prov_id)
- await self.sync_library_albums(prov_id)
- await self.sync_library_tracks(prov_id)
- await self.sync_playlists(prov_id)
+ await try_supported(self.sync_library_artists(prov_id))
+ await try_supported(self.sync_library_albums(prov_id))
+ await try_supported(self.sync_library_tracks(prov_id))
+ await try_supported(self.sync_playlists(prov_id))
+ await try_supported(self.sync_radios(prov_id))
self.sync_running = False
async def sync_library_artists(self, prov_id):
await self.mass.db.remove_playlist_track(db_playlist_id, db_id)
LOGGER.info("Finished syncing Playlist %s tracks for provider %s" % (prov_playlist_id, prov_id))
+ async def sync_radios(self, prov_id):
+ ''' sync library radios for given provider'''
+ music_provider = self.providers[prov_id]
+ prev_items = await self.radios(provider_filter=prov_id)
+ prev_db_ids = [item.item_id for item in prev_items]
+ cur_items = await music_provider.get_radios()
+ cur_db_ids = []
+ for item in cur_items:
+ db_id = await self.mass.db.get_database_id(prov_id, item.item_id, MediaType.Radio)
+ if not db_id:
+ db_id = await self.mass.db.add_radio(item)
+ cur_db_ids.append(db_id)
+ if not db_id in prev_db_ids:
+ await self.mass.db.add_to_library(db_id, MediaType.Radio, prov_id)
+ # process deletions
+ for db_id in prev_db_ids:
+ if db_id not in cur_db_ids:
+ await self.mass.db.remove_from_library(db_id, MediaType.Radio, prov_id)
+ LOGGER.info("Finished syncing Radios for provider %s" % prov_id)
+
def load_music_providers(self):
''' dynamically load musicproviders '''
for item in os.listdir(MODULES_PATH):
--- /dev/null
+#!/usr/bin/env python3
+# -*- coding:utf-8 -*-
+
+import asyncio
+import os
+from typing import List
+import sys
+import time
+from utils import run_periodic, LOGGER, parse_track_title
+from models import MusicProvider, MediaType, TrackQuality, Radio
+from constants import CONF_USERNAME, CONF_PASSWORD, CONF_ENABLED
+from asyncio_throttle import Throttler
+import json
+import aiohttp
+from modules.cache import use_cache
+import concurrent
+
+def setup(mass):
+ ''' setup the provider'''
+ enabled = mass.config["musicproviders"]['tunein'].get(CONF_ENABLED)
+ username = mass.config["musicproviders"]['tunein'].get(CONF_USERNAME)
+ password = mass.config["musicproviders"]['tunein'].get(CONF_PASSWORD)
+ if enabled and username and password:
+ provider = TuneInProvider(mass, username, password)
+ return provider
+ return False
+
+def config_entries():
+ ''' get the config entries for this provider (list with key/value pairs)'''
+ return [
+ (CONF_ENABLED, False, CONF_ENABLED),
+ (CONF_USERNAME, "", CONF_USERNAME),
+ (CONF_PASSWORD, "<password>", CONF_PASSWORD)
+ ]
+
+class TuneInProvider(MusicProvider):
+
+
+ def __init__(self, mass, username, password):
+ self.name = 'TuneIn Radio'
+ self.prov_id = 'tunein'
+ self.mass = mass
+ self.cache = mass.cache
+ self.http_session = aiohttp.ClientSession(loop=mass.event_loop, connector=aiohttp.TCPConnector(verify_ssl=False))
+ self.throttler = Throttler(rate_limit=1, period=1)
+ self._username = username
+ self._password = password
+
+ async def search(self, searchstring, media_types=List[MediaType], limit=5):
+ ''' perform search on the provider '''
+ result = {
+ "artists": [],
+ "albums": [],
+ "tracks": [],
+ "playlists": [],
+ "radios": []
+ }
+ return result
+
+ async def get_radios(self):
+ ''' get favorited/library radio stations '''
+ items = []
+ params = {"c": "presets"}
+ result = await self.__get_data("Browse.ashx", params, ignore_cache=True)
+ if result and "body" in result:
+ for item in result["body"]:
+ # TODO: expand folders
+ if item["type"] == "audio":
+ radio = await self.__parse_radio(item)
+ items.append(radio)
+ return items
+
+ async def get_radio(self, radio_id):
+ ''' get radio station details '''
+ radio = None
+ params = {"c": "composite", "detail": "listing", "id": radio_id}
+ result = await self.__get_data("Describe.ashx", params, ignore_cache=True)
+ if result and result.get("body") and result["body"][0].get("children"):
+ item = result["body"][0]["children"][0]
+ radio = await self.__parse_radio(item)
+ return radio
+
+ async def __parse_radio(self, details):
+ ''' parse Radio object from json obj returned from api '''
+ radio = Radio()
+ radio.item_id = details['preset_id']
+ radio.provider = self.prov_id
+ if "name" in details:
+ radio.name = details["name"]
+ else:
+ # parse name from text attr
+ name = details["text"]
+ if " | " in name:
+ name = name.split(" | ")[1]
+ name = name.split(" (")[0]
+ radio.name = name
+ # parse stream urls and format
+ stream_info = await self.__get_stream_urls(radio.item_id)
+ for stream in stream_info["body"]:
+ if stream["media_type"] == 'aac':
+ quality = TrackQuality.LOSSY_AAC
+ elif stream["media_type"] == 'ogg':
+ quality = TrackQuality.LOSSY_OGG
+ else:
+ quality = TrackQuality.LOSSY_MP3
+ radio.provider_ids.append({
+ "provider": self.prov_id,
+ "item_id": details['preset_id'],
+ "quality": quality,
+ "details": stream['url']
+ })
+ # image
+ if "image" in details:
+ radio.metadata["image"] = details["image"]
+ elif "logo" in details:
+ radio.metadata["image"] = details["logo"]
+ return radio
+
+ async def __get_stream_urls(self, radio_id):
+ ''' get the stream urls for the given radio id '''
+ params = {"id": radio_id}
+ res = await self.__get_data("Tune.ashx", params)
+ return res
+
+ # async def get_stream_content_type(self, radio_id):
+ # ''' return the content type for the given radio when it will be streamed'''
+ # return 'flac' #TODO handle other file formats on qobuz?
+
+ # async def get_audio_stream(self, track_id):
+ # ''' get audio stream for a track '''
+ # params = {'format_id': 27, 'track_id': track_id, 'intent': 'stream'}
+ # # we are called from other thread
+ # streamdetails_future = asyncio.run_coroutine_threadsafe(
+ # self.__get_data('track/getFileUrl', params, sign_request=True, ignore_cache=True),
+ # self.mass.event_loop
+
+ @use_cache(7)
+ async def __get_data(self, endpoint, params={}, ignore_cache=False, cache_checksum=None):
+ ''' get data from api'''
+ url = 'https://opml.radiotime.com/%s' % endpoint
+ params['render'] = 'json'
+ params['formats'] = 'ogg,aac,wma,mp3'
+ params['username'] = self._username
+ params['partnerId'] = '1'
+ async with self.throttler:
+ async with self.http_session.get(url, params=params) as response:
+ result = await response.json()
+ if not result or 'error' in result:
+ LOGGER.error(url)
+ LOGGER.error(params)
+ result = None
+ return result
+
+
\ No newline at end of file
("disable_volume", False, "player_disable_vol"),
("sox_effects", '', "http_streamer_sox_effects"),
("max_sample_rate", '96000', "max_sample_rate"),
- ("force_http_streamer", False, "force_http_streamer"),
- ("group_parent", '<player>', "group_parent")
+ ("force_http_streamer", False, "force_http_streamer")
]
if player_details.is_group:
config_entries += [ # group player settings
'''
play media on a player
player_id: id of the player
- media_item: media item(s) that should be played (Track, Album, Artist, Playlist)
+ media_item: media item(s) that should be played (Track, Album, Artist, Playlist, Radio)
queue_opt: play, replace, next or add
'''
if not player_id in self._players:
for track in tracks:
# sort by quality
match_found = False
+ is_radio = track.media_type == MediaType.Radio
for prov_media in sorted(track.provider_ids, key=operator.itemgetter('quality'), reverse=True):
media_provider = prov_media['provider']
media_item_id = prov_media['item_id']
player_supported_provs = player_prov.supported_musicproviders
if media_provider in player_supported_provs and not self.mass.config['player_settings'][player_id]['force_http_streamer']:
# the provider can handle this media_type directly !
- track.uri = await self.get_track_uri(media_item_id, media_provider, player_id)
+ track.uri = await self.get_track_uri(media_item_id, media_provider, player_id, is_radio=is_radio)
playable_tracks.append(track)
match_found = True
elif 'http' in player_prov.supported_musicproviders:
# fallback to http streaming if supported
- track.uri = await self.get_track_uri(media_item_id, media_provider, player_id, True)
+ track.uri = await self.get_track_uri(media_item_id, media_provider, player_id, True, is_radio=is_radio)
playable_tracks.append(track)
match_found = True
if match_found:
else:
raise Exception("Musicprovider and/or media not supported by player %s !" % (player_id) )
- async def get_track_uri(self, item_id, provider, player_id, http_stream=False):
+ async def get_track_uri(self, item_id, provider, player_id, http_stream=False, is_radio=False):
''' generate the URL/URI for a media item '''
uri = ""
if http_stream:
- params = {"provider": provider, "track_id": str(item_id), "player_id": str(player_id)}
- params_str = urllib.parse.urlencode(params)
- uri = 'http://%s:%s/stream?%s'% (self.local_ip, self.mass.config['base']['web']['http_port'], params_str)
+ if is_radio:
+ params = {"provider": provider, "radio_id": str(item_id), "player_id": str(player_id)}
+ params_str = urllib.parse.urlencode(params)
+ uri = 'http://%s:%s/stream_radio?%s'% (self.local_ip, self.mass.config['base']['web']['http_port'], params_str)
+ else:
+ params = {"provider": provider, "track_id": str(item_id), "player_id": str(player_id)}
+ params_str = urllib.parse.urlencode(params)
+ uri = 'http://%s:%s/stream_track?%s'% (self.local_ip, self.mass.config['base']['web']['http_port'], params_str)
elif provider == "spotify":
uri = 'spotify://spotify:track:%s' % item_id
elif provider == "qobuz":
uri = 'qobuz://%s.flac' % item_id
elif provider == "file":
uri = item_id
+ else:
+ uri = "%s://%s" %(provider, item_id)
return uri
async def player_queue(self, player_id, offset=0, limit=50):
'streamType': 'BUFFERED',
'metadata': {
'title': track.name,
- 'artist': track.artists[0].name,
+ 'artist': track.artists[0].name if track.artists else "",
},
'duration': int(track.duration)
}
json_serializer = partial(json.dumps, default=lambda x: x.__dict__)
import ssl
import concurrent
+import threading
def setup(mass):
''' setup the module and read/apply config'''
app.add_routes([web.get('/jsonrpc.js', self.json_rpc)])
app.add_routes([web.post('/jsonrpc.js', self.json_rpc)])
app.add_routes([web.get('/ws', self.websocket_handler)])
- app.add_routes([web.get('/stream', self.stream)])
+ app.add_routes([web.get('/stream_track', self.mass.http_streamer.stream_track)])
+ app.add_routes([web.get('/stream_radio', self.mass.http_streamer.stream_radio)])
app.add_routes([web.get('/api/search', self.search)])
app.add_routes([web.get('/api/config', self.get_config)])
app.add_routes([web.post('/api/config', self.save_config)])
media_types.append(MediaType.Track)
if not media_types_query or "playlists" in media_types_query:
media_types.append(MediaType.Playlist)
+ if not media_types_query or "radios" in media_types_query:
+ media_types.append(MediaType.Radio)
# get results from database
result = await self.mass.music.search(searchquery, media_types, limit=limit, online=online)
return web.json_response(result, dumps=json_serializer)
self.mass.save_config()
return web.Response(text='success')
- async def stream(self, request):
- ''' start streaming audio from provider '''
- player_id = request.query.get('player_id')
- track_id = request.query.get('track_id')
- provider = request.query.get('provider')
- resp = web.StreamResponse(status=200,
- reason='OK',
- headers={'Content-Type': 'audio/flac'})
- await resp.prepare(request)
- if request.method.upper() != 'HEAD':
- # stream audio
- queue = asyncio.Queue()
- run_async_background_task(
- self.mass.bg_executor, self.mass.http_streamer.get_audio_stream, queue, track_id, provider, player_id)
- while True:
- chunk = await queue.get()
- if not chunk:
- queue.task_done()
- break
- await resp.write(chunk)
- queue.task_done()
- LOGGER.info("Finished streaming %s" % track_id)
- return resp
-
async def json_rpc(self, request):
'''
implement LMS jsonrpc interface
return wrapper
return scheduler
+async def try_supported(task):
+ ''' try to execute a task and pass NotImplementedError Exception '''
+ ret = None
+ try:
+ ret = await task
+ except NotImplementedError:
+ pass
+ return ret
+
def run_background_task(executor, corofn, *args):
''' run non-async task in background '''
return asyncio.get_event_loop().run_in_executor(executor, corofn, *args)
{ title: this.$t('albums'), icon: "album", path: "/albums" },
{ title: this.$t('tracks'), icon: "audiotrack", path: "/tracks" },
{ title: this.$t('playlists'), icon: "playlist_play", path: "/playlists" },
+ { title: this.$t('radios'), icon: "radio", path: "/radios" },
{ title: this.$t('search'), icon: "search", path: "/search" },
{ title: this.$t('settings'), icon: "settings", path: "/config" }
]
endpoint = "/artists/"
else if (item.media_type == 2)
endpoint = "/albums/"
- else if (item.media_type == 3)
+ else if (item.media_type == 3 || item.media_type == 5)
{
this.showPlayMenu(item);
return;
albums: "Albums",
tracks: "Tracks",
playlists: "Playlists",
+ radios: "Radio",
search: "Search",
settings: "Settings",
queue: "Queue",
http_streamer: "Built-in (sox based) streamer",
qobuz: "Qobuz",
spotify: "Spotify",
+ tunein: "TuneIn",
file: "Filesystem",
chromecast: "Chromecast",
lms: "Logitech Media Server",
http_streamer_sox_effects: "Custom sox effects to apply to audio (built-in streamer only!) See http://sox.sourceforge.net/sox.html#EFFECTS",
max_sample_rate: "Maximum sample rate this player supports, higher will be downsampled",
force_http_streamer: "Force use of built-in streamer, even if the player can handle the music provider directly",
- not_grouped: "Not grouped"
+ not_grouped: "Not grouped",
+ conf_saved: "Configuration saved, restart app to make effective",
+ audio_cache_folder: "Directory to use for cache files",
+ audio_cache_max_size_gb: "Maximum size of the cache folder (GB)"
},
// player strings
players: "Players",
albums: "Albums",
tracks: "Nummers",
playlists: "Afspeellijsten",
+ radios: "Radio",
search: "Zoeken",
settings: "Instellingen",
queue: "Wachtrij",
http_streamer: "Ingebouwde (sox gebaseerde) streamer",
qobuz: "Qobuz",
spotify: "Spotify",
+ tunein: "TuneIn",
file: "Bestandssysteem",
chromecast: "Chromecast",
lms: "Logitech Media Server",
max_sample_rate: "Maximale sample rate welke deze speler ondersteund, hoger wordt gedownsampled.",
force_http_streamer: "Forceer het gebruik van de ingebouwde streamer, ook al heeft de speler directe ondersteuning voor de muziek provider",
not_grouped: "Niet gegroepeerd",
- conf_saved: "Configutaion saved, restart app to make effective"
+ conf_saved: "Configuratie is opgeslagen, herstart om actief te maken",
+ audio_cache_folder: "Map om te gebruiken voor cache bestanden",
+ audio_cache_max_size_gb: "Maximale grootte van de cache map in GB."
},
// player strings
players: "Spelers",