several linter fixes
authormarcelveldt <marcelvanderveldt@MacBook-Pro.local>
Thu, 14 Nov 2019 09:01:59 +0000 (10:01 +0100)
committermarcelveldt <marcelvanderveldt@MacBook-Pro.local>
Thu, 14 Nov 2019 09:01:59 +0000 (10:01 +0100)
music_assistant/models/musicprovider.py
music_assistant/models/player.py
music_assistant/models/player_queue.py
music_assistant/music_manager.py
music_assistant/player_manager.py
music_assistant/utils.py

index f2b68c443bbf1256945beecfc4f894facdf52dec..2649609564794dc528b7b17818c5a12052d533fd 100755 (executable)
@@ -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
index 9789e72f32f4fced3f52faa9354c5b896db9cb2e..3092ad63b39cb385fb13f79b94f4384863f90c8b 100755 (executable)
 #!/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
+        }
index 7b69f418b4107015fda1cca210e935aee848fbbb..e714bec23c0ada9bb8b20c6653bac288fe273935 100755 (executable)
@@ -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)
index d5e66244ae3acfd52300c951c58b74da2fbd2851..d181e15527e3d28761c23aecf582937db8bc15fd 100755 (executable)
@@ -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
index fb26d56488746dd0663d6972a1e5ef5aaed73bc5..fdbd14b3a1417261efa849b4cd321b9588d0520d 100755 (executable)
@@ -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
index 594b38558cde6b4de94b00211c5cad7d48dd94b9..f1b31992c62a21b63cb6faa244fbae2943db93ca 100755 (executable)
@@ -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