From b7f4d0cdca843622583a8ff23c861980e3e88d41 Mon Sep 17 00:00:00 2001 From: marcelveldt Date: Thu, 14 Nov 2019 10:01:59 +0100 Subject: [PATCH] several linter fixes --- music_assistant/models/musicprovider.py | 44 ++-- music_assistant/models/player.py | 264 ++++++++++----------- music_assistant/models/player_queue.py | 292 ++++++++++++++---------- music_assistant/music_manager.py | 86 ++++--- music_assistant/player_manager.py | 151 ++++++------ music_assistant/utils.py | 141 ++++++++---- 6 files changed, 554 insertions(+), 424 deletions(-) diff --git a/music_assistant/models/musicprovider.py b/music_assistant/models/musicprovider.py index f2b68c44..26496095 100755 --- a/music_assistant/models/musicprovider.py +++ b/music_assistant/models/musicprovider.py @@ -4,8 +4,7 @@ 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 @@ -16,7 +15,6 @@ class MusicProvider(): 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 = '' @@ -26,7 +24,7 @@ class MusicProvider(): async def setup(self, conf): """[SHOULD OVERRIDE] Setup the provider""" - return False + LOGGER.debug(conf) ### Common methods and properties #### @@ -42,7 +40,8 @@ class MusicProvider(): 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: @@ -94,8 +93,8 @@ class MusicProvider(): ] 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, @@ -150,8 +149,8 @@ class MusicProvider(): 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 @@ -165,7 +164,8 @@ class MusicProvider(): # 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: @@ -203,7 +203,8 @@ class MusicProvider(): # 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: @@ -263,8 +264,9 @@ class MusicProvider(): 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, @@ -286,9 +288,10 @@ class MusicProvider(): 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, @@ -305,7 +308,8 @@ class MusicProvider(): """ 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) @@ -375,7 +379,8 @@ class MusicProvider(): 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 @@ -427,6 +432,7 @@ class MusicProvider(): """ 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 ! @@ -473,6 +479,8 @@ class MusicProvider(): return yield + # pylint: enable=unreachable + async def get_album(self, prov_album_id) -> Album: """ get full album details by id """ raise NotImplementedError diff --git a/music_assistant/models/player.py b/music_assistant/models/player.py index 9789e72f..3092ad63 100755 --- a/music_assistant/models/player.py +++ b/music_assistant/models/player.py @@ -1,117 +1,121 @@ #!/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 ##### @@ -124,7 +128,7 @@ class Player(): 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 = '' @@ -132,25 +136,25 @@ class Player(): 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: @@ -158,7 +162,7 @@ class Player(): @property def name(self): - ''' [PROTECTED] name of this player ''' + """ [PROTECTED] name of this player """ if self.settings.get('name'): return self.settings['name'] else: @@ -166,37 +170,37 @@ class Player(): @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()) @@ -205,15 +209,15 @@ class Player(): 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()) @@ -222,7 +226,7 @@ class Player(): @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 @@ -233,27 +237,27 @@ class Player(): 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'): @@ -263,14 +267,14 @@ class Player(): @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) @@ -279,8 +283,8 @@ class Player(): 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: @@ -290,12 +294,12 @@ class Player(): @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) @@ -304,21 +308,21 @@ class Player(): 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 @@ -328,15 +332,15 @@ class Player(): # 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 @@ -344,16 +348,16 @@ class Player(): # 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 @@ -361,7 +365,7 @@ class Player(): @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) @@ -370,7 +374,7 @@ class Player(): 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) @@ -379,7 +383,7 @@ class Player(): 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) @@ -391,23 +395,23 @@ class Player(): 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) @@ -416,16 +420,16 @@ class Player(): 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() @@ -433,26 +437,26 @@ class Player(): 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'): @@ -468,7 +472,7 @@ class Player(): 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() @@ -476,23 +480,23 @@ class Player(): 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 @@ -503,7 +507,7 @@ class Player(): 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 @@ -511,14 +515,14 @@ class Player(): 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 @@ -534,43 +538,44 @@ class Player(): 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() @@ -578,7 +583,7 @@ class Player(): @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: @@ -586,15 +591,15 @@ class Player(): 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"), @@ -603,22 +608,24 @@ class Player(): # 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, @@ -635,6 +642,5 @@ class Player(): "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 + } diff --git a/music_assistant/models/player_queue.py b/music_assistant/models/player_queue.py index 7b69f418..e714bec2 100755 --- a/music_assistant/models/player_queue.py +++ b/music_assistant/models/player_queue.py @@ -1,22 +1,28 @@ #!/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" @@ -24,7 +30,7 @@ class QueueOption(str, Enum): 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 = {} @@ -36,12 +42,11 @@ class QueueItem(Track): 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 @@ -56,160 +61,182 @@ class PlayerQueue(): 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: @@ -220,31 +247,33 @@ class PlayerQueue(): 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: @@ -260,9 +289,9 @@ class PlayerQueue(): 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: @@ -273,42 +302,49 @@ class PlayerQueue(): 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: @@ -323,32 +359,38 @@ class PlayerQueue(): 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: @@ -361,7 +403,7 @@ class PlayerQueue(): 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 @@ -369,7 +411,7 @@ class PlayerQueue(): 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 @@ -379,12 +421,12 @@ class PlayerQueue(): 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, @@ -397,11 +439,10 @@ class PlayerQueue(): "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 @@ -409,7 +450,7 @@ class PlayerQueue(): 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] @@ -421,23 +462,28 @@ class PlayerQueue(): 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( @@ -448,23 +494,24 @@ class PlayerQueue(): 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: @@ -474,9 +521,14 @@ class PlayerQueue(): 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, @@ -486,5 +538,5 @@ class PlayerQueue(): "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) diff --git a/music_assistant/music_manager.py b/music_assistant/music_manager.py index d5e66244..d181e155 100755 --- a/music_assistant/music_manager.py +++ b/music_assistant/music_manager.py @@ -39,7 +39,9 @@ def sync_task(desc): 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 @@ -66,8 +68,8 @@ class MusicManager(): 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, @@ -176,7 +178,7 @@ class MusicManager(): 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'] @@ -184,7 +186,7 @@ class MusicManager(): 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]: @@ -195,15 +197,15 @@ class MusicManager(): 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 ''' @@ -253,7 +255,10 @@ class MusicManager(): 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 @@ -264,8 +269,9 @@ class MusicManager(): 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]): @@ -273,7 +279,10 @@ class MusicManager(): 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 @@ -284,8 +293,9 @@ class MusicManager(): 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]): @@ -298,7 +308,8 @@ class MusicManager(): 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] @@ -311,18 +322,18 @@ class MusicManager(): 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 @@ -338,7 +349,8 @@ class MusicManager(): 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') @@ -364,7 +376,7 @@ class MusicManager(): 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: @@ -412,8 +424,10 @@ class MusicManager(): ] 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) @@ -421,7 +435,8 @@ class MusicManager(): 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: @@ -466,7 +481,8 @@ class MusicManager(): 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: @@ -499,10 +515,10 @@ class MusicManager(): 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}' @@ -525,13 +541,13 @@ class MusicManager(): 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 @@ -541,8 +557,8 @@ class MusicManager(): 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 diff --git a/music_assistant/player_manager.py b/music_assistant/player_manager.py index fb26d564..fdbd14b3 100755 --- a/music_assistant/player_manager.py +++ b/music_assistant/player_manager.py @@ -1,43 +1,36 @@ #!/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 @@ -46,56 +39,57 @@ class PlayerManager(): 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 @@ -104,27 +98,28 @@ class PlayerManager(): 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) @@ -132,30 +127,34 @@ class PlayerManager(): 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 diff --git a/music_assistant/utils.py b/music_assistant/utils.py index 594b3855..f1b31992 100755 --- a/music_assistant/utils.py +++ b/music_assistant/utils.py @@ -14,30 +14,37 @@ except ImportError: 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() @@ -47,44 +54,52 @@ def run_async_background_task(executor, corofn, *args): 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 [" (", " [", " - ", " (", " [", "-"]: @@ -95,27 +110,33 @@ def parse_title_and_version(track_title, track_version=None): 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: @@ -128,40 +149,51 @@ def get_version_substitute(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()) @@ -175,60 +207,76 @@ def serialize_values(obj): 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) @@ -241,4 +289,5 @@ async def load_provider_module(mass, module_name, prov_type): 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 -- 2.34.1