# 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)
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,
"""All constants for Music Assistant."""
-__version__ = "0.0.87"
+__version__ = "0.1.0"
REQUIRED_PYTHON_VER = "3.7"
# configuration keys/attributes
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(
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.
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)
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 = ?"
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 = {}
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,
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,
)
return result
- return async_wrapped
+ return wrapped
return wrapper
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()
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)
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
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"):
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
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
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:
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
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:
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:
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'([+\-&|!(){}\[\]\^"~*?:\\\/])'
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,
)
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:
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:
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",
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",
)
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),
"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"]:
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)
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"]:
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 = {}
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")
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
*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():
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
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."""
"""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]
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
"""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
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):
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
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):
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()
"""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
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
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."""
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
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:
"""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()
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
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."""
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."""
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
)
)
# 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(
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
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
# 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(
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
"""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,
) -> 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."""
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",
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",
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:
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)
(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
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:
)
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(?,?,?,?);"""
(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
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)
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."""
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",
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
)
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)
(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
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",
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
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."""
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:
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
(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
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",
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."""
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)"
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:
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,
(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)
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
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
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:
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
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."""
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
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
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
)
EVENT_MUSIC_SYNC_STATUS, method_class.running_sync_jobs
)
- return async_wrapped
+ return wrapped
return wrapper
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):
# 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:
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:
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
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.
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)
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
"""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
"""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:
# 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)
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,
self.cache = mass.cache
self.musicbrainz = MusicBrainz(mass)
- async def async_setup(self):
+ async def setup(self):
"""Async initialize of module."""
@property
################ 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,
):
"""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)
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)
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,
) -> 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)
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,
)
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)
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],
)
]
@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)
]
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
]
)
]
@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)
]
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
]
)
]
@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
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."""
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],
)
[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."""
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,
"""
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:
"""
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
# 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:
"""
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
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:
################ 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,
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,
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.
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:
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:
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.
), "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(
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:
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
)
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.
), "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
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:
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
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
)
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:
"""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
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
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:
@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")
@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)
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
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]],
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
)
]
)
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.
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
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
):
"""
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
):
"""
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.
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.
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
):
"""
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.
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:
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
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,
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()
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)
# 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())
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."""
# 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")
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
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,
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:
# 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
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
# 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
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."""
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
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
)
# 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()
)
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
)
)
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."""
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:
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 (
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.
# 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()
)
# 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()
"""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:
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)
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:
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:
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.
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(
"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")
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)
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
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):
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
@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):
"""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
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):
artist: Artist = None
+ def __hash__(self):
+ """Return custom hash."""
+ return hash((self.media_type, self.provider, self.item_id))
+
@dataclass
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
# 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):
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):
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):
"""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
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
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:
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.
"""
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.
"""
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.
# 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.
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.
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:
"""
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.
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.
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()
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,
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
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
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."""
"""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
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
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:
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)
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.
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
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.
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
)
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
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
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
{"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
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):
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,
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"]
# 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 = {
"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)
+++ /dev/null
-"""
-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,
- }
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,
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
"""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.
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."""
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):
"""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
MediaType.Track,
]
- async def async_search(
+ async def search(
self, search_query: str, media_types=Optional[List[MediaType]], limit: int = 5
) -> SearchResult:
"""
"""
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
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 (
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):
"""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):
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
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"]
"""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.
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.
}
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.
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):
"""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,
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
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):
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,
"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
"""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,
"""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.
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 ==========
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])
"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."""
"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),
},
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.
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):
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.
"""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"]
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 = {}
]
-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):
"""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]:
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:
"""
# 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)
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
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")
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")
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")
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")
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")
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")
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")
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")
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
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(
).decode("utf-8"),
)
)
- artists.append(artist)
+ artists.add(artist)
track.artists = artists
if "GENRE" in song.tags:
track.metadata["genres"] = song.tags["GENRE"]
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,
)
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:
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
]
-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):
"""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)
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:
"""
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,
"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
)
]
- 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
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.
"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:
"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)
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:
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"]),
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"
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"]),
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 (
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(" - "):
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"):
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"]),
)
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"],
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 = (
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"]
"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(
)
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 = {}
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
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
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
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 = {}
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:
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)
"""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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
):
"""
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.
"""
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.
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))
# 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)
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):
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."""
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."""
# 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
]
-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):
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
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:
"""
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'
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:
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":
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"],
)
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"],
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"):
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"],
)
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"]
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 (
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 = [
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 = {}
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
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"]}
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"]}
) 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"]}
) 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"]}
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 (
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):
"""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),
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)
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
"""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()
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.
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
):
"""
# 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.
"""
# 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.
"""
# 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)
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,
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
def __init__(
self,
- mass: MusicAssistantType,
+ mass: MusicAssistant,
reader: asyncio.StreamReader,
writer: asyncio.StreamWriter,
event_callback: Callable = None,
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:
"""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
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.")
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
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):
"""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."""
]
-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):
"""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)
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:
"""
# 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:
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
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"]),
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(
)
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 = {}
-"""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
]
-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):
"""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}"
"""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()
"""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(
# 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:
# 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
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:
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
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.
"""
# 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):
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):
)
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:
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)
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
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")
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
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
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 [
)
},
)
- 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()
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")
"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(
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.
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")
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] = "",
):
"""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()
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__)))
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()
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"],
)
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
return ws_client
- async def __async_handle_command(
+ async def _handle_command(
self,
ws_client: WebSocketResponse,
command: str,
"""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)
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):
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)
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
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)
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
)
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)
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
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'