From a0ec416691c88c7c6fb82d5ff9772e2525c5e462 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 17 Feb 2021 01:05:05 +0100 Subject: [PATCH] refactor: speed, stability and code quality --- music_assistant/__main__.py | 6 +- music_assistant/constants.py | 2 +- music_assistant/helpers/cache.py | 28 +- music_assistant/helpers/encryption.py | 24 +- music_assistant/helpers/images.py | 16 +- music_assistant/helpers/migration.py | 14 +- music_assistant/helpers/musicbrainz.py | 32 +- music_assistant/helpers/process.py | 143 +----- music_assistant/helpers/typing.py | 28 +- music_assistant/helpers/util.py | 38 +- music_assistant/helpers/web.py | 21 +- music_assistant/managers/config.py | 40 +- music_assistant/managers/database.py | 297 ++++++------ music_assistant/managers/library.py | 216 +++++---- music_assistant/managers/metadata.py | 14 +- music_assistant/managers/music.py | 335 ++++++------- music_assistant/managers/players.py | 448 +++++++++--------- music_assistant/managers/streams.py | 62 ++- music_assistant/mass.py | 69 +-- music_assistant/models/media_types.py | 62 ++- music_assistant/models/player.py | 333 ++++++++++--- music_assistant/models/player_queue.py | 150 +++--- music_assistant/models/player_state.py | 407 ---------------- music_assistant/models/provider.py | 62 ++- .../providers/builtin_player/__init__.py | 56 ++- .../providers/chromecast/__init__.py | 10 +- .../providers/chromecast/player.py | 89 ++-- .../providers/fanarttv/__init__.py | 12 +- music_assistant/providers/file/__init__.py | 92 ++-- music_assistant/providers/qobuz/__init__.py | 206 ++++---- music_assistant/providers/sonos/__init__.py | 4 +- music_assistant/providers/sonos/sonos.py | 56 +-- music_assistant/providers/spotify/__init__.py | 194 ++++---- .../providers/squeezebox/__init__.py | 109 +++-- .../providers/squeezebox/socket_client.py | 58 +-- music_assistant/providers/tunein/__init__.py | 36 +- .../providers/universal_group/__init__.py | 75 ++- music_assistant/web/json_rpc.py | 42 +- music_assistant/web/server.py | 76 ++- music_assistant/web/streams.py | 18 +- requirements.txt | 2 +- 41 files changed, 1782 insertions(+), 2200 deletions(-) delete mode 100755 music_assistant/models/player_state.py diff --git a/music_assistant/__main__.py b/music_assistant/__main__.py index 4fe28a6d..e6fbb2f9 100755 --- a/music_assistant/__main__.py +++ b/music_assistant/__main__.py @@ -44,7 +44,7 @@ def main(): # setup logger logger = logging.getLogger() logformat = logging.Formatter( - "%(asctime)-15s %(levelname)-5s %(name)s.%(funcName)s -- %(message)s" + "%(asctime)-15s %(levelname)-5s %(name)s -- %(message)s" ) consolehandler = logging.StreamHandler() consolehandler.setFormatter(logformat) @@ -68,10 +68,10 @@ def main(): def on_shutdown(loop): logger.info("shutdown requested!") - loop.run_until_complete(mass.async_stop()) + loop.run_until_complete(mass.stop()) run( - mass.async_start(), + mass.start(), use_uvloop=True, shutdown_callback=on_shutdown, executor_workers=64, diff --git a/music_assistant/constants.py b/music_assistant/constants.py index 876fde6b..056d7d5d 100755 --- a/music_assistant/constants.py +++ b/music_assistant/constants.py @@ -1,6 +1,6 @@ """All constants for Music Assistant.""" -__version__ = "0.0.87" +__version__ = "0.1.0" REQUIRED_PYTHON_VER = "3.7" # configuration keys/attributes diff --git a/music_assistant/helpers/cache.py b/music_assistant/helpers/cache.py index e48aad82..dca7611a 100644 --- a/music_assistant/helpers/cache.py +++ b/music_assistant/helpers/cache.py @@ -25,7 +25,7 @@ class Cache: self._dbfile = os.path.join(mass.config.data_path, ".cache.db") self._mem_cache = {} - async def async_setup(self): + async def setup(self): """Async initialize of cache module.""" async with aiosqlite.connect(self._dbfile, timeout=180) as db_conn: await db_conn.execute( @@ -35,9 +35,9 @@ class Cache: await db_conn.commit() await db_conn.execute("VACUUM;") await db_conn.commit() - self.mass.add_job(self.async_auto_cleanup()) + self.mass.add_job(self.auto_cleanup()) - async def async_get(self, cache_key, checksum="", default=None): + async def get(self, cache_key, checksum="", default=None): """ Get object from cache and return the results. @@ -80,7 +80,7 @@ class Cache: LOGGER.debug("no cache data for %s", cache_key) return default - async def async_set(self, cache_key, data, checksum="", expiration=(86400 * 30)): + async def set(self, cache_key, data, checksum="", expiration=(86400 * 30)): """Set data in cache.""" checksum = self._get_checksum(checksum) expires = int(time.time() + expiration) @@ -94,7 +94,7 @@ class Cache: await db_conn.execute(sql_query, (cache_key, expires, data, checksum)) await db_conn.commit() - async def async_delete(self, cache_key): + async def delete(self, cache_key): """Delete data from cache.""" self._mem_cache.pop(cache_key, None) sql_query = "DELETE FROM simplecache WHERE id = ?" @@ -103,7 +103,7 @@ class Cache: await db_conn.commit() @run_periodic(3600) - async def async_auto_cleanup(self): + async def auto_cleanup(self): """Sceduled auto cleanup task.""" # for now we simply rest the memory cache self._mem_cache = {} @@ -133,7 +133,7 @@ class Cache: return functools.reduce(lambda x, y: x + y, map(ord, stringinput)) -async def async_cached( +async def cached( cache, cache_key: str, coro_func: Awaitable, @@ -142,31 +142,31 @@ async def async_cached( checksum=None ): """Return helper method to store results of a coroutine in the cache.""" - cache_result = await cache.async_get(cache_key, checksum) + cache_result = await cache.get(cache_key, checksum) if cache_result is not None: return cache_result result = await coro_func(*args) - asyncio.create_task(cache.async_set(cache_key, result, checksum, expires)) + asyncio.create_task(cache.set(cache_key, result, checksum, expires)) return result -def async_use_cache(cache_days=14, cache_checksum=None): +def use_cache(cache_days=14, cache_checksum=None): """Return decorator that can be used to cache a method's result.""" def wrapper(func): @functools.wraps(func) - async def async_wrapped(*args, **kwargs): + async def wrapped(*args, **kwargs): method_class = args[0] method_class_name = method_class.__class__.__name__ cache_str = "%s.%s" % (method_class_name, func.__name__) cache_str += __cache_id_from_args(*args, **kwargs) cache_str = cache_str.lower() - cachedata = await method_class.cache.async_get(cache_str) + cachedata = await method_class.cache.get(cache_str) if cachedata is not None: return cachedata result = await func(*args, **kwargs) asyncio.create_task( - method_class.cache.async_set( + method_class.cache.set( cache_str, result, checksum=cache_checksum, @@ -175,7 +175,7 @@ def async_use_cache(cache_days=14, cache_checksum=None): ) return result - return async_wrapped + return wrapped return wrapper diff --git a/music_assistant/helpers/encryption.py b/music_assistant/helpers/encryption.py index a6639057..eca69ba9 100644 --- a/music_assistant/helpers/encryption.py +++ b/music_assistant/helpers/encryption.py @@ -6,38 +6,38 @@ from cryptography.fernet import Fernet, InvalidToken from music_assistant.helpers.app_vars import get_app_var # noqa # pylint: disable=all -async def async_encrypt_string(str_value: str) -> str: +async def encrypt_string(str_value: str) -> str: """Encrypt a string with Fernet.""" return await asyncio.get_running_loop().run_in_executor( - None, encrypt_string, str_value + None, _encrypt_string, str_value ) -def encrypt_string(str_value: str) -> str: +def _encrypt_string(str_value: str) -> str: """Encrypt a string with Fernet.""" return Fernet(get_app_var(3)).encrypt(str_value.encode()).decode() -async def async_encrypt_bytes(bytes_value: bytes) -> bytes: +async def encrypt_bytes(bytes_value: bytes) -> bytes: """Encrypt bytes with Fernet.""" return await asyncio.get_running_loop().run_in_executor( - None, encrypt_bytes, bytes_value + None, _encrypt_bytes, bytes_value ) -def encrypt_bytes(bytes_value: bytes) -> bytes: +def _encrypt_bytes(bytes_value: bytes) -> bytes: """Encrypt bytes with Fernet.""" return Fernet(get_app_var(3)).encrypt(bytes_value) -async def async_decrypt_string(encrypted_str: str) -> str: +async def decrypt_string(encrypted_str: str) -> str: """Decrypt a string with Fernet.""" return await asyncio.get_running_loop().run_in_executor( - None, decrypt_string, encrypted_str + None, _decrypt_string, encrypted_str ) -def decrypt_string(encrypted_str: str) -> str: +def _decrypt_string(encrypted_str: str) -> str: """Decrypt a string with Fernet.""" try: return Fernet(get_app_var(3)).decrypt(encrypted_str.encode()).decode() @@ -45,14 +45,14 @@ def decrypt_string(encrypted_str: str) -> str: return None -async def async_decrypt_bytes(bytes_value: bytes) -> bytes: +async def decrypt_bytes(bytes_value: bytes) -> bytes: """Decrypt bytes with Fernet.""" return await asyncio.get_running_loop().run_in_executor( - None, decrypt_bytes, bytes_value + None, _decrypt_bytes, bytes_value ) -def decrypt_bytes(bytes_value): +def _decrypt_bytes(bytes_value): """Decrypt bytes with Fernet.""" try: return Fernet(get_app_var(3)).decrypt(bytes_value) diff --git a/music_assistant/helpers/images.py b/music_assistant/helpers/images.py index 424e0aeb..ebdea7aa 100644 --- a/music_assistant/helpers/images.py +++ b/music_assistant/helpers/images.py @@ -3,16 +3,16 @@ import os from io import BytesIO -from music_assistant.helpers.typing import MusicAssistantType +from music_assistant.helpers.typing import MusicAssistant from music_assistant.models.media_types import MediaType from PIL import Image -async def async_get_thumb_file(mass: MusicAssistantType, url, size: int = 150): +async def get_thumb_file(mass: MusicAssistant, url, size: int = 150): """Get path to (resized) thumbnail image for given image url.""" assert url cache_folder = os.path.join(mass.config.data_path, ".thumbs") - cache_id = await mass.database.async_get_thumbnail_id(url, size) + cache_id = await mass.database.get_thumbnail_id(url, size) cache_file = os.path.join(cache_folder, f"{cache_id}.png") if os.path.isfile(cache_file): # return file from cache @@ -39,11 +39,11 @@ async def async_get_thumb_file(mass: MusicAssistantType, url, size: int = 150): return cache_file -async def async_get_image_url( - mass: MusicAssistantType, item_id: str, provider_id: str, media_type: MediaType +async def get_image_url( + mass: MusicAssistant, item_id: str, provider_id: str, media_type: MediaType ): """Get url to image for given media item.""" - item = await mass.music.async_get_item(item_id, provider_id, media_type) + item = await mass.music.get_item(item_id, provider_id, media_type) if not item: return None if item and item.metadata.get("image"): @@ -66,12 +66,12 @@ async def async_get_image_url( return item.album.metadata["image"] if media_type == MediaType.Track and item.album: # try album instead for tracks - return await async_get_image_url( + return await get_image_url( mass, item.album.item_id, item.album.provider, MediaType.Album ) if media_type == MediaType.Album and item.artist: # try artist instead for albums - return await async_get_image_url( + return await get_image_url( mass, item.artist.item_id, item.artist.provider, MediaType.Artist ) return None diff --git a/music_assistant/helpers/migration.py b/music_assistant/helpers/migration.py index 14b212e8..a82931cc 100644 --- a/music_assistant/helpers/migration.py +++ b/music_assistant/helpers/migration.py @@ -9,11 +9,11 @@ from pkg_resources import packaging import aiosqlite from music_assistant.constants import __version__ as app_version from music_assistant.helpers.encryption import encrypt_string -from music_assistant.helpers.typing import MusicAssistantType +from music_assistant.helpers.typing import MusicAssistant from music_assistant.helpers.util import get_hostname -async def check_migrations(mass: MusicAssistantType): +async def check_migrations(mass: MusicAssistant): """Check for any migrations that need to be done.""" is_fresh_setup = len(mass.config.stored_config.keys()) == 0 @@ -29,7 +29,7 @@ async def check_migrations(mass: MusicAssistantType): if "server_id" not in mass.config.stored_config: mass.config.stored_config["server_id"] = str(uuid.getnode()) if "jwt_key" not in mass.config.stored_config: - mass.config.stored_config["jwt_key"] = encrypt_string(str(uuid.uuid4())) + mass.config.stored_config["jwt_key"] = await encrypt_string(str(uuid.uuid4())) if "initialized" not in mass.config.stored_config: mass.config.stored_config["initialized"] = False if "friendly_name" not in mass.config.stored_config: @@ -37,10 +37,10 @@ async def check_migrations(mass: MusicAssistantType): mass.config.save() # create default db tables (if needed) - await async_create_db_tables(mass.database.db_file) + await create_db_tables(mass.database.db_file) -async def run_migration_0070(mass: MusicAssistantType): +async def run_migration_0070(mass: MusicAssistant): """Run migration for version 0.0.70.""" # 0.0.70 introduced major changes to all data models and db structure # a full refresh of data is unavoidable @@ -82,7 +82,7 @@ async def run_migration_0070(mass: MusicAssistantType): shutil.rmtree(dirname, True) # create default db tables (if needed) - await async_create_db_tables(mass.database.db_file) + await create_db_tables(mass.database.db_file) # restore loudness measurements if tracks_loudness: @@ -94,7 +94,7 @@ async def run_migration_0070(mass: MusicAssistantType): await db_conn.commit() -async def async_create_db_tables(db_file): +async def create_db_tables(db_file): """Async initialization.""" async with aiosqlite.connect(db_file, timeout=120) as db_conn: diff --git a/music_assistant/helpers/musicbrainz.py b/music_assistant/helpers/musicbrainz.py index 5795fac4..5660d414 100644 --- a/music_assistant/helpers/musicbrainz.py +++ b/music_assistant/helpers/musicbrainz.py @@ -7,7 +7,7 @@ from typing import Optional import aiohttp from asyncio_throttle import Throttler -from music_assistant.helpers.cache import async_use_cache +from music_assistant.helpers.cache import use_cache from music_assistant.helpers.compare import compare_strings, get_compare_string LUCENE_SPECIAL = r'([+\-&|!(){}\[\]\^"~*?:\\\/])' @@ -24,7 +24,7 @@ class MusicBrainz: self.cache = mass.cache self.throttler = Throttler(rate_limit=1, period=1) - async def async_get_mb_artist_id( + async def get_mb_artist_id( self, artistname, albumname=None, @@ -44,7 +44,7 @@ class MusicBrainz: ) mb_artist_id = None if album_upc: - mb_artist_id = await self.async_search_artist_by_album( + mb_artist_id = await self.search_artist_by_album( artistname, None, album_upc ) if mb_artist_id: @@ -55,7 +55,7 @@ class MusicBrainz: mb_artist_id, ) if not mb_artist_id and track_isrc: - mb_artist_id = await self.async_search_artist_by_track( + mb_artist_id = await self.search_artist_by_track( artistname, None, track_isrc ) if mb_artist_id: @@ -66,9 +66,7 @@ class MusicBrainz: mb_artist_id, ) if not mb_artist_id and albumname: - mb_artist_id = await self.async_search_artist_by_album( - artistname, albumname - ) + mb_artist_id = await self.search_artist_by_album(artistname, albumname) if mb_artist_id: LOGGER.debug( "Got MusicbrainzArtistId for %s after search on albumname %s --> %s", @@ -77,9 +75,7 @@ class MusicBrainz: mb_artist_id, ) if not mb_artist_id and trackname: - mb_artist_id = await self.async_search_artist_by_track( - artistname, trackname - ) + mb_artist_id = await self.search_artist_by_track(artistname, trackname) if mb_artist_id: LOGGER.debug( "Got MusicbrainzArtistId for %s after search on trackname %s --> %s", @@ -89,9 +85,7 @@ class MusicBrainz: ) return mb_artist_id - async def async_search_artist_by_album( - self, artistname, albumname=None, album_upc=None - ): + async def 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), @@ -107,7 +101,7 @@ class MusicBrainz: "query": 'artist:"%s" AND release:"%s"' % (searchartist, searchalbum) } - result = await self.async_get_data(endpoint, params) + result = await self.get_data(endpoint, params) if result and "releases" in result: for strictness in [True, False]: for item in result["releases"]: @@ -126,9 +120,7 @@ class MusicBrainz: return artist["id"] return "" - async def async_search_artist_by_track( - self, artistname, trackname=None, track_isrc=None - ): + async def 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) @@ -140,7 +132,7 @@ class MusicBrainz: searchtrack = re.sub(LUCENE_SPECIAL, r"\\\1", trackname) endpoint = "recording" params = {"query": '"%s" AND artist:"%s"' % (searchtrack, searchartist)} - result = await self.async_get_data(endpoint, params) + result = await self.get_data(endpoint, params) if result and "recordings" in result: for strictness in [True, False]: for item in result["recordings"]: @@ -159,8 +151,8 @@ class MusicBrainz: return artist["id"] return "" - @async_use_cache(2) - async def async_get_data(self, endpoint: str, params: Optional[dict] = None): + @use_cache(2) + async def get_data(self, endpoint: str, params: Optional[dict] = None): """Get data from api.""" if params is None: params = {} diff --git a/music_assistant/helpers/process.py b/music_assistant/helpers/process.py index 1bcfa27b..79a740f8 100644 --- a/music_assistant/helpers/process.py +++ b/music_assistant/helpers/process.py @@ -3,17 +3,10 @@ Implementation of a (truly) non blocking subprocess. The subprocess implementation in asyncio can (still) sometimes cause deadlocks, even when properly handling reading/writes from different tasks. -Besides that, when using multiple asyncio subprocesses, together with uvloop -things go very wrong: https://github.com/MagicStack/uvloop/issues/317 - -As we rely a lot on moving chunks around through subprocesses (mainly sox), -this custom implementation can be seen as a temporary solution until the main issue -in uvloop is resolved. """ import asyncio import logging -import subprocess from typing import AsyncGenerator, List, Optional LOGGER = logging.getLogger("AsyncProcess") @@ -24,129 +17,6 @@ DEFAULT_CHUNKSIZE = 512000 class AsyncProcess: """Implementation of a (truly) non blocking subprocess.""" - # workaround that is compatible with uvloop - - def __init__( - self, process_args: List, enable_write: bool = False, enable_shell=False - ): - """Initialize.""" - self._id = "".join(process_args) - self._proc = subprocess.Popen( - process_args, - shell=enable_shell, - stdout=subprocess.PIPE, - stdin=subprocess.PIPE if enable_write else None, - # bufsize needs to be very high for smooth playback - bufsize=4000000, - ) - self.loop = asyncio.get_running_loop() - self._cancelled = False - - async def __aenter__(self) -> "AsyncProcess": - """Enter context manager.""" - return self - - async def __aexit__(self, exc_type, exc_value, traceback) -> bool: - """Exit context manager.""" - self._cancelled = True - if self._proc.poll() is None: - # process needs to be cleaned up.. - await self.loop.run_in_executor(None, self.__close) - - return exc_type - - async def iterate_chunks( - self, chunksize: int = DEFAULT_CHUNKSIZE - ) -> AsyncGenerator[bytes, None]: - """Yield chunks from the process stdout. Generator.""" - while True: - chunk = await self.read(chunksize) - yield chunk - if len(chunk) < chunksize: - # last chunk - break - - async def read(self, chunksize: int = DEFAULT_CHUNKSIZE) -> bytes: - """Read x bytes from the process stdout.""" - if self._cancelled: - raise asyncio.CancelledError() - return await self.loop.run_in_executor(None, self.__read, chunksize) - - async def write(self, data: bytes) -> None: - """Write data to process stdin.""" - if self._cancelled: - raise asyncio.CancelledError() - await self.loop.run_in_executor(None, self.__write, data) - - async def write_eof(self) -> None: - """Write eof to process.""" - if self._cancelled: - raise asyncio.CancelledError() - await self.loop.run_in_executor(None, self.__write_eof) - - async def communicate(self, input_data: Optional[bytes] = None) -> bytes: - """Write bytes to process and read back results.""" - if self._cancelled: - raise asyncio.CancelledError() - return await self.loop.run_in_executor(None, self._proc.communicate, input_data) - - def __read(self, chunksize: int = DEFAULT_CHUNKSIZE): - """Try read chunk from process.""" - try: - return self._proc.stdout.read(chunksize) - except (BrokenPipeError, ValueError, AttributeError): - # Process already exited - return b"" - - def __write(self, data: bytes): - """Write data to process stdin.""" - try: - self._proc.stdin.write(data) - self._proc.stdin.flush() - except (BrokenPipeError, ValueError, AttributeError): - # Process already exited - pass - - def __write_eof(self): - """Write eof to process stdin.""" - try: - self._proc.stdin.close() - except (BrokenPipeError, ValueError, AttributeError): - # Process already exited - pass - - def __close(self): - """Prevent subprocess deadlocking, make sure it closes.""" - LOGGER.debug("Cleaning up process %s...", self._id) - # close stdout - if not self._proc.stdout.closed: - try: - self._proc.stdout.close() - except BrokenPipeError: - pass - # close stdin if needed - if self._proc.stdin and not self._proc.stdin.closed: - try: - self._proc.stdin.close() - except BrokenPipeError: - pass - # send terminate - self._proc.terminate() - # wait for exit - try: - self._proc.wait(5) - LOGGER.debug("Process %s exited with %s.", self._id, self._proc.returncode) - except subprocess.TimeoutExpired: - LOGGER.error("Process %s did not terminate in time.", self._id) - self._proc.kill() - LOGGER.debug("Process %s closed.", self._id) - - -class AsyncProcessBroken: - """Implementation of a (truly) non blocking subprocess.""" - - # this version is not compatible with uvloop - def __init__(self, process_args: List, enable_write: bool = False): """Initialize.""" self._proc = None @@ -160,14 +30,13 @@ class AsyncProcessBroken: *self._process_args, stdin=asyncio.subprocess.PIPE if self._enable_write else None, stdout=asyncio.subprocess.PIPE, - limit=64000000 + limit=4000000 ) return self async def __aexit__(self, exc_type, exc_value, traceback) -> bool: """Exit context manager.""" self._cancelled = True - LOGGER.debug("subprocess exit requested") if self._proc.returncode is None: # prevent subprocess deadlocking, send terminate and read remaining bytes if self._enable_write and self._proc.stdin.can_write_eof(): @@ -175,7 +44,6 @@ class AsyncProcessBroken: self._proc.terminate() await self._proc.stdout.read() del self._proc - LOGGER.debug("subprocess exited") async def iterate_chunks( self, chunk_size: int = DEFAULT_CHUNKSIZE @@ -198,10 +66,15 @@ class AsyncProcessBroken: async def write(self, data: bytes) -> None: """Write data to process stdin.""" - if self._cancelled: + if self._cancelled or not self._proc: + raise asyncio.CancelledError() + if self._proc.stdin.is_closing(): raise asyncio.CancelledError() self._proc.stdin.write(data) - await self._proc.stdin.drain() + try: + await self._proc.stdin.drain() + except BrokenPipeError: + pass async def write_eof(self) -> None: """Write eof to process.""" diff --git a/music_assistant/helpers/typing.py b/music_assistant/helpers/typing.py index 29f12289..2b567a7d 100644 --- a/music_assistant/helpers/typing.py +++ b/music_assistant/helpers/typing.py @@ -1,27 +1,29 @@ """Typing helper.""" -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, Optional, Set # pylint: disable=invalid-name if TYPE_CHECKING: - from music_assistant.mass import MusicAssistant as MusicAssistantType + from music_assistant.mass import MusicAssistant from music_assistant.models.player_queue import ( - QueueItem as QueueItemType, - PlayerQueue as PlayerQueueType, + QueueItem, + PlayerQueue, ) - from music_assistant.models.streamdetails import StreamDetails as StreamDetailsType - from music_assistant.models.player import Player as PlayerType + from music_assistant.models.streamdetails import StreamDetails + from music_assistant.models.player import Player + from music_assistant.managers.config import ConfigSubItem else: - MusicAssistantType = "MusicAssistant" - QueueItemType = "QueueItem" - PlayerQueueType = "PlayerQueue" - StreamDetailsType = "StreamDetailsType" - PlayerType = "PlayerType" + MusicAssistant = "MusicAssistant" + QueueItem = "QueueItem" + PlayerQueue = "PlayerQueue" + StreamDetails = "StreamDetails" + Player = "Player" + ConfigSubItem = "ConfigSubItem" -QueueItems = List[QueueItemType] -Players = List[PlayerType] +QueueItems = Set[QueueItem] +Players = Set[Player] OptionalInt = Optional[int] OptionalStr = Optional[str] diff --git a/music_assistant/helpers/util.py b/music_assistant/helpers/util.py index 331f8b15..8c5489f6 100755 --- a/music_assistant/helpers/util.py +++ b/music_assistant/helpers/util.py @@ -8,7 +8,7 @@ import struct import tempfile import urllib.request from io import BytesIO -from typing import Any, Callable, TypeVar +from typing import Any, Callable, Dict, Optional, Set, TypeVar import memory_tempfile import ujson @@ -36,12 +36,12 @@ def run_periodic(period): """Run a coroutine at interval.""" def scheduler(fcn): - async def async_wrapper(*args, **kwargs): + async def wrapper(*args, **kwargs): while True: asyncio.create_task(fcn(*args, **kwargs)) await asyncio.sleep(period) - return async_wrapper + return wrapper return scheduler @@ -66,7 +66,7 @@ def run_background_task(corofn, *args, executor=None): return asyncio.get_event_loop().run_in_executor(executor, corofn, *args) -def run_async_background_task(executor, corofn, *args): +def run__background_task(executor, corofn, *args): """Run async task in background.""" def run_task(corofn, *args): @@ -88,7 +88,7 @@ def try_parse_int(possible_int): return 0 -async def async_iter_items(items): +async def iter_items(items): """Fake async iterator for compatability reasons.""" if not isinstance(items, list): yield items @@ -240,23 +240,22 @@ def merge_dict(base_dict: dict, new_dict: dict, allow_overwite=False): return final_dict -def merge_list(base_list: list, new_list: list): +def merge_list(base_list: list, new_list: list) -> Set: """Merge 2 lists.""" - final_list = [] - final_list += base_list + final_list = set(base_list) for item in new_list: if hasattr(item, "item_id"): for prov_item in final_list: if prov_item.item_id == item.item_id: prov_item = item if item not in final_list: - final_list.append(item) + final_list.add(item) return final_list def unique_item_ids(objects): """Filter duplicate item id's from list of items.""" - return list({object_.item_id: object_ for object_ in objects}.values()) + return set({object_.item_id for object_ in objects}) def try_load_json_file(jsonfile): @@ -280,13 +279,30 @@ def create_tempfile(): return tempfile.NamedTemporaryFile(buffering=0) -async def async_yield_chunks(_obj, chunk_size): +async def yield_chunks(_obj, chunk_size): """Yield successive n-sized chunks from list/str/bytes.""" chunk_size = int(chunk_size) for i in range(0, len(_obj), chunk_size): yield _obj[i : i + chunk_size] +def get_changed_keys( + dict1: Dict[str, Any], dict2: Dict[str, Any], ignore_keys: Optional[Set[str]] = None +) -> Set[str]: + """Compare 2 dicts and return set of changed keys.""" + if not dict2: + return set(dict1.keys()) + changed_keys = set() + for key, value in dict2.items(): + if ignore_keys and key in ignore_keys: + continue + if isinstance(value, dict): + changed_keys.update(get_changed_keys(dict1[key], value)) + elif dict1[key] != value: + changed_keys.add(key) + return changed_keys + + def create_wave_header(samplerate=44100, channels=2, bitspersample=16, duration=3600): """Generate a wave header from given params.""" file = BytesIO() diff --git a/music_assistant/helpers/web.py b/music_assistant/helpers/web.py index e0c5bcd3..98c35a4e 100644 --- a/music_assistant/helpers/web.py +++ b/music_assistant/helpers/web.py @@ -1,9 +1,7 @@ """Various helpers for web requests.""" -import asyncio import inspect import ipaddress -from datetime import datetime from functools import wraps from typing import Any, Callable, Union @@ -47,10 +45,10 @@ def serialize_values(obj): def get_val(val): if hasattr(val, "to_dict"): return val.to_dict() - if isinstance(val, (list, set, filter, {}.values().__class__)): + if isinstance(val, (list, set, filter, tuple)): + return [get_val(x) for x in val] + if val.__class__ == "dict_valueiterator": return [get_val(x) for x in val] - if isinstance(val, datetime): - return val.isoformat() if isinstance(val, dict): return {key: get_val(value) for key, value in val.items()} return val @@ -61,27 +59,16 @@ def serialize_values(obj): def json_serializer(obj): """Json serializer to recursively create serializable values for custom data types.""" return ujson.dumps(serialize_values(obj)) + # return ujson.dumps(obj) def json_response(data: Any, status: int = 200): """Return json in web request.""" - # return web.json_response(data, dumps=json_serializer) return web.Response( body=json_serializer(data), status=200, content_type="application/json" ) -async def async_json_response(data: Any, status: int = 200): - """Return json in web request.""" - if isinstance(data, list): - # we could potentially receive a large list of objects to serialize - # which is blocking IO so run it in executor to be safe - return await asyncio.get_running_loop().run_in_executor( - None, json_response, data - ) - return json_response(data) - - def api_route(ws_cmd_path, ws_require_auth=True): """Decorate a function as websocket command.""" diff --git a/music_assistant/managers/config.py b/music_assistant/managers/config.py index 4da39d0a..d64d37d3 100755 --- a/music_assistant/managers/config.py +++ b/music_assistant/managers/config.py @@ -32,7 +32,7 @@ from music_assistant.constants import ( CONF_VOLUME_NORMALISATION, EVENT_CONFIG_CHANGED, ) -from music_assistant.helpers.encryption import decrypt_string, encrypt_string +from music_assistant.helpers.encryption import _decrypt_string, _encrypt_string from music_assistant.helpers.util import merge_dict, try_load_json_file from music_assistant.helpers.web import api_route from music_assistant.models.config_entry import ConfigEntry, ConfigEntryType @@ -155,9 +155,9 @@ class ConfigManager: raise FileNotFoundError(f"data directory {data_path} does not exist!") self.__load() - async def async_setup(self): + async def setup(self): """Async initialize of module.""" - self._translations = await self.__async_fetch_translations() + self._translations = await self._fetch_translations() @api_route("config/:conf_base?/:conf_key?") def all_items(self, conf_base: str = "", conf_key: str = "") -> dict: @@ -279,7 +279,7 @@ class ConfigManager: """Return item value by key.""" return getattr(self, item_key) - async def async_close(self): + async def close(self): """Save config on exit.""" self.save() @@ -301,7 +301,7 @@ class ConfigManager: self.loading = False @staticmethod - async def __async_fetch_translations() -> dict: + async def _fetch_translations() -> dict: """Build a list of all translations.""" base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # get base translations @@ -349,7 +349,7 @@ class ConfigBaseItem: def all_keys(self): """Return all possible keys of this Config object.""" - return {key for key in self.conf_mgr.stored_config.get(self.conf_key, {}).keys()} + return self.conf_mgr.stored_config.get(self.conf_key, {}).keys() def __getitem__(self, item_key: str): """Return ConfigSubItem for given key.""" @@ -397,7 +397,7 @@ class SecuritySettings(ConfigBaseItem): def all_keys(self): """Return all possible keys of this Config object.""" - return [CONF_KEY_SECURITY_LOGIN, CONF_KEY_SECURITY_APP_TOKENS] + return DEFAULT_SECURITY_CONFIG_ENTRIES.keys() def add_app_token(self, token_info: dict): """Add token to config.""" @@ -484,19 +484,15 @@ class PlayerSettings(ConfigBaseItem): def all_keys(self): """Return all possible keys of this Config object.""" - all_keys = super().all_keys() - for player_id in self.mass.players.players: - if player_id not in all_keys: - all_keys.add(player_id) - return all_keys + return {player.player_id for player in self.mass.players} def get_config_entries(self, child_key: str) -> List[ConfigEntry]: """Return all config entries for the given child entry.""" entries = [] entries += DEFAULT_PLAYER_CONFIG_ENTRIES - player_state = self.mass.players.get_player_state(child_key) - if player_state: - entries += player_state.player.config_entries + player = self.mass.players.get_player(child_key) + if player: + entries += player.config_entries # append power control config entries power_controls = self.mass.players.get_player_controls( PlayerControlType.POWER @@ -534,8 +530,8 @@ class PlayerSettings(ConfigBaseItem): ) ) # append special group player entries - for parent_id in player_state.group_parents: - parent_player = self.mass.players.get_player_state(parent_id) + for parent_id in player.group_parents: + parent_player = self.mass.players.get_player(parent_id) if parent_player and parent_player.provider_id == "group_player": entries.append( ConfigEntry( @@ -605,7 +601,7 @@ class ConfigSubItem: entry = self.get_entry(key) if entry.entry_type == ConfigEntryType.PASSWORD: # decrypted password is only returned if explicitly asked for this key - decrypted_value = decrypt_string(entry.value) + decrypted_value = _decrypt_string(entry.value) if decrypted_value: return decrypted_value return entry.value @@ -667,7 +663,7 @@ class ConfigSubItem: if entry.store_hashed: value = pbkdf2_sha256.hash(value) if entry.entry_type == ConfigEntryType.PASSWORD: - value = encrypt_string(value) + value = _encrypt_string(value) # write value to stored config stored_conf = self.conf_mgr.stored_config @@ -681,14 +677,12 @@ class ConfigSubItem: # reload provider/plugin if value changed if self.parent_conf_key in PROVIDER_TYPE_MAPPINGS: self.conf_mgr.mass.add_job( - self.conf_mgr.mass.async_reload_provider(self.conf_key) + self.conf_mgr.mass.reload_provider(self.conf_key) ) if self.parent_conf_key == CONF_KEY_PLAYER_SETTINGS: # force update of player if it's config changed self.conf_mgr.mass.add_job( - self.conf_mgr.mass.players.async_trigger_player_update( - self.conf_key - ) + self.conf_mgr.mass.players.trigger_player_update(self.conf_key) ) # signal config changed event self.conf_mgr.mass.signal_event( diff --git a/music_assistant/managers/database.py b/music_assistant/managers/database.py index d50179f1..62639894 100755 --- a/music_assistant/managers/database.py +++ b/music_assistant/managers/database.py @@ -3,7 +3,7 @@ import logging import os from datetime import datetime -from typing import List, Optional, Union +from typing import List, Optional, Set, Union import aiosqlite from music_assistant.helpers.compare import compare_album, compare_track @@ -41,7 +41,7 @@ class DatabaseManager: """Return location of database on disk.""" return self._dbfile - async def async_get_item_by_prov_id( + async def get_item_by_prov_id( self, provider_id: str, prov_item_id: str, @@ -49,96 +49,96 @@ class DatabaseManager: ) -> Optional[MediaItem]: """Get the database item for the given prov_id.""" if media_type == MediaType.Artist: - return await self.async_get_artist_by_prov_id(provider_id, prov_item_id) + return await self.get_artist_by_prov_id(provider_id, prov_item_id) if media_type == MediaType.Album: - return await self.async_get_album_by_prov_id(provider_id, prov_item_id) + return await self.get_album_by_prov_id(provider_id, prov_item_id) if media_type == MediaType.Track: - return await self.async_get_track_by_prov_id(provider_id, prov_item_id) + return await self.get_track_by_prov_id(provider_id, prov_item_id) if media_type == MediaType.Playlist: - return await self.async_get_playlist_by_prov_id(provider_id, prov_item_id) + return await self.get_playlist_by_prov_id(provider_id, prov_item_id) if media_type == MediaType.Radio: - return await self.async_get_radio_by_prov_id(provider_id, prov_item_id) + return await self.get_radio_by_prov_id(provider_id, prov_item_id) return None - async def async_get_track_by_prov_id( + async def get_track_by_prov_id( self, provider_id: str, prov_item_id: str, ) -> Optional[FullTrack]: """Get the database track for the given prov_id.""" if provider_id == "database": - return await self.async_get_track(prov_item_id) + return await self.get_track(prov_item_id) sql_query = f"""WHERE item_id in (SELECT item_id FROM provider_mappings WHERE prov_item_id = '{prov_item_id}' AND provider = '{provider_id}' AND media_type = 'track')""" - for item in await self.async_get_tracks(sql_query): + for item in await self.get_tracks(sql_query): return item return None - async def async_get_album_by_prov_id( + async def get_album_by_prov_id( self, provider_id: str, prov_item_id: str, ) -> Optional[FullAlbum]: """Get the database album for the given prov_id.""" if provider_id == "database": - return await self.async_get_album(prov_item_id) + return await self.get_album(prov_item_id) sql_query = f"""WHERE item_id in (SELECT item_id FROM provider_mappings WHERE prov_item_id = '{prov_item_id}' AND provider = '{provider_id}' AND media_type = 'album')""" - for item in await self.async_get_albums(sql_query): + for item in await self.get_albums(sql_query): return item return None - async def async_get_artist_by_prov_id( + async def get_artist_by_prov_id( self, provider_id: str, prov_item_id: str, ) -> Optional[Artist]: """Get the database artist for the given prov_id.""" if provider_id == "database": - return await self.async_get_artist(prov_item_id) + return await self.get_artist(prov_item_id) sql_query = f"""WHERE item_id in (SELECT item_id FROM provider_mappings WHERE prov_item_id = '{prov_item_id}' AND provider = '{provider_id}' AND media_type = 'artist')""" - for item in await self.async_get_artists(sql_query): + for item in await self.get_artists(sql_query): return item return None - async def async_get_playlist_by_prov_id( + async def get_playlist_by_prov_id( self, provider_id: str, prov_item_id: str ) -> Optional[Playlist]: """Get the database playlist for the given prov_id.""" if provider_id == "database": - return await self.async_get_playlist(prov_item_id) + return await self.get_playlist(prov_item_id) sql_query = f"""WHERE item_id in (SELECT item_id FROM provider_mappings WHERE prov_item_id = '{prov_item_id}' AND provider = '{provider_id}' AND media_type = 'playlist')""" - for item in await self.async_get_playlists(sql_query): + for item in await self.get_playlists(sql_query): return item return None - async def async_get_radio_by_prov_id( + async def get_radio_by_prov_id( self, provider_id: str, prov_item_id: str, ) -> Optional[Radio]: """Get the database radio for the given prov_id.""" if provider_id == "database": - return await self.async_get_radio(prov_item_id) + return await self.get_radio(prov_item_id) sql_query = f"""WHERE item_id in (SELECT item_id FROM provider_mappings WHERE prov_item_id = '{prov_item_id}' AND provider = '{provider_id}' AND media_type = 'radio')""" - for item in await self.async_get_radios(sql_query): + for item in await self.get_radios(sql_query): return item return None - async def async_search( + async def search( self, searchquery: str, media_types: List[MediaType] ) -> SearchResult: """Search library for the given searchphrase.""" @@ -146,51 +146,49 @@ class DatabaseManager: searchquery = "%" + searchquery + "%" if media_types is None or MediaType.Artist in media_types: sql_query = ' WHERE name LIKE "%s"' % searchquery - result.artists = await self.async_get_artists(sql_query) + result.artists = await self.get_artists(sql_query) if media_types is None or MediaType.Album in media_types: sql_query = ' WHERE name LIKE "%s"' % searchquery - result.albums = await self.async_get_albums(sql_query) + result.albums = await self.get_albums(sql_query) if media_types is None or MediaType.Track in media_types: sql_query = ' WHERE name LIKE "%s"' % searchquery - result.tracks = await self.async_get_tracks(sql_query) + result.tracks = await self.get_tracks(sql_query) if media_types is None or MediaType.Playlist in media_types: sql_query = ' WHERE name LIKE "%s"' % searchquery - result.playlists = await self.async_get_playlists(sql_query) + result.playlists = await self.get_playlists(sql_query) if media_types is None or MediaType.Radio in media_types: sql_query = ' WHERE name LIKE "%s"' % searchquery - result.radios = await self.async_get_radios(sql_query) + result.radios = await self.get_radios(sql_query) return result - async def async_get_library_artists(self, orderby: str = "name") -> List[Artist]: + async def get_library_artists(self, orderby: str = "name") -> List[Artist]: """Get all library artists.""" sql_query = "WHERE in_library = 1" - return await self.async_get_artists(sql_query, orderby=orderby) + return await self.get_artists(sql_query, orderby=orderby) - async def async_get_library_albums(self, orderby: str = "name") -> List[Album]: + async def get_library_albums(self, orderby: str = "name") -> List[Album]: """Get all library albums.""" sql_query = "WHERE in_library = 1" - return await self.async_get_albums(sql_query, orderby=orderby) + return await self.get_albums(sql_query, orderby=orderby) - async def async_get_library_tracks(self, orderby: str = "name") -> List[Track]: + async def get_library_tracks(self, orderby: str = "name") -> List[Track]: """Get all library tracks.""" sql_query = "WHERE in_library = 1" - return await self.async_get_tracks(sql_query, orderby=orderby) + return await self.get_tracks(sql_query, orderby=orderby) - async def async_get_library_playlists( - self, orderby: str = "name" - ) -> List[Playlist]: + async def get_library_playlists(self, orderby: str = "name") -> List[Playlist]: """Fetch all playlist records from table.""" sql_query = "WHERE in_library = 1" - return await self.async_get_playlists(sql_query, orderby=orderby) + return await self.get_playlists(sql_query, orderby=orderby) - async def async_get_library_radios( + async def get_library_radios( self, provider_id: str = None, orderby: str = "name" ) -> List[Radio]: """Fetch all radio records from table.""" sql_query = "WHERE in_library = 1" - return await self.async_get_radios(sql_query, orderby=orderby) + return await self.get_radios(sql_query, orderby=orderby) - async def async_get_playlists( + async def get_playlists( self, filter_query: str = None, orderby: str = "name", @@ -207,14 +205,14 @@ class DatabaseManager: for db_row in await db_conn.execute_fetchall(sql_query, ()) ] - async def async_get_playlist(self, item_id: int) -> Playlist: + async def get_playlist(self, item_id: int) -> Playlist: """Get playlist record by id.""" item_id = try_parse_int(item_id) - for item in await self.async_get_playlists(f"WHERE item_id = {item_id}"): + for item in await self.get_playlists(f"WHERE item_id = {item_id}"): return item return None - async def async_get_radios( + async def get_radios( self, filter_query: str = None, orderby: str = "name", @@ -232,14 +230,14 @@ class DatabaseManager: for db_row in await db_conn.execute_fetchall(sql_query, ()) ] - async def async_get_radio(self, item_id: int) -> Playlist: + async def get_radio(self, item_id: int) -> Playlist: """Get radio record by id.""" item_id = try_parse_int(item_id) - for item in await self.async_get_radios(f"WHERE item_id = {item_id}"): + for item in await self.get_radios(f"WHERE item_id = {item_id}"): return item return None - async def async_add_playlist(self, playlist: Playlist): + async def add_playlist(self, playlist: Playlist): """Add a new playlist record to the database.""" assert playlist.name async with aiosqlite.connect(self._dbfile, timeout=120) as db_conn: @@ -252,7 +250,7 @@ class DatabaseManager: if cur_item: # update existing - return await self.async_update_playlist(cur_item[0], playlist) + return await self.update_playlist(cur_item[0], playlist) # insert playlist sql_query = """INSERT INTO playlists (name, sort_name, owner, is_editable, checksum, metadata, provider_ids) @@ -275,15 +273,15 @@ class DatabaseManager: (last_row_id,), db_conn, ) - await self.__async_add_prov_ids( + await self._add_prov_ids( new_item[0], MediaType.Playlist, playlist.provider_ids, db_conn=db_conn ) await db_conn.commit() LOGGER.debug("added playlist %s to database", playlist.name) # return created object - return await self.async_get_playlist(new_item[0]) + return await self.get_playlist(new_item[0]) - async def async_update_playlist(self, item_id: int, playlist: Playlist): + async def update_playlist(self, item_id: int, playlist: Playlist): """Update a playlist record in the database.""" async with aiosqlite.connect(self._dbfile, timeout=120) as db_conn: db_conn.row_factory = aiosqlite.Row @@ -316,15 +314,15 @@ class DatabaseManager: item_id, ), ) - await self.__async_add_prov_ids( + await self._add_prov_ids( item_id, MediaType.Playlist, playlist.provider_ids, db_conn=db_conn ) LOGGER.debug("updated playlist %s in database: %s", playlist.name, item_id) await db_conn.commit() # return updated object - return await self.async_get_playlist(item_id) + return await self.get_playlist(item_id) - async def async_add_radio(self, radio: Radio): + async def add_radio(self, radio: Radio): """Add a new radio record to the database.""" assert radio.name async with aiosqlite.connect(self._dbfile, timeout=120) as db_conn: @@ -334,7 +332,7 @@ class DatabaseManager: ) if cur_item: # update existing - return await self.async_update_radio(cur_item[0], radio) + return await self.update_radio(cur_item[0], radio) # insert radio sql_query = """INSERT INTO radios (name, sort_name, metadata, provider_ids) VALUES(?,?,?,?);""" @@ -353,15 +351,15 @@ class DatabaseManager: (last_row_id,), db_conn, ) - await self.__async_add_prov_ids( + await self._add_prov_ids( new_item[0], MediaType.Radio, radio.provider_ids, db_conn=db_conn ) await db_conn.commit() LOGGER.debug("added radio %s to database", radio.name) # return created object - return await self.async_get_radio(new_item[0]) + return await self.get_radio(new_item[0]) - async def async_update_radio(self, item_id: int, radio: Radio): + async def update_radio(self, item_id: int, radio: Radio): """Update a radio record in the database.""" async with aiosqlite.connect(self._dbfile, timeout=120) as db_conn: db_conn.row_factory = aiosqlite.Row @@ -388,17 +386,15 @@ class DatabaseManager: item_id, ), ) - await self.__async_add_prov_ids( + await self._add_prov_ids( item_id, MediaType.Radio, radio.provider_ids, db_conn=db_conn ) LOGGER.debug("updated radio %s in database: %s", radio.name, item_id) await db_conn.commit() # return updated object - return await self.async_get_radio(item_id) + return await self.get_radio(item_id) - async def async_add_to_library( - self, item_id: int, media_type: MediaType, provider: str - ): + async def 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 aiosqlite.connect(self._dbfile, timeout=120) as db_conn: item_id = try_parse_int(item_id) @@ -407,7 +403,7 @@ class DatabaseManager: await db_conn.execute(sql_query, (item_id,)) await db_conn.commit() - async def async_remove_from_library( + async def remove_from_library( self, item_id: int, media_type: MediaType, provider: str ): """Remove item from the library.""" @@ -418,7 +414,7 @@ class DatabaseManager: await db_conn.execute(sql_query, (item_id,)) await db_conn.commit() - async def async_get_artists( + async def get_artists( self, filter_query: str = None, orderby: str = "name", @@ -436,14 +432,14 @@ class DatabaseManager: for db_row in await db_conn.execute_fetchall(sql_query, ()) ] - async def async_get_artist(self, item_id: int) -> Artist: + async def get_artist(self, item_id: int) -> Artist: """Get artist record by id.""" item_id = try_parse_int(item_id) - for item in await self.async_get_artists("WHERE item_id = %d" % item_id): + for item in await self.get_artists("WHERE item_id = %d" % item_id): return item return None - async def async_add_artist(self, artist: Artist): + async def add_artist(self, artist: Artist): """Add a new artist record to the database.""" async with aiosqlite.connect(self._dbfile, timeout=120) as db_conn: db_conn.row_factory = aiosqlite.Row @@ -454,7 +450,7 @@ class DatabaseManager: ) if cur_item: # update existing - return await self.async_update_artist(cur_item[0], artist) + return await self.update_artist(cur_item[0], artist) # insert artist sql_query = """INSERT INTO artists (name, sort_name, musicbrainz_id, metadata, provider_ids) @@ -475,15 +471,15 @@ class DatabaseManager: (last_row_id,), db_conn, ) - await self.__async_add_prov_ids( + await self._add_prov_ids( new_item[0], MediaType.Artist, artist.provider_ids, db_conn=db_conn ) await db_conn.commit() LOGGER.debug("added artist %s to database", artist.name) # return created object - return await self.async_get_artist(new_item[0]) + return await self.get_artist(new_item[0]) - async def async_update_artist(self, item_id: int, artist: Artist): + async def update_artist(self, item_id: int, artist: Artist): """Update a artist record in the database.""" async with aiosqlite.connect(self._dbfile, timeout=120) as db_conn: db_conn.row_factory = aiosqlite.Row @@ -507,15 +503,15 @@ class DatabaseManager: item_id, ), ) - await self.__async_add_prov_ids( + await self._add_prov_ids( item_id, MediaType.Artist, artist.provider_ids, db_conn=db_conn ) LOGGER.debug("updated artist %s in database: %s", artist.name, item_id) await db_conn.commit() # return updated object - return await self.async_get_artist(item_id) + return await self.get_artist(item_id) - async def async_get_albums( + async def get_albums( self, filter_query: str = None, orderby: str = "name", @@ -533,13 +529,13 @@ class DatabaseManager: for db_row in await db_conn.execute_fetchall(sql_query, ()) ] - async def async_get_album(self, item_id: int) -> FullAlbum: + async def get_album(self, item_id: int) -> FullAlbum: """Get album record by id.""" item_id = try_parse_int(item_id) # get from db - for item in await self.async_get_albums("WHERE item_id = %d" % item_id): + for item in await self.get_albums("WHERE item_id = %d" % item_id): item.artist = ( - await self.async_get_artist_by_prov_id( + await self.get_artist_by_prov_id( item.artist.provider, item.artist.item_id ) or item.artist @@ -547,7 +543,7 @@ class DatabaseManager: return item return None - async def async_get_albums_from_provider_ids( + async def get_albums_from_provider_ids( self, provider_id: Union[str, List[str]], prov_item_ids: List[str] ) -> dict: """Get album records for the given prov_ids.""" @@ -559,16 +555,16 @@ class DatabaseManager: WHERE provider in ({prov_id_str}) AND media_type = 'album' AND prov_item_id in ({prov_item_id_str}) )""" - return await self.async_get_albums(sql_query) + return await self.get_albums(sql_query) - async def async_add_album(self, album: Album): + async def add_album(self, album: Album): """Add a new album record to the database.""" async with aiosqlite.connect(self._dbfile, timeout=120) as db_conn: db_conn.row_factory = aiosqlite.Row cur_item = None # always try to grab existing item by external_id if album.upc: - for item in await self.async_get_albums(f"WHERE upc='{album.upc}'"): + for item in await self.get_albums(f"WHERE upc='{album.upc}'"): cur_item = item # fallback to matching if not cur_item: @@ -576,18 +572,18 @@ class DatabaseManager: for db_row in await db_conn.execute_fetchall( sql_query, (album.sort_name,) ): - item = await self.async_get_album(db_row["item_id"]) + item = await self.get_album(db_row["item_id"]) if compare_album(item, album): cur_item = item break if cur_item: # update existing - return await self.async_update_album(cur_item.item_id, album) + return await self.update_album(cur_item.item_id, album) # insert album assert album.artist album_artist = ItemMapping.from_item( - await self.async_get_artist_by_prov_id( + await self.get_artist_by_prov_id( album.artist.provider, album.artist.item_id ) or album.artist @@ -615,24 +611,24 @@ class DatabaseManager: (last_row_id,), db_conn, ) - await self.__async_add_prov_ids( + await self._add_prov_ids( new_item[0], MediaType.Album, album.provider_ids, db_conn=db_conn ) await db_conn.commit() LOGGER.debug("added album %s to database", album.name) # return created object - return await self.async_get_album(new_item[0]) + return await self.get_album(new_item[0]) - async def async_update_album(self, item_id: int, album: Album): + async def update_album(self, item_id: int, album: Album): """Update a album record in the database.""" async with aiosqlite.connect(self._dbfile, timeout=120) as db_conn: db_conn.row_factory = aiosqlite.Row - cur_item = await self.async_get_album(item_id) + cur_item = await self.get_album(item_id) album_artist = ItemMapping.from_item( - await self.async_get_artist_by_prov_id( + await self.get_artist_by_prov_id( cur_item.artist.provider, cur_item.artist.item_id ) - or await self.async_get_artist_by_prov_id( + or await self.get_artist_by_prov_id( album.artist.provider, album.artist.item_id ) or cur_item.artist @@ -661,15 +657,15 @@ class DatabaseManager: item_id, ), ) - await self.__async_add_prov_ids( + await self._add_prov_ids( item_id, MediaType.Album, album.provider_ids, db_conn=db_conn ) LOGGER.debug("updated album %s in database: %s", album.name, item_id) await db_conn.commit() # return updated object - return await self.async_get_album(item_id) + return await self.get_album(item_id) - async def async_get_tracks( + async def get_tracks( self, filter_query: str = None, orderby: str = "name", @@ -687,7 +683,7 @@ class DatabaseManager: for db_row in await db_conn.execute_fetchall(sql_query, ()) ] - async def async_get_tracks_from_provider_ids( + async def get_tracks_from_provider_ids( self, provider_id: Union[str, List[str]], prov_item_ids: List[str] ) -> dict: """Get track records for the given prov_ids.""" @@ -699,35 +695,33 @@ class DatabaseManager: WHERE provider in ({prov_id_str}) AND media_type = 'track' AND prov_item_id in ({prov_item_id_str}) )""" - return await self.async_get_tracks(sql_query) + return await self.get_tracks(sql_query) - async def async_get_track(self, item_id: int) -> FullTrack: + async def get_track(self, item_id: int) -> FullTrack: """Get full track record by id.""" item_id = try_parse_int(item_id) - for item in await self.async_get_tracks("WHERE item_id = %d" % item_id): + for item in await self.get_tracks("WHERE item_id = %d" % item_id): # include full album info - item.albums = list( + item.albums = set( filter( None, [ - await self.async_get_album_by_prov_id( - album.provider, album.item_id - ) + await self.get_album_by_prov_id(album.provider, album.item_id) for album in item.albums ], ) ) - item.album = item.albums[0] + item.album = next(iter(item.albums)) # include full artist info - item.artists = [ - await self.async_get_artist_by_prov_id(artist.provider, artist.item_id) + item.artists = { + await self.get_artist_by_prov_id(artist.provider, artist.item_id) or artist for artist in item.artists - ] + } return item return None - async def async_add_track(self, track: Track): + async def add_track(self, track: Track): """Add a new track record to the database.""" assert track.album, "Track is missing album" assert track.artists, "Track is missing artist(s)" @@ -736,7 +730,7 @@ class DatabaseManager: cur_item = None # always try to grab existing item by matching if track.isrc: - for item in await self.async_get_tracks(f"WHERE isrc='{track.isrc}'"): + for item in await self.get_tracks(f"WHERE isrc='{track.isrc}'"): cur_item = item # fallback to matching if not cur_item: @@ -744,20 +738,20 @@ class DatabaseManager: for db_row in await db_conn.execute_fetchall( sql_query, (track.sort_name,) ): - item = await self.async_get_track(db_row["item_id"]) + item = await self.get_track(db_row["item_id"]) if compare_track(item, track): cur_item = item break if cur_item: # update existing - return await self.async_update_track(cur_item.item_id, track) + return await self.update_track(cur_item.item_id, track) # Item does not yet exist: Insert track sql_query = """INSERT INTO tracks (name, sort_name, albums, artists, duration, version, isrc, metadata, provider_ids) VALUES(?,?,?,?,?,?,?,?,?);""" # we store a mapping to artists and albums on the track for easier access/listings - track_artists = await self.__async_get_track_artists(track) - track_albums = await self.__async_get_track_albums(track) + track_artists = await self._get_track_artists(track) + track_albums = await self._get_track_albums(track) async with db_conn.execute( sql_query, @@ -779,25 +773,23 @@ class DatabaseManager: (last_row_id,), db_conn, ) - await self.__async_add_prov_ids( + await self._add_prov_ids( new_item[0], MediaType.Track, track.provider_ids, db_conn=db_conn ) await db_conn.commit() LOGGER.debug("added track %s to database", track.name) # return created object - return await self.async_get_track(new_item[0]) + return await self.get_track(new_item[0]) - async def async_update_track(self, item_id: int, track: Track): + async def update_track(self, item_id: int, track: Track): """Update a track record in the database.""" async with aiosqlite.connect(self._dbfile, timeout=120) as db_conn: db_conn.row_factory = aiosqlite.Row - cur_item = await self.async_get_track(item_id) + cur_item = await self.get_track(item_id) # we store a mapping to artists and albums on the track for easier access/listings - track_artists = await self.__async_get_track_artists( - track, cur_item.artists - ) - track_albums = await self.__async_get_track_albums(track, cur_item.albums) + track_artists = await self._get_track_artists(track, cur_item.artists) + track_albums = await self._get_track_albums(track, cur_item.albums) # merge metadata and provider id's metadata = merge_dict(cur_item.metadata, track.metadata) provider_ids = merge_list(cur_item.provider_ids, track.provider_ids) @@ -819,17 +811,15 @@ class DatabaseManager: item_id, ), ) - await self.__async_add_prov_ids( + await self._add_prov_ids( item_id, MediaType.Track, track.provider_ids, db_conn=db_conn ) LOGGER.debug("updated track %s in database: %s", track.name, item_id) await db_conn.commit() # return updated object - return await self.async_get_track(item_id) + return await self.get_track(item_id) - async def async_set_track_loudness( - self, item_id: str, provider: str, loudness: int - ): + async def set_track_loudness(self, item_id: str, provider: str, loudness: int): """Set integrated loudness for a track in db.""" async with aiosqlite.connect(self._dbfile, timeout=120) as db_conn: sql_query = """INSERT or REPLACE INTO track_loudness @@ -837,7 +827,7 @@ class DatabaseManager: await db_conn.execute(sql_query, (item_id, provider, loudness)) await db_conn.commit() - async def async_get_track_loudness(self, provider_item_id, provider): + async def get_track_loudness(self, provider_item_id, provider): """Get integrated loudness for a track in db.""" async with aiosqlite.connect(self._dbfile, timeout=120) as db_conn: sql_query = """SELECT loudness FROM track_loudness WHERE @@ -850,7 +840,7 @@ class DatabaseManager: return result[0] return None - async def async_mark_item_played(self, item_id: str, provider: str): + async def mark_item_played(self, item_id: str, provider: str): """Mark item as played in playlog.""" timestamp = datetime.utcnow().timestamp() async with aiosqlite.connect(self._dbfile, timeout=120) as db_conn: @@ -859,7 +849,7 @@ class DatabaseManager: await db_conn.execute(sql_query, (item_id, provider, timestamp)) await db_conn.commit() - async def async_get_thumbnail_id(self, url, size): + async def get_thumbnail_id(self, url, size): """Get/create id for thumbnail.""" async with aiosqlite.connect(self._dbfile, timeout=120) as db_conn: sql_query = """SELECT id FROM thumbs WHERE @@ -882,11 +872,11 @@ class DatabaseManager: await db_conn.commit() return new_item[0] - async def __async_add_prov_ids( + async def _add_prov_ids( self, item_id: int, media_type: MediaType, - provider_ids: List[MediaItemProviderId], + provider_ids: Set[MediaItemProviderId], db_conn: aiosqlite.Connection, ): """Add provider ids for media item to database.""" @@ -915,44 +905,43 @@ class DatabaseManager: return item return None - async def __async_get_track_albums( - self, track: Track, cur_albums: Optional[List[ItemMapping]] = None - ) -> List[ItemMapping]: + async def _get_track_albums( + self, track: Track, cur_albums: Optional[Set[ItemMapping]] = None + ) -> Set[ItemMapping]: """Extract all (unique) albums of track as ItemMapping.""" if not track.albums: - track.albums.append(track.album) + track.albums.add(track.album) if cur_albums is None: - cur_albums = [] - track_albums = [] - for album in track.albums + cur_albums: - cur_ids = [x.item_id for x in track_albums] + cur_albums = set() + cur_albums.update(track.albums) + track_albums = set() + for album in cur_albums: + cur_ids = {x.item_id for x in track_albums} if isinstance(album, ItemMapping): - track_album = await self.async_get_album_by_prov_id( - album.provider_id, album - ) + track_album = await self.get_album_by_prov_id(album.provider_id, album) else: - track_album = await self.async_add_album(album) + track_album = await self.add_album(album) if track_album.item_id not in cur_ids: - track_albums.append(ItemMapping.from_item(album)) + track_albums.add(ItemMapping.from_item(album)) return track_albums - async def __async_get_track_artists( - self, track: Track, cur_artists: Optional[List[ItemMapping]] = None - ) -> List[ItemMapping]: + async def _get_track_artists( + self, track: Track, cur_artists: Optional[Set[ItemMapping]] = None + ) -> Set[ItemMapping]: """Extract all (unique) artists of track as ItemMapping.""" if cur_artists is None: - cur_artists = [] - track_artists = [] - for item in cur_artists + track.artists: - cur_names = [x.name for x in track_artists] - cur_ids = [x.item_id for x in track_artists] + cur_artists = set() + cur_artists.update(track.artists) + track_artists = set() + for item in cur_artists: + cur_names = {x.name for x in track_artists} + cur_ids = {x.item_id for x in track_artists} track_artist = ( - await self.async_get_artist_by_prov_id(item.provider, item.item_id) - or item + await self.get_artist_by_prov_id(item.provider, item.item_id) or item ) if ( track_artist.name not in cur_names and track_artist.item_id not in cur_ids ): - track_artists.append(ItemMapping.from_item(track_artist)) + track_artists.add(ItemMapping.from_item(track_artist)) return track_artists diff --git a/music_assistant/managers/library.py b/music_assistant/managers/library.py index 505ed0cd..f1b0c189 100755 --- a/music_assistant/managers/library.py +++ b/music_assistant/managers/library.py @@ -27,7 +27,7 @@ def sync_task(desc): def wrapper(func): @functools.wraps(func) - async def async_wrapped(*args): + async def wrapped(*args): method_class = args[0] prov_id = args[1] # check if this sync task is not already running @@ -39,7 +39,7 @@ def sync_task(desc): 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.running_sync_jobs.add(sync_job) method_class.mass.signal_event( EVENT_MUSIC_SYNC_STATUS, method_class.running_sync_jobs ) @@ -50,7 +50,7 @@ def sync_task(desc): EVENT_MUSIC_SYNC_STATUS, method_class.running_sync_jobs ) - return async_wrapped + return wrapped return wrapper @@ -60,15 +60,15 @@ class LibraryManager: def __init__(self, mass): """Initialize class.""" - self.running_sync_jobs = [] + self.running_sync_jobs = set() self.mass = mass self.cache = mass.cache - self.mass.add_event_listener(self.mass_event, [EVENT_PROVIDER_REGISTERED]) + self.mass.add_event_listener(self.mass_event, EVENT_PROVIDER_REGISTERED) - async def async_setup(self): + async def setup(self): """Async initialize of module.""" # schedule sync task - self.mass.add_job(self.__async_music_providers_sync()) + self.mass.add_job(self._music_providers_sync()) @callback def mass_event(self, msg: str, msg_details: Any): @@ -77,53 +77,51 @@ class LibraryManager: # schedule a sync task when a new provider registers provider = self.mass.get_provider(msg_details) if provider.type == ProviderType.MUSIC_PROVIDER: - self.mass.add_job(self.async_music_provider_sync(msg_details)) + self.mass.add_job(self.music_provider_sync(msg_details)) ################ GET MediaItems that are added in the library ################ @api_route("library/artists") - async def async_get_library_artists(self, orderby: str = "name") -> List[Artist]: + async def get_library_artists(self, orderby: str = "name") -> List[Artist]: """Return all library artists, optionally filtered by provider.""" - return await self.mass.database.async_get_library_artists(orderby=orderby) + return await self.mass.database.get_library_artists(orderby=orderby) @api_route("library/albums") - async def async_get_library_albums(self, orderby: str = "name") -> List[Album]: + async def get_library_albums(self, orderby: str = "name") -> List[Album]: """Return all library albums, optionally filtered by provider.""" - return await self.mass.database.async_get_library_albums(orderby=orderby) + return await self.mass.database.get_library_albums(orderby=orderby) @api_route("library/tracks") - async def async_get_library_tracks(self, orderby: str = "name") -> List[Track]: + async def get_library_tracks(self, orderby: str = "name") -> List[Track]: """Return all library tracks, optionally filtered by provider.""" - return await self.mass.database.async_get_library_tracks(orderby=orderby) + return await self.mass.database.get_library_tracks(orderby=orderby) @api_route("library/playlists") - async def async_get_library_playlists( - self, orderby: str = "name" - ) -> List[Playlist]: + async def get_library_playlists(self, orderby: str = "name") -> List[Playlist]: """Return all library playlists, optionally filtered by provider.""" - return await self.mass.database.async_get_library_playlists(orderby=orderby) + return await self.mass.database.get_library_playlists(orderby=orderby) @api_route("library/radios") - async def async_get_library_radios(self, orderby: str = "name") -> List[Playlist]: + async def get_library_radios(self, orderby: str = "name") -> List[Playlist]: """Return all library radios, optionally filtered by provider.""" - return await self.mass.database.async_get_library_radios(orderby=orderby) + return await self.mass.database.get_library_radios(orderby=orderby) - async def async_get_library_playlist_by_name(self, name: str) -> Playlist: + async def get_library_playlist_by_name(self, name: str) -> Playlist: """Get in-library playlist by name.""" - for playlist in await self.mass.music.async_get_library_playlists(): + for playlist in await self.mass.music.get_library_playlists(): if playlist.name == name: return playlist return None - async def async_get_radio_by_name(self, name: str) -> Radio: + async def get_radio_by_name(self, name: str) -> Radio: """Get in-library radio by name.""" - for radio in await self.mass.music.async_get_library_radios(): + for radio in await self.mass.music.get_library_radios(): if radio.name == name: return radio return None @api_route("library/add") - async def async_library_add(self, items: List[MediaItem]): + async def library_add(self, items: List[MediaItem]): """Add media item(s) to the library.""" result = False for media_item in items: @@ -131,18 +129,18 @@ class LibraryManager: for prov in media_item.provider_ids: provider = self.mass.get_provider(prov.provider) if provider: - result = await provider.async_library_add( + result = await provider.library_add( prov.item_id, media_item.media_type ) # mark as library item in internal db if media_item.provider == "database": - await self.mass.database.async_add_to_library( + await self.mass.database.add_to_library( media_item.item_id, media_item.media_type, media_item.provider ) return result @api_route("library/remove") - async def async_library_remove(self, items: List[MediaItem]): + async def library_remove(self, items: List[MediaItem]): """Remove media item(s) from the library.""" result = False for media_item in items: @@ -150,33 +148,33 @@ class LibraryManager: for prov in media_item.provider_ids: provider = self.mass.get_provider(prov.provider) if provider: - result = await provider.async_library_remove( + result = await provider.library_remove( prov.item_id, media_item.media_type ) # mark as library item in internal db if media_item.provider == "database": - await self.mass.database.async_remove_from_library( + await self.mass.database.remove_from_library( media_item.item_id, media_item.media_type, media_item.provider ) return result @api_route("library/playlists/:db_playlist_id/tracks/add") - async def async_add_playlist_tracks(self, db_playlist_id: int, tracks: List[Track]): + async def add_playlist_tracks(self, db_playlist_id: int, tracks: List[Track]): """Add tracks to playlist - make sure we dont add duplicates.""" # we can only edit playlists that are in the database (marked as editable) - playlist = await self.mass.music.async_get_playlist(db_playlist_id, "database") + playlist = await self.mass.music.get_playlist(db_playlist_id, "database") if not playlist or not playlist.is_editable: return False # playlist can only have one provider (for now) - playlist_prov = playlist.provider_ids[0] + playlist_prov = next(iter(playlist.provider_ids)) # grab all existing track ids in the playlist so we can check for duplicates - cur_playlist_track_ids = [] - for item in await self.mass.music.async_get_playlist_tracks( + cur_playlist_track_ids = set() + for item in await self.mass.music.get_playlist_tracks( playlist_prov.item_id, playlist_prov.provider ): - cur_playlist_track_ids.append(item.item_id) - cur_playlist_track_ids += [i.item_id for i in item.provider_ids] - track_ids_to_add = [] + cur_playlist_track_ids.add(item.item_id) + cur_playlist_track_ids.update({i.item_id for i in item.provider_ids}) + track_ids_to_add = set() for track in tracks: # check for duplicates already_exists = track.item_id in cur_playlist_track_ids @@ -193,58 +191,58 @@ class LibraryManager: 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) + track_ids_to_add.add(track_version.item_id) break if playlist_prov.provider == "file": # the file provider can handle uri's from all providers so simply add the uri uri = f"{track_version.provider}://{track_version.item_id}" - track_ids_to_add.append(uri) + track_ids_to_add.add(uri) break # actually add the tracks to the playlist on the provider if track_ids_to_add: # invalidate cache playlist.checksum = str(time.time()) - await self.mass.database.async_update_playlist(playlist.item_id, playlist) + await self.mass.database.update_playlist(playlist.item_id, playlist) # return result of the action on the provider provider = self.mass.get_provider(playlist_prov.provider) - return await provider.async_add_playlist_tracks( + return await provider.add_playlist_tracks( playlist_prov.item_id, track_ids_to_add ) return False @api_route("library/playlists/:db_playlist_id/tracks/remove") - async def async_remove_playlist_tracks(self, db_playlist_id, tracks: List[Track]): + async def remove_playlist_tracks(self, db_playlist_id, tracks: List[Track]): """Remove tracks from playlist.""" # we can only edit playlists that are in the database (marked as editable) - playlist = await self.mass.music.async_get_playlist(db_playlist_id, "database") + playlist = await self.mass.music.get_playlist(db_playlist_id, "database") if not playlist or not playlist.is_editable: return False # playlist can only have one provider (for now) - prov_playlist = playlist.provider_ids[0] - track_ids_to_remove = [] + prov_playlist = next(iter(playlist.provider_ids)) + track_ids_to_remove = set() for track in tracks: # a track can contain multiple versions on the same provider, remove all for track_provider in track.provider_ids: if track_provider.provider == prov_playlist.provider: - track_ids_to_remove.append(track_provider.item_id) + track_ids_to_remove.add(track_provider.item_id) # actually remove the tracks from the playlist on the provider if track_ids_to_remove: # invalidate cache playlist.checksum = str(time.time()) - await self.mass.database.async_update_playlist(playlist.item_id, playlist) + await self.mass.database.update_playlist(playlist.item_id, playlist) provider = self.mass.get_provider(prov_playlist.provider) - return await provider.async_remove_playlist_tracks( + return await provider.remove_playlist_tracks( prov_playlist.item_id, track_ids_to_remove ) @run_periodic(3600 * 3) - async def __async_music_providers_sync(self): + async def _music_providers_sync(self): """Periodic sync of all music providers.""" await asyncio.sleep(10) for prov in self.mass.get_providers(ProviderType.MUSIC_PROVIDER): - await self.async_music_provider_sync(prov.id) + await self.music_provider_sync(prov.id) - async def async_music_provider_sync(self, prov_id: str): + async def music_provider_sync(self, prov_id: str): """ Sync a music provider. @@ -254,155 +252,155 @@ class LibraryManager: if not provider: return if MediaType.Album in provider.supported_mediatypes: - await self.async_library_albums_sync(prov_id) + await self.library_albums_sync(prov_id) if MediaType.Track in provider.supported_mediatypes: - await self.async_library_tracks_sync(prov_id) + await self.library_tracks_sync(prov_id) if MediaType.Artist in provider.supported_mediatypes: - await self.async_library_artists_sync(prov_id) + await self.library_artists_sync(prov_id) if MediaType.Playlist in provider.supported_mediatypes: - await self.async_library_playlists_sync(prov_id) + await self.library_playlists_sync(prov_id) if MediaType.Radio in provider.supported_mediatypes: - await self.async_library_radios_sync(prov_id) + await self.library_radios_sync(prov_id) @sync_task("artists") - async def async_library_artists_sync(self, provider_id: str): + async def library_artists_sync(self, provider_id: str): """Sync library artists for given provider.""" music_provider = self.mass.get_provider(provider_id) cache_key = f"library_artists_{provider_id}" - prev_db_ids = await self.mass.cache.async_get(cache_key, default=[]) - cur_db_ids = [] - for item in await music_provider.async_get_library_artists(): - db_item = await self.mass.music.async_get_artist( + prev_db_ids = await self.mass.cache.get(cache_key, default=[]) + cur_db_ids = set() + for item in await music_provider.get_library_artists(): + db_item = await self.mass.music.get_artist( item.item_id, provider_id, lazy=False ) - cur_db_ids.append(db_item.item_id) + cur_db_ids.add(db_item.item_id) if not db_item.in_library: - await self.mass.database.async_add_to_library( + await self.mass.database.add_to_library( db_item.item_id, MediaType.Artist, provider_id ) # process deletions for db_id in prev_db_ids: if db_id not in cur_db_ids: - await self.mass.database.async_remove_from_library( + await self.mass.database.remove_from_library( db_id, MediaType.Artist, provider_id ) # store ids in cache for next sync - await self.mass.cache.async_set(cache_key, cur_db_ids) + await self.mass.cache.set(cache_key, cur_db_ids) @sync_task("albums") - async def async_library_albums_sync(self, provider_id: str): + async def library_albums_sync(self, provider_id: str): """Sync library albums for given provider.""" music_provider = self.mass.get_provider(provider_id) cache_key = f"library_albums_{provider_id}" - prev_db_ids = await self.mass.cache.async_get(cache_key, default=[]) - cur_db_ids = [] - for item in await music_provider.async_get_library_albums(): - db_album = await self.mass.music.async_get_album( + prev_db_ids = await self.mass.cache.get(cache_key, default=[]) + cur_db_ids = set() + for item in await music_provider.get_library_albums(): + db_album = await self.mass.music.get_album( item.item_id, provider_id, lazy=False ) if not db_album.available and not item.available: # album availability changed, sort this out with auto matching magic - db_album = await self.mass.music.async_match_album(db_album) - cur_db_ids.append(db_album.item_id) + db_album = await self.mass.music.match_album(db_album) + cur_db_ids.add(db_album.item_id) if not db_album.in_library: - await self.mass.database.async_add_to_library( + await self.mass.database.add_to_library( db_album.item_id, MediaType.Album, provider_id ) # precache album tracks - await self.mass.music.async_get_album_tracks(item.item_id, provider_id) + await self.mass.music.get_album_tracks(item.item_id, provider_id) # process album deletions for db_id in prev_db_ids: if db_id not in cur_db_ids: - await self.mass.database.async_remove_from_library( + await self.mass.database.remove_from_library( db_id, MediaType.Album, provider_id ) # store ids in cache for next sync - await self.mass.cache.async_set(cache_key, cur_db_ids) + await self.mass.cache.set(cache_key, cur_db_ids) @sync_task("tracks") - async def async_library_tracks_sync(self, provider_id: str): + async def library_tracks_sync(self, provider_id: str): """Sync library tracks for given provider.""" music_provider = self.mass.get_provider(provider_id) cache_key = f"library_tracks_{provider_id}" - prev_db_ids = await self.mass.cache.async_get(cache_key, default=[]) - cur_db_ids = [] - for item in await music_provider.async_get_library_tracks(): - db_item = await self.mass.music.async_get_track( + prev_db_ids = await self.mass.cache.get(cache_key, default=[]) + cur_db_ids = set() + for item in await music_provider.get_library_tracks(): + db_item = await self.mass.music.get_track( item.item_id, provider_id, track_details=item, lazy=False ) if not db_item.available and not item.available: # track availability changed, sort this out with auto matching magic - db_item = await self.mass.music.async_add_track(item) - cur_db_ids.append(db_item.item_id) + db_item = await self.mass.music.add_track(item) + cur_db_ids.add(db_item.item_id) if not db_item.in_library: - await self.mass.database.async_add_to_library( + await self.mass.database.add_to_library( db_item.item_id, MediaType.Track, provider_id ) # process deletions for db_id in prev_db_ids: if db_id not in cur_db_ids: - await self.mass.database.async_remove_from_library( + await self.mass.database.remove_from_library( db_id, MediaType.Track, provider_id ) # store ids in cache for next sync - await self.mass.cache.async_set(cache_key, cur_db_ids) + await self.mass.cache.set(cache_key, cur_db_ids) @sync_task("playlists") - async def async_library_playlists_sync(self, provider_id: str): + async def library_playlists_sync(self, provider_id: str): """Sync library playlists for given provider.""" music_provider = self.mass.get_provider(provider_id) cache_key = f"library_playlists_{provider_id}" - prev_db_ids = await self.mass.cache.async_get(cache_key, default=[]) - cur_db_ids = [] - for playlist in await music_provider.async_get_library_playlists(): - db_item = await self.mass.music.async_get_playlist( + prev_db_ids = await self.mass.cache.get(cache_key, default=[]) + cur_db_ids = set() + for playlist in await music_provider.get_library_playlists(): + db_item = await self.mass.music.get_playlist( playlist.item_id, provider_id, lazy=False ) if db_item.checksum != playlist.checksum: - db_item = await self.mass.database.async_add_playlist(playlist) - cur_db_ids.append(db_item.item_id) - await self.mass.database.async_add_to_library( + db_item = await self.mass.database.add_playlist(playlist) + cur_db_ids.add(db_item.item_id) + await self.mass.database.add_to_library( db_item.item_id, MediaType.Playlist, playlist.provider ) # precache playlist tracks - for playlist_track in await self.mass.music.async_get_playlist_tracks( + for playlist_track in await self.mass.music.get_playlist_tracks( playlist.item_id, provider_id ): # try to find substitutes for unavailable tracks with matching technique if not db_item.available and not playlist_track.available: if playlist_track.provider == "database": - await self.mass.music.async_match_track(playlist_track) + await self.mass.music.match_track(playlist_track) else: - await self.mass.music.async_add_track(playlist_track) + await self.mass.music.add_track(playlist_track) # process playlist deletions for db_id in prev_db_ids: if db_id not in cur_db_ids: - await self.mass.database.async_remove_from_library( + await self.mass.database.remove_from_library( db_id, MediaType.Playlist, provider_id ) # store ids in cache for next sync - await self.mass.cache.async_set(cache_key, cur_db_ids) + await self.mass.cache.set(cache_key, cur_db_ids) @sync_task("radios") - async def async_library_radios_sync(self, provider_id: str): + async def library_radios_sync(self, provider_id: str): """Sync library radios for given provider.""" music_provider = self.mass.get_provider(provider_id) cache_key = f"library_radios_{provider_id}" - prev_db_ids = await self.mass.cache.async_get(cache_key, default=[]) - cur_db_ids = [] - for item in await music_provider.async_get_library_radios(): - db_radio = await self.mass.music.async_get_radio( + prev_db_ids = await self.mass.cache.get(cache_key, default=[]) + cur_db_ids = set() + for item in await music_provider.get_library_radios(): + db_radio = await self.mass.music.get_radio( item.item_id, provider_id, lazy=False ) - cur_db_ids.append(db_radio.item_id) - await self.mass.database.async_add_to_library( + cur_db_ids.add(db_radio.item_id) + await self.mass.database.add_to_library( db_radio.item_id, MediaType.Radio, provider_id ) # process deletions for db_id in prev_db_ids: if db_id not in cur_db_ids: - await self.mass.database.async_remove_from_library( + await self.mass.database.remove_from_library( db_id, MediaType.Radio, provider_id ) # store ids in cache for next sync - await self.mass.cache.async_set(cache_key, cur_db_ids) + await self.mass.cache.set(cache_key, cur_db_ids) diff --git a/music_assistant/managers/metadata.py b/music_assistant/managers/metadata.py index dac230f5..9533f24d 100755 --- a/music_assistant/managers/metadata.py +++ b/music_assistant/managers/metadata.py @@ -3,8 +3,8 @@ import logging from typing import Dict, List -from music_assistant.helpers.cache import async_cached -from music_assistant.helpers.typing import MusicAssistantType +from music_assistant.helpers.cache import cached +from music_assistant.helpers.typing import MusicAssistant from music_assistant.helpers.util import merge_dict from music_assistant.models.provider import MetadataProvider, ProviderType @@ -15,7 +15,7 @@ class MetaDataManager: """Several helpers to search and store metadata for mediaitems using metadata providers.""" # TODO: create periodic task to search for missing metadata - def __init__(self, mass: MusicAssistantType) -> None: + def __init__(self, mass: MusicAssistant) -> None: """Initialize class.""" self.mass = mass self.cache = mass.cache @@ -25,9 +25,7 @@ class MetaDataManager: """Return all providers of type MetadataProvider.""" return self.mass.get_providers(ProviderType.METADATA_PROVIDER) - async def async_get_artist_metadata( - self, mb_artist_id: str, cur_metadata: Dict - ) -> Dict: + async def get_artist_metadata(self, mb_artist_id: str, cur_metadata: Dict) -> Dict: """Get/update rich metadata for an artist by providing the musicbrainz artist id.""" metadata = cur_metadata for provider in self.providers: @@ -35,8 +33,8 @@ class MetaDataManager: # no need to query (other) metadata providers if we already have a result break cache_key = f"{provider.id}.artist_metadata.{mb_artist_id}" - res = await async_cached( - self.cache, cache_key, provider.async_get_artist_images, mb_artist_id + res = await cached( + self.cache, cache_key, provider.get_artist_images, mb_artist_id ) if res: metadata = merge_dict(metadata, res) diff --git a/music_assistant/managers/music.py b/music_assistant/managers/music.py index 8bb05a60..c823b963 100755 --- a/music_assistant/managers/music.py +++ b/music_assistant/managers/music.py @@ -11,7 +11,7 @@ from music_assistant.constants import ( EVENT_RADIO_ADDED, EVENT_TRACK_ADDED, ) -from music_assistant.helpers.cache import async_cached +from music_assistant.helpers.cache import cached from music_assistant.helpers.compare import ( compare_album, compare_artists, @@ -49,7 +49,7 @@ class MusicManager: self.cache = mass.cache self.musicbrainz = MusicBrainz(mass) - async def async_setup(self): + async def setup(self): """Async initialize of module.""" @property @@ -60,7 +60,7 @@ class MusicManager: ################ GET MediaItem(s) by id and provider ################# @api_route("items/:media_type/:provider_id/:item_id") - async def async_get_item( + async def get_item( self, item_id: str, provider_id: str, @@ -70,58 +70,52 @@ class MusicManager: ): """Get single music item by id and media type.""" if media_type == MediaType.Artist: - return await self.async_get_artist( + return await self.get_artist( item_id, provider_id, refresh=refresh, lazy=lazy ) if media_type == MediaType.Album: - return await self.async_get_album( + return await self.get_album( item_id, provider_id, refresh=refresh, lazy=lazy ) if media_type == MediaType.Track: - return await self.async_get_track( + return await self.get_track( item_id, provider_id, refresh=refresh, lazy=lazy ) if media_type == MediaType.Playlist: - return await self.async_get_playlist( + return await self.get_playlist( item_id, provider_id, refresh=refresh, lazy=lazy ) if media_type == MediaType.Radio: - return await self.async_get_radio( + return await self.get_radio( item_id, provider_id, refresh=refresh, lazy=lazy ) return None @api_route("artists/:provider_id/:item_id") - async def async_get_artist( + async def get_artist( self, item_id: str, provider_id: str, refresh: bool = False, lazy: bool = True ) -> Artist: """Return artist details for the given provider artist id.""" if provider_id == "database" and not refresh: - return await self.mass.database.async_get_artist(item_id) - db_item = await self.mass.database.async_get_artist_by_prov_id( - provider_id, item_id - ) + return await self.mass.database.get_artist(item_id) + db_item = await self.mass.database.get_artist_by_prov_id(provider_id, item_id) if db_item and refresh: provider_id, item_id = await self.__get_provider_id(db_item) elif db_item: return db_item - artist = await self.__async_get_provider_artist(item_id, provider_id) + artist = await self._get_provider_artist(item_id, provider_id) if not lazy: - return await self.async_add_artist(artist) - self.mass.add_background_task(self.async_add_artist(artist)) + return await self.add_artist(artist) + self.mass.add_background_task(self.add_artist(artist)) return db_item if db_item else artist - async def __async_get_provider_artist( - self, item_id: str, provider_id: str - ) -> Artist: + async def _get_provider_artist(self, item_id: str, provider_id: str) -> Artist: """Return artist details for the given provider artist id.""" provider = self.mass.get_provider(provider_id) if not provider or not provider.available: raise Exception("Provider %s is not available!" % provider_id) 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 cached(self.cache, cache_key, provider.get_artist, item_id) if not artist: raise Exception( "Artist %s not found on provider %s" % (item_id, provider_id) @@ -129,34 +123,30 @@ class MusicManager: return artist @api_route("albums/:provider_id/:item_id") - async def async_get_album( + async def get_album( self, item_id: str, provider_id: str, refresh: bool = False, lazy: bool = True ) -> Album: """Return album details for the given provider album id.""" if provider_id == "database" and not refresh: - return await self.mass.database.async_get_album(item_id) - db_item = await self.mass.database.async_get_album_by_prov_id( - provider_id, item_id - ) + return await self.mass.database.get_album(item_id) + db_item = await self.mass.database.get_album_by_prov_id(provider_id, item_id) if db_item and refresh: provider_id, item_id = await self.__get_provider_id(db_item) elif db_item: return db_item - album = await self.__async_get_provider_album(item_id, provider_id) + album = await self._get_provider_album(item_id, provider_id) if not lazy: - return await self.async_add_album(album) - self.mass.add_background_task(self.async_add_album(album)) + return await self.add_album(album) + self.mass.add_background_task(self.add_album(album)) return db_item if db_item else album - async def __async_get_provider_album(self, item_id: str, provider_id: str) -> Album: + async def _get_provider_album(self, item_id: str, provider_id: str) -> Album: """Return album details for the given provider album id.""" provider = self.mass.get_provider(provider_id) if not provider or not provider.available: raise Exception("Provider %s is not available!" % provider_id) cache_key = f"{provider_id}.get_album.{item_id}" - album = await async_cached( - self.cache, cache_key, provider.async_get_album, item_id - ) + album = await cached(self.cache, cache_key, provider.get_album, item_id) if not album: raise Exception( "Album %s not found on provider %s" % (item_id, provider_id) @@ -164,7 +154,7 @@ class MusicManager: return album @api_route("tracks/:provider_id/:item_id") - async def async_get_track( + async def get_track( self, item_id: str, provider_id: str, @@ -175,32 +165,28 @@ class MusicManager: ) -> Track: """Return track details for the given provider track id.""" if provider_id == "database" and not refresh: - return await self.mass.database.async_get_track(item_id) - db_item = await self.mass.database.async_get_track_by_prov_id( - provider_id, item_id - ) + return await self.mass.database.get_track(item_id) + db_item = await self.mass.database.get_track_by_prov_id(provider_id, item_id) if db_item and refresh: provider_id, item_id = await self.__get_provider_id(db_item) elif db_item: return db_item if not track_details: - track_details = await self.__async_get_provider_track(item_id, provider_id) + track_details = await self._get_provider_track(item_id, provider_id) if album_details: track_details.album = album_details if not lazy: - return await self.async_add_track(track_details) - self.mass.add_background_task(self.async_add_track(track_details)) + return await self.add_track(track_details) + self.mass.add_background_task(self.add_track(track_details)) return db_item if db_item else track_details - async def __async_get_provider_track(self, item_id: str, provider_id: str) -> Track: + async def _get_provider_track(self, item_id: str, provider_id: str) -> Track: """Return track details for the given provider track id.""" provider = self.mass.get_provider(provider_id) if not provider or not provider.available: raise Exception("Provider %s is not available!" % provider_id) cache_key = f"{provider_id}.get_track.{item_id}" - track = await async_cached( - self.cache, cache_key, provider.async_get_track, item_id - ) + track = await cached(self.cache, cache_key, provider.get_track, item_id) if not track: raise Exception( "Track %s not found on provider %s" % (item_id, provider_id) @@ -208,36 +194,32 @@ class MusicManager: return track @api_route("playlists/:provider_id/:item_id") - async def async_get_playlist( + async def get_playlist( self, item_id: str, provider_id: str, refresh: bool = False, lazy: bool = True ) -> Playlist: """Return playlist details for the given provider playlist id.""" assert item_id and provider_id - db_item = await self.mass.database.async_get_playlist_by_prov_id( - provider_id, item_id - ) + db_item = await self.mass.database.get_playlist_by_prov_id(provider_id, item_id) if db_item and refresh: provider_id, item_id = await self.__get_provider_id(db_item) elif db_item: return db_item - playlist = await self.__async_get_provider_playlist(item_id, provider_id) + playlist = await self._get_provider_playlist(item_id, provider_id) if not lazy: - return await self.async_add_playlist(playlist) - self.mass.add_background_task(self.async_add_playlist(playlist)) + return await self.add_playlist(playlist) + self.mass.add_background_task(self.add_playlist(playlist)) return db_item if db_item else playlist - async def __async_get_provider_playlist( - self, item_id: str, provider_id: str - ) -> Playlist: + async def _get_provider_playlist(self, item_id: str, provider_id: str) -> Playlist: """Return playlist details for the given provider playlist id.""" provider = self.mass.get_provider(provider_id) if not provider or not provider.available: raise Exception("Provider %s is not available!" % provider_id) cache_key = f"{provider_id}.get_playlist.{item_id}" - playlist = await async_cached( + playlist = await cached( self.cache, cache_key, - provider.async_get_playlist, + provider.get_playlist, item_id, expires=86400 * 2, ) @@ -248,33 +230,29 @@ class MusicManager: return playlist @api_route("radios/:provider_id/:item_id") - async def async_get_radio( + async def get_radio( self, item_id: str, provider_id: str, refresh: bool = False, lazy: bool = True ) -> Radio: """Return radio details for the given provider radio id.""" assert item_id and provider_id - db_item = await self.mass.database.async_get_radio_by_prov_id( - provider_id, item_id - ) + db_item = await self.mass.database.get_radio_by_prov_id(provider_id, item_id) if db_item and refresh: provider_id, item_id = await self.__get_provider_id(db_item) elif db_item: return db_item - radio = await self.__async_get_provider_radio(item_id, provider_id) + radio = await self._get_provider_radio(item_id, provider_id) if not lazy: - return await self.async_add_radio(radio) - self.mass.add_background_task(self.async_add_radio(radio)) + return await self.add_radio(radio) + self.mass.add_background_task(self.add_radio(radio)) return db_item if db_item else radio - async def __async_get_provider_radio(self, item_id: str, provider_id: str) -> Radio: + async def _get_provider_radio(self, item_id: str, provider_id: str) -> Radio: """Return radio details for the given provider playlist id.""" provider = self.mass.get_provider(provider_id) if not provider or not provider.available: raise Exception("Provider %s is not available!" % provider_id) cache_key = f"{provider_id}.get_radio.{item_id}" - radio = await async_cached( - self.cache, cache_key, provider.async_get_radio, item_id - ) + radio = await cached(self.cache, cache_key, provider.get_radio, item_id) if not radio: raise Exception( "Radio %s not found on provider %s" % (item_id, provider_id) @@ -282,23 +260,22 @@ class MusicManager: return radio @api_route("albums/:provider_id/:item_id/tracks") - async def async_get_album_tracks( - self, item_id: str, provider_id: str - ) -> List[Track]: + async def get_album_tracks(self, item_id: str, provider_id: str) -> List[Track]: """Return album tracks for the given provider album id.""" assert item_id and provider_id - album = await self.async_get_album(item_id, provider_id) + album = await self.get_album(item_id, provider_id) if album.provider == "database": # album tracks are not stored in db, we always fetch them (cached) from the provider. - provider_id = album.provider_ids[0].provider - item_id = album.provider_ids[0].item_id + prov_id = next(iter(album.provider_ids)) + provider_id = prov_id.provider + item_id = prov_id.item_id provider = self.mass.get_provider(provider_id) cache_key = f"{provider_id}.album_tracks.{item_id}" - all_prov_tracks = await async_cached( - self.cache, cache_key, provider.async_get_album_tracks, item_id + all_prov_tracks = await cached( + self.cache, cache_key, provider.get_album_tracks, item_id ) # retrieve list of db items - db_tracks = await self.mass.database.async_get_tracks_from_provider_ids( + db_tracks = await self.mass.database.get_tracks_from_provider_ids( [x.provider for x in album.provider_ids], [x.item_id for x in all_prov_tracks], ) @@ -315,11 +292,9 @@ class MusicManager: ] @api_route("albums/:provider_id/:item_id/versions") - async def async_get_album_versions( - self, item_id: str, provider_id: str - ) -> List[Album]: + async def get_album_versions(self, item_id: str, provider_id: str) -> List[Album]: """Return all versions of an album we can find on all providers.""" - album = await self.async_get_album(item_id, provider_id) + album = await self.get_album(item_id, provider_id) provider_ids = [ item.id for item in self.mass.get_providers(ProviderType.MUSIC_PROVIDER) ] @@ -328,9 +303,7 @@ class MusicManager: prov_item for prov_items in await asyncio.gather( *[ - self.async_search_provider( - search_query, prov_id, [MediaType.Album], 25 - ) + self.search_provider(search_query, prov_id, [MediaType.Album], 25) for prov_id in provider_ids ] ) @@ -339,11 +312,9 @@ class MusicManager: ] @api_route("tracks/:provider_id/:item_id/versions") - async def async_get_track_versions( - self, item_id: str, provider_id: str - ) -> List[Track]: + async def get_track_versions(self, item_id: str, provider_id: str) -> List[Track]: """Return all versions of a track we can find on all providers.""" - track = await self.async_get_track(item_id, provider_id) + track = await self.get_track(item_id, provider_id) provider_ids = [ item.id for item in self.mass.get_providers(ProviderType.MUSIC_PROVIDER) ] @@ -352,9 +323,7 @@ class MusicManager: prov_item for prov_items in await asyncio.gather( *[ - self.async_search_provider( - search_query, prov_id, [MediaType.Track], 25 - ) + self.search_provider(search_query, prov_id, [MediaType.Track], 25) for prov_id in provider_ids ] ) @@ -363,30 +332,29 @@ class MusicManager: ] @api_route("playlists/:provider_id/:item_id/tracks") - async def async_get_playlist_tracks( - self, item_id: str, provider_id: str - ) -> List[Track]: + async def get_playlist_tracks(self, item_id: str, provider_id: str) -> List[Track]: """Return playlist tracks for the given provider playlist id.""" 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. - playlist = await self.mass.database.async_get_playlist(item_id) - provider_id = playlist.provider_ids[0].provider - item_id = playlist.provider_ids[0].item_id + playlist = await self.mass.database.get_playlist(item_id) + prov_id = next(iter(playlist.provider_ids)) + provider_id = prov_id.provider + item_id = prov_id.item_id provider = self.mass.get_provider(provider_id) else: provider = self.mass.get_provider(provider_id) - playlist = await provider.async_get_playlist(item_id) + playlist = await provider.get_playlist(item_id) cache_checksum = playlist.checksum cache_key = f"{provider_id}.playlist_tracks.{item_id}" - playlist_tracks = await async_cached( + playlist_tracks = await cached( self.cache, cache_key, - provider.async_get_playlist_tracks, + provider.get_playlist_tracks, item_id, checksum=cache_checksum, ) - db_tracks = await self.mass.database.async_get_tracks_from_provider_ids( + db_tracks = await self.mass.database.get_tracks_from_provider_ids( provider_id, [x.item_id for x in playlist_tracks] ) # combine provider tracks with db tracks @@ -418,40 +386,37 @@ class MusicManager: if track_number is not None: item.track_number = track_number # make sure artists are unique - if hasattr(item, "artists"): - item.artists = unique_item_ids(item.artists) + # if hasattr(item, "artists"): + # item.artists = unique_item_ids(item.artists) return item @api_route("artists/:provider_id/:item_id/tracks") - async def async_get_artist_toptracks( - self, item_id: str, provider_id: str - ) -> List[Track]: + async def get_artist_toptracks(self, item_id: str, provider_id: str) -> List[Track]: """Return top tracks for an artist.""" - artist = await self.async_get_artist(item_id, provider_id) + artist = await self.get_artist(item_id, provider_id) # get results from all providers all_prov_tracks = [ track for prov_tracks in await asyncio.gather( *[ - self.__async_get_provider_artist_toptracks( - item.item_id, item.provider - ) + self._get_provider_artist_toptracks(item.item_id, item.provider) for item in artist.provider_ids ] ) for track in prov_tracks ] # retrieve list of db items - db_tracks = await self.mass.database.async_get_tracks_from_provider_ids( + db_tracks = await self.mass.database.get_tracks_from_provider_ids( [x.provider for x in artist.provider_ids], [x.item_id for x in all_prov_tracks], ) # combine provider tracks with db tracks and filter duplicate itemid's - return unique_item_ids( - [await self.__process_item(item, db_tracks) for item in all_prov_tracks] - ) + return {await self.__process_item(item, db_tracks) for item in all_prov_tracks} + # return unique_item_ids( + # [await self.__process_item(item, db_tracks) for item in all_prov_tracks] + # ) - async def __async_get_provider_artist_toptracks( + async def _get_provider_artist_toptracks( self, item_id: str, provider_id: str ) -> List[Track]: """Return top tracks for an artist on given provider.""" @@ -460,32 +425,30 @@ class MusicManager: LOGGER.error("Provider %s is not available", provider_id) return [] cache_key = f"{provider_id}.artist_toptracks.{item_id}" - return await async_cached( + return await cached( self.cache, cache_key, - provider.async_get_artist_toptracks, + provider.get_artist_toptracks, item_id, ) @api_route("artists/:provider_id/:item_id/albums") - async def async_get_artist_albums( - self, item_id: str, provider_id: str - ) -> List[Album]: + async def get_artist_albums(self, item_id: str, provider_id: str) -> List[Album]: """Return (all) albums for an artist.""" - artist = await self.async_get_artist(item_id, provider_id) + artist = await self.get_artist(item_id, provider_id) # get results from all providers all_prov_albums = [ album for prov_albums in await asyncio.gather( *[ - self.__async_get_provider_artist_albums(item.item_id, item.provider) + self._get_provider_artist_albums(item.item_id, item.provider) for item in artist.provider_ids ] ) for album in prov_albums ] # retrieve list of db items - db_tracks = await self.mass.database.async_get_albums_from_provider_ids( + db_tracks = await self.mass.database.get_albums_from_provider_ids( [x.provider for x in artist.provider_ids], [x.item_id for x in all_prov_albums], ) @@ -494,7 +457,7 @@ class MusicManager: [await self.__process_item(item, db_tracks) for item in all_prov_albums] ) - async def __async_get_provider_artist_albums( + async def _get_provider_artist_albums( self, item_id: str, provider_id: str ) -> List[Album]: """Return albums for an artist on given provider.""" @@ -503,15 +466,15 @@ class MusicManager: LOGGER.error("Provider %s is not available", provider_id) return [] cache_key = f"{provider_id}.artist_albums.{item_id}" - return await async_cached( + return await cached( self.cache, cache_key, - provider.async_get_artist_albums, + provider.get_artist_albums, item_id, ) @api_route("search/:provider_id") - async def async_search_provider( + async def search_provider( self, search_query: str, provider_id: str, @@ -528,20 +491,20 @@ class MusicManager: """ if provider_id == "database": # get results from database - return await self.mass.database.async_search(search_query, media_types) + return await self.mass.database.search(search_query, media_types) provider = self.mass.get_provider(provider_id) cache_key = f"{provider_id}.search.{search_query}.{media_types}.{limit}" - return await async_cached( + return await cached( self.cache, cache_key, - provider.async_search, + provider.search, search_query, media_types, limit, ) @api_route("search") - async def async_global_search( + async def global_search( self, search_query, media_types: List[MediaType], limit: int = 10 ) -> SearchResult: """ @@ -557,7 +520,7 @@ class MusicManager: item.id for item in self.mass.get_providers(ProviderType.MUSIC_PROVIDER) ] for provider_id in provider_ids: - provider_result = await self.async_search_provider( + provider_result = await self.search_provider( search_query, provider_id, media_types, limit ) result.artists += provider_result.artists @@ -568,7 +531,7 @@ class MusicManager: # TODO: sort by name and filter out duplicates ? return result - async def async_get_stream_details( + async def get_stream_details( self, media_item: MediaItem, player_id: str = "" ) -> StreamDetails: """ @@ -597,7 +560,7 @@ class MusicManager: full_track = media_item else: full_track = ( - await self.async_get_track(media_item.item_id, media_item.provider) + await self.get_track(media_item.item_id, media_item.provider) or media_item ) # sort by quality and check track availability @@ -611,8 +574,8 @@ class MusicManager: if not music_prov or not music_prov.available: continue # provider temporary unavailable ? - streamdetails: StreamDetails = ( - await music_prov.async_get_stream_details(prov_media.item_id) + streamdetails: StreamDetails = await music_prov.get_stream_details( + prov_media.item_id ) if streamdetails: try: @@ -635,65 +598,65 @@ class MusicManager: ################ ADD MediaItem(s) to database helpers ################ - async def async_add_artist(self, artist: Artist) -> Artist: + async def add_artist(self, artist: Artist) -> Artist: """Add artist to local db and return the database item.""" if not artist.musicbrainz_id: - artist.musicbrainz_id = await self.__async_get_artist_musicbrainz_id(artist) + artist.musicbrainz_id = await self._get_artist_musicbrainz_id(artist) # grab additional metadata - artist.metadata = await self.mass.metadata.async_get_artist_metadata( + artist.metadata = await self.mass.metadata.get_artist_metadata( artist.musicbrainz_id, artist.metadata ) - db_item = await self.mass.database.async_add_artist(artist) + db_item = await self.mass.database.add_artist(artist) # also fetch same artist on all providers - await self.async_match_artist(db_item) + await self.match_artist(db_item) self.mass.signal_event(EVENT_ARTIST_ADDED, db_item) return db_item - async def async_add_album(self, album: Album) -> Album: + async def add_album(self, album: Album) -> Album: """Add album to local db and return the database item.""" # make sure we have an artist assert album.artist - db_item = await self.mass.database.async_add_album(album) + db_item = await self.mass.database.add_album(album) # also fetch same album on all providers - await self.async_match_album(db_item) + await self.match_album(db_item) self.mass.signal_event(EVENT_ALBUM_ADDED, db_item) return db_item - async def async_add_track(self, track: Track) -> Track: + async def add_track(self, track: Track) -> Track: """Add track to local db and return the new database item.""" # make sure we have artists assert track.artists # make sure we have an album assert track.album or track.albums - db_item = await self.mass.database.async_add_track(track) + db_item = await self.mass.database.add_track(track) # also fetch same track on all providers (will also get other quality versions) - await self.async_match_track(db_item) + await self.match_track(db_item) self.mass.signal_event(EVENT_TRACK_ADDED, db_item) return db_item - async def async_add_playlist(self, playlist: Playlist) -> Playlist: + async def add_playlist(self, playlist: Playlist) -> Playlist: """Add playlist to local db and return the new database item.""" - db_item = await self.mass.database.async_add_playlist(playlist) + db_item = await self.mass.database.add_playlist(playlist) self.mass.signal_event(EVENT_PLAYLIST_ADDED, db_item) return db_item - async def async_add_radio(self, radio: Radio) -> Radio: + async def add_radio(self, radio: Radio) -> Radio: """Add radio to local db and return the new database item.""" - db_item = await self.mass.database.async_add_radio(radio) + db_item = await self.mass.database.add_radio(radio) self.mass.signal_event(EVENT_RADIO_ADDED, db_item) return db_item - async def __async_get_artist_musicbrainz_id(self, artist: Artist): + async def _get_artist_musicbrainz_id(self, artist: Artist): """Fetch musicbrainz id by performing search using the artist name, albums and tracks.""" # try with album first - for lookup_album in await self.__async_get_provider_artist_albums( + for lookup_album in await self._get_provider_artist_albums( artist.item_id, artist.provider ): if not lookup_album: continue if artist.name != lookup_album.artist.name: continue - musicbrainz_id = await self.musicbrainz.async_get_mb_artist_id( + musicbrainz_id = await self.musicbrainz.get_mb_artist_id( artist.name, albumname=lookup_album.name, album_upc=lookup_album.upc, @@ -701,12 +664,12 @@ class MusicManager: if musicbrainz_id: return musicbrainz_id # fallback to track - for lookup_track in await self.__async_get_provider_artist_toptracks( + for lookup_track in await self._get_provider_artist_toptracks( artist.item_id, artist.provider ): if not lookup_track: continue - musicbrainz_id = await self.musicbrainz.async_get_mb_artist_id( + musicbrainz_id = await self.musicbrainz.get_mb_artist_id( artist.name, trackname=lookup_track.name, track_isrc=lookup_track.isrc, @@ -717,7 +680,7 @@ class MusicManager: LOGGER.warning("Unable to get musicbrainz ID for artist %s !", artist.name) return artist.name - async def async_match_artist(self, db_artist: Artist): + async def match_artist(self, db_artist: Artist): """ Try to find matching artists on all providers for the provided (database) item_id. @@ -732,31 +695,27 @@ class MusicManager: continue if MediaType.Artist not in provider.supported_mediatypes: continue - if not await self.__async_match_prov_artist(db_artist, provider): + if not await self._match_prov_artist(db_artist, provider): LOGGER.debug( "Could not find match for Artist %s on provider %s", db_artist.name, provider.name, ) - async def __async_match_prov_artist( - self, db_artist: Artist, provider: MusicProvider - ): + async def _match_prov_artist(self, db_artist: Artist, provider: MusicProvider): """Try to find matching artists on given provider for the provided (database) artist.""" LOGGER.debug( "Trying to match artist %s on provider %s", db_artist.name, provider.name ) # try to get a match with some reference tracks of this artist - for ref_track in await self.async_get_artist_toptracks( + for ref_track in await self.get_artist_toptracks( db_artist.item_id, db_artist.provider ): # make sure we have a full track if isinstance(ref_track.album, ItemMapping): - ref_track = await self.async_get_track( - ref_track.item_id, ref_track.provider - ) + ref_track = await self.get_track(ref_track.item_id, ref_track.provider) searchstr = "%s %s" % (db_artist.name, ref_track.name) - search_results = await self.async_search_provider( + search_results = await self.search_provider( searchstr, provider.id, [MediaType.Track], limit=25 ) for search_result_item in search_results.tracks: @@ -766,22 +725,22 @@ class MusicManager: if compare_strings(db_artist.name, search_item_artist.name): # 100% album match # get full artist details so we have all metadata - prov_artist = await self.__async_get_provider_artist( + prov_artist = await self._get_provider_artist( search_item_artist.item_id, search_item_artist.provider ) - await self.mass.database.async_update_artist( + await self.mass.database.update_artist( db_artist.item_id, prov_artist ) return True # try to get a match with some reference albums of this artist - artist_albums = await self.async_get_artist_albums( + artist_albums = await self.get_artist_albums( db_artist.item_id, db_artist.provider ) for ref_album in artist_albums[:50]: if ref_album.album_type == AlbumType.Compilation: continue searchstr = "%s %s" % (db_artist.name, ref_album.name) - search_result = await self.async_search_provider( + search_result = await self.search_provider( searchstr, provider.id, [MediaType.Album], limit=25 ) for search_result_item in search_result.albums: @@ -791,17 +750,17 @@ class MusicManager: if compare_album(search_result_item, ref_album): # 100% album match # get full artist details so we have all metadata - prov_artist = await self.__async_get_provider_artist( + prov_artist = await self._get_provider_artist( search_result_item.artist.item_id, search_result_item.artist.provider, ) - await self.mass.database.async_update_artist( + await self.mass.database.update_artist( db_artist.item_id, prov_artist ) return True return False - async def async_match_album(self, db_album: Album): + async def match_album(self, db_album: Album): """ Try to find matching album on all providers for the provided (database) album_id. @@ -812,7 +771,7 @@ class MusicManager: ), "Matching only supported for database items!" if not isinstance(db_album, FullAlbum): # matching only works if we have a full album object - db_album = await self.mass.database.async_get_album(db_album.item_id) + db_album = await self.mass.database.get_album(db_album.item_id) async def find_prov_match(provider): LOGGER.debug( @@ -822,7 +781,7 @@ class MusicManager: searchstr = "%s %s" % (db_album.artist.name, db_album.name) if db_album.version: searchstr += " " + db_album.version - search_result = await self.async_search_provider( + search_result = await self.search_provider( searchstr, provider.id, [MediaType.Album], limit=25 ) for search_result_item in search_result.albums: @@ -831,21 +790,19 @@ class MusicManager: if not compare_album(search_result_item, db_album): continue # we must fetch the full album version, search results are simplified objects - prov_album = await self.__async_get_provider_album( + prov_album = await self._get_provider_album( search_result_item.item_id, search_result_item.provider ) if compare_album(prov_album, db_album): # 100% match, we can simply update the db with additional provider ids - await self.mass.database.async_update_album( - db_album.item_id, prov_album - ) + await self.mass.database.update_album(db_album.item_id, prov_album) match_found = True # while we're here, also match the artist if db_album.artist.provider == "database": - prov_artist = await self.__async_get_provider_artist( + prov_artist = await self._get_provider_artist( prov_album.artist.item_id, prov_album.artist.provider ) - await self.mass.database.async_update_artist( + await self.mass.database.update_artist( db_album.artist.item_id, prov_artist ) @@ -863,7 +820,7 @@ class MusicManager: if MediaType.Album in provider.supported_mediatypes: await find_prov_match(provider) - async def async_match_track(self, db_track: Track): + async def match_track(self, db_track: Track): """ Try to find matching track on all providers for the provided (database) track_id. @@ -874,7 +831,7 @@ class MusicManager: ), "Matching only supported for database items!" if isinstance(db_track.album, ItemMapping): # matching only works if we have a full track object - db_track = await self.mass.database.async_get_track(db_track.item_id) + db_track = await self.mass.database.get_track(db_track.item_id) for provider in self.mass.get_providers(ProviderType.MUSIC_PROVIDER): if MediaType.Track not in provider.supported_mediatypes: continue @@ -888,7 +845,7 @@ class MusicManager: searchstr = "%s %s" % (db_track_artist.name, db_track.name) if db_track.version: searchstr += " " + db_track.version - search_result = await self.async_search_provider( + search_result = await self.search_provider( searchstr, provider.id, [MediaType.Track], limit=25 ) for search_result_item in search_result.tracks: @@ -897,7 +854,7 @@ class MusicManager: if compare_track(search_result_item, db_track): # 100% match, we can simply update the db with additional provider ids match_found = True - await self.mass.database.async_update_track( + await self.mass.database.update_track( db_track.item_id, search_result_item ) # while we're here, also match the artist @@ -907,10 +864,10 @@ class MusicManager: db_track_artist.name, artist.name ): continue - prov_artist = await self.__async_get_provider_artist( + prov_artist = await self._get_provider_artist( artist.item_id, artist.provider ) - await self.mass.database.async_update_artist( + await self.mass.database.update_artist( db_track_artist.item_id, prov_artist ) @@ -924,7 +881,7 @@ class MusicManager: async def __get_provider_id(self, media_item: MediaItem) -> tuple: """Return provider and item id.""" if media_item.provider == "database": - media_item = await self.mass.database.async_get_item_by_prov_id( + media_item = await self.mass.database.get_item_by_prov_id( "database", media_item.item_id, media_item.media_type ) for prov in media_item.provider_ids: diff --git a/music_assistant/managers/players.py b/music_assistant/managers/players.py index a8ba7164..a8198a78 100755 --- a/music_assistant/managers/players.py +++ b/music_assistant/managers/players.py @@ -1,16 +1,15 @@ """PlayerManager: Orchestrates all players from player providers.""" import logging -from typing import List, Optional, Union +from typing import Dict, List, Optional, Set, Tuple, Union from music_assistant.constants import ( - CONF_ENABLED, CONF_POWER_CONTROL, CONF_VOLUME_CONTROL, EVENT_PLAYER_ADDED, EVENT_PLAYER_REMOVED, ) -from music_assistant.helpers.typing import MusicAssistantType +from music_assistant.helpers.typing import MusicAssistant from music_assistant.helpers.util import callback, run_periodic, try_parse_int from music_assistant.helpers.web import api_route from music_assistant.models.media_types import MediaItem, MediaType @@ -21,7 +20,6 @@ from music_assistant.models.player import ( PlayerControlType, ) from music_assistant.models.player_queue import PlayerQueue, QueueItem, QueueOption -from music_assistant.models.player_state import PlayerState from music_assistant.models.provider import PlayerProvider, ProviderType POLL_INTERVAL = 30 @@ -32,75 +30,77 @@ LOGGER = logging.getLogger("player_manager") class PlayerManager: """Several helpers to handle playback through player providers.""" - def __init__(self, mass: MusicAssistantType): + def __init__(self, mass: MusicAssistant) -> None: """Initialize class.""" self.mass = mass - self._player_states = {} + self._players = {} self._providers = {} self._player_queues = {} self._poll_ticks = 0 self._controls = {} - async def async_setup(self): + async def setup(self) -> None: """Async initialize of module.""" self.mass.add_job(self.poll_task()) - self.mass.web.register_api_route("players", self._player_states.values) - self.mass.web.register_api_route("players/queues", self._player_queues.values) - async def async_close(self): + async def close(self) -> None: """Handle stop/shutdown.""" - for player_queue in list(self._player_queues.values()): - await player_queue.async_close() - for player in self.players: - await player.async_on_remove() + for player_queue in self._player_queues.values(): + await player_queue.close() + for player in self: + await player.on_remove() @run_periodic(1) async def poll_task(self): """Check for updates on players that need to be polled.""" - for player in self.players: + for player in self: + if not player.player_state.available: + continue if player.should_poll and ( self._poll_ticks >= POLL_INTERVAL or player.state == PlaybackState.Playing ): - await player.async_on_update() + await player.on_poll() if self._poll_ticks >= POLL_INTERVAL: self._poll_ticks = 0 else: self._poll_ticks += 1 @property - def player_states(self) -> List[PlayerState]: - """Return PlayerState of all registered players.""" - return list(self._player_states.values()) + def players(self) -> Dict[str, Player]: + """Return dict of all registered players.""" + return self._players @property - def players(self) -> List[Player]: - """Return all registered players.""" - return [player_state.player for player_state in self._player_states.values()] + def player_queues(self) -> Dict[str, PlayerQueue]: + """Return dict of all player queues.""" + return self._player_queues @property - def player_queues(self) -> List[PlayerQueue]: - """Return all player queues.""" - return list(self._player_queues.values()) - - @property - def providers(self) -> List[PlayerProvider]: - """Return all loaded player providers.""" + def providers(self) -> Tuple[PlayerProvider]: + """Return tuple with all loaded player providers.""" return self.mass.get_providers(ProviderType.PLAYER_PROVIDER) + def __iter__(self): + """Iterate over players.""" + return iter(self._players.values()) + + @callback + @api_route("players") + def get_players(self) -> Tuple[Player]: + """Return all players in a tuple.""" + return tuple(self._players.values()) + @callback - @api_route("players/:player_id") - def get_player_state(self, player_id: str) -> PlayerState: - """Return PlayerState by player_id or None if player does not exist.""" - return self._player_states.get(player_id) + @api_route("players/queues") + def get_player_queues(self) -> Tuple[PlayerQueue]: + """Return all player queues in a tuple.""" + return tuple(self._player_queues.values()) @callback def get_player(self, player_id: str) -> Player: """Return Player by player_id or None if player does not exist.""" - player_state = self._player_states.get(player_id) - if player_state: - return player_state.player - return None + return self._players.get(player_id) @callback def get_player_provider(self, player_id: str) -> PlayerProvider: @@ -112,17 +112,18 @@ class PlayerManager: @api_route("players/:player_id/queue") def get_player_queue(self, player_id: str) -> PlayerQueue: """Return player's queue by player_id or None if player does not exist.""" - player_state = self.get_player_state(player_id) - if not player_state: + player = self.get_player(player_id) + if not player: LOGGER.warning("Player(queue) %s is not available!", player_id) return None - return self._player_queues.get(player_state.active_queue) + return self._player_queues.get(player.active_queue) @callback @api_route("players/:queue_id/queue/items") - def get_player_queue_items(self, queue_id: str) -> List[QueueItem]: - """Return player's queueitems by player_id or None if player does not exist.""" - return self.get_player_queue(queue_id).items + def get_player_queue_items(self, queue_id: str) -> Set[QueueItem]: + """Return player's queueitems by player_id.""" + player_queue = self.get_player_queue(queue_id) + return player_queue.items if player_queue else {} @callback @api_route("players/controls/:control_id") @@ -137,73 +138,68 @@ class PlayerManager: @api_route("players/controls") def get_player_controls( self, filter_type: Optional[PlayerControlType] = None - ) -> List[PlayerControl]: + ) -> Set[PlayerControl]: """Return all PlayerControls, optionally filtered by type.""" - return [ + return { item for item in self._controls.values() if (filter_type is None or item.type == filter_type) - ] + } # ADD/REMOVE/UPDATE HELPERS - async def async_add_player(self, player: Player) -> None: + async def add_player(self, player: Player) -> None: """Register a new player or update an existing one.""" # guard for invalid data or exit in progress if not player or self.mass.exit: return - # redirect to update if player already exists - if player.player_id in self._player_states: - return await self.async_update_player(player) - # do not add the player to states if it's disabled/unavailable - if not self.mass.config.get_player_config(player.player_id)[CONF_ENABLED]: - return - # set the mass object on the player and call on_add function + # redirect to update if player is already added + if player.added_to_mass: + return await self.trigger_player_update(player.player_id) + # make sure that the mass instance is set on the player player.mass = self.mass - await player.async_on_add() - # create playerstate and queue object - player_state = PlayerState(self.mass, player) - self._player_states[player.player_id] = player_state - - 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._player_states[player.player_id].name, - ) - self.mass.signal_event( - EVENT_PLAYER_ADDED, self._player_states[player.player_id] - ) + self._players[player.player_id] = player + # make sure that the player state is created/updated + player.player_state.update(player.create_state()) + # Fully initialize only if player is enabled + if player.enabled: + await player.on_add() + player.added_to_mass = True + # create playerqueue instance + self._player_queues[player.player_id] = PlayerQueue( + self.mass, player.player_id + ) + LOGGER.info( + "Player added: %s/%s", + player.provider_id, + player.name, + ) + self.mass.signal_event(EVENT_PLAYER_ADDED, player) + else: + LOGGER.debug( + "Ignoring player: %s/%s because it's disabled", + player.provider_id, + player.name, + ) - async def async_remove_player(self, player_id: str): + async def remove_player(self, player_id: str): """Remove a player from the registry.""" - player_state = self._player_states.pop(player_id, None) - if player_state: - await player_state.player.async_on_remove() self._player_queues.pop(player_id, None) - LOGGER.info("Player removed: %s", player_id) + player = self._players.pop(player_id, None) + if player: + await player.on_remove() + player_name = player.name if player else player_id + LOGGER.info("Player removed: %s", player_name) self.mass.signal_event(EVENT_PLAYER_REMOVED, {"player_id": player_id}) - async def async_update_player(self, player: Player): - """Update an existing player (or register as new if non existing).""" - if self.mass.exit: - return - if player.player_id not in self._player_states: - return await self.async_add_player(player) - await self._player_states[player.player_id].async_update(player) - - async def async_trigger_player_update(self, player_id: str): + async def trigger_player_update(self, player_id: str): """Trigger update of an existing player..""" player = self.get_player(player_id) - player_state = self.get_player_state(player_id) - if player and player_state: - await player_state.async_update(player) + if player: + await player.on_poll() @api_route("players/controls/:control_id/register") - async def async_register_player_control( - self, control_id: str, control: PlayerControl - ): + async def register_player_control(self, control_id: str, control: PlayerControl): """Register a playercontrol with the player manager.""" control.mass = self.mass control.type = PlayerControlType(control.type) @@ -215,23 +211,19 @@ class PlayerManager: control.name, ) # update all players using this playercontrol - for player_state in self.player_states: - conf = self.mass.config.player_settings[player_state.player_id] + for player in self: + conf = self.mass.config.player_settings[player.player_id] if control_id in [ conf.get(CONF_POWER_CONTROL), conf.get(CONF_VOLUME_CONTROL), ]: - self.mass.add_job( - self.async_trigger_player_update(player_state.player_id) - ) + self.mass.add_job(self.trigger_player_update(player.player_id)) @api_route("players/controls/:control_id/update") - async def async_update_player_control( - self, control_id: str, control: PlayerControl - ): + async def update_player_control(self, control_id: str, control: PlayerControl): """Update a playercontrol's state on the player manager.""" if control_id not in self._controls: - return await self.async_register_player_control(control_id, control) + return await self.register_player_control(control_id, control) new_state = control.state if self._controls[control_id].state == new_state: return @@ -243,20 +235,18 @@ class PlayerManager: new_state, ) # update all players using this playercontrol - for player_state in self.player_states: - conf = self.mass.config.player_settings[player_state.player_id] + for player in self: + conf = self.mass.config.player_settings[player.player_id] if control_id in [ conf.get(CONF_POWER_CONTROL), conf.get(CONF_VOLUME_CONTROL), ]: - self.mass.add_job( - self.async_trigger_player_update(player_state.player_id) - ) + self.mass.add_job(self.trigger_player_update(player.player_id)) # SERVICE CALLS / PLAYER COMMANDS @api_route("players/:player_id/play_media") - async def async_play_media( + async def play_media( self, player_id: str, items: Union[MediaItem, List[MediaItem]], @@ -280,28 +270,28 @@ class PlayerManager: for media_item in items: # collect tracks to play if media_item.media_type == MediaType.Artist: - tracks = await self.mass.music.async_get_artist_toptracks( + tracks = await self.mass.music.get_artist_toptracks( media_item.item_id, provider_id=media_item.provider ) elif media_item.media_type == MediaType.Album: - tracks = await self.mass.music.async_get_album_tracks( + tracks = await self.mass.music.get_album_tracks( media_item.item_id, provider_id=media_item.provider ) elif media_item.media_type == MediaType.Playlist: - tracks = await self.mass.music.async_get_playlist_tracks( + tracks = await self.mass.music.get_playlist_tracks( media_item.item_id, provider_id=media_item.provider ) elif media_item.media_type == MediaType.Radio: # single radio tracks = [ - await self.mass.music.async_get_radio( + await self.mass.music.get_radio( media_item.item_id, provider_id=media_item.provider ) ] else: # single track tracks = [ - await self.mass.music.async_get_track( + await self.mass.music.get_track( media_item.item_id, provider_id=media_item.provider ) ] @@ -317,22 +307,22 @@ class PlayerManager: ) queue_items.append(queue_item) # turn on player - await self.async_cmd_power_on(player_id) + await self.cmd_power_on(player_id) # load items into the queue player_queue = self.get_player_queue(player_id) if queue_opt == QueueOption.Replace or ( len(queue_items) > 10 and queue_opt in [QueueOption.Play, QueueOption.Next] ): - return await player_queue.async_load(queue_items) + return await player_queue.load(queue_items) if queue_opt == QueueOption.Next: - return await player_queue.async_insert(queue_items, 1) + return await player_queue.insert(queue_items, 1) if queue_opt == QueueOption.Play: - return await player_queue.async_insert(queue_items, 0) + return await player_queue.insert(queue_items, 0) if queue_opt == QueueOption.Add: - return await player_queue.async_append(queue_items) + return await player_queue.append(queue_items) @api_route("players/:player_id/play_uri") - async def async_cmd_play_uri(self, player_id: str, uri: str): + async def cmd_play_uri(self, player_id: str, uri: str): """ Play the specified uri/url on the given player. @@ -349,190 +339,190 @@ class PlayerManager: queue_item.queue_item_id, ) # turn on player - await self.async_cmd_power_on(player_id) + await self.cmd_power_on(player_id) # load item into the queue player_queue = self.get_player_queue(player_id) - return await player_queue.async_insert([queue_item], 0) + return await player_queue.insert([queue_item], 0) @api_route("players/:player_id/cmd/stop") - async def async_cmd_stop(self, player_id: str) -> None: + async def cmd_stop(self, player_id: str) -> None: """ Send STOP command to given player. :param player_id: player_id of the player to handle the command. """ - player_state = self.get_player_state(player_id) - if not player_state: + player = self.get_player(player_id) + if not player: return - queue_id = player_state.active_queue + queue_id = player.active_queue queue_player = self.get_player(queue_id) - return await queue_player.async_cmd_stop() + return await queue_player.cmd_stop() @api_route("players/:player_id/cmd/play") - async def async_cmd_play(self, player_id: str) -> None: + async def cmd_play(self, player_id: str) -> None: """ Send PLAY command to given player. :param player_id: player_id of the player to handle the command. """ - player_state = self.get_player_state(player_id) - if not player_state: + player = self.get_player(player_id) + if not player: return - queue_id = player_state.active_queue + queue_id = player.active_queue queue_player = self.get_player(queue_id) # unpause if paused else resume queue if queue_player.state == PlaybackState.Paused: - return await queue_player.async_cmd_play() + return await queue_player.cmd_play() # power on at play request - await self.async_cmd_power_on(player_id) - return await self._player_queues[queue_id].async_resume() + await self.cmd_power_on(player_id) + return await self._player_queues[queue_id].resume() @api_route("players/:player_id/cmd/pause") - async def async_cmd_pause(self, player_id: str): + async def 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_state = self.get_player_state(player_id) - if not player_state: + player = self.get_player(player_id) + if not player: return - queue_id = player_state.active_queue + queue_id = player.active_queue queue_player = self.get_player(queue_id) - return await queue_player.async_cmd_pause() + return await queue_player.cmd_pause() @api_route("players/:player_id/cmd/play_pause") - async def async_cmd_play_pause(self, player_id: str): + async def 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_state = self.get_player_state(player_id) - if not player_state: + player = self.get_player(player_id) + if not player: return - if player_state.state == PlaybackState.Playing: - return await self.async_cmd_pause(player_id) - return await self.async_cmd_play(player_id) + if player.state == PlaybackState.Playing: + return await self.cmd_pause(player_id) + return await self.cmd_play(player_id) @api_route("players/:player_id/cmd/next") - async def async_cmd_next(self, player_id: str): + async def 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_state = self.get_player_state(player_id) - if not player_state: + player = self.get_player(player_id) + if not player: return - queue_id = player_state.active_queue - return await self.get_player_queue(queue_id).async_next() + queue_id = player.active_queue + return await self.get_player_queue(queue_id).next() @api_route("players/:player_id/cmd/previous") - async def async_cmd_previous(self, player_id: str): + async def 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_state = self.get_player_state(player_id) - if not player_state: + player = self.get_player(player_id) + if not player: return - queue_id = player_state.active_queue - return await self.get_player_queue(queue_id).async_previous() + queue_id = player.active_queue + return await self.get_player_queue(queue_id).previous() @api_route("players/:player_id/cmd/power_on") - async def async_cmd_power_on(self, player_id: str) -> None: + async def cmd_power_on(self, player_id: str) -> None: """ Send POWER ON command to given player. :param player_id: player_id of the player to handle the command. """ - player_state = self.get_player_state(player_id) - if not player_state: + player = self.get_player(player_id) + if not player: return - player_config = self.mass.config.player_settings[player_state.player_id] + player_config = self.mass.config.player_settings[player.player_id] # turn on player - await player_state.player.async_cmd_power_on() + await player.cmd_power_on() # player control support if player_config.get(CONF_POWER_CONTROL): control = self.get_player_control(player_config[CONF_POWER_CONTROL]) if control: - await control.async_set_state(True) + await control.set_state(True) @api_route("players/:player_id/cmd/power_off") - async def async_cmd_power_off(self, player_id: str) -> None: + async def cmd_power_off(self, player_id: str) -> None: """ Send POWER OFF command to given player. :param player_id: player_id of the player to handle the command. """ - player_state = self.get_player_state(player_id) - if not player_state: + player = self.get_player(player_id) + if not player: return # send stop if player is playing - if player_state.active_queue == player_id and player_state.state in [ + if player.active_queue == player_id and player.state in [ PlaybackState.Playing, PlaybackState.Paused, ]: - await self.async_cmd_stop(player_id) - player_config = self.mass.config.player_settings[player_state.player_id] + await self.cmd_stop(player_id) + player_config = self.mass.config.player_settings[player.player_id] # turn off player - await player_state.player.async_cmd_power_off() + await player.cmd_power_off() # player control support if player_config.get(CONF_POWER_CONTROL): control = self.get_player_control(player_config[CONF_POWER_CONTROL]) if control: - await control.async_set_state(False) + await control.set_state(False) # handle group power - if player_state.is_group_player: + if player.is_group_player: # player is group, turn off all childs - for child_player_id in player_state.group_childs: + for child_player_id in player.group_childs: child_player = self.get_player(child_player_id) - if child_player and child_player.powered: - self.mass.add_job(self.async_cmd_power_off(child_player_id)) + if child_player and child_player.player_state.powered: + self.mass.add_job(self.cmd_power_off(child_player_id)) else: # if this was the last powered player in the group, turn off group - for parent_player_id in player_state.group_parents: - parent_player = self.get_player_state(parent_player_id) - if not parent_player or not parent_player.powered: + for parent_player_id in player.group_parents: + parent_player = self.get_player(parent_player_id) + if not parent_player or not parent_player.player_state.powered: continue has_powered_players = False for child_player_id in parent_player.group_childs: if child_player_id == player_id: continue - child_player = self.get_player_state(child_player_id) - if child_player and child_player.powered: + child_player = self.get_player(child_player_id) + if child_player and child_player.player_state.powered: has_powered_players = True if not has_powered_players: - self.mass.add_job(self.async_cmd_power_off(parent_player_id)) + self.mass.add_job(self.cmd_power_off(parent_player_id)) @api_route("players/:player_id/cmd/power_toggle") - async def async_cmd_power_toggle(self, player_id: str): + async def 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_state = self.get_player_state(player_id) - if not player_state: + player = self.get_player(player_id) + if not player: return - if player_state.powered: - return await self.async_cmd_power_off(player_id) - return await self.async_cmd_power_on(player_id) + if player.player_state.powered: + return await self.cmd_power_off(player_id) + return await self.cmd_power_on(player_id) @api_route("players/:player_id/cmd/volume_set/:volume_level?") - async def async_cmd_volume_set(self, player_id: str, volume_level: int) -> None: + async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: """ 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). """ - player_state = self.get_player_state(player_id) - if not player_state: + player = self.get_player(player_id) + if not player: return - player_config = self.mass.config.player_settings[player_state.player_id] + player_config = self.mass.config.player_settings[player.player_id] volume_level = try_parse_int(volume_level) if volume_level < 0: volume_level = 0 @@ -542,86 +532,90 @@ class PlayerManager: if player_config.get(CONF_VOLUME_CONTROL): control = self.get_player_control(player_config[CONF_VOLUME_CONTROL]) if control: - await control.async_set_state(volume_level) + await control.set_state(volume_level) # just force full volume on actual player if volume is outsourced to volumecontrol - await player_state.player.async_cmd_volume_set(100) + await player.cmd_volume_set(100) # handle group volume - elif player_state.is_group_player: - cur_volume = player_state.volume_level + elif player.is_group_player: + cur_volume = player.volume_level new_volume = volume_level volume_dif = new_volume - cur_volume if cur_volume == 0: volume_dif_percent = 1 + (new_volume / 100) else: volume_dif_percent = volume_dif / cur_volume - for child_player_id in player_state.group_childs: + for child_player_id in player.group_childs: if child_player_id == player_id: continue - child_player = self.get_player_state(child_player_id) - if child_player and child_player.available and child_player.powered: + child_player = self.get_player(child_player_id) + if ( + child_player + and child_player.available + and child_player.player_state.powered + ): cur_child_volume = child_player.volume_level new_child_volume = cur_child_volume + ( cur_child_volume * volume_dif_percent ) - await self.async_cmd_volume_set(child_player_id, new_child_volume) + await self.cmd_volume_set(child_player_id, new_child_volume) # regular volume command else: - await player_state.player.async_cmd_volume_set(volume_level) + await player.cmd_volume_set(volume_level) @api_route("players/:player_id/cmd/volume_up") - async def async_cmd_volume_up(self, player_id: str): + async def 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_state = self.get_player_state(player_id) - if not player_state: + player = self.get_player(player_id) + if not player: return - if player_state.volume_level <= 10 or player_state.volume_level >= 90: + if player.volume_level <= 10 or player.volume_level >= 90: step_size = 2 else: step_size = 5 - new_level = player_state.volume_level + step_size + new_level = player.volume_level + step_size if new_level > 100: new_level = 100 - return await self.async_cmd_volume_set(player_id, new_level) + return await self.cmd_volume_set(player_id, new_level) @api_route("players/:player_id/cmd/volume_down") - async def async_cmd_volume_down(self, player_id: str): + async def 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_state = self.get_player_state(player_id) - if not player_state: + player = self.get_player(player_id) + if not player: return - if player_state.volume_level <= 10 or player_state.volume_level >= 90: + if player.volume_level <= 10 or player.volume_level >= 90: step_size = 2 else: step_size = 5 - new_level = player_state.volume_level - step_size + new_level = player.volume_level - step_size if new_level < 0: new_level = 0 - return await self.async_cmd_volume_set(player_id, new_level) + return await self.cmd_volume_set(player_id, new_level) @api_route("players/:player_id/cmd/volume_mute/:is_muted") - async def async_cmd_volume_mute(self, player_id: str, is_muted: bool = False): + async def cmd_volume_mute(self, player_id: str, is_muted: bool = 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. """ - player_state = self.get_player_state(player_id) - if not player_state: + player = self.get_player(player_id) + if not player: return # TODO: handle mute on volumecontrol? - return await player_state.player.async_cmd_volume_mute(is_muted) + return await player.cmd_volume_mute(is_muted) @api_route("players/:queue_id/queue/cmd/shuffle_enabled/:enable_shuffle?") - async def async_player_queue_cmd_set_shuffle( + async def player_queue_cmd_set_shuffle( self, queue_id: str, enable_shuffle: bool = False ): """ @@ -633,10 +627,10 @@ class PlayerManager: player_queue = self.get_player_queue(queue_id) if not player_queue: return - return await player_queue.async_set_shuffle_enabled(enable_shuffle) + return await player_queue.set_shuffle_enabled(enable_shuffle) @api_route("players/:queue_id/queue/cmd/repeat_enabled/:enable_repeat?") - async def async_player_queue_cmd_set_repeat( + async def player_queue_cmd_set_repeat( self, queue_id: str, enable_repeat: bool = False ): """ @@ -648,10 +642,10 @@ class PlayerManager: player_queue = self.get_player_queue(queue_id) if not player_queue: return - return await player_queue.async_set_repeat_enabled(enable_repeat) + return await player_queue.set_repeat_enabled(enable_repeat) @api_route("players/:queue_id/queue/cmd/next") - async def async_player_queue_cmd_next(self, queue_id: str): + async def player_queue_cmd_next(self, queue_id: str): """ Send next track command to given playerqueue. @@ -660,10 +654,10 @@ class PlayerManager: player_queue = self.get_player_queue(queue_id) if not player_queue: return - return await player_queue.async_next() + return await player_queue.next() @api_route("players/:queue_id/queue/cmd/previous") - async def async_player_queue_cmd_previous(self, queue_id: str): + async def player_queue_cmd_previous(self, queue_id: str): """ Send previous track command to given playerqueue. @@ -672,10 +666,10 @@ class PlayerManager: player_queue = self.get_player_queue(queue_id) if not player_queue: return - return await player_queue.async_previous() + return await player_queue.previous() @api_route("players/:queue_id/queue/cmd/move/:queue_item_id?/:pos_shift?") - async def async_player_queue_cmd_move_item( + async def player_queue_cmd_move_item( self, queue_id: str, queue_item_id: str, pos_shift: int = 1 ): """ @@ -688,20 +682,18 @@ class PlayerManager: player_queue = self.get_player_queue(queue_id) if not player_queue: return - return await player_queue.async_move_item(queue_item_id, pos_shift) + return await player_queue.move_item(queue_item_id, pos_shift) @api_route("players/:queue_id/queue/cmd/play_index/:index?") - async def async_play_index(self, queue_id: str, index: Union[int, str]) -> None: + async def play_index(self, queue_id: str, index: Union[int, str]) -> None: """Play item at index (or item_id) X in queue.""" player_queue = self.get_player_queue(queue_id) if not player_queue: return - return await player_queue.async_play_index(index) + return await player_queue.play_index(index) @api_route("players/:queue_id/queue/cmd/clear") - async def async_player_queue_cmd_clear( - self, queue_id: str, enable_repeat: bool = False - ): + async def player_queue_cmd_clear(self, queue_id: str, enable_repeat: bool = False): """ Clear all items in player's queue. @@ -710,20 +702,18 @@ class PlayerManager: player_queue = self.get_player_queue(queue_id) if not player_queue: return - return await player_queue.async_clear() + return await player_queue.clear() # OTHER/HELPER FUNCTIONS - async def async_get_gain_correct( - self, player_id: str, item_id: str, provider_id: str - ): + async def 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( + track_loudness = await self.mass.database.get_track_loudness( item_id, provider_id ) if track_loudness is None: diff --git a/music_assistant/managers/streams.py b/music_assistant/managers/streams.py index 0ca98e6d..4900bb84 100755 --- a/music_assistant/managers/streams.py +++ b/music_assistant/managers/streams.py @@ -21,7 +21,7 @@ from music_assistant.constants import ( EVENT_STREAM_STARTED, ) from music_assistant.helpers.process import AsyncProcess -from music_assistant.helpers.typing import MusicAssistantType +from music_assistant.helpers.typing import MusicAssistant from music_assistant.helpers.util import create_tempfile, get_ip from music_assistant.models.streamdetails import ContentType, StreamDetails, StreamType @@ -42,13 +42,13 @@ class SoxOutputFormat(Enum): class StreamManager: """Built-in streamer utilizing SoX.""" - def __init__(self, mass: MusicAssistantType) -> None: + def __init__(self, mass: MusicAssistant) -> None: """Initialize class.""" self.mass = mass self.local_ip = get_ip() self.analyze_jobs = {} - async def async_get_sox_stream( + async def get_sox_stream( self, streamdetails: StreamDetails, output_format: SoxOutputFormat = SoxOutputFormat.FLAC, @@ -86,7 +86,7 @@ class StreamManager: async def fill_buffer(): """Forward audio chunks to sox stdin.""" # feed audio data into sox stdin for processing - async for chunk in self.async_get_media_stream(streamdetails): + async for chunk in self.get_media_stream(streamdetails): await sox_proc.write(chunk) await sox_proc.write_eof() @@ -109,7 +109,7 @@ class StreamManager: streamdetails.item_id, ) - async def async_queue_stream_flac(self, player_id) -> AsyncGenerator[bytes, None]: + async def queue_stream_flac(self, player_id) -> AsyncGenerator[bytes, None]: """Stream the PlayerQueue's tracks as constant feed in flac format.""" player_conf = self.mass.config.get_player_config(player_id) sample_rate = player_conf.get(CONF_MAX_SAMPLE_RATE, 96000) @@ -132,9 +132,7 @@ class StreamManager: # feed stdin with pcm samples async def fill_buffer(): """Feed audio data into sox stdin for processing.""" - async for chunk in self.async_queue_stream_pcm( - player_id, sample_rate, 32 - ): + async for chunk in self.queue_stream_pcm(player_id, sample_rate, 32): await sox_proc.write(chunk) fill_buffer_task = self.mass.loop.create_task(fill_buffer()) @@ -144,7 +142,7 @@ class StreamManager: yield chunk await asyncio.wait([fill_buffer_task]) - async def async_queue_stream_pcm( + async def queue_stream_pcm( self, player_id, sample_rate=96000, bit_depth=32 ) -> AsyncGenerator[bytes, None]: """Stream the PlayerQueue's tracks as constant feed in PCM raw audio.""" @@ -159,9 +157,9 @@ class StreamManager: # get the (next) track in queue if queue_index is None: # report start of queue playback so we can calculate current track/duration etc. - queue_index = await player_queue.async_queue_stream_start() + queue_index = await player_queue.queue_stream_start() else: - queue_index = await player_queue.async_queue_stream_next(queue_index) + queue_index = await player_queue.queue_stream_next(queue_index) queue_track = player_queue.get_item(queue_index) if not queue_track: LOGGER.info("no (more) tracks left in queue") @@ -174,11 +172,11 @@ class StreamManager: buffer_size = sample_size * fade_length if fade_length else sample_size * 10 # get streamdetails - streamdetails = await self.mass.music.async_get_stream_details( + streamdetails = await self.mass.music.get_stream_details( queue_track, player_id ) # get gain correct / replaygain - gain_correct = await self.mass.players.async_get_gain_correct( + gain_correct = await self.mass.players.get_gain_correct( player_id, streamdetails.item_id, streamdetails.provider ) streamdetails.gain_correct = gain_correct @@ -194,7 +192,7 @@ class StreamManager: prev_chunk = None bytes_written = 0 # handle incoming audio chunks - async for is_last_chunk, chunk in self.mass.streams.async_get_sox_stream( + async for is_last_chunk, chunk in self.mass.streams.get_sox_stream( streamdetails, SoxOutputFormat.S32, resample=sample_rate, @@ -207,7 +205,7 @@ class StreamManager: if not chunk and bytes_written == 0: # stream error: got empy first chunk # prevent player queue get stuck by sending next track command - self.mass.add_job(player_queue.async_next()) + self.mass.add_job(player_queue.next()) LOGGER.error("Stream error on track %s", queue_track.item_id) return if cur_chunk <= 2 and not last_fadeout_data: @@ -221,7 +219,7 @@ class StreamManager: # HANDLE CROSSFADE OF PREVIOUS TRACK FADE_OUT AND THIS TRACK FADE_IN elif cur_chunk == 2 and last_fadeout_data: # combine the first 2 chunks and strip off silence - first_part = await async_strip_silence(prev_chunk + chunk, pcm_args) + first_part = await strip_silence(prev_chunk + chunk, pcm_args) if len(first_part) < buffer_size: # part is too short after the strip action?! # so we just use the full first part @@ -230,7 +228,7 @@ class StreamManager: remaining_bytes = first_part[buffer_size:] del first_part # do crossfade - crossfade_part = await async_crossfade_pcm_parts( + crossfade_part = await crossfade_pcm_parts( fade_in_part, last_fadeout_data, pcm_args, fade_length ) # send crossfade_part @@ -250,9 +248,7 @@ class StreamManager: # last chunk received so create the last_part # with the previous chunk and this chunk # and strip off silence - last_part = await async_strip_silence( - prev_chunk + chunk, pcm_args, True - ) + last_part = await strip_silence(prev_chunk + chunk, pcm_args, True) if len(last_part) < buffer_size: # part is too short after the strip action # so we just use the entire original data @@ -313,7 +309,7 @@ class StreamManager: self.mass.add_job(gc.collect) LOGGER.info("streaming of queue for player %s completed", player_id) - async def async_stream_queue_item( + async def stream_queue_item( self, player_id: str, queue_item_id: str ) -> AsyncGenerator[bytes, None]: """Stream a single Queue item.""" @@ -324,32 +320,30 @@ class StreamManager: queue_item = player_queue.by_item_id(queue_item_id) if not queue_item: raise FileNotFoundError("invalid queue_item_id") - streamdetails = await self.mass.music.async_get_stream_details( - queue_item, player_id - ) + streamdetails = await self.mass.music.get_stream_details(queue_item, player_id) # get gain correct / replaygain - gain_correct = await self.mass.players.async_get_gain_correct( + gain_correct = await self.mass.players.get_gain_correct( player_id, streamdetails.item_id, streamdetails.provider ) streamdetails.gain_correct = gain_correct # start streaming LOGGER.debug("Start streaming %s (%s)", queue_item_id, queue_item.name) - async for _, audio_chunk in self.async_get_sox_stream( + async for _, audio_chunk in self.get_sox_stream( streamdetails, gain_db_adjust=gain_correct, chunk_size=4000000 ): yield audio_chunk LOGGER.debug("Finished streaming %s (%s)", queue_item_id, queue_item.name) - async def async_get_media_stream( + async def get_media_stream( self, streamdetails: StreamDetails ) -> AsyncGenerator[bytes, None]: """Get the (original/untouched) audio data for the given streamdetails. Generator.""" stream_path = streamdetails.path stream_type = StreamType(streamdetails.type) audio_data = b"" - track_loudness = await self.mass.database.async_get_track_loudness( + track_loudness = await self.mass.database.get_track_loudness( streamdetails.item_id, streamdetails.provider ) needs_analyze = track_loudness is None @@ -397,7 +391,7 @@ class StreamManager: streamdetails.provider, streamdetails.item_id, ) - await self.mass.database.async_mark_item_played( + await self.mass.database.mark_item_played( streamdetails.item_id, streamdetails.provider ) @@ -416,7 +410,7 @@ class StreamManager: # get track loudness track_loudness = self.mass.add_job( - self.mass.database.async_get_track_loudness( + self.mass.database.get_track_loudness( streamdetails.item_id, streamdetails.provider ) ).result() @@ -433,7 +427,7 @@ class StreamManager: ) loudness = float(value.decode().strip()) self.mass.add_job( - self.mass.database.async_set_track_loudness( + self.mass.database.set_track_loudness( streamdetails.item_id, streamdetails.provider, loudness ) ) @@ -442,7 +436,7 @@ class StreamManager: self.analyze_jobs.pop(item_key, None) -async def async_crossfade_pcm_parts( +async def crossfade_pcm_parts( fade_in_part: bytes, fade_out_part: bytes, pcm_args: List[str], fade_length: int ) -> bytes: """Crossfade two chunks of pcm/raw audio using sox.""" @@ -471,9 +465,7 @@ async def async_crossfade_pcm_parts( return crossfade_part -async def async_strip_silence( - audio_data: bytes, pcm_args: List[str], reverse=False -) -> bytes: +async def strip_silence(audio_data: bytes, pcm_args: List[str], reverse=False) -> bytes: """Strip silence from (a chunk of) pcm audio.""" args = ["sox", "--ignore-length", "-t"] + pcm_args + ["-", "-t"] + pcm_args + ["-"] if reverse: diff --git a/music_assistant/mass.py b/music_assistant/mass.py index 0823fc6d..7fe71d70 100644 --- a/music_assistant/mass.py +++ b/music_assistant/mass.py @@ -6,7 +6,7 @@ import importlib import logging import os import threading -from typing import Any, Awaitable, Callable, Coroutine, Dict, List, Optional, Union +from typing import Any, Awaitable, Callable, Coroutine, Dict, Optional, Tuple, Union import aiohttp from music_assistant.constants import ( @@ -37,13 +37,16 @@ def global_exception_handler(loop: asyncio.AbstractEventLoop, context: Dict) -> LOGGER.exception( "Caught exception: %s", context.get("exception", context["message"]) ) + if "Broken pipe" in str(context.get("exception")): + # fix for the spamming subprocess + return loop.default_exception_handler(context) class MusicAssistant: """Main MusicAssistant object.""" - def __init__(self, datapath: str, debug: bool = False, port: int = 8095): + def __init__(self, datapath: str, debug: bool = False, port: int = 8095) -> None: """ Create an instance of MusicAssistant. @@ -71,7 +74,7 @@ class MusicAssistant: # shared zeroconf instance self.zeroconf = Zeroconf(interfaces=InterfaceChoice.All) - async def async_start(self): + async def start(self) -> None: """Start running the music assistant server.""" # initialize loop self._loop = asyncio.get_event_loop() @@ -84,26 +87,26 @@ class MusicAssistant: ) # run migrations if needed await check_migrations(self) - await self._config.async_setup() - await self._cache.async_setup() - await self._music.async_setup() - await self._players.async_setup() - await self.__async_preload_providers() - await self.async_setup_discovery() - await self._web.async_setup() - await self._library.async_setup() + await self._config.setup() + await self._cache.setup() + await self._music.setup() + await self._players.setup() + await self._preload_providers() + await self.setup_discovery() + await self._web.setup() + await self._library.setup() self.loop.create_task(self.__process_background_tasks()) - async def async_stop(self): + async def stop(self) -> None: """Stop running the music assistant server.""" self._exit = True LOGGER.info("Application shutdown") self.signal_event(EVENT_SHUTDOWN) - await self.config.async_close() - await self._web.async_stop() + await self.config.close() + await self._web.stop() for prov in self._providers.values(): - await prov.async_on_stop() - await self._players.async_close() + await prov.on_stop() + await self._players.close() await self._http_session.connector.close() self._http_session.detach() @@ -167,7 +170,7 @@ class MusicAssistant: """Return the default http session.""" return self._http_session - async def async_register_provider(self, provider: Provider) -> None: + async def register_provider(self, provider: Provider) -> None: """Register a new Provider/Plugin.""" assert provider.id and provider.name if provider.id in self._providers: @@ -177,7 +180,7 @@ class MusicAssistant: provider.available = False self._providers[provider.id] = provider if self.config.get_provider_config(provider.id, provider.type)[CONF_ENABLED]: - if await provider.async_on_start() is not False: + if await provider.on_start() is not False: provider.available = True LOGGER.debug("Provider registered: %s", provider.name) self.signal_event(EVENT_PROVIDER_REGISTERED, provider.id) @@ -188,24 +191,24 @@ class MusicAssistant: else: LOGGER.debug("Not loading provider %s as it is disabled", provider.name) - async def async_unregister_provider(self, provider_id: str) -> None: + async def unregister_provider(self, provider_id: str) -> None: """Unregister an existing Provider/Plugin.""" if provider_id in self._providers: # unload it if it's loaded - await self._providers[provider_id].async_on_stop() + await self._providers[provider_id].on_stop() LOGGER.debug("Provider unregistered: %s", provider_id) self.signal_event(EVENT_PROVIDER_UNREGISTERED, provider_id) return self._providers.pop(provider_id, None) - async def async_reload_provider(self, provider_id: str) -> None: + async def reload_provider(self, provider_id: str) -> None: """Reload an existing Provider/Plugin.""" - provider = await self.async_unregister_provider(provider_id) + provider = await self.unregister_provider(provider_id) if provider is not None: # simply re-register the same provider again - await self.async_register_provider(provider) + await self.register_provider(provider) else: # try preloading all providers - self.add_job(self.__async_preload_providers()) + self.add_job(self._preload_providers()) @callback def get_provider(self, provider_id: str) -> Provider: @@ -220,14 +223,14 @@ class MusicAssistant: self, filter_type: Optional[ProviderType] = None, include_unavailable: bool = False, - ) -> List[Provider]: + ) -> Tuple[Provider]: """Return all providers, optionally filtered by type.""" - return [ + return ( item for item in self._providers.values() if (filter_type is None or item.type == filter_type) and (include_unavailable or item.available) - ] + ) @callback def signal_event(self, event_msg: str, event_details: Any = None) -> None: @@ -245,7 +248,7 @@ class MusicAssistant: def add_event_listener( self, cb_func: Callable[..., Union[None, Awaitable]], - event_filter: Union[None, str, List] = None, + event_filter: Union[None, str, Tuple] = None, ) -> Callable: """ Add callback to event listeners. @@ -321,10 +324,10 @@ class MusicAssistant: await task await asyncio.sleep(1) - async def async_setup_discovery(self) -> None: + async def setup_discovery(self) -> None: """Make this Music Assistant instance discoverable on the network.""" - def setup_discovery(): + def _setup_discovery(): zeroconf_type = "_music-assistant._tcp.local." info = ServiceInfo( @@ -348,9 +351,9 @@ class MusicAssistant: "Music Assistant instance with identical name present in the local network!" ) - self.add_job(setup_discovery) + self.add_job(_setup_discovery) - async def __async_preload_providers(self): + async def _preload_providers(self) -> None: """Dynamically load all providermodules.""" base_dir = os.path.dirname(os.path.abspath(__file__)) modules_path = os.path.join(base_dir, "providers") @@ -374,7 +377,7 @@ class MusicAssistant: prov_mod = importlib.import_module( f".{module_name}", "music_assistant.providers" ) - await prov_mod.async_setup(self) + await prov_mod.setup(self) # pylint: disable=broad-except except Exception as exc: LOGGER.exception("Error preloading module %s: %s", module_name, exc) diff --git a/music_assistant/models/media_types.py b/music_assistant/models/media_types.py index 9add6423..b505ca9c 100755 --- a/music_assistant/models/media_types.py +++ b/music_assistant/models/media_types.py @@ -2,7 +2,7 @@ from dataclasses import dataclass, field from enum import Enum, IntEnum -from typing import Any, List, Mapping +from typing import Any, Dict, List, Mapping, Set import ujson from mashumaro import DataClassDictMixin @@ -59,6 +59,10 @@ class MediaItemProviderId(DataClassDictMixin): details: str = None available: bool = True + def __hash__(self): + """Return custom hash.""" + return hash((self.provider, self.item_id, self.quality)) + @dataclass class MediaItem(DataClassDictMixin): @@ -67,8 +71,8 @@ class MediaItem(DataClassDictMixin): item_id: str = "" provider: str = "" name: str = "" - metadata: Any = field(default_factory=dict) - provider_ids: List[MediaItemProviderId] = field(default_factory=list) + metadata: Dict[str, Any] = field(default_factory=dict) + provider_ids: Set[MediaItemProviderId] = field(default_factory=set) in_library: bool = False media_type: MediaType = MediaType.Track @@ -115,18 +119,24 @@ class MediaItem(DataClassDictMixin): @property def available(self): """Return (calculated) availability.""" - for item in self.provider_ids: - if item.available: - return True + return any(x.available for x in self.provider_ids) + + def __hash__(self): + """Return custom hash.""" + return hash((self.media_type, self.provider, self.item_id)) @dataclass -class Artist(MediaItem): +class Artist(MediaItem, DataClassDictMixin): """Model for an artist.""" media_type: MediaType = MediaType.Artist musicbrainz_id: str = "" + def __hash__(self): + """Return custom hash.""" + return hash((self.media_type, self.provider, self.item_id)) + @dataclass class ItemMapping(DataClassDictMixin): @@ -142,9 +152,13 @@ class ItemMapping(DataClassDictMixin): """Create ItemMapping object from regular item.""" return cls.from_dict(item.to_dict()) + def __hash__(self): + """Return custom hash.""" + return hash((self.media_type, self.provider, self.item_id)) + @dataclass -class Album(MediaItem): +class Album(MediaItem, DataClassDictMixin): """Model for an album.""" media_type: MediaType = MediaType.Album @@ -154,6 +168,10 @@ class Album(MediaItem): album_type: AlbumType = AlbumType.Unknown upc: str = "" + def __hash__(self): + """Return custom hash.""" + return hash((self.media_type, self.provider, self.item_id)) + @dataclass class FullAlbum(Album): @@ -161,6 +179,10 @@ class FullAlbum(Album): artist: Artist = None + def __hash__(self): + """Return custom hash.""" + return hash((self.media_type, self.provider, self.item_id)) + @dataclass class Track(MediaItem): @@ -170,8 +192,8 @@ class Track(MediaItem): duration: int = 0 version: str = "" isrc: str = "" - artists: List[ItemMapping] = field(default_factory=list) - albums: List[ItemMapping] = field(default_factory=list) + artists: Set[ItemMapping] = field(default_factory=set) + albums: Set[ItemMapping] = field(default_factory=set) # album track only album: ItemMapping = None disc_number: int = 0 @@ -179,15 +201,23 @@ class Track(MediaItem): # playlist track only position: int = 0 + def __hash__(self): + """Return custom hash.""" + return hash((self.media_type, self.provider, self.item_id)) + @dataclass class FullTrack(Track): """Model for an album with full details.""" - artists: List[Artist] = field(default_factory=list) - albums: List[Album] = field(default_factory=list) + artists: Set[Artist] = field(default_factory=set) + albums: Set[Album] = field(default_factory=set) album: Album = None + def __hash__(self): + """Return custom hash.""" + return hash((self.media_type, self.provider, self.item_id)) + @dataclass class Playlist(MediaItem): @@ -198,6 +228,10 @@ class Playlist(MediaItem): checksum: str = "" # some value to detect playlist track changes is_editable: bool = False + def __hash__(self): + """Return custom hash.""" + return hash((self.media_type, self.provider, self.item_id)) + @dataclass class Radio(MediaItem): @@ -206,6 +240,10 @@ class Radio(MediaItem): media_type: MediaType = MediaType.Radio duration: int = 86400 + def __hash__(self): + """Return custom hash.""" + return hash((self.media_type, self.provider, self.item_id)) + @dataclass class SearchResult(DataClassDictMixin): diff --git a/music_assistant/models/player.py b/music_assistant/models/player.py index 56c5c5e8..c2cdb3cb 100755 --- a/music_assistant/models/player.py +++ b/music_assistant/models/player.py @@ -1,12 +1,20 @@ """Models and helpers for a player.""" from abc import abstractmethod -from dataclasses import dataclass +from dataclasses import dataclass, field +from datetime import datetime from enum import Enum, IntEnum -from typing import Any, List, Optional +from typing import Any, Optional, Set from mashumaro import DataClassDictMixin -from music_assistant.helpers.typing import MusicAssistantType, QueueItems +from music_assistant.constants import ( + CONF_ENABLED, + CONF_NAME, + CONF_POWER_CONTROL, + CONF_VOLUME_CONTROL, + EVENT_PLAYER_CHANGED, +) +from music_assistant.helpers.typing import ConfigSubItem, MusicAssistant, QueueItems from music_assistant.helpers.util import callback from music_assistant.models.config_entry import ConfigEntry @@ -37,11 +45,77 @@ class PlayerFeature(IntEnum): CROSSFADE = 2 +class PlayerControlType(Enum): + """Enum with different player control types.""" + + POWER = 0 + VOLUME = 1 + UNKNOWN = 99 + + +@dataclass +class PlayerControl(DataClassDictMixin): + """ + Model for a player control. + + Allows for a plugin-like + structure to override common player commands. + """ + + # pylint: disable=no-member + + type: PlayerControlType = PlayerControlType.UNKNOWN + control_id: str = "" + provider: str = "" + name: str = "" + state: Any = None + + async def set_state(self, new_state: Any) -> None: + """Handle command to set the state for a player control.""" + # by default we just signal an event on the eventbus + # pickup this event (e.g. from the websocket api) + # or override this method with your own implementation. + + self.mass.signal_event(f"players/controls/{self.control_id}/state", new_state) + + +@dataclass +class PlayerState(DataClassDictMixin): + """Model for a (calculated) player state.""" + + player_id: str = None + provider_id: str = None + name: str = None + powered: bool = False + state: PlaybackState = PlaybackState.Off + available: bool = False + volume_level: int = 0 + elapsed_time: int = 0 + muted: bool = False + is_group_player: bool = False + group_childs: Set[str] = field(default_factory=set) + device_info: DeviceInfo = field(default_factory=DeviceInfo) + updated_at: datetime = None + group_parents: Set[str] = field(default_factory=set) + features: Set[PlayerFeature] = field(default_factory=set) + active_queue: str = None + + def update(self, new_obj: "PlayerState") -> Set[str]: + """Update state from other PlayerState instance and return changed keys.""" + changed_keys = set() + # pylint: disable=no-member + for key in self.__dataclass_fields__.keys(): + new_val = getattr(new_obj, key) + if getattr(self, key) != new_val: + setattr(self, key, new_val) + if key != "updated_at": + changed_keys.add(key) + return changed_keys + + class Player: """Model for a music player.""" - mass: MusicAssistantType = None # will be set by player manager - # Public properties: should be overriden with provider specific implementation @property @@ -120,9 +194,9 @@ class Player: return False @property - def group_childs(self) -> List[str]: + def group_childs(self) -> Set[str]: """Return list of child player id's if player is a group player.""" - return [] + return {} @property def device_info(self) -> DeviceInfo: @@ -135,28 +209,28 @@ class Player: return False @property - def features(self) -> List[PlayerFeature]: + def features(self) -> Set[PlayerFeature]: """Return list of features this player supports.""" - return [] + return {} @property - def config_entries(self) -> List[ConfigEntry]: + def config_entries(self) -> Set[ConfigEntry]: """Return player specific config entries (if any).""" - return [] + return {} # Public methods / player commands: should be overriden with provider specific implementation - async def async_on_update(self) -> None: + async def on_poll(self) -> None: """Call when player is periodically polled by the player manager (should_poll=True).""" self.update_state() - async def async_on_add(self) -> None: + async def on_add(self) -> None: """Call when player is added to the player manager.""" - async def async_on_remove(self) -> None: + async def on_remove(self) -> None: """Call when player is removed from the player manager.""" - async def async_cmd_play_uri(self, uri: str) -> None: + async def cmd_play_uri(self, uri: str) -> None: """ Play the specified uri/url on the player. @@ -164,35 +238,35 @@ class Player: """ raise NotImplementedError - async def async_cmd_stop(self) -> None: + async def cmd_stop(self) -> None: """Send STOP command to player.""" raise NotImplementedError - async def async_cmd_play(self) -> None: + async def cmd_play(self) -> None: """Send PLAY command to player.""" raise NotImplementedError - async def async_cmd_pause(self) -> None: + async def cmd_pause(self) -> None: """Send PAUSE command to player.""" raise NotImplementedError - async def async_cmd_next(self) -> None: + async def cmd_next(self) -> None: """Send NEXT TRACK command to player.""" raise NotImplementedError - async def async_cmd_previous(self) -> None: + async def cmd_previous(self) -> None: """Send PREVIOUS TRACK command to player.""" raise NotImplementedError - async def async_cmd_power_on(self) -> None: + async def cmd_power_on(self) -> None: """Send POWER ON command to player.""" raise NotImplementedError - async def async_cmd_power_off(self) -> None: + async def cmd_power_off(self) -> None: """Send POWER OFF command to player.""" raise NotImplementedError - async def async_cmd_volume_set(self, volume_level: int) -> None: + async def cmd_volume_set(self, volume_level: int) -> None: """ Send volume level command to player. @@ -200,7 +274,7 @@ class Player: """ raise NotImplementedError - async def async_cmd_volume_mute(self, is_muted: bool = False) -> None: + async def cmd_volume_mute(self, is_muted: bool = False) -> None: """ Send volume MUTE command to given player. @@ -210,7 +284,7 @@ class Player: # OPTIONAL: QUEUE SERVICE CALLS/COMMANDS - OVERRIDE ONLY IF SUPPORTED BY PROVIDER - async def async_cmd_queue_play_index(self, index: int) -> None: + async def cmd_queue_play_index(self, index: int) -> None: """ Play item at index X on player's queue. @@ -219,7 +293,7 @@ class Player: if PlayerFeature.QUEUE in self.features: raise NotImplementedError - async def async_cmd_queue_load(self, queue_items: QueueItems) -> None: + async def cmd_queue_load(self, queue_items: QueueItems) -> None: """ Load/overwrite given items in the player's queue implementation. @@ -228,7 +302,7 @@ class Player: if PlayerFeature.QUEUE in self.features: raise NotImplementedError - async def async_cmd_queue_insert( + async def cmd_queue_insert( self, queue_items: QueueItems, insert_at_index: int ) -> None: """ @@ -241,7 +315,7 @@ class Player: if PlayerFeature.QUEUE in self.features: raise NotImplementedError - async def async_cmd_queue_append(self, queue_items: QueueItems) -> None: + async def cmd_queue_append(self, queue_items: QueueItems) -> None: """ Append new items at the end of the queue. @@ -250,7 +324,7 @@ class Player: if PlayerFeature.QUEUE in self.features: raise NotImplementedError - async def async_cmd_queue_update(self, queue_items: QueueItems) -> None: + async def cmd_queue_update(self, queue_items: QueueItems) -> None: """ Overwrite the existing items in the queue, used for reordering. @@ -259,48 +333,189 @@ class Player: if PlayerFeature.QUEUE in self.features: raise NotImplementedError - async def async_cmd_queue_clear(self) -> None: + async def cmd_queue_clear(self) -> None: """Clear the player's queue.""" if PlayerFeature.QUEUE in self.features: raise NotImplementedError - # Do not override below this point + # Private properties and methods + # Do not override below this point! - @callback - def update_state(self) -> None: - """Call to store current player state in the player manager.""" - self.mass.add_job(self.mass.players.async_update_player(self)) + @property + def active_queue(self) -> str: + """Return the active parent player/queue for a player.""" + return self._cur_state.active_queue or self.player_id + @property + def group_parents(self) -> Set[str]: + """Return all groups this player belongs to.""" + return self._cur_state.group_parents -class PlayerControlType(Enum): - """Enum with different player control types.""" + @property + def config(self) -> ConfigSubItem: + """Return this player's configuration.""" + return self.mass.config.get_player_config(self.player_id) - POWER = 0 - VOLUME = 1 - UNKNOWN = 99 + @property + def enabled(self): + """Return True if this player is enabled.""" + return self.config[CONF_ENABLED] + @property + def power_control(self) -> Optional[PlayerControl]: + """Return this player's Power Control.""" + player_control_conf = self.config.get(CONF_POWER_CONTROL) + if player_control_conf: + return self.mass.players.get_player_control(player_control_conf) + return None -@dataclass -class PlayerControl(DataClassDictMixin): - """ - Model for a player control. + @property + def volume_control(self) -> Optional[PlayerControl]: + """Return this player's Volume Control.""" + player_control_conf = self.config.get(CONF_VOLUME_CONTROL) + if player_control_conf: + return self.mass.players.get_player_control(player_control_conf) + return None - Allows for a plugin-like - structure to override common player commands. - """ + @property + def player_state(self) -> PlayerState: + """Return calculated/final state for this player.""" + return self._cur_state - # pylint: disable=no-member + @callback + def update_state(self) -> None: + """Call to update current player state in the player manager.""" + if not self.added_to_mass: + if self.enabled: + # player is now enabled and can be added + self.mass.add_job(self.mass.players.add_player(self)) + return + new_state = self.create_state() + changed_keys = self._cur_state.update(new_state) + # basic throttle: do not send state changed events if player did not change + if not changed_keys: + return + self._cur_state = new_state + # always update the player queue + player_queue = self.mass.players.get_player_queue(self.active_queue) + if player_queue: + self.mass.add_job(player_queue.update_state) + if len(changed_keys) == 1 and "elapsed_time" in changed_keys: + # no need to send player update if only the elapsed time changes + # this is already handled by the queue manager + return + self.mass.signal_event(EVENT_PLAYER_CHANGED, new_state) + # update group player childs when parent updates + for child_player_id in self.group_childs: + self.mass.add_job(self.mass.players.trigger_player_update(child_player_id)) + # update group player when child updates + for group_player_id in self._cur_state.group_parents: + self.mass.add_job(self.mass.players.trigger_player_update(group_player_id)) - type: PlayerControlType = PlayerControlType.UNKNOWN - control_id: str = "" - provider: str = "" - name: str = "" - state: Any = None + @callback + def _get_name(self) -> str: + """Return final/calculated player name.""" + conf_name = self.config.get(CONF_NAME) + return conf_name if conf_name else self.name - async def async_set_state(self, new_state: Any) -> None: - """Handle command to set the state for a player control.""" - # by default we just signal an event on the eventbus - # pickup this event (e.g. from the websocket api) - # or override this method with your own implementation. + @callback + def _get_powered(self) -> bool: + """Return final/calculated player's power state.""" + if not self.available or not self.enabled: + return False + power_control = self.power_control + if power_control: + return power_control.state + return self.powered - self.mass.signal_event(f"players/controls/{self.control_id}/state", new_state) + @callback + def _get_state(self) -> PlaybackState: + """Return final/calculated player's playback state.""" + if self.powered and self.active_queue != self.player_id: + # use group state + return self.mass.players.get_player(self.active_queue).state + if self.state == PlaybackState.Stopped and not self.powered: + return PlaybackState.Off + return self.state + + @callback + def _get_available(self) -> bool: + """Return current availablity of player.""" + return False if not self.enabled else self.available + + @callback + def _get_volume_level(self) -> int: + """Return final/calculated player's volume_level.""" + if not self.available or not self.enabled: + return 0 + # handle volume control + volume_control = self.volume_control + if volume_control: + return volume_control.state + # handle group volume + if self.is_group_player: + group_volume = 0 + active_players = 0 + for child_player_id in self.group_childs: + child_player = self.mass.players.get_player(child_player_id) + if child_player: + group_volume += child_player.player_state.volume_level + active_players += 1 + if active_players: + group_volume = group_volume / active_players + return int(group_volume) + return int(self.volume_level) + + @callback + def _get_group_parents(self) -> Set[str]: + """Return all group players this player belongs to.""" + if self.is_group_player: + return {} + return { + player.player_id + for player in self.mass.players + if player.is_group_player and self.player_id in player.group_childs + } + + @callback + def _get_active_queue(self) -> str: + """Return the active parent player/queue for a player.""" + # if a group is powered on, all of it's childs will have/use + # the parent's player's queue. + for group_player_id in self.group_parents: + group_player = self.mass.players.get_player(group_player_id) + if group_player and group_player.powered: + return group_player_id + return self.player_id + + @callback + def create_state(self) -> PlayerState: + """Create PlayerState.""" + return PlayerState( + player_id=self.player_id, + provider_id=self.provider_id, + name=self._get_name(), + powered=self._get_powered(), + state=self.state, + available=self._get_available(), + volume_level=self._get_volume_level(), + elapsed_time=self.elapsed_time, + muted=self.muted, + is_group_player=self.is_group_player, + group_childs=self.group_childs, + device_info=self.device_info, + group_parents=self._get_group_parents(), + features=self.features, + active_queue=self._get_active_queue(), + updated_at=datetime.now(), + ) + + def to_dict(self) -> dict: + """Return playerstate for compatability with json serializer.""" + return self._cur_state.to_dict() + + def __init__(self, *args, **kwargs) -> None: + """Initialize a Player instance.""" + self.mass: Optional[MusicAssistant] = None + self.added_to_mass = False + self._cur_state = PlayerState() diff --git a/music_assistant/models/player_queue.py b/music_assistant/models/player_queue.py index 7e8db0a1..8f6fd502 100755 --- a/music_assistant/models/player_queue.py +++ b/music_assistant/models/player_queue.py @@ -6,7 +6,7 @@ import time import uuid from dataclasses import dataclass from enum import Enum -from typing import List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple, Union from music_assistant.constants import ( CONF_CROSSFADE_DURATION, @@ -15,10 +15,10 @@ from music_assistant.constants import ( EVENT_QUEUE_UPDATED, ) from music_assistant.helpers.typing import ( - MusicAssistantType, + MusicAssistant, OptionalInt, OptionalStr, - PlayerType, + Player, ) from music_assistant.helpers.util import callback from music_assistant.models.media_types import Radio, Track @@ -62,7 +62,7 @@ class QueueItem(Track): class PlayerQueue: """Class that holds the queue items for a player.""" - def __init__(self, mass: MusicAssistantType, player_id: str) -> None: + def __init__(self, mass: MusicAssistant, player_id: str) -> None: """Initialize class.""" self.mass = mass self._queue_id = player_id @@ -74,25 +74,20 @@ class PlayerQueue: self._last_item = None self._queue_stream_start_index = 0 self._queue_stream_next_index = 0 - self._last_player_state = PlaybackState.Stopped + self._last_player = PlaybackState.Stopped # load previous queue settings from disk - self.mass.add_job(self.__async_restore_saved_state()) + self.mass.add_job(self._restore_saved_state()) - async def async_close(self) -> None: + async def close(self) -> None: """Handle shutdown/close.""" # pylint: disable=unused-argument - await self.__async_save_state() + await self._save_state() @property - def player(self) -> PlayerType: + def player(self) -> Player: """Return handle to (master) player of this queue.""" return self.mass.players.get_player(self._queue_id) - @property - def player_state(self) -> PlayerType: - """Return handle to player state.""" - return self.mass.players.get_player_state(self._queue_id) - @property def queue_id(self) -> str: """Return the Queue's id.""" @@ -110,7 +105,7 @@ class PlayerQueue: """Return shuffle enabled property.""" return self._shuffle_enabled - async def async_set_shuffle_enabled(self, enable_shuffle: bool) -> None: + async def set_shuffle_enabled(self, enable_shuffle: bool) -> None: """Set shuffle.""" if not self._shuffle_enabled and enable_shuffle: # shuffle requested @@ -119,7 +114,7 @@ class PlayerQueue: played_items = self.items[: self.cur_index] next_items = self.__shuffle_items(self.items[self.cur_index + 1 :]) items = played_items + [self.cur_item] + next_items - self.mass.add_job(self.async_update(items)) + self.mass.add_job(self.update(items)) elif self._shuffle_enabled and not enable_shuffle: # unshuffle self._shuffle_enabled = False @@ -128,22 +123,22 @@ class PlayerQueue: next_items = self.items[self.cur_index + 1 :] next_items.sort(key=lambda x: x.sort_index, reverse=False) items = played_items + [self.cur_item] + next_items - self.mass.add_job(self.async_update(items)) - self.mass.add_job(self.async_update_state()) - self.mass.signal_event(EVENT_QUEUE_UPDATED, self.to_dict()) + self.mass.add_job(self.update(items)) + self.update_state() + self.mass.signal_event(EVENT_QUEUE_UPDATED, self) @property def repeat_enabled(self) -> bool: """Return if crossfade is enabled for this player.""" return self._repeat_enabled - async def async_set_repeat_enabled(self, enable_repeat: bool) -> None: + async def set_repeat_enabled(self, enable_repeat: bool) -> None: """Set the repeat mode for this queue.""" if self._repeat_enabled != enable_repeat: self._repeat_enabled = enable_repeat - self.mass.add_job(self.async_update_state()) - self.mass.add_job(self.__async_save_state()) - self.mass.signal_event(EVENT_QUEUE_UPDATED, self.to_dict()) + self.update_state() + self.mass.add_job(self._save_state()) + self.mass.signal_event(EVENT_QUEUE_UPDATED, self) @property def cur_index(self) -> OptionalInt: @@ -277,39 +272,39 @@ class PlayerQueue: return item return None - async def async_next(self) -> None: + async def next(self) -> None: """Play the next track in the queue.""" if self.cur_index is None: return if self.use_queue_stream: - return await self.async_play_index(self.cur_index + 1) - return await self.player.async_cmd_next() + return await self.play_index(self.cur_index + 1) + return await self.player.cmd_next() - async def async_previous(self) -> None: + async def previous(self) -> None: """Play the previous track in the queue.""" if self.cur_index is None: return if self.use_queue_stream: - return await self.async_play_index(self.cur_index - 1) - return await self.player.async_cmd_previous() + return await self.play_index(self.cur_index - 1) + return await self.player.cmd_previous() - async def async_resume(self) -> None: + async def resume(self) -> None: """Resume previous queue.""" if self.items: prev_index = self.cur_index if self.use_queue_stream or not self.supports_queue: - await self.async_play_index(prev_index) + await self.play_index(prev_index) 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 self._items = self._items[prev_index:] - return await self.player.async_cmd_queue_load(self._items) + return await self.player.cmd_queue_load(self._items) else: LOGGER.warning( "resume queue requested for %s but queue is empty", self.queue_id ) - async def async_play_index(self, index: Union[int, str]) -> None: + async def play_index(self, index: Union[int, str]) -> None: """Play item at index (or item_id) X in queue.""" if not isinstance(index, int): index = self.__index_by_id(index) @@ -319,21 +314,21 @@ class PlayerQueue: self._queue_stream_next_index = index if self.use_queue_stream: queue_stream_uri = self.get_stream_url() - return await self.player.async_cmd_play_uri(queue_stream_uri) + return await self.player.cmd_play_uri(queue_stream_uri) if self.supports_queue: try: - return await self.player.async_cmd_queue_play_index(index) + return await self.player.cmd_queue_play_index(index) except NotImplementedError: # not supported by player, use load queue instead LOGGER.debug( "cmd_queue_insert not supported by player, fallback to cmd_queue_load " ) self._items = self._items[index:] - return await self.player.async_cmd_queue_load(self._items) + return await self.player.cmd_queue_load(self._items) else: - return await self.player.async_cmd_play_uri(self._items[index].uri) + return await self.player.cmd_play_uri(self._items[index].uri) - async def async_move_item(self, queue_item_id: str, pos_shift: int = 1) -> None: + async def move_item(self, queue_item_id: str, pos_shift: int = 1) -> None: """ Move queue item x up/down the queue. @@ -353,11 +348,9 @@ class PlayerQueue: return # move the item in the list items.insert(new_index, items.pop(item_index)) - await self.async_update(items) - if pos_shift == 0: - await self.async_play_index(new_index) + await self.update(items) - async def async_load(self, queue_items: List[QueueItem]) -> None: + async def load(self, queue_items: List[QueueItem]) -> None: """Load (overwrite) queue with new items.""" for index, item in enumerate(queue_items): item.sort_index = index @@ -365,13 +358,13 @@ class PlayerQueue: queue_items = self.__shuffle_items(queue_items) self._items = queue_items if self.use_queue_stream or not self.supports_queue: - await self.async_play_index(0) + await self.play_index(0) else: - await self.player.async_cmd_queue_load(queue_items) - self.mass.signal_event(EVENT_QUEUE_ITEMS_UPDATED, self.to_dict()) - self.mass.add_job(self.__async_save_state()) + await self.player.cmd_queue_load(queue_items) + self.mass.signal_event(EVENT_QUEUE_ITEMS_UPDATED, self) + self.mass.add_job(self._save_state()) - async def async_insert(self, queue_items: List[QueueItem], offset: int = 0) -> None: + async def insert(self, queue_items: List[QueueItem], offset: int = 0) -> None: """ Insert new items at offset x from current position. @@ -387,7 +380,7 @@ class PlayerQueue: or self.cur_index == 0 or (self.cur_index + offset > len(self.items)) ): - return await self.async_load(queue_items) + return await self.load(queue_items) insert_at_index = self.cur_index + offset for index, item in enumerate(queue_items): item.sort_index = insert_at_index + index @@ -408,22 +401,22 @@ class PlayerQueue: ) if self.use_queue_stream: if offset == 0: - await self.async_play_index(insert_at_index) + await self.play_index(insert_at_index) else: # send queue to player's own implementation try: - await self.player.async_cmd_queue_insert(queue_items, insert_at_index) + await self.player.cmd_queue_insert(queue_items, insert_at_index) except NotImplementedError: # not supported by player, use load queue instead LOGGER.debug( "cmd_queue_insert not supported by player, fallback to cmd_queue_load " ) self._items = self._items[self.cur_index :] - return await self.player.async_cmd_queue_load(self._items) - self.mass.signal_event(EVENT_QUEUE_ITEMS_UPDATED, self.to_dict()) - self.mass.add_job(self.__async_save_state()) + return await self.player.cmd_queue_load(self._items) + self.mass.signal_event(EVENT_QUEUE_ITEMS_UPDATED, self) + self.mass.add_job(self._save_state()) - async def async_append(self, queue_items: List[QueueItem]) -> None: + async def append(self, queue_items: List[QueueItem]) -> None: """Append new items at the end of the queue.""" for index, item in enumerate(queue_items): item.sort_index = len(self.items) + index @@ -432,57 +425,58 @@ class PlayerQueue: next_items = self.items[self.cur_index + 1 :] + queue_items next_items = self.__shuffle_items(next_items) items = played_items + [self.cur_item] + next_items - return await self.async_update(items) + return await self.update(items) self._items = self._items + queue_items if self.supports_queue and not self.use_queue_stream: # send queue to player's own implementation try: - await self.player.async_cmd_queue_append(queue_items) + await self.player.cmd_queue_append(queue_items) except NotImplementedError: # not supported by player, use load queue instead LOGGER.debug( "cmd_queue_append not supported by player, fallback to cmd_queue_load " ) self._items = self._items[self.cur_index :] - return await self.player.async_cmd_queue_load(self._items) - self.mass.signal_event(EVENT_QUEUE_ITEMS_UPDATED, self.to_dict()) - self.mass.add_job(self.__async_save_state()) + return await self.player.cmd_queue_load(self._items) + self.mass.signal_event(EVENT_QUEUE_ITEMS_UPDATED, self) + self.mass.add_job(self._save_state()) - async def async_update(self, queue_items: List[QueueItem]) -> None: + async def update(self, queue_items: List[QueueItem]) -> None: """Update the existing queue items, mostly caused by reordering.""" self._items = queue_items if self.supports_queue and not self.use_queue_stream: # send queue to player's own implementation try: - await self.player.async_cmd_queue_update(queue_items) + await self.player.cmd_queue_update(queue_items) except NotImplementedError: # not supported by player, use load queue instead LOGGER.debug( "cmd_queue_update not supported by player, fallback to cmd_queue_load " ) self._items = self._items[self.cur_index :] - await self.player.async_cmd_queue_load(self._items) - self.mass.signal_event(EVENT_QUEUE_ITEMS_UPDATED, self.to_dict()) - self.mass.add_job(self.__async_save_state()) + await self.player.cmd_queue_load(self._items) + self.mass.signal_event(EVENT_QUEUE_ITEMS_UPDATED, self) + self.mass.add_job(self._save_state()) - async def async_clear(self) -> None: + async def clear(self) -> None: """Clear all items in the queue.""" - await self.mass.players.async_cmd_stop(self.queue_id) + await self.mass.players.cmd_stop(self.queue_id) self._items = [] if self.supports_queue: # send queue cmd to player's own implementation try: - await self.player.async_cmd_queue_clear() + await self.player.cmd_queue_clear() except NotImplementedError: # not supported by player, try update instead try: - await self.player.async_cmd_queue_update([]) + await self.player.cmd_queue_update([]) except NotImplementedError: # not supported by player, ignore pass - self.mass.signal_event(EVENT_QUEUE_ITEMS_UPDATED, self.to_dict()) + self.mass.signal_event(EVENT_QUEUE_ITEMS_UPDATED, self) - async def async_update_state(self) -> None: + @callback + def update_state(self) -> None: """Update queue details, called when player updates.""" new_index = self._cur_index track_time = self._cur_item_time @@ -511,7 +505,7 @@ class PlayerQueue: and self.cur_item.streamdetails ): # new active item in queue - self.mass.signal_event(EVENT_QUEUE_UPDATED, self.to_dict()) + self.mass.signal_event(EVENT_QUEUE_UPDATED, self) # invalidate previous streamdetails if self._last_item: self._last_item.streamdetails = None @@ -524,7 +518,7 @@ class PlayerQueue: {"queue_id": self.queue_id, "cur_item_time": track_time}, ) - async def async_queue_stream_start(self) -> None: + async def queue_stream_start(self) -> None: """Call when queue_streamer starts playing the queue stream.""" self._cur_item_time = 0 self._cur_index = self._queue_stream_next_index @@ -532,7 +526,7 @@ class PlayerQueue: self._queue_stream_start_index = self._cur_index return self._cur_index - async def async_queue_stream_next(self, cur_index: int) -> None: + async def queue_stream_next(self, cur_index: int) -> None: """Call when queue_streamer loads next track in buffer.""" next_index = 0 if len(self.items) > (next_index): @@ -543,7 +537,7 @@ class PlayerQueue: self._queue_stream_next_index = next_index + 1 return next_index - def to_dict(self) -> dict: + def to_dict(self) -> Dict[str, Any]: """Instance attributes as dict so it can be serialized to json.""" return { "queue_id": self.player.player_id, @@ -598,10 +592,10 @@ class PlayerQueue: item_index = index return item_index - async def __async_restore_saved_state(self) -> None: + async def _restore_saved_state(self) -> None: """Try to load the saved queue for this player from cache file.""" cache_str = "queue_state_%s" % self.queue_id - cache_data = await self.mass.cache.async_get(cache_str) + cache_data = await self.mass.cache.get(cache_str) if cache_data: self._shuffle_enabled = cache_data["shuffle_enabled"] self._repeat_enabled = cache_data["repeat_enabled"] @@ -611,7 +605,7 @@ class PlayerQueue: # pylint: enable=unused-argument - async def __async_save_state(self) -> None: + async def _save_state(self) -> None: """Save current queue settings to file.""" cache_str = "queue_state_%s" % self.queue_id cache_data = { @@ -620,5 +614,5 @@ class PlayerQueue: "items": self._items, "cur_index": self._cur_index, } - await self.mass.cache.async_set(cache_str, cache_data) + await self.mass.cache.set(cache_str, cache_data) LOGGER.info("queue state saved to file for player %s", self.queue_id) diff --git a/music_assistant/models/player_state.py b/music_assistant/models/player_state.py deleted file mode 100755 index 2ffb0b6f..00000000 --- a/music_assistant/models/player_state.py +++ /dev/null @@ -1,407 +0,0 @@ -""" -Models and helpers for the calculated state of a player. - -PlayerProviders send Player objects to us with the raw/untouched player state. -Due to configuration settings and other influences this playerstate needs alteration, -that's why we store the final player state (we present to outside world) -into a PlayerState object. -""" - -import logging -from datetime import datetime -from typing import List, Optional - -from music_assistant.constants import ( - ATTR_ACTIVE_QUEUE, - ATTR_AVAILABLE, - ATTR_CURRENT_URI, - ATTR_DEVICE_INFO, - ATTR_ELAPSED_TIME, - ATTR_FEATURES, - ATTR_GROUP_CHILDS, - ATTR_GROUP_PARENTS, - ATTR_IS_GROUP_PLAYER, - ATTR_MUTED, - ATTR_NAME, - ATTR_PLAYER_ID, - ATTR_POWERED, - ATTR_PROVIDER_ID, - ATTR_SHOULD_POLL, - ATTR_STATE, - ATTR_UPDATED_AT, - ATTR_VOLUME_LEVEL, - CONF_ENABLED, - CONF_GROUP_DELAY, - CONF_NAME, - CONF_POWER_CONTROL, - CONF_VOLUME_CONTROL, - EVENT_PLAYER_CHANGED, -) -from music_assistant.helpers.typing import MusicAssistantType -from music_assistant.models.player import ( - DeviceInfo, - PlaybackState, - Player, - PlayerFeature, -) - -LOGGER = logging.getLogger("player_state") - -# List of all player_state attributes -PLAYER_ATTRIBUTES = [ - ATTR_ACTIVE_QUEUE, - ATTR_AVAILABLE, - ATTR_CURRENT_URI, - ATTR_DEVICE_INFO, - ATTR_ELAPSED_TIME, - ATTR_FEATURES, - ATTR_GROUP_CHILDS, - ATTR_GROUP_PARENTS, - ATTR_IS_GROUP_PLAYER, - ATTR_MUTED, - ATTR_NAME, - ATTR_PLAYER_ID, - ATTR_POWERED, - ATTR_PROVIDER_ID, - ATTR_SHOULD_POLL, - ATTR_STATE, - ATTR_VOLUME_LEVEL, -] - -# list of Player attributes that can/will cause a player changed event -UPDATE_ATTRIBUTES = [ - ATTR_NAME, - ATTR_POWERED, - ATTR_STATE, - ATTR_AVAILABLE, - ATTR_CURRENT_URI, - ATTR_VOLUME_LEVEL, - ATTR_MUTED, - ATTR_IS_GROUP_PLAYER, - ATTR_GROUP_CHILDS, - ATTR_SHOULD_POLL, -] - - -class PlayerState: - """ - Model for the calculated state of a player. - - PlayerProviders send Player objects to us with the raw/untouched player state. - Due to configuration settings and other influences this playerstate needs alteration, - that's why we store the final player state (we present to outside world) - into this PlayerState object. - """ - - def __init__(self, mass: MusicAssistantType, player: Player): - """Initialize a PlayerState from a Player object.""" - self.mass = mass - # make sure the MusicAssistant obj is present on the player - player.mass = mass - self._player = player - self._player_id = player.player_id - self._provider_id = player.provider_id - self._features = player.features - self._muted = player.muted - self._is_group_player = player.is_group_player - self._group_childs = player.group_childs - self._device_info = player.device_info - self._elapsed_time = player.elapsed_time - self._current_uri = player.current_uri - self._available = player.available - self._name = player.name - self._powered = player.powered - self._state = player.state - self._volume_level = player.volume_level - self._updated_at = datetime.utcnow() - self._group_parents = self.get_group_parents() - self._active_queue = self.get_active_queue() - self._group_delay = self.get_group_delay() - # schedule update to set the transforms - self.mass.add_job(self.async_update(player)) - - @property - def player(self): - """Return the underlying player object.""" - return self._player - - @property - def player_id(self) -> str: - """Return player id of this player.""" - return self._player_id - - @property - def provider_id(self) -> str: - """Return provider id of this player.""" - return self._provider_id - - @property - def name(self) -> str: - """Return name of the player.""" - return self._name - - @property - def powered(self) -> bool: - """Return current power state of player.""" - return self._powered - - @property - def elapsed_time(self) -> int: - """Return elapsed time of current playing media in seconds.""" - return self._elapsed_time - - @property - def elapsed_milliseconds(self) -> Optional[int]: - """ - Return elapsed time of current playing media in milliseconds. - - This is an optional property. - If provided, the property must return the REALTIME value while playing. - Used for synced playback in player groups. - """ - # always realtime returned from player - if self.player.elapsed_milliseconds is not None: - return self.player.elapsed_milliseconds - self.group_delay - return None - - @property - def state(self) -> PlaybackState: - """Return current PlaybackState of player.""" - return self._state - - @property - def available(self) -> bool: - """Return current availablity of player.""" - return self._available - - @property - def current_uri(self) -> Optional[str]: - """Return currently loaded uri of player (if any).""" - return self._current_uri - - @property - def volume_level(self) -> int: - """Return current volume level of player (scale 0..100).""" - return self._volume_level - - @property - def muted(self) -> bool: - """Return current mute state of player.""" - return self._muted - - @property - def is_group_player(self) -> bool: - """Return True if this player is a group player.""" - return self._is_group_player - - @property - def group_childs(self) -> List[str]: - """Return list of child player id's if player is a group player.""" - return self._group_childs - - @property - def device_info(self) -> DeviceInfo: - """Return the device info for this player.""" - return self._device_info - - @property - def should_poll(self) -> bool: - """Return True if this player should be polled for state updates.""" - return self._player.should_poll # always realtime returned from player - - @property - def features(self) -> List[PlayerFeature]: - """Return list of features this player supports.""" - return self._features - - @property - def group_delay(self) -> int: - """Return group delay of this player in milliseconds (if configured).""" - return self._group_delay - - async def async_update(self, player: Player): - """Run update player state task in executor.""" - self.mass.add_job(self.update, player) - - def update(self, player: Player): - """Update attributes from player object.""" - new_available = self.get_available(player.available) - if self.available == new_available and not new_available: - return # ignore players that are unavailable - - # detect state changes - changed_keys = set() - for attr in PLAYER_ATTRIBUTES: - - new_value = getattr(self._player, attr, None) - - # handle transformations - if attr == ATTR_NAME: - new_value = self.get_name(new_value) - elif attr == ATTR_POWERED: - new_value = self.get_power(new_value) - elif attr == ATTR_STATE: - new_value = self.get_state(new_value) - elif attr == ATTR_AVAILABLE: - new_value = self.get_available(new_value) - elif attr == ATTR_VOLUME_LEVEL: - new_value = self.get_volume_level(new_value) - elif attr == ATTR_GROUP_PARENTS: - new_value = self.get_group_parents() - elif attr == ATTR_ACTIVE_QUEUE: - new_value = self.get_active_queue() - - current_value = getattr(self, attr) - - if current_value != new_value: - # value changed - setattr(self, "_" + attr, new_value) - changed_keys.add(attr) - - if changed_keys: - self._updated_at = datetime.utcnow() - - if changed_keys.intersection(set(UPDATE_ATTRIBUTES)): - self.mass.signal_event(EVENT_PLAYER_CHANGED, self) - # update group player childs when parent updates - for child_player_id in self.group_childs: - self.mass.add_job( - self.mass.players.async_trigger_player_update(child_player_id) - ) - # update group player when child updates - for group_player_id in self.group_parents: - self.mass.add_job( - self.mass.players.async_trigger_player_update(group_player_id) - ) - - # always update the player queue - player_queue = self.mass.players.get_player_queue(self.active_queue) - if player_queue: - self.mass.add_job(player_queue.async_update_state()) - self._group_delay = self.get_group_delay() - - def get_name(self, name: str) -> str: - """Return final/calculated player name.""" - conf_name = self.mass.config.get_player_config(self.player_id)[CONF_NAME] - return conf_name if conf_name else name - - def get_power(self, power: bool) -> bool: - """Return final/calculated player's power state.""" - if not self.available: - return False - player_config = self.mass.config.player_settings[self.player_id] - if player_config.get(CONF_POWER_CONTROL): - control = self.mass.players.get_player_control( - player_config[CONF_POWER_CONTROL] - ) - if control: - return control.state - return power - - def get_state(self, state: PlaybackState) -> PlaybackState: - """Return final/calculated player's playback state.""" - if self.powered and self.active_queue != self.player_id: - # use group state - return self.mass.players.get_player_state(self.active_queue).state - if state == PlaybackState.Stopped and not self.powered: - return PlaybackState.Off - return state - - def get_available(self, available: bool) -> bool: - """Return current availablity of player.""" - player_enabled = self.mass.config.get_player_config(self.player_id)[ - CONF_ENABLED - ] - return False if not player_enabled else available - - def get_volume_level(self, volume_level: int) -> int: - """Return final/calculated player's volume_level.""" - if not self.available: - return 0 - player_config = self.mass.config.player_settings[self.player_id] - if player_config.get(CONF_VOLUME_CONTROL): - control = self.mass.players.get_player_control( - player_config[CONF_VOLUME_CONTROL] - ) - if control: - return control.state - # handle group volume - if self.is_group_player: - group_volume = 0 - active_players = 0 - for child_player_id in self.group_childs: - child_player = self.mass.players.get_player_state(child_player_id) - if child_player and child_player.available and child_player.powered: - group_volume += child_player.volume_level - active_players += 1 - if active_players: - group_volume = group_volume / active_players - return group_volume - return volume_level - - @property - def group_parents(self) -> List[str]: - """Return all group players this player belongs to.""" - return self._group_parents - - def get_group_parents(self) -> List[str]: - """Return all group players this player belongs to.""" - if self.is_group_player: - return [] - result = [] - for player in self.mass.players.player_states: - if not player.is_group_player: - continue - if self.player_id not in player.group_childs: - continue - result.append(player.player_id) - return result - - @property - def active_queue(self) -> str: - """Return the active parent player/queue for a player.""" - return self._active_queue - - def get_active_queue(self) -> str: - """Return the active parent player/queue for a player.""" - # if a group is powered on, all of it's childs will have/use - # the parent's player's queue. - for group_player_id in self.group_parents: - group_player = self.mass.players.get_player_state(group_player_id) - if group_player and group_player.powered: - return group_player_id - return self.player_id - - @property - def updated_at(self) -> datetime: - """Return the datetime (UTC) that the player state was last updated.""" - return self._updated_at - - def get_group_delay(self): - """Get group delay for a player.""" - player_settings = self.mass.config.get_player_config(self.player_id) - if player_settings: - return player_settings.get(CONF_GROUP_DELAY, 0) - return 0 - - def to_dict(self): - """Instance attributes as dict so it can be serialized to json.""" - return { - ATTR_PLAYER_ID: self.player_id, - ATTR_PROVIDER_ID: self.provider_id, - ATTR_NAME: self.name, - ATTR_POWERED: self.powered, - ATTR_ELAPSED_TIME: int(self.elapsed_time), - ATTR_STATE: self.state.value, - ATTR_AVAILABLE: self.available, - ATTR_CURRENT_URI: self.current_uri, - ATTR_VOLUME_LEVEL: int(self.volume_level), - ATTR_MUTED: self.muted, - ATTR_IS_GROUP_PLAYER: self.is_group_player, - ATTR_GROUP_CHILDS: self.group_childs, - ATTR_DEVICE_INFO: self.device_info.to_dict(), - ATTR_UPDATED_AT: self.updated_at.isoformat(), - ATTR_GROUP_PARENTS: self.group_parents, - ATTR_FEATURES: self.features, - ATTR_ACTIVE_QUEUE: self.active_queue, - } diff --git a/music_assistant/models/provider.py b/music_assistant/models/provider.py index 95e29e3b..77d731c3 100644 --- a/music_assistant/models/provider.py +++ b/music_assistant/models/provider.py @@ -4,7 +4,7 @@ from abc import abstractmethod from enum import Enum from typing import Dict, List, Optional -from music_assistant.helpers.typing import MusicAssistantType, Players +from music_assistant.helpers.typing import MusicAssistant, Players from music_assistant.models.config_entry import ConfigEntry from music_assistant.models.media_types import ( Album, @@ -30,9 +30,7 @@ class ProviderType(Enum): class Provider: """Base model for a provider/plugin.""" - mass: MusicAssistantType = ( - None # will be set automagically while loading the provider - ) + mass: MusicAssistant = None # will be set automagically while loading the provider available: bool = False # will be set automagically while loading the provider @property @@ -56,7 +54,7 @@ class Provider: """Return Config Entries for this provider.""" @abstractmethod - async def async_on_start(self) -> bool: + async def on_start(self) -> bool: """ Handle initialization of the provider based on config. @@ -65,7 +63,7 @@ class Provider: raise NotImplementedError @abstractmethod - async def async_on_stop(self) -> None: + async def on_stop(self) -> None: """Handle correct close/cleanup of the provider on exit. Called on shutdown/reload.""" @@ -98,11 +96,7 @@ class PlayerProvider(Provider): def players(self) -> Players: """Return all players belonging to this provider.""" # pylint: disable=no-member - return [ - player - for player in self.mass.players.players - if player.provider_id == self.id - ] + return [player for player in self.mass.players if player.provider_id == self.id] class MetadataProvider(Provider): @@ -117,11 +111,11 @@ class MetadataProvider(Provider): """Return ProviderType.""" return ProviderType.METADATA_PROVIDER - async def async_get_artist_images(self, mb_artist_id: str) -> Dict: + async def get_artist_images(self, mb_artist_id: str) -> Dict: """Retrieve artist metadata as dict by musicbrainz artist id.""" raise NotImplementedError - async def async_get_album_images(self, mb_album_id: str) -> Dict: + async def get_album_images(self, mb_album_id: str) -> Dict: """Retrieve album metadata as dict by musicbrainz album id.""" raise NotImplementedError @@ -149,7 +143,7 @@ class MusicProvider(Provider): MediaType.Track, ] - async def async_search( + async def search( self, search_query: str, media_types=Optional[List[MediaType]], limit: int = 5 ) -> SearchResult: """ @@ -161,100 +155,98 @@ class MusicProvider(Provider): """ raise NotImplementedError - async def async_get_library_artists(self) -> List[Artist]: + async def get_library_artists(self) -> List[Artist]: """Retrieve library artists from the provider.""" if MediaType.Artist in self.supported_mediatypes: raise NotImplementedError - async def async_get_library_albums(self) -> List[Album]: + async def get_library_albums(self) -> List[Album]: """Retrieve library albums from the provider.""" if MediaType.Album in self.supported_mediatypes: raise NotImplementedError - async def async_get_library_tracks(self) -> List[Track]: + async def get_library_tracks(self) -> List[Track]: """Retrieve library tracks from the provider.""" if MediaType.Track in self.supported_mediatypes: raise NotImplementedError - async def async_get_library_playlists(self) -> List[Playlist]: + async def get_library_playlists(self) -> List[Playlist]: """Retrieve library/subscribed playlists from the provider.""" if MediaType.Playlist in self.supported_mediatypes: raise NotImplementedError - async def async_get_radios(self) -> List[Radio]: + async def get_radios(self) -> List[Radio]: """Retrieve library/subscribed radio stations from the provider.""" if MediaType.Radio in self.supported_mediatypes: raise NotImplementedError - async def async_get_artist(self, prov_artist_id: str) -> Artist: + async def get_artist(self, prov_artist_id: str) -> Artist: """Get full artist details by id.""" if MediaType.Artist in self.supported_mediatypes: raise NotImplementedError - async def async_get_artist_albums(self, prov_artist_id: str) -> List[Album]: + async def get_artist_albums(self, prov_artist_id: str) -> List[Album]: """Get a list of all albums for the given artist.""" if MediaType.Album in self.supported_mediatypes: raise NotImplementedError - async def async_get_artist_toptracks(self, prov_artist_id: str) -> List[Track]: + async def get_artist_toptracks(self, prov_artist_id: str) -> List[Track]: """Get a list of most popular tracks for the given artist.""" if MediaType.Track in self.supported_mediatypes: raise NotImplementedError - async def async_get_album(self, prov_album_id: str) -> Album: + async def get_album(self, prov_album_id: str) -> Album: """Get full album details by id.""" if MediaType.Album in self.supported_mediatypes: raise NotImplementedError - async def async_get_track(self, prov_track_id: str) -> Track: + async def get_track(self, prov_track_id: str) -> Track: """Get full track details by id.""" if MediaType.Track in self.supported_mediatypes: raise NotImplementedError - async def async_get_playlist(self, prov_playlist_id: str) -> Playlist: + async def get_playlist(self, prov_playlist_id: str) -> Playlist: """Get full playlist details by id.""" if MediaType.Playlist in self.supported_mediatypes: raise NotImplementedError - async def async_get_radio(self, prov_radio_id: str) -> Radio: + async def get_radio(self, prov_radio_id: str) -> Radio: """Get full radio details by id.""" if MediaType.Radio in self.supported_mediatypes: raise NotImplementedError - async def async_get_album_tracks(self, prov_album_id: str) -> List[Track]: + async def get_album_tracks(self, prov_album_id: str) -> List[Track]: """Get album tracks for given album id.""" if MediaType.Album in self.supported_mediatypes: raise NotImplementedError - async def async_get_playlist_tracks(self, prov_playlist_id: str) -> List[Track]: + async def get_playlist_tracks(self, prov_playlist_id: str) -> List[Track]: """Get all playlist tracks for given playlist id.""" if MediaType.Playlist in self.supported_mediatypes: raise NotImplementedError - async def async_library_add(self, prov_item_id: str, media_type: MediaType) -> bool: + async def library_add(self, prov_item_id: str, media_type: MediaType) -> bool: """Add item to provider's library. Return true on succes.""" raise NotImplementedError - async def async_library_remove( - self, prov_item_id: str, media_type: MediaType - ) -> bool: + async def library_remove(self, prov_item_id: str, media_type: MediaType) -> bool: """Remove item from provider's library. Return true on succes.""" raise NotImplementedError - async def async_add_playlist_tracks( + async def add_playlist_tracks( self, prov_playlist_id: str, prov_track_ids: List[str] ) -> bool: """Add track(s) to playlist. Return true on succes.""" if MediaType.Playlist in self.supported_mediatypes: raise NotImplementedError - async def async_remove_playlist_tracks( + async def remove_playlist_tracks( self, prov_playlist_id: str, prov_track_ids: List[str] ) -> bool: """Remove track(s) from playlist. Return true on succes.""" if MediaType.Playlist in self.supported_mediatypes: raise NotImplementedError - async def async_get_stream_details(self, item_id: str) -> StreamDetails: + async def get_stream_details(self, item_id: str) -> StreamDetails: """Get streamdetails for a track/radio.""" raise NotImplementedError diff --git a/music_assistant/providers/builtin_player/__init__.py b/music_assistant/providers/builtin_player/__init__.py index acfa3682..f78b33fd 100644 --- a/music_assistant/providers/builtin_player/__init__.py +++ b/music_assistant/providers/builtin_player/__init__.py @@ -3,7 +3,7 @@ import logging import time from typing import List -from music_assistant.helpers.typing import MusicAssistantType +from music_assistant.helpers.typing import MusicAssistant from music_assistant.helpers.util import run_periodic from music_assistant.models.config_entry import ConfigEntry from music_assistant.models.player import ( @@ -23,14 +23,14 @@ PLAYER_CONFIG_ENTRIES = [] PLAYER_FEATURES = [] WS_COMMAND_WSPLAYER_CMD = "wsplayer command" -WS_COMMAND_WSPLAYER_STATE = "wsplayer state" +WS_COMMAND_WSplayer = "wsplayer state" WS_COMMAND_WSPLAYER_REGISTER = "wsplayer register" -async def async_setup(mass): +async def setup(mass): """Perform async setup of this Plugin/Provider.""" prov = MassPlayerProvider() - await mass.async_register_provider(prov) + await mass.register_provider(prov) class MassPlayerProvider(PlayerProvider): @@ -55,44 +55,42 @@ class MassPlayerProvider(PlayerProvider): """Return Config Entries for this provider.""" return [] - async def async_on_start(self) -> bool: + async def on_start(self) -> bool: """Handle initialization of the provider based on config.""" # listen for websockets commands to dynamically create players - self.mass.add_job(self.async_check_players()) + self.mass.add_job(self.check_players()) self.mass.web.register_api_route( - WS_COMMAND_WSPLAYER_REGISTER, self.async_handle_ws_player_state - ) - self.mass.web.register_api_route( - WS_COMMAND_WSPLAYER_STATE, self.async_handle_ws_player_state + WS_COMMAND_WSPLAYER_REGISTER, self.handle_ws_player ) + self.mass.web.register_api_route(WS_COMMAND_WSplayer, self.handle_ws_player) return True - async def async_on_stop(self): + async def on_stop(self): """Handle correct close/cleanup of the provider on exit.""" for player in self.players: - await player.async_cmd_stop() + await player.cmd_stop() - async def async_handle_ws_player_state(self, player_id: str, details: dict): + async def handle_ws_player(self, player_id: str, details: dict): """Handle state message from ws player.""" player = self.mass.players.get_player(player_id) if not player: # register new player player = WebsocketsPlayer(self.mass, player_id, details["name"]) - await self.mass.players.async_add_player(player) - await player.handle_player_state(details) + await self.mass.players.add_player(player) + await player.handle_player(details) @run_periodic(30) - async def async_check_players(self) -> None: + async def check_players(self) -> None: """Invalidate players that did not send a heartbeat message in a while.""" cur_time = time.time() - offline_players = [] + offline_players = set() for player in self.players: if not isinstance(player, WebsocketsPlayer): continue if cur_time - player.last_message > 30: - offline_players.append(player.player_id) + offline_players.add(player.player_id) for player_id in offline_players: - await self.mass.players.async_remove_player(player_id) + await self.mass.players.remove_player(player_id) class WebsocketsPlayer(Player): @@ -104,7 +102,7 @@ class WebsocketsPlayer(Player): and our internal event bus. """ - def __init__(self, mass: MusicAssistantType, player_id: str, player_name: str): + def __init__(self, mass: MusicAssistant, player_id: str, player_name: str): """Initialize the wsplayer.""" self._player_id = player_id self._player_name = player_name @@ -117,7 +115,7 @@ class WebsocketsPlayer(Player): self._device_info = DeviceInfo() self.last_message = time.time() - async def handle_player_state(self, data: dict): + async def handle_player(self, data: dict): """Handle state event from player.""" if "volume_level" in data: self._volume_level = data["volume_level"] @@ -202,7 +200,7 @@ class WebsocketsPlayer(Player): """Return player specific config entries (if any).""" return PLAYER_CONFIG_ENTRIES - async def async_cmd_play_uri(self, uri: str) -> None: + async def cmd_play_uri(self, uri: str) -> None: """ Play the specified uri/url on the player. @@ -211,32 +209,32 @@ class WebsocketsPlayer(Player): data = {"player_id": self.player_id, "cmd": "play_uri", "uri": uri} self.mass.signal_event(WS_COMMAND_WSPLAYER_CMD, data) - async def async_cmd_stop(self) -> None: + async def cmd_stop(self) -> None: """Send STOP command to player.""" data = {"player_id": self.player_id, "cmd": "stop"} self.mass.signal_event(WS_COMMAND_WSPLAYER_CMD, data) - async def async_cmd_play(self) -> None: + async def cmd_play(self) -> None: """Send PLAY command to player.""" data = {"player_id": self.player_id, "cmd": "play"} self.mass.signal_event(WS_COMMAND_WSPLAYER_CMD, data) - async def async_cmd_pause(self) -> None: + async def cmd_pause(self) -> None: """Send PAUSE command to player.""" data = {"player_id": self.player_id, "cmd": "pause"} self.mass.signal_event(WS_COMMAND_WSPLAYER_CMD, data) - async def async_cmd_power_on(self) -> None: + async def cmd_power_on(self) -> None: """Send POWER ON command to player.""" self._powered = True self.update_state() - async def async_cmd_power_off(self) -> None: + async def cmd_power_off(self) -> None: """Send POWER OFF command to player.""" self._powered = False self.update_state() - async def async_cmd_volume_set(self, volume_level: int) -> None: + async def cmd_volume_set(self, volume_level: int) -> None: """ Send volume level command to player. @@ -249,7 +247,7 @@ class WebsocketsPlayer(Player): } self.mass.signal_event(WS_COMMAND_WSPLAYER_CMD, data) - async def async_cmd_volume_mute(self, is_muted: bool = False) -> None: + async def cmd_volume_mute(self, is_muted: bool = False) -> None: """ Send volume MUTE command to given player. diff --git a/music_assistant/providers/chromecast/__init__.py b/music_assistant/providers/chromecast/__init__.py index 12ae3ab5..49c9ec87 100644 --- a/music_assistant/providers/chromecast/__init__.py +++ b/music_assistant/providers/chromecast/__init__.py @@ -15,11 +15,11 @@ from .player import ChromecastPlayer LOGGER = logging.getLogger(PROV_ID) -async def async_setup(mass): +async def setup(mass): """Perform async setup of this Plugin/Provider.""" logging.getLogger("pychromecast").setLevel(logging.WARNING) prov = ChromecastProvider() - await mass.async_register_provider(prov) + await mass.register_provider(prov) class ChromecastProvider(PlayerProvider): @@ -47,7 +47,7 @@ class ChromecastProvider(PlayerProvider): """Return Config Entries for this provider.""" return PROVIDER_CONFIG_ENTRIES - async def async_on_start(self) -> bool: + async def on_start(self) -> bool: """Handle initialization of the provider based on config.""" self._listener = pychromecast.CastListener( self.__chromecast_add_update_callback, @@ -63,7 +63,7 @@ class ChromecastProvider(PlayerProvider): self.mass.add_job(start_discovery) return True - async def async_on_stop(self): + async def on_stop(self): """Handle correct close/cleanup of the provider on exit.""" if not self._browser: return @@ -94,7 +94,7 @@ class ChromecastProvider(PlayerProvider): player = ChromecastPlayer(self.mass, cast_info) # if player was already added, the player will take care of reconnects itself. player.set_cast_info(cast_info) - self.mass.add_job(self.mass.players.async_add_player(player)) + self.mass.add_job(self.mass.players.add_player(player)) @staticmethod def __chromecast_remove_callback(cast_uuid, cast_service_name, cast_service): diff --git a/music_assistant/providers/chromecast/player.py b/music_assistant/providers/chromecast/player.py index 84d47d6d..0e29e42c 100644 --- a/music_assistant/providers/chromecast/player.py +++ b/music_assistant/providers/chromecast/player.py @@ -7,8 +7,8 @@ from typing import List, Optional import pychromecast from asyncio_throttle import Throttler from music_assistant.helpers.compare import compare_strings -from music_assistant.helpers.typing import MusicAssistantType -from music_assistant.helpers.util import async_yield_chunks +from music_assistant.helpers.typing import MusicAssistant +from music_assistant.helpers.util import yield_chunks from music_assistant.models.config_entry import ConfigEntry from music_assistant.models.player import ( DeviceInfo, @@ -38,8 +38,9 @@ class ChromecastPlayer(Player): "elected leader" itself. """ - def __init__(self, mass: MusicAssistantType, cast_info: ChromecastInfo) -> None: + def __init__(self, mass: MusicAssistant, cast_info: ChromecastInfo) -> None: """Initialize the cast device.""" + super().__init__() self.mass = mass self._cast_info = cast_info self._player_id = cast_info.uuid @@ -192,7 +193,7 @@ class ChromecastPlayer(Player): """Return player specific config entries (if any).""" return PLAYER_CONFIG_ENTRIES - async def async_on_add(self) -> None: + async def on_add(self) -> None: """Call when player is added to the player manager.""" chromecast = await self.mass.loop.run_in_executor( None, @@ -223,7 +224,7 @@ class ChromecastPlayer(Player): """Set (or update) the cast discovery info.""" self._cast_info = cast_info - async def async_disconnect(self): + async def disconnect(self): """Disconnect Chromecast object if it is set.""" if self._chromecast is None: # Can't disconnect if not connected. @@ -252,9 +253,9 @@ class ChromecastPlayer(Player): self._status_listener.invalidate() self._status_listener = None - async def async_on_remove(self) -> None: + async def on_remove(self) -> None: """Call when player is removed from the player manager.""" - await self.async_disconnect() + await self.disconnect() # ========== Callbacks ========== @@ -299,79 +300,69 @@ class ChromecastPlayer(Player): if self._cast_info.is_audio_group and new_available: self.mass.add_job(self._chromecast.mz_controller.update_members) - async def async_on_update(self) -> None: + async def on_poll(self) -> None: """Call when player is periodically polled by the player manager (should_poll=True).""" - if self.mass.players.get_player_state(self.player_id).active_queue.startswith( - "group_player" - ): + if self.active_queue.startswith("group_player"): # the group player wants very accurate elapsed_time state so we request it very often - await self.async_chromecast_command( + await self.chromecast_command( self._chromecast.media_controller.update_status ) self.update_state() # ========== Service Calls ========== - async def async_cmd_stop(self) -> None: + async def cmd_stop(self) -> None: """Send stop command to player.""" if self._chromecast and self._chromecast.media_controller: - await self.async_chromecast_command(self._chromecast.quit_app) + await self.chromecast_command(self._chromecast.quit_app) - async def async_cmd_play(self) -> None: + async def cmd_play(self) -> None: """Send play command to player.""" if self._chromecast.media_controller: - await self.async_chromecast_command(self._chromecast.media_controller.play) + await self.chromecast_command(self._chromecast.media_controller.play) - async def async_cmd_pause(self) -> None: + async def cmd_pause(self) -> None: """Send pause command to player.""" if self._chromecast.media_controller: - await self.async_chromecast_command(self._chromecast.media_controller.pause) + await self.chromecast_command(self._chromecast.media_controller.pause) - async def async_cmd_next(self) -> None: + async def cmd_next(self) -> None: """Send next track command to player.""" if self._chromecast.media_controller: - await self.async_chromecast_command( - self._chromecast.media_controller.queue_next - ) + await self.chromecast_command(self._chromecast.media_controller.queue_next) - async def async_cmd_previous(self) -> None: + async def cmd_previous(self) -> None: """Send previous track command to player.""" if self._chromecast.media_controller: - await self.async_chromecast_command( - self._chromecast.media_controller.queue_prev - ) + await self.chromecast_command(self._chromecast.media_controller.queue_prev) - async def async_cmd_power_on(self) -> None: + async def cmd_power_on(self) -> None: """Send power ON command to player.""" - await self.async_chromecast_command(self._chromecast.set_volume_muted, False) + await self.chromecast_command(self._chromecast.set_volume_muted, False) - async def async_cmd_power_off(self) -> None: + async def cmd_power_off(self) -> None: """Send power OFF command to player.""" # chromecast has no real poweroff so we send mute instead - await self.async_chromecast_command(self._chromecast.set_volume_muted, True) + await self.chromecast_command(self._chromecast.set_volume_muted, True) - async def async_cmd_volume_set(self, volume_level: int) -> None: + async def cmd_volume_set(self, volume_level: int) -> None: """Send new volume level command to player.""" - await self.async_chromecast_command( - self._chromecast.set_volume, volume_level / 100 - ) + await self.chromecast_command(self._chromecast.set_volume, volume_level / 100) - async def async_cmd_volume_mute(self, is_muted: bool = False) -> None: + async def cmd_volume_mute(self, is_muted: bool = False) -> None: """Send mute command to player.""" - await self.async_chromecast_command(self._chromecast.set_volume_muted, is_muted) + await self.chromecast_command(self._chromecast.set_volume_muted, is_muted) - async def async_cmd_play_uri(self, uri: str) -> None: + async def cmd_play_uri(self, uri: str) -> None: """Play single uri on player.""" player_queue = self.mass.players.get_player_queue(self.player_id) if player_queue.use_queue_stream: # create CC queue so that skip and previous will work queue_item = QueueItem(name="Music Assistant", uri=uri) - return await self.async_cmd_queue_load([queue_item, queue_item]) - await self.async_chromecast_command( - self._chromecast.play_media, uri, "audio/flac" - ) + return await self.cmd_queue_load([queue_item, queue_item]) + await self.chromecast_command(self._chromecast.play_media, uri, "audio/flac") - async def async_cmd_queue_load(self, queue_items: List[QueueItem]) -> None: + async def cmd_queue_load(self, queue_items: List[QueueItem]) -> None: """Load (overwrite) queue with new items.""" player_queue = self.mass.players.get_player_queue(self.player_id) cc_queue_items = self.__create_queue_items(queue_items[:50]) @@ -384,20 +375,20 @@ class ChromecastPlayer(Player): "startIndex": 0, # Item index to play after this request or keep same item if undefined "items": cc_queue_items, # only load 50 tracks at once or the socket will crash } - await self.async_chromecast_command(self.__send_player_queue, queuedata) + await self.chromecast_command(self.__send_player_queue, queuedata) if len(queue_items) > 50: - await self.async_cmd_queue_append(queue_items[51:]) + await self.cmd_queue_append(queue_items[51:]) - async def async_cmd_queue_append(self, queue_items: List[QueueItem]) -> None: + async def cmd_queue_append(self, queue_items: List[QueueItem]) -> None: """Append new items at the end of the queue.""" cc_queue_items = self.__create_queue_items(queue_items) - async for chunk in async_yield_chunks(cc_queue_items, 50): + async for chunk in yield_chunks(cc_queue_items, 50): queuedata = { "type": "QUEUE_INSERT", "insertBefore": None, "items": chunk, } - await self.async_chromecast_command(self.__send_player_queue, queuedata) + await self.chromecast_command(self.__send_player_queue, queuedata) def __create_queue_items(self, tracks) -> None: """Create list of CC queue items from tracks.""" @@ -424,7 +415,7 @@ class ChromecastPlayer(Player): "streamType": "LIVE" if player_queue.use_queue_stream else "BUFFERED", "metadata": { "title": track.name, - "artist": track.artists[0].name if track.artists else "", + "artist": next(iter(track.artists)).name if track.artists else "", }, "duration": int(track.duration), }, @@ -449,7 +440,7 @@ class ChromecastPlayer(Player): else: send_queue() - async def async_chromecast_command(self, func, *args, **kwargs): + async def chromecast_command(self, func, *args, **kwargs): """Execute command on Chromecast.""" # Chromecast socket really doesn't like multiple commands arriving at the same time # so we apply some throtling. diff --git a/music_assistant/providers/fanarttv/__init__.py b/music_assistant/providers/fanarttv/__init__.py index 55c0a984..3856e07c 100755 --- a/music_assistant/providers/fanarttv/__init__.py +++ b/music_assistant/providers/fanarttv/__init__.py @@ -20,10 +20,10 @@ LOGGER = logging.getLogger(PROV_ID) CONFIG_ENTRIES = [] -async def async_setup(mass) -> None: +async def setup(mass) -> None: """Perform async setup of this Plugin/Provider.""" prov = FanartTvProvider(mass) - await mass.async_register_provider(prov) + await mass.register_provider(prov) class FanartTvProvider(MetadataProvider): @@ -34,7 +34,7 @@ class FanartTvProvider(MetadataProvider): self.mass = mass self.throttler = Throttler(rate_limit=1, period=2) - async def async_on_start(self) -> bool: + async def on_start(self) -> bool: """ Handle initialization of the provider based on config. @@ -57,10 +57,10 @@ class FanartTvProvider(MetadataProvider): """Return Config Entries for this provider.""" return CONFIG_ENTRIES - async def async_get_artist_images(self, mb_artist_id: str) -> Dict: + async def get_artist_images(self, mb_artist_id: str) -> Dict: """Retrieve images by musicbrainz artist id.""" metadata = {} - data = await self.__async_get_data("music/%s" % mb_artist_id) + data = await self._get_data("music/%s" % mb_artist_id) if data: if data.get("hdmusiclogo"): metadata["logo"] = data["hdmusiclogo"][0]["url"] @@ -79,7 +79,7 @@ class FanartTvProvider(MetadataProvider): metadata["banner"] = data["musicbanner"][0]["url"] return metadata - async def __async_get_data(self, endpoint, params=None): + async def _get_data(self, endpoint, params=None): """Get data from api.""" if params is None: params = {} diff --git a/music_assistant/providers/file/__init__.py b/music_assistant/providers/file/__init__.py index 75fca69a..68a11b12 100644 --- a/music_assistant/providers/file/__init__.py +++ b/music_assistant/providers/file/__init__.py @@ -42,10 +42,10 @@ CONFIG_ENTRIES = [ ] -async def async_setup(mass): +async def setup(mass): """Perform async setup of this Plugin/Provider.""" prov = FileProvider() - await mass.async_register_provider(prov) + await mass.register_provider(prov) class FileProvider(MusicProvider): @@ -85,7 +85,7 @@ class FileProvider(MusicProvider): """Return MediaTypes the provider supports.""" return [MediaType.Album, MediaType.Artist, MediaType.Playlist, MediaType.Track] - async def async_on_start(self) -> bool: + async def on_start(self) -> bool: """Handle initialization of the provider based on config.""" conf = self.mass.config.get_provider_config(self.id) if not conf[CONF_MUSIC_DIR]: @@ -98,11 +98,11 @@ class FileProvider(MusicProvider): else: self._playlists_dir = None - async def async_on_stop(self): + async def on_stop(self): """Handle correct close/cleanup of the provider on exit.""" # nothing to be done - async def async_search( + async def search( self, search_query: str, media_types=Optional[List[MediaType]], limit: int = 5 ) -> SearchResult: """ @@ -116,7 +116,7 @@ class FileProvider(MusicProvider): # TODO ! return result - async def async_get_library_artists(self) -> List[Artist]: + async def get_library_artists(self) -> List[Artist]: """Retrieve all library artists.""" if not os.path.isdir(self._music_dir): LOGGER.error("music path does not exist: %s", self._music_dir) @@ -125,24 +125,24 @@ class FileProvider(MusicProvider): for dirname in os.listdir(self._music_dir): dirpath = os.path.join(self._music_dir, dirname) if os.path.isdir(dirpath) and not dirpath.startswith("."): - artist = await self.async_get_artist(dirpath) + artist = await self.get_artist(dirpath) if artist: yield artist - async def async_get_library_albums(self) -> List[Album]: + async def get_library_albums(self) -> List[Album]: """Get album folders recursively.""" - async for artist in self.async_get_library_artists(): - async for album in self.async_get_artist_albums(artist.item_id): + async for artist in self.get_library_artists(): + async for album in self.get_artist_albums(artist.item_id): yield album - async def async_get_library_tracks(self) -> List[Track]: + async def get_library_tracks(self) -> List[Track]: """Get all tracks recursively.""" # TODO: support disk subfolders - async for album in self.async_get_library_albums(): - async for track in self.async_get_album_tracks(album.item_id): + async for album in self.get_library_albums(): + async for track in self.get_album_tracks(album.item_id): yield track - async def async_get_library_playlists(self) -> List[Playlist]: + async def get_library_playlists(self) -> List[Playlist]: """Retrieve playlists from disk.""" if not self._playlists_dir: yield None @@ -154,11 +154,11 @@ class FileProvider(MusicProvider): and not filename.startswith(".") and filename.lower().endswith(".m3u") ): - playlist = await self.async_get_playlist(filepath) + playlist = await self.get_playlist(filepath) if playlist: yield playlist - async def async_get_artist(self, prov_artist_id: str) -> Artist: + async def get_artist(self, prov_artist_id: str) -> Artist: """Get full artist details by id.""" if os.sep not in prov_artist_id: itempath = base64.b64decode(prov_artist_id).decode("utf-8") @@ -173,12 +173,12 @@ class FileProvider(MusicProvider): artist.item_id = prov_artist_id artist.provider = PROV_ID artist.name = name - artist.provider_ids.append( + artist.provider_ids.add( MediaItemProviderId(provider=PROV_ID, item_id=artist.item_id) ) return artist - async def async_get_album(self, prov_album_id: str) -> Album: + async def get_album(self, prov_album_id: str) -> Album: """Get full album details by id.""" if os.sep not in prov_album_id: itempath = base64.b64decode(prov_album_id).decode("utf-8") @@ -194,15 +194,15 @@ class FileProvider(MusicProvider): album.item_id = prov_album_id album.provider = PROV_ID album.name, album.version = parse_title_and_version(name) - album.artist = await self.async_get_artist(artistpath) + album.artist = await self.get_artist(artistpath) if not album.artist: raise Exception("No album artist ! %s" % artistpath) - album.provider_ids.append( + album.provider_ids.add( MediaItemProviderId(provider=PROV_ID, item_id=prov_album_id) ) return album - async def async_get_track(self, prov_track_id: str) -> Track: + async def get_track(self, prov_track_id: str) -> Track: """Get full track details by id.""" if os.sep not in prov_track_id: itempath = base64.b64decode(prov_track_id).decode("utf-8") @@ -211,9 +211,9 @@ class FileProvider(MusicProvider): if not os.path.isfile(itempath): LOGGER.error("track path does not exist: %s", itempath) return None - return await self.__async_parse_track(itempath) + return await self._parse_track(itempath) - async def async_get_playlist(self, prov_playlist_id: str) -> Playlist: + async def get_playlist(self, prov_playlist_id: str) -> Playlist: """Get full playlist details by id.""" if os.sep not in prov_playlist_id: itempath = base64.b64decode(prov_playlist_id).decode("utf-8") @@ -230,14 +230,14 @@ class FileProvider(MusicProvider): playlist.provider = PROV_ID playlist.name = itempath.split(os.sep)[-1].replace(".m3u", "") playlist.is_editable = True - playlist.provider_ids.append( + playlist.provider_ids.add( MediaItemProviderId(provider=PROV_ID, item_id=prov_playlist_id) ) playlist.owner = "disk" playlist.checksum = os.path.getmtime(itempath) return playlist - async def async_get_album_tracks(self, prov_album_id) -> List[Track]: + async def get_album_tracks(self, prov_album_id) -> List[Track]: """Get album tracks for given album id.""" if os.sep not in prov_album_id: albumpath = base64.b64decode(prov_album_id).decode("utf-8") @@ -246,16 +246,16 @@ class FileProvider(MusicProvider): if not os.path.isdir(albumpath): LOGGER.error("album path does not exist: %s", albumpath) return - album = await self.async_get_album(albumpath) + album = await self.get_album(albumpath) for filename in os.listdir(albumpath): filepath = os.path.join(albumpath, filename) if os.path.isfile(filepath) and not filepath.startswith("."): - track = await self.__async_parse_track(filepath) + track = await self._parse_track(filepath) if track: track.album = album yield track - async def async_get_playlist_tracks(self, prov_playlist_id: str) -> List[Track]: + async def get_playlist_tracks(self, prov_playlist_id: str) -> List[Track]: """Get playlist tracks for given playlist id.""" if os.sep not in prov_playlist_id: itempath = base64.b64decode(prov_playlist_id).decode("utf-8") @@ -269,12 +269,12 @@ class FileProvider(MusicProvider): for line in _file.readlines(): line = line.strip() if line and not line.startswith("#"): - track = await self.__async_parse_track_from_uri(line) + track = await self._parse_track_from_uri(line) if track: yield track index += 1 - async def async_get_artist_albums(self, prov_artist_id: str) -> List[Album]: + async def get_artist_albums(self, prov_artist_id: str) -> List[Album]: """Get a list of albums for the given artist.""" if os.sep not in prov_artist_id: artistpath = base64.b64decode(prov_artist_id).decode("utf-8") @@ -286,17 +286,17 @@ class FileProvider(MusicProvider): for dirname in os.listdir(artistpath): dirpath = os.path.join(artistpath, dirname) if os.path.isdir(dirpath) and not dirpath.startswith("."): - album = await self.async_get_album(dirpath) + album = await self.get_album(dirpath) if album: yield album - async def async_get_artist_toptracks(self, prov_artist_id: str) -> List[Track]: + async def get_artist_toptracks(self, prov_artist_id: str) -> List[Track]: """Get a list of random tracks as we have no clue about preference.""" - async for album in self.async_get_artist_albums(prov_artist_id): - async for track in self.async_get_album_tracks(album.item_id): + async for album in self.get_artist_albums(prov_artist_id): + async for track in self.get_album_tracks(album.item_id): yield track - async def async_get_stream_details(self, item_id: str) -> StreamDetails: + async def get_stream_details(self, item_id: str) -> StreamDetails: """Return the content details for the given track when it will be streamed.""" if os.sep not in item_id: track_id = base64.b64decode(item_id).decode("utf-8") @@ -313,7 +313,7 @@ class FileProvider(MusicProvider): bit_depth=16, ) - async def __async_parse_track(self, filename): + async def _parse_track(self, filename): """Try to parse a track from a filename with taglib.""" track = Track() # pylint: disable=broad-except @@ -328,18 +328,18 @@ class FileProvider(MusicProvider): name = song.tags["TITLE"][0] track.name, track.version = parse_title_and_version(name) albumpath = filename.rsplit(os.sep, 1)[0] - track.album = await self.async_get_album(albumpath) - artists = [] + track.album = await self.get_album(albumpath) + artists = set() for artist_str in song.tags["ARTIST"]: local_artist_path = os.path.join(self._music_dir, artist_str) if os.path.isfile(local_artist_path): - artist = await self.async_get_artist(local_artist_path) + artist = await self.get_artist(local_artist_path) else: artist = Artist() artist.name = artist_str fake_artistpath = os.path.join(self._music_dir, artist_str) artist.item_id = fake_artistpath # temporary id - artist.provider_ids.append( + artist.provider_ids.add( MediaItemProviderId( provider=PROV_ID, item_id=base64.b64encode( @@ -347,7 +347,7 @@ class FileProvider(MusicProvider): ).decode("utf-8"), ) ) - artists.append(artist) + artists.add(artist) track.artists = artists if "GENRE" in song.tags: track.metadata["genres"] = song.tags["GENRE"] @@ -377,7 +377,7 @@ class FileProvider(MusicProvider): else: quality = TrackQuality.LOSSY_MP3 quality_details = "%s kbps" % (song.bitrate) - track.provider_ids.append( + track.provider_ids.add( MediaItemProviderId( provider=PROV_ID, item_id=prov_item_id, @@ -387,7 +387,7 @@ class FileProvider(MusicProvider): ) return track - async def __async_parse_track_from_uri(self, uri): + async def _parse_track_from_uri(self, uri): """Try to parse a track from an uri found in playlist.""" # pylint: disable=broad-except if "://" in uri: @@ -395,16 +395,16 @@ class FileProvider(MusicProvider): prov_id = uri.split("://")[0] prov_item_id = uri.split("/")[-1].split(".")[0].split(":")[-1] try: - return await self.mass.music.async_get_track(prov_item_id, prov_id) + return await self.mass.music.get_track(prov_item_id, prov_id) except Exception as exc: LOGGER.warning("Could not parse uri %s to track: %s", uri, str(exc)) return None # try to treat uri as filename # TODO: filename could be related to musicdir or full path - track = await self.async_get_track(uri) + track = await self.get_track(uri) if track: return track - track = await self.async_get_track(os.path.join(self._music_dir, uri)) + track = await self.get_track(os.path.join(self._music_dir, uri)) if track: return track return None diff --git a/music_assistant/providers/qobuz/__init__.py b/music_assistant/providers/qobuz/__init__.py index 7557310d..9ddf77ee 100644 --- a/music_assistant/providers/qobuz/__init__.py +++ b/music_assistant/providers/qobuz/__init__.py @@ -48,10 +48,10 @@ CONFIG_ENTRIES = [ ] -async def async_setup(mass): +async def setup(mass): """Perform async setup of this Plugin/Provider.""" prov = QobuzProvider() - await mass.async_register_provider(prov) + await mass.register_provider(prov) class QobuzProvider(MusicProvider): @@ -81,7 +81,7 @@ class QobuzProvider(MusicProvider): """Return MediaTypes the provider supports.""" return [MediaType.Album, MediaType.Artist, MediaType.Playlist, MediaType.Track] - async def async_on_start(self) -> bool: + async def on_start(self) -> bool: """Handle initialization of the provider based on config.""" # pylint: disable=attribute-defined-outside-init config = self.mass.config.get_provider_config(self.id) @@ -95,11 +95,11 @@ class QobuzProvider(MusicProvider): self.__logged_in = False self._throttler = Throttler(rate_limit=4, period=1) self.mass.add_event_listener( - self.async_mass_event, [EVENT_STREAM_STARTED, EVENT_STREAM_ENDED] + self.mass_event, (EVENT_STREAM_STARTED, EVENT_STREAM_ENDED) ) return True - async def async_search( + async def search( self, search_query: str, media_types=Optional[List[MediaType]], limit: int = 5 ) -> SearchResult: """ @@ -121,151 +121,147 @@ class QobuzProvider(MusicProvider): params["type"] = "tracks" if media_types[0] == MediaType.Playlist: params["type"] = "playlists" - searchresult = await self.__async_get_data("catalog/search", params) + searchresult = await self._get_data("catalog/search", params) if searchresult: if "artists" in searchresult: result.artists = [ - await self.__async_parse_artist(item) + await self._parse_artist(item) for item in searchresult["artists"]["items"] if (item and item["id"]) ] if "albums" in searchresult: result.albums = [ - await self.__async_parse_album(item) + await self._parse_album(item) for item in searchresult["albums"]["items"] if (item and item["id"]) ] if "tracks" in searchresult: result.tracks = [ - await self.__async_parse_track(item) + await self._parse_track(item) for item in searchresult["tracks"]["items"] if (item and item["id"]) ] if "playlists" in searchresult: result.playlists = [ - await self.__async_parse_playlist(item) + await self._parse_playlist(item) for item in searchresult["playlists"]["items"] if (item and item["id"]) ] return result - async def async_get_library_artists(self) -> List[Artist]: + async def get_library_artists(self) -> List[Artist]: """Retrieve all library artists from Qobuz.""" params = {"type": "artists"} endpoint = "favorite/getUserFavorites" return [ - await self.__async_parse_artist(item) - for item in await self.__async_get_all_items( - endpoint, params, key="artists" - ) + await self._parse_artist(item) + for item in await self._get_all_items(endpoint, params, key="artists") if (item and item["id"]) ] - async def async_get_library_albums(self) -> List[Album]: + async def get_library_albums(self) -> List[Album]: """Retrieve all library albums from Qobuz.""" params = {"type": "albums"} endpoint = "favorite/getUserFavorites" return [ - await self.__async_parse_album(item) - for item in await self.__async_get_all_items(endpoint, params, key="albums") + await self._parse_album(item) + for item in await self._get_all_items(endpoint, params, key="albums") if (item and item["id"]) ] - async def async_get_library_tracks(self) -> List[Track]: + async def get_library_tracks(self) -> List[Track]: """Retrieve library tracks from Qobuz.""" params = {"type": "tracks"} endpoint = "favorite/getUserFavorites" return [ - await self.__async_parse_track(item) - for item in await self.__async_get_all_items(endpoint, params, key="tracks") + await self._parse_track(item) + for item in await self._get_all_items(endpoint, params, key="tracks") if (item and item["id"]) ] - async def async_get_library_playlists(self) -> List[Playlist]: + async def get_library_playlists(self) -> List[Playlist]: """Retrieve all library playlists from the provider.""" endpoint = "playlist/getUserPlaylists" return [ - await self.__async_parse_playlist(item) - for item in await self.__async_get_all_items(endpoint, key="playlists") + await self._parse_playlist(item) + for item in await self._get_all_items(endpoint, key="playlists") if (item and item["id"]) ] - async def async_get_radios(self) -> List[Radio]: + async def get_radios(self) -> List[Radio]: """Retrieve library/subscribed radio stations from the provider.""" return [] # TODO - async def async_get_artist(self, prov_artist_id) -> Artist: + async def get_artist(self, prov_artist_id) -> Artist: """Get full artist details by id.""" params = {"artist_id": prov_artist_id} - artist_obj = await self.__async_get_data("artist/get", params) + artist_obj = await self._get_data("artist/get", params) return ( - await self.__async_parse_artist(artist_obj) + await self._parse_artist(artist_obj) if artist_obj and artist_obj["id"] else None ) - async def async_get_album(self, prov_album_id) -> Album: + async def get_album(self, prov_album_id) -> Album: """Get full album details by id.""" params = {"album_id": prov_album_id} - album_obj = await self.__async_get_data("album/get", params) + album_obj = await self._get_data("album/get", params) return ( - await self.__async_parse_album(album_obj) + await self._parse_album(album_obj) if album_obj and album_obj["id"] else None ) - async def async_get_track(self, prov_track_id) -> Track: + async def get_track(self, prov_track_id) -> Track: """Get full track details by id.""" params = {"track_id": prov_track_id} - track_obj = await self.__async_get_data("track/get", params) + track_obj = await self._get_data("track/get", params) return ( - await self.__async_parse_track(track_obj) + await self._parse_track(track_obj) if track_obj and track_obj["id"] else None ) - async def async_get_playlist(self, prov_playlist_id) -> Playlist: + async def get_playlist(self, prov_playlist_id) -> Playlist: """Get full playlist details by id.""" params = {"playlist_id": prov_playlist_id} - playlist_obj = await self.__async_get_data("playlist/get", params) + playlist_obj = await self._get_data("playlist/get", params) return ( - await self.__async_parse_playlist(playlist_obj) + await self._parse_playlist(playlist_obj) if playlist_obj and playlist_obj["id"] else None ) - async def async_get_album_tracks(self, prov_album_id) -> List[Track]: + async def get_album_tracks(self, prov_album_id) -> List[Track]: """Get all album tracks for given album id.""" params = {"album_id": prov_album_id} return [ - await self.__async_parse_track(item) - for item in await self.__async_get_all_items( - "album/get", params, key="tracks" - ) + await self._parse_track(item) + for item in await self._get_all_items("album/get", params, key="tracks") if (item and item["id"]) ] - async def async_get_playlist_tracks(self, prov_playlist_id) -> List[Track]: + async def get_playlist_tracks(self, prov_playlist_id) -> List[Track]: """Get all playlist tracks for given playlist id.""" params = {"playlist_id": prov_playlist_id, "extra": "tracks"} endpoint = "playlist/get" return [ - await self.__async_parse_track(item) - for item in await self.__async_get_all_items(endpoint, params, key="tracks") + await self._parse_track(item) + for item in await self._get_all_items(endpoint, params, key="tracks") if (item and item["id"]) ] - async def async_get_artist_albums(self, prov_artist_id) -> List[Album]: + async def get_artist_albums(self, prov_artist_id) -> List[Album]: """Get a list of albums for the given artist.""" params = {"artist_id": prov_artist_id, "extra": "albums"} endpoint = "artist/get" return [ - await self.__async_parse_album(item) - for item in await self.__async_get_all_items(endpoint, params, key="albums") + await self._parse_album(item) + for item in await self._get_all_items(endpoint, params, key="albums") if (item and item["id"]) ] - async def async_get_artist_toptracks(self, prov_artist_id) -> List[Track]: + async def get_artist_toptracks(self, prov_artist_id) -> List[Track]: """Get a list of most popular tracks for the given artist.""" params = { "artist_id": prov_artist_id, @@ -273,19 +269,19 @@ class QobuzProvider(MusicProvider): "offset": 0, "limit": 25, } - result = await self.__async_get_data("artist/get", params) + result = await self._get_data("artist/get", params) if result and result["playlists"]: return [ - await self.__async_parse_track(item) + await self._parse_track(item) for item in result["playlists"][0]["tracks"]["items"] if (item and item["id"]) ] # fallback to search - artist = await self.async_get_artist(prov_artist_id) + artist = await self.get_artist(prov_artist_id) params = {"query": artist.name, "limit": 25, "type": "tracks"} - searchresult = await self.__async_get_data("catalog/search", params) + searchresult = await self._get_data("catalog/search", params) return [ - await self.__async_parse_track(item) + await self._parse_track(item) for item in searchresult["tracks"]["items"] if ( item @@ -295,91 +291,87 @@ class QobuzProvider(MusicProvider): ) ] - async def async_get_similar_artists(self, prov_artist_id): + async def get_similar_artists(self, prov_artist_id): """Get similar artists for given artist.""" # https://www.qobuz.com/api.json/0.2/artist/getSimilarArtists?artist_id=220020&offset=0&limit=3 - async def async_library_add(self, prov_item_id, media_type: MediaType): + async def library_add(self, prov_item_id, media_type: MediaType): """Add item to library.""" result = None if media_type == MediaType.Artist: - result = await self.__async_get_data( + result = await self._get_data( "favorite/create", {"artist_ids": prov_item_id} ) elif media_type == MediaType.Album: - result = await self.__async_get_data( + result = await self._get_data( "favorite/create", {"album_ids": prov_item_id} ) elif media_type == MediaType.Track: - result = await self.__async_get_data( + result = await self._get_data( "favorite/create", {"track_ids": prov_item_id} ) elif media_type == MediaType.Playlist: - result = await self.__async_get_data( + result = await self._get_data( "playlist/subscribe", {"playlist_id": prov_item_id} ) return result - async def async_library_remove(self, prov_item_id, media_type: MediaType): + async def library_remove(self, prov_item_id, media_type: MediaType): """Remove item from library.""" result = None if media_type == MediaType.Artist: - result = await self.__async_get_data( + result = await self._get_data( "favorite/delete", {"artist_ids": prov_item_id} ) elif media_type == MediaType.Album: - result = await self.__async_get_data( + result = await self._get_data( "favorite/delete", {"album_ids": prov_item_id} ) elif media_type == MediaType.Track: - result = await self.__async_get_data( + result = await self._get_data( "favorite/delete", {"track_ids": prov_item_id} ) elif media_type == MediaType.Playlist: - playlist = await self.async_get_playlist(prov_item_id) + playlist = await self.get_playlist(prov_item_id) if playlist.is_editable: - result = await self.__async_get_data( + result = await self._get_data( "playlist/delete", {"playlist_id": prov_item_id} ) else: - result = await self.__async_get_data( + result = await self._get_data( "playlist/unsubscribe", {"playlist_id": prov_item_id} ) return result - async def async_add_playlist_tracks(self, prov_playlist_id, prov_track_ids): + async def add_playlist_tracks(self, prov_playlist_id, prov_track_ids): """Add track(s) to playlist.""" params = { "playlist_id": prov_playlist_id, "track_ids": ",".join(prov_track_ids), } - return await self.__async_get_data("playlist/addTracks", params) + return await self._get_data("playlist/addTracks", params) - async def async_remove_playlist_tracks(self, prov_playlist_id, prov_track_ids): + async def remove_playlist_tracks(self, prov_playlist_id, prov_track_ids): """Remove track(s) from playlist.""" - playlist_track_ids = [] + playlist_track_ids = set() 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._get_all_items("playlist/get", params, key="tracks"): if track["id"] in prov_track_ids: - playlist_track_ids.append(track["playlist_track_id"]) + playlist_track_ids.add(track["playlist_track_id"]) params = { "playlist_id": prov_playlist_id, "track_ids": ",".join(playlist_track_ids), } - return await self.__async_get_data("playlist/deleteTracks", params) + return await self._get_data("playlist/deleteTracks", params) - async def async_get_stream_details(self, item_id: str) -> StreamDetails: + async def get_stream_details(self, item_id: str) -> StreamDetails: """Return the content details for the given track when it will be streamed.""" streamdata = 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": item_id, "intent": "stream"} - result = await self.__async_get_data( - "track/getFileUrl", params, sign_request=True - ) + result = await self._get_data("track/getFileUrl", params, sign_request=True) if result and result.get("url"): streamdata = result break @@ -404,7 +396,7 @@ class QobuzProvider(MusicProvider): details=streamdata, # we need these details for reporting playback ) - async def async_mass_event(self, msg, msg_details): + async def mass_event(self, msg, msg_details): """ Received event from mass. @@ -437,7 +429,7 @@ class QobuzProvider(MusicProvider): "format_id": format_id, } ] - await self.__async_post_data("track/reportStreamingStart", data=events) + await self._post_data("track/reportStreamingStart", data=events) elif msg == EVENT_STREAM_ENDED and msg_details.provider == PROV_ID: # report streaming ended to qobuz # if msg_details.details < 6: @@ -448,14 +440,14 @@ class QobuzProvider(MusicProvider): "track_id": str(msg_details.item_id), "duration": try_parse_int(msg_details.seconds_played), } - await self.__async_get_data("/track/reportStreamingEnd", params) + await self._get_data("/track/reportStreamingEnd", params) - async def __async_parse_artist(self, artist_obj): + async def _parse_artist(self, artist_obj): """Parse qobuz artist object to generic layout.""" artist = Artist( item_id=str(artist_obj["id"]), provider=PROV_ID, name=artist_obj["name"] ) - artist.provider_ids.append( + artist.provider_ids.add( MediaItemProviderId(provider=PROV_ID, item_id=str(artist_obj["id"])) ) artist.metadata["image"] = self.__get_image(artist_obj) @@ -465,7 +457,7 @@ class QobuzProvider(MusicProvider): artist.metadata["qobuz_url"] = artist_obj["url"] return artist - async def __async_parse_album(self, album_obj: dict, artist_obj: dict = None): + async def _parse_album(self, album_obj: dict, artist_obj: dict = None): """Parse qobuz album object to generic layout.""" album = Album(item_id=str(album_obj["id"]), provider=PROV_ID) if album_obj["maximum_sampling_rate"] > 192: @@ -480,7 +472,7 @@ class QobuzProvider(MusicProvider): quality = TrackQuality.LOSSY_AAC else: quality = TrackQuality.FLAC_LOSSLESS - album.provider_ids.append( + album.provider_ids.add( MediaItemProviderId( provider=PROV_ID, item_id=str(album_obj["id"]), @@ -495,7 +487,7 @@ class QobuzProvider(MusicProvider): if artist_obj: album.artist = artist_obj else: - album.artist = await self.__async_parse_artist(album_obj["artist"]) + album.artist = await self._parse_artist(album_obj["artist"]) if ( album_obj.get("product_type", "") == "single" or album_obj.get("release_type", "") == "single" @@ -534,7 +526,7 @@ class QobuzProvider(MusicProvider): album.metadata["description"] = album_obj["description"] return album - async def __async_parse_track(self, track_obj): + async def _parse_track(self, track_obj): """Parse qobuz track object to generic layout.""" track = Track( item_id=str(track_obj["id"]), @@ -544,9 +536,9 @@ class QobuzProvider(MusicProvider): duration=track_obj["duration"], ) if track_obj.get("performer") and "Various " not in track_obj["performer"]: - artist = await self.__async_parse_artist(track_obj["performer"]) + artist = await self._parse_artist(track_obj["performer"]) if artist: - track.artists.append(artist) + track.artists.add(artist) if not track.artists: # try to grab artist from album if ( @@ -554,9 +546,9 @@ class QobuzProvider(MusicProvider): and track_obj["album"].get("artist") and "Various " not in track_obj["album"]["artist"] ): - artist = await self.__async_parse_artist(track_obj["album"]["artist"]) + artist = await self._parse_artist(track_obj["album"]["artist"]) if artist: - track.artists.append(artist) + track.artists.add(artist) if not track.artists: # last resort: parse from performers string for performer_str in track_obj["performers"].split(" - "): @@ -566,13 +558,13 @@ class QobuzProvider(MusicProvider): artist = Artist() artist.name = name artist.item_id = name - track.artists.append(artist) + track.artists.add(artist) # TODO: fix grabbing composer from details track.name, track.version = parse_title_and_version( track_obj["title"], track_obj.get("version") ) if "album" in track_obj: - album = await self.__async_parse_album(track_obj["album"]) + album = await self._parse_album(track_obj["album"]) if album: track.album = album if track_obj.get("hires"): @@ -605,7 +597,7 @@ class QobuzProvider(MusicProvider): quality = TrackQuality.LOSSY_AAC else: quality = TrackQuality.FLAC_LOSSLESS - track.provider_ids.append( + track.provider_ids.add( MediaItemProviderId( provider=PROV_ID, item_id=str(track_obj["id"]), @@ -616,7 +608,7 @@ class QobuzProvider(MusicProvider): ) return track - async def __async_parse_playlist(self, playlist_obj): + async def _parse_playlist(self, playlist_obj): """Parse qobuz playlist object to generic layout.""" playlist = Playlist( item_id=playlist_obj["id"], @@ -624,7 +616,7 @@ class QobuzProvider(MusicProvider): name=playlist_obj["name"], owner=playlist_obj["owner"]["name"], ) - playlist.provider_ids.append( + playlist.provider_ids.add( MediaItemProviderId(provider=PROV_ID, item_id=str(playlist_obj["id"])) ) playlist.is_editable = ( @@ -637,7 +629,7 @@ class QobuzProvider(MusicProvider): playlist.checksum = playlist_obj["updated_at"] return playlist - async def __async_auth_token(self): + async def _auth_token(self): """Login to qobuz and store the token.""" if self.__user_auth_info: return self.__user_auth_info["user_auth_token"] @@ -646,7 +638,7 @@ class QobuzProvider(MusicProvider): "password": self.__password, "device_manufacturer_id": "music_assistant", } - details = await self.__async_get_data("user/login", params) + details = await self._get_data("user/login", params) if details and "user" in details: self.__user_auth_info = details LOGGER.info( @@ -654,7 +646,7 @@ class QobuzProvider(MusicProvider): ) return details["user_auth_token"] - async def __async_get_all_items(self, endpoint, params=None, key="tracks"): + async def _get_all_items(self, endpoint, params=None, key="tracks"): """Get all items from a paged list.""" if not params: params = {} @@ -664,7 +656,7 @@ class QobuzProvider(MusicProvider): while True: params["limit"] = limit params["offset"] = offset - result = await self.__async_get_data(endpoint, params=params) + result = await self._get_data(endpoint, params=params) offset += limit if not result: break @@ -675,14 +667,14 @@ class QobuzProvider(MusicProvider): break return all_items - async def __async_get_data(self, endpoint, params=None, sign_request=False): + async def _get_data(self, endpoint, params=None, sign_request=False): """Get data from api.""" if not params: params = {} url = "http://www.qobuz.com/api.json/0.2/%s" % endpoint headers = {"X-App-Id": get_app_var(0)} if endpoint != "user/login": - auth_token = await self.__async_auth_token() + auth_token = await self._auth_token() if not auth_token: LOGGER.debug("Not logged in") return None @@ -699,7 +691,7 @@ class QobuzProvider(MusicProvider): params["request_ts"] = request_ts params["request_sig"] = request_sig params["app_id"] = get_app_var(0) - params["user_auth_token"] = await self.__async_auth_token() + params["user_auth_token"] = await self._auth_token() async with self._throttler: async with self.mass.http_session.get( url, headers=headers, params=params, verify_ssl=False @@ -712,7 +704,7 @@ class QobuzProvider(MusicProvider): return None return result - async def __async_post_data(self, endpoint, params=None, data=None): + async def _post_data(self, endpoint, params=None, data=None): """Post data to api.""" if not params: params = {} @@ -720,7 +712,7 @@ class QobuzProvider(MusicProvider): data = {} url = "http://www.qobuz.com/api.json/0.2/%s" % endpoint params["app_id"] = get_app_var(0) - params["user_auth_token"] = await self.__async_auth_token() + params["user_auth_token"] = await self._auth_token() async with self.mass.http_session.post( url, params=params, json=data, verify_ssl=False ) as response: diff --git a/music_assistant/providers/sonos/__init__.py b/music_assistant/providers/sonos/__init__.py index 71f69096..782c3451 100644 --- a/music_assistant/providers/sonos/__init__.py +++ b/music_assistant/providers/sonos/__init__.py @@ -3,7 +3,7 @@ from .sonos import SonosProvider -async def async_setup(mass): +async def setup(mass): """Perform async setup of this Plugin/Provider.""" prov = SonosProvider() - await mass.async_register_provider(prov) + await mass.register_provider(prov) diff --git a/music_assistant/providers/sonos/sonos.py b/music_assistant/providers/sonos/sonos.py index 8d8603bd..aae18158 100644 --- a/music_assistant/providers/sonos/sonos.py +++ b/music_assistant/providers/sonos/sonos.py @@ -51,16 +51,16 @@ class SonosProvider(PlayerProvider): """Return Config Entries for this provider.""" return CONFIG_ENTRIES - async def async_on_start(self) -> bool: + async def on_start(self) -> bool: """Handle initialization of the provider.""" - self._tasks.append(self.mass.add_job(self.__async_periodic_discovery())) + self._tasks.append(self.mass.add_job(self._periodic_discovery())) - async def async_on_stop(self): + async def on_stop(self): """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): + async def cmd_play_uri(self, player_id: str, uri: str): """ Play the specified uri/url on the goven player. @@ -72,7 +72,7 @@ class SonosProvider(PlayerProvider): else: LOGGER.warning("Received command for unavailable player: %s", player_id) - async def async_cmd_stop(self, player_id: str) -> None: + async def cmd_stop(self, player_id: str) -> None: """ Send STOP command to given player. @@ -84,7 +84,7 @@ class SonosProvider(PlayerProvider): else: LOGGER.warning("Received command for unavailable player: %s", player_id) - async def async_cmd_play(self, player_id: str) -> None: + async def cmd_play(self, player_id: str) -> None: """ Send STOP command to given player. @@ -96,7 +96,7 @@ class SonosProvider(PlayerProvider): else: LOGGER.warning("Received command for unavailable player: %s", player_id) - async def async_cmd_pause(self, player_id: str): + async def cmd_pause(self, player_id: str): """ Send PAUSE command to given player. @@ -108,7 +108,7 @@ class SonosProvider(PlayerProvider): else: LOGGER.warning("Received command for unavailable player: %s", player_id) - async def async_cmd_next(self, player_id: str): + async def cmd_next(self, player_id: str): """ Send NEXT TRACK command to given player. @@ -120,7 +120,7 @@ class SonosProvider(PlayerProvider): else: LOGGER.warning("Received command for unavailable player: %s", player_id) - async def async_cmd_previous(self, player_id: str): + async def cmd_previous(self, player_id: str): """ Send PREVIOUS TRACK command to given player. @@ -132,7 +132,7 @@ class SonosProvider(PlayerProvider): else: LOGGER.warning("Received command for unavailable player: %s", player_id) - async def async_cmd_power_on(self, player_id: str) -> None: + async def cmd_power_on(self, player_id: str) -> None: """ Send POWER ON command to given player. @@ -146,7 +146,7 @@ class SonosProvider(PlayerProvider): else: LOGGER.warning("Received command for unavailable player: %s", player_id) - async def async_cmd_power_off(self, player_id: str) -> None: + async def cmd_power_off(self, player_id: str) -> None: """ Send POWER OFF command to given player. @@ -160,7 +160,7 @@ class SonosProvider(PlayerProvider): else: LOGGER.warning("Received command for unavailable player: %s", player_id) - async def async_cmd_volume_set(self, player_id: str, volume_level: int) -> None: + async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: """ Send volume level command to given player. @@ -173,7 +173,7 @@ class SonosProvider(PlayerProvider): else: LOGGER.warning("Received command for unavailable player: %s", player_id) - async def async_cmd_volume_mute(self, player_id: str, is_muted=False): + async def cmd_volume_mute(self, player_id: str, is_muted=False): """ Send volume MUTE command to given player. @@ -186,7 +186,7 @@ class SonosProvider(PlayerProvider): else: LOGGER.warning("Received command for unavailable player: %s", player_id) - async def async_cmd_queue_play_index(self, player_id: str, index: int): + async def cmd_queue_play_index(self, player_id: str, index: int): """ Play item at index X on player's queue. @@ -199,7 +199,7 @@ class SonosProvider(PlayerProvider): else: LOGGER.warning("Received command for unavailable player: %s", player_id) - async def async_cmd_queue_load(self, player_id: str, queue_items: List[QueueItem]): + async def cmd_queue_load(self, player_id: str, queue_items: List[QueueItem]): """ Load/overwrite given items in the player's queue implementation. @@ -214,7 +214,7 @@ class SonosProvider(PlayerProvider): else: LOGGER.warning("Received command for unavailable player: %s", player_id) - async def async_cmd_queue_insert( + async def cmd_queue_insert( self, player_id: str, queue_items: List[QueueItem], insert_at_index: int ): """ @@ -234,9 +234,7 @@ class SonosProvider(PlayerProvider): 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 cmd_queue_append(self, player_id: str, queue_items: List[QueueItem]): """ Append new items at the end of the queue. @@ -245,12 +243,12 @@ class SonosProvider(PlayerProvider): """ player_queue = self.mass.players.get_player_queue(player_id) if player_queue: - return await self.async_cmd_queue_insert( + return await self.cmd_queue_insert( player_id, queue_items, len(player_queue.items) ) LOGGER.warning("Received command for unavailable player: %s", player_id) - async def async_cmd_queue_clear(self, player_id: str): + async def cmd_queue_clear(self, player_id: str): """ Clear the player's queue. @@ -263,7 +261,7 @@ class SonosProvider(PlayerProvider): LOGGER.warning("Received command for unavailable player: %s", player_id) @run_periodic(1800) - async def __async_periodic_discovery(self): + async def _periodic_discovery(self): """Run Sonos discovery at interval.""" self._tasks.append(self.mass.add_job(None, self.__run_discovery)) @@ -281,9 +279,7 @@ class SonosProvider(PlayerProvider): # remove any disconnected players... for player in list(self._players.values()): if not player.is_group and player.soco.uid not in new_device_ids: - self.mass.add_job( - self.mass.players.async_remove_player(player.player_id) - ) + self.mass.add_job(self.mass.players.remove_player(player.player_id)) for sub in player.subscriptions: sub.unsubscribe() self._players.pop(player, None) @@ -326,7 +322,7 @@ class SonosProvider(PlayerProvider): subscribe(soco_device.avTransport, self.__player_event) subscribe(soco_device.renderingControl, self.__player_event) subscribe(soco_device.zoneGroupTopology, self.__topology_changed) - self.mass.run_task(self.mass.players.async_add_player(player)) + self.mass.run_task(self.mass.players.add_player(player)) return player def __player_event(self, player_id: str, event): @@ -358,8 +354,8 @@ class SonosProvider(PlayerProvider): rel_time = __timespan_secs(position_info.get("RelTime")) player.elapsed_time = rel_time if player.state == PlaybackState.Playing: - self.mass.add_job(self.__async_report_progress(player_id)) - self.mass.add_job(self.mass.players.async_update_player(player)) + self.mass.add_job(self._report_progress(player_id)) + player.update_state() def __process_groups(self, sonos_groups): """Process all sonos groups.""" @@ -375,7 +371,7 @@ 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.players.async_update_player(group_player)) + self.mass.run_task(self.mass.players.update_player(group_player)) async def __topology_changed(self, player_id, event=None): """Received topology changed event from one of the sonos players.""" @@ -383,7 +379,7 @@ class SonosProvider(PlayerProvider): # Schedule discovery to work out the changes. self.mass.add_job(self.__run_discovery) - async def __async_report_progress(self, player_id: str): + async def _report_progress(self, player_id: str): """Report current progress while playing.""" if player_id in self._report_progress_tasks: return # already running diff --git a/music_assistant/providers/spotify/__init__.py b/music_assistant/providers/spotify/__init__.py index d1642a0b..3e835b72 100644 --- a/music_assistant/providers/spotify/__init__.py +++ b/music_assistant/providers/spotify/__init__.py @@ -49,10 +49,10 @@ CONFIG_ENTRIES = [ ] -async def async_setup(mass): +async def setup(mass): """Perform async setup of this Plugin/Provider.""" prov = SpotifyProvider() - await mass.async_register_provider(prov) + await mass.register_provider(prov) class SpotifyProvider(MusicProvider): @@ -91,7 +91,7 @@ class SpotifyProvider(MusicProvider): MediaType.Track, ] - async def async_on_start(self) -> bool: + async def on_start(self) -> bool: """Handle initialization of the provider based on config.""" config = self.mass.config.get_provider_config(self.id) # pylint: disable=attribute-defined-outside-init @@ -104,11 +104,11 @@ class SpotifyProvider(MusicProvider): self._password = config[CONF_PASSWORD] self.__auth_token = {} self._throttler = Throttler(rate_limit=4, period=1) - token = await self.async_get_token() + token = await self.get_token() return token is not None - async def async_search( + async def search( self, search_query: str, media_types=Optional[List[MediaType]], limit: int = 5 ) -> SearchResult: """ @@ -130,196 +130,188 @@ class SpotifyProvider(MusicProvider): searchtypes.append("playlist") searchtype = ",".join(searchtypes) params = {"q": search_query, "type": searchtype, "limit": limit} - searchresult = await self.__async_get_data("search", params=params) + searchresult = await self._get_data("search", params=params) if searchresult: if "artists" in searchresult: result.artists = [ - await self.__async_parse_artist(item) + await self._parse_artist(item) for item in searchresult["artists"]["items"] if (item and item["id"]) ] if "albums" in searchresult: result.albums = [ - await self.__async_parse_album(item) + await self._parse_album(item) for item in searchresult["albums"]["items"] if (item and item["id"]) ] if "tracks" in searchresult: result.tracks = [ - await self.__async_parse_track(item) + await self._parse_track(item) for item in searchresult["tracks"]["items"] if (item and item["id"]) ] if "playlists" in searchresult: result.playlists = [ - await self.__async_parse_playlist(item) + await self._parse_playlist(item) for item in searchresult["playlists"]["items"] if (item and item["id"]) ] return result - async def async_get_library_artists(self) -> List[Artist]: + async def get_library_artists(self) -> List[Artist]: """Retrieve library artists from spotify.""" - spotify_artists = await self.__async_get_data( - "me/following?type=artist&limit=50" - ) + spotify_artists = await self._get_data("me/following?type=artist&limit=50") return [ - await self.__async_parse_artist(item) + await self._parse_artist(item) for item in spotify_artists["artists"]["items"] if (item and item["id"]) ] - async def async_get_library_albums(self) -> List[Album]: + async def get_library_albums(self) -> List[Album]: """Retrieve library albums from the provider.""" return [ - await self.__async_parse_album(item["album"]) - for item in await self.__async_get_all_items("me/albums") + await self._parse_album(item["album"]) + for item in await self._get_all_items("me/albums") if (item["album"] and item["album"]["id"]) ] - async def async_get_library_tracks(self) -> List[Track]: + async def get_library_tracks(self) -> List[Track]: """Retrieve library tracks from the provider.""" return [ - await self.__async_parse_track(item["track"]) - for item in await self.__async_get_all_items("me/tracks") + await self._parse_track(item["track"]) + for item in await self._get_all_items("me/tracks") if (item and item["track"]["id"]) ] - async def async_get_library_playlists(self) -> List[Playlist]: + async def get_library_playlists(self) -> List[Playlist]: """Retrieve playlists from the provider.""" return [ - await self.__async_parse_playlist(item) - for item in await self.__async_get_all_items("me/playlists") + await self._parse_playlist(item) + for item in await self._get_all_items("me/playlists") if (item and item["id"]) ] - async def async_get_radios(self) -> List[Radio]: + async def get_radios(self) -> List[Radio]: """Retrieve library/subscribed radio stations from the provider.""" return [] # TODO: Return spotify radio - async def async_get_artist(self, prov_artist_id) -> Artist: + async def get_artist(self, prov_artist_id) -> Artist: """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) if artist_obj else None + artist_obj = await self._get_data("artists/%s" % prov_artist_id) + return await self._parse_artist(artist_obj) if artist_obj else None - async def async_get_album(self, prov_album_id) -> Album: + async def get_album(self, prov_album_id) -> Album: """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) if album_obj else None + album_obj = await self._get_data("albums/%s" % prov_album_id) + return await self._parse_album(album_obj) if album_obj else None - async def async_get_track(self, prov_track_id) -> Track: + async def get_track(self, prov_track_id) -> Track: """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) if track_obj else None + track_obj = await self._get_data("tracks/%s" % prov_track_id) + return await self._parse_track(track_obj) if track_obj else None - async def async_get_playlist(self, prov_playlist_id) -> Playlist: + async def get_playlist(self, prov_playlist_id) -> Playlist: """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) if playlist_obj else None + playlist_obj = await self._get_data(f"playlists/{prov_playlist_id}") + return await self._parse_playlist(playlist_obj) if playlist_obj else None - async def async_get_album_tracks(self, prov_album_id) -> List[Track]: + async def get_album_tracks(self, prov_album_id) -> List[Track]: """Get all album tracks for given album id.""" return [ - await self.__async_parse_track(item) - for item in await self.__async_get_all_items( - f"albums/{prov_album_id}/tracks" - ) + await self._parse_track(item) + for item in await self._get_all_items(f"albums/{prov_album_id}/tracks") if (item and item["id"]) ] - async def async_get_playlist_tracks(self, prov_playlist_id) -> List[Track]: + async def get_playlist_tracks(self, prov_playlist_id) -> List[Track]: """Get all playlist tracks for given playlist id.""" return [ - await self.__async_parse_track(item["track"]) - for item in await self.__async_get_all_items( + await self._parse_track(item["track"]) + for item in await self._get_all_items( f"playlists/{prov_playlist_id}/tracks" ) if (item and item["track"] and item["track"]["id"]) ] - async def async_get_artist_albums(self, prov_artist_id) -> List[Album]: + async def get_artist_albums(self, prov_artist_id) -> List[Album]: """Get a list of all albums for the given artist.""" return [ - await self.__async_parse_album(item) - for item in await self.__async_get_all_items( + await self._parse_album(item) + for item in await self._get_all_items( f"artists/{prov_artist_id}/albums?include_groups=album,single,compilation" ) if (item and item["id"]) ] - async def async_get_artist_toptracks(self, prov_artist_id) -> List[Track]: + async def get_artist_toptracks(self, prov_artist_id) -> List[Track]: """Get a list of 10 most popular tracks for the given artist.""" - artist = await self.async_get_artist(prov_artist_id) + artist = await self.get_artist(prov_artist_id) endpoint = f"artists/{prov_artist_id}/top-tracks" - items = await self.__async_get_data(endpoint) + items = await self._get_data(endpoint) return [ - await self.__async_parse_track(item, artist=artist) + await self._parse_track(item, artist=artist) for item in items["tracks"] if (item and item["id"]) ] - async def async_library_add(self, prov_item_id, media_type: MediaType): + async def library_add(self, prov_item_id, media_type: MediaType): """Add item to library.""" result = False if media_type == MediaType.Artist: - result = await self.__async_put_data( + result = await self._put_data( "me/following", {"ids": prov_item_id, "type": "artist"} ) elif media_type == MediaType.Album: - result = await self.__async_put_data("me/albums", {"ids": prov_item_id}) + result = await self._put_data("me/albums", {"ids": prov_item_id}) elif media_type == MediaType.Track: - result = await self.__async_put_data("me/tracks", {"ids": prov_item_id}) + result = await self._put_data("me/tracks", {"ids": prov_item_id}) elif media_type == MediaType.Playlist: - result = await self.__async_put_data( + result = await self._put_data( f"playlists/{prov_item_id}/followers", data={"public": False} ) return result - async def async_library_remove(self, prov_item_id, media_type: MediaType): + async def library_remove(self, prov_item_id, media_type: MediaType): """Remove item from library.""" result = False if media_type == MediaType.Artist: - result = await self.__async_delete_data( + result = await self._delete_data( "me/following", {"ids": prov_item_id, "type": "artist"} ) elif media_type == MediaType.Album: - result = await self.__async_delete_data("me/albums", {"ids": prov_item_id}) + result = await self._delete_data("me/albums", {"ids": prov_item_id}) elif media_type == MediaType.Track: - result = await self.__async_delete_data("me/tracks", {"ids": prov_item_id}) + result = await self._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._delete_data(f"playlists/{prov_item_id}/followers") return result - async def async_add_playlist_tracks(self, prov_playlist_id, prov_track_ids): + async def add_playlist_tracks(self, prov_playlist_id, prov_track_ids): """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._post_data(f"playlists/{prov_playlist_id}/tracks", data=data) - async def async_remove_playlist_tracks(self, prov_playlist_id, prov_track_ids): + async def remove_playlist_tracks(self, prov_playlist_id, prov_track_ids): """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( + return await self._delete_data( f"playlists/{prov_playlist_id}/tracks", data=data ) - async def async_get_stream_details(self, item_id: str) -> StreamDetails: + async def 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(item_id) + track = await self.get_track(item_id) if not track: return None # make sure that the token is still valid by just requesting it - await self.async_get_token() + await self.get_token() spotty = self.get_spotty_binary() spotty_exec = ( '%s -n temp -c "%s" -b 320 --pass-through --single-track spotify://track:%s' @@ -339,12 +331,12 @@ class SpotifyProvider(MusicProvider): bit_depth=16, ) - async def __async_parse_artist(self, artist_obj): + async def _parse_artist(self, artist_obj): """Parse spotify artist object to generic layout.""" artist = Artist( item_id=artist_obj["id"], provider=self.id, name=artist_obj["name"] ) - artist.provider_ids.append( + artist.provider_ids.add( MediaItemProviderId(provider=PROV_ID, item_id=artist_obj["id"]) ) if "genres" in artist_obj: @@ -359,12 +351,12 @@ class SpotifyProvider(MusicProvider): artist.metadata["spotify_url"] = artist_obj["external_urls"]["spotify"] return artist - async def __async_parse_album(self, album_obj): + async def _parse_album(self, album_obj): """Parse spotify album object to generic layout.""" album = Album(item_id=album_obj["id"], provider=self.id) album.name, album.version = parse_title_and_version(album_obj["name"]) for artist in album_obj["artists"]: - album.artist = await self.__async_parse_artist(artist) + album.artist = await self._parse_artist(artist) if album.artist: break if album_obj["album_type"] == "single": @@ -389,7 +381,7 @@ class SpotifyProvider(MusicProvider): album.metadata["spotify_url"] = album_obj["external_urls"]["spotify"] if album_obj.get("explicit"): album.metadata["explicit"] = str(album_obj["explicit"]).lower() - album.provider_ids.append( + album.provider_ids.add( MediaItemProviderId( provider=PROV_ID, item_id=album_obj["id"], @@ -398,7 +390,7 @@ class SpotifyProvider(MusicProvider): ) return album - async def __async_parse_track(self, track_obj, artist=None): + async def _parse_track(self, track_obj, artist=None): """Parse spotify track object to generic layout.""" track = Track( item_id=track_obj["id"], @@ -408,17 +400,17 @@ class SpotifyProvider(MusicProvider): track_number=track_obj["track_number"], ) if artist: - track.artists.append(artist) + track.artists.add(artist) for track_artist in track_obj.get("artists", []): - artist = await self.__async_parse_artist(track_artist) - if artist and artist.item_id not in [x.item_id for x in track.artists]: - track.artists.append(artist) + artist = await self._parse_artist(track_artist) + if artist and artist.item_id not in {x.item_id for x in track.artists}: + track.artists.add(artist) track.name, track.version = parse_title_and_version(track_obj["name"]) track.metadata["explicit"] = str(track_obj["explicit"]).lower() if "external_ids" in track_obj and "isrc" in track_obj["external_ids"]: track.isrc = track_obj["external_ids"]["isrc"] if "album" in track_obj: - track.album = await self.__async_parse_album(track_obj["album"]) + track.album = await self._parse_album(track_obj["album"]) if track_obj["album"].get("images"): track.metadata["image"] = track_obj["album"]["images"][0]["url"] if track_obj.get("copyright"): @@ -427,7 +419,7 @@ class SpotifyProvider(MusicProvider): track.metadata["explicit"] = True if track_obj.get("external_urls"): track.metadata["spotify_url"] = track_obj["external_urls"]["spotify"] - track.provider_ids.append( + track.provider_ids.add( MediaItemProviderId( provider=PROV_ID, item_id=track_obj["id"], @@ -437,10 +429,10 @@ class SpotifyProvider(MusicProvider): ) return track - async def __async_parse_playlist(self, playlist_obj): + async def _parse_playlist(self, playlist_obj): """Parse spotify playlist object to generic layout.""" playlist = Playlist(item_id=playlist_obj["id"], provider=self.id) - playlist.provider_ids.append( + playlist.provider_ids.add( MediaItemProviderId(provider=PROV_ID, item_id=playlist_obj["id"]) ) playlist.name = playlist_obj["name"] @@ -456,7 +448,7 @@ class SpotifyProvider(MusicProvider): playlist.checksum = playlist_obj["snapshot_id"] return playlist - async def async_get_token(self): + async def get_token(self): """Get auth token on spotify.""" # return existing token if we have one in memory if self.__auth_token and ( @@ -467,17 +459,17 @@ class SpotifyProvider(MusicProvider): if not self._username or not self._password: return tokeninfo # retrieve token with spotty - tokeninfo = await self.__async_get_token() + tokeninfo = await self._get_token() if tokeninfo: self.__auth_token = tokeninfo - self.sp_user = await self.__async_get_data("me") + self.sp_user = await self._get_data("me") LOGGER.info("Succesfully logged in to Spotify as %s", self.sp_user["id"]) self.__auth_token = tokeninfo else: LOGGER.error("Login failed for user %s", self._username) return tokeninfo - async def __async_get_token(self): + async def _get_token(self): """Get spotify auth token with spotty bin.""" # get token with spotty scopes = [ @@ -531,7 +523,7 @@ class SpotifyProvider(MusicProvider): return tokeninfo return None - async def __async_get_all_items(self, endpoint, params=None, key="items"): + async def _get_all_items(self, endpoint, params=None, key="items"): """Get all items from a paged list.""" if not params: params = {} @@ -541,7 +533,7 @@ class SpotifyProvider(MusicProvider): while True: params["limit"] = limit params["offset"] = offset - result = await self.__async_get_data(endpoint, params=params) + result = await self._get_data(endpoint, params=params) offset += limit if not result or key not in result or not result[key]: break @@ -550,14 +542,14 @@ class SpotifyProvider(MusicProvider): break return all_items - async def __async_get_data(self, endpoint, params=None): + async def _get_data(self, endpoint, params=None): """Get data from api.""" if not params: params = {} url = "https://api.spotify.com/v1/%s" % endpoint params["market"] = "from_token" params["country"] = "from_token" - token = await self.async_get_token() + token = await self.get_token() if not token: return None headers = {"Authorization": "Bearer %s" % token["accessToken"]} @@ -571,12 +563,12 @@ class SpotifyProvider(MusicProvider): result = None return result - async def __async_delete_data(self, endpoint, params=None, data=None): + async def _delete_data(self, endpoint, params=None, data=None): """Delete data from api.""" if not params: params = {} url = "https://api.spotify.com/v1/%s" % endpoint - token = await self.async_get_token() + token = await self.get_token() if not token: return None headers = {"Authorization": "Bearer %s" % token["accessToken"]} @@ -585,12 +577,12 @@ class SpotifyProvider(MusicProvider): ) as response: return await response.text() - async def __async_put_data(self, endpoint, params=None, data=None): + async def _put_data(self, endpoint, params=None, data=None): """Put data on api.""" if not params: params = {} url = "https://api.spotify.com/v1/%s" % endpoint - token = await self.async_get_token() + token = await self.get_token() if not token: return None headers = {"Authorization": "Bearer %s" % token["accessToken"]} @@ -599,12 +591,12 @@ class SpotifyProvider(MusicProvider): ) as response: return await response.text() - async def __async_post_data(self, endpoint, params=None, data=None): + async def _post_data(self, endpoint, params=None, data=None): """Post data on api.""" if not params: params = {} url = "https://api.spotify.com/v1/%s" % endpoint - token = await self.async_get_token() + token = await self.get_token() if not token: return None headers = {"Authorization": "Bearer %s" % token["accessToken"]} diff --git a/music_assistant/providers/squeezebox/__init__.py b/music_assistant/providers/squeezebox/__init__.py index 986b2d11..00144cb4 100644 --- a/music_assistant/providers/squeezebox/__init__.py +++ b/music_assistant/providers/squeezebox/__init__.py @@ -5,7 +5,7 @@ import logging from typing import List from music_assistant.constants import CONF_CROSSFADE_DURATION -from music_assistant.helpers.typing import MusicAssistantType +from music_assistant.helpers.typing import MusicAssistant from music_assistant.helpers.util import callback from music_assistant.models.config_entry import ConfigEntry from music_assistant.models.player import ( @@ -31,10 +31,10 @@ PLAYER_FEATURES = [PlayerFeature.QUEUE, PlayerFeature.CROSSFADE, PlayerFeature.G PLAYER_CONFIG_ENTRIES = [] # we don't have any player config entries (for now) -async def async_setup(mass): +async def setup(mass): """Perform async setup of this Plugin/Provider.""" prov = PySqueezeProvider() - await mass.async_register_provider(prov) + await mass.register_provider(prov) class PySqueezeProvider(PlayerProvider): @@ -57,23 +57,23 @@ class PySqueezeProvider(PlayerProvider): """Return Config Entries for this provider.""" return CONFIG_ENTRIES - async def async_on_start(self) -> bool: + async def on_start(self) -> bool: """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) + asyncio.start_server(self._client_connected, "0.0.0.0", 3483) ) ) # setup discovery - self._tasks.append(self.mass.add_job(self.async_start_discovery())) + self._tasks.append(self.mass.add_job(self.start_discovery())) - async def async_on_stop(self): + async def on_stop(self): """Handle correct close/cleanup of the provider on exit.""" for task in self._tasks: task.cancel() - async def async_start_discovery(self): + async def start_discovery(self): """Start discovery for players.""" transport, _ = await self.mass.loop.create_datagram_endpoint( lambda: DiscoveryProtocol(self.mass.web.port), @@ -85,7 +85,7 @@ class PySqueezeProvider(PlayerProvider): finally: transport.close() - async def __async_client_connected(self, reader, writer): + async def _client_connected(self, reader, writer): """Handle a client connection on the socket.""" addr = writer.get_extra_info("peername") LOGGER.debug("Socket client connected: %s", addr) @@ -109,8 +109,9 @@ class PySqueezeProvider(PlayerProvider): class SqueezePlayer(Player): """Squeezebox player.""" - def __init__(self, mass: MusicAssistantType, socket_client: SqueezeSocketClient): + def __init__(self, mass: MusicAssistant, socket_client: SqueezeSocketClient): """Initialize.""" + super().__init__() self.mass = mass self._socket_client = socket_client @@ -133,7 +134,7 @@ class SqueezePlayer(Player): """Set a (new) socket client to this player.""" self._socket_client = socket_client - async def async_on_remove(self) -> None: + async def on_remove(self) -> None: """Call when player is removed from the player manager.""" self.socket_client.disconnect() @@ -205,66 +206,64 @@ class SqueezePlayer(Player): address=self.socket_client.device_address, ) - async def async_cmd_stop(self): + async def cmd_stop(self): """Send stop command to player.""" - return await self.socket_client.async_cmd_stop() + return await self.socket_client.cmd_stop() - async def async_cmd_play(self): + async def cmd_play(self): """Send play (unpause) command to player.""" - return await self.socket_client.async_cmd_play() + return await self.socket_client.cmd_play() - async def async_cmd_pause(self): + async def cmd_pause(self): """Send pause command to player.""" - return await self.socket_client.async_cmd_pause() + return await self.socket_client.cmd_pause() - async def async_cmd_power_on(self) -> None: + async def cmd_power_on(self) -> None: """Send POWER ON command to player.""" # save power and volume state in cache - cache_str = f"squeezebox_player_state_{self.player_id}" - await self.mass.cache.async_set(cache_str, (True, self.volume_level)) - return await self.socket_client.async_cmd_power(True) + cache_str = f"squeezebox_player_{self.player_id}" + await self.mass.cache.set(cache_str, (True, self.volume_level)) + return await self.socket_client.cmd_power(True) - async def async_cmd_power_off(self) -> None: + async def cmd_power_off(self) -> None: """Send POWER OFF command to player.""" # save power and volume state in cache - cache_str = f"squeezebox_player_state_{self.player_id}" - await self.mass.cache.async_set(cache_str, (False, self.volume_level)) - return await self.socket_client.async_cmd_power(False) + cache_str = f"squeezebox_player_{self.player_id}" + await self.mass.cache.set(cache_str, (False, self.volume_level)) + return await self.socket_client.cmd_power(False) - async def async_cmd_volume_set(self, volume_level: int): + async def cmd_volume_set(self, volume_level: int): """Send new volume level command to player.""" - return await self.socket_client.async_cmd_volume_set(volume_level) + return await self.socket_client.cmd_volume_set(volume_level) - async def async_cmd_mute(self, muted: bool = False): + async def cmd_mute(self, muted: bool = False): """Send mute command to player.""" - return await self.socket_client.async_cmd_mute(muted) + return await self.socket_client.cmd_mute(muted) - async def async_cmd_play_uri(self, uri: str): + async def cmd_play_uri(self, uri: str): """Request player to start playing a single uri.""" crossfade = self.mass.config.player_settings[self.player_id][ CONF_CROSSFADE_DURATION ] - return await self.socket_client.async_play_uri( - uri, crossfade_duration=crossfade - ) + return await self.socket_client.play_uri(uri, crossfade_duration=crossfade) - async def async_cmd_next(self): + async def cmd_next(self): """Send NEXT TRACK command to player.""" queue = self.mass.players.get_player_queue(self.player_id) if queue: new_track = queue.get_item(queue.cur_index + 1) if new_track: - return await self.async_cmd_play_uri(new_track.uri) + return await self.cmd_play_uri(new_track.uri) - async def async_cmd_previous(self): + async def cmd_previous(self): """Send PREVIOUS TRACK command to player.""" queue = self.mass.players.get_player_queue(self.player_id) if queue: new_track = queue.get_item(queue.cur_index - 1) if new_track: - return await self.async_cmd_play_uri(new_track.uri) + return await self.cmd_play_uri(new_track.uri) - async def async_cmd_queue_play_index(self, index: int): + async def cmd_queue_play_index(self, index: int): """ Play item at index X on player's queue. @@ -274,19 +273,19 @@ class SqueezePlayer(Player): if queue: new_track = queue.get_item(index) if new_track: - return await self.async_cmd_play_uri(new_track.uri) + return await self.cmd_play_uri(new_track.uri) - async def async_cmd_queue_load(self, queue_items: List[QueueItem]): + async def cmd_queue_load(self, queue_items: List[QueueItem]): """ Load/overwrite given items in the player's queue implementation. :param queue_items: a list of QueueItems """ if queue_items: - await self.async_cmd_play_uri(queue_items[0].uri) - return await self.async_cmd_play_uri(queue_items[0].uri) + await self.cmd_play_uri(queue_items[0].uri) + return await self.cmd_play_uri(queue_items[0].uri) - async def async_cmd_queue_insert( + async def cmd_queue_insert( self, queue_items: List[QueueItem], insert_at_index: int ): """ @@ -300,9 +299,9 @@ class SqueezePlayer(Player): # we only check the start index queue = self.mass.players.get_player_queue(self.player_id) if queue and insert_at_index == queue.cur_index: - return await self.async_cmd_queue_play_index(insert_at_index) + return await self.cmd_queue_play_index(insert_at_index) - async def async_cmd_queue_append(self, queue_items: List[QueueItem]): + async def cmd_queue_append(self, queue_items: List[QueueItem]): """ Append new items at the end of the queue. @@ -310,7 +309,7 @@ class SqueezePlayer(Player): """ # automagically handled by built-in queue controller - async def async_cmd_queue_update(self, queue_items: List[QueueItem]): + async def cmd_queue_update(self, queue_items: List[QueueItem]): """ Overwrite the existing items in the queue, used for reordering. @@ -318,25 +317,25 @@ class SqueezePlayer(Player): """ # automagically handled by built-in queue controller - async def async_cmd_queue_clear(self): + async def cmd_queue_clear(self): """Clear the player's queue.""" # queue is handled by built-in queue controller but send stop - return await self.async_cmd_stop() + return await self.cmd_stop() - async def async_restore_states(self): + async def restore_states(self): """Restore power/volume states.""" - cache_str = f"squeezebox_player_state_{self.player_id}" - cache_data = await self.mass.cache.async_get(cache_str) + cache_str = f"squeezebox_player_{self.player_id}" + cache_data = await self.mass.cache.get(cache_str) last_power, last_volume = cache_data if cache_data else (False, 40) - await self.socket_client.async_cmd_volume_set(last_volume) - await self.socket_client.async_cmd_power(last_power) + await self.socket_client.cmd_volume_set(last_volume) + await self.socket_client.cmd_power(last_power) @callback def handle_socket_client_event(self, event: SqueezeEvent): """Process incoming event from the socket client.""" if event == SqueezeEvent.CONNECTED: # restore previous power/volume - self.mass.add_job(self.async_restore_states()) + self.mass.add_job(self.restore_states()) elif event == SqueezeEvent.DECODER_READY: # tell player to load next queue track queue = self.mass.players.get_player_queue(self.player_id) @@ -347,7 +346,7 @@ class SqueezePlayer(Player): CONF_CROSSFADE_DURATION ] self.mass.add_job( - self.socket_client.async_play_uri( + self.socket_client.play_uri( next_item.uri, send_flush=False, crossfade_duration=crossfade, diff --git a/music_assistant/providers/squeezebox/socket_client.py b/music_assistant/providers/squeezebox/socket_client.py index 88c271c8..5fe3b99a 100644 --- a/music_assistant/providers/squeezebox/socket_client.py +++ b/music_assistant/providers/squeezebox/socket_client.py @@ -8,7 +8,7 @@ import time from enum import Enum from typing import Callable -from music_assistant.helpers.typing import MusicAssistantType +from music_assistant.helpers.typing import MusicAssistant from music_assistant.helpers.util import run_periodic from .constants import PROV_ID @@ -50,7 +50,7 @@ class SqueezeSocketClient: def __init__( self, - mass: MusicAssistantType, + mass: MusicAssistant, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, event_callback: Callable = None, @@ -74,8 +74,8 @@ class SqueezeSocketClient: self._connected = True self._event_callbacks = [] self._tasks = [ - asyncio.create_task(self.__async_socket_reader()), - asyncio.create_task(self.__async_send_heartbeat()), + asyncio.create_task(self._socket_reader()), + asyncio.create_task(self._send_heartbeat()), ] def disconnect(self) -> None: @@ -163,60 +163,60 @@ class SqueezeSocketClient: """Return uri of currently loaded track.""" return self._current_uri - async def __async_initialize_player(self): + async def _initialize_player(self): """Set some startup settings for the player.""" # send version - await self.__async_send_frame(b"vers", b"7.8") - await self.__async_send_frame(b"setd", struct.pack("B", 0)) - await self.__async_send_frame(b"setd", struct.pack("B", 4)) + await self._send_frame(b"vers", b"7.8") + await self._send_frame(b"setd", struct.pack("B", 0)) + await self._send_frame(b"setd", struct.pack("B", 4)) - async def async_cmd_stop(self): + async def cmd_stop(self): """Send stop command to player.""" data = self.__pack_stream(b"q", autostart=b"0", flags=0) - await self.__async_send_frame(b"strm", data) + await self._send_frame(b"strm", data) - async def async_cmd_play(self): + async def cmd_play(self): """Send play (unpause) command to player.""" data = self.__pack_stream(b"u", autostart=b"0", flags=0) - await self.__async_send_frame(b"strm", data) + await self._send_frame(b"strm", data) - async def async_cmd_pause(self): + async def cmd_pause(self): """Send pause command to player.""" data = self.__pack_stream(b"p", autostart=b"0", flags=0) - await self.__async_send_frame(b"strm", data) + await self._send_frame(b"strm", data) - async def async_cmd_power(self, powered: bool = True): + async def cmd_power(self, powered: bool = True): """Send power command to player.""" # power is not supported so abuse mute instead power_int = 1 if powered else 0 - await self.__async_send_frame(b"aude", struct.pack("2B", power_int, 1)) + await self._send_frame(b"aude", struct.pack("2B", power_int, 1)) self._powered = powered self.signal_event(SqueezeEvent.STATE_UPDATED) - async def async_cmd_volume_set(self, volume_level: int): + async def cmd_volume_set(self, volume_level: int): """Send new volume level command to player.""" self._volume_control.volume = volume_level old_gain = self._volume_control.old_gain() new_gain = self._volume_control.new_gain() - await self.__async_send_frame( + await self._send_frame( b"audg", struct.pack("!LLBBLL", old_gain, old_gain, 1, 255, new_gain, new_gain), ) - async def async_cmd_mute(self, muted: bool = False): + async def cmd_mute(self, muted: bool = False): """Send mute command to player.""" muted_int = 0 if muted else 1 - await self.__async_send_frame(b"aude", struct.pack("2B", muted_int, 0)) + await self._send_frame(b"aude", struct.pack("2B", muted_int, 0)) self.muted = muted self.signal_event(SqueezeEvent.STATE_UPDATED) - async def async_play_uri( + async def play_uri( self, uri: str, send_flush: bool = True, crossfade_duration: int = 0 ): """Request player to start playing a single uri.""" if send_flush: data = self.__pack_stream(b"f", autostart=b"0", flags=0) - await self.__async_send_frame(b"strm", data) + await self._send_frame(b"strm", data) self._current_uri = uri self._powered = True enable_crossfade = crossfade_duration > 0 @@ -246,18 +246,18 @@ class SqueezeSocketClient: headers = f"Connection: close\r\nAccept: */*\r\nHost: {host}:{port}\r\n" request = "GET %s HTTP/1.1\r\n%s\r\n" % (uri, headers) data = data + request.encode("utf-8") - await self.__async_send_frame(b"strm", data) + await self._send_frame(b"strm", data) @run_periodic(5) - async def __async_send_heartbeat(self): + async def _send_heartbeat(self): """Send periodic heartbeat message to player.""" if not self._connected: return timestamp = int(time.time()) data = self.__pack_stream(b"t", replay_gain=timestamp, flags=0) - await self.__async_send_frame(b"strm", data) + await self._send_frame(b"strm", data) - async def __async_send_frame(self, command, data): + async def _send_frame(self, command, data): """Send command to Squeeze player.""" if self._reader.at_eof() or self._writer.is_closing(): LOGGER.debug("Socket is disconnected.") @@ -271,7 +271,7 @@ class SqueezeSocketClient: self._connected = False self.signal_event(SqueezeEvent.DISCONNECTED) - async def __async_socket_reader(self): + async def _socket_reader(self): """Handle incoming data from socket.""" buffer = b"" # keep reading bytes from the socket @@ -341,7 +341,7 @@ class SqueezeSocketClient: self._player_id = str(device_mac).lower() self._device_type = DEVICE_TYPE.get(dev_id, "unknown device") LOGGER.debug("Player connected: %s", self.name) - asyncio.create_task(self.__async_initialize_player()) + asyncio.create_task(self._initialize_player()) self.signal_event(SqueezeEvent.CONNECTED) def _process_stat(self, data): @@ -458,7 +458,7 @@ class SqueezeSocketClient: """Process incoming RESP message: Response received at player.""" # pylint: disable=unused-argument # send continue - asyncio.create_task(self.__async_send_frame(b"cont", b"0")) + asyncio.create_task(self._send_frame(b"cont", b"0")) def _process_setd(self, data): """Process incoming SETD message: Get/set player firmware settings.""" diff --git a/music_assistant/providers/tunein/__init__.py b/music_assistant/providers/tunein/__init__.py index 25e3792d..dd330731 100644 --- a/music_assistant/providers/tunein/__init__.py +++ b/music_assistant/providers/tunein/__init__.py @@ -33,10 +33,10 @@ CONFIG_ENTRIES = [ ] -async def async_setup(mass): +async def setup(mass): """Perform async setup of this Plugin/Provider.""" prov = TuneInProvider() - await mass.async_register_provider(prov) + await mass.register_provider(prov) class TuneInProvider(MusicProvider): @@ -68,7 +68,7 @@ class TuneInProvider(MusicProvider): """Return MediaTypes the provider supports.""" return [MediaType.Radio] - async def async_on_start(self) -> bool: + async def on_start(self) -> bool: """Handle initialization of the provider based on config.""" # pylint: disable=attribute-defined-outside-init config = self.mass.config.get_provider_config(self.id) @@ -80,7 +80,7 @@ class TuneInProvider(MusicProvider): self._throttler = Throttler(rate_limit=1, period=1) return True - async def async_search( + async def search( self, search_query: str, media_types=Optional[List[MediaType]], limit: int = 5 ) -> SearchResult: """ @@ -94,29 +94,29 @@ class TuneInProvider(MusicProvider): # TODO: search for radio stations return result - async def async_get_library_radios(self) -> List[Radio]: + async def get_library_radios(self) -> List[Radio]: """Retrieve library/subscribed radio stations from the provider.""" params = {"c": "presets"} - result = await self.__async_get_data("Browse.ashx", params) + result = await self._get_data("Browse.ashx", params) if result and "body" in result: return [ - await self.__async_parse_radio(item) + await self._parse_radio(item) for item in result["body"] if item["type"] == "audio" ] return [] - async def async_get_radio(self, prov_radio_id: str) -> Radio: + async def get_radio(self, prov_radio_id: str) -> Radio: """Get radio station details.""" radio = None params = {"c": "composite", "detail": "listing", "id": prov_radio_id} - result = await self.__async_get_data("Describe.ashx", params) + result = await self._get_data("Describe.ashx", params) if result and result.get("body") and result["body"][0].get("children"): item = result["body"][0]["children"][0] - radio = await self.__async_parse_radio(item) + radio = await self._parse_radio(item) return radio - async def __async_parse_radio(self, details: dict) -> Radio: + async def _parse_radio(self, details: dict) -> Radio: """Parse Radio object from json obj returned from api.""" radio = Radio(item_id=details["preset_id"], provider=PROV_ID) if "name" in details: @@ -129,7 +129,7 @@ class TuneInProvider(MusicProvider): name = name.split(" (")[0] radio.name = name # parse stream urls and format - stream_info = await self.__async_get_stream_urls(radio.item_id) + stream_info = await self._get_stream_urls(radio.item_id) for stream in stream_info["body"]: if stream["media_type"] == "aac": quality = TrackQuality.LOSSY_AAC @@ -137,7 +137,7 @@ class TuneInProvider(MusicProvider): quality = TrackQuality.LOSSY_OGG else: quality = TrackQuality.LOSSY_MP3 - radio.provider_ids.append( + radio.provider_ids.add( MediaItemProviderId( provider=PROV_ID, item_id="%s--%s" % (details["preset_id"], stream["media_type"]), @@ -152,20 +152,20 @@ class TuneInProvider(MusicProvider): radio.metadata["image"] = details["logo"] return radio - async def __async_get_stream_urls(self, radio_id): + async def _get_stream_urls(self, radio_id): """Return the stream urls for the given radio id.""" params = {"id": radio_id} - res = await self.__async_get_data("Tune.ashx", params) + res = await self._get_data("Tune.ashx", params) return res - async def async_get_stream_details(self, item_id: str) -> StreamDetails: + async def get_stream_details(self, item_id: str) -> StreamDetails: """Get streamdetails for a radio station.""" radio_id = item_id.split("--")[0] if len(item_id.split("--")) > 1: media_type = item_id.split("--")[1] else: media_type = "" - stream_info = await self.__async_get_stream_urls(radio_id) + stream_info = await self._get_stream_urls(radio_id) for stream in stream_info["body"]: if stream["media_type"] == media_type or not media_type: return StreamDetails( @@ -180,7 +180,7 @@ class TuneInProvider(MusicProvider): ) return None - async def __async_get_data(self, endpoint, params=None): + async def _get_data(self, endpoint, params=None): """Get data from api.""" if not params: params = {} diff --git a/music_assistant/providers/universal_group/__init__.py b/music_assistant/providers/universal_group/__init__.py index cbf1c054..920798dc 100644 --- a/music_assistant/providers/universal_group/__init__.py +++ b/music_assistant/providers/universal_group/__init__.py @@ -1,10 +1,10 @@ -"""Group player provider: enables grouping of all playertypes.""" +"""Group player provider: enables grouping of all Players.""" import asyncio import logging from typing import List -from music_assistant.helpers.typing import MusicAssistantType +from music_assistant.helpers.typing import MusicAssistant from music_assistant.models.config_entry import ConfigEntry, ConfigEntryType from music_assistant.models.player import DeviceInfo, PlaybackState, Player from music_assistant.models.provider import PlayerProvider @@ -28,10 +28,10 @@ CONFIG_ENTRIES = [ ] -async def async_setup(mass): +async def setup(mass): """Perform async setup of this Plugin/Provider.""" prov = GroupPlayerProvider() - await mass.async_register_provider(prov) + await mass.register_provider(prov) class GroupPlayerProvider(PlayerProvider): @@ -52,25 +52,26 @@ class GroupPlayerProvider(PlayerProvider): """Return Config Entries for this provider.""" return CONFIG_ENTRIES - async def async_on_start(self) -> bool: + async def on_start(self) -> bool: """Handle initialization of the provider based on config.""" conf = self.mass.config.player_providers[PROV_ID] for index in range(conf[CONF_PLAYER_COUNT]): player = GroupPlayer(self.mass, index) - self.mass.add_job(self.mass.players.async_add_player(player)) + self.mass.add_job(self.mass.players.add_player(player)) return True - async def async_on_stop(self): + async def on_stop(self): """Handle correct close/cleanup of the provider on exit. Called on shutdown.""" for player in self.players: - await player.async_cmd_stop() + await player.cmd_stop() class GroupPlayer(Player): """Model for a group player.""" - def __init__(self, mass: MusicAssistantType, player_index: int): + def __init__(self, mass: MusicAssistant, player_index: int): """Initialize.""" + super().__init__() self.mass = mass self._player_index = player_index self._player_id = f"{PROV_ID}_{player_index}" @@ -171,7 +172,7 @@ class GroupPlayer(Player): """Return config entries for this group player.""" return self._config_entries - async def async_on_update(self) -> None: + async def on_poll(self) -> None: """Call when player is periodically polled by the player manager (should_poll=True).""" self._config_entries = self.__get_config_entries() self._group_childs = self.__get_group_childs() @@ -188,7 +189,7 @@ class GroupPlayer(Player): """Return config entries for this group player.""" all_players = [ {"text": item.name, "value": item.player_id} - for item in self.mass.players.player_states + for item in self.mass.players if item.player_id is not self._player_id ] selected_players_ids = self.mass.config.get_player_config(self.player_id).get( @@ -197,10 +198,10 @@ class GroupPlayer(Player): # selected_players_ids = [] selected_players = [] for player_id in selected_players_ids: - player_state = self.mass.players.get_player_state(player_id) - if player_state: + player = self.mass.players.get_player(player_id) + if player: selected_players.append( - {"text": player_state.name, "value": player_state.player_id} + {"text": player.name, "value": player.player_id} ) default_master = "" if selected_players: @@ -229,9 +230,9 @@ class GroupPlayer(Player): # SERVICE CALLS / PLAYER COMMANDS - async def async_cmd_play_uri(self, uri: str): + async def cmd_play_uri(self, uri: str): """Play the specified uri/url on the player.""" - await self.async_cmd_stop() + await self.cmd_stop() self._current_uri = uri self._state = PlaybackState.Playing self._powered = True @@ -242,11 +243,11 @@ class GroupPlayer(Player): child_player = self.mass.players.get_player(child_player_id) if child_player: queue_stream_uri = f"{self.mass.web.stream_url}/group/{self.player_id}?player_id={child_player_id}" - await child_player.async_cmd_play_uri(queue_stream_uri) + await child_player.cmd_play_uri(queue_stream_uri) self.update_state() - self.stream_task = self.mass.add_job(self.async_queue_stream_task()) + self.stream_task = self.mass.add_job(self.queue_stream_task()) - async def async_cmd_stop(self) -> None: + async def cmd_stop(self) -> None: """Send STOP command to player.""" self._state = PlaybackState.Stopped if self.stream_task: @@ -261,10 +262,10 @@ class GroupPlayer(Player): for child_player_id in self.group_childs: child_player = self.mass.players.get_player(child_player_id) if child_player: - await child_player.async_cmd_stop() + await child_player.cmd_stop() self.update_state() - async def async_cmd_play(self) -> None: + async def cmd_play(self) -> None: """Send PLAY command to player.""" if not self.state == PlaybackState.Paused: return @@ -272,31 +273,31 @@ class GroupPlayer(Player): for child_player_id in self.group_childs: child_player = self.mass.players.get_player(child_player_id) if child_player: - await child_player.async_cmd_play() + await child_player.cmd_play() self._state = PlaybackState.Playing self.update_state() - async def async_cmd_pause(self): + async def cmd_pause(self): """Send PAUSE command to player.""" # forward this command to each child player for child_player_id in self.group_childs: child_player = self.mass.players.get_player(child_player_id) if child_player: - await child_player.async_cmd_pause() + await child_player.cmd_pause() self._state = PlaybackState.Paused self.update_state() - async def async_cmd_power_on(self) -> None: + async def cmd_power_on(self) -> None: """Send POWER ON command to player.""" self._powered = True self.update_state() - async def async_cmd_power_off(self) -> None: + async def cmd_power_off(self) -> None: """Send POWER OFF command to player.""" self._powered = False self.update_state() - async def async_cmd_volume_set(self, volume_level: int) -> None: + async def cmd_volume_set(self, volume_level: int) -> None: """ Send volume level command to player. @@ -304,14 +305,14 @@ class GroupPlayer(Player): """ # this is already handled by the player manager - async def async_cmd_volume_mute(self, is_muted=False): + async def cmd_volume_mute(self, is_muted=False): """ Send volume MUTE command to given player. :param is_muted: bool with new mute state. """ for child_player_id in self.group_childs: - self.mass.players.async_cmd_volume_mute(child_player_id) + self.mass.players.cmd_volume_mute(child_player_id) self.muted = is_muted async def subscribe_stream_client(self, child_player_id): @@ -347,7 +348,7 @@ class GroupPlayer(Player): child_player_id, ) - async def async_queue_stream_task(self): + async def queue_stream_task(self): """Handle streaming queue to connected child players.""" ticks = 0 while ticks < 60 and len(self.connected_clients) != len(self.group_childs): @@ -362,9 +363,7 @@ class GroupPlayer(Player): ) self.sync_task = asyncio.create_task(self.__synchronize_players()) - async for audio_chunk in self.mass.streams.async_queue_stream_flac( - self.player_id - ): + async for audio_chunk in self.mass.streams.queue_stream_flac(self.player_id): # make sure we still have clients connected if not self.connected_clients: @@ -447,12 +446,12 @@ class GroupPlayer(Player): avg_lag, ) # we correct the lag by pausing the master player for a very short time - await master_player.async_cmd_pause() + await master_player.cmd_pause() # sending the command takes some time, account for that too if avg_lag > 20: sleep_time = avg_lag - 20 await asyncio.sleep(sleep_time / 1000) - asyncio.create_task(master_player.async_cmd_play()) + asyncio.create_task(master_player.cmd_play()) break # no more processing this round if we've just corrected a lag # calculate drift (player is going faster in relation to the master) @@ -475,12 +474,12 @@ class GroupPlayer(Player): avg_drift, ) # we correct the drift by pausing the player for a very short time - # this is not the best approach but works with all playertypes + # this is not the best approach but works with all Players # temporary solution until I find something better like sending more/less pcm chunks - await child_player.async_cmd_pause() + await child_player.cmd_pause() # sending the command takes some time, account for that too if avg_drift > 20: sleep_time = drift - 20 await asyncio.sleep(sleep_time / 1000) - await child_player.async_cmd_play() + await child_player.cmd_play() break # no more processing this round if we've just corrected a lag diff --git a/music_assistant/web/json_rpc.py b/music_assistant/web/json_rpc.py index 0296de80..8c3fbb9a 100644 --- a/music_assistant/web/json_rpc.py +++ b/music_assistant/web/json_rpc.py @@ -19,45 +19,45 @@ async def json_rpc_endpoint(request: Request): cmds = params[1] cmd_str = " ".join(cmds) if cmd_str == "play": - await request.app["mass"].players.async_cmd_play(player_id) + await request.app["mass"].players.cmd_play(player_id) elif cmd_str == "pause": - await request.app["mass"].players.async_cmd_pause(player_id) + await request.app["mass"].players.cmd_pause(player_id) elif cmd_str == "stop": - await request.app["mass"].players.async_cmd_stop(player_id) + await request.app["mass"].players.cmd_stop(player_id) elif cmd_str == "next": - await request.app["mass"].players.async_cmd_next(player_id) + await request.app["mass"].players.cmd_next(player_id) elif cmd_str == "previous": - await request.app["mass"].players.async_cmd_previous(player_id) + await request.app["mass"].players.cmd_previous(player_id) elif "power" in cmd_str: powered = cmds[1] if len(cmds) > 1 else False if powered: - await request.app["mass"].players.async_cmd_power_on(player_id) + await request.app["mass"].players.cmd_power_on(player_id) else: - await request.app["mass"].players.async_cmd_power_off(player_id) + await request.app["mass"].players.cmd_power_off(player_id) elif cmd_str == "playlist index +1": - await request.app["mass"].players.async_cmd_next(player_id) + await request.app["mass"].players.cmd_next(player_id) elif cmd_str == "playlist index -1": - await request.app["mass"].players.async_cmd_previous(player_id) + await request.app["mass"].players.cmd_previous(player_id) elif "mixer volume" in cmd_str and "+" in cmds[2]: - player_state = request.app["mass"].players.get_player_state(player_id) - volume_level = player_state.volume_level + int(cmds[2].split("+")[1]) - await request.app["mass"].players.async_cmd_volume_set(player_id, volume_level) + player = request.app["mass"].players.get_player(player_id) + volume_level = player.volume_level + int(cmds[2].split("+")[1]) + await request.app["mass"].players.cmd_volume_set(player_id, volume_level) elif "mixer volume" in cmd_str and "-" in cmds[2]: - player_state = request.app["mass"].players.get_player_state(player_id) - volume_level = player_state.volume_level - int(cmds[2].split("-")[1]) - await request.app["mass"].players.async_cmd_volume_set(player_id, volume_level) + player = request.app["mass"].players.get_player(player_id) + volume_level = player.volume_level - int(cmds[2].split("-")[1]) + await request.app["mass"].players.cmd_volume_set(player_id, volume_level) elif "mixer volume" in cmd_str: - await request.app["mass"].players.async_cmd_volume_set(player_id, cmds[2]) + await request.app["mass"].players.cmd_volume_set(player_id, cmds[2]) elif cmd_str == "mixer muting 1": - await request.app["mass"].players.async_cmd_volume_mute(player_id, True) + await request.app["mass"].players.cmd_volume_mute(player_id, True) elif cmd_str == "mixer muting 0": - await request.app["mass"].players.async_cmd_volume_mute(player_id, False) + await request.app["mass"].players.cmd_volume_mute(player_id, False) elif cmd_str == "button volup": - await request.app["mass"].players.async_cmd_volume_up(player_id) + await request.app["mass"].players.cmd_volume_up(player_id) elif cmd_str == "button voldown": - await request.app["mass"].players.async_cmd_volume_down(player_id) + await request.app["mass"].players.cmd_volume_down(player_id) elif cmd_str == "button power": - await request.app["mass"].players.async_cmd_power_toggle(player_id) + await request.app["mass"].players.cmd_power_toggle(player_id) else: return Response(text="command not supported") return Response(text="success") diff --git a/music_assistant/web/server.py b/music_assistant/web/server.py index 9ba04663..076928fa 100755 --- a/music_assistant/web/server.py +++ b/music_assistant/web/server.py @@ -27,8 +27,8 @@ from music_assistant.constants import ( from music_assistant.constants import __version__ as MASS_VERSION from music_assistant.helpers import repath from music_assistant.helpers.encryption import decrypt_string -from music_assistant.helpers.images import async_get_image_url, async_get_thumb_file -from music_assistant.helpers.typing import MusicAssistantType +from music_assistant.helpers.images import get_image_url, get_thumb_file +from music_assistant.helpers.typing import MusicAssistant from music_assistant.helpers.util import get_hostname, get_ip from music_assistant.helpers.web import api_route, json_serializer, parse_arguments from music_assistant.models.media_types import ItemMapping, MediaItem @@ -42,7 +42,7 @@ LOGGER = logging.getLogger("webserver") class WebServer: """Webserver and json/websocket api.""" - def __init__(self, mass: MusicAssistantType, port: int): + def __init__(self, mass: MusicAssistant, port: int): """Initialize class.""" self.jwt_key = None self.app = None @@ -55,16 +55,16 @@ class WebServer: self._runner = None self.api_routes = {} - async def async_setup(self): + async def setup(self): """Perform async setup.""" - self.jwt_key = decrypt_string(self.mass.config.stored_config["jwt_key"]) + self.jwt_key = await decrypt_string(self.mass.config.stored_config["jwt_key"]) self.app = web.Application() self.app["mass"] = self.mass self.app["clients"] = [] # add all routes self.app.add_routes(stream_routes) self.app.router.add_route("*", "/jsonrpc.js", json_rpc_endpoint) - self.app.router.add_get("/ws", self.__async_websocket_handler) + self.app.router.add_get("/ws", self._websocket_handler) # register all methods decorated as api_route for cls in [ @@ -86,14 +86,14 @@ class WebServer: ) }, ) - cors.add(self.app.router.add_get("/info", self.async_info)) + cors.add(self.app.router.add_get("/info", self.info)) # Host the frontend app webdir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static/") if os.path.isdir(webdir): - self.app.router.add_get("/", self.async_index) + self.app.router.add_get("/", self.index) self.app.router.add_static("/", webdir, append_version=True) else: - self.app.router.add_get("/", self.async_info) + self.app.router.add_get("/", self.info) self._runner = web.AppRunner(self.app, access_log=None) await self._runner.setup() @@ -101,9 +101,9 @@ class WebServer: http_site = web.TCPSite(self._runner, host=None, port=self.port) await http_site.start() LOGGER.info("Started Music Assistant server on port %s", self.port) - self.mass.add_event_listener(self.__async_handle_mass_events) + self.mass.add_event_listener(self._handle_mass_events) - async def async_stop(self): + async def stop(self): """Stop the webserver.""" for ws_client in self.app["clients"]: await ws_client.close(message=b"server shutdown") @@ -170,7 +170,7 @@ class WebServer: "initialized": self.mass.config.stored_config["initialized"], } - async def async_index(self, request: web.Request): + async def index(self, request: web.Request): """Get the index page.""" # pylint: disable=unused-argument html_app = os.path.join( @@ -179,21 +179,19 @@ class WebServer: return web.FileResponse(html_app) @api_route("info", False) - async def async_info(self, request: web.Request = None): + async def info(self, request: web.Request = None): """Return discovery info on index page.""" if request: return web.json_response(self.discovery_info) return self.discovery_info @api_route("revoke_token") - async def async_revoke_token(self, client_id: str): + async def revoke_token(self, client_id: str): """Revoke token for client.""" return self.mass.config.security.revoke_app_token(client_id) @api_route("get_token", False) - async def async_get_token( - self, username: str, password: str, app_id: str = "" - ) -> dict: + async def get_token(self, username: str, password: str, app_id: str = "") -> dict: """ Validate given credentials and return JWT token. @@ -225,7 +223,7 @@ class WebServer: raise AuthenticationError("Invalid credentials") @api_route("setup", False) - async def async_create_user_setup(self, username: str, password: str): + async def create_user_setup(self, username: str, password: str): """Handle first-time server setup through onboarding wizard.""" if self.mass.config.stored_config["initialized"]: raise AuthenticationError("Already initialized") @@ -235,11 +233,11 @@ class WebServer: self.mass.config.stored_config["initialized"] = True self.mass.config.save() # fix discovery info - await self.mass.async_setup_discovery() + await self.mass.setup_discovery() return True @api_route("images/thumb") - async def async_get_image_thumb( + async def get_image_thumb( self, size: int, url: Optional[str] = "", @@ -247,11 +245,11 @@ class WebServer: ): """Get (resized) thumb image for given URL or media item as base64 encoded string.""" if not url and item: - url = await async_get_image_url( + url = await get_image_url( self.mass, item.item_id, item.provider, item.media_type ) if url: - img_file = await async_get_thumb_file(self.mass, url, size) + img_file = await get_thumb_file(self.mass, url, size) if img_file: with open(img_file, "rb") as _file: icon_data = _file.read() @@ -260,11 +258,11 @@ class WebServer: raise KeyError("Invalid item or url") @api_route("images/provider-icons/:provider_id?") - async def async_get_provider_icon(self, provider_id: Optional[str]): + async def get_provider_icon(self, provider_id: Optional[str]): """Get Provider icon as base64 encoded string.""" if not provider_id: return { - prov.id: await self.async_get_provider_icon(prov.id) + prov.id: await self.get_provider_icon(prov.id) for prov in self.mass.get_providers(include_unavailable=True) } base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -276,7 +274,7 @@ class WebServer: return "data:image/png;base64," + icon_data.decode() raise KeyError("Invalid provider: %s" % provider_id) - async def __async_websocket_handler(self, request: web.Request): + async def _websocket_handler(self, request: web.Request): """Handle websocket client.""" ws_client = WebSocketResponse() @@ -300,7 +298,7 @@ class WebServer: json_msg = msg.json(loads=ujson.loads) if "command" in json_msg and "data" in json_msg: # handle command - await self.__async_handle_command( + await self._handle_command( ws_client, json_msg["command"], json_msg["data"], @@ -308,16 +306,16 @@ class WebServer: ) elif "event" in json_msg: # handle event - await self.__async_handle_event( + await self._handle_event( ws_client, json_msg["event"], json_msg.get("data") ) except AuthenticationError as exc: # pylint:disable=broad-except # disconnect client on auth errors - await self.__async_send_json(ws_client, error=str(exc), **json_msg) + await self._send_json(ws_client, error=str(exc), **json_msg) await ws_client.close(message=str(exc).encode()) except Exception as exc: # pylint:disable=broad-except # log the error only - await self.__async_send_json(ws_client, error=str(exc), **json_msg) + await self._send_json(ws_client, error=str(exc), **json_msg) LOGGER.error("Error with WS client", exc_info=exc) # websocket disconnected @@ -326,7 +324,7 @@ class WebServer: return ws_client - async def __async_handle_command( + async def _handle_command( self, ws_client: WebSocketResponse, command: str, @@ -336,7 +334,7 @@ class WebServer: """Handle websocket command.""" res = None if command == "auth": - return await self.__async_handle_auth(ws_client, data) + return await self._handle_auth(ws_client, data) # work out handler for the given path/command for key in self.api_routes: match = repath.match_pattern(key, command) @@ -356,20 +354,18 @@ class WebServer: if asyncio.iscoroutine(res): res = await res # return result of command to client - return await self.__async_send_json( + return await self._send_json( ws_client, id=msg_id, result=command, data=res ) raise KeyError("Unknown command") - async def __async_handle_event( - self, ws_client: WebSocketResponse, event: str, data: Any - ): + async def _handle_event(self, ws_client: WebSocketResponse, event: str, data: Any): """Handle event message from ws client.""" LOGGER.info("received event %s", event) if ws_client.authenticated: self.mass.signal_event(event, data) - async def __async_handle_auth(self, ws_client: WebSocketResponse, token: str): + async def _handle_auth(self, ws_client: WebSocketResponse, token: str): """Handle authentication with JWT token.""" token_info = jwt.decode(token, self.mass.web.jwt_key, algorithms=["HS256"]) if self.mass.config.security.is_token_revoked(token_info): @@ -377,19 +373,19 @@ class WebServer: ws_client.authenticated = True self.mass.config.security.set_last_login(token_info["client_id"]) # TODO: store token/app_id on ws_client obj and periodiclaly check if token is expired or revoked - await self.__async_send_json(ws_client, result="auth", data=token_info) + await self._send_json(ws_client, result="auth", data=token_info) - async def __async_send_json(self, ws_client: WebSocketResponse, **kwargs): + async def _send_json(self, ws_client: WebSocketResponse, **kwargs): """Send message (back) to websocket client.""" await ws_client.send_str(json_serializer(kwargs)) - async def __async_handle_mass_events(self, event: str, event_data: Any): + async def _handle_mass_events(self, event: str, event_data: Any): """Broadcast events to connected clients.""" for ws_client in self.app["clients"]: if not ws_client.authenticated: continue try: - await self.__async_send_json(ws_client, event=event, data=event_data) + await self._send_json(ws_client, event=event, data=event_data) except ConnectionResetError: # client is already disconnected self.app["clients"].remove(ws_client) diff --git a/music_assistant/web/streams.py b/music_assistant/web/streams.py index 3681f2e4..76e6cb26 100644 --- a/music_assistant/web/streams.py +++ b/music_assistant/web/streams.py @@ -15,10 +15,8 @@ async def stream_media(request: Request): return Response(status=404, reason="Media item is not playable!") item_id = request.match_info["item_id"] provider = request.rel_url.query.get("provider", "database") - media_item = await request.app["mass"].music.async_get_item( - item_id, provider, media_type - ) - streamdetails = await request.app["mass"].music.async_get_stream_details(media_item) + media_item = await request.app["mass"].music.get_item(item_id, provider, media_type) + streamdetails = await request.app["mass"].music.get_stream_details(media_item) # prepare request content_type = streamdetails.content_type.value @@ -28,7 +26,7 @@ async def stream_media(request: Request): await resp.prepare(request) # stream track - async for audio_chunk in request.app["mass"].streams.async_get_media_stream( + async for audio_chunk in request.app["mass"].streams.get_media_stream( streamdetails ): await resp.write(audio_chunk) @@ -50,9 +48,7 @@ async def stream_queue(request: Request): await resp.prepare(request) # stream queue - async for audio_chunk in request.app["mass"].streams.async_queue_stream_flac( - player_id - ): + async for audio_chunk in request.app["mass"].streams.queue_stream_flac(player_id): await resp.write(audio_chunk) return resp @@ -70,7 +66,7 @@ async def stream_queue_item(request: Request): ) await resp.prepare(request) - async for audio_chunk in request.app["mass"].streams.async_stream_queue_item( + async for audio_chunk in request.app["mass"].streams.stream_queue_item( player_id, queue_item_id ): await resp.write(audio_chunk) @@ -93,7 +89,7 @@ async def stream_group(request: Request): await resp.prepare(request) # stream queue - player_state = request.app["mass"].players.get_player(group_player_id) - async for audio_chunk in player_state.subscribe_stream_client(child_player_id): + player = request.app["mass"].players.get_player(group_player_id) + async for audio_chunk in player.subscribe_stream_client(child_player_id): await resp.write(audio_chunk) return resp diff --git a/requirements.txt b/requirements.txt index 521ae59e..951bff2f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,4 +19,4 @@ cryptography==3.3.2 ujson==4.0.1 mashumaro==1.24 typing-inspect==0.6.0; python_version < '3.8' -uvloop==0.14.0; sys_platform != 'win32' +uvloop==0.15.1; sys_platform != 'win32' -- 2.34.1