import asyncio
from typing import List
from ..utils import LOGGER, compare_strings
-from ..cache import use_cache, cached_iterator, cached
-from ..constants import CONF_ENABLED
+from ..cache import cached_iterator, cached
from .media_types import Album, Artist, Track, Playlist, MediaType, Radio
Provider specific get methods shoud be overriden in the provider specific implementation
Uses a form of lazy provisioning to local db as cache
"""
-
def __init__(self, mass):
"""[DO NOT OVERRIDE]"""
self.prov_id = ''
async def setup(self, conf):
"""[SHOULD OVERRIDE] Setup the provider"""
- return False
+ LOGGER.debug(conf)
### Common methods and properties ####
if not item_id:
# artist not yet in local database so fetch details
cache_key = f'{self.prov_id}.get_artist.{prov_item_id}'
- artist_details = await cached(self.cache, cache_key, self.get_artist, prov_item_id )
+ artist_details = await cached(self.cache, cache_key,
+ self.get_artist, prov_item_id)
if not artist_details:
raise Exception('artist not found: %s' % prov_item_id)
if lazy:
]
for prov_id, provider in self.mass.music.providers.items():
if not prov_id in item_provider_keys:
- await provider.match_artist(
- new_artist, new_artist_albums, new_artist_toptracks)
+ await provider.match_artist(new_artist, new_artist_albums,
+ new_artist_toptracks)
return item_id
async def get_artist_musicbrainz_id(self,
if musicbrainz_id:
break
if not musicbrainz_id:
- LOGGER.warning("Unable to get musicbrainz ID for artist %s !" %
- artist_details.name)
+ LOGGER.debug("Unable to get musicbrainz ID for artist %s !",
+ artist_details.name)
musicbrainz_id = artist_details.name
return musicbrainz_id
# album not yet in local database so fetch details
if not album_details:
cache_key = f'{self.prov_id}.get_album.{prov_item_id}'
- album_details = await cached(self.cache, cache_key, self.get_album, prov_item_id)
+ album_details = await cached(self.cache, cache_key,
+ self.get_album, prov_item_id)
if not album_details:
raise Exception('album not found: %s' % prov_item_id)
if lazy:
# track not yet in local database so fetch details
if not track_details:
cache_key = f'{self.prov_id}.get_track.{prov_item_id}'
- track_details = await cached(self.cache, cache_key, self.get_track, prov_item_id)
+ track_details = await cached(self.cache, cache_key,
+ self.get_track, prov_item_id)
if not track_details:
raise Exception('track not found: %s' % prov_item_id)
if lazy:
async def album_tracks(self, prov_album_id) -> List[Track]:
""" return album tracks for the given provider album id"""
cache_key = f'{self.prov_id}.album_tracks.{prov_album_id}'
- async for item in cached_iterator(
- self.cache, self.get_album_tracks(prov_album_id), cache_key):
+ async for item in cached_iterator(self.cache,
+ self.get_album_tracks(prov_album_id),
+ cache_key):
if not item:
continue
db_id = await self.mass.db.get_database_id(item.provider,
cache_key = f'{self.prov_id}.playlist_tracks.{prov_playlist_id}'
pos = 0
async for item in cached_iterator(
- self.cache,
- self.get_playlist_tracks(prov_playlist_id),
- cache_key, checksum=cache_checksum):
+ self.cache,
+ self.get_playlist_tracks(prov_playlist_id),
+ cache_key,
+ checksum=cache_checksum):
if not item:
continue
db_id = await self.mass.db.get_database_id(item.provider,
""" return top tracks for an artist """
cache_key = f'{self.prov_id}.artist_toptracks.{prov_artist_id}'
async for item in cached_iterator(
- self.cache, self.get_artist_toptracks(prov_artist_id), cache_key):
+ self.cache, self.get_artist_toptracks(prov_artist_id),
+ cache_key):
if item:
db_id = await self.mass.db.get_database_id(
self.prov_id, item.item_id, MediaType.Track)
search_results = await self.search(searchstr, [MediaType.Album],
limit=5)
for item in search_results["albums"]:
- if (item and (item.name in searchalbum.name
+ if (item and
+ (item.name in searchalbum.name
or searchalbum.name in item.name) and compare_strings(
item.artist.name, searchalbum.artist.name, strict=False)):
# some providers mess up versions in the title, try to fix that situation
""" perform search on the provider """
return {"artists": [], "albums": [], "tracks": [], "playlists": []}
+ # pylint: disable=unreachable
async def get_library_artists(self) -> List[Artist]:
""" retrieve library artists from the provider """
# iterator !
return
yield
+ # pylint: enable=unreachable
+
async def get_album(self, prov_album_id) -> Album:
""" get full album details by id """
raise NotImplementedError
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
-import asyncio
-from enum import Enum
-from typing import List
-import operator
+"""
+ Models and helpers for a player.
+"""
+
import time
-from ..utils import run_periodic, LOGGER, try_parse_int, \
- try_parse_bool, try_parse_float
+from ..utils import try_parse_int, try_parse_bool, try_parse_float
from ..constants import EVENT_PLAYER_CHANGED
-from .media_types import Track, MediaType
-from .player_queue import PlayerQueue, QueueItem
+from .player_queue import PlayerQueue
from .playerstate import PlayerState
+# pylint: disable=too-many-instance-attributes
+# pylint: disable=too-many-public-methods
+# pylint: disable=too-few-public-methods
class Player():
- ''' representation of a player '''
+ """
+ Representation of a musicplayer.
+ Should be subclassed/overriden with provider specific implementation.
+ """
#### Provider specific implementation, should be overridden ####
async def cmd_stop(self):
- ''' [MUST OVERRIDE] send stop command to player '''
+ """ [MUST OVERRIDE] send stop command to player """
raise NotImplementedError
async def cmd_play(self):
- ''' [MUST OVERRIDE] send play (unpause) command to player '''
+ """ [MUST OVERRIDE] send play (unpause) command to player """
raise NotImplementedError
async def cmd_pause(self):
- ''' [MUST OVERRIDE] send pause command to player '''
+ """ [MUST OVERRIDE] send pause command to player """
raise NotImplementedError
async def cmd_next(self):
- ''' [CAN OVERRIDE] send next track command to player '''
+ """ [CAN OVERRIDE] send next track command to player """
return await self.queue.play_index(self.queue.cur_index+1)
async def cmd_previous(self):
- ''' [CAN OVERRIDE] send previous track command to player '''
+ """ [CAN OVERRIDE] send previous track command to player """
return await self.queue.play_index(self.queue.cur_index-1)
-
+
async def cmd_power_on(self):
- ''' [MUST OVERRIDE] send power ON command to player '''
+ """ [MUST OVERRIDE] send power ON command to player """
raise NotImplementedError
async def cmd_power_off(self):
- ''' [MUST OVERRIDE] send power TOGGLE command to player '''
+ """ [MUST OVERRIDE] send power TOGGLE command to player """
raise NotImplementedError
async def cmd_volume_set(self, volume_level):
- ''' [MUST OVERRIDE] send new volume level command to player '''
+ """ [MUST OVERRIDE] send new volume level command to player """
raise NotImplementedError
async def cmd_volume_mute(self, is_muted=False):
- ''' [MUST OVERRIDE] send mute command to player '''
+ """ [MUST OVERRIDE] send mute command to player """
raise NotImplementedError
- async def cmd_queue_play_index(self, index:int):
- '''
+ async def cmd_queue_play_index(self, index: int):
+ """
[OVERRIDE IF SUPPORTED]
play item at index X on player's queue
:attrib index: (int) index of the queue item that should start playing
- '''
+ """
item = await self.queue.get_item(index)
if item:
return await self.cmd_play_uri(item.uri)
async def cmd_queue_load(self, queue_items):
- '''
+ """
[OVERRIDE IF SUPPORTED]
load/overwrite given items in the player's own queue implementation
:param queue_items: a list of QueueItems
- '''
+ """
item = queue_items[0]
return await self.cmd_play_uri(item.uri)
async def cmd_queue_insert(self, queue_items, insert_at_index):
- '''
+ """
[OVERRIDE IF SUPPORTED]
insert new items at position X into existing queue
if offset 0 or None, will start playing newly added item(s)
:param queue_items: a list of QueueItems
:param insert_at_index: queue position to insert new items
- '''
+ """
raise NotImplementedError
async def cmd_queue_append(self, queue_items):
- '''
+ """
[OVERRIDE IF SUPPORTED]
append new items at the end of the queue
:param queue_items: a list of QueueItems
- '''
+ """
raise NotImplementedError
async def cmd_queue_update(self, queue_items):
- '''
+ """
[OVERRIDE IF SUPPORTED]
overwrite the existing items in the queue, used for reordering
:param queue_items: a list of QueueItems
- '''
+ """
raise NotImplementedError
async def cmd_queue_clear(self):
- '''
+ """
[OVERRIDE IF SUPPORTED]
empty the queue
- '''
+ """
raise NotImplementedError
- async def cmd_play_uri(self, uri:str):
- '''
+ async def cmd_play_uri(self, uri: str):
+ """
[MUST OVERRIDE]
tell player to start playing a single uri
- '''
+ """
raise NotImplementedError
#### Common implementation, should NOT be overrridden #####
self._name = ''
self._state = PlayerState.Stopped
self._group_childs = []
- self._powered = False
+ self._powered = False
self._cur_time = 0
self._media_position_updated_at = 0
self._cur_uri = ''
self._muted = False
self._queue = PlayerQueue(mass, self)
self.__update_player_settings()
- self._initialized = False
+ self.initialized = False
# public attributes
self.supports_queue = True # has native support for a queue
self.supports_gapless = False # has native gapless support
self.supports_crossfade = False # has native crossfading support
-
+
@property
def player_id(self):
- ''' [PROTECTED] player_id of this player '''
+ """ [PROTECTED] player_id of this player """
return self._player_id
@property
def player_provider(self):
- ''' [PROTECTED] provider id of this player '''
+ """ [PROTECTED] provider id of this player """
return self._prov_id
@property
def enabled(self):
- ''' [PROTECTED] enabled state of this player '''
+ """ [PROTECTED] enabled state of this player """
if self.settings.get('enabled'):
return True
else:
@property
def name(self):
- ''' [PROTECTED] name of this player '''
+ """ [PROTECTED] name of this player """
if self.settings.get('name'):
return self.settings['name']
else:
@name.setter
def name(self, name):
- ''' [PROTECTED] set (real) name of this player '''
+ """ [PROTECTED] set (real) name of this player """
if name != self._name:
self._name = name
self.mass.event_loop.create_task(self.update())
@property
def is_group(self):
- ''' [PROTECTED] is_group property of this player '''
+ """ [PROTECTED] is_group property of this player """
return len(self._group_childs) > 0
@property
def group_parents(self):
- ''' [PROTECTED] player ids of all groups this player belongs to '''
+ """ [PROTECTED] player ids of all groups this player belongs to """
player_ids = []
- for item in self.mass.players._players.values():
+ for item in self.mass.players.players:
if self.player_id in item.group_childs:
player_ids.append(item.player_id)
return player_ids
@property
def group_childs(self)->list:
- '''
+ """
[PROTECTED]
return all child player ids for this group player as list
empty list if this player is not a group player
- '''
+ """
return self._group_childs
@group_childs.setter
- def group_childs(self, group_childs:list):
- ''' [PROTECTED] set group_childs property of this player '''
+ def group_childs(self, group_childs: list):
+ """ [PROTECTED] set group_childs property of this player """
if group_childs != self._group_childs:
self._group_childs = group_childs
self.mass.event_loop.create_task(self.update())
self.mass.players.trigger_update(child_player_id))
def add_group_child(self, child_player_id):
- ''' add player as child to this group player '''
+ """ add player as child to this group player """
if not child_player_id in self._group_childs:
self._group_childs.append(child_player_id)
self.mass.event_loop.create_task(self.update())
self.mass.event_loop.create_task(
- self.mass.players.trigger_update(child_player_id))
+ self.mass.players.trigger_update(child_player_id))
def remove_group_child(self, child_player_id):
- ''' remove player as child from this group player '''
+ """ remove player as child from this group player """
if child_player_id in self._group_childs:
self._group_childs.remove(child_player_id)
self.mass.event_loop.create_task(self.update())
@property
def state(self):
- ''' [PROTECTED] state property of this player '''
+ """ [PROTECTED] state property of this player """
if not self.powered or not self.enabled:
return PlayerState.Off
# prefer group player state
return self._state
@state.setter
- def state(self, state:PlayerState):
- ''' [PROTECTED] set state property of this player '''
+ def state(self, state: PlayerState):
+ """ [PROTECTED] set state property of this player """
if state != self._state:
self._state = state
self.mass.event_loop.create_task(self.update())
@property
def powered(self):
- ''' [PROTECTED] return power state for this player '''
+ """ [PROTECTED] return power state for this player """
if not self.enabled:
return False
# homeassistant integration
- if (self.mass.hass.enabled and self.settings.get('hass_power_entity') and
+ if (self.mass.hass.enabled and self.settings.get('hass_power_entity') and
self.settings.get('hass_power_entity_source')):
hass_state = self.mass.hass.get_state(
- self.settings['hass_power_entity'],
- attribute='source')
+ self.settings['hass_power_entity'],
+ attribute='source')
return hass_state == self.settings['hass_power_entity_source']
elif self.mass.hass.enabled and self.settings.get('hass_power_entity'):
hass_state = self.mass.hass.get_state(
- self.settings['hass_power_entity'])
+ self.settings['hass_power_entity'])
return hass_state != 'off'
# mute as power
elif self.settings.get('mute_as_power'):
@powered.setter
def powered(self, powered):
- ''' [PROTECTED] set (real) power state for this player '''
+ """ [PROTECTED] set (real) power state for this player """
if powered != self._powered:
self._powered = powered
self.mass.event_loop.create_task(self.update())
@property
def cur_time(self):
- ''' [PROTECTED] cur_time (player's elapsed time) property of this player '''
+ """ [PROTECTED] cur_time (player's elapsed time) property of this player """
# prefer group player state
for group_id in self.group_parents:
group_player = self.mass.players.get_player_sync(group_id)
return self._cur_time
@cur_time.setter
- def cur_time(self, cur_time:int):
- ''' [PROTECTED] set cur_time (player's elapsed time) property of this player '''
+ def cur_time(self, cur_time: int):
+ """ [PROTECTED] set cur_time (player's elapsed time) property of this player """
if cur_time is None:
cur_time = 0
if cur_time != self._cur_time:
@property
def media_position_updated_at(self):
- ''' [PROTECTED] When was the position of the current playing media valid. '''
+ """ [PROTECTED] When was the position of the current playing media valid. """
return self._media_position_updated_at
@property
def cur_uri(self):
- ''' [PROTECTED] cur_uri (uri loaded in player) property of this player '''
+ """ [PROTECTED] cur_uri (uri loaded in player) property of this player """
# prefer group player's state
for group_id in self.group_parents:
group_player = self.mass.players.get_player_sync(group_id)
return self._cur_uri
@cur_uri.setter
- def cur_uri(self, cur_uri:str):
- ''' [PROTECTED] set cur_uri (uri loaded in player) property of this player '''
+ def cur_uri(self, cur_uri: str):
+ """ [PROTECTED] set cur_uri (uri loaded in player) property of this player """
if cur_uri != self._cur_uri:
self._cur_uri = cur_uri
self.mass.event_loop.create_task(self.update())
@property
def volume_level(self):
- ''' [PROTECTED] volume_level property of this player '''
+ """ [PROTECTED] volume_level property of this player """
# handle group volume
if self.is_group:
group_volume = 0
active_players = 0
for child_player_id in self.group_childs:
- child_player = self.mass.players._players.get(child_player_id)
+ child_player = self.mass.players.get_player_sync(child_player_id)
if child_player and child_player.enabled and child_player.powered:
group_volume += child_player.volume_level
active_players += 1
# handle hass integration
elif self.mass.hass.enabled and self.settings.get('hass_volume_entity'):
hass_state = self.mass.hass.get_state(
- self.settings['hass_volume_entity'],
- attribute='volume_level')
+ self.settings['hass_volume_entity'],
+ attribute='volume_level')
return int(try_parse_float(hass_state)*100)
else:
return self._volume_level
@volume_level.setter
- def volume_level(self, volume_level:int):
- ''' [PROTECTED] set volume_level property of this player '''
+ def volume_level(self, volume_level: int):
+ """ [PROTECTED] set volume_level property of this player """
volume_level = try_parse_int(volume_level)
if volume_level != self._volume_level:
self._volume_level = volume_level
# trigger update on group player
for group_parent_id in self.group_parents:
self.mass.event_loop.create_task(
- self.mass.players.trigger_update(group_parent_id))
+ self.mass.players.trigger_update(group_parent_id))
@property
def muted(self):
- ''' [PROTECTED] muted property of this player '''
+ """ [PROTECTED] muted property of this player """
return self._muted
@muted.setter
- def muted(self, is_muted:bool):
- ''' [PROTECTED] set muted property of this player '''
+ def muted(self, is_muted: bool):
+ """ [PROTECTED] set muted property of this player """
is_muted = try_parse_bool(is_muted)
if is_muted != self._muted:
self._muted = is_muted
@property
def queue(self):
- ''' [PROTECTED] player's queue '''
+ """ [PROTECTED] player's queue """
# prefer group player's state
for group_id in self.group_parents:
group_player = self.mass.players.get_player_sync(group_id)
return self._queue
async def stop(self):
- ''' [PROTECTED] send stop command to player '''
+ """ [PROTECTED] send stop command to player """
# redirect playback related commands to parent player
for group_id in self.group_parents:
group_player = self.mass.players.get_player_sync(group_id)
return await self.cmd_stop()
async def play(self):
- ''' [PROTECTED] send play (unpause) command to player '''
+ """ [PROTECTED] send play (unpause) command to player """
# redirect playback related commands to parent player
for group_id in self.group_parents:
group_player = self.mass.players.get_player_sync(group_id)
return await self.queue.resume()
async def pause(self):
- ''' [PROTECTED] send pause command to player '''
+ """ [PROTECTED] send pause command to player """
# redirect playback related commands to parent player
for group_id in self.group_parents:
group_player = self.mass.players.get_player_sync(group_id)
if group_player.state != PlayerState.Off:
return await group_player.pause()
return await self.cmd_pause()
-
+
async def play_pause(self):
- ''' toggle play/pause'''
+ """ toggle play/pause"""
if self.state == PlayerState.Playing:
return await self.pause()
else:
return await self.play()
-
+
async def next(self):
- ''' [PROTECTED] send next command to player '''
+ """ [PROTECTED] send next command to player """
# redirect playback related commands to parent player
for group_id in self.group_parents:
group_player = self.mass.players.get_player_sync(group_id)
return await self.queue.next()
async def previous(self):
- ''' [PROTECTED] send previous command to player '''
+ """ [PROTECTED] send previous command to player """
# redirect playback related commands to parent player
for group_id in self.group_parents:
group_player = self.mass.players.get_player_sync(group_id)
if group_player.state != PlayerState.Off:
return await group_player.previous()
return await self.queue.previous()
-
+
async def power(self, power):
- ''' [PROTECTED] send power ON command to player '''
+ """ [PROTECTED] send power ON command to player """
power = try_parse_bool(power)
if power:
return await self.power_on()
return await self.power_off()
async def power_on(self):
- ''' [PROTECTED] send power ON command to player '''
+ """ [PROTECTED] send power ON command to player """
await self.cmd_power_on()
# handle mute as power
if self.settings.get('mute_as_power'):
await self.volume_mute(False)
# handle hass integration
- if (self.mass.hass.enabled and
- self.settings.get('hass_power_entity') and
+ if (self.mass.hass.enabled and
+ self.settings.get('hass_power_entity') and
self.settings.get('hass_power_entity_source')):
cur_source = await self.mass.hass.get_state_async(
- self.settings['hass_power_entity'], attribute='source')
+ self.settings['hass_power_entity'], attribute='source')
if not cur_source:
- service_data = {
- 'entity_id': self.settings['hass_power_entity'],
- 'source': self.settings['hass_power_entity_source']
+ service_data = {
+ 'entity_id': self.settings['hass_power_entity'],
+ 'source': self.settings['hass_power_entity_source']
}
await self.mass.hass.call_service('media_player', 'select_source', service_data)
elif self.mass.hass.enabled and self.settings.get('hass_power_entity'):
domain = self.settings['hass_power_entity'].split('.')[0]
- service_data = { 'entity_id': self.settings['hass_power_entity']}
+ service_data = {'entity_id': self.settings['hass_power_entity']}
await self.mass.hass.call_service(domain, 'turn_on', service_data)
# handle play on power on
if self.settings.get('play_power_on'):
break
async def power_off(self):
- ''' [PROTECTED] send power OFF command to player '''
+ """ [PROTECTED] send power OFF command to player """
if self._state in [PlayerState.Playing, PlayerState.Paused]:
await self.stop()
await self.cmd_power_off()
if self.settings.get('mute_as_power'):
await self.volume_mute(True)
# handle hass integration
- if (self.mass.hass.enabled and
- self.settings.get('hass_power_entity') and
+ if (self.mass.hass.enabled and
+ self.settings.get('hass_power_entity') and
self.settings.get('hass_power_entity_source')):
cur_source = await self.mass.hass.get_state_async(
- self.settings['hass_power_entity'], attribute='source')
+ self.settings['hass_power_entity'], attribute='source')
if cur_source == self.settings['hass_power_entity_source']:
- service_data = { 'entity_id': self.settings['hass_power_entity'] }
+ service_data = {'entity_id': self.settings['hass_power_entity']}
await self.mass.hass.call_service('media_player', 'turn_off', service_data)
elif self.mass.hass.enabled and self.settings.get('hass_power_entity'):
domain = self.settings['hass_power_entity'].split('.')[0]
- service_data = { 'entity_id': self.settings['hass_power_entity']}
+ service_data = {'entity_id': self.settings['hass_power_entity']}
await self.mass.hass.call_service(domain, 'turn_off', service_data)
# handle group power
if self.is_group:
# player is group, turn off all childs
for child_player_id in self.group_childs:
- child_player = self.mass.players._players.get(child_player_id)
+ child_player = await self.mass.players.get_player(child_player_id)
if child_player and child_player.powered:
await child_player.power_off()
# if player has group parent(s), check if it should be turned off
for child_player_id in group_player.group_childs:
if child_player_id == self.player_id:
continue
- child_player = self.mass.players._players.get(child_player_id)
+ child_player = await self.mass.players.get_player(child_player_id)
if child_player and child_player.powered:
needs_power = True
break
await group_player.power_off()
async def power_toggle(self):
- ''' [PROTECTED] send toggle power command to player '''
+ """ [PROTECTED] send toggle power command to player """
if self.powered:
return await self.power_off()
else:
return await self.power_on()
async def volume_set(self, volume_level):
- ''' [PROTECTED] send new volume level command to player '''
+ """ [PROTECTED] send new volume level command to player """
volume_level = try_parse_int(volume_level)
if volume_level < 0:
volume_level = 0
else:
volume_dif_percent = volume_dif/cur_volume
for child_player_id in self.group_childs:
- child_player = self.mass.players._players.get(child_player_id)
+ child_player = await self.mass.players.get_player(child_player_id)
if child_player and child_player.enabled and child_player.powered:
cur_child_volume = child_player.volume_level
new_child_volume = cur_child_volume + (cur_child_volume * volume_dif_percent)
await child_player.volume_set(new_child_volume)
# handle hass integration
elif self.mass.hass.enabled and self.settings.get('hass_volume_entity'):
- service_data = {
- 'entity_id': self.settings['hass_volume_entity'],
+ service_data = {
+ 'entity_id': self.settings['hass_volume_entity'],
'volume_level': volume_level/100
}
await self.mass.hass.call_service('media_player', 'volume_set', service_data)
- await self.cmd_volume_set(100) # just force full volume on actual player if volume is outsourced to hass
+ # just force full volume on actual player if volume is outsourced to hass
+ await self.cmd_volume_set(100)
else:
await self.cmd_volume_set(volume_level)
async def volume_up(self):
- ''' [PROTECTED] send volume up command to player '''
+ """ [PROTECTED] send volume up command to player """
new_level = self.volume_level + 1
if new_level > 100:
new_level = 100
return await self.volume_set(new_level)
async def volume_down(self):
- ''' [PROTECTED] send volume down command to player '''
+ """ [PROTECTED] send volume down command to player """
new_level = self.volume_level - 1
if new_level < 0:
new_level = 0
return await self.volume_set(new_level)
async def volume_mute(self, is_muted=False):
- ''' [PROTECTED] send mute command to player '''
+ """ [PROTECTED] send mute command to player """
return await self.cmd_volume_mute(is_muted)
async def update(self, force=False):
- ''' [PROTECTED] signal player updated '''
- if not force and (not self._initialized or not self.enabled):
+ """ [PROTECTED] signal player updated """
+ if not force and (not self.initialized or not self.enabled):
return
# update queue state if player state changes
await self.queue.update_state()
@property
def settings(self):
- ''' [PROTECTED] get player config settings '''
+ """ [PROTECTED] get player config settings """
if self.player_id in self.mass.config['player_settings']:
return self.mass.config['player_settings'][self.player_id]
else:
return self.mass.config['player_settings'][self.player_id]
def __update_player_settings(self):
- ''' [PROTECTED] update player config settings '''
- player_settings = self.mass.config['player_settings'].get(self.player_id,{})
+ """ [PROTECTED] update player config settings """
+ player_settings = self.mass.config['player_settings'].get(self.player_id, {})
# generate config for the player
config_entries = [ # default config entries for a player
("enabled", True, "player_enabled"),
("name", "", "player_name"),
("mute_as_power", False, "player_mute_power"),
("max_sample_rate", 96000, "max_sample_rate"),
- ('volume_normalisation', True, 'enable_r128_volume_normalisation'),
+ ('volume_normalisation', True, 'enable_r128_volume_normalisation'),
('target_volume', '-23', 'target_volume_lufs'),
('fallback_gain_correct', '-12', 'fallback_gain_correct'),
("crossfade_duration", 0, "crossfade_duration"),
# append player specific settings
config_entries += self.mass.players.providers[self._prov_id].player_config_entries
# hass integration
- if self.mass.config['base'].get('homeassistant',{}).get("enabled"):
+ if self.mass.config['base'].get('homeassistant', {}).get("enabled"):
# append hass specific config entries
config_entries += [("hass_power_entity", "", "hass_player_power"),
- ("hass_power_entity_source", "", "hass_player_source"),
- ("hass_volume_entity", "", "hass_player_volume")]
+ ("hass_power_entity_source", "", "hass_player_source"),
+ ("hass_volume_entity", "", "hass_player_volume")]
+ # pylint: disable=unused-variable
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
else:
player_settings[key] = def_value
+ # pylint: enable=unused-variable
self.mass.config['player_settings'][self.player_id] = player_settings
self.mass.config['player_settings'][self.player_id]['__desc__'] = config_entries
-
+
def to_dict(self):
- ''' instance attributes as dict so it can be serialized to json '''
+ """ instance attributes as dict so it can be serialized to json """
return {
"player_id": self.player_id,
"player_provider": self.player_provider,
"group_childs": self.group_childs,
"enabled": self.enabled,
"supports_queue": self.supports_queue,
- "supports_gapless": self.supports_gapless,
- "supports_queue": self.supports_queue
- }
\ No newline at end of file
+ "supports_gapless": self.supports_gapless
+ }
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
+"""
+ Models and helpers for a player queue.
+"""
+
import asyncio
from typing import List
-import operator
import random
import uuid
-import os
from enum import Enum
-from ..utils import LOGGER, json, filename_from_string, serialize_values
-from ..constants import CONF_ENABLED, EVENT_PLAYBACK_STARTED, \
- EVENT_PLAYBACK_STOPPED, EVENT_QUEUE_UPDATED, EVENT_QUEUE_ITEMS_UPDATED
-from .media_types import Track, TrackQuality
+from ..utils import LOGGER, serialize_values
+from ..constants import EVENT_PLAYBACK_STARTED, EVENT_PLAYBACK_STOPPED, \
+ EVENT_QUEUE_UPDATED, EVENT_QUEUE_ITEMS_UPDATED
+from .media_types import Track
from .playerstate import PlayerState
+# pylint: disable=too-many-instance-attributes
+# pylint: disable=too-many-public-methods
+# pylint: disable=too-few-public-methods
class QueueOption(str, Enum):
+ """Enum representation of the queue (play) options"""
Play = "play"
Replace = "replace"
Next = "next"
class QueueItem(Track):
- ''' representation of a queue item, simplified version of track '''
+ """Representation of a queue item, extended version of track."""
def __init__(self, media_item=None):
super().__init__()
self.streamdetails = {}
setattr(self, key, value)
class PlayerQueue():
- '''
- basic implementation of a queue for a player
- if no player specific queue exists, this will be used
- '''
- # TODO: Persistent storage in DB
-
+ """
+ Model for a player's queue.
+ Can be overriden by custom implementation, but will not be needed
+ in most cases.
+ """
def __init__(self, mass, player):
self.mass = mass
self._player = player
self._last_player_state = PlayerState.Stopped
self._last_track = None
asyncio.run_coroutine_threadsafe(
- self.mass.add_event_listener(self.on_shutdown, "shutdown"), self.mass.event_loop)
+ self.mass.add_event_listener(self.on_shutdown, "shutdown"),
+ self.mass.event_loop)
# load previous queue settings from disk
- asyncio.run_coroutine_threadsafe(self.__restore_saved_state(), self.mass.event_loop)
+ asyncio.run_coroutine_threadsafe(self.__restore_saved_state(),
+ self.mass.event_loop)
@property
def shuffle_enabled(self):
+ """Shuffle enabled property"""
return self._shuffle_enabled
@shuffle_enabled.setter
def shuffle_enabled(self, enable_shuffle: bool):
- ''' enable/disable shuffle '''
+ """enable/disable shuffle"""
if not self._shuffle_enabled and enable_shuffle:
# shuffle requested
self._shuffle_enabled = True
- played_items = self.items[:self.cur_index]
- next_items = self.__shuffle_items(self.items[self.cur_index:])
- items = played_items + next_items
- self.mass.event_loop.create_task(self.update(items))
+ if self.cur_index is not None:
+ played_items = self.items[:self.cur_index]
+ next_items = self.__shuffle_items(self.items[self.cur_index +
+ 1:])
+ items = played_items + [self.cur_item] + next_items
+ self.mass.event_loop.create_task(self.update(items))
elif self._shuffle_enabled and not enable_shuffle:
# unshuffle
self._shuffle_enabled = False
- played_items = self.items[:self.cur_index]
- next_items = self.items[self.cur_index:]
- next_items.sort(key=lambda x: x.sort_index, reverse=False)
- items = played_items + next_items
- self.mass.event_loop.create_task(self.update(items))
+ if self.cur_index is not None:
+ played_items = self.items[:self.cur_index]
+ next_items = self.items[self.cur_index + 1:]
+ next_items.sort(key=lambda x: x.sort_index, reverse=False)
+ items = played_items + [self.cur_item] + next_items
+ self.mass.event_loop.create_task(self.update(items))
+ self.mass.event_loop.create_task(self.update_state())
@property
def repeat_enabled(self):
+ """Returns if crossfade is enabled for this player."""
return self._repeat_enabled
@repeat_enabled.setter
def repeat_enabled(self, enable_repeat: bool):
- ''' enable/disable repeat '''
+ """Set the repeat mode for this queue."""
if self._repeat_enabled != enable_repeat:
self._repeat_enabled = enable_repeat
- self.mass.event_loop.create_task(
- self.mass.signal_event(EVENT_QUEUE_UPDATED, self.to_dict()))
+ self.mass.event_loop.create_task(self.update_state())
+ self.mass.event_loop.create_task(self.__save_state())
@property
def crossfade_enabled(self):
+ """Returns if crossfade is enabled for this player's queue."""
return self._player.settings.get('crossfade_duration', 0) > 0
@property
def gapless_enabled(self):
+ """Returns if gapless support is enabled for this player."""
return self._player.settings.get('gapless_enabled', True)
@property
def cur_index(self):
- ''' match current uri with queue items to determine queue index '''
+ """
+ Returns the current index of the queue.
+ Returns None if queue is empty.
+ """
if not self._items:
return None
return self._cur_index
@property
def cur_item_id(self):
- ''' return the queue item id of the current item in the queue '''
- if self.cur_index == None or not len(self.items) > self.cur_index:
+ """
+ Return the queue item id of the current item in the queue.
+ Returns None if queue is empty.
+ """
+ if self.cur_index is None or not len(self.items) > self.cur_index:
return None
return self.items[self.cur_index].queue_item_id
@property
def cur_item(self):
- ''' return the current item in the queue '''
- if self.cur_index == None or not len(self.items) > self.cur_index:
+ """
+ Return the current item in the queue.
+ Returns None if queue is empty.
+ """
+ if self.cur_index is None or not len(self.items) > self.cur_index:
return None
return self.items[self.cur_index]
@property
def cur_item_time(self):
- ''' time (progress) for current playing item '''
+ """Returns the time (progress) for current (playing) item."""
return self._cur_item_time
-
+
@property
def next_index(self):
- '''
- return the next queue index for this player
- '''
+ """
+ Returns the next index for this player's queue.
+ Returns None if queue is empty or no more items.
+ """
if not self.items:
# queue is empty
return None
- if self.cur_index == None:
+ if self.cur_index is None:
# playback started
return 0
else:
# player already playing (or paused) so return the next item
if len(self.items) > (self.cur_index + 1):
return self.cur_index + 1
- elif self._repeat_enabled:
+ if self._repeat_enabled:
# repeat enabled, start queue at beginning
return 0
return None
@property
def next_item(self):
- '''
- return the next item in the queue
- '''
- if self.next_index != None:
+ """
+ Returns the next item in the queue.
+ Returns None if queue is empty or no more items.
+ """
+ if self.next_index is not None:
return self.items[self.next_index]
return None
-
+
@property
def items(self):
- '''
- return all queue items for this player
- '''
+ """
+ Returns all queue items for this player's queue.
+ """
return self._items
@property
def use_queue_stream(self):
- '''
+ """
bool to indicate that we need to use the queue stream
for example if crossfading is requested but a player doesn't natively support it
it will send a constant stream of audio to the player and all tracks
- '''
- return ((self.crossfade_enabled and not self._player.supports_crossfade) or
- (self.gapless_enabled and not self._player.supports_gapless))
-
+ """
+ return ((self.crossfade_enabled
+ and not self._player.supports_crossfade) or
+ (self.gapless_enabled and not self._player.supports_gapless))
+
async def get_item(self, index):
- ''' get item by index from queue '''
- if index != None and len(self.items) > index:
+ """get item by index from queue"""
+ if index is not None and len(self.items) > index:
return self.items[index]
return None
- async def by_item_id(self, queue_item_id:str):
- ''' get item by queue_item_id from queue '''
+ async def by_item_id(self, queue_item_id: str):
+ """get item by queue_item_id from queue"""
if not queue_item_id:
return None
for item in self.items:
if item.queue_item_id == queue_item_id:
return item
return None
-
+
async def next(self):
- ''' request next track in queue '''
- if self.cur_index == None:
+ """Request player to play the next track in the queue."""
+ if self.cur_index is None:
return
if self.use_queue_stream:
- return await self.play_index(self.cur_index+1)
+ return await self.play_index(self.cur_index + 1)
else:
return await self._player.cmd_next()
async def previous(self):
- ''' request previous track in queue '''
- if self.cur_index == None:
+ """Request player to play the previous track in the queue."""
+ if self.cur_index is None:
return
if self.use_queue_stream:
- return await self.play_index(self.cur_index-1)
+ return await self.play_index(self.cur_index - 1)
else:
return await self._player.cmd_previous()
async def resume(self):
- ''' resume previous queue '''
+ """Resume previous queue."""
if self.items:
prev_index = self.cur_index
if self.use_queue_stream or not self._player.supports_queue:
await self._player.cmd_queue_load(self.items)
await self.play_index(prev_index)
else:
- LOGGER.warning("resume queue requested for %s but queue is empty" % self._player.name)
-
+ LOGGER.warning("resume queue requested for %s but queue is empty",
+ self._player.name)
+
async def play_index(self, index):
- ''' play item at index X in queue '''
+ """Play item at index X in queue."""
if not isinstance(index, int):
index = self.__index_by_id(index)
if not len(self.items) > index:
return
if self.use_queue_stream:
self._next_queue_startindex = index
- queue_stream_uri = 'http://%s:%s/stream/%s'% (
- self.mass.web.local_ip, self.mass.web.http_port, self._player.player_id)
+ queue_stream_uri = 'http://%s:%s/stream/%s' % (
+ self.mass.web.local_ip, self.mass.web.http_port,
+ self._player.player_id)
return await self._player.cmd_play_uri(queue_stream_uri)
elif self._player.supports_queue:
return await self._player.cmd_queue_play_index(index)
else:
return await self._player.cmd_play_uri(self._items[index].uri)
-
+
async def move_item(self, queue_item_id, pos_shift=1):
- '''
+ """
move queue item x up/down the queue
param pos_shift: move item x positions down if positive value
move item x positions up if negative value
move item to top of queue as next item
- '''
+ """
items = self.items.copy()
item_index = self.__index_by_id(queue_item_id)
if pos_shift == 0 and self._player.state == PlayerState.Playing:
await self.update(items)
if pos_shift == 0:
await self.play_index(new_index)
-
- async def load(self, queue_items:List[QueueItem]):
- ''' load (overwrite) queue with new items '''
+
+ async def load(self, queue_items: List[QueueItem]):
+ """load (overwrite) queue with new items"""
for index, item in enumerate(queue_items):
item.sort_index = index
if self._shuffle_enabled:
else:
await self._player.cmd_queue_load(queue_items)
await self.mass.signal_event(EVENT_QUEUE_ITEMS_UPDATED, self.to_dict())
-
- async def insert(self, queue_items:List[QueueItem], offset=0):
- '''
+ self.mass.event_loop.create_task(self.__save_state())
+
+ async def insert(self, queue_items: List[QueueItem], offset=0):
+ """
insert new items at offset x from current position
keeps remaining items in queue
if offset 0, will start playing newly added item(s)
:param queue_items: a list of QueueItem
:param offset: offset from current queue position
- '''
-
- if not self.items or self.cur_index == None or self.cur_index + offset > len(self.items):
+ """
+
+ if not self.items or self.cur_index is None or self.cur_index + offset > len(
+ self.items):
return await self.load(queue_items)
insert_at_index = self.cur_index + offset
for index, item in enumerate(queue_items):
item.sort_index = insert_at_index + index
if self.shuffle_enabled:
queue_items = self.__shuffle_items(queue_items)
- self._items = self._items[:insert_at_index] + queue_items + self._items[insert_at_index:]
+ self._items = self._items[:insert_at_index] + queue_items + self._items[
+ insert_at_index:]
if self.use_queue_stream or not self._player.supports_queue:
if offset == 0:
await self.play_index(insert_at_index)
else:
try:
- await self._player.cmd_queue_insert(queue_items, insert_at_index)
+ await self._player.cmd_queue_insert(queue_items,
+ insert_at_index)
except NotImplementedError:
# not supported by player, use load queue instead
- LOGGER.debug("cmd_queue_insert not supported by player, fallback to cmd_queue_load ")
+ LOGGER.debug(
+ "cmd_queue_insert not supported by player, fallback to cmd_queue_load "
+ )
self._items = self._items[self.cur_index:]
await self._player.cmd_queue_load(self._items)
self.mass.event_loop.create_task(
- self.mass.signal_event(EVENT_QUEUE_ITEMS_UPDATED, self.to_dict()))
+ self.mass.signal_event(EVENT_QUEUE_ITEMS_UPDATED, self.to_dict()))
+ self.mass.event_loop.create_task(self.__save_state())
- async def append(self, queue_items:List[QueueItem]):
- '''
+ async def append(self, queue_items: List[QueueItem]):
+ """
append new items at the end of the queue
- '''
+ """
for index, item in enumerate(queue_items):
item.sort_index = len(self.items) + index
if self.shuffle_enabled:
await self._player.cmd_queue_append(queue_items)
except NotImplementedError:
# not supported by player, use load queue instead
- LOGGER.debug("cmd_queue_append not supported by player, fallback to cmd_queue_load ")
+ LOGGER.debug(
+ "cmd_queue_append not supported by player, fallback to cmd_queue_load "
+ )
self._items = self._items[self.cur_index:]
await self._player.cmd_queue_load(self._items)
self.mass.event_loop.create_task(
self.mass.signal_event(EVENT_QUEUE_ITEMS_UPDATED, self.to_dict()))
+ self.mass.event_loop.create_task(self.__save_state())
- async def update(self, queue_items:List[QueueItem]):
- '''
+ async def update(self, queue_items: List[QueueItem]):
+ """
update the existing queue items, mostly caused by reordering
- '''
+ """
self._items = queue_items
if self._player.supports_queue and not self.use_queue_stream:
try:
await self._player.cmd_queue_update(queue_items)
except NotImplementedError:
# not supported by player, use load queue instead
- LOGGER.debug("cmd_queue_update not supported by player, fallback to cmd_queue_load ")
+ LOGGER.debug(
+ "cmd_queue_update not supported by player, fallback to cmd_queue_load "
+ )
self._items = self._items[self.cur_index:]
await self._player.cmd_queue_load(self._items)
self.mass.event_loop.create_task(
self.mass.signal_event(EVENT_QUEUE_ITEMS_UPDATED, self.to_dict()))
+ self.mass.event_loop.create_task(self.__save_state())
async def clear(self):
- '''
+ """
clear all items in the queue
- '''
+ """
await self._player.stop()
self._items = []
if self._player.supports_queue:
self.mass.signal_event(EVENT_QUEUE_ITEMS_UPDATED, self.to_dict()))
async def update_state(self):
- ''' update queue details, called when player updates '''
+ """update queue details, called when player updates"""
cur_index = self._cur_index
track_time = self._cur_item_time
# handle queue stream
cur_index, track_time = await self.__get_queue_stream_index()
# normal queue based approach
elif not self.use_queue_stream:
- track_time = self._player._cur_time
+ track_time = self._player.cur_time
for index, queue_item in enumerate(self.items):
if queue_item.uri == self._player.cur_uri:
cur_index = index
await self.mass.signal_event(EVENT_QUEUE_UPDATED, self.to_dict())
async def start_queue_stream(self):
- ''' called by the queue streamer when it starts playing the queue stream '''
+ """called by the queue streamer when it starts playing the queue stream"""
self._last_queue_startindex = self._next_queue_startindex
return await self.get_item(self._next_queue_startindex)
def to_dict(self):
- ''' instance attributes as dict so it can be serialized to json '''
+ """instance attributes as dict so it can be serialized to json"""
return {
"player_id": self._player.player_id,
"shuffle_enabled": self.shuffle_enabled,
"next_index": self.next_index,
"cur_item": serialize_values(self.cur_item),
"cur_item_time": self.cur_item_time,
- "next_index": self.next_index,
"next_item": serialize_values(self.next_item),
"queue_stream_enabled": self.use_queue_stream
}
-
+
async def __get_queue_stream_index(self):
# player is playing a constant stream of the queue so we need to do this the hard way
queue_index = 0
total_time = 0
track_time = 0
if self.items and len(self.items) > self._last_queue_startindex:
- queue_index = self._last_queue_startindex # holds the last starting position
+ queue_index = self._last_queue_startindex # holds the last starting position
queue_track = None
while len(self.items) > queue_index:
queue_track = self.items[queue_index]
break
self._next_queue_startindex = queue_index + 1
return queue_index, track_time
-
+
async def __process_queue_update(self, new_index, track_time):
- ''' compare the queue index to determine if playback changed '''
+ """compare the queue index to determine if playback changed"""
new_track = await self.get_item(new_index)
- if (not self._last_track and new_track) or self._last_track != new_track:
+ if (not self._last_track
+ and new_track) or self._last_track != new_track:
# queue track updated
# account for track changing state so trigger track change after 1 second
if self._last_track and self._last_track.streamdetails:
- self._last_track.streamdetails["seconds_played"] = self._last_item_time
- await self.mass.signal_event(EVENT_PLAYBACK_STOPPED, self._last_track.streamdetails)
+ self._last_track.streamdetails[
+ "seconds_played"] = self._last_item_time
+ await self.mass.signal_event(EVENT_PLAYBACK_STOPPED,
+ self._last_track.streamdetails)
if new_track and new_track.streamdetails:
- await self.mass.signal_event(EVENT_PLAYBACK_STARTED, new_track.streamdetails)
+ await self.mass.signal_event(EVENT_PLAYBACK_STARTED,
+ new_track.streamdetails)
self._last_track = new_track
if self._last_player_state != self._player.state:
self._last_player_state = self._player.state
- if (self._player.cur_time == 0 and
- self._player.state in [PlayerState.Stopped, PlayerState.Off]):
+ if (self._player.cur_time == 0 and self._player.state in [
+ PlayerState.Stopped, PlayerState.Off
+ ]):
# player stopped playing
if self._last_track:
await self.mass.signal_event(
self._last_item_time = track_time
self._cur_item_time = track_time
self._cur_index = new_index
-
- def __shuffle_items(self, queue_items):
- ''' shuffle a list of tracks '''
+
+ @staticmethod
+ def __shuffle_items(queue_items):
+ """shuffle a list of tracks"""
# for now we use default python random function
# can be extended with some more magic last_played and stuff
return random.sample(queue_items, len(queue_items))
def __index_by_id(self, queue_item_id):
- ''' get index by queue_item_id '''
+ """get index by queue_item_id"""
item_index = None
for index, item in enumerate(self.items):
if item.queue_item_id == queue_item_id:
item_index = index
return item_index
-
+
async def __restore_saved_state(self):
- ''' try to load the saved queue for this player from cache file '''
+ """try to load the saved queue for this player from cache file"""
cache_str = 'queue_%s' % self._player.player_id
cache_data = await self.mass.cache.get(cache_str)
if cache_data:
self._cur_index = cache_data["cur_item"]
self._next_queue_startindex = cache_data["next_queue_index"]
+ # pylint: disable=unused-argument
async def on_shutdown(self, msg, msg_details):
"""Handle shutdown event, save queue state."""
- ''' save current queue settings to file '''
+ await self.__save_state()
+ # pylint: enable=unused-argument
+
+ async def __save_state(self):
+ """save current queue settings to file"""
cache_str = 'queue_%s' % self._player.player_id
cache_data = {
"shuffle_enabled": self._shuffle_enabled,
"next_queue_index": self._next_queue_startindex
}
await self.mass.cache.set(cache_str, cache_data)
- LOGGER.info("queue state saved to file for player %s", self._player.player_id)
-
\ No newline at end of file
+ LOGGER.info("queue state saved to file for player %s",
+ self._player.player_id)
method_class.running_sync_jobs.remove(sync_job)
await method_class.mass.signal_event(
EVENT_MUSIC_SYNC_STATUS, method_class.running_sync_jobs)
+
return wrapped
+
return wrapper
self.providers.pop(reload_module, None)
LOGGER.info('Unloaded %s module', reload_module)
# load all modules (that are not already loaded)
- await load_provider_modules(self.mass,
- self.providers, CONF_KEY_MUSICPROVIDERS)
+ await load_provider_modules(self.mass, self.providers,
+ CONF_KEY_MUSICPROVIDERS)
async def item(self,
item_id,
async for item in self.mass.db.artist_tracks(artist.item_id):
if (item.name + item.version) not in track_names:
yield item
- track_names.append(item.name+item.version)
+ track_names.append(item.name + item.version)
for prov_mapping in artist.provider_ids:
prov_id = prov_mapping['provider']
prov_item_id = prov_mapping['item_id']
async for item in prov_obj.artist_toptracks(prov_item_id):
if (item.name + item.version) not in track_names:
yield item
- track_names.append(item.name+item.version)
+ track_names.append(item.name + item.version)
async def artist_albums(self, artist_id,
provider='database') -> List[Album]:
async for item in self.mass.db.artist_albums(artist.item_id):
if (item.name + item.version) not in album_names:
yield item
- album_names.append(item.name+item.version)
+ album_names.append(item.name + item.version)
for prov_mapping in artist.provider_ids:
prov_id = prov_mapping['provider']
prov_item_id = prov_mapping['item_id']
prov_obj = self.providers[prov_id]
- async for item in prov_obj.artist_albums(prov_item_id):
+ async for item in prov_obj.artist_albums(prov_item_id):
if (item.name + item.version) not in album_names:
yield item
- album_names.append(item.name+item.version)
+ album_names.append(item.name + item.version)
async def album_tracks(self, album_id, provider='database') -> List[Track]:
''' get the album tracks for given album '''
result = False
for item in media_items:
# make sure we have a database item
- media_item = await self.item(item.item_id, item.media_type, item.provider, lazy=False)
+ media_item = await self.item(item.item_id,
+ item.media_type,
+ item.provider,
+ lazy=False)
if not media_item:
continue
# add to provider's libraries
result = await self.providers[prov_id].add_library(
prov_item_id, media_item.media_type)
# mark as library item in internal db
- await self.mass.db.add_to_library(
- media_item.item_id, media_item.media_type, prov_id)
+ await self.mass.db.add_to_library(media_item.item_id,
+ media_item.media_type,
+ prov_id)
return result
async def library_remove(self, media_items: List[MediaItem]):
result = False
for item in media_items:
# make sure we have a database item
- media_item = await self.item(item.item_id, item.media_type, item.provider, lazy=False)
+ media_item = await self.item(item.item_id,
+ item.media_type,
+ item.provider,
+ lazy=False)
if not media_item:
continue
# remove from provider's libraries
result = await self.providers[prov_id].remove_library(
prov_item_id, media_item.media_type)
# mark as library item in internal db
- await self.mass.db.remove_from_library(
- media_item.item_id, media_item.media_type, prov_id)
+ await self.mass.db.remove_from_library(media_item.item_id,
+ media_item.media_type,
+ prov_id)
return result
async def add_playlist_tracks(self, db_playlist_id, tracks: List[Track]):
playlist_prov = playlist.provider_ids[0]
# grab all existing track ids in the playlist so we can check for duplicates
cur_playlist_track_ids = []
- async for item in self.providers[playlist_prov['provider']].playlist_tracks(
+ async for item in self.providers[
+ playlist_prov['provider']].playlist_tracks(
playlist_prov['item_id']):
cur_playlist_track_ids.append(item.item_id)
cur_playlist_track_ids += [i['item_id'] for i in item.provider_ids]
already_exists = True
if already_exists:
continue
- # we can only add a track to a provider playlist if the track is available on that provider
+ # we can only add a track to a provider playlist if track is available on that provider
# this should all be handled in the frontend but these checks are here just to be safe
# a track can contain multiple versions on the same provider
- # simply sort by quality and just add the first one (assuming the track is still available)
+ # simply sort by quality and just add the first one (assuming track is still available)
for track_version in sorted(track.provider_ids,
- key=operator.itemgetter('quality'),
- reverse=True):
+ key=operator.itemgetter('quality'),
+ reverse=True):
if track_version['provider'] == playlist_prov['provider']:
track_ids_to_add.append(track_version['item_id'])
break
elif playlist_prov['provider'] == 'file':
- # the file provider can handle uri's from all providers in the file so simply add the uri
+ # the file provider can handle uri's from all providers so simply add the uri
uri = f'{track_version["provider"]}://{track_version["item_id"]}'
track_ids_to_add.append(uri)
break
track_ids_to_add)
return False
- async def remove_playlist_tracks(self, db_playlist_id, tracks: List[Track]):
+ async def remove_playlist_tracks(self, db_playlist_id,
+ tracks: List[Track]):
''' remove tracks from playlist '''
# we can only edit playlists that are in the database (marked as editable)
playlist = await self.playlist(db_playlist_id, 'database')
prov_playlist_playlist_id,
track_ids_to_remove)
- @run_periodic(3600)
+ @run_periodic(3600*3)
async def __sync_music_providers(self):
''' periodic sync of all music providers '''
for prov_id in self.providers:
]
cur_db_ids = []
async for item in music_provider.get_library_albums():
-
- db_album = await music_provider.album(item.item_id, album_details=item, lazy=False)
+
+ db_album = await music_provider.album(item.item_id,
+ album_details=item,
+ lazy=False)
if not db_album:
LOGGER.error("provider %s album: %s", prov_id, item.__dict__)
cur_db_ids.append(db_album.item_id)
await self.mass.db.add_to_library(db_album.item_id,
MediaType.Album, prov_id)
# precache album tracks
- [item async for item in music_provider.album_tracks(item.item_id)]
+ async for item in music_provider.album_tracks(item.item_id):
+ pass
# process deletions
for db_id in prev_db_ids:
if db_id not in cur_db_ids:
await self.mass.db.add_to_library(db_id, MediaType.Playlist,
prov_id)
# precache playlist tracks
- [item async for item in music_provider.playlist_tracks(item.item_id)]
+ async for item in music_provider.playlist_tracks(item.item_id):
+ pass
# process playlist deletions
for db_id in prev_db_ids:
if db_id not in cur_db_ids:
prov_id)
async def get_image_thumb(self,
- item_id,
- media_type: MediaType,
- provider,
- size=50):
+ item_id,
+ media_type: MediaType,
+ provider,
+ size=50):
''' get path to (resized) thumb image for given media item '''
cache_folder = os.path.join(self.mass.datapath, '.thumbs')
cache_id = f'{item_id}{media_type}{provider}'
elif media_type == MediaType.Track and item.album:
# try album image instead for tracks
return await self.get_image_thumb(item.album.item_id,
- MediaType.Album,
- item.album.provider, size)
+ MediaType.Album,
+ item.album.provider, size)
elif media_type == MediaType.Album and item.artist:
# try artist image instead for albums
return await self.get_image_thumb(item.artist.item_id,
- MediaType.Artist,
- item.artist.provider, size)
+ MediaType.Artist,
+ item.artist.provider, size)
if not img_url:
return None
# fetch image and store in cache
async with session.get(img_url, verify_ssl=False) as response:
assert response.status == 200
img_data = await response.read()
- with open(cache_file_org, 'wb') as f:
- f.write(img_data)
+ with open(cache_file_org, 'wb') as img_file:
+ img_file.write(img_data)
if not size:
# return base image
return cache_file_org
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
-import asyncio
import os
-from enum import Enum
from typing import List
-import operator
-import random
-import functools
-import urllib
-
-from .constants import CONF_KEY_PLAYERPROVIDERS, EVENT_PLAYER_ADDED, EVENT_PLAYER_REMOVED, EVENT_HASS_ENTITY_CHANGED
-from .utils import run_periodic, LOGGER, try_parse_int, try_parse_float, \
- get_ip, run_async_background_task, load_provider_modules, iter_items
-from .models.media_types import MediaItem, MediaType, TrackQuality
+
+from .constants import CONF_KEY_PLAYERPROVIDERS, EVENT_PLAYER_ADDED, \
+ EVENT_PLAYER_REMOVED, EVENT_HASS_ENTITY_CHANGED
+from .utils import LOGGER, load_provider_modules, iter_items
+from .models.media_types import MediaItem, MediaType
from .models.player_queue import QueueItem, QueueOption
-from .models.playerstate import PlayerState
from .models.player import Player
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
-MODULES_PATH = os.path.join(BASE_DIR, "playerproviders" )
+MODULES_PATH = os.path.join(BASE_DIR, "playerproviders")
class PlayerManager():
- ''' several helpers to handle playback through player providers '''
-
+ """ several helpers to handle playback through player providers """
def __init__(self, mass):
self.mass = mass
self._players = {}
self.providers = {}
-
+
async def setup(self):
- ''' async initialize of module '''
+ """ async initialize of module """
# load providers
await self.load_modules()
# register state listener
- await self.mass.add_event_listener(self.handle_mass_events, EVENT_HASS_ENTITY_CHANGED)
+ await self.mass.add_event_listener(self.handle_mass_events,
+ EVENT_HASS_ENTITY_CHANGED)
- async def load_modules(self):
+ async def load_modules(self, reload_module=None):
"""Dynamically (un)load musicprovider modules."""
if reload_module and reload_module in self.providers:
# unload existing module
self.providers.pop(reload_module, None)
LOGGER.info('Unloaded %s module', reload_module)
# load all modules (that are not already loaded)
- await load_provider_modules(self.mass,
- self.providers, CONF_KEY_PLAYERPROVIDERS)
-
+ await load_provider_modules(self.mass, self.providers,
+ CONF_KEY_PLAYERPROVIDERS)
+
@property
def players(self):
- ''' return list of all players '''
+ """ return list of all players """
return self._players.values()
- async def get_player(self, player_id:str):
- ''' return player by id '''
+ async def get_player(self, player_id: str):
+ """ return player by id """
return self._players.get(player_id, None)
- def get_player_sync(self, player_id:str):
- ''' return player by id (non async) '''
+ def get_player_sync(self, player_id: str):
+ """ return player by id (non async) """
return self._players.get(player_id, None)
- async def add_player(self, player:Player):
- ''' register a new player '''
- player._initialized = True
+ async def add_player(self, player: Player):
+ """ register a new player """
+ player.initialized = True
self._players[player.player_id] = player
await self.mass.signal_event(EVENT_PLAYER_ADDED, player.to_dict())
# TODO: turn on player if it was previously turned on ?
- LOGGER.info(f"New player added: {player.player_provider}/{player.player_id}")
+ LOGGER.info("New player added: %s/%s", player.player_provider,
+ player.player_id)
return player
- async def remove_player(self, player_id:str):
- ''' handle a player remove '''
+ async def remove_player(self, player_id: str):
+ """ handle a player remove """
self._players.pop(player_id, None)
- await self.mass.signal_event(EVENT_PLAYER_REMOVED, {"player_id": player_id})
- LOGGER.info(f"Player removed: {player_id}")
+ await self.mass.signal_event(EVENT_PLAYER_REMOVED,
+ {"player_id": player_id})
+ LOGGER.info("Player removed: %s", player_id)
- async def trigger_update(self, player_id:str):
- ''' manually trigger update for a player '''
+ async def trigger_update(self, player_id: str):
+ """ manually trigger update for a player """
if player_id in self._players:
await self._players[player_id].update(force=True)
-
- async def play_media(self,
- player_id:str,
- media_items:List[MediaItem],
- queue_opt:QueueOption='play'):
- '''
+
+ async def play_media(self,
+ player_id: str,
+ media_items: List[MediaItem],
+ queue_opt=QueueOption.Play):
+ """
play media item(s) on the given player
- :param media_item: media item(s) that should be played (Track, Album, Artist, Playlist, Radio)
- single item or list of items
- :param queue_opt:
- QueueOption.Play -> insert new items in queue and start playing at the inserted position
- QueueOption.Replace -> replace queue contents with these items
- QueueOption.Next -> play item(s) after current playing item
- QueueOption.Add -> append new items at end of the queue
- '''
+ :param media_item: media item(s) that should be played (single item or list of items)
+ :param queue_opt:
+ Play -> insert new items in queue and start playing at the inserted position
+ Replace -> replace queue contents with these items
+ Next -> play item(s) after current playing item
+ Add -> append new items at end of the queue
+ """
player = await self.get_player(player_id)
if not player:
return
for media_item in media_items:
# collect tracks to play
if media_item.media_type == MediaType.Artist:
- tracks = self.mass.music.artist_toptracks(media_item.item_id,
- provider=media_item.provider)
+ tracks = self.mass.music.artist_toptracks(
+ media_item.item_id, provider=media_item.provider)
elif media_item.media_type == MediaType.Album:
- tracks = self.mass.music.album_tracks(media_item.item_id,
- provider=media_item.provider)
+ tracks = self.mass.music.album_tracks(
+ media_item.item_id, provider=media_item.provider)
elif media_item.media_type == MediaType.Playlist:
- tracks = self.mass.music.playlist_tracks(media_item.item_id,
- provider=media_item.provider)
+ tracks = self.mass.music.playlist_tracks(
+ media_item.item_id, provider=media_item.provider)
else:
- tracks = iter_items(media_item) # single track
+ tracks = iter_items(media_item) # single track
async for track in tracks:
queue_item = QueueItem(track)
# generate uri for this queue item
- queue_item.uri = 'http://%s:%s/stream/%s/%s'% (
- self.mass.web.local_ip, self.mass.web.http_port, player_id, queue_item.queue_item_id)
+ queue_item.uri = 'http://%s:%s/stream/%s/%s' % (
+ self.mass.web.local_ip, self.mass.web.http_port, player_id,
+ queue_item.queue_item_id)
queue_items.append(queue_item)
-
+
# load items into the queue
- if (queue_opt == QueueOption.Replace or
- (len(queue_items) > 10 and
- queue_opt in [QueueOption.Play, QueueOption.Next])):
+ if (queue_opt == QueueOption.Replace
+ or (len(queue_items) > 10
+ and queue_opt in [QueueOption.Play, QueueOption.Next])):
return await player.queue.load(queue_items)
elif queue_opt == QueueOption.Next:
return await player.queue.insert(queue_items, 1)
return await player.queue.insert(queue_items, 0)
elif queue_opt == QueueOption.Add:
return await player.queue.append(queue_items)
-
+
async def handle_mass_events(self, msg, msg_details=None):
- ''' listen to some events on event bus '''
+ """ listen to some events on event bus """
if msg == EVENT_HASS_ENTITY_CHANGED:
# handle players with hass integration enabled
player_ids = list(self._players.keys())
for player_id in player_ids:
player = self._players[player_id]
- if (msg_details['entity_id'] == player.settings.get('hass_power_entity') or
- msg_details['entity_id'] == player.settings.get('hass_volume_entity')):
+ if (msg_details['entity_id'] == player.settings.get(
+ 'hass_power_entity') or msg_details['entity_id'] ==
+ player.settings.get('hass_volume_entity')):
await player.update()
-
- async def get_gain_correct(self, player_id, item_id, provider_id, replaygain=False):
- ''' get gain correction for given player / track combination '''
+
+ async def get_gain_correct(self, player_id, item_id, provider_id):
+ """ get gain correction for given player / track combination """
player = self._players[player_id]
if not player.settings['volume_normalisation']:
return 0
target_gain = int(player.settings['target_volume'])
fallback_gain = int(player.settings['fallback_gain_correct'])
- track_loudness = await self.mass.db.get_track_loudness(item_id, provider_id)
- if track_loudness == None:
+ track_loudness = await self.mass.db.get_track_loudness(
+ item_id, provider_id)
+ if track_loudness is None:
gain_correct = fallback_gain
else:
gain_correct = target_gain - track_loudness
- gain_correct = round(gain_correct,2)
- LOGGER.debug(f"Loudness level for track {provider_id}/{item_id} is {track_loudness} - calculated replayGain is {gain_correct}")
- return gain_correct
\ No newline at end of file
+ gain_correct = round(gain_correct, 2)
+ LOGGER.debug(
+ "Loudness level for track %s/%s is %s - calculated replayGain is %s",
+ provider_id, item_id, track_loudness, gain_correct)
+ return gain_correct
import json
LOGGER = logging.getLogger('music_assistant')
-from .constants import CONF_KEY_MUSICPROVIDERS, CONF_KEY_PLAYERPROVIDERS, CONF_ENABLED
+from .constants import CONF_KEY_MUSICPROVIDERS, CONF_ENABLED
IS_HASSIO = os.path.isfile('/data/options.json')
+
def run_periodic(period):
def scheduler(fcn):
async def wrapper(*args, **kwargs):
while True:
asyncio.create_task(fcn(*args, **kwargs))
await asyncio.sleep(period)
+
return wrapper
+
return scheduler
+
def filename_from_string(string):
- ''' create filename from unsafe string '''
- keepcharacters = (' ','.','_')
- return "".join(c for c in string if c.isalnum() or c in keepcharacters).rstrip()
+ """ create filename from unsafe string """
+ keepcharacters = (' ', '.', '_')
+ return "".join(c for c in string
+ if c.isalnum() or c in keepcharacters).rstrip()
+
def run_background_task(corofn, *args, executor=None):
- ''' run non-async task in background '''
+ """ run non-async task in background """
return asyncio.get_event_loop().run_in_executor(executor, corofn, *args)
+
def run_async_background_task(executor, corofn, *args):
- ''' run async task in background '''
+ """ run async task in background """
def run_task(corofn, *args):
LOGGER.debug('running %s in background task', corofn.__name__)
new_loop = asyncio.new_event_loop()
new_loop.close()
LOGGER.debug('completed %s in background task', corofn.__name__)
return res
- return asyncio.get_event_loop().run_in_executor(executor, run_task, corofn, *args)
+
+ return asyncio.get_event_loop().run_in_executor(executor, run_task, corofn,
+ *args)
+
def get_sort_name(name):
- ''' create a sort name for an artist/title '''
+ """ create a sort name for an artist/title """
sort_name = name
for item in ["The ", "De ", "de ", "Les "]:
if name.startswith(item):
sort_name = "".join(name.split(item)[1:])
return sort_name
+
def try_parse_int(possible_int):
try:
return int(possible_int)
- except:
+ except (TypeError, ValueError):
return 0
+
async def iter_items(items):
- '''fake async iterator for compatability reasons.'''
+ """fake async iterator for compatability reasons."""
if not isinstance(items, list):
yield items
else:
for item in items:
yield item
+
def try_parse_float(possible_float):
try:
return float(possible_float)
- except:
+ except (TypeError, ValueError):
return 0.0
+
def try_parse_bool(possible_bool):
if isinstance(possible_bool, bool):
return possible_bool
else:
return possible_bool in ['true', 'True', '1', 'on', 'ON', 1]
+
def parse_title_and_version(track_title, track_version=None):
- ''' try to parse clean track title and version from the title '''
+ """ try to parse clean track title and version from the title """
title = track_title.lower()
version = ''
for splitter in [" (", " [", " - ", " (", " [", "-"]:
for end_splitter in [")", "]"]:
if end_splitter in title_part:
title_part = title_part.split(end_splitter)[0]
- for ignore_str in ["feat.", "featuring", "ft.", "with ", " & ", "explicit"]:
+ for ignore_str in [
+ "feat.", "featuring", "ft.", "with ", " & ", "explicit"
+ ]:
if ignore_str in title_part:
- title = title.split(splitter+title_part)[0]
- for version_str in ["version", "live", "edit", "remix", "mix",
- "acoustic", " instrumental", "karaoke", "remaster", "versie", "radio", "unplugged", "disco"]:
+ title = title.split(splitter + title_part)[0]
+ for version_str in [
+ "version", "live", "edit", "remix", "mix", "acoustic",
+ " instrumental", "karaoke", "remaster", "versie",
+ "radio", "unplugged", "disco"
+ ]:
if version_str in title_part:
version = title_part
- title = title.split(splitter+version)[0]
+ title = title.split(splitter + version)[0]
title = title.strip().title()
if not version and track_version:
version = track_version
version = get_version_substitute(version).title()
return title, version
+
def get_version_substitute(version_str):
- ''' transform provider version str to universal version type '''
+ """ transform provider version str to universal version type """
version_str = version_str.lower()
# substitute edit and edition with version
if 'edition' in version_str or 'edit' in version_str:
- version_str = version_str.replace(' edition',' version')
- version_str = version_str.replace(' edit ',' version')
+ version_str = version_str.replace(' edition', ' version')
+ version_str = version_str.replace(' edit ', ' version')
if version_str.startswith('the '):
version_str = version_str.split('the ')[1]
if "radio mix" in version_str:
version_str = 'remaster'
return version_str.strip()
+
+# pylint: disable=broad-except
def get_ip():
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
# doesn't even have to be reachable
s.connect(('10.255.255.255', 1))
IP = s.getsockname()[0]
- except:
+ except Exception:
IP = '127.0.0.1'
finally:
s.close()
return IP
+
+# pylint: enable=broad-except
+
+
def get_hostname():
+ """Get hostname for this machine."""
return socket.gethostname()
+
def get_folder_size(folderpath):
- ''' get folder size in gb'''
+ """ get folder size in gb"""
total_size = 0
+ # pylint: disable=unused-variable
for dirpath, dirnames, filenames in os.walk(folderpath):
for f in filenames:
fp = os.path.join(dirpath, f)
total_size += os.path.getsize(fp)
- total_size_gb = total_size/float(1<<30)
+ # pylint: enable=unused-variable
+ total_size_gb = total_size / float(1 << 30)
return total_size_gb
+
def serialize_values(obj):
- ''' recursively create serializable values for custom data types '''
+ """Recursively create serializable values for (custom) data types."""
def get_val(val):
if isinstance(val, (int, str, bool, float, tuple)):
return val
elif isinstance(val, list):
new_list = []
for item in val:
- new_list.append( get_val(item))
+ new_list.append(get_val(item))
return new_list
elif hasattr(val, 'to_dict'):
return get_val(val.to_dict())
for key, value in val.__dict__.items():
new_dict[key] = get_val(value)
return new_dict
+
return get_val(obj)
-def get_compare_string(str):
- ''' get clean lowered string for compare actions '''
- unaccented_string = unidecode.unidecode(str)
- return re.sub(r"[^a-zA-Z0-9]","",unaccented_string).lower()
+
+def get_compare_string(input_str):
+ """ get clean lowered string for compare actions """
+ unaccented_string = unidecode.unidecode(input_str)
+ return re.sub(r"[^a-zA-Z0-9]", "", unaccented_string).lower()
+
def compare_strings(str1, str2, strict=False):
- ''' compare strings and return True if we have an (almost) perfect match '''
+ """ compare strings and return True if we have an (almost) perfect match """
match = str1.lower() == str2.lower()
if not match and not strict:
match = get_compare_string(str1) == get_compare_string(str2)
return match
+
def json_serializer(obj):
- ''' json serializer to recursively create serializable values for custom data types '''
+ """ json serializer to recursively create serializable values for custom data types """
return json.dumps(serialize_values(obj), skipkeys=True)
def try_load_json_file(jsonfile):
- ''' try to load json from file '''
+ """ try to load json from file """
+ # pylint: disable=broad-except
try:
with open(jsonfile) as f:
return json.loads(f.read())
except Exception as exc:
- LOGGER.debug("Could not load json from file %s - %s" % (jsonfile, str(exc)))
+ LOGGER.debug("Could not load json from file %s",
+ jsonfile,
+ exc_info=exc)
return None
+ # pylint: enable=broad-except
+
-async def load_provider_modules(mass, provider_modules, prov_type=CONF_KEY_MUSICPROVIDERS):
- ''' dynamically load music/player providers '''
+async def load_provider_modules(mass,
+ provider_modules,
+ prov_type=CONF_KEY_MUSICPROVIDERS):
+ """ dynamically load music/player providers """
base_dir = os.path.dirname(os.path.abspath(__file__))
- modules_path = os.path.join(base_dir, prov_type )
+ modules_path = os.path.join(base_dir, prov_type)
# load modules
for item in os.listdir(modules_path):
- if (os.path.isfile(os.path.join(modules_path, item)) and not item.startswith("_") and
- item.endswith('.py') and not item.startswith('.')):
- module_name = item.replace(".py","")
+ if (os.path.isfile(os.path.join(modules_path, item))
+ and not item.startswith("_") and item.endswith('.py')
+ and not item.startswith('.')):
+ module_name = item.replace(".py", "")
if module_name not in provider_modules:
- prov_mod = await load_provider_module(mass, module_name, prov_type)
+ prov_mod = await load_provider_module(mass, module_name,
+ prov_type)
if prov_mod:
provider_modules[module_name] = prov_mod
+
async def load_provider_module(mass, module_name, prov_type):
- ''' dynamically load music/player provider '''
+ """ dynamically load music/player provider """
+ # pylint: disable=broad-except
try:
- prov_mod = importlib.import_module(f".{module_name}",
- f"music_assistant.{prov_type}")
+ prov_mod = importlib.import_module(f".{module_name}",
+ f"music_assistant.{prov_type}")
prov_conf_entries = prov_mod.CONFIG_ENTRIES
prov_id = module_name
prov_name = prov_mod.PROV_NAME
prov_class = prov_mod.PROV_CLASS
# get/create config for the module
- prov_config = mass.config.create_module_config(
- prov_id, prov_conf_entries, prov_type)
+ prov_config = mass.config.create_module_config(prov_id,
+ prov_conf_entries,
+ prov_type)
if prov_config[CONF_ENABLED]:
prov_mod_cls = getattr(prov_mod, prov_class)
provider = prov_mod_cls(mass)
return None
except Exception as exc:
LOGGER.error("Error loading module %s: %s", module_name, exc)
- LOGGER.debug(exc_info=exc)
+ LOGGER.debug("Error loading module", exc_info=exc)
+ # pylint: enable=broad-except