From 470b7c1a71feb0163030495ba78c2a9795886587 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 11 Sep 2020 20:17:00 +0200 Subject: [PATCH] linting --- .vscode/settings.json | 5 +- music_assistant/__main__.py | 10 +- music_assistant/app_vars.py | 34 ++- music_assistant/cache.py | 34 +-- music_assistant/config.py | 40 ++- music_assistant/constants.py | 3 +- music_assistant/database.py | 240 +++++++++++----- music_assistant/http_streamer.py | 106 ++++--- music_assistant/mass.py | 34 +-- music_assistant/metadata.py | 53 ++-- music_assistant/models/__init__.py | 1 + music_assistant/models/config_entry.py | 3 +- music_assistant/models/media_types.py | 30 +- music_assistant/models/musicprovider.py | 14 +- music_assistant/models/player.py | 15 +- music_assistant/models/player_queue.py | 144 +++++----- music_assistant/models/playerprovider.py | 124 +++++---- music_assistant/models/provider.py | 12 +- music_assistant/models/streamdetails.py | 9 +- music_assistant/music_manager.py | 259 +++++++++++++----- music_assistant/player_manager.py | 89 ++++-- music_assistant/providers/__init__.py | 1 + .../providers/chromecast/__init__.py | 205 +++----------- music_assistant/providers/chromecast/const.py | 2 - .../providers/chromecast/models.py | 16 +- .../providers/chromecast/player.py | 59 ++-- music_assistant/providers/demo/__init__.py | 3 +- .../providers/demo/demo_musicprovider.py | 1 + .../providers/demo/demo_playerprovider.py | 63 +++-- music_assistant/providers/file/file.py | 12 +- .../providers/home_assistant/__init__.py | 91 ++++-- music_assistant/providers/qobuz/__init__.py | 133 +++++---- music_assistant/providers/sonos/__init__.py | 2 +- music_assistant/providers/sonos/sonos.py | 54 +++- music_assistant/providers/spotify/__init__.py | 124 +++++---- .../providers/squeezebox/__init__.py | 76 +++-- .../providers/squeezebox/constants.py | 2 +- .../providers/squeezebox/discovery.py | 68 +++-- .../providers/squeezebox/socket_client.py | 99 +++---- music_assistant/providers/tunein/__init__.py | 52 ++-- music_assistant/providers/webplayer/todo.py | 7 +- music_assistant/utils.py | 103 +++---- music_assistant/web.py | 137 +++++---- setup.cfg | 3 +- setup.py | 15 +- 45 files changed, 1544 insertions(+), 1043 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 42b4250b..17f512aa 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { - "python.linting.pylintEnabled": true, + "python.linting.pylintEnabled": false, "python.linting.enabled": true, - "python.pythonPath": "venv/bin/python3" + "python.pythonPath": "venv/bin/python3", + "python.linting.flake8Enabled": true } \ No newline at end of file diff --git a/music_assistant/__main__.py b/music_assistant/__main__.py index af48b818..6868b79f 100755 --- a/music_assistant/__main__.py +++ b/music_assistant/__main__.py @@ -1,10 +1,7 @@ """Start Music Assistant.""" import argparse -import asyncio import logging import os -import platform -import sys from aiorun import run from music_assistant.mass import MusicAssistant @@ -27,7 +24,9 @@ def get_arguments(): help="Directory that contains the MusicAssistant configuration", ) parser.add_argument( - "--debug", action="store_true", help="Start MusicAssistant with verbose debug logging" + "--debug", + action="store_true", + help="Start MusicAssistant with verbose debug logging", ) arguments = parser.parse_args() return arguments @@ -65,8 +64,9 @@ def main(): def on_shutdown(loop): logger.info("shutdown requested!") loop.run_until_complete(mass.async_stop()) - + run(mass.async_start(), use_uvloop=True, shutdown_callback=on_shutdown) + if __name__ == "__main__": main() diff --git a/music_assistant/app_vars.py b/music_assistant/app_vars.py index 9d99597f..b0103969 100644 --- a/music_assistant/app_vars.py +++ b/music_assistant/app_vars.py @@ -1 +1,33 @@ -(lambda __g: [[[[None for __g['get_app_var'], get_app_var.__name__ in [(lambda index: (lambda __l: [APP_VARS[__l['index']] for __l['index'] in [(index)]][0])({}), 'get_app_var')]][0] for __g['APP_VARS'] in [(base64.b64decode(VARS_ENC).decode('utf-8').split(','))]][0] for __g['VARS_ENC'] in [(b'OTQyODUyNTY3LDc2MTczMGQzZjk1ZTRhZjA5YWM2M2I5YTM3Y2NjOTZhLDJlYjk2ZjliMzc0OTRiZTE4MjQ5OTlkNTgwMjhhMzA1LFNTcnRNMnhlM2wwMDNnOEh4RmVUUUtub3BaNklCaUwzRTlPc1QxODFYMDA9')]][0] for __g['base64'] in [(__import__('base64', __g, __g))]][0])(globals()) +"""Some magic to store some appvars.""" +# pylint: skip-file +# flake8: noqa +( + lambda __g: [ + [ + [ + [ + None + for __g["get_app_var"], get_app_var.__name__ in [ + ( + lambda index: ( + lambda __l: [ + APP_VARS[__l["index"]] for __l["index"] in [(index)] + ][0] + )({}), + "get_app_var", + ) + ] + ][0] + for __g["APP_VARS"] in [ + (base64.b64decode(VARS_ENC).decode("utf-8").split(",")) + ] + ][0] + for __g["VARS_ENC"] in [ + ( + b"OTQyODUyNTY3LDc2MTczMGQzZjk1ZTRhZjA5YWM2M2I5YTM3Y2NjOTZhLDJlYjk2ZjliMzc0OTRiZTE4MjQ5OTlkNTgwMjhhMzA1LFNTcnRNMnhlM2wwMDNnOEh4RmVUUUtub3BaNklCaUwzRTlPc1QxODFYMDA9" + ) + ] + ][0] + for __g["base64"] in [(__import__("base64", __g, __g))] + ][0] +)(globals()) diff --git a/music_assistant/cache.py b/music_assistant/cache.py index 84436be0..97e28c74 100644 --- a/music_assistant/cache.py +++ b/music_assistant/cache.py @@ -14,7 +14,7 @@ LOGGER = logging.getLogger("mass") class Cache(object): - """basic stateless caching system.""" + """Basic stateless caching system.""" _db = None @@ -35,13 +35,13 @@ class Cache(object): await db_conn.commit() self.mass.add_job(self.async_auto_cleanup()) - async def async_get(self, cache_key, checksum=""): """ - get object from cache and return the results - cache_key: the (unique) name of the cache object as reference - checkum: optional argument to check if the checksum in the - cacheobject matches the checkum provided + Get object from cache and return the results. + + cache_key: the (unique) name of the cache object as reference + checkum: optional argument to check if the checksum in the + cacheobject matches the checkum provided """ result = None cur_time = int(time.time()) @@ -64,9 +64,7 @@ class Cache(object): return result async def async_set(self, cache_key, data, checksum="", expiration=(86400 * 30)): - """ - set data in cache - """ + """Set data in cache.""" checksum = self._get_checksum(checksum) expires = int(time.time() + expiration) data = pickle.dumps(data) @@ -78,7 +76,7 @@ class Cache(object): @run_periodic(3600) async def async_auto_cleanup(self): - """(scheduled) auto cleanup task""" + """Sceduled auto cleanup task.""" cur_timestamp = int(time.time()) LOGGER.debug("Running cleanup...") sql_query = "SELECT id, expires FROM simplecache" @@ -99,7 +97,7 @@ class Cache(object): @staticmethod def _get_checksum(stringinput): - """get int checksum from string""" + """Get int checksum from string.""" if not stringinput: return 0 else: @@ -107,8 +105,10 @@ class Cache(object): return reduce(lambda x, y: x + y, map(ord, stringinput)) -async def async_cached_generator(cache, cache_key, coro_func, expires=(86400 * 30), checksum=None): - """Helper method to store results of a async generator in the cache.""" +async def async_cached_generator( + cache, cache_key, coro_func, expires=(86400 * 30), checksum=None +): + """Return helper method to store results of a async generator in the cache.""" cache_result = await cache.async_get(cache_key, checksum) if cache_result is not None: for item in cache_result: @@ -123,8 +123,10 @@ async def async_cached_generator(cache, cache_key, coro_func, expires=(86400 * 3 await cache.async_set(cache_key, cache_result, checksum, expires) -async def async_cached(cache, cache_key, coro_func, expires=(86400 * 30), checksum=None): - """Helper method to store results of a coroutine in the cache.""" +async def async_cached( + cache, cache_key, coro_func, expires=(86400 * 30), checksum=None +): + """Return helper method to store results of a coroutine in the cache.""" cache_result = await cache.async_get(cache_key, checksum) # normal async function if cache_result is not None: @@ -135,7 +137,7 @@ async def async_cached(cache, cache_key, coro_func, expires=(86400 * 30), checks def async_use_cache(cache_days=14, cache_checksum=None): - """Decorator that can be used to cache a method's result.""" + """Return decorator that can be used to cache a method's result.""" def wrapper(func): @functools.wraps(func) diff --git a/music_assistant/config.py b/music_assistant/config.py index 6084cf00..cc2a1e30 100755 --- a/music_assistant/config.py +++ b/music_assistant/config.py @@ -1,6 +1,5 @@ """All classes and helpers for the Configuration.""" -import base64 import logging import os import shutil @@ -9,7 +8,7 @@ from enum import Enum from typing import List from cryptography.fernet import Fernet, InvalidToken -from music_assistant.app_vars import get_app_var +from music_assistant.app_vars import get_app_var # noqa from music_assistant.constants import ( CONF_CROSSFADE_DURATION, CONF_ENABLED, @@ -20,8 +19,6 @@ from music_assistant.constants import ( CONF_NAME, EVENT_CONFIG_CHANGED, ) - -# from music_assistant.mass import MusicAssistant from music_assistant.models.config_entry import ConfigEntry, ConfigEntryType from music_assistant.utils import get_external_ip, json, try_load_json_file from passlib.hash import pbkdf2_sha256 @@ -150,16 +147,19 @@ class ConfigBaseType(Enum): class ConfigItem: """ Configuration Item connected to Config Entries. + Returns default value from config entry if no value present. """ def __init__(self, mass, parent_item_key: str, base_type: ConfigBaseType): + """Initialize class.""" self._parent_item_key = parent_item_key self._base_type = base_type self.mass = mass self.stored_config = OrderedDict() def __repr__(self): + """Print class.""" return f"{OrderedDict}({self.items()})" def items(self) -> dict: @@ -218,12 +218,16 @@ class ConfigItem: raise ValueError else: # single value item - if entry.entry_type == ConfigEntryType.STRING and not isinstance(value, str): + if entry.entry_type == ConfigEntryType.STRING and not isinstance( + value, str + ): if not value: value = "" else: raise ValueError - if entry.entry_type == ConfigEntryType.BOOL and not isinstance(value, bool): + if entry.entry_type == ConfigEntryType.BOOL and not isinstance( + value, bool + ): raise ValueError if entry.entry_type == ConfigEntryType.FLOAT and not isinstance( value, (float, int) @@ -248,7 +252,9 @@ class ConfigItem: player = self.mass.player_manager.get_player(self._parent_item_key) if player: - self.mass.add_job(self.mass.player_manager.async_update_player(player)) + self.mass.add_job( + self.mass.player_manager.async_update_player(player) + ) return # raise KeyError if we're trying to set a value not defined as ConfigEntry raise KeyError @@ -266,15 +272,18 @@ class ConfigBase(OrderedDict): """Configuration class with ConfigItem items.""" def __init__(self, mass, base_type=ConfigBaseType): + """Initialize class.""" self.mass = mass self._base_type = base_type super().__init__() def __getitem__(self, item_key): - """Convenience method for get.""" - if not item_key in self: + """Return convenience method for get.""" + if item_key not in self: # create new ConfigDictItem on the fly - super().__setitem__(item_key, ConfigItem(self.mass, item_key, self._base_type)) + super().__setitem__( + item_key, ConfigItem(self.mass, item_key, self._base_type) + ) return super().__getitem__(item_key) @@ -282,6 +291,7 @@ class MassConfig: """Class which holds our configuration.""" def __init__(self, mass, data_path: str): + """Initialize class.""" self._data_path = data_path self.loading = False self.mass = mass @@ -347,7 +357,7 @@ class MassConfig: return pbkdf2_sha256.verify(password, self.base["security"]["password"]) def __getitem__(self, item_key): - """Convenience method for get.""" + """Return item value by key.""" return getattr(self, item_key) async def async_close(self): @@ -366,7 +376,11 @@ class MassConfig: if os.path.isfile(conf_file): shutil.move(conf_file, conf_file_backup) # create dict for stored config - stored_conf = {CONF_KEY_BASE: {}, CONF_KEY_PLAYERSETTINGS: {}, CONF_KEY_PROVIDERS: {}} + stored_conf = { + CONF_KEY_BASE: {}, + CONF_KEY_PLAYERSETTINGS: {}, + CONF_KEY_PROVIDERS: {}, + } for conf_key in stored_conf: for key, value in self[conf_key].items(): stored_conf[conf_key][key] = value.stored_config @@ -378,7 +392,7 @@ class MassConfig: self.loading = False def __load(self): - """load config from file""" + """Load config from file.""" self.loading = True conf_file = os.path.join(self.data_path, "config.json") data = try_load_json_file(conf_file) diff --git a/music_assistant/constants.py b/music_assistant/constants.py index 3056d156..034b664d 100755 --- a/music_assistant/constants.py +++ b/music_assistant/constants.py @@ -15,7 +15,6 @@ CONF_CROSSFADE_DURATION = "crossfade_duration" CONF_FALLBACK_GAIN_CORRECT = "fallback_gain_correct" - CONF_KEY_BASE = "base" CONF_KEY_PLAYERSETTINGS = "player_settings" CONF_KEY_PROVIDERS = "providers" @@ -34,4 +33,4 @@ EVENT_QUEUE_ITEMS_UPDATED = "queue items updated" EVENT_SHUTDOWN = "application shutdown" EVENT_PROVIDER_REGISTERED = "provider registered" EVENT_PLAYER_CONTROL_REGISTERED = "player control registered" -EVENT_PLAYER_CONTROL_UPDATED = "player control updated" \ No newline at end of file +EVENT_PLAYER_CONTROL_UPDATED = "player control updated" diff --git a/music_assistant/database.py b/music_assistant/database.py index 14829c4c..2ce68875 100755 --- a/music_assistant/database.py +++ b/music_assistant/database.py @@ -1,11 +1,10 @@ """Database logic.""" # pylint: disable=too-many-lines -import asyncio import logging import os import sqlite3 -from typing import List from functools import partial +from typing import List import aiosqlite from music_assistant.models.media_types import ( @@ -22,25 +21,28 @@ from music_assistant.models.media_types import ( Track, TrackQuality, ) -from music_assistant.utils import get_sort_name, try_parse_int, compare_strings +from music_assistant.utils import compare_strings, get_sort_name, try_parse_int LOGGER = logging.getLogger("mass") -import contextlib - class DbConnect: + """Helper to initialize the db connection or utilize an existing one.""" + def __init__(self, dbfile: str, db_conn: sqlite3.Connection = None): + """Initialize class.""" self._db_conn_provided = db_conn is not None self._db_conn = db_conn self._dbfile = dbfile async def __aenter__(self): + """Enter.""" if not self._db_conn_provided: self._db_conn = await aiosqlite.connect(self._dbfile, timeout=120) return self._db_conn async def __aexit__(self, exc_type, exc_value, traceback): + """Exit.""" if not self._db_conn_provided: await self._db_conn.close() return False @@ -50,6 +52,7 @@ class Database: """Class that holds the (logic to the) database.""" def __init__(self, mass): + """Initialize class.""" self.mass = mass self._dbfile = os.path.join(mass.config.data_path, "database.db") self.db_conn = partial(DbConnect, self._dbfile) @@ -177,7 +180,9 @@ class Database: return item_id[0] return None - async def async_search(self, searchquery: str, media_types: List[MediaType]) -> SearchResult: + async def async_search( + self, searchquery: str, media_types: List[MediaType] + ) -> SearchResult: """Search library for the given searchphrase.""" async with DbConnect(self._dbfile) as db_conn: result = SearchResult([], [], [], [], []) @@ -185,27 +190,34 @@ class Database: if media_types is None or MediaType.Artist in media_types: sql_query = ' WHERE name LIKE "%s"' % searchquery result.artists = [ - item async for item in self.async_get_artists(sql_query, db_conn=db_conn) + item + async for item in self.async_get_artists(sql_query, db_conn=db_conn) ] if media_types is None or MediaType.Album in media_types: sql_query = ' WHERE name LIKE "%s"' % searchquery result.albums = [ - item async for item in self.async_get_albums(sql_query, db_conn=db_conn) + item + async for item in self.async_get_albums(sql_query, db_conn=db_conn) ] if media_types is None or MediaType.Track in media_types: sql_query = ' WHERE name LIKE "%s"' % searchquery result.tracks = [ - item async for item in self.async_get_tracks(sql_query, db_conn=db_conn) + item + async for item in self.async_get_tracks(sql_query, db_conn=db_conn) ] if media_types is None or MediaType.Playlist in media_types: sql_query = ' WHERE name LIKE "%s"' % searchquery result.playlists = [ - item async for item in self.async_get_playlists(sql_query, db_conn=db_conn) + item + async for item in self.async_get_playlists( + sql_query, db_conn=db_conn + ) ] if media_types is None or MediaType.Radio in media_types: sql_query = ' WHERE name LIKE "%s"' % searchquery result.radios = [ - item async for item in self.async_get_radios(sql_query, db_conn=db_conn) + item + async for item in self.async_get_radios(sql_query, db_conn=db_conn) ] return result @@ -218,9 +230,11 @@ class Database: provider = "{provider_id}" AND media_type = {int(MediaType.Artist)})""" else: sql_query = f"""WHERE artist_id in - (SELECT item_id FROM library_items + (SELECT item_id FROM library_items WHERE media_type = {int(MediaType.Artist)})""" - async for item in self.async_get_artists(sql_query, orderby=orderby, fulldata=True): + async for item in self.async_get_artists( + sql_query, orderby=orderby, fulldata=True + ): yield item async def async_get_library_albums( @@ -233,7 +247,9 @@ class Database: else: sql_query = f"""WHERE album_id in (SELECT item_id FROM library_items WHERE media_type = {int(MediaType.Album)})""" - async for item in self.async_get_albums(sql_query, orderby=orderby, fulldata=True): + async for item in self.async_get_albums( + sql_query, orderby=orderby, fulldata=True + ): yield item async def async_get_library_tracks( @@ -242,7 +258,7 @@ class Database: """Get all library tracks, optionally filtered by provider.""" if provider_id is not None: sql_query = f"""WHERE track_id in - (SELECT item_id FROM library_items WHERE provider = "{provider_id}" + (SELECT item_id FROM library_items WHERE provider = "{provider_id}" AND media_type = {int(MediaType.Track)})""" else: sql_query = f"""WHERE track_id in @@ -279,7 +295,10 @@ class Database: yield item async def async_get_playlists( - self, filter_query: str = None, orderby: str = "name", db_conn: sqlite3.Connection = None + self, + filter_query: str = None, + orderby: str = "name", + db_conn: sqlite3.Connection = None, ) -> List[Playlist]: """Get all playlists from database.""" async with DbConnect(self._dbfile, db_conn) as db_conn: @@ -321,12 +340,17 @@ class Database: async def async_get_playlist(self, playlist_id: int) -> Playlist: """Get playlist record by id.""" playlist_id = try_parse_int(playlist_id) - async for item in self.async_get_playlists(f"WHERE playlist_id = {playlist_id}"): + async for item in self.async_get_playlists( + f"WHERE playlist_id = {playlist_id}" + ): return item return None async def async_get_radios( - self, filter_query: str = None, orderby: str = "name", db_conn: sqlite3.Connection = None + self, + filter_query: str = None, + orderby: str = "name", + db_conn: sqlite3.Connection = None, ) -> List[Radio]: """Fetch radio records from database.""" sql_query = "SELECT * FROM radios" @@ -345,7 +369,9 @@ class Database: metadata=await self.__async_get_metadata( db_row["radio_id"], MediaType.Radio, db_conn ), - tags=await self.__async_get_tags(db_row["radio_id"], MediaType.Radio, db_conn), + tags=await self.__async_get_tags( + db_row["radio_id"], MediaType.Radio, db_conn + ), external_ids=await self.__async_get_external_ids( db_row["radio_id"], MediaType.Radio, db_conn ), @@ -402,7 +428,9 @@ class Database: async with db_conn.execute(sql_query, (last_row_id,)) as cursor: playlist_id = await cursor.fetchone() playlist_id = playlist_id[0] - LOGGER.debug("added playlist %s to database: %s", playlist.name, playlist_id) + LOGGER.debug( + "added playlist %s to database: %s", playlist.name, playlist_id + ) # add/update metadata await self.__async_add_prov_ids( playlist_id, MediaType.Playlist, playlist.provider_ids, db_conn @@ -415,7 +443,7 @@ class Database: return playlist_id async def async_add_radio(self, radio: Radio): - """add a new radio record to the database.""" + """Add a new radio record to the database.""" assert radio.name async with DbConnect(self._dbfile) as db_conn: async with db_conn.execute( @@ -435,15 +463,23 @@ class Database: async with db_conn.execute(sql_query, (last_row_id,)) as cursor: radio_id = await cursor.fetchone() radio_id = radio_id[0] - LOGGER.debug("added radio station %s to database: %s", radio.name, radio_id) + LOGGER.debug( + "added radio station %s to database: %s", radio.name, radio_id + ) # add/update metadata - await self.__async_add_prov_ids(radio_id, MediaType.Radio, radio.provider_ids, db_conn) - await self.__async_add_metadata(radio_id, MediaType.Radio, radio.metadata, db_conn) + await self.__async_add_prov_ids( + radio_id, MediaType.Radio, radio.provider_ids, db_conn + ) + await self.__async_add_metadata( + radio_id, MediaType.Radio, radio.metadata, db_conn + ) # save await db_conn.commit() return radio_id - async def async_add_to_library(self, item_id: int, media_type: MediaType, provider: str): + async def async_add_to_library( + self, item_id: int, media_type: MediaType, provider: str + ): """Add an item to the library (item must already be present in the db!).""" async with DbConnect(self._dbfile) as db_conn: item_id = try_parse_int(item_id) @@ -452,7 +488,9 @@ class Database: await db_conn.execute(sql_query, (item_id, provider, media_type)) await db_conn.commit() - async def async_remove_from_library(self, item_id: int, media_type: MediaType, provider: str): + async def async_remove_from_library( + self, item_id: int, media_type: MediaType, provider: str + ): """Remove item from the library.""" async with DbConnect(self._dbfile) as db_conn: item_id = try_parse_int(item_id) @@ -545,8 +583,12 @@ class Database: await self.__async_add_prov_ids( artist_id, MediaType.Artist, artist.provider_ids, db_conn ) - await self.__async_add_metadata(artist_id, MediaType.Artist, artist.metadata, db_conn) - await self.__async_add_tags(artist_id, MediaType.Artist, artist.tags, db_conn) + await self.__async_add_metadata( + artist_id, MediaType.Artist, artist.metadata, db_conn + ) + await self.__async_add_tags( + artist_id, MediaType.Artist, artist.tags, db_conn + ) await self.__async_add_external_ids( artist_id, MediaType.Artist, artist.external_ids, db_conn ) @@ -602,13 +644,15 @@ class Database: album.tags = await self.__async_get_tags( album.item_id, MediaType.Album, db_conn ) - album.labels = await self.__async_get_album_labels(album.item_id, db_conn) + album.labels = await self.__async_get_album_labels( + album.item_id, db_conn + ) yield album async def async_get_album( self, album_id: int, fulldata=True, db_conn: sqlite3.Connection = None ) -> Album: - """get album record by id""" + """Get album record by id.""" album_id = try_parse_int(album_id) async for item in self.async_get_albums( "WHERE album_id = %d" % album_id, fulldata=fulldata, db_conn=db_conn @@ -646,7 +690,9 @@ class Database: if not album_id: sql_query = """SELECT album_id, year, version, albumtype FROM albums WHERE artist_id=? AND name=?""" - async with db_conn.execute(sql_query, (album.artist.item_id, album.name)) as cursor: + async with db_conn.execute( + sql_query, (album.artist.item_id, album.name) + ) as cursor: albums = await cursor.fetchall() for result in albums: if (not album.version and result["year"] == album.year) or ( @@ -675,8 +721,12 @@ class Database: await db_conn.commit() # always add metadata and tags etc. because we might have received # additional info or a match from other provider - await self.__async_add_prov_ids(album_id, MediaType.Album, album.provider_ids, db_conn) - await self.__async_add_metadata(album_id, MediaType.Album, album.metadata, db_conn) + await self.__async_add_prov_ids( + album_id, MediaType.Album, album.provider_ids, db_conn + ) + await self.__async_add_metadata( + album_id, MediaType.Album, album.metadata, db_conn + ) await self.__async_add_tags(album_id, MediaType.Album, album.tags, db_conn) await self.__async_add_album_labels(album_id, album.labels, db_conn) await self.__async_add_external_ids( @@ -738,7 +788,9 @@ class Database: ) yield track - async def async_get_track(self, track_id: int, fulldata=True, db_conn: sqlite3.Connection = None) -> Track: + async def async_get_track( + self, track_id: int, fulldata=True, db_conn: sqlite3.Connection = None + ) -> Track: """Get track record by id.""" track_id = try_parse_int(track_id) async for item in self.async_get_tracks( @@ -760,14 +812,18 @@ class Database: track_id = await self.__async_get_item_by_external_id(track, db_conn) # fallback to matching on album_id, name and version if not track_id: - sql_query = ( - "SELECT track_id, duration, version FROM tracks WHERE album_id=? AND name=?" - ) - async with db_conn.execute(sql_query, (track.album.item_id, track.name)) as cursor: + sql_query = "SELECT track_id, duration, version \ + FROM tracks WHERE album_id=? AND name=?" + async with db_conn.execute( + sql_query, (track.album.item_id, track.name) + ) as cursor: results = await cursor.fetchall() for result in results: # we perform an additional safety check on the duration or version - if (track.version and compare_strings(result["version"], track.version)) or ( + if ( + track.version + and compare_strings(result["version"], track.version) + ) or ( ( not track.version and not result["version"] @@ -779,9 +835,8 @@ class Database: # no match found: insert track if not track_id: assert track.name and track.album.item_id - sql_query = ( - "INSERT INTO tracks (name, album_id, duration, version) VALUES(?,?,?,?);" - ) + sql_query = "INSERT INTO tracks (name, album_id, duration, version) \ + VALUES(?,?,?,?);" query_params = ( track.name, track.album.item_id, @@ -802,8 +857,12 @@ class Database: for artist in track.artists: sql_query = "INSERT or IGNORE INTO track_artists (track_id, artist_id) VALUES(?,?);" await db_conn.execute(sql_query, (track_id, artist.item_id)) - await self.__async_add_prov_ids(track_id, MediaType.Track, track.provider_ids, db_conn) - await self.__async_add_metadata(track_id, MediaType.Track, track.metadata, db_conn) + await self.__async_add_prov_ids( + track_id, MediaType.Track, track.provider_ids, db_conn + ) + await self.__async_add_metadata( + track_id, MediaType.Track, track.metadata, db_conn + ) await self.__async_add_tags(track_id, MediaType.Track, track.tags, db_conn) await self.__async_add_external_ids( track_id, MediaType.Track, track.external_ids, db_conn @@ -818,28 +877,40 @@ class Database: ) return track_id - async def async_update_playlist(self, playlist_id: int, column_key: str, column_value: str): + async def async_update_playlist( + self, playlist_id: int, column_key: str, column_value: str + ): """Update column of existing playlist.""" async with DbConnect(self._dbfile) as db_conn: sql_query = f"UPDATE playlists SET {column_key}=? WHERE playlist_id=?;" await db_conn.execute(sql_query, (column_value, playlist_id)) await db_conn.commit() - async def async_get_artist_tracks(self, artist_id: int, orderby: str = "name") -> List[Track]: - """get all library tracks for the given artist""" + async def async_get_artist_tracks( + self, artist_id: int, orderby: str = "name" + ) -> List[Track]: + """Get all library tracks for the given artist.""" artist_id = try_parse_int(artist_id) sql_query = f"""WHERE track_id in (SELECT track_id FROM track_artists WHERE artist_id = {artist_id})""" - async for item in self.async_get_tracks(sql_query, orderby=orderby, fulldata=False): + async for item in self.async_get_tracks( + sql_query, orderby=orderby, fulldata=False + ): yield item - async def async_get_artist_albums(self, artist_id: int, orderby: str = "name") -> List[Album]: - """get all library albums for the given artist""" + async def async_get_artist_albums( + self, artist_id: int, orderby: str = "name" + ) -> List[Album]: + """Get all library albums for the given artist.""" sql_query = " WHERE artist_id = %s" % artist_id - async for item in self.async_get_albums(sql_query, orderby=orderby, fulldata=False): + async for item in self.async_get_albums( + sql_query, orderby=orderby, fulldata=False + ): yield item - async def async_set_track_loudness(self, provider_track_id: str, provider: str, loudness: int): + async def async_set_track_loudness( + self, provider_track_id: str, provider: str, loudness: int + ): """Set integrated loudness for a track in db.""" async with DbConnect(self._dbfile) as db_conn: sql_query = """INSERT or REPLACE INTO track_loudness @@ -852,14 +923,20 @@ class Database: async with DbConnect(self._dbfile) as db_conn: sql_query = """SELECT loudness FROM track_loudness WHERE provider_track_id = ? AND provider = ?""" - async with db_conn.execute(sql_query, (provider_track_id, provider)) as cursor: + async with db_conn.execute( + sql_query, (provider_track_id, provider) + ) as cursor: result = await cursor.fetchone() if result: return result[0] return None async def __async_add_metadata( - self, item_id: int, media_type: MediaType, metadata: dict, db_conn: sqlite3.Connection + self, + item_id: int, + media_type: MediaType, + metadata: dict, + db_conn: sqlite3.Connection, ): """Add or update metadata.""" for key, value in metadata.items(): @@ -877,7 +954,9 @@ class Database: ) -> dict: """Get metadata for media item.""" metadata = {} - sql_query = "SELECT key, value FROM metadata WHERE item_id = ? AND media_type = ?" + sql_query = ( + "SELECT key, value FROM metadata WHERE item_id = ? AND media_type = ?" + ) if filter_key: sql_query += ' AND key = "%s"' % filter_key async with db_conn.execute(sql_query, (item_id, media_type)) as cursor: @@ -889,9 +968,13 @@ class Database: return metadata async def __async_add_tags( - self, item_id: int, media_type: MediaType, tags: List[str], db_conn: sqlite3.Connection + self, + item_id: int, + media_type: MediaType, + tags: List[str], + db_conn: sqlite3.Connection, ): - """add tags to db""" + """Add tags to db.""" for tag in tags: sql_query = "INSERT or IGNORE INTO tags (name) VALUES(?);" async with db_conn.execute(sql_query, (tag,)) as cursor: @@ -903,7 +986,7 @@ class Database: async def __async_get_tags( self, item_id: int, media_type: MediaType, db_conn: sqlite3.Connection ) -> List[str]: - """get tags for media item""" + """Get tags for media item.""" tags = [] sql_query = """SELECT name FROM tags INNER JOIN media_tags ON tags.tag_id = media_tags.tag_id WHERE item_id = ? AND media_type = ?""" @@ -916,18 +999,20 @@ class Database: async def __async_add_album_labels( self, album_id: int, labels: List[str], db_conn: sqlite3.Connection ): - """add labels to album in db""" + """Add labels to album in db.""" for label in labels: sql_query = "INSERT or IGNORE INTO labels (name) VALUES(?);" async with db_conn.execute(sql_query, (label,)) as cursor: label_id = cursor.lastrowid - sql_query = "INSERT or IGNORE INTO album_labels (album_id, label_id) VALUES(?,?);" + sql_query = ( + "INSERT or IGNORE INTO album_labels (album_id, label_id) VALUES(?,?);" + ) await db_conn.execute(sql_query, (album_id, label_id)) async def __async_get_album_labels( self, album_id: int, db_conn: sqlite3.Connection ) -> List[str]: - """get labels for album item""" + """Get labels for album item.""" labels = [] sql_query = """SELECT name FROM labels INNER JOIN album_labels ON labels.label_id = album_labels.label_id WHERE album_id = ?""" @@ -940,20 +1025,26 @@ class Database: async def __async_get_track_artists( self, track_id: int, db_conn: sqlite3.Connection, fulldata: bool = False ) -> List[Artist]: - """get artists for track""" + """Get artists for track.""" sql_query = ( "WHERE artist_id in (SELECT artist_id FROM track_artists WHERE track_id = %s)" % track_id ) return [ item - async for item in self.async_get_artists(sql_query, fulldata=fulldata, db_conn=db_conn) + async for item in self.async_get_artists( + sql_query, fulldata=fulldata, db_conn=db_conn + ) ] async def __async_add_external_ids( - self, item_id: int, media_type: MediaType, external_ids: dict, db_conn: sqlite3.Connection + self, + item_id: int, + media_type: MediaType, + external_ids: dict, + db_conn: sqlite3.Connection, ): - """add or update external_ids""" + """Add or update external_ids.""" for key, value in external_ids.items(): sql_query = """INSERT or REPLACE INTO external_ids (item_id, media_type, key, value) VALUES(?,?,?,?);""" @@ -962,9 +1053,11 @@ class Database: async def __async_get_external_ids( self, item_id: int, media_type: MediaType, db_conn: sqlite3.Connection ) -> dict: - """get external_ids for media item""" + """Get external_ids for media item.""" external_ids = {} - sql_query = "SELECT key, value FROM external_ids WHERE item_id = ? AND media_type = ?" + sql_query = ( + "SELECT key, value FROM external_ids WHERE item_id = ? AND media_type = ?" + ) for db_row in await db_conn.execute_fetchall(sql_query, (item_id, media_type)): external_ids[db_row[0]] = db_row[1] return external_ids @@ -1017,8 +1110,12 @@ class Database: ) -> List[str]: """Get the providers that have this media_item added to the library.""" providers = [] - sql_query = "SELECT provider FROM library_items WHERE item_id = ? AND media_type = ?" - for db_row in await db_conn.execute_fetchall(sql_query, (db_item_id, media_type)): + sql_query = ( + "SELECT provider FROM library_items WHERE item_id = ? AND media_type = ?" + ) + for db_row in await db_conn.execute_fetchall( + sql_query, (db_item_id, media_type) + ): providers.append(db_row[0]) return providers @@ -1027,9 +1124,8 @@ class Database: ) -> int: """Try to get existing item in db by matching the new item's external id's.""" for key, value in media_item.external_ids.items(): - sql_query = ( - "SELECT (item_id) FROM external_ids WHERE media_type=? AND key=? AND value=?;" - ) + sql_query = "SELECT (item_id) FROM external_ids \ + WHERE media_type=? AND key=? AND value=?;" for db_row in await db_conn.execute_fetchall( sql_query, (media_item.media_type, key, value) ): diff --git a/music_assistant/http_streamer.py b/music_assistant/http_streamer.py index 2e5bfad7..0edceaa7 100755 --- a/music_assistant/http_streamer.py +++ b/music_assistant/http_streamer.py @@ -1,10 +1,10 @@ """ - HTTPStreamer: handles all audio streaming to players, - either by sending tracks one by one or send one continuous stream - of music with crossfade/gapless support (queue stream). +HTTPStreamer: handles all audio streaming to players. + +Either by sending tracks one by one or send one continuous stream +of music with crossfade/gapless support (queue stream). """ import asyncio -import concurrent import gc import io import logging @@ -13,7 +13,6 @@ import shlex import signal import subprocess import threading -from asyncio import CancelledError from contextlib import suppress import pyloudnorm @@ -32,6 +31,7 @@ class HTTPStreamer: """Built-in streamer using sox and webserver.""" def __init__(self, mass): + """Initialize class.""" self.mass = mass self.local_ip = get_ip() self.analyze_jobs = {} @@ -39,9 +39,7 @@ class HTTPStreamer: @require_local_subnet async def async_stream(self, http_request): - """ - start stream for a player - """ + """Start stream for a player.""" # make sure we have valid params player_id = http_request.match_info.get("player_id", "") player_queue = self.mass.player_manager.get_player_queue(player_id) @@ -53,7 +51,9 @@ class HTTPStreamer: if not queue_item: return web.Response(status=404, reason="Invalid Queue item Id") # prepare headers as audio/flac content - resp = web.StreamResponse(status=200, reason="OK", headers={"Content-Type": "audio/flac"}) + resp = web.StreamResponse( + status=200, reason="OK", headers={"Content-Type": "audio/flac"} + ) await resp.prepare(http_request) # run the streamer in executor to prevent the subprocess locking up our eventloop cancelled = threading.Event() @@ -63,7 +63,12 @@ class HTTPStreamer: ) else: bg_task = self.mass.loop.run_in_executor( - None, self.__get_queue_item_stream, player_id, queue_item, resp, cancelled + None, + self.__get_queue_item_stream, + player_id, + queue_item, + resp, + cancelled, ) # let the streaming begin! try: @@ -74,21 +79,25 @@ class HTTPStreamer: return resp def __get_queue_item_stream(self, player_id, queue_item, buffer, cancelled): - """start streaming single queue track""" + """Start streaming single queue track.""" # pylint: disable=unused-variable LOGGER.debug( "stream single queue track started for track %s on player %s", queue_item.name, player_id, ) - for is_last_chunk, audio_chunk in self.__get_audio_stream(player_id, queue_item, cancelled): + for is_last_chunk, audio_chunk in self.__get_audio_stream( + player_id, queue_item, cancelled + ): if cancelled.is_set(): # http session ended # we must consume the data to prevent hanging subprocess instances continue # put chunk in buffer with suppress((BrokenPipeError, ConnectionResetError)): - asyncio.run_coroutine_threadsafe(buffer.write(audio_chunk), self.mass.loop).result() + asyncio.run_coroutine_threadsafe( + buffer.write(audio_chunk), self.mass.loop + ).result() # all chunks received: streaming finished if cancelled.is_set(): LOGGER.debug( @@ -99,10 +108,14 @@ class HTTPStreamer: else: # indicate EOF if no more data with suppress((BrokenPipeError, ConnectionResetError)): - asyncio.run_coroutine_threadsafe(buffer.write_eof(), self.mass.loop).result() + asyncio.run_coroutine_threadsafe( + buffer.write_eof(), self.mass.loop + ).result() LOGGER.debug( - "stream single track finished for track %s on player %s", queue_item.name, player_id + "stream single track finished for track %s on player %s", + queue_item.name, + player_id, ) def __get_queue_stream(self, player_id, buffer, cancelled): @@ -127,16 +140,11 @@ class HTTPStreamer: def fill_buffer(): while True: - chunk = sox_proc.stdout.read(128000) + chunk = sox_proc.stdout.read(128000) # noqa if not chunk: break if chunk and not cancelled.is_set(): - with suppress( - ( - BrokenPipeError, - ConnectionResetError, - ) - ): + with suppress((BrokenPipeError, ConnectionResetError)): asyncio.run_coroutine_threadsafe( buffer.write(chunk), self.mass.loop ).result() @@ -144,7 +152,9 @@ class HTTPStreamer: # indicate EOF if no more data if not cancelled.is_set(): with suppress((BrokenPipeError, ConnectionResetError)): - asyncio.run_coroutine_threadsafe(buffer.write_eof(), self.mass.loop).result() + asyncio.run_coroutine_threadsafe( + buffer.write_eof(), self.mass.loop + ).result() # start fill buffer task in background fill_buffer_thread = threading.Thread(target=fill_buffer) @@ -159,7 +169,9 @@ class HTTPStreamer: # get the (next) track in queue if is_start: # report start of queue playback so we can calculate current track/duration etc. - queue_track = self.mass.add_job(player_queue.async_start_queue_stream()).result() + queue_track = self.mass.add_job( + player_queue.async_start_queue_stream() + ).result() is_start = False else: queue_track = player_queue.next_item @@ -247,8 +259,13 @@ class HTTPStreamer: # so we just use the entire original data last_part = prev_chunk + chunk if len(last_part) < fade_bytes: - LOGGER.warning("Not enough data for crossfade: %s", len(last_part)) - if not player_queue.crossfade_enabled or len(last_part) < fade_bytes: + LOGGER.warning( + "Not enough data for crossfade: %s", len(last_part) + ) + if ( + not player_queue.crossfade_enabled + or len(last_part) < fade_bytes + ): # crossfading is not enabled so just pass the (stripped) audio data sox_proc.stdin.write(last_part) bytes_written += len(last_part) @@ -309,8 +326,11 @@ class HTTPStreamer: else: LOGGER.info("streaming of queue for player %s completed", player_id) - def __get_audio_stream(self, player_id, queue_item, cancelled, chunksize=128000, resample=None): + def __get_audio_stream( + self, player_id, queue_item, cancelled, chunksize=128000, resample=None + ): """Get audio stream from provider and apply additional effects/processing if needed.""" + # pylint: disable=subprocess-popen-preexec-fn player_queue = self.mass.player_manager.get_player_queue(player_id) streamdetails = self.mass.add_job( player_queue.async_get_stream_details(player_id, queue_item) @@ -335,7 +355,11 @@ class HTTPStreamer: sox_options, ) process = subprocess.Popen( - args, shell=True, stdout=subprocess.PIPE, bufsize=chunksize, preexec_fn=os.setsid + args, + shell=True, + stdout=subprocess.PIPE, + bufsize=chunksize, + preexec_fn=os.setsid, ) elif streamdetails.type in [StreamType.URL, StreamType.FILE]: args = 'sox -t %s "%s" -t %s - %s' % ( @@ -346,7 +370,11 @@ class HTTPStreamer: ) args = shlex.split(args) process = subprocess.Popen( - args, shell=False, stdout=subprocess.PIPE, bufsize=chunksize, preexec_fn=os.setsid + args, + shell=False, + stdout=subprocess.PIPE, + bufsize=chunksize, + preexec_fn=os.setsid, ) elif streamdetails.type == StreamType.EXECUTABLE: args = "%s | sox -t %s - -t %s - %s" % ( @@ -356,7 +384,11 @@ class HTTPStreamer: sox_options, ) process = subprocess.Popen( - args, shell=True, stdout=subprocess.PIPE, bufsize=chunksize, preexec_fn=os.setsid + args, + shell=True, + stdout=subprocess.PIPE, + bufsize=chunksize, + preexec_fn=os.setsid, ) else: LOGGER.warning("no streaming options for %s", queue_item.name) @@ -390,7 +422,9 @@ class HTTPStreamer: if queue_item.media_type == MediaType.Track: self.mass.loop.run_in_executor(None, self.__analyze_audio, streamdetails) - def __get_player_sox_options(self, player_id: str, streamdetails: StreamDetails) -> str: + def __get_player_sox_options( + self, player_id: str, streamdetails: StreamDetails + ) -> str: """Get player specific sox effect options.""" sox_options = [] player_conf = self.mass.config.get_player_config(player_id) @@ -451,7 +485,7 @@ class HTTPStreamer: @staticmethod def __crossfade_pcm_parts(fade_in_part, fade_out_part, pcm_args, fade_length): - """crossfade two chunks of audio using sox""" + """Crossfade two chunks of audio using sox.""" # create fade-in part fadeinfile = create_tempfile() args = "sox --ignore-length -t %s - -t %s %s fade t %s" % ( @@ -472,7 +506,9 @@ class HTTPStreamer: fade_length, ) args = shlex.split(args) - process = subprocess.Popen(args, shell=False, stdout=subprocess.PIPE, stdin=subprocess.PIPE) + process = subprocess.Popen( + args, shell=False, stdout=subprocess.PIPE, stdin=subprocess.PIPE + ) process.communicate(fade_out_part) # create crossfade using sox and some temp files # TODO: figure out how to make this less complex and without the tempfiles @@ -484,7 +520,9 @@ class HTTPStreamer: pcm_args, ) args = shlex.split(args) - process = subprocess.Popen(args, shell=False, stdout=subprocess.PIPE, stdin=subprocess.PIPE) + process = subprocess.Popen( + args, shell=False, stdout=subprocess.PIPE, stdin=subprocess.PIPE + ) crossfade_part, _ = process.communicate() fadeinfile.close() fadeoutfile.close() diff --git a/music_assistant/mass.py b/music_assistant/mass.py index edf7b147..a9a21449 100644 --- a/music_assistant/mass.py +++ b/music_assistant/mass.py @@ -23,17 +23,7 @@ from music_assistant.music_manager import MusicManager from music_assistant.player_manager import PlayerManager from music_assistant.utils import callback, get_hostname, get_ip_pton, is_callback from music_assistant.web import Web -from zeroconf import DNSPointer, DNSRecord -from zeroconf import Error as ZeroconfError -from zeroconf import ( - InterfaceChoice, - IPVersion, - NonUniqueNameException, - ServiceBrowser, - ServiceInfo, - ServiceStateChange, - Zeroconf, -) +from zeroconf import NonUniqueNameException, ServiceInfo, Zeroconf LOGGER = logging.getLogger("mass") @@ -44,7 +34,8 @@ class MusicAssistant: def __init__(self, datapath): """ - Create an instance of MusicAssistant + Create an instance of MusicAssistant. + :param datapath: file location to store the data """ @@ -80,7 +71,7 @@ class MusicAssistant: await self.__async_setup_discovery() async def async_stop(self): - """stop running the music assistant server""" + """Stop running the music assistant server.""" LOGGER.info("Application shutdown") self.signal_event(EVENT_SHUTDOWN) self._exit = True @@ -111,13 +102,15 @@ class MusicAssistant: @callback def get_provider(self, provider_id: str) -> Provider: """Return provider/plugin by id.""" - if not provider_id in self._providers: + if provider_id not in self._providers: LOGGER.warning("Provider %s is not available", provider_id) return None return self._providers[provider_id] @callback - def get_providers(self, filter_type: Optional[ProviderType] = None) -> List[Provider]: + def get_providers( + self, filter_type: Optional[ProviderType] = None + ) -> List[Provider]: """Return all providers, optionally filtered by type.""" return [ item @@ -160,6 +153,7 @@ class MusicAssistant: def signal_event(self, event_msg: str, event_details: Any = None): """ Signal (systemwide) event. + :param event_msg: the eventmessage to signal :param event_details: optional details to send with the event. """ @@ -177,6 +171,7 @@ class MusicAssistant: ) -> Callable: """ Add callback to event listeners. + Returns function to remove the listener. :param cb_func: callback function or coroutine :param event_filter: Optionally only listen for these events @@ -189,8 +184,11 @@ class MusicAssistant: return remove_listener - def add_job(self, target: Callable[..., Any], *args: Any) -> Optional[asyncio.Future]: + def add_job( + self, target: Callable[..., Any], *args: Any + ) -> Optional[asyncio.Future]: """Add a job/task to the event loop. + target: target to call. args: parameters for method to call. """ @@ -207,7 +205,9 @@ class MusicAssistant: if threading.current_thread() is not threading.main_thread(): # called from other thread if asyncio.iscoroutine(check_target): - task = asyncio.run_coroutine_threadsafe(target, self.loop) # type: ignore + task = asyncio.run_coroutine_threadsafe( + target, self.loop + ) # type: ignore elif asyncio.iscoroutinefunction(check_target): task = asyncio.run_coroutine_threadsafe(target(*args), self.loop) elif is_callback(check_target): diff --git a/music_assistant/metadata.py b/music_assistant/metadata.py index 5c082e54..099ee345 100755 --- a/music_assistant/metadata.py +++ b/music_assistant/metadata.py @@ -1,7 +1,7 @@ """All logic for metadata retrieval.""" +# TODO: split up into (optional) providers import json import logging -# TODO: split up into (optional) providers import re from typing import Optional @@ -16,23 +16,24 @@ LOGGER = logging.getLogger("mass") class MetaData: - """several helpers to search and store metadata for mediaitems""" + """Several helpers to search and store metadata for mediaitems.""" # TODO: create periodic task to search for missing metadata def __init__(self, mass): + """Initialize class.""" self.mass = mass self.musicbrainz = MusicBrainz(mass) self.fanarttv = FanartTv(mass) async def async_setup(self): - """async initialize of metadata module""" + """Async setup of metadata module.""" await self.musicbrainz.async_setup() await self.fanarttv.async_setup() async def async_get_artist_metadata(self, mb_artist_id, cur_metadata): - """get/update rich metadata for an artist by providing the musicbrainz artist id""" + """Get/update rich metadata for an artist by providing the musicbrainz artist id.""" metadata = cur_metadata - if not "fanart" in metadata: + if "fanart" not in metadata: res = await self.fanarttv.async_get_artist_images(mb_artist_id) if res: self.merge_metadata(cur_metadata, res) @@ -46,7 +47,7 @@ class MetaData: trackname=None, track_isrc=None, ): - """retrieve musicbrainz artist id for the given details""" + """Retrieve musicbrainz artist id for the given details.""" LOGGER.debug( "searching musicbrainz for %s \ (albumname: %s - album_upc: %s - trackname: %s - track_isrc: %s)", @@ -105,7 +106,7 @@ class MetaData: @staticmethod def merge_metadata(cur_metadata, new_values): - """merge new info into the metadata dict without overwiteing existing values""" + """Merge new info into the metadata dict without overwriting existing values.""" for key, value in new_values.items(): if not cur_metadata.get(key): cur_metadata[key] = value @@ -113,21 +114,26 @@ class MetaData: class MusicBrainz: + """Handle getting Id's from MusicBrainz.""" + def __init__(self, mass): + """Initialize class.""" self.mass = mass self.cache = mass.cache self.throttler = None self._http_session = None async def async_setup(self): - """perform async setup""" + """Perform async setup.""" self._http_session = aiohttp.ClientSession( loop=self.mass.loop, connector=aiohttp.TCPConnector() ) self.throttler = Throttler(rate_limit=1, period=1) - async def async_search_artist_by_album(self, artistname, albumname=None, album_upc=None): - """retrieve musicbrainz artist id by providing the artist name and albumname or upc""" + async def async_search_artist_by_album( + self, artistname, albumname=None, album_upc=None + ): + """Retrieve musicbrainz artist id by providing the artist name and albumname or upc.""" for searchartist in [ re.sub(LUCENE_SPECIAL, r"\\\1", artistname), get_compare_string(artistname), @@ -161,8 +167,10 @@ class MusicBrainz: return artist["id"] return "" - async def async_search_artist_by_track(self, artistname, trackname=None, track_isrc=None): - """retrieve artist id by providing the artist name and trackname or track isrc""" + async def async_search_artist_by_track( + self, artistname, trackname=None, track_isrc=None + ): + """Retrieve artist id by providing the artist name and trackname or track isrc.""" endpoint = "recording" searchartist = re.sub(LUCENE_SPECIAL, r"\\\1", artistname) # searchartist = searchartist.replace('/','').replace('\\','').replace('-', '') @@ -206,7 +214,10 @@ class MusicBrainz: ) as response: try: result = await response.json() - except Exception as exc: + except ( + aiohttp.client_exceptions.ContentTypeError, + json.decoder.JSONDecodeError, + ) as exc: msg = await response.text() LOGGER.exception("%s - %s", str(exc), msg) result = None @@ -214,21 +225,24 @@ class MusicBrainz: class FanartTv: + """FanartTv support for metadata retrieval.""" + def __init__(self, mass): + """Initialize class.""" self.mass = mass self.cache = mass.cache self._http_session = None self.throttler = None async def async_setup(self): - """perform async setup""" + """Perform async setup.""" self._http_session = aiohttp.ClientSession( loop=self.mass.loop, connector=aiohttp.TCPConnector() ) self.throttler = Throttler(rate_limit=1, period=2) async def async_get_artist_images(self, mb_artist_id): - """retrieve images by musicbrainz artist id""" + """Retrieve images by musicbrainz artist id.""" metadata = {} data = await self.async_get_data("music/%s" % mb_artist_id) if data: @@ -243,7 +257,7 @@ class FanartTv: metadata[key] = item["url"] if data.get("artistthumb"): url = data["artistthumb"][0]["url"] - if not "2a96cbd8b46e442fc41c2b86b821562f" in url: + if "2a96cbd8b46e442fc41c2b86b821562f" not in url: metadata["image"] = url if data.get("musicbanner"): metadata["banner"] = data["musicbanner"][0]["url"] @@ -251,7 +265,7 @@ class FanartTv: @async_use_cache(30) async def async_get_data(self, endpoint, params=None): - """get data from api""" + """Get data from api.""" if params is None: params = {} url = "http://webservice.fanart.tv/v3/%s" % endpoint @@ -262,7 +276,10 @@ class FanartTv: ) as response: try: result = await response.json() - except (aiohttp.client_exceptions.ContentTypeError, json.decoder.JSONDecodeError): + except ( + aiohttp.client_exceptions.ContentTypeError, + json.decoder.JSONDecodeError, + ): LOGGER.error("Failed to retrieve %s", endpoint) text_result = await response.text() LOGGER.debug(text_result) diff --git a/music_assistant/models/__init__.py b/music_assistant/models/__init__.py index e69de29b..67f04471 100644 --- a/music_assistant/models/__init__.py +++ b/music_assistant/models/__init__.py @@ -0,0 +1 @@ +"""Models.""" diff --git a/music_assistant/models/config_entry.py b/music_assistant/models/config_entry.py index aa7781ae..1797dbdb 100644 --- a/music_assistant/models/config_entry.py +++ b/music_assistant/models/config_entry.py @@ -17,7 +17,6 @@ class ConfigEntryType(str, Enum): HEADER = "header" - @dataclass class ConfigEntry: """Model for a Config Entry.""" @@ -33,4 +32,4 @@ class ConfigEntry: depends_on: str = "" # entry_key that needs to be set before this setting shows up in frontend hidden: bool = False # hide from UI value: Optional[Any] = None # set by the configuration manager - store_hashed: bool = False # value will be hashed, non reversible + store_hashed: bool = False # value will be hashed, non reversible diff --git a/music_assistant/models/media_types.py b/music_assistant/models/media_types.py index 8c6afd75..de0e8f79 100755 --- a/music_assistant/models/media_types.py +++ b/music_assistant/models/media_types.py @@ -1,12 +1,13 @@ """Models and helpers for media items.""" from dataclasses import dataclass, field -from enum import Enum, Enum +from enum import Enum from typing import List, Optional class MediaType(int, Enum): """Enum for MediaType.""" + Artist = 1 Album = 2 Track = 3 @@ -32,6 +33,7 @@ def media_type_from_string(media_type_str: str) -> MediaType: class ContributorRole(int, Enum): """Enum for Contributor Role.""" + Artist = 1 Writer = 2 Producer = 3 @@ -39,6 +41,7 @@ class ContributorRole(int, Enum): class AlbumType(int, Enum): """Enum for Album type.""" + Album = 1 Single = 2 Compilation = 3 @@ -46,6 +49,7 @@ class AlbumType(int, Enum): class TrackQuality(int, Enum): """Enum for Track Quality.""" + LOSSY_MP3 = 0 LOSSY_OGG = 1 LOSSY_AAC = 2 @@ -58,8 +62,9 @@ class TrackQuality(int, Enum): @dataclass -class MediaItemProviderId(): +class MediaItemProviderId: """Model for a MediaItem's provider id.""" + provider: str item_id: str quality: Optional[TrackQuality] = TrackQuality.UNKNOWN @@ -68,15 +73,16 @@ class MediaItemProviderId(): class ExternalId(str, Enum): """Enum with external id's.""" + MUSICBRAINZ = "musicbrainz" UPC = "upc" ISRC = "isrc" - @dataclass class MediaItem(object): """Representation of a media item.""" + item_id: str = "" provider: str = "" name: str = "" @@ -91,14 +97,16 @@ class MediaItem(object): @dataclass class Artist(MediaItem): - """Model for an artist""" + """Model for an artist.""" + media_type: MediaType = MediaType.Artist sort_name: str = "" @dataclass class Album(MediaItem): - """Model for an album""" + """Model for an album.""" + media_type: MediaType = MediaType.Album version: str = "" year: int = 0 @@ -109,7 +117,8 @@ class Album(MediaItem): @dataclass class Track(MediaItem): - """Model for a track""" + """Model for a track.""" + media_type: MediaType = MediaType.Track duration: int = 0 version: str = "" @@ -121,7 +130,8 @@ class Track(MediaItem): @dataclass class Playlist(MediaItem): - """Model for a playlist""" + """Model for a playlist.""" + media_type: MediaType = MediaType.Playlist owner: str = "" checksum: [Optional[str]] = None # some value to detect playlist track changes @@ -130,14 +140,16 @@ class Playlist(MediaItem): @dataclass class Radio(MediaItem): - """Model for a radio station""" + """Model for a radio station.""" + media_type: MediaType = MediaType.Radio duration: int = 86400 @dataclass -class SearchResult(): +class SearchResult: """Model for Media Item Search result.""" + artists: List[Artist] = field(default_factory=list) albums: List[Album] = field(default_factory=list) tracks: List[Track] = field(default_factory=list) diff --git a/music_assistant/models/musicprovider.py b/music_assistant/models/musicprovider.py index 21634542..a24db70b 100755 --- a/music_assistant/models/musicprovider.py +++ b/music_assistant/models/musicprovider.py @@ -13,14 +13,15 @@ from music_assistant.models.media_types import ( SearchResult, Track, ) -from music_assistant.models.streamdetails import StreamDetails from music_assistant.models.provider import Provider, ProviderType +from music_assistant.models.streamdetails import StreamDetails @dataclass class MusicProvider(Provider): """ Base class for a Musicprovider. + Should be overriden in the provider specific implementation. """ @@ -43,6 +44,7 @@ class MusicProvider(Provider): ) -> SearchResult: """ Perform search on musicprovider. + :param search_query: Search query. :param media_types: A list of media_types to include. All types if None. :param limit: Number of items to return in the search (per type). @@ -76,7 +78,7 @@ class MusicProvider(Provider): @abstractmethod async def async_get_artist(self, prov_artist_id: str) -> Artist: - """Get full artist details by id""" + """Get full artist details by id.""" raise NotImplementedError @abstractmethod @@ -101,12 +103,12 @@ class MusicProvider(Provider): @abstractmethod async def async_get_playlist(self, prov_playlist_id: str) -> Playlist: - """Get full playlist details by id""" + """Get full playlist details by id.""" raise NotImplementedError @abstractmethod async def async_get_radio(self, prov_radio_id: str) -> Radio: - """Get full radio details by id""" + """Get full radio details by id.""" raise NotImplementedError @abstractmethod @@ -125,7 +127,9 @@ class MusicProvider(Provider): raise NotImplementedError @abstractmethod - async def async_library_remove(self, prov_item_id: str, media_type: MediaType) -> bool: + async def async_library_remove( + self, prov_item_id: str, media_type: MediaType + ) -> bool: """Remove item from provider's library. Return true on succes.""" raise NotImplementedError diff --git a/music_assistant/models/player.py b/music_assistant/models/player.py index 78f985e7..acd945cf 100755 --- a/music_assistant/models/player.py +++ b/music_assistant/models/player.py @@ -6,7 +6,6 @@ from enum import Enum from typing import Any, Awaitable, Callable, List, Optional, Union from music_assistant.models.config_entry import ConfigEntry -from music_assistant.constants import EVENT_PLAYER_CONTROL_UPDATED, EVENT_PLAYER_CHANGED class PlayerState(str, Enum): @@ -57,11 +56,11 @@ class Player: config_entries: List[ConfigEntry] = field(default_factory=list) updated_at: datetime = datetime.utcnow() # managed by playermanager! active_queue: str = "" # managed by playermanager! - group_parents: List[str] = field(default_factory=list) # managed by playermanager! - cur_queue_item_id: str = None # managed by playermanager! + group_parents: List[str] = field(default_factory=list) # managed by playermanager! + cur_queue_item_id: str = None # managed by playermanager! def __setattr__(self, name, value): - """Event when control is updated. Do not override""" + """Event when control is updated. Do not override.""" if name == "updated_at": # updated at is set by the on_update callback # make sure we do not hit an endless loop @@ -84,8 +83,12 @@ class PlayerControlType(int, Enum): @dataclass class PlayerControl: - """Model for a player control which allows for a - plugin-like structure to override common player commands.""" + """ + Model for a player control. + + Allows for a plugin-like + structure to override common player commands. + """ type: PlayerControlType = PlayerControlType.UNKNOWN id: str = "" diff --git a/music_assistant/models/player_queue.py b/music_assistant/models/player_queue.py index 65355d58..3ea790e5 100755 --- a/music_assistant/models/player_queue.py +++ b/music_assistant/models/player_queue.py @@ -1,6 +1,4 @@ -""" - Models and helpers for a player queue. -""" +"""Models and helpers for a player queue.""" import logging import random @@ -15,10 +13,10 @@ from music_assistant.constants import ( EVENT_QUEUE_ITEMS_UPDATED, EVENT_QUEUE_UPDATED, ) -from music_assistant.models.media_types import Track, MediaType +from music_assistant.models.media_types import MediaType, Track from music_assistant.models.player import PlayerFeature, PlayerState from music_assistant.models.streamdetails import StreamDetails -from music_assistant.utils import callback, json_serializer +from music_assistant.utils import callback # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-public-methods @@ -28,7 +26,7 @@ LOGGER = logging.getLogger("mass") class QueueOption(str, Enum): - """Enum representation of the queue (play) options""" + """Enum representation of the queue (play) options.""" Play = "play" Replace = "replace" @@ -45,6 +43,7 @@ class QueueItem(Track): queue_item_id: str = "" def __init__(self, media_item=None): + """Initialize class.""" super().__init__() self.queue_item_id = str(uuid.uuid4()) # if existing media_item given, load those values @@ -54,11 +53,10 @@ class QueueItem(Track): class PlayerQueue: - """ - Class that holds the queue items for a player. - """ + """Class that holds the queue items for a player.""" def __init__(self, mass, player_id): + """Initialize class.""" self.mass = mass self.player_id = player_id self._items = [] @@ -75,7 +73,7 @@ class PlayerQueue: self.mass.add_job(self.__async_restore_saved_state()) async def async_close(self): - """Call on shutdown/close.""" + """Handle shutdown/close.""" # pylint: disable=unused-argument await self.__async_save_state() @@ -86,12 +84,12 @@ class PlayerQueue: @property def shuffle_enabled(self): - """Shuffle enabled property""" + """Return shuffle enabled property.""" return self._shuffle_enabled @shuffle_enabled.setter def shuffle_enabled(self, enable_shuffle: bool): - """enable/disable shuffle""" + """Set shuffle.""" if not self._shuffle_enabled and enable_shuffle: # shuffle requested self._shuffle_enabled = True @@ -113,7 +111,7 @@ class PlayerQueue: @property def repeat_enabled(self): - """Returns if crossfade is enabled for this player.""" + """Return if crossfade is enabled for this player.""" return self._repeat_enabled @repeat_enabled.setter @@ -126,13 +124,16 @@ class PlayerQueue: @property def crossfade_enabled(self): - """Returns if crossfade is enabled for this player's queue.""" - return self.mass.config.player_settings[self.player_id]["crossfade_duration"] > 0 + """Return if crossfade is enabled for this player's queue.""" + return ( + self.mass.config.player_settings[self.player_id]["crossfade_duration"] > 0 + ) @property def cur_index(self): """ - Returns the current index of the queue. + Return the current index of the queue. + Returns None if queue is empty. """ if not self._items: @@ -143,6 +144,7 @@ class PlayerQueue: def cur_item_id(self): """ 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: @@ -153,6 +155,7 @@ class PlayerQueue: def cur_item(self): """ 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: @@ -161,14 +164,14 @@ class PlayerQueue: @property def cur_item_time(self): - """Returns the time (progress) for current (playing) item.""" + """Return the time (progress) for current (playing) item.""" return self._cur_item_time @property def next_index(self): - """ - Returns the next index for this player's queue. - Returns None if queue is empty or no more items. + """Return the next index for this player's queue. + + Return None if queue is empty or no more items. """ if not self.items: # queue is empty @@ -187,8 +190,8 @@ class PlayerQueue: @property def next_item(self): - """ - Returns the next item in the queue. + """Return the next item in the queue. + Returns None if queue is empty or no more items. """ if self.next_index is not None: @@ -197,31 +200,30 @@ class PlayerQueue: @property def items(self): - """ - Returns all queue items for this player's queue. - """ + """Return 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 with all tracks + 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 with all tracks. """ supports_crossfade = PlayerFeature.CROSSFADE in self.player.features return self.crossfade_enabled and not supports_crossfade @callback def get_item(self, index): - """get item by index from queue""" + """Get item by index from queue.""" if index is not None and len(self.items) > index: return self.items[index] return None @callback def by_item_id(self, queue_item_id: str): - """get item by queue_item_id from queue""" + """Get item by queue_item_id from queue.""" if not queue_item_id: return None for item in self.items: @@ -259,11 +261,15 @@ class PlayerQueue: else: # at this point we don't know if the queue is synced with the player # so just to be safe we send the queue_items to the player - player_provider = self.mass.player_manager.get_player_provider(self.player_id) + player_provider = self.mass.player_manager.get_player_provider( + self.player_id + ) await player_provider.async_cmd_queue_load(self.player_id, self.items) await self.async_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 async_play_index(self, index): """Play item at index X in queue.""" @@ -279,10 +285,14 @@ class PlayerQueue: self.mass.web.internal_url, self.player.player_id, ) - return await player_prov.async_cmd_play_uri(self.player_id, queue_stream_uri) + return await player_prov.async_cmd_play_uri( + self.player_id, queue_stream_uri + ) elif supports_queue: try: - return await player_prov.async_cmd_queue_play_index(self.player_id, index) + return await player_prov.async_cmd_queue_play_index( + self.player_id, index + ) except NotImplementedError: # not supported by player, use load queue instead LOGGER.debug( @@ -291,11 +301,14 @@ class PlayerQueue: self._items = self._items[index:] await player_prov.async_cmd_queue_load(self.player_id, self._items) else: - return await player_prov.async_cmd_play_uri(self.player_id, self._items[index].uri) + return await player_prov.async_cmd_play_uri( + self.player_id, self._items[index].uri + ) async def async_move_item(self, queue_item_id, pos_shift=1): """ - move queue item x up/down the queue + 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 @@ -317,7 +330,7 @@ class PlayerQueue: await self.async_play_index(new_index) async def async_load(self, queue_items: List[QueueItem]): - """load (overwrite) queue with new items""" + """Load (overwrite) queue with new items.""" supports_queue = PlayerFeature.QUEUE in self.player.features for index, item in enumerate(queue_items): item.sort_index = index @@ -334,8 +347,9 @@ class PlayerQueue: async def async_insert(self, queue_items: List[QueueItem], offset=0): """ - insert new items at offset x from current position - keeps remaining items in queue + 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 @@ -354,7 +368,9 @@ class PlayerQueue: 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 supports_queue: if offset == 0: await self.async_play_index(insert_at_index) @@ -376,9 +392,7 @@ class PlayerQueue: self.mass.add_job(self.__async_save_state()) async def async_append(self, queue_items: List[QueueItem]): - """ - append new items at the end of the queue - """ + """Append new items at the end of the queue.""" supports_queue = PlayerFeature.QUEUE in self.player.features for index, item in enumerate(queue_items): item.sort_index = len(self.items) + index @@ -405,9 +419,7 @@ class PlayerQueue: self.mass.add_job(self.__async_save_state()) async def async_update(self, queue_items: List[QueueItem]): - """ - update the existing queue items, mostly caused by reordering - """ + """Update the existing queue items, mostly caused by reordering.""" supports_queue = PlayerFeature.QUEUE in self.player.features self._items = queue_items if supports_queue and not self.use_queue_stream: @@ -426,9 +438,7 @@ class PlayerQueue: self.mass.add_job(self.__async_save_state()) async def async_clear(self): - """ - clear all items in the queue - """ + """Clear all items in the queue.""" supports_queue = PlayerFeature.QUEUE in self.player.features await self.mass.player_manager.async_cmd_stop(self.player_id) self._items = [] @@ -465,7 +475,7 @@ class PlayerQueue: self.mass.signal_event(EVENT_QUEUE_UPDATED, self.to_dict()) async def async_start_queue_stream(self): - """called by the queue streamer when it starts playing the queue stream""" + """Call when queue_streamer starts playing the queue stream.""" self._last_queue_startindex = self._next_queue_startindex return self.get_item(self._next_queue_startindex) @@ -474,6 +484,7 @@ class PlayerQueue: ) -> StreamDetails: """ Get streamdetails for the given queue_item. + This is called just-in-time when a player/queue wants a QueueItem to be played. Do not try to request streamdetails in advance as this is expiring data. param player_id: The id of the player that will be playing the stream. @@ -488,13 +499,17 @@ class PlayerQueue: queue_item.item_id, queue_item.provider, lazy=True, refresh=True ) # sort by quality and check track availability - for prov_media in sorted(full_track.provider_ids, key=lambda x: x.quality, reverse=True): + for prov_media in sorted( + full_track.provider_ids, key=lambda x: x.quality, reverse=True + ): # get streamdetails from provider music_prov = self.mass.get_provider(prov_media.provider) if not music_prov: continue # provider temporary unavailable ? - streamdetails = await music_prov.async_get_stream_details(prov_media.item_id) + streamdetails = await music_prov.async_get_stream_details( + prov_media.item_id + ) if streamdetails: # set streamdetails as attribute on the queue_item @@ -503,7 +518,7 @@ class PlayerQueue: return None 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, @@ -521,13 +536,16 @@ class PlayerQueue: @callback def __get_queue_stream_index(self): + """Get index of queue stream.""" # player is playing a constant stream of the queue so we need to do this the hard way queue_index = 0 elapsed_time_queue = self.player.elapsed_time 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] @@ -541,14 +559,16 @@ class PlayerQueue: return queue_index, track_time async def async_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 = self.get_item(new_index) 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 - self.mass.signal_event(EVENT_PLAYBACK_STOPPED, self._last_track.streamdetails) + self.mass.signal_event( + EVENT_PLAYBACK_STOPPED, self._last_track.streamdetails + ) if new_track and new_track.streamdetails: self.mass.signal_event(EVENT_PLAYBACK_STARTED, new_track.streamdetails) self._last_track = new_track @@ -560,7 +580,9 @@ class PlayerQueue: ]: # player stopped playing if self._last_track: - self.mass.signal_event(EVENT_PLAYBACK_STOPPED, self._last_track.streamdetails) + self.mass.signal_event( + EVENT_PLAYBACK_STOPPED, self._last_track.streamdetails + ) # update vars if track_time > 2: # account for track changing state so keep this a few seconds behind @@ -570,13 +592,13 @@ class PlayerQueue: @staticmethod def __shuffle_items(queue_items): - """shuffle a list of tracks""" + """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: @@ -584,7 +606,7 @@ class PlayerQueue: return item_index async def __async_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_state_%s" % self.player.player_id cache_data = await self.mass.cache.async_get(cache_str) if cache_data: @@ -597,7 +619,7 @@ class PlayerQueue: # pylint: enable=unused-argument async def __async_save_state(self): - """save current queue settings to file""" + """Save current queue settings to file.""" cache_str = "queue_state_%s" % self.player_id cache_data = { "shuffle_enabled": self._shuffle_enabled, diff --git a/music_assistant/models/playerprovider.py b/music_assistant/models/playerprovider.py index 0061140c..a4cfe8fb 100755 --- a/music_assistant/models/playerprovider.py +++ b/music_assistant/models/playerprovider.py @@ -7,12 +7,15 @@ from typing import List from music_assistant.models.player_queue import QueueItem from music_assistant.models.provider import Provider, ProviderType + @dataclass class PlayerProvider(Provider): """ - Base class for a Playerprovider - Should be overridden/subclassed by provider specific implementation. + Base class for a Playerprovider. + + Should be overridden/subclassed by provider specific implementation. """ + type: ProviderType = ProviderType.PLAYER_PROVIDER # SERVICE CALLS / PLAYER COMMANDS @@ -20,82 +23,92 @@ class PlayerProvider(Provider): @abstractmethod async def async_cmd_play_uri(self, player_id: str, uri: str): """ - Play the specified uri/url on the goven player. - :param player_id: player_id of the player to handle the command. + Play the specified uri/url on the given player. + + :param player_id: player_id of the player to handle the command. """ raise NotImplementedError @abstractmethod async def async_cmd_stop(self, player_id: str): """ - Send STOP command to given player. - :param player_id: player_id of the player to handle the command. + Send STOP command to given player. + + :param player_id: player_id of the player to handle the command. """ raise NotImplementedError @abstractmethod async def async_cmd_play(self, player_id: str): """ - Send STOP command to given player. - :param player_id: player_id of the player to handle the command. + Send PLAY command to given player. + + :param player_id: player_id of the player to handle the command. """ raise NotImplementedError @abstractmethod async def async_cmd_pause(self, player_id: str): """ - Send PAUSE command to given player. - :param player_id: player_id of the player to handle the command. + Send PAUSE command to given player. + + :param player_id: player_id of the player to handle the command. """ raise NotImplementedError @abstractmethod async def async_cmd_next(self, player_id: str): """ - Send NEXT TRACK command to given player. - :param player_id: player_id of the player to handle the command. + Send NEXT TRACK command to given player. + + :param player_id: player_id of the player to handle the command. """ raise NotImplementedError @abstractmethod async def async_cmd_previous(self, player_id: str): """ - Send PREVIOUS TRACK command to given player. - :param player_id: player_id of the player to handle the command. + Send PREVIOUS TRACK command to given player. + + :param player_id: player_id of the player to handle the command. """ raise NotImplementedError @abstractmethod async def async_cmd_power_on(self, player_id: str): """ - Send POWER ON command to given player. - :param player_id: player_id of the player to handle the command. + Send POWER ON command to given player. + + :param player_id: player_id of the player to handle the command. """ raise NotImplementedError @abstractmethod async def async_cmd_power_off(self, player_id: str): """ - Send POWER OFF command to given player. - :param player_id: player_id of the player to handle the command. + Send POWER OFF command to given player. + + :param player_id: player_id of the player to handle the command. """ raise NotImplementedError @abstractmethod async def async_cmd_volume_set(self, player_id: str, volume_level: int): """ - Send volume level command to given player. - :param player_id: player_id of the player to handle the command. - :param volume_level: volume level to set (0..100). + Send volume level command to given player. + + :param player_id: player_id of the player to handle the command. + :param volume_level: volume level to set (0..100). """ raise NotImplementedError @abstractmethod async def async_cmd_volume_mute(self, player_id: str, is_muted=False): """ - Send volume MUTE command to given player. - :param player_id: player_id of the player to handle the command. - :param is_muted: bool with new mute state. + Send volume MUTE command to given player. + + :param player_id: player_id of the player to handle the command. + :param is_muted: bool with new mute state. """ raise NotImplementedError @@ -104,52 +117,65 @@ class PlayerProvider(Provider): async def async_cmd_queue_play_index(self, player_id: str, index: int): """ - Play item at index X on player's queue - :param player_id: player_id of the player to handle the command. - :param index: (int) index of the queue item that should start playing + Play item at index X on player's queue. + + :param player_id: player_id of the player to handle the command. + :param index: (int) index of the queue item that should start playing """ raise NotImplementedError async def async_cmd_queue_load(self, player_id: str, queue_items: List[QueueItem]): """ - Load/overwrite given items in the player's queue implementation - :param player_id: player_id of the player to handle the command. - :param queue_items: a list of QueueItems + Load/overwrite given items in the player's queue implementation. + + :param player_id: player_id of the player to handle the command. + :param queue_items: a list of QueueItems """ raise NotImplementedError - async def async_cmd_queue_insert(self, - player_id: str, - queue_items: List[QueueItem], - insert_at_index: int): + async def async_cmd_queue_insert( + self, player_id: str, queue_items: List[QueueItem], insert_at_index: int + ): """ - Insert new items at position X into existing queue. - If insert_at_index 0 or None, will start playing newly added item(s) - :param player_id: player_id of the player to handle the command. - :param queue_items: a list of QueueItems - :param insert_at_index: queue position to insert new items + Insert new items at position X into existing queue. + + If insert_at_index 0 or None, will start playing newly added item(s) + :param player_id: player_id of the player to handle the command. + :param queue_items: a list of QueueItems + :param insert_at_index: queue position to insert new items """ + # pylint: disable=abstract-method raise NotImplementedError - async def async_cmd_queue_append(self, player_id: str, queue_items: List[QueueItem]): + async def async_cmd_queue_append( + self, player_id: str, queue_items: List[QueueItem] + ): """ - Append new items at the end of the queue. - :param player_id: player_id of the player to handle the command. - :param queue_items: a list of QueueItems + Append new items at the end of the queue. + + :param player_id: player_id of the player to handle the command. + :param queue_items: a list of QueueItems """ + # pylint: disable=abstract-method raise NotImplementedError - async def async_cmd_queue_update(self, player_id: str, queue_items: List[QueueItem]): + async def async_cmd_queue_update( + self, player_id: str, queue_items: List[QueueItem] + ): """ - Overwrite the existing items in the queue, used for reordering. - :param player_id: player_id of the player to handle the command. - :param queue_items: a list of QueueItems + Overwrite the existing items in the queue, used for reordering. + + :param player_id: player_id of the player to handle the command. + :param queue_items: a list of QueueItems """ + # pylint: disable=abstract-method raise NotImplementedError async def async_cmd_queue_clear(self, player_id: str): """ - Clear the player's queue. - :param player_id: player_id of the player to handle the command. + Clear the player's queue. + + :param player_id: player_id of the player to handle the command. """ + # pylint: disable=abstract-method raise NotImplementedError diff --git a/music_assistant/models/provider.py b/music_assistant/models/provider.py index 1628079e..35530c19 100644 --- a/music_assistant/models/provider.py +++ b/music_assistant/models/provider.py @@ -3,7 +3,7 @@ from abc import abstractmethod from dataclasses import dataclass from enum import Enum -from typing import TYPE_CHECKING, Any, List, Optional +from typing import TYPE_CHECKING, List from music_assistant.models.config_entry import ConfigEntry @@ -44,17 +44,19 @@ class Provider: @abstractmethod async def async_on_start(self) -> bool: - """Called on startup. + """ Handle initialization of the provider based on config. - Return bool if start was succesfull""" + + Return bool if start was succesfull. Called on startup. + """ raise NotImplementedError @abstractmethod async def async_on_stop(self): - """Called on shutdown. Handle correct close/cleanup of the provider on exit.""" + """Handle correct close/cleanup of the provider on exit. Called on shutdown.""" raise NotImplementedError async def async_on_reload(self): - """Called on reload. Handle configuration changes for this provider.""" + """Handle configuration changes for this provider. Called on reload.""" await self.async_on_stop() await self.async_on_start() diff --git a/music_assistant/models/streamdetails.py b/music_assistant/models/streamdetails.py index 1911205c..8f42bd31 100644 --- a/music_assistant/models/streamdetails.py +++ b/music_assistant/models/streamdetails.py @@ -1,6 +1,4 @@ -""" - Models and helpers for the streamdetails of a MediaItem. -""" +"""Models and helpers for the streamdetails of a MediaItem.""" from dataclasses import dataclass from enum import Enum @@ -9,6 +7,7 @@ from typing import Any, Optional class StreamType(str, Enum): """Enum with stream types.""" + EXECUTABLE = "executable" URL = "url" FILE = "file" @@ -16,6 +15,7 @@ class StreamType(str, Enum): class ContentType(str, Enum): """Enum with stream content types.""" + OGG = "ogg" FLAC = "flac" MP3 = "mp3" @@ -24,8 +24,9 @@ class ContentType(str, Enum): @dataclass -class StreamDetails(): +class StreamDetails: """Model for streamdetails.""" + type: StreamType provider: str item_id: str diff --git a/music_assistant/music_manager.py b/music_assistant/music_manager.py index d2b80da1..2c88381f 100755 --- a/music_assistant/music_manager.py +++ b/music_assistant/music_manager.py @@ -9,7 +9,6 @@ import time from typing import List, Optional import aiohttp -from PIL import Image from music_assistant.cache import async_cached, async_cached_generator from music_assistant.constants import EVENT_MUSIC_SYNC_STATUS from music_assistant.models.media_types import ( @@ -26,13 +25,13 @@ from music_assistant.models.media_types import ( from music_assistant.models.musicprovider import MusicProvider from music_assistant.models.provider import ProviderType from music_assistant.utils import compare_strings, run_periodic - +from PIL import Image LOGGER = logging.getLogger("mass") def sync_task(desc): - """Decorator to report a sync task.""" + """Return decorator to report a sync task.""" def wrapper(func): @functools.wraps(func) @@ -42,16 +41,22 @@ def sync_task(desc): # check if this sync task is not already running for sync_prov_id, sync_desc in method_class.running_sync_jobs: if sync_prov_id == prov_id and sync_desc == desc: - LOGGER.debug("Syncjob %s for provider %s is already running!", desc, prov_id) + LOGGER.debug( + "Syncjob %s for provider %s is already running!", desc, prov_id + ) return LOGGER.debug("Start syncjob %s for provider %s.", desc, prov_id) sync_job = (prov_id, desc) method_class.running_sync_jobs.append(sync_job) - method_class.mass.signal_event(EVENT_MUSIC_SYNC_STATUS, method_class.running_sync_jobs) + method_class.mass.signal_event( + EVENT_MUSIC_SYNC_STATUS, method_class.running_sync_jobs + ) await func(*args) LOGGER.info("Finished syncing %s for provider %s", desc, prov_id) method_class.running_sync_jobs.remove(sync_job) - method_class.mass.signal_event(EVENT_MUSIC_SYNC_STATUS, method_class.running_sync_jobs) + method_class.mass.signal_event( + EVENT_MUSIC_SYNC_STATUS, method_class.running_sync_jobs + ) return async_wrapped @@ -62,6 +67,7 @@ class MusicManager: """Several helpers around the musicproviders.""" def __init__(self, mass): + """Initialize class.""" self.running_sync_jobs = [] self.mass = mass self.cache = mass.cache @@ -95,7 +101,9 @@ class MusicManager: return await self.async_get_radio(item_id, provider_id) return None - async def async_get_artist(self, item_id: str, provider_id: str, lazy: bool = True) -> Artist: + async def async_get_artist( + self, item_id: str, provider_id: str, lazy: bool = True + ) -> Artist: """Return artist details for the given provider artist id.""" assert item_id and provider_id db_id = await self.mass.database.async_get_database_id( @@ -107,9 +115,13 @@ class MusicManager: if not provider.available: return None cache_key = f"{provider_id}.get_artist.{item_id}" - artist = await async_cached(self.cache, cache_key, provider.async_get_artist(item_id)) + artist = await async_cached( + self.cache, cache_key, provider.async_get_artist(item_id) + ) if not artist: - raise Exception("Artist %s not found on provider %s" % (item_id, provider_id)) + raise Exception( + "Artist %s not found on provider %s" % (item_id, provider_id) + ) if lazy: self.mass.add_job(self.__async_add_artist(artist)) artist.is_lazy = True @@ -118,7 +130,11 @@ class MusicManager: return await self.mass.database.async_get_artist(db_id) async def async_get_album( - self, item_id: str, provider_id: str, lazy=True, album_details: Optional[Album] = None + self, + item_id: str, + provider_id: str, + lazy=True, + album_details: Optional[Album] = None, ) -> Album: """Return album details for the given provider album id.""" assert item_id and provider_id @@ -136,7 +152,9 @@ class MusicManager: self.cache, cache_key, provider.async_get_album(item_id) ) if not album_details: - raise Exception("Album %s not found on provider %s" % (item_id, provider_id)) + raise Exception( + "Album %s not found on provider %s" % (item_id, provider_id) + ) if lazy: self.mass.add_job(self.__async_add_album(album_details)) album_details.is_lazy = True @@ -159,8 +177,8 @@ class MusicManager: ) if db_id and refresh: # in some cases (e.g. at playback time or requesting full track info) - # it's useful to have the track refreshed from the provider instead of the database cache - # this is to make sure that the track is available and perhaps + # it's useful to have the track refreshed from the provider instead of + # the database cache to make sure that the track is available and perhaps # another or a higher quality version is available. if lazy: self.mass.add_job(self.__async_match_track(db_id)) @@ -177,7 +195,9 @@ class MusicManager: self.cache, cache_key, provider.async_get_track(item_id) ) if not track_details: - raise Exception("Track %s not found on provider %s" % (item_id, provider_id)) + raise Exception( + "Track %s not found on provider %s" % (item_id, provider_id) + ) if lazy: self.mass.add_job(self.__async_add_track(track_details)) track_details.is_lazy = True @@ -215,8 +235,10 @@ class MusicManager: db_id = await self.mass.database.async_add_radio(item_details) return await self.mass.database.async_get_radio(db_id) - async def async_get_album_tracks(self, item_id: str, provider_id: str) -> List[Track]: - """Return album tracks for the given provider album id. Generator!""" + async def async_get_album_tracks( + self, item_id: str, provider_id: str + ) -> List[Track]: + """Return album tracks for the given provider album id. Generator.""" assert item_id and provider_id if provider_id == "database": # album tracks are not stored in db, we always fetch them (cached) from the provider. @@ -236,15 +258,19 @@ class MusicManager: ) if db_id: # return database track instead if we have a match - db_item = await self.mass.database.async_get_track(db_id, fulldata=False) + db_item = await self.mass.database.async_get_track( + db_id, fulldata=False + ) db_item.disc_number = item.disc_number db_item.track_number = item.track_number yield db_item else: yield item - async def async_get_playlist_tracks(self, item_id: str, provider_id: str) -> List[Track]: - """Return playlist tracks for the given provider playlist id. Generator!""" + async def async_get_playlist_tracks( + self, item_id: str, provider_id: str + ) -> List[Track]: + """Return playlist tracks for the given provider playlist id. Generator.""" assert item_id and provider_id if provider_id == "database": # playlist tracks are not stored in db, we always fetch them (cached) from the provider. @@ -278,8 +304,10 @@ class MusicManager: pos += 1 yield item - async def async_get_artist_toptracks(self, artist_id: str, provider_id: str) -> List[Track]: - """Return top tracks for an artist. Generator!""" + async def async_get_artist_toptracks( + self, artist_id: str, provider_id: str + ) -> List[Track]: + """Return top tracks for an artist. Generator.""" async with self.mass.database.db_conn() as db_conn: if provider_id == "database": # tracks from all providers @@ -289,12 +317,15 @@ class MusicManager: ) for prov_id in artist.provider_ids: provider = self.mass.get_provider(prov_id.provider) - if not provider or not MediaType.Track in provider.supported_mediatypes: + if ( + not provider + or MediaType.Track not in provider.supported_mediatypes + ): continue async for item in self.async_get_artist_toptracks( prov_id.item_id, prov_id.provider ): - if not item.item_id in item_ids: + if item.item_id not in item_ids: yield item item_ids.append(item.item_id) else: @@ -302,34 +333,48 @@ class MusicManager: provider = self.mass.get_provider(provider_id) cache_key = f"{provider_id}.artist_toptracks.{artist_id}" async for item in async_cached_generator( - self.cache, cache_key, provider.async_get_artist_toptracks(artist_id) + self.cache, + cache_key, + provider.async_get_artist_toptracks(artist_id), ): if item: assert item.item_id and item.provider db_id = await self.mass.database.async_get_database_id( - item.provider, item.item_id, MediaType.Track, db_conn=db_conn + item.provider, + item.item_id, + MediaType.Track, + db_conn=db_conn, ) if db_id: # return database track instead if we have a match - yield await self.mass.database.async_get_track(db_id, fulldata=False, db_conn=db_conn) + yield await self.mass.database.async_get_track( + db_id, fulldata=False, db_conn=db_conn + ) else: yield item - async def async_get_artist_albums(self, artist_id: str, provider_id: str) -> List[Album]: - """Return (all) albums for an artist. Generator!""" + async def async_get_artist_albums( + self, artist_id: str, provider_id: str + ) -> List[Album]: + """Return (all) albums for an artist. Generator.""" async with self.mass.database.db_conn() as db_conn: if provider_id == "database": # albums from all providers item_ids = [] - artist = await self.mass.database.async_get_artist(artist_id, True, db_conn=db_conn) + artist = await self.mass.database.async_get_artist( + artist_id, True, db_conn=db_conn + ) for prov_id in artist.provider_ids: provider = self.mass.get_provider(prov_id.provider) - if not provider or not MediaType.Album in provider.supported_mediatypes: + if ( + not provider + or MediaType.Album not in provider.supported_mediatypes + ): continue async for item in self.async_get_artist_albums( prov_id.item_id, prov_id.provider ): - if not item.item_id in item_ids: + if item.item_id not in item_ids: yield item item_ids.append(item.item_id) else: @@ -345,7 +390,9 @@ class MusicManager: ) if db_id: # return database album instead if we have a match - yield await self.mass.database.async_get_album(db_id, db_conn=db_conn) + yield await self.mass.database.async_get_album( + db_id, db_conn=db_conn + ) else: yield item @@ -354,7 +401,7 @@ class MusicManager: async def async_get_library_artists( self, orderby: str = "name", provider_filter: str = None ) -> List[Artist]: - """Return all library artists, optionally filtered by provider. Generator!""" + """Return all library artists, optionally filtered by provider. Generator.""" async for item in self.mass.database.async_get_library_artists( provider_id=provider_filter, orderby=orderby ): @@ -363,7 +410,7 @@ class MusicManager: async def async_get_library_albums( self, orderby: str = "name", provider_filter: str = None ) -> List[Album]: - """Return all library albums, optionally filtered by provider. Generator!""" + """Return all library albums, optionally filtered by provider. Generator.""" async for item in self.mass.database.async_get_library_albums( provider_id=provider_filter, orderby=orderby ): @@ -372,7 +419,7 @@ class MusicManager: async def async_get_library_tracks( self, orderby: str = "name", provider_filter: str = None ) -> List[Track]: - """Return all library tracks, optionally filtered by provider. Generator!""" + """Return all library tracks, optionally filtered by provider. Generator.""" async for item in self.mass.database.async_get_library_tracks( provider_id=provider_filter, orderby=orderby ): @@ -381,7 +428,7 @@ class MusicManager: async def async_get_library_playlists( self, orderby: str = "name", provider_filter: str = None ) -> List[Playlist]: - """Return all library playlists, optionally filtered by provider. Generator!""" + """Return all library playlists, optionally filtered by provider. Generator.""" async for item in self.mass.database.async_get_library_playlists( provider_id=provider_filter, orderby=orderby ): @@ -390,7 +437,7 @@ class MusicManager: async def async_get_library_radios( self, orderby: str = "name", provider_filter: str = None ) -> List[Playlist]: - """Return all library radios, optionally filtered by provider. Generator!""" + """Return all library radios, optionally filtered by provider. Generator.""" async for item in self.mass.database.async_get_library_radios( provider_id=provider_filter, orderby=orderby ): @@ -424,7 +471,9 @@ class MusicManager: await self.__async_match_album(db_id) return db_id - async def __async_add_track(self, track: Track, album_id: Optional[str] = None) -> int: + async def __async_add_track( + self, track: Track, album_id: Optional[str] = None + ) -> int: """Add track to local db and return the new database id.""" track_artists = [] # we need to fetch track artists too @@ -437,7 +486,9 @@ class MusicManager: track.artists = track_artists # fetch album details - prefer optional provided album_id if album_id: - album_details = await self.async_get_album(album_id, track.provider, lazy=False) + album_details = await self.async_get_album( + album_id, track.provider, lazy=False + ) if album_details: track.album = album_details # make sure we have a database album @@ -454,7 +505,9 @@ class MusicManager: async def __async_get_artist_musicbrainz_id(self, artist: Artist): """Fetch musicbrainz id by performing search using the artist name, albums and tracks.""" # try with album first - async for lookup_album in self.async_get_artist_albums(artist.item_id, artist.provider): + async for lookup_album in self.async_get_artist_albums( + artist.item_id, artist.provider + ): if not lookup_album: continue musicbrainz_id = await self.mass.metadata.async_get_mb_artist_id( @@ -465,7 +518,9 @@ class MusicManager: if musicbrainz_id: return musicbrainz_id # fallback to track - async for lookup_track in self.async_get_artist_toptracks(artist.item_id, artist.provider): + async for lookup_track in self.async_get_artist_toptracks( + artist.item_id, artist.provider + ): if not lookup_track: continue musicbrainz_id = await self.mass.metadata.async_get_mb_artist_id( @@ -482,6 +537,7 @@ class MusicManager: async def __async_match_artist(self, db_artist_id: int): """ Try to find matching artists on all providers for the provided (database) artist_id. + This is used to link objects of different providers together. :attrib db_artist_id: Database artist_id. """ @@ -494,10 +550,14 @@ class MusicManager: for provider in self.mass.get_providers(ProviderType.MUSIC_PROVIDER): if provider.id in cur_providers: continue - LOGGER.debug("Trying to match artist %s on provider %s", artist.name, provider.name) + LOGGER.debug( + "Trying to match artist %s on provider %s", artist.name, provider.name + ) match_found = False # try to get a match with some reference albums of this artist - async for ref_album in self.async_get_artist_albums(artist.item_id, artist.provider): + async for ref_album in self.async_get_artist_albums( + artist.item_id, artist.provider + ): if match_found: break searchstr = "%s - %s" % (artist.name, ref_album.name) @@ -516,7 +576,9 @@ class MusicManager: continue # double safety check - artist must match exactly ! if not compare_strings( - search_result_item.artist.name, artist.name, strict=strictness + search_result_item.artist.name, + artist.name, + strict=strictness, ): continue # just load this item in the database where it will be strictly matched @@ -547,7 +609,9 @@ class MusicManager: if not search_result_item: continue if not compare_strings( - search_result_item.name, search_track.name, strict=strictness + search_result_item.name, + search_track.name, + strict=strictness, ): continue # double safety check - artist must match exactly ! @@ -558,20 +622,29 @@ class MusicManager: continue # load this item in the database where it will be strictly matched await self.async_get_artist( - match_artist.item_id, match_artist.provider, lazy=False + match_artist.item_id, + match_artist.provider, + lazy=False, ) match_found = True break if match_found: - LOGGER.debug("Found match for Artist %s on provider %s", artist.name, provider.name) + LOGGER.debug( + "Found match for Artist %s on provider %s", + artist.name, + provider.name, + ) else: LOGGER.warning( - "Could not find match for Artist %s on provider %s", artist.name, provider.name + "Could not find match for Artist %s on provider %s", + artist.name, + provider.name, ) async def __async_match_album(self, db_album_id: int): """ Try to find matching album on all providers for the provided (database) album_id. + This is used to link objects of different providers/qualities together. :attrib db_album_id: Database album_id. """ @@ -585,7 +658,9 @@ class MusicManager: for provider in providers: if provider.id in cur_providers: continue - LOGGER.debug("Trying to match album %s on provider %s", album.name, provider.name) + LOGGER.debug( + "Trying to match album %s on provider %s", album.name, provider.name + ) match_found = False searchstr = "%s - %s" % (album.artist.name, album.name) if album.version: @@ -616,15 +691,20 @@ class MusicManager: ) match_found = True if match_found: - LOGGER.debug("Found match for Album %s on provider %s", album.name, provider.name) + LOGGER.debug( + "Found match for Album %s on provider %s", album.name, provider.name + ) else: LOGGER.warning( - "Could not find match for Album %s on provider %s", album.name, provider.name + "Could not find match for Album %s on provider %s", + album.name, + provider.name, ) async def __async_match_track(self, db_track_id: int): """ Try to find matching track on all providers for the provided (database) track_id. + This is used to link objects of different providers/qualities together. :attrib db_track_id: Database track_id. """ @@ -634,7 +714,9 @@ class MusicManager: self._match_jobs.append(match_job_id) track = await self.mass.database.async_get_track(db_track_id, fulldata=False) for provider in self.mass.get_providers(ProviderType.MUSIC_PROVIDER): - LOGGER.debug("Trying to match track %s on provider %s", track.name, provider.name) + LOGGER.debug( + "Trying to match track %s on provider %s", track.name, provider.name + ) match_found = False searchstr = "%s - %s" % (track.artists[0].name, track.name) if track.version: @@ -660,7 +742,9 @@ class MusicManager: if artist_match_found: break for search_item_artist in search_result_item.artists: - if not compare_strings(artist.name, search_item_artist.name, strict=False): + if not compare_strings( + artist.name, search_item_artist.name, strict=False + ): continue # just load this item in the database where it will be strictly matched await self.async_get_track( @@ -673,10 +757,14 @@ class MusicManager: artist_match_found = True break if match_found: - LOGGER.debug("Found match for Track %s on provider %s", track.name, provider.name) + LOGGER.debug( + "Found match for Track %s on provider %s", track.name, provider.name + ) else: LOGGER.warning( - "Could not find match for Track %s on provider %s", track.name, provider.name + "Could not find match for Track %s on provider %s", + track.name, + provider.name, ) ################ Various convenience/helper methods ################ @@ -704,6 +792,7 @@ class MusicManager: ) -> SearchResult: """ Perform search on given provider. + :param search_query: Search query :param provider_id: provider_id of the provider to perform the search on. :param media_types: A list of media_types to include. All types if None. @@ -715,7 +804,9 @@ class MusicManager: provider = self.mass.get_provider(provider_id) cache_key = f"{provider_id}.search.{search_query}.{media_types}.{limit}" return await async_cached( - self.cache, cache_key, provider.async_search(search_query, media_types, limit) + self.cache, + cache_key, + provider.async_search(search_query, media_types, limit), ) async def async_global_search( @@ -723,6 +814,7 @@ class MusicManager: ) -> SearchResult: """ Perform global search for media items on all providers. + :param search_query: Search query. :param media_types: A list of media_types to include. All types if None. :param limit: number of items to return in the search (per type). @@ -750,7 +842,10 @@ class MusicManager: for media_item in media_items: # make sure we have a database item db_item = await self.async_get_item( - media_item.item_id, media_item.provider, media_item.media_type, lazy=False + media_item.item_id, + media_item.provider, + media_item.media_type, + lazy=False, ) if not db_item: continue @@ -758,7 +853,9 @@ class MusicManager: for prov in db_item.provider_ids: provider = self.mass.get_provider(prov.provider) if provider: - result = await provider.async_library_add(prov.item_id, media_item.media_type) + result = await provider.async_library_add( + prov.item_id, media_item.media_type + ) # mark as library item in internal db await self.mass.database.async_add_to_library( db_item.item_id, db_item.media_type, prov.provider @@ -771,7 +868,10 @@ class MusicManager: for media_item in media_items: # make sure we have a database item db_item = await self.async_get_item( - media_item.item_id, media_item.provider, media_item.media_type, lazy=False + media_item.item_id, + media_item.provider, + media_item.media_type, + lazy=False, ) if not db_item: continue @@ -816,7 +916,9 @@ class MusicManager: # 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 track is still available) - for track_version in sorted(track.provider_ids, key=lambda x: x.quality, reverse=True): + for track_version in sorted( + track.provider_ids, key=lambda x: x.quality, reverse=True + ): if track_version.provider == playlist_prov.provider: track_ids_to_add.append(track_version.item_id) break @@ -833,7 +935,9 @@ class MusicManager: ) # return result of the action on the provider provider = self.mass.get_provider(playlist_prov.provider) - return await provider.async_add_playlist_tracks(playlist_prov.item_id, track_ids_to_add) + return await provider.async_add_playlist_tracks( + playlist_prov.item_id, track_ids_to_add + ) return False async def async_remove_playlist_tracks(self, db_playlist_id, tracks: List[Track]): @@ -878,7 +982,9 @@ class MusicManager: img_url = "" # we only retrieve items that we already have in cache item = None - if await self.mass.database.async_get_database_id(provider_id, item_id, media_type): + if await self.mass.database.async_get_database_id( + provider_id, item_id, media_type + ): item = await self.async_get_item(item_id, provider_id, media_type) if not item: return "" @@ -930,6 +1036,7 @@ class MusicManager: async def async_music_provider_sync(self, prov_id: str): """ Sync a music provider. + param prov_id: {string} -- provider id to sync """ provider = self.mass.get_provider(prov_id) @@ -952,13 +1059,15 @@ class MusicManager: music_provider = self.mass.get_provider(provider_id) prev_db_ids = [ item.item_id - async for item in self.async_get_library_artists(provider_filter=provider_id) + async for item in self.async_get_library_artists( + provider_filter=provider_id + ) ] cur_db_ids = [] async for item in music_provider.async_get_library_artists(): db_item = await self.async_get_artist(item.item_id, provider_id, lazy=False) cur_db_ids.append(db_item.item_id) - if not db_item.item_id in prev_db_ids: + if db_item.item_id not in prev_db_ids: await self.mass.database.async_add_to_library( db_item.item_id, MediaType.Artist, provider_id ) @@ -986,7 +1095,7 @@ class MusicManager: if not db_album: LOGGER.error("provider %s album: %s", provider_id, str(item)) cur_db_ids.append(db_album.item_id) - if not db_album.item_id in prev_db_ids: + if db_album.item_id not in prev_db_ids: await self.mass.database.async_add_to_library( db_album.item_id, MediaType.Album, provider_id ) @@ -1007,9 +1116,11 @@ class MusicManager: ] cur_db_ids = [] async for item in music_provider.async_get_library_tracks(): - db_item = await self.async_get_track(item.item_id, provider_id=provider_id, lazy=False) + db_item = await self.async_get_track( + item.item_id, provider_id=provider_id, lazy=False + ) cur_db_ids.append(db_item.item_id) - if not db_item.item_id in prev_db_ids: + if db_item.item_id not in prev_db_ids: await self.mass.database.async_add_to_library( db_item.item_id, MediaType.Track, provider_id ) @@ -1026,7 +1137,9 @@ class MusicManager: music_provider = self.mass.get_provider(provider_id) prev_db_ids = [ item.item_id - async for item in self.async_get_library_playlists(provider_filter=provider_id) + async for item in self.async_get_library_playlists( + provider_filter=provider_id + ) ] cur_db_ids = [] async for playlist in music_provider.async_get_library_playlists(): @@ -1035,7 +1148,7 @@ class MusicManager: # always add to db because playlist attributes could have changed db_id = await self.mass.database.async_add_playlist(playlist) cur_db_ids.append(db_id) - if not db_id in prev_db_ids: + if db_id not in prev_db_ids: await self.mass.database.async_add_to_library( db_id, MediaType.Playlist, playlist.provider ) @@ -1049,7 +1162,7 @@ class MusicManager: @sync_task("radios") async def async_library_radios_sync(self, provider_id: str): - """sync library radios for given provider""" + """Sync library radios for given provider.""" music_provider = self.mass.get_provider(provider_id) prev_db_ids = [ item.item_id @@ -1065,8 +1178,10 @@ class MusicManager: if not db_id: db_id = await self.mass.database.async_add_radio(item) cur_db_ids.append(db_id) - if not db_id in prev_db_ids: - await self.mass.database.async_add_to_library(db_id, MediaType.Radio, provider_id) + if db_id not in prev_db_ids: + await self.mass.database.async_add_to_library( + db_id, MediaType.Radio, provider_id + ) # process deletions for db_id in prev_db_ids: if db_id not in cur_db_ids: diff --git a/music_assistant/player_manager.py b/music_assistant/player_manager.py index 9c7af47c..e29da1a7 100755 --- a/music_assistant/player_manager.py +++ b/music_assistant/player_manager.py @@ -1,11 +1,8 @@ -""" - PlayerManager: - Orchestrates all players from player providers and forwarding of commands and states. -""" +"""PlayerManager: Orchestrates all players from player providers.""" -from datetime import datetime import logging -from typing import List, Optional, Any +from datetime import datetime +from typing import Any, List, Optional from music_assistant.constants import ( CONF_ENABLED, @@ -42,9 +39,10 @@ LOGGER = logging.getLogger("mass") class PlayerManager: - """several helpers to handle playback through player providers""" + """Several helpers to handle playback through player providers.""" def __init__(self, mass): + """Initialize class.""" self.mass = mass self._players = {} self._org_players = {} @@ -55,7 +53,7 @@ class PlayerManager: self._player_controls_config_entries = [] async def async_setup(self): - """Async initialize of module""" + """Async initialize of module.""" self.mass.add_job(self.poll_task()) async def async_close(self): @@ -67,8 +65,8 @@ class PlayerManager: async def poll_task(self): """Check for updates on players that need to be polled.""" for player in self._org_players.values(): - if (player.should_poll - and (self._poll_ticks >= POLL_INTERVAL or player.state == PlayerState.Playing) + if player.should_poll and ( + self._poll_ticks >= POLL_INTERVAL or player.state == PlayerState.Playing ): # Just request update, value checking for changes is handled await self.async_update_player(player) @@ -101,7 +99,7 @@ class PlayerManager: @callback def get_player_queue(self, player_id: str) -> PlayerQueue: """Return player's queue by player_id or None if player does not exist.""" - if not player_id in self._players: + if player_id not in self._players: return None player = self._players[player_id] return self._player_queues.get(player.active_queue) @@ -109,7 +107,7 @@ class PlayerManager: @callback def get_player_control(self, control_id: str) -> PlayerControl: """Return PlayerControl by id.""" - if not control_id in self._controls: + if control_id not in self._controls: LOGGER.warning("PlayerControl %s is not available", control_id) return None return self._controls[control_id] @@ -135,11 +133,15 @@ class PlayerManager: await self.__async_create_player_state(player) if is_new_player: # create player queue - if not player.player_id in self._player_queues: - self._player_queues[player.player_id] = PlayerQueue(self.mass, player.player_id) + if player.player_id not in self._player_queues: + self._player_queues[player.player_id] = PlayerQueue( + self.mass, player.player_id + ) # TODO: turn on player if it was previously turned on ? LOGGER.info( - "New player added: %s/%s", player.provider_id, self._players[player.player_id].name + "New player added: %s/%s", + player.provider_id, + self._players[player.player_id].name, ) self.mass.signal_event(EVENT_PLAYER_ADDED, self._players[player.player_id]) @@ -154,7 +156,7 @@ class PlayerManager: """Update an existing player (or register as new if non existing).""" if not player: return - if not player.player_id in self._players: + if player.player_id not in self._players: return await self.async_add_player(player) await self.__async_create_player_state(player) @@ -179,7 +181,10 @@ class PlayerManager: # update all players using this playercontrol for player_id, player in self._players.items(): conf = self.mass.config.player_settings[player_id] - if control.id in [conf.get(CONF_POWER_CONTROL), conf.get(CONF_VOLUME_CONTROL)]: + if control.id in [ + conf.get(CONF_POWER_CONTROL), + conf.get(CONF_VOLUME_CONTROL), + ]: self.mass.add_job(self.async_update_player(player)) # SERVICE CALLS / PLAYER COMMANDS @@ -191,7 +196,8 @@ class PlayerManager: queue_opt: QueueOption = QueueOption.Play, ): """ - Play media item(s) on the given player + Play media item(s) on the given player. + :param player_id: player_id of the player to handle the command. :param media_item: media item(s) that should be played (single item or list of items) :param queue_opt: @@ -248,6 +254,7 @@ class PlayerManager: async def async_cmd_stop(self, player_id: str): """ Send STOP command to given player. + :param player_id: player_id of the player to handle the command. """ # TODO: redirect playback related commands to parent player? @@ -256,6 +263,7 @@ class PlayerManager: async def async_cmd_play(self, player_id: str): """ Send PLAY command to given player. + :param player_id: player_id of the player to handle the command. """ # power on at play request @@ -270,6 +278,7 @@ class PlayerManager: async def async_cmd_pause(self, player_id: str): """ Send PAUSE command to given player. + :param player_id: player_id of the player to handle the command. """ return await self.get_player_provider(player_id).async_cmd_pause(player_id) @@ -278,6 +287,7 @@ class PlayerManager: async def async_cmd_play_pause(self, player_id: str): """ Toggle play/pause on given player. + :param player_id: player_id of the player to handle the command. """ player = self.get_player(player_id) @@ -288,6 +298,7 @@ class PlayerManager: async def async_cmd_next(self, player_id: str): """ Send NEXT TRACK command to given player. + :param player_id: player_id of the player to handle the command. """ return await self.get_player_queue(player_id).async_next() @@ -295,6 +306,7 @@ class PlayerManager: async def async_cmd_previous(self, player_id: str): """ Send PREVIOUS TRACK command to given player. + :param player_id: player_id of the player to handle the command. """ return await self.get_player_queue(player_id).async_previous() @@ -302,6 +314,7 @@ class PlayerManager: async def async_cmd_power_on(self, player_id: str): """ Send POWER ON command to given player. + :param player_id: player_id of the player to handle the command. """ player = self._players[player_id] @@ -317,6 +330,7 @@ class PlayerManager: async def async_cmd_power_off(self, player_id: str): """ Send POWER OFF command to given player. + :param player_id: player_id of the player to handle the command. """ player = self._players[player_id] @@ -338,6 +352,7 @@ class PlayerManager: async def async_cmd_power_toggle(self, player_id: str): """ Send POWER TOGGLE command to given player. + :param player_id: player_id of the player to handle the command. """ player = self._players[player_id] @@ -348,6 +363,7 @@ class PlayerManager: async def async_cmd_volume_set(self, player_id: str, volume_level: int): """ Send volume level command to given player. + :param player_id: player_id of the player to handle the command. :param volume_level: volume level to set (0..100). """ @@ -381,7 +397,9 @@ class PlayerManager: child_player = self._players.get(child_player_id) if child_player and child_player.available and child_player.powered: cur_child_volume = child_player.volume_level - new_child_volume = cur_child_volume + (cur_child_volume * volume_dif_percent) + new_child_volume = cur_child_volume + ( + cur_child_volume * volume_dif_percent + ) await self.async_cmd_volume_set(child_player_id, new_child_volume) # regular volume command else: @@ -390,6 +408,7 @@ class PlayerManager: async def async_cmd_volume_up(self, player_id: str): """ Send volume UP command to given player. + :param player_id: player_id of the player to handle the command. """ player = self._players[player_id] @@ -401,6 +420,7 @@ class PlayerManager: async def async_cmd_volume_down(self, player_id: str): """ Send volume DOWN command to given player. + :param player_id: player_id of the player to handle the command. """ player = self._players[player_id] @@ -412,6 +432,7 @@ class PlayerManager: async def async_cmd_volume_mute(self, player_id: str, is_muted=False): """ Send MUTE command to given player. + :param player_id: player_id of the player to handle the command. :param is_muted: bool with the new mute state. """ @@ -421,14 +442,18 @@ class PlayerManager: # OTHER/HELPER FUNCTIONS - async def async_get_gain_correct(self, player_id: str, item_id: str, provider_id: str): + async def async_get_gain_correct( + self, player_id: str, item_id: str, provider_id: str + ): """Get gain correction for given player / track combination.""" player_conf = self.mass.config.get_player_config(player_id) if not player_conf["volume_normalisation"]: return 0 target_gain = int(player_conf["target_volume"]) fallback_gain = int(player_conf["fallback_gain_correct"]) - track_loudness = await self.mass.database.async_get_track_loudness(item_id, provider_id) + track_loudness = await self.mass.database.async_get_track_loudness( + item_id, provider_id + ) if track_loudness is None: gain_correct = fallback_gain else: @@ -446,7 +471,9 @@ class PlayerManager: async def __async_create_player_state(self, player: Player): """Create/update internal Player object with all calculated properties.""" self._org_players[player.player_id] = player - player_enabled = bool(self.mass.config.get_player_config(player.player_id)[CONF_ENABLED]) + player_enabled = bool( + self.mass.config.get_player_config(player.player_id)[CONF_ENABLED] + ) if player.player_id in self._players: player_state = self._players[player.player_id] else: @@ -475,7 +502,9 @@ class PlayerManager: player_state.config_entries = self.__get_player_config_entries(player) player_state.active_queue = active_queue if active_queue in self._player_queues: - player_state.cur_queue_item_id = self._player_queues[active_queue].cur_item_id + player_state.cur_queue_item_id = self._player_queues[ + active_queue + ].cur_item_id @callback def __get_player_name(self, player: Player): @@ -544,7 +573,7 @@ class PlayerManager: for group_player in self._players.values(): if not group_player.is_group_player: continue - if not player.player_id in group_player.group_childs: + if player.player_id not in group_player.group_childs: continue result.append(group_player.player_id) return result @@ -571,7 +600,9 @@ class PlayerManager: # append power control config entries power_controls = self.get_player_controls(PlayerControlType.POWER) if power_controls: - controls = [{"text": item.name, "value": item.id} for item in power_controls] + controls = [ + {"text": item.name, "value": item.id} for item in power_controls + ] entries.append( ConfigEntry( entry_key=CONF_POWER_CONTROL, @@ -583,7 +614,9 @@ class PlayerManager: # append volume control config entries volume_controls = self.get_player_controls(PlayerControlType.VOLUME) if volume_controls: - controls = [{"text": item.name, "value": item.id} for item in volume_controls] + controls = [ + {"text": item.name, "value": item.id} for item in volume_controls + ] entries.append( ConfigEntry( entry_key=CONF_VOLUME_CONTROL, @@ -597,7 +630,7 @@ class PlayerManager: @callback def __player_updated(self, player_id: str, changed_value: str): """Call when player is updated.""" - if not player_id in self._players: + if player_id not in self._players: return player = self._players[player_id] if not player.available and changed_value != "available": @@ -618,5 +651,3 @@ class PlayerManager: self.mass.add_job(self.async_update_player(child_player)) if player_id in self._player_queues and player.active_queue == player_id: self.mass.add_job(self._player_queues[player_id].async_update_state()) - - diff --git a/music_assistant/providers/__init__.py b/music_assistant/providers/__init__.py index e69de29b..2209974f 100644 --- a/music_assistant/providers/__init__.py +++ b/music_assistant/providers/__init__.py @@ -0,0 +1 @@ +"""Providers package.""" diff --git a/music_assistant/providers/chromecast/__init__.py b/music_assistant/providers/chromecast/__init__.py index 1dcb2fbe..02e6ad88 100644 --- a/music_assistant/providers/chromecast/__init__.py +++ b/music_assistant/providers/chromecast/__init__.py @@ -1,29 +1,14 @@ -#!/usr/bin/env python3 -# -*- coding:utf-8 -*- +"""ChromeCast playerprovider.""" -import asyncio import logging -import time -import types import uuid -from typing import List, Optional, Tuple +from typing import List -import aiohttp -import attr import pychromecast -import zeroconf -from music_assistant.constants import CONF_ENABLED, CONF_HOSTNAME, CONF_PORT -from music_assistant.mass import MusicAssistant -from music_assistant.models.config_entry import ConfigEntry, ConfigEntryType -from music_assistant.models.player import DeviceInfo, Player, PlayerState +from music_assistant.models.config_entry import ConfigEntry from music_assistant.models.player_queue import QueueItem from music_assistant.models.playerprovider import PlayerProvider -from music_assistant.utils import try_parse_int -from pychromecast.controllers.multizone import MultizoneController, MultizoneManager -from pychromecast.socket_client import ( - CONNECTION_STATUS_CONNECTED, - CONNECTION_STATUS_DISCONNECTED, -) +from pychromecast.controllers.multizone import MultizoneManager from .const import PROV_ID, PROV_NAME, PROVIDER_CONFIG_ENTRIES from .models import ChromecastInfo @@ -42,6 +27,8 @@ async def async_setup(mass): class ChromecastProvider(PlayerProvider): """Support for ChromeCast Audio PlayerProvider.""" + # pylint: disable=abstract-method + def __init__(self, *args, **kwargs): """Initialize.""" self.mz_mgr = MultizoneManager() @@ -66,17 +53,19 @@ class ChromecastProvider(PlayerProvider): return PROVIDER_CONFIG_ENTRIES async def async_on_start(self) -> bool: - """Called on startup. Handle initialization of the provider based on config.""" + """Handle initialization of the provider based on config.""" self._listener = pychromecast.CastListener( self.__chromecast_add_update_callback, self.__chromecast_remove_callback, self.__chromecast_add_update_callback, ) - self._browser = pychromecast.discovery.start_discovery(self._listener, self.mass.zeroconf) + self._browser = pychromecast.discovery.start_discovery( + self._listener, self.mass.zeroconf + ) return True async def async_on_stop(self): - """Called on shutdown. Handle correct close/cleanup of the provider on exit.""" + """Handle correct close/cleanup of the provider on exit.""" if not self._browser: return # stop discovery @@ -88,6 +77,7 @@ class ChromecastProvider(PlayerProvider): async def async_cmd_play_uri(self, player_id: str, uri: str): """ Play the specified uri/url on the goven player. + :param player_id: player_id of the player to handle the command. """ self.mass.add_job(self._players[player_id].play_uri, uri) @@ -95,6 +85,7 @@ class ChromecastProvider(PlayerProvider): async def async_cmd_stop(self, player_id: str): """ Send STOP command to given player. + :param player_id: player_id of the player to handle the command. """ self.mass.add_job(self._players[player_id].stop) @@ -102,6 +93,7 @@ class ChromecastProvider(PlayerProvider): async def async_cmd_play(self, player_id: str): """ Send STOP command to given player. + :param player_id: player_id of the player to handle the command. """ self.mass.add_job(self._players[player_id].play) @@ -109,6 +101,7 @@ class ChromecastProvider(PlayerProvider): async def async_cmd_pause(self, player_id: str): """ Send PAUSE command to given player. + :param player_id: player_id of the player to handle the command. """ self.mass.add_job(self._players[player_id].pause) @@ -116,6 +109,7 @@ class ChromecastProvider(PlayerProvider): async def async_cmd_next(self, player_id: str): """ Send NEXT TRACK command to given player. + :param player_id: player_id of the player to handle the command. """ self.mass.add_job(self._players[player_id].next) @@ -123,6 +117,7 @@ class ChromecastProvider(PlayerProvider): async def async_cmd_previous(self, player_id: str): """ Send PREVIOUS TRACK command to given player. + :param player_id: player_id of the player to handle the command. """ self.mass.add_job(self._players[player_id].previous) @@ -130,6 +125,7 @@ class ChromecastProvider(PlayerProvider): async def async_cmd_power_on(self, player_id: str): """ Send POWER ON command to given player. + :param player_id: player_id of the player to handle the command. """ self.mass.add_job(self._players[player_id].power_on) @@ -137,6 +133,7 @@ class ChromecastProvider(PlayerProvider): async def async_cmd_power_off(self, player_id: str): """ Send POWER OFF command to given player. + :param player_id: player_id of the player to handle the command. """ self.mass.add_job(self._players[player_id].power_off) @@ -144,6 +141,7 @@ class ChromecastProvider(PlayerProvider): async def async_cmd_volume_set(self, player_id: str, volume_level: int): """ Send volume level command to given player. + :param player_id: player_id of the player to handle the command. :param volume_level: volume level to set (0..100). """ @@ -152,6 +150,7 @@ class ChromecastProvider(PlayerProvider): async def async_cmd_volume_mute(self, player_id: str, is_muted=False): """ Send volume MUTE command to given player. + :param player_id: player_id of the player to handle the command. :param is_muted: bool with new mute state. """ @@ -159,15 +158,19 @@ class ChromecastProvider(PlayerProvider): async def async_cmd_queue_load(self, player_id: str, queue_items: List[QueueItem]): """ - Load/overwrite given items in the player's queue implementation + Load/overwrite given items in the player's queue implementation. + :param player_id: player_id of the player to handle the command. :param queue_items: a list of QueueItems """ self.mass.add_job(self._players[player_id].queue_load, queue_items) - async def async_cmd_queue_append(self, player_id: str, queue_items: List[QueueItem]): + async def async_cmd_queue_append( + self, player_id: str, queue_items: List[QueueItem] + ): """ Append new items at the end of the queue. + :param player_id: player_id of the player to handle the command. :param queue_items: a list of QueueItems """ @@ -188,10 +191,14 @@ class ChromecastProvider(PlayerProvider): port=service[5], ) player_id = cast_info.uuid - LOGGER.debug("Chromecast discovered: %s (%s)", cast_info.friendly_name, player_id) + LOGGER.debug( + "Chromecast discovered: %s (%s)", cast_info.friendly_name, player_id + ) if player_id in self._players: # player already added, the player will take care of reconnects itself. - LOGGER.debug("Player is already added: %s (%s)", cast_info.friendly_name, player_id) + LOGGER.debug( + "Player is already added: %s (%s)", cast_info.friendly_name, player_id + ) else: player = ChromecastPlayer(self.mass, cast_info) self._players[player_id] = player @@ -199,153 +206,9 @@ class ChromecastProvider(PlayerProvider): self.mass.add_job(self._players[player_id].set_cast_info, cast_info) def __chromecast_remove_callback(self, cast_uuid, cast_service_name, cast_service): + """Handle a Chromecast removed event.""" # pylint: disable=unused-argument player_id = str(cast_service[1]) friendly_name = cast_service[3] LOGGER.debug("Chromecast removed: %s - %s", friendly_name, player_id) self.mass.add_job(self.mass.player_manager.async_remove_player(player_id)) - - -# class StatusListener: -# def __init__(self, player_id, status_callback, mass): -# self.__handle_callback = status_callback -# self.mass = mass -# self.player_id = player_id - -# def new_cast_status(self, status): -# """chromecast status changed (like volume etc.)""" -# self.mass.run_task(self.__handle_callback(caststatus=status)) - -# def new_media_status(self, status): -# """mediacontroller has new state""" -# self.mass.run_task(self.__handle_callback(mediastatus=status)) - -# def new_connection_status(self, status): -# """will be called when the connection changes""" -# if status.status == CONNECTION_STATUS_DISCONNECTED: -# # schedule a new scan which will handle reconnects and group parent changes -# self.mass.loop.run_in_executor( -# None, self.mass.player_manager.providers[PROV_ID].run_chromecast_discovery -# ) - - -# class MZListener: -# def __init__(self, mz, callback, loop): -# self._mz = mz -# self._loop = loop -# self.__handle_group_members_update = callback - -# def multizone_member_added(self, uuid): -# asyncio.run_coroutine_threadsafe( -# self.__handle_group_members_update(self._mz, added_player=str(uuid)), -# self._loop, -# ) - -# def multizone_member_removed(self, uuid): -# asyncio.run_coroutine_threadsafe( -# self.__handle_group_members_update(self._mz, removed_player=str(uuid)), -# self._loop, -# ) - -# def multizone_status_received(self): -# asyncio.run_coroutine_threadsafe( -# self.__handle_group_members_update(self._mz), self._loop -# ) - - -# async def async_ __report_progress(self): -# """report current progress while playing""" -# # chromecast does not send updates of the player's progress (cur_time) -# # so we need to send it in periodically -# while self._state == PlayerState.Playing: -# self.cur_time = self.media_status.adjusted_current_time -# await asyncio.sleep(1) -# self.__cc_report_progress_task = None - -# async def async_ handle_player_state(self, caststatus=None, mediastatus=None): -# """handle a player state message from the socket""" -# # handle generic cast status -# if caststatus: -# self.muted = caststatus.volume_muted -# self.volume_level = caststatus.volume_level * 100 -# self.name = self._chromecast.name -# # handle media status -# if mediastatus: -# if mediastatus.player_state in ["PLAYING", "BUFFERING"]: -# self.state = PlayerState.Playing -# self.powered = True -# elif mediastatus.player_state == "PAUSED": -# self.state = PlayerState.Paused -# else: -# self.state = PlayerState.Stopped -# self.current_uri = mediastatus.content_id -# self.cur_time = mediastatus.adjusted_current_time -# if ( -# self._state == PlayerState.Playing -# and self.__cc_report_progress_task is None -# ): -# self.__cc_report_progress_task = self.mass.add_job( -# self.__report_progress() -# ) - -# def __chromecast_discovered(self, cast_info): -# """callback when a (new) chromecast device is discovered""" -# player_id = cast_info.uuid -# player = self.mass.player_manager.get_player_sync(player_id) -# if self.mass.player_manager.get_player_sync(player_id): -# # player already added, the player will take care of reconnects itself. -# LOGGER.warning("Player is already added: %s", player_id) -# self.mass.add_job(player.async_set_cast_info(cast_info)) -# else: -# player = ChromecastPlayer(self.mass, cast_info) -# # player.cc = chromecast -# # player.mz = None - -# # # register status listeners -# # status_listener = StatusListener( -# # player_id, player.handle_player_state, self.mass -# # ) -# # if chromecast.cast_type == "group": -# # mz = MultizoneController(chromecast.uuid) -# # mz.register_listener( -# # MZListener(mz, self.__handle_group_members_update, self.mass.loop) -# # ) -# # chromecast.register_handler(mz) -# # player.mz = mz -# # chromecast.register_connection_listener(status_listener) -# # chromecast.register_status_listener(status_listener) -# # chromecast.media_controller.register_status_listener(status_listener) -# # player.cc.wait() -# self.mass.run_task(self.add_player(player)) - -# def __update_group_players(self): -# """update childs of all group players""" -# for player in self.players: -# if player.cc.cast_type == "group": -# player.mz.update_members() - -# async def async_ __handle_group_members_update( -# self, mz, added_player=None, removed_player=None -# ): -# """handle callback from multizone manager""" -# group_player_id = str(uuid.UUID(mz._uuid)) -# group_player = await self.get_player(group_player_id) -# if added_player: -# player_id = str(uuid.UUID(added_player)) -# child_player = await self.get_player(player_id) -# if child_player and player_id != group_player_id: -# group_player.add_group_child(player_id) -# LOGGER.debug("%s added to %s", child_player.name, group_player.name) -# elif removed_player: -# player_id = str(uuid.UUID(removed_player)) -# group_player.remove_group_child(player_id) -# LOGGER.debug("%s removed from %s", player_id, group_player.name) -# else: -# for member in mz.members: -# player_id = str(uuid.UUID(member)) -# child_player = await self.get_player(player_id) -# if not child_player or player_id == group_player_id: -# continue -# if not player_id in group_player.group_childs: -# group_player.add_group_child(player_id) -# LOGGER.debug("%s added to %s", child_player.name, group_player.name) diff --git a/music_assistant/providers/chromecast/const.py b/music_assistant/providers/chromecast/const.py index 19eefd3d..fe355031 100644 --- a/music_assistant/providers/chromecast/const.py +++ b/music_assistant/providers/chromecast/const.py @@ -1,6 +1,4 @@ """Constants for the implementation.""" -from music_assistant.constants import CONF_ENABLED -from music_assistant.models.config_entry import ConfigEntry, ConfigEntryType PROV_ID = "chromecast" PROV_NAME = "Chromecast" diff --git a/music_assistant/providers/chromecast/models.py b/music_assistant/providers/chromecast/models.py index cb654a9a..1664757e 100644 --- a/music_assistant/providers/chromecast/models.py +++ b/music_assistant/providers/chromecast/models.py @@ -1,14 +1,13 @@ +""" +Class to hold all data about a chromecast for creating connections. -"""Class to hold all data about a chromecast for creating connections. - This also has the same attributes as the mDNS fields by zeroconf. +This also has the same attributes as the mDNS fields by zeroconf. """ import logging from dataclasses import dataclass, field from typing import Optional, Tuple from pychromecast.const import CAST_MANUFACTURERS -from pychromecast.controllers.multizone import MultizoneController - from .const import PROV_ID @@ -19,6 +18,7 @@ DEFAULT_PORT = 8009 @dataclass() class ChromecastInfo: """Class to hold all data about a chromecast for creating connections. + This also has the same attributes as the mDNS fields by zeroconf. """ @@ -30,7 +30,7 @@ class ChromecastInfo: friendly_name: Optional[str] = "" def __post_init__(self): - """Always convert UUID to string.""" + """Convert UUID to string.""" self.uuid = str(self.uuid) @property @@ -53,6 +53,7 @@ class ChromecastInfo: class CastStatusListener: """Helper class to handle pychromecast status callbacks. + Necessary because a CastDevice entity can create a new socket client and therefore callbacks from multiple chromecast connections can potentially arrive. This class allows invalidating past chromecast objects. @@ -90,7 +91,9 @@ class CastStatusListener: def added_to_multizone(self, group_uuid): """Handle the cast added to a group.""" - LOGGER.debug("Player %s is added to group %s", self._cast_device.name, group_uuid) + LOGGER.debug( + "Player %s is added to group %s", self._cast_device.name, group_uuid + ) def removed_from_multizone(self, group_uuid): """Handle the cast removed from a group.""" @@ -107,6 +110,7 @@ class CastStatusListener: def invalidate(self): """Invalidate this status listener. + All following callbacks won't be forwarded. """ # pylint: disable=protected-access diff --git a/music_assistant/providers/chromecast/player.py b/music_assistant/providers/chromecast/player.py index 5481866e..4849a042 100644 --- a/music_assistant/providers/chromecast/player.py +++ b/music_assistant/providers/chromecast/player.py @@ -1,10 +1,11 @@ """Representation of a Cast device on the network.""" import logging -from typing import List, Optional +import uuid from datetime import datetime +from typing import List, Optional + import pychromecast -import uuid -from music_assistant.models.player import DeviceInfo, Player, PlayerFeature, PlayerState +from music_assistant.models.player import DeviceInfo, PlayerFeature, PlayerState from music_assistant.models.player_queue import QueueItem from music_assistant.utils import compare_strings from pychromecast.controllers.multizone import MultizoneController @@ -13,7 +14,7 @@ from pychromecast.socket_client import ( CONNECTION_STATUS_DISCONNECTED, ) -from .const import PLAYER_CONFIG_ENTRIES, PROV_ID, PROVIDER_CONFIG_ENTRIES +from .const import PLAYER_CONFIG_ENTRIES, PROV_ID from .models import CastStatusListener, ChromecastInfo LOGGER = logging.getLogger(PROV_ID) @@ -22,6 +23,7 @@ PLAYER_FEATURES = [PlayerFeature.QUEUE] class ChromecastPlayer: """Representation of a Cast device on the network. + This class is the holder of the pychromecast.Chromecast object and handles all reconnects and audio group changing "elected leader" itself. @@ -40,6 +42,7 @@ class ChromecastPlayer: self.media_status = None self.media_status_received = None self.mz_mgr = None + self.mz_manager = None self._available = False self._powered = False self._status_listener: Optional[CastStatusListener] = None @@ -54,7 +57,9 @@ class ChromecastPlayer: @property def name(self): """Return name of this player.""" - return self._chromecast.name if self._chromecast else self._cast_info.friendly_name + return ( + self._chromecast.name if self._chromecast else self._cast_info.friendly_name + ) @property def powered(self): @@ -126,8 +131,14 @@ class ChromecastPlayer: @property def group_childs(self): """Return group_childs.""" - if self._cast_info.is_audio_group and self._chromecast and not self._is_speaker_group: - return [str(uuid.UUID(item)) for item in self._chromecast.mz_controller.members] + if ( + self._cast_info.is_audio_group + and self._chromecast + and not self._is_speaker_group + ): + return [ + str(uuid.UUID(item)) for item in self._chromecast.mz_controller.members + ] return [] @property @@ -179,7 +190,9 @@ class ChromecastPlayer: """Disconnect Chromecast object if it is set.""" if self._chromecast is None: return - LOGGER.warning("[%s] Disconnecting from chromecast socket", self._cast_info.friendly_name) + LOGGER.warning( + "[%s] Disconnecting from chromecast socket", self._cast_info.friendly_name + ) self._available = False self.mass.add_job(self._chromecast.disconnect) self._invalidate() @@ -191,7 +204,6 @@ class ChromecastPlayer: self.media_status = None self.media_status_received = None self.mz_mgr = None - self.mz_controller = None if self._status_listener is not None: self._status_listener.invalidate() self._status_listener = None @@ -206,7 +218,9 @@ class ChromecastPlayer: self._cast_info.is_audio_group and self._chromecast.mz_controller and self._chromecast.mz_controller.members - and compare_strings(self._chromecast.mz_controller.members[0], self.player_id) + and compare_strings( + self._chromecast.mz_controller.members[0], self.player_id + ) ) self.mass.add_job(self.mass.player_manager.async_update_player(self)) @@ -332,7 +346,7 @@ class ChromecastPlayer: self._chromecast.play_media(uri, "audio/flac") def queue_load(self, queue_items: List[QueueItem]): - """load (overwrite) queue with new items""" + """Load (overwrite) queue with new items.""" if not self._chromecast.socket_client.is_connected: LOGGER.warning("Ignore player command: Socket client is not connected.") return @@ -352,9 +366,7 @@ class ChromecastPlayer: self.queue_append(queue_items[51:]) def queue_append(self, queue_items: List[QueueItem]): - """ - append new items at the end of the queue - """ + """Append new items at the end of the queue.""" if not self._chromecast.socket_client.is_connected: LOGGER.warning("Ignore player command: Socket client is not connected.") return @@ -364,7 +376,7 @@ class ChromecastPlayer: self.__send_player_queue(queuedata) def __create_queue_items(self, tracks): - """create list of CC queue items from tracks""" + """Create list of CC queue items from tracks.""" queue_items = [] for track in tracks: queue_item = self.__create_queue_item(track) @@ -372,7 +384,7 @@ class ChromecastPlayer: return queue_items def __create_queue_item(self, track): - """create CC queue item from track info""" + """Create CC queue item from track info.""" player_queue = self.mass.player_manager.get_player_queue(self.player_id) return { "opt_itemId": track.queue_item_id, @@ -399,8 +411,9 @@ class ChromecastPlayer: } def __send_player_queue(self, queuedata): - """Send new data to the CC queue""" + """Send new data to the CC queue.""" media_controller = self._chromecast.media_controller + # pylint: disable=protected-access receiver_ctrl = media_controller._socket_client.receiver_controller def send_queue(): @@ -409,12 +422,14 @@ class ChromecastPlayer: media_controller.send_message(queuedata, inc_session_id=False) if not media_controller.status.media_session_id: - receiver_ctrl.launch_app(media_controller.app_id, callback_function=send_queue) + receiver_ctrl.launch_app( + media_controller.app_id, callback_function=send_queue + ) else: send_queue() -def chunks(l, n): - """Yield successive n-sized chunks from l.""" - for i in range(0, len(l), n): - yield l[i : i + n] +def chunks(_list, chunk_size): + """Yield successive n-sized chunks from list.""" + for i in range(0, len(_list), chunk_size): + yield _list[i : i + chunk_size] diff --git a/music_assistant/providers/demo/__init__.py b/music_assistant/providers/demo/__init__.py index 2a2bf9ea..09c851c7 100644 --- a/music_assistant/providers/demo/__init__.py +++ b/music_assistant/providers/demo/__init__.py @@ -2,7 +2,8 @@ from .demo_playerprovider import DemoPlayerProvider + async def async_setup(mass): """Perform async setup of this Plugin/Provider.""" prov = DemoPlayerProvider() - await mass.async_register_provider(prov) \ No newline at end of file + await mass.async_register_provider(prov) diff --git a/music_assistant/providers/demo/demo_musicprovider.py b/music_assistant/providers/demo/demo_musicprovider.py index e69de29b..dfee4949 100644 --- a/music_assistant/providers/demo/demo_musicprovider.py +++ b/music_assistant/providers/demo/demo_musicprovider.py @@ -0,0 +1 @@ +"""Demo music provider.""" diff --git a/music_assistant/providers/demo/demo_playerprovider.py b/music_assistant/providers/demo/demo_playerprovider.py index 5a61e7a4..45d91f0a 100644 --- a/music_assistant/providers/demo/demo_playerprovider.py +++ b/music_assistant/providers/demo/demo_playerprovider.py @@ -1,24 +1,20 @@ """Demo/test providers.""" -from abc import abstractmethod -from dataclasses import dataclass -from typing import List import functools +from typing import List +import vlc from music_assistant.models.config_entry import ConfigEntry from music_assistant.models.player import DeviceInfo, Player, PlayerFeature, PlayerState from music_assistant.models.player_queue import QueueItem from music_assistant.models.playerprovider import PlayerProvider -import vlc PROV_ID = "demo_player" PROV_NAME = "Demo/Test players" class DemoPlayerProvider(PlayerProvider): - """ - Demo PlayerProvider which provides fake players. - """ + """Demo PlayerProvider which provides fake players.""" def __init__(self, *args, **kwargs): """Initialize.""" @@ -41,7 +37,7 @@ class DemoPlayerProvider(PlayerProvider): return [] async def async_on_start(self) -> bool: - """Called on startup. Handle initialization of the provider based on config.""" + """Handle initialization of the provider based on config.""" # create some fake players for count in range(5)[1:]: player_id = f"demo_{count}" @@ -94,7 +90,7 @@ class DemoPlayerProvider(PlayerProvider): return True async def async_on_stop(self): - """Called on shutdown. Handle correct close/cleanup of the provider on exit.""" + """Handle correct close/cleanup of the provider on exit.""" for player_id, player in self._players.items(): player.vlc_player.release() player.vlc_instance.release() @@ -103,7 +99,8 @@ class DemoPlayerProvider(PlayerProvider): self._players = {} def player_event(self, player_id, event): - """Called on vlc player events.""" + """Call on vlc player events.""" + # pylint: disable = unused-argument vlc_player: vlc.MediaPlayer = self._players[player_id].vlc_player self._players[player_id].muted = vlc_player.audio_get_mute() self._players[player_id].volume_level = vlc_player.audio_get_volume() @@ -115,13 +112,16 @@ class DemoPlayerProvider(PlayerProvider): else: self._players[player_id].state = PlayerState.Stopped self._players[player_id].elapsed_time = int(vlc_player.get_time() / 1000) - self.mass.add_job(self.mass.player_manager.async_update_player(self._players[player_id])) + self.mass.add_job( + self.mass.player_manager.async_update_player(self._players[player_id]) + ) # SERVICE CALLS / PLAYER COMMANDS async def async_cmd_play_uri(self, player_id: str, uri: str): """ Play the specified uri/url on the given player. + :param player_id: player_id of the player to handle the command. """ # self._players[player_id].current_uri = uri @@ -132,13 +132,15 @@ class DemoPlayerProvider(PlayerProvider): async def async_cmd_stop(self, player_id: str): """ Send STOP command to given player. + :param player_id: player_id of the player to handle the command. """ self.mass.add_job(self._players[player_id].vlc_player.stop) async def async_cmd_play(self, player_id: str): """ - Send STOP command to given player. + Send PLAY command to given player. + :param player_id: player_id of the player to handle the command. """ if self._players[player_id].vlc_player.get_media(): @@ -147,6 +149,7 @@ class DemoPlayerProvider(PlayerProvider): async def async_cmd_pause(self, player_id: str): """ Send PAUSE command to given player. + :param player_id: player_id of the player to handle the command. """ self.mass.add_job(self._players[player_id].vlc_player.pause) @@ -154,6 +157,7 @@ class DemoPlayerProvider(PlayerProvider): async def async_cmd_next(self, player_id: str): """ Send NEXT TRACK command to given player. + :param player_id: player_id of the player to handle the command. """ self.mass.add_job(self._players[player_id].vlc_player.next_chapter) @@ -161,6 +165,7 @@ class DemoPlayerProvider(PlayerProvider): async def async_cmd_previous(self, player_id: str): """ Send PREVIOUS TRACK command to given player. + :param player_id: player_id of the player to handle the command. """ self.mass.add_job(self._players[player_id].vlc_player.previous_chapter) @@ -168,31 +173,41 @@ class DemoPlayerProvider(PlayerProvider): async def async_cmd_power_on(self, player_id: str): """ Send POWER ON command to given player. + :param player_id: player_id of the player to handle the command. """ self._players[player_id].powered = True - self.mass.add_job(self.mass.player_manager.async_update_player(self._players[player_id])) + self.mass.add_job( + self.mass.player_manager.async_update_player(self._players[player_id]) + ) async def async_cmd_power_off(self, player_id: str): """ Send POWER OFF command to given player. + :param player_id: player_id of the player to handle the command. """ self.mass.add_job(self._players[player_id].vlc_player.stop) self._players[player_id].powered = False - self.mass.add_job(self.mass.player_manager.async_update_player(self._players[player_id])) + self.mass.add_job( + self.mass.player_manager.async_update_player(self._players[player_id]) + ) async def async_cmd_volume_set(self, player_id: str, volume_level: int): """ Send volume level command to given player. + :param player_id: player_id of the player to handle the command. :param volume_level: volume level to set (0..100). """ - self.mass.add_job(self._players[player_id].vlc_player.audio_set_volume, volume_level) + self.mass.add_job( + self._players[player_id].vlc_player.audio_set_volume, volume_level + ) async def async_cmd_volume_mute(self, player_id: str, is_muted=False): """ Send volume MUTE command to given player. + :param player_id: player_id of the player to handle the command. :param is_muted: bool with new mute state. """ @@ -203,7 +218,8 @@ class DemoPlayerProvider(PlayerProvider): async def async_cmd_queue_play_index(self, player_id: str, index: int): """ - Play item at index X on player's queue + Play item at index X on player's queue. + :param player_id: player_id of the player to handle the command. :param index: (int) index of the queue item that should start playing """ @@ -211,7 +227,8 @@ class DemoPlayerProvider(PlayerProvider): async def async_cmd_queue_load(self, player_id: str, queue_items: List[QueueItem]): """ - Load/overwrite given items in the player's queue implementation + Load/overwrite given items in the player's queue implementation. + :param player_id: player_id of the player to handle the command. :param queue_items: a list of QueueItems """ @@ -222,6 +239,7 @@ class DemoPlayerProvider(PlayerProvider): ): """ Insert new items at position X into existing queue. + If insert_at_index 0 or None, will start playing newly added item(s) :param player_id: player_id of the player to handle the command. :param queue_items: a list of QueueItems @@ -229,17 +247,23 @@ class DemoPlayerProvider(PlayerProvider): """ raise NotImplementedError - async def async_cmd_queue_append(self, player_id: str, queue_items: List[QueueItem]): + async def async_cmd_queue_append( + self, player_id: str, queue_items: List[QueueItem] + ): """ Append new items at the end of the queue. + :param player_id: player_id of the player to handle the command. :param queue_items: a list of QueueItems """ raise NotImplementedError - async def async_cmd_queue_update(self, player_id: str, queue_items: List[QueueItem]): + async def async_cmd_queue_update( + self, player_id: str, queue_items: List[QueueItem] + ): """ Overwrite the existing items in the queue, used for reordering. + :param player_id: player_id of the player to handle the command. :param queue_items: a list of QueueItems """ @@ -248,6 +272,7 @@ class DemoPlayerProvider(PlayerProvider): async def async_cmd_queue_clear(self, player_id: str): """ Clear the player's queue. + :param player_id: player_id of the player to handle the command. """ raise NotImplementedError diff --git a/music_assistant/providers/file/file.py b/music_assistant/providers/file/file.py index 096fecc1..7aa7f227 100644 --- a/music_assistant/providers/file/file.py +++ b/music_assistant/providers/file/file.py @@ -1,8 +1,11 @@ """Filesystem musicprovider support for MusicAssistant.""" +# pylint: skip-file +# flake8: noqa import base64 import os from typing import List +import taglib from music_assistant.constants import CONF_ENABLED from music_assistant.models.media_types import ( Album, @@ -14,7 +17,6 @@ from music_assistant.models.media_types import ( ) from music_assistant.models.musicprovider import MusicProvider from music_assistant.utils import LOGGER, parse_title_and_version -import taglib PROV_NAME = "Local files and playlists" PROV_CLASS = "FileProvider" @@ -111,9 +113,7 @@ class FileProvider(MusicProvider): artist.item_id = prov_item_id artist.provider = self.prov_id artist.name = name - artist.ids.append( - {"provider": self.prov_id, "item_id": artist.item_id} - ) + artist.ids.append({"provider": self.prov_id, "item_id": artist.item_id}) return artist async def async_get_album(self, prov_item_id) -> Album: @@ -164,9 +164,7 @@ class FileProvider(MusicProvider): playlist.provider = self.prov_id playlist.name = itempath.split(os.sep)[-1].replace(".m3u", "") playlist.is_editable = True - playlist.ids.append( - {"provider": self.prov_id, "item_id": prov_item_id} - ) + playlist.ids.append({"provider": self.prov_id, "item_id": prov_item_id}) playlist.owner = "disk" playlist.checksum = os.path.getmtime(itempath) return playlist diff --git a/music_assistant/providers/home_assistant/__init__.py b/music_assistant/providers/home_assistant/__init__.py index a2a90f2d..7e10da23 100644 --- a/music_assistant/providers/home_assistant/__init__.py +++ b/music_assistant/providers/home_assistant/__init__.py @@ -1,9 +1,6 @@ """Plugin that enables integration with Home Assistant.""" -import asyncio -import functools import logging -import os from typing import List import slugify as slug @@ -39,7 +36,9 @@ CONFIG_ENTRY_URL = ConfigEntry( entry_key=CONF_URL, entry_type=ConfigEntryType.STRING, description_key="hass_url" ) CONFIG_ENTRY_TOKEN = ConfigEntry( - entry_key=CONF_TOKEN, entry_type=ConfigEntryType.PASSWORD, description_key="hass_token" + entry_key=CONF_TOKEN, + entry_type=ConfigEntryType.PASSWORD, + description_key="hass_token", ) CONFIG_ENTRY_PUBLISH_PLAYERS = ConfigEntry( entry_key=CONF_PUBLISH_PLAYERS, @@ -58,8 +57,8 @@ async def async_setup(mass): class HomeAssistantPlugin(Provider): - """ - Homeassistant plugin + """Homeassistant plugin. + allows publishing of our players to hass allows using hass entities (like switches, media_players or gui inputs) to be triggered """ @@ -112,12 +111,14 @@ class HomeAssistantPlugin(Provider): return entries async def async_on_start(self) -> bool: - """Called on startup. Handle initialization of the provider based on config.""" + """Handle initialization of the provider based on config.""" config = self.mass.config.get_provider_config(PROV_ID) if IS_SUPERVISOR: self._hass = HomeAssistant(loop=self.mass.loop) else: - self._hass = HomeAssistant(config[CONF_URL], config[CONF_TOKEN], loop=self.mass.loop) + self._hass = HomeAssistant( + config[CONF_URL], config[CONF_TOKEN], loop=self.mass.loop + ) # register callbacks self._hass.register_event_callback(self.__async_hass_event) self.mass.add_event_listener( @@ -129,20 +130,20 @@ class HomeAssistantPlugin(Provider): return True async def async_on_stop(self): - """Called on shutdown. Handle correct close/cleanup of the provider on exit.""" + """Handle correct close/cleanup of the provider on exit.""" for task in self._tasks: task.cancel() if self._hass: await self._hass.async_close() async def __async_mass_event(self, event, event_data): - """Received event from Music Assistant""" + """Receive event from Music Assistant.""" if event in [EVENT_PLAYER_CHANGED, EVENT_PLAYER_ADDED]: await self.__async_publish_player(event_data) # TODO: player removals async def __async_hass_event(self, event_type, event_data): - """Received event from Home Assistant""" + """Receive event from Home Assistant.""" if event_type == EVENT_STATE_CHANGED: if event_data["entity_id"] in self._tracked_entities: new_state = event_data["new_state"] @@ -184,7 +185,9 @@ class HomeAssistantPlugin(Provider): await self.mass.player_manager.async_cmd_volume_down(player_id) elif service == "volume_set": volume_level = service_data["volume_level"] * 100 - await self.mass.player_manager.async_cmd_volume_set(player_id, volume_level) + await self.mass.player_manager.async_cmd_volume_set( + player_id, volume_level + ) elif service == "media_play": await self.mass.player_manager.async_cmd_play(player_id) elif service == "media_pause": @@ -198,7 +201,11 @@ class HomeAssistantPlugin(Provider): elif service in ["play_media", "select_source"]: return await self.__async_handle_play_media(player_id, service_data) else: - LOGGER.error("%s service is unhandled. Service data: %s", service, service_data) + LOGGER.error( + "%s service is unhandled. Service data: %s", + service, + service_data, + ) async def __async_handle_play_media(self, player_id, service_data): """Handle play media request from homeassistant.""" @@ -206,7 +213,7 @@ class HomeAssistantPlugin(Provider): if not media_content_id: media_content_id = service_data.get("source") queue_opt = "add" if service_data.get("enqueue") else "play" - if not "://" in media_content_id: + if "://" not in media_content_id: media_items = [] for playlist_str in media_content_id.split(","): playlist_str = playlist_str.strip() @@ -216,7 +223,9 @@ class HomeAssistantPlugin(Provider): if playlist: media_items.append(playlist) else: - radio = await self.mass.music_manager.async_get_radio_by_name(playlist_str) + radio = await self.mass.music_manager.async_get_radio_by_name( + playlist_str + ) if radio: media_items.append(radio) queue_opt = "play" @@ -228,7 +237,9 @@ class HomeAssistantPlugin(Provider): playlist = await self.mass.music_manager.async_getplaylist( "spotify", media_content_id.split(":")[-1] ) - return await self.mass.player_manager.async_play_media(player_id, playlist, queue_opt) + return await self.mass.player_manager.async_play_media( + player_id, playlist, queue_opt + ) async def __async_publish_player(self, player: Player): """Publish player details to Home Assistant.""" @@ -237,11 +248,13 @@ class HomeAssistantPlugin(Provider): if not player.available: return player_id = player.player_id - entity_id = "media_player.mass_" + slug.slugify(player.name, separator="_").lower() + entity_id = ( + "media_player.mass_" + slug.slugify(player.name, separator="_").lower() + ) player_queue = self.mass.player_manager.get_player_queue(player_id) cur_item = player_queue.cur_item if player_queue else None state_attributes = { - "supported_features": 196541, # https://github.com/home-assistant/core/blob/dev/homeassistant/components/media_player/const.py#L59 + "supported_features": 196541, "friendly_name": player.name, "source_list": self._sources, "source": "unknown", @@ -251,14 +264,19 @@ class HomeAssistantPlugin(Provider): "media_duration": cur_item.duration if cur_item else None, "media_position": player_queue.cur_item_time if player_queue else None, "media_title": cur_item.name if cur_item else None, - "media_artist": cur_item.artists[0].name if cur_item and cur_item.artists else None, - "media_album_name": cur_item.album.name if cur_item and cur_item.album else None, + "media_artist": cur_item.artists[0].name + if cur_item and cur_item.artists + else None, + "media_album_name": cur_item.album.name + if cur_item and cur_item.album + else None, "entity_picture": "", "mass_player_id": player_id, } if cur_item: host = self.mass.web.internal_url item_type = "radio" if cur_item.media_type == MediaType.Radio else "track" + # pylint: disable=line-too-long img_url = f"{host}/api/{item_type}/{cur_item.item_id}/thumb?provider={cur_item.provider}" state_attributes["entity_picture"] = img_url self._published_players[entity_id] = player.player_id @@ -273,7 +291,8 @@ class HomeAssistantPlugin(Provider): async for playlist in self.mass.music_manager.async_get_library_playlists() ] self._sources += [ - playlist.name async for playlist in self.mass.music_manager.async_get_library_radios() + playlist.name + async for playlist in self.mass.music_manager.async_get_library_radios() ] @callback @@ -315,7 +334,11 @@ class HomeAssistantPlugin(Provider): if entity_id.startswith("media_player.mass_"): continue result.append( - {"value": f"volume_{entity_id}", "text": entity_name, "entity_id": entity_id} + { + "value": f"volume_{entity_id}", + "text": entity_name, + "entity_id": entity_id, + } ) return result @@ -326,14 +349,18 @@ class HomeAssistantPlugin(Provider): continue cur_state = entity_obj["state"] not in ["off", "unavailable"] if control_entity.get("source"): - cur_state = entity_obj["attributes"].get("source") == control_entity["source"] + cur_state = ( + entity_obj["attributes"].get("source") == control_entity["source"] + ) await self.mass.player_manager.async_update_player_control( control_entity["value"], cur_state ) for control_entity in self.__get_volume_control_entities(): if control_entity["entity_id"] != entity_obj["entity_id"]: continue - cur_state = int(try_parse_float(entity_obj["attributes"].get("volume_level")) * 100) + cur_state = int( + try_parse_float(entity_obj["attributes"].get("volume_level")) * 100 + ) await self.mass.player_manager.async_update_player_control( control_entity["value"], cur_state ) @@ -352,14 +379,16 @@ class HomeAssistantPlugin(Provider): if not control_entity["value"] in enabled_controls: continue entity_id = control_entity["entity_id"] - if not entity_id in self._hass.states: + if entity_id not in self._hass.states: LOGGER.warning("entity not found: %s", entity_id) continue state_obj = self._hass.states[entity_id] cur_state = state_obj["state"] not in ["off", "unavailable"] source = control_entity.get("source") if source: - cur_state = state_obj["attributes"].get("source") == control_entity["source"] + cur_state = ( + state_obj["attributes"].get("source") == control_entity["source"] + ) control = PlayerControl( type=PlayerControlType.POWER, @@ -372,7 +401,7 @@ class HomeAssistantPlugin(Provider): control.entity_id = entity_id control.source = source await self.mass.player_manager.async_register_player_control(control) - if not entity_id in self._tracked_entities: + if entity_id not in self._tracked_entities: self._tracked_entities.append(entity_id) async def __async_register_volume_controls(self): @@ -383,10 +412,12 @@ class HomeAssistantPlugin(Provider): if not control_entity["value"] in enabled_controls: continue entity_id = control_entity["entity_id"] - if not entity_id in self._hass.states: + if entity_id not in self._hass.states: LOGGER.warning("entity not found: %s", entity_id) continue - cur_volume = try_parse_float(self._hass.get_state(entity_id, "volume_level")) * 100 + cur_volume = ( + try_parse_float(self._hass.get_state(entity_id, "volume_level")) * 100 + ) control = PlayerControl( type=PlayerControlType.VOLUME, id=control_entity["value"], @@ -397,7 +428,7 @@ class HomeAssistantPlugin(Provider): # store some vars on the control object for convenience control.entity_id = entity_id await self.mass.player_manager.async_register_player_control(control) - if not entity_id in self._tracked_entities: + if entity_id not in self._tracked_entities: self._tracked_entities.append(entity_id) async def async_power_control_set_state(self, control_id: str, new_state: bool): diff --git a/music_assistant/providers/qobuz/__init__.py b/music_assistant/providers/qobuz/__init__.py index d0dbd680..ab157f8b 100644 --- a/music_assistant/providers/qobuz/__init__.py +++ b/music_assistant/providers/qobuz/__init__.py @@ -7,7 +7,7 @@ from typing import List, Optional import aiohttp from asyncio_throttle import Throttler -from music_assistant.app_vars import get_app_var +from music_assistant.app_vars import get_app_var # noqa from music_assistant.constants import ( CONF_PASSWORD, CONF_USERNAME, @@ -37,10 +37,14 @@ LOGGER = logging.getLogger(PROV_ID) CONFIG_ENTRIES = [ ConfigEntry( - entry_key=CONF_USERNAME, entry_type=ConfigEntryType.STRING, description_key=CONF_USERNAME + entry_key=CONF_USERNAME, + entry_type=ConfigEntryType.STRING, + description_key=CONF_USERNAME, ), ConfigEntry( - entry_key=CONF_PASSWORD, entry_type=ConfigEntryType.PASSWORD, description_key=CONF_PASSWORD + entry_key=CONF_PASSWORD, + entry_type=ConfigEntryType.PASSWORD, + description_key=CONF_PASSWORD, ), ] @@ -54,7 +58,10 @@ async def async_setup(mass): class QobuzProvider(MusicProvider): """Provider for the Qobux music service.""" + # pylint: disable=abstract-method + _http_session = None + __user_auth_info = None @property def id(self) -> str: @@ -74,15 +81,10 @@ class QobuzProvider(MusicProvider): @property def supported_mediatypes(self) -> List[MediaType]: """Return MediaTypes the provider supports.""" - return [ - MediaType.Album, - MediaType.Artist, - MediaType.Playlist, - MediaType.Track, - ] + return [MediaType.Album, MediaType.Artist, MediaType.Playlist, MediaType.Track] async def async_on_start(self) -> bool: - """Called on startup. Handle initialization of the provider based on config.""" + """Handle initialization of the provider based on config.""" # pylint: disable=attribute-defined-outside-init self._http_session = aiohttp.ClientSession( loop=self.mass.loop, connector=aiohttp.TCPConnector() @@ -93,7 +95,7 @@ class QobuzProvider(MusicProvider): return False self.__username = config[CONF_USERNAME] self.__password = config[CONF_PASSWORD] - + self.__user_auth_info = None self.__logged_in = False self._throttler = Throttler(rate_limit=4, period=1) @@ -102,7 +104,7 @@ class QobuzProvider(MusicProvider): return True async def async_on_stop(self): - """Called on shutdown. Handle correct close/cleanup of the provider on exit.""" + """Handle correct close/cleanup of the provider on exit.""" if self._http_session: await self._http_session.close() @@ -111,6 +113,7 @@ class QobuzProvider(MusicProvider): ) -> SearchResult: """ Perform search on musicprovider. + :param search_query: Search query. :param media_types: A list of media_types to include. All types if None. :param limit: Number of items to return in the search (per type). @@ -197,25 +200,25 @@ class QobuzProvider(MusicProvider): return await self.__async_parse_artist(artist_obj) async def async_get_album(self, prov_album_id) -> Album: - """get full album details by id""" + """Get full album details by id.""" params = {"album_id": prov_album_id} album_obj = await self.__async_get_data("album/get", params) return await self.__async_parse_album(album_obj) async def async_get_track(self, prov_track_id) -> Track: - """get full track details by id""" + """Get full track details by id.""" params = {"track_id": prov_track_id} track_obj = await self.__async_get_data("track/get", params) return await self.__async_parse_track(track_obj) async def async_get_playlist(self, prov_playlist_id) -> Playlist: - """get full playlist details by id""" + """Get full playlist details by id.""" params = {"playlist_id": prov_playlist_id} playlist_obj = await self.__async_get_data("playlist/get", params) return await self.__async_parse_playlist(playlist_obj) async def async_get_album_tracks(self, prov_album_id) -> List[Track]: - """get all album tracks for given album id""" + """Get all album tracks for given album id.""" params = {"album_id": prov_album_id} async for item in self.__async_get_all_items("album/get", params, key="tracks"): track = await self.__async_parse_track(item) @@ -229,7 +232,7 @@ class QobuzProvider(MusicProvider): ) async def async_get_playlist_tracks(self, prov_playlist_id) -> List[Track]: - """get all playlist tracks for given playlist id""" + """Get all playlist tracks for given playlist id.""" params = {"playlist_id": prov_playlist_id, "extra": "tracks"} endpoint = "playlist/get" async for item in self.__async_get_all_items(endpoint, params, key="tracks"): @@ -246,7 +249,7 @@ class QobuzProvider(MusicProvider): # track version if the original is marked unavailable ? async def async_get_artist_albums(self, prov_artist_id) -> List[Album]: - """get a list of albums for the given artist""" + """Get a list of albums for the given artist.""" params = {"artist_id": prov_artist_id, "extra": "albums"} endpoint = "artist/get" async for item in self.__async_get_all_items(endpoint, params, key="albums"): @@ -256,27 +259,35 @@ class QobuzProvider(MusicProvider): yield album async def async_get_artist_toptracks(self, prov_artist_id) -> List[Track]: - """get a list of most popular tracks for the given artist""" + """Get a list of most popular tracks for the given artist.""" # artist toptracks not supported on Qobuz, so use search instead # assuming qobuz returns results sorted by popularity artist = await self.async_get_artist(prov_artist_id) params = {"query": artist.name, "limit": 25, "type": "tracks"} searchresult = await self.__async_get_data("catalog/search", params) for item in searchresult["tracks"]["items"]: - if "performer" in item and str(item["performer"]["id"]) == str(prov_artist_id): + if "performer" in item and str(item["performer"]["id"]) == str( + prov_artist_id + ): track = await self.__async_parse_track(item) if track: yield track async def async_library_add(self, prov_item_id, media_type: MediaType): - """add item to library""" + """Add item to library.""" result = None if media_type == MediaType.Artist: - result = await self.__async_get_data("favorite/create", {"artist_ids": prov_item_id}) + result = await self.__async_get_data( + "favorite/create", {"artist_ids": prov_item_id} + ) elif media_type == MediaType.Album: - result = await self.__async_get_data("favorite/create", {"album_ids": prov_item_id}) + result = await self.__async_get_data( + "favorite/create", {"album_ids": prov_item_id} + ) elif media_type == MediaType.Track: - result = await self.__async_get_data("favorite/create", {"track_ids": prov_item_id}) + result = await self.__async_get_data( + "favorite/create", {"track_ids": prov_item_id} + ) elif media_type == MediaType.Playlist: result = await self.__async_get_data( "playlist/subscribe", {"playlist_id": prov_item_id} @@ -284,14 +295,20 @@ class QobuzProvider(MusicProvider): return result async def async_library_remove(self, prov_item_id, media_type: MediaType): - """remove item from library""" + """Remove item from library.""" result = None if media_type == MediaType.Artist: - result = await self.__async_get_data("favorite/delete", {"artist_ids": prov_item_id}) + result = await self.__async_get_data( + "favorite/delete", {"artist_ids": prov_item_id} + ) elif media_type == MediaType.Album: - result = await self.__async_get_data("favorite/delete", {"album_ids": prov_item_id}) + result = await self.__async_get_data( + "favorite/delete", {"album_ids": prov_item_id} + ) elif media_type == MediaType.Track: - result = await self.__async_get_data("favorite/delete", {"track_ids": prov_item_id}) + result = await self.__async_get_data( + "favorite/delete", {"track_ids": prov_item_id} + ) elif media_type == MediaType.Playlist: playlist = await self.async_get_playlist(prov_item_id) if playlist.is_editable: @@ -305,7 +322,7 @@ class QobuzProvider(MusicProvider): return result async def async_add_playlist_tracks(self, prov_playlist_id, prov_track_ids): - """add track(s) to playlist""" + """Add track(s) to playlist.""" params = { "playlist_id": prov_playlist_id, "track_ids": ",".join(prov_track_ids), @@ -313,10 +330,12 @@ class QobuzProvider(MusicProvider): return await self.__async_get_data("playlist/addTracks", params) async def async_remove_playlist_tracks(self, prov_playlist_id, prov_track_ids): - """remove track(s) from playlist""" + """Remove track(s) from playlist.""" playlist_track_ids = [] params = {"playlist_id": prov_playlist_id, "extra": "tracks"} - for track in await self.__async_get_all_items("playlist/get", params, key="tracks"): + for track in await self.__async_get_all_items( + "playlist/get", params, key="tracks" + ): if track["id"] in prov_track_ids: playlist_track_ids.append(track["playlist_track_id"]) params = { @@ -325,24 +344,24 @@ class QobuzProvider(MusicProvider): } return await self.__async_get_data("playlist/deleteTracks", params) - async def async_get_stream_details(self, track_id: str) -> StreamDetails: + async def async_get_stream_details(self, item_id: str) -> StreamDetails: """Return the content details for the given track when it will be streamed.""" streamdetails = None for format_id in [27, 7, 6, 5]: # it seems that simply requesting for highest available quality does not work # from time to time the api response is empty for this request ?! - params = {"format_id": format_id, "track_id": track_id, "intent": "stream"} + params = {"format_id": format_id, "track_id": item_id, "intent": "stream"} streamdetails = await self.__async_get_data( "track/getFileUrl", params, sign_request=True ) if streamdetails and streamdetails.get("url"): break if not streamdetails or not streamdetails.get("url"): - LOGGER.error("Unable to retrieve stream url for track %s", track_id) + LOGGER.error("Unable to retrieve stream url for track %s", item_id) return None return StreamDetails( type=StreamType.URL, - item_id=str(track_id), + item_id=str(item_id), provider=PROV_ID, path=streamdetails["url"], content_type=ContentType(streamdetails["mime_type"].split("/")[1]), @@ -353,8 +372,9 @@ class QobuzProvider(MusicProvider): async def async_mass_event(self, msg, msg_details): """ - received event from mass - we use this to report playback start/stop to qobuz + Received event from mass. + + We use this to report playback start/stop to qobuz. """ if not self.__user_auth_info: return @@ -395,7 +415,7 @@ class QobuzProvider(MusicProvider): await self.__async_get_data("/track/reportStreamingEnd", params) async def __async_parse_artist(self, artist_obj): - """parse qobuz artist object to generic layout""" + """Parse qobuz artist object to generic layout.""" artist = Artist() if not artist_obj or not artist_obj.get("id"): return None @@ -408,7 +428,10 @@ class QobuzProvider(MusicProvider): if artist_obj.get("image"): for key in ["extralarge", "large", "medium", "small"]: if artist_obj["image"].get(key): - if not "2a96cbd8b46e442fc41c2b86b821562f" in artist_obj["image"][key]: + if ( + "2a96cbd8b46e442fc41c2b86b821562f" + not in artist_obj["image"][key] + ): artist.metadata["image"] = artist_obj["image"][key] break if artist_obj.get("biography"): @@ -418,7 +441,7 @@ class QobuzProvider(MusicProvider): return artist async def __async_parse_album(self, album_obj): - """parse qobuz album object to generic layout""" + """Parse qobuz album object to generic layout.""" album = Album() if ( not album_obj @@ -492,7 +515,7 @@ class QobuzProvider(MusicProvider): return album async def __async_parse_track(self, track_obj): - """parse qobuz track object to generic layout""" + """Parse qobuz track object to generic layout.""" track = Track() if ( not track_obj @@ -504,7 +527,7 @@ class QobuzProvider(MusicProvider): return None track.item_id = str(track_obj["id"]) track.provider = PROV_ID - if track_obj.get("performer") and not "Various " in track_obj["performer"]: + if track_obj.get("performer") and "Various " not in track_obj["performer"]: artist = await self.__async_parse_artist(track_obj["performer"]) if artist: track.artists.append(artist) @@ -513,7 +536,7 @@ class QobuzProvider(MusicProvider): if ( track_obj.get("album") and track_obj["album"].get("artist") - and not "Various " in track_obj["album"]["artist"] + and "Various " not in track_obj["album"]["artist"] ): artist = await self.__async_parse_artist(track_obj["album"]["artist"]) if artist: @@ -574,7 +597,7 @@ class QobuzProvider(MusicProvider): return track async def __async_parse_playlist(self, playlist_obj): - """parse qobuz playlist object to generic layout""" + """Parse qobuz playlist object to generic layout.""" playlist = Playlist() if not playlist_obj or not playlist_obj.get("id"): return None @@ -597,7 +620,7 @@ class QobuzProvider(MusicProvider): return playlist async def __async_auth_token(self): - """login to qobuz and store the token""" + """Login to qobuz and store the token.""" if self.__user_auth_info: return self.__user_auth_info["user_auth_token"] params = { @@ -608,11 +631,13 @@ class QobuzProvider(MusicProvider): details = await self.__async_get_data("user/login", params) if details and "user" in details: self.__user_auth_info = details - LOGGER.info("Succesfully logged in to Qobuz as %s", details["user"]["display_name"]) + LOGGER.info( + "Succesfully logged in to Qobuz as %s", details["user"]["display_name"] + ) return details["user_auth_token"] async def __async_get_all_items(self, endpoint, params=None, key="tracks"): - """get all items from a paged list""" + """Get all items from a paged list.""" if not params: params = {} limit = 50 @@ -622,7 +647,7 @@ class QobuzProvider(MusicProvider): params["offset"] = offset result = await self.__async_get_data(endpoint, params=params) offset += limit - if not result or not key in result or not "items" in result[key]: + if not result or key not in result or "items" not in result[key]: break for item in result[key]["items"]: yield item @@ -630,7 +655,7 @@ class QobuzProvider(MusicProvider): break async def __async_get_data(self, endpoint, params=None, sign_request=False): - """get data from api""" + """Get data from api.""" if not params: params = {} url = "http://www.qobuz.com/api.json/0.2/%s" % endpoint @@ -659,13 +684,15 @@ class QobuzProvider(MusicProvider): url, headers=headers, params=params, verify_ssl=False ) as response: result = await response.json() - if "error" in result or ("status" in result and "error" in result["status"]): + if "error" in result or ( + "status" in result and "error" in result["status"] + ): LOGGER.error("%s - %s", endpoint, result) return None return result async def __async_post_data(self, endpoint, params=None, data=None): - """post data to api""" + """Post data to api.""" if not params: params = {} if not data: @@ -677,7 +704,9 @@ class QobuzProvider(MusicProvider): url, params=params, json=data, verify_ssl=False ) as response: result = await response.json() - if "error" in result or ("status" in result and "error" in result["status"]): + if "error" in result or ( + "status" in result and "error" in result["status"] + ): LOGGER.error("%s - %s", endpoint, result) return None return result diff --git a/music_assistant/providers/sonos/__init__.py b/music_assistant/providers/sonos/__init__.py index f06b3f0a..71f69096 100644 --- a/music_assistant/providers/sonos/__init__.py +++ b/music_assistant/providers/sonos/__init__.py @@ -6,4 +6,4 @@ from .sonos import SonosProvider async def async_setup(mass): """Perform async setup of this Plugin/Provider.""" prov = SonosProvider() - await mass.async_register_provider(prov) \ No newline at end of file + await mass.async_register_provider(prov) diff --git a/music_assistant/providers/sonos/sonos.py b/music_assistant/providers/sonos/sonos.py index 15e936e7..f6921849 100644 --- a/music_assistant/providers/sonos/sonos.py +++ b/music_assistant/providers/sonos/sonos.py @@ -22,7 +22,9 @@ PLAYER_CONFIG_ENTRIES = [] # we don't have any player config entries (for now) class SonosProvider(PlayerProvider): - """Support for Sonos speakers""" + """Support for Sonos speakers.""" + + # pylint: disable=abstract-method _discovery_running = False _tasks = [] @@ -45,17 +47,18 @@ class SonosProvider(PlayerProvider): return CONFIG_ENTRIES async def async_on_start(self) -> bool: - """Called on startup. Handle initialization of the provider.""" + """Handle initialization of the provider.""" self._tasks.append(self.mass.add_job(self.__async_periodic_discovery())) async def async_on_stop(self): - """Called on shutdown. Handle correct close/cleanup of the provider on exit.""" + """Handle correct close/cleanup of the provider on exit.""" for task in self._tasks: task.cancel() async def async_cmd_play_uri(self, player_id: str, uri: str): """ Play the specified uri/url on the goven player. + :param player_id: player_id of the player to handle the command. """ player = self._players.get(player_id) @@ -67,6 +70,7 @@ class SonosProvider(PlayerProvider): async def async_cmd_stop(self, player_id: str): """ Send STOP command to given player. + :param player_id: player_id of the player to handle the command. """ player = self._players.get(player_id) @@ -78,6 +82,7 @@ class SonosProvider(PlayerProvider): async def async_cmd_play(self, player_id: str): """ Send STOP command to given player. + :param player_id: player_id of the player to handle the command. """ player = self._players.get(player_id) @@ -89,6 +94,7 @@ class SonosProvider(PlayerProvider): async def async_cmd_pause(self, player_id: str): """ Send PAUSE command to given player. + :param player_id: player_id of the player to handle the command. """ player = self._players.get(player_id) @@ -100,6 +106,7 @@ class SonosProvider(PlayerProvider): async def async_cmd_next(self, player_id: str): """ Send NEXT TRACK command to given player. + :param player_id: player_id of the player to handle the command. """ player = self._players.get(player_id) @@ -111,6 +118,7 @@ class SonosProvider(PlayerProvider): async def async_cmd_previous(self, player_id: str): """ Send PREVIOUS TRACK command to given player. + :param player_id: player_id of the player to handle the command. """ player = self._players.get(player_id) @@ -122,6 +130,7 @@ class SonosProvider(PlayerProvider): async def async_cmd_power_on(self, player_id: str): """ Send POWER ON command to given player. + :param player_id: player_id of the player to handle the command. """ player = self._players.get(player_id) @@ -135,6 +144,7 @@ class SonosProvider(PlayerProvider): async def async_cmd_power_off(self, player_id: str): """ Send POWER OFF command to given player. + :param player_id: player_id of the player to handle the command. """ player = self._players.get(player_id) @@ -149,6 +159,7 @@ class SonosProvider(PlayerProvider): async def async_cmd_volume_set(self, player_id: str, volume_level: int): """ Send volume level command to given player. + :param player_id: player_id of the player to handle the command. :param volume_level: volume level to set (0..100). """ @@ -161,6 +172,7 @@ class SonosProvider(PlayerProvider): async def async_cmd_volume_mute(self, player_id: str, is_muted=False): """ Send volume MUTE command to given player. + :param player_id: player_id of the player to handle the command. :param is_muted: bool with new mute state. """ @@ -172,7 +184,8 @@ class SonosProvider(PlayerProvider): async def async_cmd_queue_play_index(self, player_id: str, index: int): """ - Play item at index X on player's queue + Play item at index X on player's queue. + :param player_id: player_id of the player to handle the command. :param index: (int) index of the queue item that should start playing """ @@ -184,7 +197,8 @@ class SonosProvider(PlayerProvider): async def async_cmd_queue_load(self, player_id: str, queue_items: List[QueueItem]): """ - Load/overwrite given items in the player's queue implementation + Load/overwrite given items in the player's queue implementation. + :param player_id: player_id of the player to handle the command. :param queue_items: a list of QueueItems """ @@ -201,6 +215,7 @@ class SonosProvider(PlayerProvider): ): """ Insert new items at position X into existing queue. + If insert_at_index 0 or None, will start playing newly added item(s) :param player_id: player_id of the player to handle the command. :param queue_items: a list of QueueItems @@ -209,13 +224,18 @@ class SonosProvider(PlayerProvider): player = self._players.get(player_id) if player: for pos, item in enumerate(queue_items): - self.mass.add_job(player.soco.add_uri_to_queue, item.uri, insert_at_index + pos) + self.mass.add_job( + player.soco.add_uri_to_queue, item.uri, insert_at_index + pos + ) else: LOGGER.warning("Received command for unavailable player: %s", player_id) - async def async_cmd_queue_append(self, player_id: str, queue_items: List[QueueItem]): + async def async_cmd_queue_append( + self, player_id: str, queue_items: List[QueueItem] + ): """ Append new items at the end of the queue. + :param player_id: player_id of the player to handle the command. :param queue_items: a list of QueueItems """ @@ -230,6 +250,7 @@ class SonosProvider(PlayerProvider): async def async_cmd_queue_clear(self, player_id: str): """ Clear the player's queue. + :param player_id: player_id of the player to handle the command. """ player = self._players.get(player_id) @@ -256,8 +277,10 @@ class SonosProvider(PlayerProvider): cur_player_ids = [item.player_id for item in self._players.values()] # remove any disconnected players... for player in list(self._players.values()): - if not player.is_group and not player.soco.uid in new_device_ids: - self.mass.add_job(self.mass.player_manager.async_remove_player(player.player_id)) + if not player.is_group and player.soco.uid not in new_device_ids: + self.mass.add_job( + self.mass.player_manager.async_remove_player(player.player_id) + ) for sub in player.subscriptions: sub.unsubscribe() self._players.pop(player, None) @@ -349,14 +372,14 @@ class SonosProvider(PlayerProvider): group_player.is_group_player = True group_player.name = group.label group_player.group_childs = [item.uid for item in group.members] - self.mass.run_task(self.mass.player_manager.async_update_player(group_player)) + self.mass.run_task( + self.mass.player_manager.async_update_player(group_player) + ) async def __topology_changed(self, player_id, event=None): - """ - Received topology changed event - from one of the sonos players. - Schedule discovery to work out the changes. - """ + """Received topology changed event from one of the sonos players.""" + # pylint: disable=unused-argument + # Schedule discovery to work out the changes. self.mass.add_job(self.__run_discovery) async def __async_report_progress(self, player_id: str): @@ -404,4 +427,5 @@ class ProcessSonosEventQueue: def put(self, item, block=True, timeout=None): """Process event.""" + # pylint: disable=unused-argument self._callback_handler(self._player_id, item) diff --git a/music_assistant/providers/spotify/__init__.py b/music_assistant/providers/spotify/__init__.py index 0fdb80dd..8d1c9d58 100644 --- a/music_assistant/providers/spotify/__init__.py +++ b/music_assistant/providers/spotify/__init__.py @@ -9,7 +9,7 @@ from typing import List, Optional import aiohttp from asyncio_throttle import Throttler -from music_assistant.app_vars import get_app_var +from music_assistant.app_vars import get_app_var # noqa from music_assistant.constants import CONF_PASSWORD, CONF_USERNAME from music_assistant.models.config_entry import ConfigEntry, ConfigEntryType from music_assistant.models.media_types import ( @@ -35,10 +35,14 @@ LOGGER = logging.getLogger(PROV_ID) CONFIG_ENTRIES = [ ConfigEntry( - entry_key=CONF_USERNAME, entry_type=ConfigEntryType.STRING, description_key=CONF_USERNAME + entry_key=CONF_USERNAME, + entry_type=ConfigEntryType.STRING, + description_key=CONF_USERNAME, ), ConfigEntry( - entry_key=CONF_PASSWORD, entry_type=ConfigEntryType.PASSWORD, description_key=CONF_PASSWORD + entry_key=CONF_PASSWORD, + entry_type=ConfigEntryType.PASSWORD, + description_key=CONF_PASSWORD, ), ] @@ -52,6 +56,8 @@ async def async_setup(mass): class SpotifyProvider(MusicProvider): """Implementation for the Spotify MusicProvider.""" + # pylint: disable=abstract-method + _http_session = None __auth_token = None sp_user = None @@ -83,7 +89,7 @@ class SpotifyProvider(MusicProvider): ] async def async_on_start(self) -> bool: - """Called on startup. Handle initialization of the provider based on config.""" + """Handle initialization of the provider based on config.""" config = self.mass.config.get_provider_config(self.id) # pylint: disable=attribute-defined-outside-init self._cur_user = None @@ -103,7 +109,7 @@ class SpotifyProvider(MusicProvider): return token is not None async def async_on_stop(self): - """Called on shutdown. Handle correct close/cleanup of the provider on exit.""" + """Handle correct close/cleanup of the provider on exit.""" if self._http_session: await self._http_session.close() @@ -112,6 +118,7 @@ class SpotifyProvider(MusicProvider): ) -> SearchResult: """ Perform search on musicprovider. + :param search_query: Search query. :param media_types: A list of media_types to include. All types if None. :param limit: Number of items to return in the search (per type). @@ -153,8 +160,10 @@ class SpotifyProvider(MusicProvider): return result async def async_get_library_artists(self) -> List[Artist]: - """retrieve library artists from spotify""" - spotify_artists = await self.__async_get_data("me/following?type=artist&limit=50") + """Retrieve library artists from spotify.""" + spotify_artists = await self.__async_get_data( + "me/following?type=artist&limit=50" + ) if spotify_artists: # TODO: use cursor method to retrieve more than 50 artists for artist_obj in spotify_artists["artists"]["items"]: @@ -162,21 +171,21 @@ class SpotifyProvider(MusicProvider): yield prov_artist async def async_get_library_albums(self) -> List[Album]: - """retrieve library albums from the provider""" + """Retrieve library albums from the provider.""" async for item in self.__async_get_all_items("me/albums"): album = await self.__async_parse_album(item) if album: yield album async def async_get_library_tracks(self) -> List[Track]: - """retrieve library tracks from the provider""" + """Retrieve library tracks from the provider.""" async for item in self.__async_get_all_items("me/tracks"): track = await self.__async_parse_track(item) if track: yield track async def async_get_library_playlists(self) -> List[Playlist]: - """retrieve playlists from the provider""" + """Retrieve playlists from the provider.""" async for item in self.__async_get_all_items("me/playlists"): playlist = await self.__async_parse_playlist(item) if playlist: @@ -187,27 +196,27 @@ class SpotifyProvider(MusicProvider): yield None # TODO: Return spotify radio async def async_get_artist(self, prov_artist_id) -> Artist: - """get full artist details by id""" + """Get full artist details by id.""" artist_obj = await self.__async_get_data("artists/%s" % prov_artist_id) return await self.__async_parse_artist(artist_obj) async def async_get_album(self, prov_album_id) -> Album: - """get full album details by id""" + """Get full album details by id.""" album_obj = await self.__async_get_data("albums/%s" % prov_album_id) return await self.__async_parse_album(album_obj) async def async_get_track(self, prov_track_id) -> Track: - """get full track details by id""" + """Get full track details by id.""" track_obj = await self.__async_get_data("tracks/%s" % prov_track_id) return await self.__async_parse_track(track_obj) async def async_get_playlist(self, prov_playlist_id) -> Playlist: - """get full playlist details by id""" + """Get full playlist details by id.""" playlist_obj = await self.__async_get_data(f"playlists/{prov_playlist_id}") return await self.__async_parse_playlist(playlist_obj) async def async_get_album_tracks(self, prov_album_id) -> List[Track]: - """get all album tracks for given album id""" + """Get all album tracks for given album id.""" endpoint = f"albums/{prov_album_id}/tracks" async for track_obj in self.__async_get_all_items(endpoint): track = await self.__async_parse_track(track_obj) @@ -215,7 +224,7 @@ class SpotifyProvider(MusicProvider): yield track async def async_get_playlist_tracks(self, prov_playlist_id) -> List[Track]: - """get all playlist tracks for given playlist id""" + """Get all playlist tracks for given playlist id.""" endpoint = f"playlists/{prov_playlist_id}/tracks" async for track_obj in self.__async_get_all_items(endpoint): playlist_track = await self.__async_parse_track(track_obj) @@ -229,7 +238,7 @@ class SpotifyProvider(MusicProvider): ) async def async_get_artist_albums(self, prov_artist_id) -> List[Album]: - """get a list of all albums for the given artist""" + """Get a list of all albums for the given artist.""" params = {"include_groups": "album,single,compilation"} endpoint = f"artists/{prov_artist_id}/albums" async for item in self.__async_get_all_items(endpoint, params): @@ -238,7 +247,7 @@ class SpotifyProvider(MusicProvider): yield album async def async_get_artist_toptracks(self, prov_artist_id) -> List[Track]: - """get a list of 10 most popular tracks for the given artist""" + """Get a list of 10 most popular tracks for the given artist.""" artist = await self.async_get_artist(prov_artist_id) endpoint = f"artists/{prov_artist_id}/top-tracks" items = await self.__async_get_data(endpoint) @@ -249,7 +258,7 @@ class SpotifyProvider(MusicProvider): yield track async def async_library_add(self, prov_item_id, media_type: MediaType): - """add item to library""" + """Add item to library.""" result = False if media_type == MediaType.Artist: result = await self.__async_put_data( @@ -266,7 +275,7 @@ class SpotifyProvider(MusicProvider): return result async def async_library_remove(self, prov_item_id, media_type: MediaType): - """remove item from library""" + """Remove item from library.""" result = False if media_type == MediaType.Artist: result = await self.__async_delete_data( @@ -277,29 +286,35 @@ class SpotifyProvider(MusicProvider): elif media_type == MediaType.Track: result = await self.__async_delete_data("me/tracks", {"ids": prov_item_id}) elif media_type == MediaType.Playlist: - result = await self.__async_delete_data(f"playlists/{prov_item_id}/followers") + result = await self.__async_delete_data( + f"playlists/{prov_item_id}/followers" + ) return result async def async_add_playlist_tracks(self, prov_playlist_id, prov_track_ids): - """add track(s) to playlist""" + """Add track(s) to playlist.""" track_uris = [] for track_id in prov_track_ids: track_uris.append("spotify:track:%s" % track_id) data = {"uris": track_uris} - return await self.__async_post_data(f"playlists/{prov_playlist_id}/tracks", data=data) + return await self.__async_post_data( + f"playlists/{prov_playlist_id}/tracks", data=data + ) async def async_remove_playlist_tracks(self, prov_playlist_id, prov_track_ids): - """remove track(s) from playlist""" + """Remove track(s) from playlist.""" track_uris = [] for track_id in prov_track_ids: track_uris.append({"uri": "spotify:track:%s" % track_id}) data = {"tracks": track_uris} - return await self.__async_delete_data(f"playlists/{prov_playlist_id}/tracks", data=data) + return await self.__async_delete_data( + f"playlists/{prov_playlist_id}/tracks", data=data + ) - async def async_get_stream_details(self, track_id: str) -> StreamDetails: + async def async_get_stream_details(self, item_id: str) -> StreamDetails: """Return the content details for the given track when it will be streamed.""" # make sure a valid track is requested. - track = await self.async_get_track(track_id) + track = await self.async_get_track(item_id) if not track: return None # make sure that the token is still valid by just requesting it @@ -321,20 +336,22 @@ class SpotifyProvider(MusicProvider): ) async def __async_parse_artist(self, artist_obj): - """parse spotify artist object to generic layout""" + """Parse spotify artist object to generic layout.""" if not artist_obj: return None artist = Artist() artist.item_id = artist_obj["id"] artist.provider = self.id - artist.provider_ids.append(MediaItemProviderId(provider=PROV_ID, item_id=artist_obj["id"])) + artist.provider_ids.append( + MediaItemProviderId(provider=PROV_ID, item_id=artist_obj["id"]) + ) artist.name = artist_obj["name"] if "genres" in artist_obj: artist.tags = artist_obj["genres"] if artist_obj.get("images"): for img in artist_obj["images"]: img_url = img["url"] - if not "2a96cbd8b46e442fc41c2b86b821562f" in img_url: + if "2a96cbd8b46e442fc41c2b86b821562f" not in img_url: artist.metadata["image"] = img_url break if artist_obj.get("external_urls"): @@ -342,7 +359,7 @@ class SpotifyProvider(MusicProvider): return artist async def __async_parse_album(self, album_obj): - """parse spotify album object to generic layout""" + """Parse spotify album object to generic layout.""" if not album_obj: return None if "album" in album_obj: @@ -381,13 +398,15 @@ class SpotifyProvider(MusicProvider): album.metadata["explicit"] = str(album_obj["explicit"]).lower() album.provider_ids.append( MediaItemProviderId( - provider=PROV_ID, item_id=album_obj["id"], quality=TrackQuality.LOSSY_OGG + provider=PROV_ID, + item_id=album_obj["id"], + quality=TrackQuality.LOSSY_OGG, ) ) return album async def __async_parse_track(self, track_obj): - """parse spotify track object to generic layout""" + """Parse spotify track object to generic layout.""" if not track_obj: return None if "track" in track_obj: @@ -419,13 +438,15 @@ class SpotifyProvider(MusicProvider): track.metadata["spotify_url"] = track_obj["external_urls"]["spotify"] track.provider_ids.append( MediaItemProviderId( - provider=PROV_ID, item_id=track_obj["id"], quality=TrackQuality.LOSSY_OGG + provider=PROV_ID, + item_id=track_obj["id"], + quality=TrackQuality.LOSSY_OGG, ) ) return track async def __async_parse_playlist(self, playlist_obj): - """parse spotify playlist object to generic layout""" + """Parse spotify playlist object to generic layout.""" if not playlist_obj.get("id"): return None @@ -438,7 +459,8 @@ class SpotifyProvider(MusicProvider): playlist.name = playlist_obj["name"] playlist.owner = playlist_obj["owner"]["display_name"] playlist.is_editable = ( - playlist_obj["owner"]["id"] == self.sp_user["id"] or playlist_obj["collaborative"] + playlist_obj["owner"]["id"] == self.sp_user["id"] + or playlist_obj["collaborative"] ) if playlist_obj.get("images"): playlist.metadata["image"] = playlist_obj["images"][0]["url"] @@ -448,9 +470,11 @@ class SpotifyProvider(MusicProvider): return playlist async def async_get_token(self): - """get auth token on spotify""" + """Get auth token on spotify.""" # return existing token if we have one in memory - if self.__auth_token and (self.__auth_token["expiresAt"] > int(time.time()) + 20): + if self.__auth_token and ( + self.__auth_token["expiresAt"] > int(time.time()) + 20 + ): return self.__auth_token tokeninfo = {} if not self._username or not self._password: @@ -468,7 +492,7 @@ class SpotifyProvider(MusicProvider): return tokeninfo def __get_token(self): - """get spotify auth token with spotty bin""" + """Get spotify auth token with spotty bin.""" # get token with spotty scopes = [ "user-read-playback-state", @@ -517,7 +541,7 @@ class SpotifyProvider(MusicProvider): return tokeninfo async def __async_get_all_items(self, endpoint, params=None, key="items"): - """get all items from a paged list""" + """Get all items from a paged list.""" if not params: params = {} limit = 50 @@ -527,7 +551,7 @@ class SpotifyProvider(MusicProvider): params["offset"] = offset result = await self.__async_get_data(endpoint, params=params) offset += limit - if not result or not key in result or not result[key]: + if not result or key not in result or not result[key]: break for item in result[key]: yield item @@ -535,7 +559,7 @@ class SpotifyProvider(MusicProvider): break async def __async_get_data(self, endpoint, params=None): - """get data from api""" + """Get data from api.""" if not params: params = {} url = "https://api.spotify.com/v1/%s" % endpoint @@ -554,7 +578,7 @@ class SpotifyProvider(MusicProvider): return result async def __async_delete_data(self, endpoint, params=None, data=None): - """delete data from api""" + """Delete data from api.""" if not params: params = {} url = "https://api.spotify.com/v1/%s" % endpoint @@ -566,7 +590,7 @@ class SpotifyProvider(MusicProvider): return await response.text() async def __async_put_data(self, endpoint, params=None, data=None): - """put data on api""" + """Put data on api.""" if not params: params = {} url = "https://api.spotify.com/v1/%s" % endpoint @@ -578,7 +602,7 @@ class SpotifyProvider(MusicProvider): return await response.text() async def __async_post_data(self, endpoint, params=None, data=None): - """post data on api""" + """Post data on api.""" if not params: params = {} url = "https://api.spotify.com/v1/%s" % endpoint @@ -591,13 +615,17 @@ class SpotifyProvider(MusicProvider): @staticmethod def get_spotty_binary(): - """find the correct spotty binary belonging to the platform""" + """Find the correct spotty binary belonging to the platform.""" sp_binary = None if platform.system() == "Windows": - sp_binary = os.path.join(os.path.dirname(__file__), "spotty", "windows", "spotty.exe") + sp_binary = os.path.join( + os.path.dirname(__file__), "spotty", "windows", "spotty.exe" + ) elif platform.system() == "Darwin": # macos binary is x86_64 intel - sp_binary = os.path.join(os.path.dirname(__file__), "spotty", "darwin", "spotty") + sp_binary = os.path.join( + os.path.dirname(__file__), "spotty", "darwin", "spotty" + ) elif platform.system() == "Linux": # try to find out the correct architecture by trial and error architecture = platform.machine() diff --git a/music_assistant/providers/squeezebox/__init__.py b/music_assistant/providers/squeezebox/__init__.py index ec646b2f..3e1d1c35 100644 --- a/music_assistant/providers/squeezebox/__init__.py +++ b/music_assistant/providers/squeezebox/__init__.py @@ -1,23 +1,14 @@ """Squeezebox emulated player provider.""" import asyncio -import decimal import logging -import os -import random -import socket -import struct -import sys -import time -from collections import OrderedDict from typing import List from music_assistant.constants import CONF_CROSSFADE_DURATION -from music_assistant.models.config_entry import ConfigEntry, ConfigEntryType -from music_assistant.models.player import DeviceInfo, Player, PlayerFeature, PlayerState +from music_assistant.models.config_entry import ConfigEntry +from music_assistant.models.player import DeviceInfo, PlayerFeature from music_assistant.models.player_queue import QueueItem from music_assistant.models.playerprovider import PlayerProvider -from music_assistant.utils import get_hostname, get_ip, run_periodic, try_parse_int from .constants import PROV_ID, PROV_NAME from .discovery import DiscoveryProtocol @@ -40,7 +31,7 @@ async def async_setup(mass): class PySqueezeProvider(PlayerProvider): - """Python implementation of SlimProto server""" + """Python implementation of SlimProto server.""" _socket_clients = {} _tasks = [] @@ -61,16 +52,18 @@ class PySqueezeProvider(PlayerProvider): return CONFIG_ENTRIES async def async_on_start(self) -> bool: - """Called on startup. Handle initialization of the provider.""" + """Handle initialization of the provider. Called on startup.""" # start slimproto server self._tasks.append( - self.mass.add_job(asyncio.start_server(self.__async_client_connected, "0.0.0.0", 3483)) + self.mass.add_job( + asyncio.start_server(self.__async_client_connected, "0.0.0.0", 3483) + ) ) # setup discovery self._tasks.append(self.mass.add_job(self.async_start_discovery())) async def async_on_stop(self): - """Called on shutdown. Handle correct close/cleanup of the provider on exit.""" + """Handle correct close/cleanup of the provider on exit.""" for task in self._tasks: task.cancel() for client in self._socket_clients.values(): @@ -78,12 +71,15 @@ class PySqueezeProvider(PlayerProvider): async def async_cmd_play_uri(self, player_id: str, uri: str): """ - Play the specified uri/url on the goven player. + Play the specified uri/url on the given player. + :param player_id: player_id of the player to handle the command. """ socket_client = self._socket_clients.get(player_id) if socket_client: - crossfade = self.mass.config.player_settings[player_id][CONF_CROSSFADE_DURATION] + crossfade = self.mass.config.player_settings[player_id][ + CONF_CROSSFADE_DURATION + ] await socket_client.async_cmd_play_uri( uri, send_flush=True, crossfade_duration=crossfade ) @@ -93,6 +89,7 @@ class PySqueezeProvider(PlayerProvider): async def async_cmd_stop(self, player_id: str): """ Send STOP command to given player. + :param player_id: player_id of the player to handle the command. """ socket_client = self._socket_clients.get(player_id) @@ -103,7 +100,8 @@ class PySqueezeProvider(PlayerProvider): async def async_cmd_play(self, player_id: str): """ - Send STOP command to given player. + Send PLAY command to given player. + :param player_id: player_id of the player to handle the command. """ socket_client = self._socket_clients.get(player_id) @@ -115,6 +113,7 @@ class PySqueezeProvider(PlayerProvider): async def async_cmd_pause(self, player_id: str): """ Send PAUSE command to given player. + :param player_id: player_id of the player to handle the command. """ socket_client = self._socket_clients.get(player_id) @@ -126,6 +125,7 @@ class PySqueezeProvider(PlayerProvider): async def async_cmd_next(self, player_id: str): """ Send NEXT TRACK command to given player. + :param player_id: player_id of the player to handle the command. """ queue = self.mass.player_manager.get_player_queue(player_id) @@ -137,6 +137,7 @@ class PySqueezeProvider(PlayerProvider): async def async_cmd_previous(self, player_id: str): """ Send PREVIOUS TRACK command to given player. + :param player_id: player_id of the player to handle the command. """ queue = self.mass.player_manager.get_player_queue(player_id) @@ -148,6 +149,7 @@ class PySqueezeProvider(PlayerProvider): async def async_cmd_power_on(self, player_id: str): """ Send POWER ON command to given player. + :param player_id: player_id of the player to handle the command. """ socket_client = self._socket_clients.get(player_id) @@ -155,13 +157,16 @@ class PySqueezeProvider(PlayerProvider): await socket_client.async_cmd_power(True) # save power and volume state in cache cache_str = f"squeezebox_player_state_{player_id}" - await self.mass.cache.async_set(cache_str, (True, socket_client.volume_level)) + await self.mass.cache.async_set( + cache_str, (True, socket_client.volume_level) + ) else: LOGGER.warning("Received command for unavailable player: %s", player_id) async def async_cmd_power_off(self, player_id: str): """ Send POWER OFF command to given player. + :param player_id: player_id of the player to handle the command. """ socket_client = self._socket_clients.get(player_id) @@ -170,13 +175,16 @@ class PySqueezeProvider(PlayerProvider): # store last power state as we need it when the player (re)connects # save power and volume state in cache cache_str = f"squeezebox_player_state_{player_id}" - await self.mass.cache.async_set(cache_str, (False, socket_client.volume_level)) + await self.mass.cache.async_set( + cache_str, (False, socket_client.volume_level) + ) else: LOGGER.warning("Received command for unavailable player: %s", player_id) async def async_cmd_volume_set(self, player_id: str, volume_level: int): """ Send volume level command to given player. + :param player_id: player_id of the player to handle the command. :param volume_level: volume level to set (0..100). """ @@ -185,13 +193,16 @@ class PySqueezeProvider(PlayerProvider): await socket_client.async_cmd_volume_set(volume_level) # save power and volume state in cache cache_str = f"squeezebox_player_state_{player_id}" - await self.mass.cache.async_set(cache_str, (socket_client.powered, volume_level)) + await self.mass.cache.async_set( + cache_str, (socket_client.powered, volume_level) + ) else: LOGGER.warning("Received command for unavailable player: %s", player_id) async def async_cmd_volume_mute(self, player_id: str, is_muted=False): """ Send volume MUTE command to given player. + :param player_id: player_id of the player to handle the command. :param is_muted: bool with new mute state. """ @@ -203,7 +214,8 @@ class PySqueezeProvider(PlayerProvider): async def async_cmd_queue_play_index(self, player_id: str, index: int): """ - Play item at index X on player's queue + Play item at index X on player's queue. + :param player_id: player_id of the player to handle the command. :param index: (int) index of the queue item that should start playing """ @@ -215,7 +227,8 @@ class PySqueezeProvider(PlayerProvider): async def async_cmd_queue_load(self, player_id: str, queue_items: List[QueueItem]): """ - Load/overwrite given items in the player's queue implementation + Load/overwrite given items in the player's queue implementation. + :param player_id: player_id of the player to handle the command. :param queue_items: a list of QueueItems """ @@ -227,6 +240,7 @@ class PySqueezeProvider(PlayerProvider): ): """ Insert new items at position X into existing queue. + If insert_at_index 0 or None, will start playing newly added item(s) :param player_id: player_id of the player to handle the command. :param queue_items: a list of QueueItems @@ -238,17 +252,23 @@ class PySqueezeProvider(PlayerProvider): if queue and insert_at_index == queue.cur_index: return await self.async_cmd_queue_play_index(player_id, insert_at_index) - async def async_cmd_queue_append(self, player_id: str, queue_items: List[QueueItem]): + async def async_cmd_queue_append( + self, player_id: str, queue_items: List[QueueItem] + ): """ Append new items at the end of the queue. + :param player_id: player_id of the player to handle the command. :param queue_items: a list of QueueItems """ pass # automagically handled by built-in queue controller - async def async_cmd_queue_update(self, player_id: str, queue_items: List[QueueItem]): + async def async_cmd_queue_update( + self, player_id: str, queue_items: List[QueueItem] + ): """ Overwrite the existing items in the queue, used for reordering. + :param player_id: player_id of the player to handle the command. :param queue_items: a list of QueueItems """ @@ -257,12 +277,14 @@ class PySqueezeProvider(PlayerProvider): async def async_cmd_queue_clear(self, player_id: str): """ Clear the player's queue. + :param player_id: player_id of the player to handle the command. """ # queue is handled by built-in queue controller but send stop return await self.async_cmd_stop(player_id) async def async_start_discovery(self): + """Start discovery for players.""" transport, protocol = await self.mass.loop.create_datagram_endpoint( lambda: DiscoveryProtocol(self.mass.web.http_port), local_addr=("0.0.0.0", 3483), @@ -314,7 +336,9 @@ class PySqueezeProvider(PlayerProvider): if queue: next_item = queue.next_item if next_item: - crossfade = self.mass.config.player_settings[player_id][CONF_CROSSFADE_DURATION] + crossfade = self.mass.config.player_settings[player_id][ + CONF_CROSSFADE_DURATION + ] await self._socket_clients[player_id].async_cmd_play_uri( next_item.uri, send_flush=False, crossfade_duration=crossfade ) diff --git a/music_assistant/providers/squeezebox/constants.py b/music_assistant/providers/squeezebox/constants.py index f710bfb8..ecd683c1 100644 --- a/music_assistant/providers/squeezebox/constants.py +++ b/music_assistant/providers/squeezebox/constants.py @@ -1,4 +1,4 @@ """Constants for Squeezebox emulation.""" PROV_ID = "squeezebox" -PROV_NAME = "Squeezebox emulation" \ No newline at end of file +PROV_NAME = "Squeezebox emulation" diff --git a/music_assistant/providers/squeezebox/discovery.py b/music_assistant/providers/squeezebox/discovery.py index 06afbcdf..d5ae0b80 100644 --- a/music_assistant/providers/squeezebox/discovery.py +++ b/music_assistant/providers/squeezebox/discovery.py @@ -1,21 +1,21 @@ """Squeezebox emulation discovery implementation.""" -from collections import OrderedDict -import socket import logging +import socket import struct +from collections import OrderedDict -from music_assistant.utils import ( - get_hostname, - get_ip -) +from music_assistant.utils import get_hostname, get_ip LOGGER = logging.getLogger("squeezebox") -class Datagram(): + +class Datagram: """Description of a discovery datagram.""" + @classmethod - def decode(self, data): + def decode(cls, data): + """Decode a datagram message.""" if data[0] == "e": return TLVDiscoveryRequestDatagram(data) elif data[0] == "E": @@ -40,13 +40,15 @@ class ClientDiscoveryDatagram(Datagram): client = None def __init__(self, data): - s = struct.unpack("!cxBB8x6B", data.encode()) - assert s[0] == "d" - self.device = s[1] - self.firmware = hex(s[2]) - self.client = ":".join(["%02x" % (x,) for x in s[3:]]) + """Initialize class.""" + msg = struct.unpack("!cxBB8x6B", data.encode()) + assert msg[0] == "d" + self.device = msg[1] + self.firmware = hex(msg[2]) + self.client = ":".join(["%02x" % (x,) for x in msg[3:]]) def __repr__(self): + """Print the class contents.""" return "<%s device=%r firmware=%r client=%r>" % ( self.__class__.__name__, self.device, @@ -57,7 +59,10 @@ class ClientDiscoveryDatagram(Datagram): class DiscoveryResponseDatagram(Datagram): """Description of a discovery response datagram.""" + def __init__(self, hostname, port): + """Initialize class.""" + # pylint: disable=unused-argument hostname = hostname[:16].encode("UTF-8") hostname += (16 - len(hostname)) * "\x00" self.packet = struct.pack("!c16s", "D", hostname).decode() @@ -65,16 +70,18 @@ class DiscoveryResponseDatagram(Datagram): class TLVDiscoveryRequestDatagram(Datagram): """Description of a discovery request datagram.""" + def __init__(self, data): + """Initialize class.""" requestdata = OrderedDict() assert data[0] == "e" idx = 1 length = len(data) - 5 while idx <= length: - typ, l = struct.unpack_from("4sB", data.encode(), idx) - if l: - val = data[idx + 5 : idx + 5 + l] - idx += 5 + l + typ, _len = struct.unpack_from("4sB", data.encode(), idx) + if _len: + val = data[idx + 5 : idx + 5 + _len] + idx += 5 + _len else: val = None idx += 5 @@ -83,12 +90,15 @@ class TLVDiscoveryRequestDatagram(Datagram): self.data = requestdata def __repr__(self): + """Pretty print class.""" return "<%s data=%r>" % (self.__class__.__name__, self.data.items()) class TLVDiscoveryResponseDatagram(Datagram): """Description of a TLV discovery response datagram.""" + def __init__(self, responsedata): + """Initialize class.""" parts = ["E"] # new discovery format for typ, value in responsedata.items(): if value is None: @@ -102,10 +112,14 @@ class TLVDiscoveryResponseDatagram(Datagram): class DiscoveryProtocol: """Description of a discovery protocol.""" + def __init__(self, web_port): + """Initialze class.""" self.web_port = web_port + self.transport = None def connection_made(self, transport): + """Call on connection.""" self.transport = transport # Allow receiving multicast broadcasts sock = self.transport.get_extra_info("socket") @@ -114,12 +128,16 @@ class DiscoveryProtocol: sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) def error_received(self, exc): + """Call on Error.""" LOGGER.error(exc) def connection_lost(self, *args, **kwargs): + """Call on Connection lost.""" + # pylint: disable=unused-argument LOGGER.debug("Connection lost to discovery") - def build_TLV_response(self, requestdata): + def build_tlv_response(self, requestdata): + """Build TLV Response message.""" responsedata = OrderedDict() for typ, value in requestdata.items(): if typ == "NAME": @@ -150,21 +168,25 @@ class DiscoveryProtocol: return responsedata def datagram_received(self, data, addr): + """Datagram received callback.""" + # pylint: disable=broad-except try: data = data.decode() dgram = Datagram.decode(data) if isinstance(dgram, ClientDiscoveryDatagram): - self.sendDiscoveryResponse(addr) + self.send_discovery_response(addr) elif isinstance(dgram, TLVDiscoveryRequestDatagram): - resonsedata = self.build_TLV_response(dgram.data) - self.sendTLVDiscoveryResponse(resonsedata, addr) + resonsedata = self.build_tlv_response(dgram.data) + self.send_tlv_discovery_response(resonsedata, addr) except Exception as exc: LOGGER.exception(exc) - def sendDiscoveryResponse(self, addr): + def send_discovery_response(self, addr): + """Send discovery response message.""" dgram = DiscoveryResponseDatagram(get_hostname(), 3483) self.transport.sendto(dgram.packet.encode(), addr) - def sendTLVDiscoveryResponse(self, resonsedata, addr): + def send_tlv_discovery_response(self, resonsedata, addr): + """Send TLV discovery response message.""" dgram = TLVDiscoveryResponseDatagram(resonsedata) self.transport.sendto(dgram.packet.encode(), addr) diff --git a/music_assistant/providers/squeezebox/socket_client.py b/music_assistant/providers/squeezebox/socket_client.py index a136c6fe..9c1930ab 100644 --- a/music_assistant/providers/squeezebox/socket_client.py +++ b/music_assistant/providers/squeezebox/socket_client.py @@ -48,7 +48,7 @@ class Event(Enum): class SqueezeSocketClient: - """Squeezebox socket client""" + """Squeezebox socket client.""" def __init__( self, @@ -87,7 +87,7 @@ class SqueezeSocketClient: @property def player_id(self) -> str: - """Return player_id (=mac address) of the player.""" + """Return player id (=mac address) of the player.""" return self._player_id @property @@ -97,7 +97,7 @@ class SqueezeSocketClient: @property def device_address(self) -> str: - """Return device IP address of the player""" + """Return device IP address of the player.""" dev_address = self._writer.get_extra_info("peername") return dev_address[0] if dev_address else "" @@ -174,7 +174,8 @@ class SqueezeSocketClient: old_gain = self._volume_control.old_gain() new_gain = self._volume_control.new_gain() await self.__async_send_frame( - b"audg", struct.pack("!LLBBLL", old_gain, old_gain, 1, 255, new_gain, new_gain) + b"audg", + struct.pack("!LLBBLL", old_gain, old_gain, 1, 255, new_gain, new_gain), ) self._volume_level = volume_level @@ -195,9 +196,8 @@ class SqueezeSocketClient: self._powered = True enable_crossfade = crossfade_duration > 0 command = b"s" - autostart = ( - b"3" # we use direct stream for now so let the player do the messy work with buffers - ) + # we use direct stream for now so let the player do the messy work with buffers + autostart = b"3" trans_type = b"1" if enable_crossfade else b"0" formatbyte = b"f" # fixed to flac uri = "/stream" + uri.split("/stream")[1] @@ -206,8 +206,8 @@ class SqueezeSocketClient: autostart=autostart, flags=0x00, formatbyte=formatbyte, - transType=trans_type, - transDuration=crossfade_duration, + trans_type=trans_type, + trans_duration=crossfade_duration, ) # extract host and port from uri regex = "(?:http.*://)?(?P[^:/ ]+).?(?P[0-9]*).*" @@ -227,7 +227,7 @@ class SqueezeSocketClient: async def __async_send_heartbeat(self): """Send periodic heartbeat message to player.""" timestamp = int(time.time()) - data = self.__pack_stream(b"t", replayGain=timestamp, flags=0) + data = self.__pack_stream(b"t", replay_gain=timestamp, flags=0) await self.__async_send_frame(b"strm", data) async def __async_send_frame(self, command, data): @@ -273,13 +273,13 @@ class SqueezeSocketClient: pcmargs=(b"?", b"?", b"?", b"?"), threshold=200, spdif=b"0", - transDuration=0, - transType=b"0", + trans_duration=0, + trans_type=b"0", flags=0x40, - outputThreshold=0, - replayGain=0, - serverPort=8095, - serverIp=0, + output_threshold=0, + replay_gain=0, + server_port=8095, + server_ip=0, ): """Create stream request message based on given arguments.""" return struct.pack( @@ -290,14 +290,14 @@ class SqueezeSocketClient: *pcmargs, threshold, spdif, - transDuration, - transType, + trans_duration, + trans_type, flags, - outputThreshold, + output_threshold, 0, - replayGain, - serverPort, - serverIp, + replay_gain, + server_port, + server_ip, ) @callback @@ -347,30 +347,33 @@ class SqueezeSocketClient: @callback def _process_stat_stmd(self, data): """Process incoming stat STMd message (decoder ready).""" - #pylint: disable=unused-argument + # pylint: disable=unused-argument LOGGER.debug("STMu received - Decoder Ready for next track.") asyncio.create_task(self._event_callback(Event.EVENT_DECODER_READY, self)) @callback def _process_stat_stmf(self, data): """Process incoming stat STMf message (connection closed).""" - #pylint: disable=unused-argument + # pylint: disable=unused-argument LOGGER.debug("STMf received - connection closed.") self._state = State.Stopped asyncio.create_task(self._event_callback(Event.EVENT_UPDATED, self)) @callback def _process_stat_stmo(self, data): - """Process incoming stat STMo message: - No more decoded (uncompressed) data to play; triggers rebuffering.""" - #pylint: disable=unused-argument + """ + Process incoming stat STMo message. + + No more decoded (uncompressed) data to play; triggers rebuffering. + """ + # pylint: disable=unused-argument LOGGER.debug("STMo received - output underrun.") LOGGER.debug("Output Underrun") @callback def _process_stat_stmp(self, data): """Process incoming stat STMp message: Pause confirmed.""" - #pylint: disable=unused-argument + # pylint: disable=unused-argument LOGGER.debug("STMp received - pause confirmed.") self._state = State.Paused asyncio.create_task(self._event_callback(Event.EVENT_UPDATED, self)) @@ -378,22 +381,22 @@ class SqueezeSocketClient: @callback def _process_stat_stmr(self, data): """Process incoming stat STMr message: Resume confirmed.""" - #pylint: disable=unused-argument + # pylint: disable=unused-argument LOGGER.debug("STMr received - resume confirmed.") self._state = State.Playing asyncio.create_task(self._event_callback(Event.EVENT_UPDATED, self)) @callback def _process_stat_stms(self, data): + # pylint: disable=unused-argument """Process incoming stat STMs message: Playback of new track has started.""" LOGGER.debug("STMs received - playback of new track has started.") - #pylint: disable=unused-argument self._state = State.Playing asyncio.create_task(self._event_callback(Event.EVENT_UPDATED, self)) @callback def _process_stat_stmt(self, data): - """Process incoming stat STMt message: heartbeat from client""" + """Process incoming stat STMt message: heartbeat from client.""" # pylint: disable=unused-variable timestamp = time.time() self._last_heartbeat = timestamp @@ -422,7 +425,7 @@ class SqueezeSocketClient: @callback def _process_stat_stmu(self, data): """Process incoming stat STMu message: Buffer underrun: Normal end of playback.""" - #pylint: disable=unused-argument + # pylint: disable=unused-argument LOGGER.debug("STMu received - end of playback.") self.state = State.Stopped asyncio.create_task(self._event_callback(Event.EVENT_UPDATED, self)) @@ -430,7 +433,7 @@ class SqueezeSocketClient: @callback def _process_resp(self, data): """Process incoming RESP message: Response received at player.""" - #pylint: disable=unused-argument + # pylint: disable=unused-argument # send continue asyncio.create_task(self.__async_send_frame(b"cont", b"0")) @@ -446,9 +449,7 @@ class SqueezeSocketClient: class PySqueezeVolume(object): - - """Represents a sound volume. This is an awful lot more complex than it - sounds.""" + """Represents a sound volume. This is an awful lot more complex than it sounds.""" minimum = 0 maximum = 100 @@ -562,30 +563,36 @@ class PySqueezeVolume(object): # new gain parameters, from the same place total_volume_range = -50 # dB - step_point = -1 # Number of steps, up from the bottom, where a 2nd volume ramp kicks in. - step_fraction = 1 # fraction of totalVolumeRange where alternate volume ramp kicks in. + step_point = ( + -1 + ) # Number of steps, up from the bottom, where a 2nd volume ramp kicks in. + step_fraction = ( + 1 # fraction of totalVolumeRange where alternate volume ramp kicks in. + ) def __init__(self): + """Initialize class.""" self.volume = 50 def increment(self): - """Increment the volume""" + """Increment the volume.""" self.volume += self.step if self.volume > self.maximum: self.volume = self.maximum def decrement(self): - """Decrement the volume""" + """Decrement the volume.""" self.volume -= self.step if self.volume < self.minimum: self.volume = self.minimum def old_gain(self): - """Return the "Old" gain value as required by the squeezebox""" + """Return the "Old" gain value as required by the squeezebox.""" return self.old_map[self.volume] def decibels(self): """Return the "new" gain value.""" + # pylint: disable=invalid-name step_db = self.total_volume_range * self.step_fraction max_volume_db = 0 # different on the boom? @@ -609,10 +616,10 @@ class PySqueezeVolume(object): return m * (x2 - x1) + y1 def new_gain(self): - db = self.decibels() - floatmult = 10 ** (db / 20.0) + """Return new gainvalue of the volume control.""" + decibel = self.decibels() + floatmult = 10 ** (decibel / 20.0) # avoid rounding errors somehow - if -30 <= db <= 0: + if -30 <= decibel <= 0: return int(floatmult * (1 << 8) + 0.5) * (1 << 8) - else: - return int((floatmult * (1 << 16)) + 0.5) + return int((floatmult * (1 << 16)) + 0.5) diff --git a/music_assistant/providers/tunein/__init__.py b/music_assistant/providers/tunein/__init__.py index fb730b85..821f7a73 100644 --- a/music_assistant/providers/tunein/__init__.py +++ b/music_assistant/providers/tunein/__init__.py @@ -1,34 +1,20 @@ -"""Tunein musicprovider support for MusicAssistant.""" -import datetime -import hashlib +"""Tune-In musicprovider support for MusicAssistant.""" import logging -import time from typing import List, Optional import aiohttp from asyncio_throttle import Throttler -from music_assistant.constants import ( - CONF_PASSWORD, - CONF_USERNAME, - EVENT_PLAYBACK_STOPPED, - EVENT_STREAM_STARTED, -) +from music_assistant.constants import CONF_PASSWORD, CONF_USERNAME from music_assistant.models.config_entry import ConfigEntry, ConfigEntryType from music_assistant.models.media_types import ( - Album, - AlbumType, - Artist, MediaItemProviderId, MediaType, - Playlist, Radio, SearchResult, - Track, TrackQuality, ) from music_assistant.models.musicprovider import MusicProvider from music_assistant.models.streamdetails import ContentType, StreamDetails, StreamType -from music_assistant.utils import parse_title_and_version, try_parse_int PROV_ID = "tunein" PROV_NAME = "TuneIn Radio" @@ -36,10 +22,14 @@ LOGGER = logging.getLogger(PROV_ID) CONFIG_ENTRIES = [ ConfigEntry( - entry_key=CONF_USERNAME, entry_type=ConfigEntryType.STRING, description_key=CONF_USERNAME + entry_key=CONF_USERNAME, + entry_type=ConfigEntryType.STRING, + description_key=CONF_USERNAME, ), ConfigEntry( - entry_key=CONF_PASSWORD, entry_type=ConfigEntryType.PASSWORD, description_key=CONF_PASSWORD + entry_key=CONF_PASSWORD, + entry_type=ConfigEntryType.PASSWORD, + description_key=CONF_PASSWORD, ), ] @@ -51,6 +41,9 @@ async def async_setup(mass): class TuneInProvider(MusicProvider): + """Provider implementation for Tune In.""" + + # pylint: disable=abstract-method _username = None _password = None @@ -78,7 +71,7 @@ class TuneInProvider(MusicProvider): return [MediaType.Radio] async def async_on_start(self) -> bool: - """Called on startup. Handle initialization of the provider based on config.""" + """Handle initialization of the provider based on config.""" # pylint: disable=attribute-defined-outside-init self._http_session = aiohttp.ClientSession( loop=self.mass.loop, connector=aiohttp.TCPConnector() @@ -92,7 +85,7 @@ class TuneInProvider(MusicProvider): self._throttler = Throttler(rate_limit=1, period=1) async def async_on_stop(self): - """Called on shutdown. Handle correct close/cleanup of the provider on exit.""" + """Handle correct close/cleanup of the provider on exit.""" if self._http_session: await self._http_session.close() @@ -101,6 +94,7 @@ class TuneInProvider(MusicProvider): ) -> SearchResult: """ Perform search on musicprovider. + :param search_query: Search query. :param media_types: A list of media_types to include. All types if None. :param limit: Number of items to return in the search (per type). @@ -120,10 +114,10 @@ class TuneInProvider(MusicProvider): radio = await self.__async_parse_radio(item) yield radio - async def async_get_radio(self, radio_id: str) -> Radio: + async def async_get_radio(self, prov_radio_id: str) -> Radio: """Get radio station details.""" radio = None - params = {"c": "composite", "detail": "listing", "id": radio_id} + params = {"c": "composite", "detail": "listing", "id": prov_radio_id} result = await self.__async_get_data("Describe.ashx", params) if result and result.get("body") and result["body"][0].get("children"): item = result["body"][0]["children"][0] @@ -131,7 +125,7 @@ class TuneInProvider(MusicProvider): return radio async def __async_parse_radio(self, details: dict) -> Radio: - """parse Radio object from json obj returned from api""" + """Parse Radio object from json obj returned from api.""" radio = Radio(item_id=details["preset_id"], provider=PROV_ID) if "name" in details: radio.name = details["name"] @@ -187,22 +181,26 @@ class TuneInProvider(MusicProvider): item_id=item_id, provider=PROV_ID, path=stream["url"], - content_type=stream["media_type"], + content_type=ContentType(stream["media_type"]), sample_rate=44100, bit_depth=16, details=stream, ) return None - async def __async_get_data(self, endpoint, params={}): - """get data from api""" + async def __async_get_data(self, endpoint, params=None): + """Get data from api.""" + if not params: + params = {} url = "https://opml.radiotime.com/%s" % endpoint params["render"] = "json" params["formats"] = "ogg,aac,wma,mp3" params["username"] = self._username params["partnerId"] = "1" async with self._throttler: - async with self._http_session.get(url, params=params, verify_ssl=False) as response: + async with self._http_session.get( + url, params=params, verify_ssl=False + ) as response: result = await response.json() if not result or "error" in result: LOGGER.error(url) diff --git a/music_assistant/providers/webplayer/todo.py b/music_assistant/providers/webplayer/todo.py index 40c095e2..fcacca28 100644 --- a/music_assistant/providers/webplayer/todo.py +++ b/music_assistant/providers/webplayer/todo.py @@ -1,8 +1,6 @@ -#!/usr/bin/env python3 -# -*- coding:utf-8 -*- - +# pylint: skip-file +# flake8: noqa import asyncio -from collections import OrderedDict import decimal import os import random @@ -10,6 +8,7 @@ import socket import struct import sys import time +from collections import OrderedDict from typing import List from music_assistant.constants import CONF_ENABLED diff --git a/music_assistant/utils.py b/music_assistant/utils.py index 49df6616..0117a161 100755 --- a/music_assistant/utils.py +++ b/music_assistant/utils.py @@ -11,7 +11,6 @@ import tempfile import urllib.request from datetime import datetime from enum import Enum -from types import FunctionType, MethodType from typing import Any, Callable, TypeVar import memory_tempfile @@ -43,6 +42,8 @@ def is_callback(func: Callable[..., Any]) -> bool: def run_periodic(period): + """Run a coroutine at interval.""" + def scheduler(fcn): async def async_wrapper(*args, **kwargs): while True: @@ -56,25 +57,26 @@ def run_periodic(period): def get_external_ip(): """Try to get the external (WAN) IP address.""" + # pylint: disable=broad-except try: return urllib.request.urlopen("https://ident.me").read().decode("utf8") - except: + except Exception: return None def filename_from_string(string): - """create filename from unsafe string""" + """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): new_loop = asyncio.new_event_loop() @@ -88,7 +90,7 @@ def run_async_background_task(executor, 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): @@ -97,6 +99,7 @@ def get_sort_name(name): def try_parse_int(possible_int): + """Try to parse an int.""" try: return int(possible_int) except (TypeError, ValueError): @@ -104,7 +107,7 @@ def try_parse_int(possible_int): async def async_iter_items(items): - """fake async iterator for compatability reasons.""" + """Fake async iterator for compatability reasons.""" if not isinstance(items, list): yield items else: @@ -113,6 +116,7 @@ async def async_iter_items(items): def try_parse_float(possible_float): + """Try to parse a float.""" try: return float(possible_float) except (TypeError, ValueError): @@ -120,6 +124,7 @@ def try_parse_float(possible_float): def try_parse_bool(possible_bool): + """Try to parse a bool.""" if isinstance(possible_bool, bool): return possible_bool else: @@ -127,7 +132,7 @@ def try_parse_bool(possible_bool): 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 [" (", " [", " - ", " (", " [", "-"]: @@ -174,7 +179,7 @@ def parse_title_and_version(track_title, track_version=None): 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: @@ -193,22 +198,23 @@ def get_version_substitute(version_str): return version_str.strip() -# pylint: disable=broad-except def get_ip(): - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + """Get primary IP-address for this host.""" + # pylint: disable=broad-except + sock = 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] + sock.connect(("10.255.255.255", 1)) + _ip = sock.getsockname()[0] except Exception: - IP = "127.0.0.1" + _ip = "127.0.0.1" finally: - s.close() - return IP + sock.close() + return _ip def get_ip_pton(): - """Return socket pton for local ip""" + """Return socket pton for local ip.""" try: return socket.inet_pton(socket.AF_INET, get_ip()) except OSError: @@ -224,20 +230,24 @@ def get_hostname(): def get_folder_size(folderpath): - """get folder size in gb""" + """Return 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) + for _file in filenames: + _fp = os.path.join(dirpath, _file) + total_size += os.path.getsize(_fp) # pylint: enable=unused-variable total_size_gb = total_size / float(1 << 30) return total_size_gb class EnhancedJSONEncoder(json.JSONEncoder): + """Custom JSON decoder.""" + def default(self, obj): + """Return default handler.""" + # pylint: disable=method-hidden if dataclasses.is_dataclass(obj): return dataclasses.asdict(obj) if isinstance(obj, Enum): @@ -253,66 +263,41 @@ class EnhancedJSONEncoder(json.JSONEncoder): return super().default(obj) +# pylint: disable=invalid-name json_serializer = functools.partial(json.dumps, cls=EnhancedJSONEncoder) - -# def json_serializer(obj): -# """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)) -# return new_list -# elif hasattr(val, "to_dict"): -# return get_val(val.to_dict()) -# elif isinstance(val, dict): -# new_dict = {} -# for key, value in val.items(): -# new_dict[key] = get_val(value) -# return new_dict -# elif hasattr(val, "__dict__"): -# new_dict = {} -# for key, value in val.__dict__.items(): -# new_dict[key] = get_val(value) -# return new_dict - -# return get_val(obj) +# pylint: enable=invalid-name def get_compare_string(input_str): - """get clean lowered string for compare actions""" + """Return 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""" -# return json.dumps(json_serializer(obj), skipkeys=True) - - def try_load_json_file(jsonfile): - """try to load json from file""" + """Try to load json from file.""" try: - with open(jsonfile) as f: - return json.loads(f.read()) + with open(jsonfile) as _file: + return json.loads(_file.read()) except (FileNotFoundError, json.JSONDecodeError) as exc: - logging.getLogger().debug("Could not load json from file %s", jsonfile, exc_info=exc) + logging.getLogger().debug( + "Could not load json from file %s", jsonfile, exc_info=exc + ) return None def create_tempfile(): """Return a (named) temporary file.""" if platform.system() == "Linux": - return memory_tempfile.MemoryTempfile(fallback=True).NamedTemporaryFile(buffering=0) + return memory_tempfile.MemoryTempfile(fallback=True).NamedTemporaryFile( + buffering=0 + ) return tempfile.NamedTemporaryFile(buffering=0) diff --git a/music_assistant/web.py b/music_assistant/web.py index a7237eda..d7706fec 100755 --- a/music_assistant/web.py +++ b/music_assistant/web.py @@ -1,17 +1,19 @@ -"""The web module handles serving the frontend and the rest/websocket api's""" +"""The web module handles serving the frontend and the rest/websocket api's.""" import asyncio -import base64 import datetime +import functools import inspect import ipaddress -import functools import json import logging import os import ssl import aiohttp +import aiohttp_cors +import jwt from aiohttp import web +from aiohttp_jwt import JWTMiddleware, login_required from music_assistant.constants import ( CONF_KEY_BASE, CONF_KEY_PLAYERSETTINGS, @@ -20,17 +22,12 @@ from music_assistant.constants import ( from music_assistant.models.media_types import MediaType, media_type_from_string from music_assistant.utils import ( EnhancedJSONEncoder, - get_ip, - json_serializer, get_external_ip, get_hostname, + get_ip, + json_serializer, ) -import aiohttp_cors -import jwt -from aiohttp_jwt import JWTMiddleware, login_required, check_permissions, match_any - - LOGGER = logging.getLogger("mass") @@ -38,9 +35,12 @@ class ClassRouteTableDef(web.RouteTableDef): """Helper class to add class based routing tables.""" def __repr__(self) -> str: + """Print the class contents.""" return "".format(len(self._items)) def route(self, method: str, path: str, **kwargs): + """Return the route.""" + # pylint: disable=missing-function-docstring def inner(handler): handler.route_info = (method, path, kwargs) return handler @@ -48,19 +48,25 @@ class ClassRouteTableDef(web.RouteTableDef): return inner def add_class_routes(self, instance) -> None: + """Add class routes.""" + # pylint: disable=missing-function-docstring def predicate(member) -> bool: - return all((inspect.iscoroutinefunction(member), hasattr(member, "route_info"))) + return all( + (inspect.iscoroutinefunction(member), hasattr(member, "route_info")) + ) for _, handler in inspect.getmembers(instance, predicate): method, path, kwargs = handler.route_info super().route(method, path, **kwargs)(handler) +# pylint: disable=invalid-name routes = ClassRouteTableDef() +# pylint: enable=invalid-name def require_local_subnet(func): - """Decorator to specify web method as available locally only.""" + """Return decorator to specify web method as available locally only.""" @functools.wraps(func) async def wrapped(*args, **kwargs): @@ -83,9 +89,10 @@ def require_local_subnet(func): class Web: - """webserver and json/websocket api""" + """Webserver and json/websocket api.""" def __init__(self, mass): + """Initialize class.""" self.mass = mass # load/create/update config self._local_ip = get_ip() @@ -93,19 +100,25 @@ class Web: self.runner = None enable_ssl = self.config["ssl_certificate"] and self.config["ssl_key"] - if self.config["ssl_certificate"] and not os.path.isfile(self.config["ssl_certificate"]): + if self.config["ssl_certificate"] and not os.path.isfile( + self.config["ssl_certificate"] + ): enable_ssl = False - LOGGER.warning("SSL certificate file not found: %s", self.config["ssl_certificate"]) + LOGGER.warning( + "SSL certificate file not found: %s", self.config["ssl_certificate"] + ) if self.config["ssl_key"] and not os.path.isfile(self.config["ssl_key"]): enable_ssl = False - LOGGER.warning("SSL certificate key file not found: %s", self.config["ssl_key"]) + LOGGER.warning( + "SSL certificate key file not found: %s", self.config["ssl_key"] + ) if not self.config.get("external_url"): enable_ssl = False self._enable_ssl = enable_ssl self._jwt_shared_secret = f"mass_{self._local_ip}_{self.http_port}" async def async_setup(self): - """perform async setup""" + """Perform async setup.""" routes.add_class_routes(self) jwt_middleware = JWTMiddleware( self._jwt_shared_secret, request_property="user", credentials_required=False @@ -156,12 +169,11 @@ class Web: LOGGER.info("Started HTTP webserver on port %s", self.http_port) if self._enable_ssl: ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) - ssl_context.load_cert_chain(self.config["ssl_certificate"], self.config["ssl_key"]) + ssl_context.load_cert_chain( + self.config["ssl_certificate"], self.config["ssl_key"] + ) https_site = web.TCPSite( - self.runner, - "0.0.0.0", - self.https_port, - ssl_context=ssl_context, + self.runner, "0.0.0.0", self.https_port, ssl_context=ssl_context ) await https_site.start() LOGGER.info( @@ -209,7 +221,7 @@ class Web: @routes.post("/login") async def async_login(self, request): - """Handler to retrieve a JWT token.""" + """Handle the retrieval of a JWT token.""" form = await request.json() username = form.get("username") password = form.get("password") @@ -220,14 +232,16 @@ class Web: @routes.get("/info") async def async_info(self, request): + # pylint: disable=unused-argument """Return (discovery) info about this instance.""" return web.json_response(self.discovery_info, dumps=json_serializer) async def async_index(self, request): + """Get the index page, redirect if we do not have a web directory.""" # pylint: disable=unused-argument webdir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "web/") if not os.path.isdir(webdir): - raise web.HTTPFound('https://music-assistant.github.io/app') + raise web.HTTPFound("https://music-assistant.github.io/app") return web.FileResponse(os.path.join(webdir, "index.html")) @login_required @@ -288,7 +302,7 @@ class Web: @login_required @routes.put("/api/library") async def async_library_add(self, request): - """Add item(s) to the library""" + """Add item(s) to the library.""" body = await request.json() media_items = await self.__async_media_items_from_body(body) result = await self.mass.music_manager.async_library_add(media_items) @@ -297,7 +311,7 @@ class Web: @login_required @routes.delete("/api/library") async def async_library_remove(self, request): - """R remove item(s) from the library""" + """Remove item(s) from the library.""" body = await request.json() media_items = await self.__async_media_items_from_body(body) result = await self.mass.music_manager.async_library_remove(media_items) @@ -306,31 +320,35 @@ class Web: @login_required @routes.get("/api/artists/{item_id}") async def async_artist(self, request): - """get full artist details""" + """Get full artist details.""" item_id = request.match_info.get("item_id") provider = request.rel_url.query.get("provider") lazy = request.rel_url.query.get("lazy", "false") != "false" if item_id is None or provider is None: return web.Response(text="invalid item or provider", status=501) - result = await self.mass.music_manager.async_get_artist(item_id, provider, lazy=lazy) + result = await self.mass.music_manager.async_get_artist( + item_id, provider, lazy=lazy + ) return web.json_response(result, dumps=json_serializer) @login_required @routes.get("/api/albums/{item_id}") async def async_album(self, request): - """get full album details""" + """Get full album details.""" item_id = request.match_info.get("item_id") provider = request.rel_url.query.get("provider") lazy = request.rel_url.query.get("lazy", "false") != "false" if item_id is None or provider is None: return web.Response(text="invalid item or provider", status=501) - result = await self.mass.music_manager.async_get_album(item_id, provider, lazy=lazy) + result = await self.mass.music_manager.async_get_album( + item_id, provider, lazy=lazy + ) return web.json_response(result, dumps=json_serializer) @login_required @routes.get("/api/tracks/{item_id}") async def async_track(self, request): - """get full track details""" + """Get full track details.""" item_id = request.match_info.get("item_id") provider = request.rel_url.query.get("provider") lazy = request.rel_url.query.get("lazy", "false") != "false" @@ -344,7 +362,7 @@ class Web: @login_required @routes.get("/api/playlists/{item_id}") async def async_playlist(self, request): - """get full playlist details""" + """Get full playlist details.""" item_id = request.match_info.get("item_id") provider = request.rel_url.query.get("provider") if item_id is None or provider is None: @@ -355,7 +373,7 @@ class Web: @login_required @routes.get("/api/radios/{item_id}") async def async_radio(self, request): - """get full radio details""" + """Get full radio details.""" item_id = request.match_info.get("item_id") provider = request.rel_url.query.get("provider") if item_id is None or provider is None: @@ -365,7 +383,7 @@ class Web: @routes.get("/api/{media_type}/{media_id}/thumb") async def async_get_image(self, request): - """get (resized) thumb image""" + """Get (resized) thumb image.""" media_type_str = request.match_info.get("media_type") media_type = media_type_from_string(media_type_str) media_id = request.match_info.get("media_id") @@ -384,7 +402,7 @@ class Web: @login_required @routes.get("/api/artists/{item_id}/toptracks") async def async_artist_toptracks(self, request): - """get top tracks for given artist""" + """Get top tracks for given artist.""" item_id = request.match_info.get("item_id") provider = request.rel_url.query.get("provider") if item_id is None or provider is None: @@ -395,7 +413,7 @@ class Web: @login_required @routes.get("/api/artists/{item_id}/albums") async def async_artist_albums(self, request): - """get (all) albums for given artist""" + """Get (all) albums for given artist.""" item_id = request.match_info.get("item_id") provider = request.rel_url.query.get("provider") if item_id is None or provider is None: @@ -406,7 +424,7 @@ class Web: @login_required @routes.get("/api/playlists/{item_id}/tracks") async def async_playlist_tracks(self, request): - """get playlist tracks from provider""" + """Get playlist tracks from provider.""" item_id = request.match_info.get("item_id") provider = request.rel_url.query.get("provider") if item_id is None or provider is None: @@ -421,7 +439,9 @@ class Web: item_id = request.match_info.get("item_id") body = await request.json() tracks = await self.__async_media_items_from_body(body) - result = await self.mass.music_manager.async_add_playlist_tracks(item_id, tracks) + result = await self.mass.music_manager.async_add_playlist_tracks( + item_id, tracks + ) return web.json_response(result, dumps=json_serializer) @login_required @@ -431,7 +451,9 @@ class Web: item_id = request.match_info.get("item_id") body = await request.json() tracks = await self.__async_media_items_from_body(body) - result = await self.mass.music_manager.async_remove_playlist_tracks(item_id, tracks) + result = await self.mass.music_manager.async_remove_playlist_tracks( + item_id, tracks + ) return web.json_response(result, dumps=json_serializer) @login_required @@ -501,7 +523,7 @@ class Web: @login_required @routes.post("/api/players/{player_id}/play_media/{queue_opt}") async def async_player_play_media(self, request): - """issue player play_media command""" + """Issue player play media command.""" player_id = request.match_info.get("player_id") player = self.mass.player_manager.get_player(player_id) if not player: @@ -509,7 +531,9 @@ class Web: queue_opt = request.match_info.get("queue_opt", "play") body = await request.json() media_items = await self.__async_media_items_from_body(body) - result = await self.mass.player_manager.async_play_media(player_id, media_items, queue_opt) + result = await self.mass.player_manager.async_play_media( + player_id, media_items, queue_opt + ) return web.json_response(result, dumps=json_serializer) @login_required @@ -542,7 +566,7 @@ class Web: @login_required @routes.get("/api/players/{player_id}/queue") async def async_player_queue(self, request): - """return the player queue details""" + """Return the player queue details.""" player_id = request.match_info.get("player_id") player_queue = self.mass.player_manager.get_player_queue(player_id) return web.json_response(player_queue, dumps=json_serializer) @@ -550,7 +574,7 @@ class Web: @login_required @routes.put("/api/players/{player_id}/queue/{cmd}") async def async_player_queue_cmd(self, request): - """change the player queue details""" + """Change the player queue details.""" player_id = request.match_info.get("player_id") player_queue = self.mass.player_manager.get_player_queue(player_id) cmd = request.match_info.get("cmd") @@ -574,7 +598,7 @@ class Web: @login_required @routes.get("/api/players/{player_id}") async def async_player(self, request): - """get single player.""" + """Get single player.""" player_id = request.match_info.get("player_id") player = self.mass.player_manager.get_player(player_id) if not player: @@ -585,7 +609,7 @@ class Web: @routes.get("/api/config") async def async_get_config(self, request): # pylint: disable=unused-argument - """get the config""" + """Get the full config.""" conf = { CONF_KEY_BASE: self.mass.config.base, CONF_KEY_PROVIDERS: self.mass.config.providers, @@ -596,7 +620,7 @@ class Web: @login_required @routes.get("/api/config/{base}") async def async_get_config_item(self, request): - """Get the config.""" + """Get the config by base type.""" conf_base = request.match_info.get("base") conf = self.mass.config[conf_base] return web.json_response(conf, dumps=json_serializer) @@ -604,19 +628,21 @@ class Web: @login_required @routes.put("/api/config/{base}/{key}/{entry_key}") async def async_put_config(self, request): - """save (partial) config""" + """Save the given config item.""" conf_key = request.match_info.get("key") conf_base = request.match_info.get("base") entry_key = request.match_info.get("entry_key") try: new_value = await request.json() except json.decoder.JSONDecodeError: - new_value = self.mass.config[conf_base][conf_key].get_entry(entry_key).default_value + new_value = ( + self.mass.config[conf_base][conf_key].get_entry(entry_key).default_value + ) self.mass.config[conf_base][conf_key][entry_key] = new_value return web.json_response(True) async def async_websocket_handler(self, request): - """websockets handler""" + """Handle websockets connection.""" ws_response = None authenticated = False remove_callbacks = [] @@ -671,7 +697,9 @@ class Web: player_id = msg_details.get("player_id") cmd = msg_details.get("cmd") cmd_args = msg_details.get("cmd_args") - player_cmd = getattr(self.mass.player_manager, f"async_cmd_{cmd}", None) + player_cmd = getattr( + self.mass.player_manager, f"async_cmd_{cmd}", None + ) if player_cmd and cmd_args is not None: result = await player_cmd(player_id, cmd_args) elif player_cmd: @@ -692,7 +720,8 @@ class Web: @require_local_subnet async def async_json_rpc(self, request): """ - implement LMS jsonrpc interface + Implement LMS jsonrpc interface. + for some compatability with tools that talk to lms only support for basic commands """ @@ -748,7 +777,7 @@ class Web: return web.Response(text="success") async def __async_media_items_from_body(self, data): - """Helper to turn posted body data into media items.""" + """Convert posted body data into media items.""" if not isinstance(data, list): data = [data] media_items = [] @@ -760,7 +789,7 @@ class Web: return media_items async def __async_stream_json(self, request, iterator): - """stream items from async iterator as json object""" + """Stream items from async iterator as json object.""" resp = web.StreamResponse( status=200, reason="OK", headers={"Content-Type": "application/json"} ) @@ -799,4 +828,4 @@ class Web: "expires": token_expires, "scopes": scopes, } - return None \ No newline at end of file + return None diff --git a/setup.cfg b/setup.cfg index f7bc069c..c8ed9243 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,7 +12,8 @@ ignore = W503, E203, D202, - W504 + W504, + E266 [isort] multi_line_output = 3 diff --git a/setup.py b/setup.py index 39c4353b..f24185b5 100644 --- a/setup.py +++ b/setup.py @@ -25,14 +25,14 @@ DOWNLOAD_URL = f"{GITHUB_URL}/archive/{mass_const.__version__}.zip" PROJECT_URLS = { "Bug Reports": f"{GITHUB_URL}/issues", "Website": "https://music-assistant.github.io/", - "Discord": "https://discord.gg/9xHYFY" + "Discord": "https://discord.gg/9xHYFY", } PACKAGES = find_packages(exclude=["tests", "tests.*"]) PACKAGE_FILES = [] -for (path, directories, filenames) in os.walk('music_assistant/'): +for (path, directories, filenames) in os.walk("music_assistant/"): for filename in filenames: - PACKAGE_FILES.append(os.path.join('..', path, filename)) + PACKAGE_FILES.append(os.path.join("..", path, filename)) with open("requirements.txt") as f: REQUIRES = f.read().splitlines() @@ -53,8 +53,11 @@ setup( install_requires=REQUIRES, python_requires=f">={mass_const.REQUIRED_PYTHON_VER}", test_suite="tests", - entry_points={"console_scripts": ["mass = music_assistant.__main__:main", "musicassistant = music_assistant.__main__:main"]}, - package_data={ - 'music_assistant': PACKAGE_FILES, + entry_points={ + "console_scripts": [ + "mass = music_assistant.__main__:main", + "musicassistant = music_assistant.__main__:main", + ] }, + package_data={"music_assistant": PACKAGE_FILES}, ) -- 2.34.1