from modules.cache import Cache
from modules.music import Music
from modules.player import Player
+from modules.http_streamer import HTTPStreamer
from modules.homeassistant import setup as hass_setup
from modules.web import setup as web_setup
self.hass = hass_setup(self)
self.music = Music(self)
self.player = Player(self)
+ self.http_streamer = HTTPStreamer(self)
# start the event loop
try:
self.event_listeners[cb_id] = cb
async def remove_event_listener(self, cb_id):
- ''' add callback to our event listeners '''
+ ''' remove callback from our event listeners '''
self.event_listeners.pop(cb_id, None)
def save_config(self):
''' save config to file '''
# backup existing file
conf_file = os.path.join(self.datapath, 'config.json')
- conf_file_backup = os.path.join(self.datapath, 'config.json')
+ conf_file_backup = os.path.join(self.datapath, 'config.json.backup')
if os.path.isfile(conf_file):
shutil.move(conf_file, conf_file_backup)
+ # remove description keys from config
+ final_conf = {}
+ for key, value in self.config.items():
+ final_conf[key] = {}
+ for subkey, subvalue in value.items():
+ if subkey != "__desc__":
+ final_conf[key][subkey] = subvalue
with open(conf_file, 'w') as f:
- f.write(json.dumps(self.config, indent=4))
+ f.write(json.dumps(final_conf, indent=4))
def parse_config(self):
'''get config from config file'''
self.name = ''
self.state = PlayerState.Stopped
self.powered = False
- self.cur_item = Track()
+ self.cur_item = None
self.cur_item_time = 0
self.volume_level = 0
self.shuffle_enabled = True
--- /dev/null
+#!/usr/bin/env python3
+# -*- coding:utf-8 -*-
+
+import asyncio
+import os
+from utils import LOGGER, try_parse_int, get_ip, run_async_background_task
+from models import TrackQuality
+import shutil
+import xml.etree.ElementTree as ET
+import random
+
+
+AUDIO_TEMP_DIR = "/tmp/audio_tmp"
+AUDIO_CACHE_DIR = "/tmp/audio_cache"
+
+class HTTPStreamer():
+ ''' Built-in streamer using sox and webserver '''
+
+ def __init__(self, mass):
+ self.mass = mass
+ self.create_config_entries()
+ self.local_ip = get_ip()
+ # create needed temp/cache dirs
+ if self.mass.config['base']['http_streamer']['enable_cache'] and not os.path.isdir(AUDIO_CACHE_DIR):
+ os.makedirs(AUDIO_CACHE_DIR)
+ if not os.path.isdir(AUDIO_TEMP_DIR):
+ os.makedirs(AUDIO_TEMP_DIR)
+
+ def create_config_entries(self):
+ ''' sets the config entries for this module (list with key/value pairs)'''
+ config_entries = [
+ ('volume_normalisation', True, 'enable_r128_volume_normalisation'),
+ ('target_volume', '-23', 'target_volume_lufs'),
+ ('fallback_gain_correct', '-12', 'fallback_gain_correct'),
+ ('enable_cache', True, 'enable_audio_cache'),
+ ('trim_silence', True, 'trim_silence')
+ ]
+ if not self.mass.config['base'].get('http_streamer'):
+ self.mass.config['base']['http_streamer'] = {}
+ self.mass.config['base']['http_streamer']['__desc__'] = config_entries
+ for key, def_value, desc in config_entries:
+ 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, track_id, provider, player_id=None):
+ ''' get audio stream for a track '''
+ queue = asyncio.Queue()
+ run_async_background_task(
+ self.mass.bg_executor, self.__get_audio_stream, queue, track_id, provider, player_id)
+ while True:
+ chunk = await queue.get()
+ if not chunk:
+ queue.task_done()
+ break
+ yield chunk
+ queue.task_done()
+ await queue.join()
+ # TODO: handle disconnects ?
+ LOGGER.info("Finished streaming %s" % track_id)
+
+ async def __get_audio_stream(self, audioqueue, track_id, provider, player_id=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)
+ 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)
+ LOGGER.info("Running sox with args: %s" % args)
+ process = await asyncio.create_subprocess_shell(args,
+ stdout=asyncio.subprocess.PIPE)
+ buffer_task = None
+ else:
+ # stream from provider
+ 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,
+ stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE)
+ buffer_task = asyncio.get_event_loop().create_task(
+ self.__fill_audio_buffer(process.stdin, track_id, provider, input_content_type))
+ # put chunks from stdout into queue
+ while not process.stdout.at_eof():
+ chunk = await process.stdout.read(10240000)
+ if not chunk:
+ break
+ await audioqueue.put(chunk)
+ # TODO: cooldown if the queue can't catch up to prevent memory being filled up with entire track
+ await process.wait()
+ await audioqueue.put('') # indicate EOF
+ LOGGER.info("streaming of track_id %s completed" % track_id)
+
+ async def __get_player_sox_options(self, track_id, provider, player_id):
+ ''' get player specific sox options '''
+ sox_effects = ' '
+ if not player_id:
+ return ''
+ if 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:
+ quality = TrackQuality.LOSSY_MP3
+ track_future = asyncio.run_coroutine_threadsafe(
+ self.mass.music.track(track_id, provider),
+ self.mass.event_loop
+ )
+ track = track_future.result()
+ for item in track.provider_ids:
+ if item['provider'] == provider and item['item_id'] == track_id:
+ quality = item['quality']
+ break
+ if quality > TrackQuality.FLAC_LOSSLESS_HI_RES_3 and max_sample_rate == 192000:
+ sox_effects += 'rate -v 192000'
+ elif quality > TrackQuality.FLAC_LOSSLESS_HI_RES_2 and max_sample_rate == 96000:
+ 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 + ' '
+
+ async def __analyze_audio(self, tmpfile, track_id, provider, content_type):
+ ''' analyze track audio, for now we only calculate EBU R128 loudness '''
+ LOGGER.info('Start analyzing file %s' % tmpfile)
+ cachefile = self.__get_track_cache_filename(track_id, provider)
+ # not needed to do processing if there already is a cachedfile
+ bs1770_binary = self.__get_bs1770_binary()
+ if bs1770_binary:
+ # calculate integrated r128 loudness with bs1770
+ analyse_dir = os.path.join(self.mass.datapath, 'analyse_info')
+ analysis_file = os.path.join(analyse_dir, "%s_%s.xml" %(provider, track_id.split(os.sep)[-1]))
+ if not os.path.isfile(analysis_file):
+ if not os.path.isdir(analyse_dir):
+ os.makedirs(analyse_dir)
+ cmd = '%s %s --xml --ebu -f %s' % (bs1770_binary, tmpfile, analysis_file)
+ process = await asyncio.create_subprocess_shell(cmd)
+ await process.wait()
+ if self.mass.config['base']['http_streamer']['enable_cache'] and not os.path.isfile(cachefile):
+ # use sox to store cache file (optionally strip silence from start and end)
+ if self.mass.config['base']['http_streamer']['trim_silence']:
+ cmd = 'sox -t %s %s -t flac -C5 %s silence 1 0.1 1%% reverse silence 1 0.1 1%% reverse' %(content_type, tmpfile, cachefile)
+ else:
+ # cachefile is always stored as flac
+ cmd = 'sox -t %s %s -t flac -C5 %s' %(content_type, tmpfile, cachefile)
+ process = await asyncio.create_subprocess_shell(cmd)
+ await process.wait()
+ # always clean up temp file
+ while os.path.isfile(tmpfile):
+ os.remove(tmpfile)
+ await asyncio.sleep(0.5)
+ LOGGER.info('Fininished analyzing file %s' % tmpfile)
+
+ async def __get_track_gain_correct(self, track_id, provider):
+ ''' get the gain correction that should be applied to a track '''
+ target_gain = int(self.mass.config['base']['http_streamer']['target_volume'])
+ fallback_gain = int(self.mass.config['base']['http_streamer']['fallback_gain_correct'])
+ analysis_file = os.path.join(self.mass.datapath, 'analyse_info', "%s_%s.xml" %(provider, track_id.split(os.sep)[-1]))
+ if not os.path.isfile(analysis_file):
+ return fallback_gain
+ try: # read audio analysis if available
+ tree = ET.parse(analysis_file)
+ trackinfo = tree.getroot().find("album").find("track")
+ track_lufs = trackinfo.find('integrated').get('lufs')
+ gain_correct = target_gain - float(track_lufs)
+ except Exception as exc:
+ LOGGER.error('could not retrieve track gain - %s' % exc)
+ gain_correct = fallback_gain # fallback value
+ if os.path.isfile(analysis_file):
+ os.remove(analysis_file)
+ # reschedule analyze task to try again
+ cachefile = self.__get_track_cache_filename(track_id, provider)
+ self.mass.event_loop.create_task(self.__analyze_audio(cachefile, track_id, provider, 'flac'))
+ return round(gain_correct,2)
+
+ async def __fill_audio_buffer(self, buf, track_id, provider, content_type):
+ ''' get audio data from provider and write to buffer'''
+ # fill the buffer with audio data
+ # a tempfile is created so we can do audio analysis
+ tmpfile = os.path.join(AUDIO_TEMP_DIR, '%s%s%s.tmp' % (random.randint(0, 999), track_id, random.randint(0, 999)))
+ fd = open(tmpfile, 'wb')
+ async for chunk in self.mass.music.providers[provider].get_audio_stream(track_id):
+ buf.write(chunk)
+ await buf.drain()
+ fd.write(chunk)
+ await buf.drain()
+ buf.write_eof()
+ fd.close()
+ # successfull completion, send tmpfile to be processed in the background in main loop
+ self.mass.event_loop.create_task(self.__analyze_audio(tmpfile, track_id, provider, content_type))
+ LOGGER.info("fill_audio_buffer complete for track %s" % track_id)
+ return
+
+ @staticmethod
+ def __get_track_cache_filename(track_id, provider):
+ ''' get filename for a track to use as cache file '''
+ return os.path.join(AUDIO_CACHE_DIR, '%s_%s' %(provider, track_id.split(os.sep)[-1]))
+
+ @staticmethod
+ def __get_bs1770_binary():
+ ''' get the path to the bs1770 binary for the current OS '''
+ import platform
+ bs1770_binary = None
+ if platform.system() == "Windows":
+ bs1770_binary = os.path.join(os.path.dirname(__file__), "bs1770gain", "win64", "bs1770gain")
+ elif platform.system() == "Darwin":
+ # macos binary is x86_64 intel
+ bs1770_binary = os.path.join(os.path.dirname(__file__), "bs1770gain", "osx", "bs1770gain")
+ elif platform.system() == "Linux":
+ architecture = platform.machine()
+ if architecture.startswith('AMD64') or architecture.startswith('x86_64'):
+ bs1770_binary = os.path.join(os.path.dirname(__file__), "bs1770gain", "linux64", "bs1770gain")
+ # TODO: build armhf binary
+ return bs1770_binary
\ No newline at end of file
async with aiohttp.ClientSession(loop=asyncio.get_event_loop(), connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
async with session.get(streamdetails['url']) as resp:
while True:
- chunk = await resp.content.read(10240000)
+ chunk = await resp.content.read(512000)
if not chunk:
break
yield chunk
- await asyncio.sleep(0.1)
LOGGER.info("end of stream for track_id %s" % track_id)
async def __parse_artist(self, artist_obj):
async with self.throttler:
async with self.http_session.get(url, headers=headers, params=params) as response:
result = await response.json()
- if 'error' in result:
+ if not result or 'error' in result:
LOGGER.error(url)
LOGGER.error(params)
LOGGER.error(result)
result = None
- result = await response.json()
return result
args = ['-n', 'temp', '-u', self._username, '-p', self._password, '--pass-through', '--single-track', track_id]
process = await asyncio.create_subprocess_exec(spotty, *args, stdout=asyncio.subprocess.PIPE)
while not process.stdout.at_eof():
- chunk = await process.stdout.read(10240000)
+ chunk = await process.stdout.read(128000)
if not chunk:
break
yield chunk
- await asyncio.sleep(0.1)
+ await process.wait()
LOGGER.info("end of stream for track_id %s" % track_id)
async def __parse_artist(self, artist_obj):
async with self.throttler:
async with self.http_session.get(url, headers=headers, params=params) as response:
result = await response.json()
- if 'error' in result:
+ if not result or 'error' in result:
LOGGER.error(url)
LOGGER.error(params)
- return None
+ result = None
return result
async def __delete_data(self, endpoint, params={}):
import asyncio
import os
from utils import run_periodic, LOGGER, try_parse_int, try_parse_float, get_ip, run_async_background_task
-import aiohttp
-from difflib import SequenceMatcher as Matcher
from models import MediaType, PlayerState, MusicPlayer, TrackQuality
-from typing import List
-import toolz
import operator
-import socket
import random
from copy import deepcopy
import functools
-import time
-import shutil
-import xml.etree.ElementTree as ET
-import concurrent
-import aiohttp
-import random
import urllib
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
MODULES_PATH = os.path.join(BASE_DIR, "playerproviders" )
-AUDIO_TEMP_DIR = "/tmp/audio_tmp"
-AUDIO_CACHE_DIR = "/tmp/audio_cache"
+
class Player():
''' several helpers to handle playback through player providers '''
self.mass = mass
self.providers = {}
self._players = {}
- self.create_config_entries()
self.local_ip = get_ip()
- # create needed temp/cache dirs
- if self.mass.config['base']['http_streamer']['enable_cache'] and not os.path.isdir(AUDIO_CACHE_DIR):
- os.makedirs(AUDIO_CACHE_DIR)
- if not os.path.isdir(AUDIO_TEMP_DIR):
- os.makedirs(AUDIO_TEMP_DIR)
# dynamically load provider modules
self.load_providers()
-
- def create_config_entries(self):
- ''' sets the config entries for this module (list with key/value pairs)'''
- # player specific settings
- self.mass.config['player_settings']['__desc__'] = [
- ("enabled", False, "player_enabled"),
- ("name", "", "player_name"),
- ("group_parent", "<player>", "player_group_with"),
- ("mute_as_power", False, "player_mute_power"),
- ("disable_volume", False, "player_disable_vol"),
- ("apply_group_volume", False, "player_group_vol"),
- ("apply_group_power", False, "player_group_pow"),
- ("play_power_on", False, "player_power_play"),
- ("sox_effects", '', "http_streamer_sox_effects"),
- ("max_sample_rate", '96000', "max_sample_rate"),
- ("force_http_streamer", False, "force_http_streamer")
- ]
- # config for the http streamer
- config_entries = [
- ('volume_normalisation', True, 'enable_r128_volume_normalisation'),
- ('target_volume', '-23', 'target_volume_lufs'),
- ('fallback_gain_correct', '-12', 'fallback_gain_correct'),
- ('enable_cache', True, 'enable_audio_cache'),
- ('trim_silence', True, 'trim_silence')
- ]
- if not self.mass.config['base'].get('http_streamer'):
- self.mass.config['base']['http_streamer'] = {}
- self.mass.config['base']['http_streamer']['__desc__'] = config_entries
- for key, def_value, desc in config_entries:
- if not key in self.mass.config['base']['http_streamer']:
- self.mass.config['base']['http_streamer'][key] = def_value
async def players(self):
''' return all players '''
return await self.__player_command_group_volume(player, player_childs, cmd_args)
if player.is_group and cmd == 'power' and cmd_args == 'off':
for item in player_childs:
- asyncio.create_task(self.player_command(item.player_id, cmd, cmd_args))
+ await self.player_command(item.player_id, cmd, cmd_args)
# normal execution of command on player
prov_id = self._players[player_id].player_provider
prov = self.providers[prov_id]
async def __get_player_settings(self, player_id):
''' get (or create) player config '''
+ config_entries = [ # default config entries for a player
+ ("enabled", False, "player_enabled"),
+ ("name", "", "player_name"),
+ ("group_parent", "<player>", "player_group_with"),
+ ("mute_as_power", False, "player_mute_power"),
+ ("disable_volume", False, "player_disable_vol"),
+ ("apply_group_volume", False, "player_group_vol"),
+ ("apply_group_power", False, "player_group_pow"),
+ ("play_power_on", False, "player_power_play"),
+ ("sox_effects", '', "http_streamer_sox_effects"),
+ ("max_sample_rate", '96000', "max_sample_rate"),
+ ("force_http_streamer", False, "force_http_streamer")
+ ]
player_settings = self.mass.config['player_settings'].get(player_id,{})
- for key, def_value, desc in self.mass.config['player_settings']['__desc__']:
+ for key, def_value, desc in config_entries:
if not key in player_settings:
if (isinstance(def_value, str) and def_value.startswith('<')):
player_settings[key] = None
player_prov = self.providers[player.player_provider]
return await player_prov.player_queue(player_id, offset=offset, limit=limit)
- async def get_audio_stream(self, track_id, provider, player_id=None):
- ''' get audio stream for a track '''
- queue = asyncio.Queue()
- run_async_background_task(
- self.mass.bg_executor, self.__get_audio_stream, queue, track_id, provider, player_id)
- while True:
- chunk = await queue.get()
- if not chunk:
- break
- yield chunk
- queue.task_done()
-
- async def __get_audio_stream(self, audioqueue, track_id, provider, player_id=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)
- 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)
- LOGGER.info("Running sox with args: %s" % args)
- process = await asyncio.create_subprocess_shell(args,
- stdout=asyncio.subprocess.PIPE)
- buffer_task = None
- else:
- # stream from provider
- 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,
- stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE)
- buffer_task = asyncio.get_event_loop().create_task(
- self.__fill_audio_buffer(process.stdin, track_id, provider, input_content_type))
- # put chunks from stdout into queue
- while not process.stdout.at_eof():
- chunk = await process.stdout.read(10240000)
- await audioqueue.put(chunk)
- if not chunk:
- break
- await asyncio.sleep(0.1)
- await process.wait()
- await audioqueue.put('') # indicate EOF
- LOGGER.info("streaming of track_id %s completed" % track_id)
-
- async def __get_player_sox_options(self, track_id, provider, player_id):
- ''' get player specific sox options '''
- sox_effects = ' '
- if not player_id:
- return ''
- if 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:
- quality = TrackQuality.LOSSY_MP3
- track_future = asyncio.run_coroutine_threadsafe(
- self.mass.music.track(track_id, provider),
- self.mass.event_loop
- )
- track = track_future.result()
- for item in track.provider_ids:
- if item['provider'] == provider and item['item_id'] == track_id:
- quality = item['quality']
- break
- if quality > TrackQuality.FLAC_LOSSLESS_HI_RES_3 and max_sample_rate == 192000:
- sox_effects += 'rate -v 192000'
- elif quality > TrackQuality.FLAC_LOSSLESS_HI_RES_2 and max_sample_rate == 96000:
- 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 + ' '
-
- async def __analyze_audio(self, tmpfile, track_id, provider, content_type):
- ''' analyze track audio, for now we only calculate EBU R128 loudness '''
- LOGGER.info('Start analyzing file %s' % tmpfile)
- cachefile = self.__get_track_cache_filename(track_id, provider)
- # not needed to do processing if there already is a cachedfile
- bs1770_binary = self.__get_bs1770_binary()
- if bs1770_binary:
- # calculate integrated r128 loudness with bs1770
- analyse_dir = os.path.join(self.mass.datapath, 'analyse_info')
- analysis_file = os.path.join(analyse_dir, "%s_%s.xml" %(provider, track_id.split(os.sep)[-1]))
- if not os.path.isfile(analysis_file):
- if not os.path.isdir(analyse_dir):
- os.makedirs(analyse_dir)
- cmd = '%s %s --xml --ebu -f %s' % (bs1770_binary, tmpfile, analysis_file)
- process = await asyncio.create_subprocess_shell(cmd)
- await process.wait()
- if self.mass.config['base']['http_streamer']['enable_cache'] and not os.path.isfile(cachefile):
- # use sox to store cache file (optionally strip silence from start and end)
- if self.mass.config['base']['http_streamer']['trim_silence']:
- cmd = 'sox -t %s %s -t flac -C5 %s silence 1 0.1 1%% reverse silence 1 0.1 1%% reverse' %(content_type, tmpfile, cachefile)
- else:
- # cachefile is always stored as flac
- cmd = 'sox -t %s %s -t flac -C5 %s' %(content_type, tmpfile, cachefile)
- process = await asyncio.create_subprocess_shell(cmd)
- await process.wait()
- # always clean up temp file
- while os.path.isfile(tmpfile):
- os.remove(tmpfile)
- await asyncio.sleep(0.5)
- LOGGER.info('Fininished analyzing file %s' % tmpfile)
-
- async def __get_track_gain_correct(self, track_id, provider):
- ''' get the gain correction that should be applied to a track '''
- target_gain = int(self.mass.config['base']['http_streamer']['target_volume'])
- fallback_gain = int(self.mass.config['base']['http_streamer']['fallback_gain_correct'])
- analysis_file = os.path.join(self.mass.datapath, 'analyse_info', "%s_%s.xml" %(provider, track_id.split(os.sep)[-1]))
- if not os.path.isfile(analysis_file):
- return fallback_gain
- try: # read audio analysis if available
- tree = ET.parse(analysis_file)
- trackinfo = tree.getroot().find("album").find("track")
- track_lufs = trackinfo.find('integrated').get('lufs')
- gain_correct = target_gain - float(track_lufs)
- except Exception as exc:
- LOGGER.error('could not retrieve track gain - %s' % exc)
- gain_correct = fallback_gain # fallback value
- if os.path.isfile(analysis_file):
- os.remove(analysis_file)
- # reschedule analyze task to try again
- cachefile = self.__get_track_cache_filename(track_id, provider)
- self.mass.event_loop.create_task(self.__analyze_audio(cachefile, track_id, provider, 'flac'))
- return round(gain_correct,2)
-
- async def __fill_audio_buffer(self, buf, track_id, provider, content_type):
- ''' get audio data from provider and write to buffer'''
- # fill the buffer with audio data
- # a tempfile is created so we can do audio analysis
- tmpfile = os.path.join(AUDIO_TEMP_DIR, '%s%s%s.tmp' % (random.randint(0, 999), track_id, random.randint(0, 999)))
- fd = open(tmpfile, 'wb')
- async for chunk in self.mass.music.providers[provider].get_audio_stream(track_id):
- buf.write(chunk)
- await buf.drain()
- fd.write(chunk)
- await buf.drain()
- buf.write_eof()
- fd.close()
- # successfull completion, send tmpfile to be processed in the background in main loop
- self.mass.event_loop.create_task(self.__analyze_audio(tmpfile, track_id, provider, content_type))
- LOGGER.info("fill_audio_buffer complete for track %s" % track_id)
- return
-
- @staticmethod
- def __get_track_cache_filename(track_id, provider):
- ''' get filename for a track to use as cache file '''
- return os.path.join(AUDIO_CACHE_DIR, '%s_%s' %(provider, track_id.split(os.sep)[-1]))
-
- @staticmethod
- def __get_bs1770_binary():
- ''' get the path to the bs1770 binary for the current OS '''
- import platform
- bs1770_binary = None
- if platform.system() == "Windows":
- bs1770_binary = os.path.join(os.path.dirname(__file__), "bs1770gain", "win64", "bs1770gain")
- elif platform.system() == "Darwin":
- # macos binary is x86_64 intel
- bs1770_binary = os.path.join(os.path.dirname(__file__), "bs1770gain", "osx", "bs1770gain")
- elif platform.system() == "Linux":
- architecture = platform.machine()
- if architecture.startswith('AMD64') or architecture.startswith('x86_64'):
- bs1770_binary = os.path.join(os.path.dirname(__file__), "bs1770gain", "linux64", "bs1770gain")
- # TODO: build armhf binary
- return bs1770_binary
-
def load_providers(self):
''' dynamically load providers '''
for item in os.listdir(MODULES_PATH):
async def player_command(self, player_id, cmd:str, cmd_args=None):
''' issue command on player (play, pause, next, previous, stop, power, volume, mute) '''
if cmd == 'play':
- if self._chromecasts[player_id].media_controller.status.player_is_paused:
+ if self._chromecasts[player_id].media_controller.status.player_is_playing:
+ pass
+ elif self._chromecasts[player_id].media_controller.status.player_is_paused:
self._chromecasts[player_id].media_controller.play()
else:
await self.__resume_queue(player_id)
if queue_opt == 'replace' or not self._player_queue[player_id]:
# overwrite queue with new items
self._player_queue[player_id] = media_items
- await self.__queue_play(player_id, 0)
+ await self.__queue_play(player_id, 0, send_flush=True)
elif queue_opt == 'play':
# replace current item with new item(s)
self._player_queue[player_id] = self._player_queue[player_id][:cur_queue_index] + media_items + self._player_queue[player_id][cur_queue_index+1:]
- await self.__queue_play(player_id, cur_queue_index)
+ await self.__queue_play(player_id, cur_queue_index, send_flush=True)
elif queue_opt == 'next':
# insert new items at current index +1
self._player_queue[player_id] = self._player_queue[player_id][:cur_queue_index+1] + media_items + self._player_queue[player_id][cur_queue_index+1:]
### Provider specific (helper) methods #####
- async def __queue_play(self, player_id, index):
+ async def __queue_play(self, player_id, index, send_flush=False):
''' send play command to player '''
- if not index:
+ if index == None:
index = self._player_queue_index[player_id]
if len(self._player_queue[player_id]) >= index-1:
track = self._player_queue[player_id][index]
- self._lmsplayers[player_id].stop()
+ if send_flush:
+ self._lmsplayers[player_id].flush()
self._lmsplayers[player_id].play(track.uri)
self._player_queue_index[player_id] = index
self._lmsplayers[lms_player.player_id] = lms_player
asyncio.create_task(self.__handle_player_event(lms_player.player_id, event, event_data))
+ @run_periodic(5)
+ async def send_heartbeat():
+ try:
+ timestamp = int(time.time())
+ data = lms_player.pack_stream(b"t", replayGain=timestamp, flags=0)
+ lms_player.send_frame(b"strm", data)
+ except RuntimeError:
+ reader.close()
+
lms_player.send_frame = send_frame
lms_player.send_event = handle_event
- heartbeat_task = asyncio.create_task(self.send_heartbeat(lms_player))
+ heartbeat_task = asyncio.create_task(send_heartbeat())
# keep reading bytes from the socket
while True:
heartbeat_task.cancel()
asyncio.create_task(self.__handle_player_event(lms_player.player_id, 'disconnected'))
- @run_periodic(5)
- async def send_heartbeat(self, lms_player):
- timestamp = int(time.time())
- data = lms_player.pack_stream(b"t", replayGain=timestamp, flags=0)
- lms_player.send_frame(b"strm", data)
-
+
### Provider specific implementation #####
class PyLMSPlayer(object):
data = self.pack_stream(b"q", autostart=b"0", flags=0)
self.send_frame(b"strm", data)
+ def flush(self):
+ data = self.pack_stream(b"f", autostart=b"1", flags=0)
+ self.send_frame(b"strm", data)
+
def pause(self):
data = self.pack_stream(b"p", autostart=b"0", flags=0)
self.send_frame(b"strm", data)
self._volume.volume = new_vol
self.send_volume()
- def play(self, uri, crossfade=False):
+ def play(self, uri, crossfade=True):
+ # TODO: attach crossfade to a config setting
command = b's'
autostart = b'3' # we use direct stream for now so let the player do the messy work with buffers
transType= b'1' if crossfade else b'0'
headers={'Content-Type': 'audio/flac'})
await resp.prepare(request)
if request.method.upper() != 'HEAD':
- async for chunk in self.mass.player.get_audio_stream(track_id, provider, player_id):
+ async for chunk in self.mass.http_streamer.get_audio_stream(track_id, provider, player_id):
await resp.write(chunk)
return resp
\ No newline at end of file
--- /dev/null
+var Config = Vue.component('Config', {
+ template: `
+ <section>
+
+ <v-list two-line>
+
+ <!-- base/generic config -->
+ <v-list-group prepend-icon="settings" no-action>
+ <template v-slot:activator>
+ <v-list-tile>
+ <v-list-tile-content>
+ <v-list-tile-title>{{ $t('generic_settings') }}</v-list-tile-title>
+ </v-list-tile-content>
+ </v-list-tile>
+ </template>
+ <template v-for="(conf_value, conf_key) in conf.base">
+ <v-list-tile>
+ <v-list-tile-content>
+ <v-list-tile-title class="title">{{ conf_key }}</v-list-tile-title>
+ </v-list-tile-content>
+ </v-list-tile>
+
+ <div v-for="conf_item_key in conf.base[conf_key].__desc__">
+ <v-list-tile>
+ <v-switch v-if="typeof(conf_item_key[1]) == 'boolean'" v-model="conf.base[conf_key][conf_item_key[0]]" :label="$t(conf_item_key[2])"></v-switch>
+ <v-text-field v-else-if="conf_item_key[1] == '<password>'" v-model="conf.base[conf_key][conf_item_key[0]]" :label="$t(conf_item_key[2])" box type="password"></v-text-field>
+ <v-select v-else-if="conf_item_key[1] == '<player>'" v-model="conf.base[conf_key][conf_item_key[0]]" :label="$t(conf_item_key[2])" box type="password"></v-select>
+ <v-text-field v-else v-model="conf.base[conf_key][conf_item_key[0]]" :label="$t(conf_item_key[2])" box></v-text-field>
+ </v-list-tile>
+ </div>
+ <v-divider></v-divider>
+ </template>
+ </v-list-group>
+
+
+ <!-- music providers -->
+ <v-list-group prepend-icon="library_music" no-action>
+ <template v-slot:activator>
+ <v-list-tile>
+ <v-list-tile-content>
+ <v-list-tile-title>{{ $t('music_providers') }}</v-list-tile-title>
+ </v-list-tile-content>
+ </v-list-tile>
+ </template>
+ <template v-for="(conf_value, conf_key) in conf.musicproviders">
+ <v-list-tile>
+ <v-list-tile-avatar>
+ <img :src="'images/icons/' + conf_key + '.png'"/>
+ </v-list-tile-avatar>
+ <v-list-tile-content>
+ <v-list-tile-title class="title">{{ conf_key }}</v-list-tile-title>
+ </v-list-tile-content>
+ </v-list-tile>
+
+ <div v-for="conf_item_key in conf.musicproviders[conf_key].__desc__">
+ <v-list-tile>
+ <v-switch v-if="typeof(conf_item_key[1]) == 'boolean'" v-model="conf.musicproviders[conf_key][conf_item_key[0]]" :label="$t(conf_item_key[2])"></v-switch>
+ <v-text-field v-else-if="conf_item_key[1] == '<password>'" v-model="conf.musicproviders[conf_key][conf_item_key[0]]" :label="$t(conf_item_key[2])" box type="password"></v-text-field>
+ <v-select v-else-if="conf_item_key[1] == '<player>'" v-model="conf.musicproviders[conf_key][conf_item_key[0]]" :label="$t(conf_item_key[2])" box type="password"></v-select>
+ <v-text-field v-else v-model="conf.musicproviders[conf_key][conf_item_key[0]]" :label="$t(conf_item_key[2])" box></v-text-field>
+ </v-list-tile>
+ </div>
+ <v-divider></v-divider>
+ </template>
+ </v-list-group>
+
+ <!-- player providers -->
+ <v-list-group prepend-icon="speaker_group" no-action>
+ <template v-slot:activator>
+ <v-list-tile>
+ <v-list-tile-content>
+ <v-list-tile-title>{{ $t('player_providers') }}</v-list-tile-title>
+ </v-list-tile-content>
+ </v-list-tile>
+ </template>
+ <template v-for="(conf_value, conf_key) in conf.playerproviders">
+ <v-list-tile>
+ <v-list-tile-avatar>
+ <img :src="'images/icons/' + conf_key + '.png'"/>
+ </v-list-tile-avatar>
+ <v-list-tile-content>
+ <v-list-tile-title class="title">{{ conf_key }}</v-list-tile-title>
+ </v-list-tile-content>
+ </v-list-tile>
+
+ <div v-for="conf_item_key in conf.playerproviders[conf_key].__desc__">
+ <v-list-tile>
+ <v-switch v-if="typeof(conf_item_key[1]) == 'boolean'" v-model="conf.playerproviders[conf_key][conf_item_key[0]]" :label="$t(conf_item_key[2])"></v-switch>
+ <v-text-field v-else-if="conf_item_key[1] == '<password>'" v-model="conf.playerproviders[conf_key][conf_item_key[0]]" :label="$t(conf_item_key[2])" box type="password"></v-text-field>
+ <v-select v-else-if="conf_item_key[1] == '<player>'" v-model="conf.playerproviders[conf_key][conf_item_key[0]]" :label="$t(conf_item_key[2])" box type="password"></v-select>
+ <v-text-field v-else v-model="conf.playerproviders[conf_key][conf_item_key[0]]" :label="$t(conf_item_key[2])" box></v-text-field>
+ </v-list-tile>
+ </div>
+ <v-divider></v-divider>
+ </template>
+ </v-list-group>
+
+ <!-- player settings -->
+ <v-list-group prepend-icon="speaker" no-action>
+ <template v-slot:activator>
+ <v-list-tile>
+ <v-list-tile-content>
+ <v-list-tile-title>{{ $t('player_settings') }}</v-list-tile-title>
+ </v-list-tile-content>
+ </v-list-tile>
+ </template>
+ <template v-for="(player, key) in players" v-if="key != '__desc__' && key in players">
+ <v-list-tile>
+ <v-list-tile-content>
+ <v-list-tile-title class="title">{{ players[key].name }}</v-list-tile-title>
+ <v-list-tile-sub-title class="title">ID: {{ key }} Provider: {{ players[key].player_provider }}</v-list-tile-sub-title>
+ </v-list-tile-content>
+ </v-list-tile>
+
+ <div v-for="conf_item_key in conf.player_settings.__desc__" v-if="conf.player_settings[key].enabled">
+ <v-list-tile>
+ <v-switch v-if="typeof(conf_item_key[1]) == 'boolean'" v-model="conf.player_settings[key][conf_item_key[0]]" :label="$t(conf_item_key[2])"></v-switch>
+ <v-text-field v-else-if="conf_item_key[1] == '<password>'" v-model="conf.player_settings[key][conf_item_key[0]]" :label="$t(conf_item_key[2])" box type="password"></v-text-field>
+ <v-select v-else-if="conf_item_key[1] == '<player>'" v-model="conf.player_settings[key][conf_item_key[0]]" :label="$t(conf_item_key[2])"
+ :items="playersLst"
+ item-text="name"
+ item-value="id" box>
+ </v-select>
+ <v-text-field v-else v-model="conf.player_settings[key][conf_item_key[0]]" :label="$t(conf_item_key[2])" box></v-text-field>
+ </v-list-tile>
+ <v-list-tile v-if="!conf.player_settings[key].enabled">
+ <v-switch v-model="conf.player_settings[key].enabled" :label="$t('enabled')"></v-switch>
+ </v-list-tile>
+ </div>
+ <div v-if="!conf.player_settings[key].enabled">
+ <v-list-tile>
+ <v-switch v-model="conf.player_settings[key].enabled" :label="$t('enabled')"></v-switch>
+ </v-list-tile>
+ </div>
+ <v-divider></v-divider>
+ </template>
+ </v-list-group>
+
+ <v-btn @click="saveConfig()">Save</v-btn>
+ </v-list>
+ </section>
+ `,
+ props: [],
+ data() {
+ return {
+ conf: {},
+ players: {}
+ }
+ },
+ computed: {
+ playersLst()
+ {
+ var playersLst = [];
+ for (player_id in this.conf.player_settings)
+ if (player_id != '__desc__')
+ playersLst.push({id: player_id, name: this.conf.player_settings[player_id].name})
+ return playersLst;
+ }
+
+ },
+ created() {
+ this.$globals.windowtitle = this.$t('settings');
+ this.getPlayers();
+ this.getConfig();
+ console.log(this.$globals.all_players);
+ },
+ methods: {
+ getConfig () {
+ axios
+ .get('/api/config')
+ .then(result => {
+ this.conf = result.data;
+ })
+ .catch(error => {
+ console.log("error", error);
+ });
+ },
+ saveConfig () {
+ axios
+ .post('/api/config', this.conf)
+ .then(result => {
+ console.log(result);
+ })
+ .catch(error => {
+ console.log("error", error);
+ });
+ },
+ getPlayers () {
+ const api_url = '/api/players';
+ axios
+ .get(api_url)
+ .then(result => {
+ for (var item of result.data)
+ this.$set(this.players, item.player_id, item)
+ })
+ .catch(error => {
+ console.log("error", error);
+ this.showProgress = false;
+ });
+ },
+ }
+})
template: `
<section>
- <v-list two-line>
+ <v-tabs v-model="active" color="transparent" light slider-color="black">
+ <v-tab ripple v-for="(conf_value, conf_key) in conf" :key="conf_key">{{ $t('conf.'+conf_key) }}</v-tab>
+ <v-tab-item v-for="(conf_value, conf_key) in conf" :key="conf_key">
- <!-- base/generic config -->
- <v-list-group prepend-icon="settings" no-action>
- <template v-slot:activator>
- <v-list-tile>
- <v-list-tile-content>
- <v-list-tile-title>{{ $t('generic_settings') }}</v-list-tile-title>
- </v-list-tile-content>
- </v-list-tile>
- </template>
- <template v-for="(conf_value, conf_key) in conf.base">
- <v-list-tile>
- <v-list-tile-content>
- <v-list-tile-title class="title">{{ conf_key }}</v-list-tile-title>
- </v-list-tile-content>
- </v-list-tile>
-
- <div v-for="conf_item_key in conf.base[conf_key].__desc__">
- <v-list-tile>
- <v-switch v-if="typeof(conf_item_key[1]) == 'boolean'" v-model="conf.base[conf_key][conf_item_key[0]]" :label="$t(conf_item_key[2])"></v-switch>
- <v-text-field v-else-if="conf_item_key[1] == '<password>'" v-model="conf.base[conf_key][conf_item_key[0]]" :label="$t(conf_item_key[2])" box type="password"></v-text-field>
- <v-select v-else-if="conf_item_key[1] == '<player>'" v-model="conf.base[conf_key][conf_item_key[0]]" :label="$t(conf_item_key[2])" box type="password"></v-select>
- <v-text-field v-else v-model="conf.base[conf_key][conf_item_key[0]]" :label="$t(conf_item_key[2])" box></v-text-field>
- </v-list-tile>
- </div>
- <v-divider></v-divider>
- </template>
- </v-list-group>
+ <!-- generic and module settings -->
+ <v-list two-line v-if="conf_key != 'player_settings'">
+ <v-list-group no-action v-for="(conf_subvalue, conf_subkey) in conf[conf_key]" :key="conf_key+conf_subkey">
+ <template v-slot:activator>
+ <v-list-tile>
+ <v-list-tile-avatar>
+ <img :src="'images/icons/' + conf_subkey + '.png'"/>
+ </v-list-tile-avatar>
+ <v-list-tile-content>
+ <v-list-tile-title>{{ $t('conf.'+conf_subkey) }}</v-list-tile-title>
+ </v-list-tile-content>
+ </v-list-tile>
+ </template>
+ <div v-for="conf_item_key in conf[conf_key][conf_subkey].__desc__">
+ <v-list-tile>
+ <v-switch v-if="typeof(conf_item_key[1]) == 'boolean'" v-model="conf[conf_key][conf_subkey][conf_item_key[0]]" :label="$t('conf.'+conf_item_key[2])"></v-switch>
+ <v-text-field v-else-if="conf_item_key[1] == '<password>'" v-model="conf[conf_key][conf_subkey][conf_item_key[0]]" :label="$t('conf.'+conf_item_key[2])" box type="password"></v-text-field>
+ <v-select v-else-if="conf_item_key[1] == '<player>'" v-model="conf[conf_key][conf_subkey][conf_item_key[0]]" :label="$t('conf.'+conf_item_key[2])" box type="password"></v-select>
+ <v-text-field v-else v-model="conf[conf_key][conf_subkey][conf_item_key[0]]" :label="$t('conf.'+conf_item_key[2])" box></v-text-field>
+ </v-list-tile>
+ </div>
+ <v-divider></v-divider>
+ </v-list-group>
+ </v-list two-line>
+ <!-- player settings -->
+ <v-list two-line v-if="conf_key == 'player_settings'">
+ <v-list-group no-action v-for="(player, key) in players" v-if="key != '__desc__' && key in players" :key="key">
+ <template v-slot:activator>
+ <v-list-tile>
+ <v-list-tile-avatar>
+ <img :src="'images/icons/' + players[key].player_provider + '.png'"/>
+ </v-list-tile-avatar>
+ <v-list-tile-content>
+ <v-list-tile-title class="title">{{ players[key].name }}</v-list-tile-title>
+ <v-list-tile-sub-title class="title">{{ key }}</v-list-tile-sub-title>
+ </v-list-tile-content>
+ </v-list-tile>
+ </template>
+ <div v-for="conf_item_key in conf.player_settings.__desc__" v-if="conf.player_settings[key].enabled">
+ <v-list-tile>
+ <v-switch v-if="typeof(conf_item_key[1]) == 'boolean'" v-model="conf.player_settings[key][conf_item_key[0]]" :label="$t('conf.'+conf_item_key[2])"></v-switch>
+ <v-text-field v-else-if="conf_item_key[1] == '<password>'" v-model="conf.player_settings[key][conf_item_key[0]]" :label="$t('conf.'+conf_item_key[2])" box type="password"></v-text-field>
+ <v-select v-else-if="conf_item_key[1] == '<player>'" v-model="conf.player_settings[key][conf_item_key[0]]" :label="$t('conf.'+conf_item_key[2])"
+ :items="playersLst"
+ item-text="name"
+ item-value="id" box>
+ </v-select>
+ <v-text-field v-else v-model="conf.player_settings[key][conf_item_key[0]]" :label="$t('conf.'+conf_item_key[2])" box></v-text-field>
+ </v-list-tile>
+ <v-list-tile v-if="!conf.player_settings[key].enabled">
+ <v-switch v-model="conf.player_settings[key].enabled" :label="$t('conf.'+'enabled')"></v-switch>
+ </v-list-tile>
+ </div>
+ <div v-if="!conf.player_settings[key].enabled">
+ <v-list-tile>
+ <v-switch v-model="conf.player_settings[key].enabled" :label="$t('conf.'+'enabled')"></v-switch>
+ </v-list-tile>
+ </div>
+ <v-divider></v-divider>
+ </v-list-group>
+ </v-list two-line>
+ </v-tab-item>
+ </v-tab>
+ </v-tabs>
- <!-- music providers -->
- <v-list-group prepend-icon="library_music" no-action>
- <template v-slot:activator>
- <v-list-tile>
- <v-list-tile-content>
- <v-list-tile-title>{{ $t('music_providers') }}</v-list-tile-title>
- </v-list-tile-content>
- </v-list-tile>
- </template>
- <template v-for="(conf_value, conf_key) in conf.musicproviders">
- <v-list-tile>
- <v-list-tile-avatar>
- <img :src="'images/icons/' + conf_key + '.png'"/>
- </v-list-tile-avatar>
- <v-list-tile-content>
- <v-list-tile-title class="title">{{ conf_key }}</v-list-tile-title>
- </v-list-tile-content>
- </v-list-tile>
-
- <div v-for="conf_item_key in conf.musicproviders[conf_key].__desc__">
- <v-list-tile>
- <v-switch v-if="typeof(conf_item_key[1]) == 'boolean'" v-model="conf.musicproviders[conf_key][conf_item_key[0]]" :label="$t(conf_item_key[2])"></v-switch>
- <v-text-field v-else-if="conf_item_key[1] == '<password>'" v-model="conf.musicproviders[conf_key][conf_item_key[0]]" :label="$t(conf_item_key[2])" box type="password"></v-text-field>
- <v-select v-else-if="conf_item_key[1] == '<player>'" v-model="conf.musicproviders[conf_key][conf_item_key[0]]" :label="$t(conf_item_key[2])" box type="password"></v-select>
- <v-text-field v-else v-model="conf.musicproviders[conf_key][conf_item_key[0]]" :label="$t(conf_item_key[2])" box></v-text-field>
- </v-list-tile>
- </div>
- <v-divider></v-divider>
- </template>
- </v-list-group>
- <!-- player providers -->
- <v-list-group prepend-icon="speaker_group" no-action>
- <template v-slot:activator>
- <v-list-tile>
- <v-list-tile-content>
- <v-list-tile-title>{{ $t('player_providers') }}</v-list-tile-title>
- </v-list-tile-content>
- </v-list-tile>
- </template>
- <template v-for="(conf_value, conf_key) in conf.playerproviders">
- <v-list-tile>
- <v-list-tile-avatar>
- <img :src="'images/icons/' + conf_key + '.png'"/>
- </v-list-tile-avatar>
- <v-list-tile-content>
- <v-list-tile-title class="title">{{ conf_key }}</v-list-tile-title>
- </v-list-tile-content>
- </v-list-tile>
-
- <div v-for="conf_item_key in conf.playerproviders[conf_key].__desc__">
- <v-list-tile>
- <v-switch v-if="typeof(conf_item_key[1]) == 'boolean'" v-model="conf.playerproviders[conf_key][conf_item_key[0]]" :label="$t(conf_item_key[2])"></v-switch>
- <v-text-field v-else-if="conf_item_key[1] == '<password>'" v-model="conf.playerproviders[conf_key][conf_item_key[0]]" :label="$t(conf_item_key[2])" box type="password"></v-text-field>
- <v-select v-else-if="conf_item_key[1] == '<player>'" v-model="conf.playerproviders[conf_key][conf_item_key[0]]" :label="$t(conf_item_key[2])" box type="password"></v-select>
- <v-text-field v-else v-model="conf.playerproviders[conf_key][conf_item_key[0]]" :label="$t(conf_item_key[2])" box></v-text-field>
- </v-list-tile>
- </div>
- <v-divider></v-divider>
- </template>
- </v-list-group>
-
- <!-- player settings -->
- <v-list-group prepend-icon="speaker" no-action>
- <template v-slot:activator>
- <v-list-tile>
- <v-list-tile-content>
- <v-list-tile-title>{{ $t('player_settings') }}</v-list-tile-title>
- </v-list-tile-content>
- </v-list-tile>
- </template>
- <template v-for="(player, key) in players" v-if="key != '__desc__' && key in players">
- <v-list-tile>
- <v-list-tile-content>
- <v-list-tile-title class="title">{{ players[key].name }}</v-list-tile-title>
- <v-list-tile-sub-title class="title">ID: {{ key }} Provider: {{ players[key].player_provider }}</v-list-tile-sub-title>
- </v-list-tile-content>
- </v-list-tile>
-
- <div v-for="conf_item_key in conf.player_settings.__desc__" v-if="conf.player_settings[key].enabled">
- <v-list-tile>
- <v-switch v-if="typeof(conf_item_key[1]) == 'boolean'" v-model="conf.player_settings[key][conf_item_key[0]]" :label="$t(conf_item_key[2])"></v-switch>
- <v-text-field v-else-if="conf_item_key[1] == '<password>'" v-model="conf.player_settings[key][conf_item_key[0]]" :label="$t(conf_item_key[2])" box type="password"></v-text-field>
- <v-select v-else-if="conf_item_key[1] == '<player>'" v-model="conf.player_settings[key][conf_item_key[0]]" :label="$t(conf_item_key[2])"
- :items="playersLst"
- item-text="name"
- item-value="id" box>
- </v-select>
- <v-text-field v-else v-model="conf.player_settings[key][conf_item_key[0]]" :label="$t(conf_item_key[2])" box></v-text-field>
- </v-list-tile>
- <v-list-tile v-if="!conf.player_settings[key].enabled">
- <v-switch v-model="conf.player_settings[key].enabled" :label="$t('enabled')"></v-switch>
- </v-list-tile>
- </div>
- <div v-if="!conf.player_settings[key].enabled">
- <v-list-tile>
- <v-switch v-model="conf.player_settings[key].enabled" :label="$t('enabled')"></v-switch>
- </v-list-tile>
- </div>
- <v-divider></v-divider>
- </template>
- </v-list-group>
-
- <v-btn @click="saveConfig()">Save</v-btn>
- </v-list>
</section>
`,
props: [],
data() {
return {
conf: {},
- players: {}
+ players: {},
+ active: 0
}
},
computed: {
playersLst()
{
var playersLst = [];
- for (player_id in this.conf.player_settings)
- if (player_id != '__desc__')
- playersLst.push({id: player_id, name: this.conf.player_settings[player_id].name})
+ playersLst.push({id: null, name: this.$t('conf.'+'not_grouped')})
+ for (player_id in this.players)
+ playersLst.push({id: player_id, name: this.conf.player_settings[player_id].name})
return playersLst;
}
},
+ watch: {
+ conf: {
+ handler: _.debounce(function (val) {
+ console.log("save config needed!");
+ this.saveConfig();
+ }, 1000)
+ }
+ },
created() {
this.$globals.windowtitle = this.$t('settings');
this.getPlayers();
search: "Search",
settings: "Settings",
queue: "Queue",
- generic_settings: "Generic settings",
- music_providers: "Music providers",
- player_providers: "Player providers",
- player_settings: "Player settings",
type_to_search: "Type here to search...",
- enabled: "Enabled",
add_library: "Add to library",
remove_library: "Remove from library",
// settings strings
- username: "Username",
- password: "Password",
- hostname: "Hostname (or IP)",
- port: "Port",
- hass_url: "URL to homeassistant (e.g. https://homeassistant:8123)",
- hass_token: "Long Lived Access Token",
- hass_publish: "Publish players to Home Assistant",
- hass_player_power: "Attach player power to homeassistant entity",
- hass_player_source: "Source on the homeassistant entity (optional)",
- hass_player_volume: "Attach player volume to homeassistant entity",
- web_ssl_cert: "Path to ssl certificate file",
- web_ssl_key: "Path to ssl keyfile",
- web_ssl_host: "Hostname (FQDN used in the certificate)",
- player_enabled: "Enable player",
- player_name: "Custom name for this player",
- player_group_with: "Group this player to another (parent)player",
- player_mute_power: "Use muting as power control",
- player_disable_vol: "Disable volume controls",
- player_group_vol: "Apply group volume to childs (for group players only)",
- player_group_pow: "Apply group power based on childs (for group players only)",
- player_power_play: "Issue play command on power on",
- file_prov_music_path: "Path to music files",
- file_prov_playlists_path: "Path to playlists (.m3u)",
+ conf: {
+ enabled: "Enabled",
+ base: "Generic settings",
+ musicproviders: "Music providers",
+ playerproviders: "Player providers",
+ player_settings: "Player settings",
+ homeassistant: "Home Assistant integration",
+ web: "Webserver",
+ http_streamer: "Built-in (sox based) streamer",
+ qobuz: "Qobuz",
+ spotify: "Spotify",
+ file: "Filesystem",
+ chromecast: "Chromecast",
+ lms: "Logitech Media Server",
+ pylms: "Emulated (built-in) Squeezebox support",
+ username: "Username",
+ password: "Password",
+ hostname: "Hostname (or IP)",
+ port: "Port",
+ hass_url: "URL to homeassistant (e.g. https://homeassistant:8123)",
+ hass_token: "Long Lived Access Token",
+ hass_publish: "Publish players to Home Assistant",
+ hass_player_power: "Attach player power to homeassistant entity",
+ hass_player_source: "Source on the homeassistant entity (optional)",
+ hass_player_volume: "Attach player volume to homeassistant entity",
+ web_ssl_cert: "Path to ssl certificate file",
+ web_ssl_key: "Path to ssl keyfile",
+ player_enabled: "Enable player",
+ player_name: "Custom name for this player",
+ player_group_with: "Group this player to another (parent)player",
+ player_mute_power: "Use muting as power control",
+ player_disable_vol: "Disable volume controls",
+ player_group_vol: "Apply group volume to childs (for group players only)",
+ player_group_pow: "Apply group power based on childs (for group players only)",
+ player_power_play: "Issue play command on power on",
+ file_prov_music_path: "Path to music files",
+ file_prov_playlists_path: "Path to playlists (.m3u)",
+ web_http_port: "HTTP port",
+ web_https_port: "HTTPS port",
+ cert_fqdn_host: "FQDN of hostname in certificate",
+ enable_r128_volume_normalisation: "Enable R128 volume normalization",
+ target_volume_lufs: "Target volume (R128 default is -23 LUFS)",
+ fallback_gain_correct: "Fallback gain correction if R128 readings not (yet) available",
+ enable_audio_cache: "Allow caching of audio to temp files",
+ trim_silence: "Strip silence from beginning and end of audio (temp files only!)",
+ 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"
+ },
// player strings
players: "Players",
play: "Play",
paused: "paused",
off: "off"
}
-
},
nl: {
search: "Zoeken",
settings: "Instellingen",
queue: "Wachtrij",
- generic_settings: "Algemene instellingen",
- music_providers: "Muziek providers",
- player_providers: "Speler providers",
- player_settings: "Speler instellingen",
- enabled: "Ingeschakeld",
type_to_search: "Type hier om te zoeken...",
add_library: "Voeg toe aan bibliotheek",
remove_library: "Verwijder uit bibliotheek",
// settings strings
- username: "Gebruikersnaam",
- password: "Wachtwoord",
- hostname: "Hostnaam (of IP)",
- port: "Poort",
- hass_url: "URL naar homeassistant (b.v. https://homeassistant:8123)",
- hass_token: "Token met lange levensduur",
- hass_publish: "Publiceer spelers naar Home Assistant",
- hass_player_power: "Verbind speler aan/uit met homeassistant entity",
- hass_player_source: "Benodigde bron op de verbonden homeassistant entity (optioneel)",
- hass_player_volume: "Verbind volume van speler aan een homeassistant entity",
- web_ssl_cert: "Pad naar ssl certificaat bestand",
- web_ssl_key: "Pad naar ssl certificaat key bestand",
- web_ssl_host: "Hostname (FQDN van certificaat)",
- player_enabled: "Speler inschakelen",
- player_name: "Aangepaste naam voor deze speler",
- player_group_with: "Groupeer deze speler met een andere (hoofd)speler",
- player_mute_power: "Gebruik mute als aan/uit",
- player_disable_vol: "Schakel volume bediening helemaal uit",
- player_group_vol: "Pas groep volume toe op onderliggende spelers (alleen groep spelers)",
- player_group_pow: "Pas groep aan/uit toe op onderliggende spelers (alleen groep spelers)",
- player_power_play: "Automatisch afspelen bij inschakelen",
- file_prov_music_path: "Pad naar muziek bestanden",
- file_prov_playlists_path: "Pad naar playlist bestanden (.m3u)",
+ conf: {
+ enabled: "Ingeschakeld",
+ base: "Algemene instellingen",
+ musicproviders: "Muziek providers",
+ playerproviders: "Speler providers",
+ player_settings: "Speler instellingen",
+ homeassistant: "Home Assistant integratie",
+ web: "Webserver",
+ http_streamer: "Ingebouwde (sox gebaseerde) streamer",
+ qobuz: "Qobuz",
+ spotify: "Spotify",
+ file: "Bestandssysteem",
+ chromecast: "Chromecast",
+ lms: "Logitech Media Server",
+ pylms: "Geemuleerde (ingebouwde) Squeezebox ondersteuning",
+ username: "Gebruikersnaam",
+ password: "Wachtwoord",
+ hostname: "Hostnaam (of IP)",
+ port: "Poort",
+ hass_url: "URL naar homeassistant (b.v. https://homeassistant:8123)",
+ hass_token: "Token met lange levensduur",
+ hass_publish: "Publiceer spelers naar Home Assistant",
+ hass_player_power: "Verbind speler aan/uit met homeassistant entity",
+ hass_player_source: "Benodigde bron op de verbonden homeassistant entity (optioneel)",
+ hass_player_volume: "Verbind volume van speler aan een homeassistant entity",
+ web_ssl_cert: "Pad naar ssl certificaat bestand",
+ web_ssl_key: "Pad naar ssl certificaat key bestand",
+ player_enabled: "Speler inschakelen",
+ player_name: "Aangepaste naam voor deze speler",
+ player_group_with: "Groupeer deze speler met een andere (hoofd)speler",
+ player_mute_power: "Gebruik mute als aan/uit",
+ player_disable_vol: "Schakel volume bediening helemaal uit",
+ player_group_vol: "Pas groep volume toe op onderliggende spelers (alleen groep spelers)",
+ player_group_pow: "Pas groep aan/uit toe op onderliggende spelers (alleen groep spelers)",
+ player_power_play: "Automatisch afspelen bij inschakelen",
+ file_prov_music_path: "Pad naar muziek bestanden",
+ file_prov_playlists_path: "Pad naar playlist bestanden (.m3u)",
+ web_http_port: "HTTP poort",
+ web_https_port: "HTTPS poort",
+ cert_fqdn_host: "Hostname (FQDN van certificaat)",
+ enable_r128_volume_normalisation: "Schakel R128 volume normalisatie in",
+ target_volume_lufs: "Doelvolume (R128 standaard is -23 LUFS)",
+ fallback_gain_correct: "Fallback gain correctie indien R128 meting (nog) niet beschikbaar is",
+ enable_audio_cache: "Sta het cachen van audio toe naar temp map",
+ trim_silence: "Strip stilte van begin en eind van audio (in temp bestanden)",
+ http_streamer_sox_effects: "Eigen sox effects toepassen op audio (alleen voor ingebouwde streamer). Zie http://sox.sourceforge.net/sox.html#EFFECTS",
+ 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"
+ },
// player strings
players: "Spelers",
play: "Afspelen",