{
- "python.linting.pylintEnabled": true,
+ "python.linting.pylintEnabled": false,
"python.linting.enabled": true,
- "python.pythonPath": "venv/bin/python3"
+ "python.pythonPath": "venv/bin/python3",
+ "python.linting.flake8Enabled": true
}
\ No newline at end of file
"""Start Music Assistant."""
import argparse
-import asyncio
import logging
import os
-import platform
-import sys
from aiorun import run
from music_assistant.mass import MusicAssistant
help="Directory that contains the MusicAssistant configuration",
)
parser.add_argument(
- "--debug", action="store_true", help="Start MusicAssistant with verbose debug logging"
+ "--debug",
+ action="store_true",
+ help="Start MusicAssistant with verbose debug logging",
)
arguments = parser.parse_args()
return arguments
def on_shutdown(loop):
logger.info("shutdown requested!")
loop.run_until_complete(mass.async_stop())
-
+
run(mass.async_start(), use_uvloop=True, shutdown_callback=on_shutdown)
+
if __name__ == "__main__":
main()
-(lambda __g: [[[[None for __g['get_app_var'], get_app_var.__name__ in [(lambda index: (lambda __l: [APP_VARS[__l['index']] for __l['index'] in [(index)]][0])({}), 'get_app_var')]][0] for __g['APP_VARS'] in [(base64.b64decode(VARS_ENC).decode('utf-8').split(','))]][0] for __g['VARS_ENC'] in [(b'OTQyODUyNTY3LDc2MTczMGQzZjk1ZTRhZjA5YWM2M2I5YTM3Y2NjOTZhLDJlYjk2ZjliMzc0OTRiZTE4MjQ5OTlkNTgwMjhhMzA1LFNTcnRNMnhlM2wwMDNnOEh4RmVUUUtub3BaNklCaUwzRTlPc1QxODFYMDA9')]][0] for __g['base64'] in [(__import__('base64', __g, __g))]][0])(globals())
+"""Some magic to store some appvars."""
+# pylint: skip-file
+# flake8: noqa
+(
+ lambda __g: [
+ [
+ [
+ [
+ None
+ for __g["get_app_var"], get_app_var.__name__ in [
+ (
+ lambda index: (
+ lambda __l: [
+ APP_VARS[__l["index"]] for __l["index"] in [(index)]
+ ][0]
+ )({}),
+ "get_app_var",
+ )
+ ]
+ ][0]
+ for __g["APP_VARS"] in [
+ (base64.b64decode(VARS_ENC).decode("utf-8").split(","))
+ ]
+ ][0]
+ for __g["VARS_ENC"] in [
+ (
+ b"OTQyODUyNTY3LDc2MTczMGQzZjk1ZTRhZjA5YWM2M2I5YTM3Y2NjOTZhLDJlYjk2ZjliMzc0OTRiZTE4MjQ5OTlkNTgwMjhhMzA1LFNTcnRNMnhlM2wwMDNnOEh4RmVUUUtub3BaNklCaUwzRTlPc1QxODFYMDA9"
+ )
+ ]
+ ][0]
+ for __g["base64"] in [(__import__("base64", __g, __g))]
+ ][0]
+)(globals())
class Cache(object):
- """basic stateless caching system."""
+ """Basic stateless caching system."""
_db = None
await db_conn.commit()
self.mass.add_job(self.async_auto_cleanup())
-
async def async_get(self, cache_key, checksum=""):
"""
- get object from cache and return the results
- cache_key: the (unique) name of the cache object as reference
- checkum: optional argument to check if the checksum in the
- cacheobject matches the checkum provided
+ Get object from cache and return the results.
+
+ cache_key: the (unique) name of the cache object as reference
+ checkum: optional argument to check if the checksum in the
+ cacheobject matches the checkum provided
"""
result = None
cur_time = int(time.time())
return result
async def async_set(self, cache_key, data, checksum="", expiration=(86400 * 30)):
- """
- set data in cache
- """
+ """Set data in cache."""
checksum = self._get_checksum(checksum)
expires = int(time.time() + expiration)
data = pickle.dumps(data)
@run_periodic(3600)
async def async_auto_cleanup(self):
- """(scheduled) auto cleanup task"""
+ """Sceduled auto cleanup task."""
cur_timestamp = int(time.time())
LOGGER.debug("Running cleanup...")
sql_query = "SELECT id, expires FROM simplecache"
@staticmethod
def _get_checksum(stringinput):
- """get int checksum from string"""
+ """Get int checksum from string."""
if not stringinput:
return 0
else:
return reduce(lambda x, y: x + y, map(ord, stringinput))
-async def async_cached_generator(cache, cache_key, coro_func, expires=(86400 * 30), checksum=None):
- """Helper method to store results of a async generator in the cache."""
+async def async_cached_generator(
+ cache, cache_key, coro_func, expires=(86400 * 30), checksum=None
+):
+ """Return helper method to store results of a async generator in the cache."""
cache_result = await cache.async_get(cache_key, checksum)
if cache_result is not None:
for item in cache_result:
await cache.async_set(cache_key, cache_result, checksum, expires)
-async def async_cached(cache, cache_key, coro_func, expires=(86400 * 30), checksum=None):
- """Helper method to store results of a coroutine in the cache."""
+async def async_cached(
+ cache, cache_key, coro_func, expires=(86400 * 30), checksum=None
+):
+ """Return helper method to store results of a coroutine in the cache."""
cache_result = await cache.async_get(cache_key, checksum)
# normal async function
if cache_result is not None:
def async_use_cache(cache_days=14, cache_checksum=None):
- """Decorator that can be used to cache a method's result."""
+ """Return decorator that can be used to cache a method's result."""
def wrapper(func):
@functools.wraps(func)
"""All classes and helpers for the Configuration."""
-import base64
import logging
import os
import shutil
from typing import List
from cryptography.fernet import Fernet, InvalidToken
-from music_assistant.app_vars import get_app_var
+from music_assistant.app_vars import get_app_var # noqa
from music_assistant.constants import (
CONF_CROSSFADE_DURATION,
CONF_ENABLED,
CONF_NAME,
EVENT_CONFIG_CHANGED,
)
-
-# from music_assistant.mass import MusicAssistant
from music_assistant.models.config_entry import ConfigEntry, ConfigEntryType
from music_assistant.utils import get_external_ip, json, try_load_json_file
from passlib.hash import pbkdf2_sha256
class ConfigItem:
"""
Configuration Item connected to Config Entries.
+
Returns default value from config entry if no value present.
"""
def __init__(self, mass, parent_item_key: str, base_type: ConfigBaseType):
+ """Initialize class."""
self._parent_item_key = parent_item_key
self._base_type = base_type
self.mass = mass
self.stored_config = OrderedDict()
def __repr__(self):
+ """Print class."""
return f"{OrderedDict}({self.items()})"
def items(self) -> dict:
raise ValueError
else:
# single value item
- if entry.entry_type == ConfigEntryType.STRING and not isinstance(value, str):
+ if entry.entry_type == ConfigEntryType.STRING and not isinstance(
+ value, str
+ ):
if not value:
value = ""
else:
raise ValueError
- if entry.entry_type == ConfigEntryType.BOOL and not isinstance(value, bool):
+ if entry.entry_type == ConfigEntryType.BOOL and not isinstance(
+ value, bool
+ ):
raise ValueError
if entry.entry_type == ConfigEntryType.FLOAT and not isinstance(
value, (float, int)
player = self.mass.player_manager.get_player(self._parent_item_key)
if player:
- self.mass.add_job(self.mass.player_manager.async_update_player(player))
+ self.mass.add_job(
+ self.mass.player_manager.async_update_player(player)
+ )
return
# raise KeyError if we're trying to set a value not defined as ConfigEntry
raise KeyError
"""Configuration class with ConfigItem items."""
def __init__(self, mass, base_type=ConfigBaseType):
+ """Initialize class."""
self.mass = mass
self._base_type = base_type
super().__init__()
def __getitem__(self, item_key):
- """Convenience method for get."""
- if not item_key in self:
+ """Return convenience method for get."""
+ if item_key not in self:
# create new ConfigDictItem on the fly
- super().__setitem__(item_key, ConfigItem(self.mass, item_key, self._base_type))
+ super().__setitem__(
+ item_key, ConfigItem(self.mass, item_key, self._base_type)
+ )
return super().__getitem__(item_key)
"""Class which holds our configuration."""
def __init__(self, mass, data_path: str):
+ """Initialize class."""
self._data_path = data_path
self.loading = False
self.mass = mass
return pbkdf2_sha256.verify(password, self.base["security"]["password"])
def __getitem__(self, item_key):
- """Convenience method for get."""
+ """Return item value by key."""
return getattr(self, item_key)
async def async_close(self):
if os.path.isfile(conf_file):
shutil.move(conf_file, conf_file_backup)
# create dict for stored config
- stored_conf = {CONF_KEY_BASE: {}, CONF_KEY_PLAYERSETTINGS: {}, CONF_KEY_PROVIDERS: {}}
+ stored_conf = {
+ CONF_KEY_BASE: {},
+ CONF_KEY_PLAYERSETTINGS: {},
+ CONF_KEY_PROVIDERS: {},
+ }
for conf_key in stored_conf:
for key, value in self[conf_key].items():
stored_conf[conf_key][key] = value.stored_config
self.loading = False
def __load(self):
- """load config from file"""
+ """Load config from file."""
self.loading = True
conf_file = os.path.join(self.data_path, "config.json")
data = try_load_json_file(conf_file)
CONF_FALLBACK_GAIN_CORRECT = "fallback_gain_correct"
-
CONF_KEY_BASE = "base"
CONF_KEY_PLAYERSETTINGS = "player_settings"
CONF_KEY_PROVIDERS = "providers"
EVENT_SHUTDOWN = "application shutdown"
EVENT_PROVIDER_REGISTERED = "provider registered"
EVENT_PLAYER_CONTROL_REGISTERED = "player control registered"
-EVENT_PLAYER_CONTROL_UPDATED = "player control updated"
\ No newline at end of file
+EVENT_PLAYER_CONTROL_UPDATED = "player control updated"
"""Database logic."""
# pylint: disable=too-many-lines
-import asyncio
import logging
import os
import sqlite3
-from typing import List
from functools import partial
+from typing import List
import aiosqlite
from music_assistant.models.media_types import (
Track,
TrackQuality,
)
-from music_assistant.utils import get_sort_name, try_parse_int, compare_strings
+from music_assistant.utils import compare_strings, get_sort_name, try_parse_int
LOGGER = logging.getLogger("mass")
-import contextlib
-
class DbConnect:
+ """Helper to initialize the db connection or utilize an existing one."""
+
def __init__(self, dbfile: str, db_conn: sqlite3.Connection = None):
+ """Initialize class."""
self._db_conn_provided = db_conn is not None
self._db_conn = db_conn
self._dbfile = dbfile
async def __aenter__(self):
+ """Enter."""
if not self._db_conn_provided:
self._db_conn = await aiosqlite.connect(self._dbfile, timeout=120)
return self._db_conn
async def __aexit__(self, exc_type, exc_value, traceback):
+ """Exit."""
if not self._db_conn_provided:
await self._db_conn.close()
return False
"""Class that holds the (logic to the) database."""
def __init__(self, mass):
+ """Initialize class."""
self.mass = mass
self._dbfile = os.path.join(mass.config.data_path, "database.db")
self.db_conn = partial(DbConnect, self._dbfile)
return item_id[0]
return None
- async def async_search(self, searchquery: str, media_types: List[MediaType]) -> SearchResult:
+ async def async_search(
+ self, searchquery: str, media_types: List[MediaType]
+ ) -> SearchResult:
"""Search library for the given searchphrase."""
async with DbConnect(self._dbfile) as db_conn:
result = SearchResult([], [], [], [], [])
if media_types is None or MediaType.Artist in media_types:
sql_query = ' WHERE name LIKE "%s"' % searchquery
result.artists = [
- item async for item in self.async_get_artists(sql_query, db_conn=db_conn)
+ item
+ async for item in self.async_get_artists(sql_query, db_conn=db_conn)
]
if media_types is None or MediaType.Album in media_types:
sql_query = ' WHERE name LIKE "%s"' % searchquery
result.albums = [
- item async for item in self.async_get_albums(sql_query, db_conn=db_conn)
+ item
+ async for item in self.async_get_albums(sql_query, db_conn=db_conn)
]
if media_types is None or MediaType.Track in media_types:
sql_query = ' WHERE name LIKE "%s"' % searchquery
result.tracks = [
- item async for item in self.async_get_tracks(sql_query, db_conn=db_conn)
+ item
+ async for item in self.async_get_tracks(sql_query, db_conn=db_conn)
]
if media_types is None or MediaType.Playlist in media_types:
sql_query = ' WHERE name LIKE "%s"' % searchquery
result.playlists = [
- item async for item in self.async_get_playlists(sql_query, db_conn=db_conn)
+ item
+ async for item in self.async_get_playlists(
+ sql_query, db_conn=db_conn
+ )
]
if media_types is None or MediaType.Radio in media_types:
sql_query = ' WHERE name LIKE "%s"' % searchquery
result.radios = [
- item async for item in self.async_get_radios(sql_query, db_conn=db_conn)
+ item
+ async for item in self.async_get_radios(sql_query, db_conn=db_conn)
]
return result
provider = "{provider_id}" AND media_type = {int(MediaType.Artist)})"""
else:
sql_query = f"""WHERE artist_id in
- (SELECT item_id FROM library_items
+ (SELECT item_id FROM library_items
WHERE media_type = {int(MediaType.Artist)})"""
- async for item in self.async_get_artists(sql_query, orderby=orderby, fulldata=True):
+ async for item in self.async_get_artists(
+ sql_query, orderby=orderby, fulldata=True
+ ):
yield item
async def async_get_library_albums(
else:
sql_query = f"""WHERE album_id in
(SELECT item_id FROM library_items WHERE media_type = {int(MediaType.Album)})"""
- async for item in self.async_get_albums(sql_query, orderby=orderby, fulldata=True):
+ async for item in self.async_get_albums(
+ sql_query, orderby=orderby, fulldata=True
+ ):
yield item
async def async_get_library_tracks(
"""Get all library tracks, optionally filtered by provider."""
if provider_id is not None:
sql_query = f"""WHERE track_id in
- (SELECT item_id FROM library_items WHERE provider = "{provider_id}"
+ (SELECT item_id FROM library_items WHERE provider = "{provider_id}"
AND media_type = {int(MediaType.Track)})"""
else:
sql_query = f"""WHERE track_id in
yield item
async def async_get_playlists(
- self, filter_query: str = None, orderby: str = "name", db_conn: sqlite3.Connection = None
+ self,
+ filter_query: str = None,
+ orderby: str = "name",
+ db_conn: sqlite3.Connection = None,
) -> List[Playlist]:
"""Get all playlists from database."""
async with DbConnect(self._dbfile, db_conn) as db_conn:
async def async_get_playlist(self, playlist_id: int) -> Playlist:
"""Get playlist record by id."""
playlist_id = try_parse_int(playlist_id)
- async for item in self.async_get_playlists(f"WHERE playlist_id = {playlist_id}"):
+ async for item in self.async_get_playlists(
+ f"WHERE playlist_id = {playlist_id}"
+ ):
return item
return None
async def async_get_radios(
- self, filter_query: str = None, orderby: str = "name", db_conn: sqlite3.Connection = None
+ self,
+ filter_query: str = None,
+ orderby: str = "name",
+ db_conn: sqlite3.Connection = None,
) -> List[Radio]:
"""Fetch radio records from database."""
sql_query = "SELECT * FROM radios"
metadata=await self.__async_get_metadata(
db_row["radio_id"], MediaType.Radio, db_conn
),
- tags=await self.__async_get_tags(db_row["radio_id"], MediaType.Radio, db_conn),
+ tags=await self.__async_get_tags(
+ db_row["radio_id"], MediaType.Radio, db_conn
+ ),
external_ids=await self.__async_get_external_ids(
db_row["radio_id"], MediaType.Radio, db_conn
),
async with db_conn.execute(sql_query, (last_row_id,)) as cursor:
playlist_id = await cursor.fetchone()
playlist_id = playlist_id[0]
- LOGGER.debug("added playlist %s to database: %s", playlist.name, playlist_id)
+ LOGGER.debug(
+ "added playlist %s to database: %s", playlist.name, playlist_id
+ )
# add/update metadata
await self.__async_add_prov_ids(
playlist_id, MediaType.Playlist, playlist.provider_ids, db_conn
return playlist_id
async def async_add_radio(self, radio: Radio):
- """add a new radio record to the database."""
+ """Add a new radio record to the database."""
assert radio.name
async with DbConnect(self._dbfile) as db_conn:
async with db_conn.execute(
async with db_conn.execute(sql_query, (last_row_id,)) as cursor:
radio_id = await cursor.fetchone()
radio_id = radio_id[0]
- LOGGER.debug("added radio station %s to database: %s", radio.name, radio_id)
+ LOGGER.debug(
+ "added radio station %s to database: %s", radio.name, radio_id
+ )
# add/update metadata
- await self.__async_add_prov_ids(radio_id, MediaType.Radio, radio.provider_ids, db_conn)
- await self.__async_add_metadata(radio_id, MediaType.Radio, radio.metadata, db_conn)
+ await self.__async_add_prov_ids(
+ radio_id, MediaType.Radio, radio.provider_ids, db_conn
+ )
+ await self.__async_add_metadata(
+ radio_id, MediaType.Radio, radio.metadata, db_conn
+ )
# save
await db_conn.commit()
return radio_id
- async def async_add_to_library(self, item_id: int, media_type: MediaType, provider: str):
+ async def async_add_to_library(
+ self, item_id: int, media_type: MediaType, provider: str
+ ):
"""Add an item to the library (item must already be present in the db!)."""
async with DbConnect(self._dbfile) as db_conn:
item_id = try_parse_int(item_id)
await db_conn.execute(sql_query, (item_id, provider, media_type))
await db_conn.commit()
- async def async_remove_from_library(self, item_id: int, media_type: MediaType, provider: str):
+ async def async_remove_from_library(
+ self, item_id: int, media_type: MediaType, provider: str
+ ):
"""Remove item from the library."""
async with DbConnect(self._dbfile) as db_conn:
item_id = try_parse_int(item_id)
await self.__async_add_prov_ids(
artist_id, MediaType.Artist, artist.provider_ids, db_conn
)
- await self.__async_add_metadata(artist_id, MediaType.Artist, artist.metadata, db_conn)
- await self.__async_add_tags(artist_id, MediaType.Artist, artist.tags, db_conn)
+ await self.__async_add_metadata(
+ artist_id, MediaType.Artist, artist.metadata, db_conn
+ )
+ await self.__async_add_tags(
+ artist_id, MediaType.Artist, artist.tags, db_conn
+ )
await self.__async_add_external_ids(
artist_id, MediaType.Artist, artist.external_ids, db_conn
)
album.tags = await self.__async_get_tags(
album.item_id, MediaType.Album, db_conn
)
- album.labels = await self.__async_get_album_labels(album.item_id, db_conn)
+ album.labels = await self.__async_get_album_labels(
+ album.item_id, db_conn
+ )
yield album
async def async_get_album(
self, album_id: int, fulldata=True, db_conn: sqlite3.Connection = None
) -> Album:
- """get album record by id"""
+ """Get album record by id."""
album_id = try_parse_int(album_id)
async for item in self.async_get_albums(
"WHERE album_id = %d" % album_id, fulldata=fulldata, db_conn=db_conn
if not album_id:
sql_query = """SELECT album_id, year, version, albumtype FROM
albums WHERE artist_id=? AND name=?"""
- async with db_conn.execute(sql_query, (album.artist.item_id, album.name)) as cursor:
+ async with db_conn.execute(
+ sql_query, (album.artist.item_id, album.name)
+ ) as cursor:
albums = await cursor.fetchall()
for result in albums:
if (not album.version and result["year"] == album.year) or (
await db_conn.commit()
# always add metadata and tags etc. because we might have received
# additional info or a match from other provider
- await self.__async_add_prov_ids(album_id, MediaType.Album, album.provider_ids, db_conn)
- await self.__async_add_metadata(album_id, MediaType.Album, album.metadata, db_conn)
+ await self.__async_add_prov_ids(
+ album_id, MediaType.Album, album.provider_ids, db_conn
+ )
+ await self.__async_add_metadata(
+ album_id, MediaType.Album, album.metadata, db_conn
+ )
await self.__async_add_tags(album_id, MediaType.Album, album.tags, db_conn)
await self.__async_add_album_labels(album_id, album.labels, db_conn)
await self.__async_add_external_ids(
)
yield track
- async def async_get_track(self, track_id: int, fulldata=True, db_conn: sqlite3.Connection = None) -> Track:
+ async def async_get_track(
+ self, track_id: int, fulldata=True, db_conn: sqlite3.Connection = None
+ ) -> Track:
"""Get track record by id."""
track_id = try_parse_int(track_id)
async for item in self.async_get_tracks(
track_id = await self.__async_get_item_by_external_id(track, db_conn)
# fallback to matching on album_id, name and version
if not track_id:
- sql_query = (
- "SELECT track_id, duration, version FROM tracks WHERE album_id=? AND name=?"
- )
- async with db_conn.execute(sql_query, (track.album.item_id, track.name)) as cursor:
+ sql_query = "SELECT track_id, duration, version \
+ FROM tracks WHERE album_id=? AND name=?"
+ async with db_conn.execute(
+ sql_query, (track.album.item_id, track.name)
+ ) as cursor:
results = await cursor.fetchall()
for result in results:
# we perform an additional safety check on the duration or version
- if (track.version and compare_strings(result["version"], track.version)) or (
+ if (
+ track.version
+ and compare_strings(result["version"], track.version)
+ ) or (
(
not track.version
and not result["version"]
# no match found: insert track
if not track_id:
assert track.name and track.album.item_id
- sql_query = (
- "INSERT INTO tracks (name, album_id, duration, version) VALUES(?,?,?,?);"
- )
+ sql_query = "INSERT INTO tracks (name, album_id, duration, version) \
+ VALUES(?,?,?,?);"
query_params = (
track.name,
track.album.item_id,
for artist in track.artists:
sql_query = "INSERT or IGNORE INTO track_artists (track_id, artist_id) VALUES(?,?);"
await db_conn.execute(sql_query, (track_id, artist.item_id))
- await self.__async_add_prov_ids(track_id, MediaType.Track, track.provider_ids, db_conn)
- await self.__async_add_metadata(track_id, MediaType.Track, track.metadata, db_conn)
+ await self.__async_add_prov_ids(
+ track_id, MediaType.Track, track.provider_ids, db_conn
+ )
+ await self.__async_add_metadata(
+ track_id, MediaType.Track, track.metadata, db_conn
+ )
await self.__async_add_tags(track_id, MediaType.Track, track.tags, db_conn)
await self.__async_add_external_ids(
track_id, MediaType.Track, track.external_ids, db_conn
)
return track_id
- async def async_update_playlist(self, playlist_id: int, column_key: str, column_value: str):
+ async def async_update_playlist(
+ self, playlist_id: int, column_key: str, column_value: str
+ ):
"""Update column of existing playlist."""
async with DbConnect(self._dbfile) as db_conn:
sql_query = f"UPDATE playlists SET {column_key}=? WHERE playlist_id=?;"
await db_conn.execute(sql_query, (column_value, playlist_id))
await db_conn.commit()
- async def async_get_artist_tracks(self, artist_id: int, orderby: str = "name") -> List[Track]:
- """get all library tracks for the given artist"""
+ async def async_get_artist_tracks(
+ self, artist_id: int, orderby: str = "name"
+ ) -> List[Track]:
+ """Get all library tracks for the given artist."""
artist_id = try_parse_int(artist_id)
sql_query = f"""WHERE track_id in
(SELECT track_id FROM track_artists WHERE artist_id = {artist_id})"""
- async for item in self.async_get_tracks(sql_query, orderby=orderby, fulldata=False):
+ async for item in self.async_get_tracks(
+ sql_query, orderby=orderby, fulldata=False
+ ):
yield item
- async def async_get_artist_albums(self, artist_id: int, orderby: str = "name") -> List[Album]:
- """get all library albums for the given artist"""
+ async def async_get_artist_albums(
+ self, artist_id: int, orderby: str = "name"
+ ) -> List[Album]:
+ """Get all library albums for the given artist."""
sql_query = " WHERE artist_id = %s" % artist_id
- async for item in self.async_get_albums(sql_query, orderby=orderby, fulldata=False):
+ async for item in self.async_get_albums(
+ sql_query, orderby=orderby, fulldata=False
+ ):
yield item
- async def async_set_track_loudness(self, provider_track_id: str, provider: str, loudness: int):
+ async def async_set_track_loudness(
+ self, provider_track_id: str, provider: str, loudness: int
+ ):
"""Set integrated loudness for a track in db."""
async with DbConnect(self._dbfile) as db_conn:
sql_query = """INSERT or REPLACE INTO track_loudness
async with DbConnect(self._dbfile) as db_conn:
sql_query = """SELECT loudness FROM track_loudness WHERE
provider_track_id = ? AND provider = ?"""
- async with db_conn.execute(sql_query, (provider_track_id, provider)) as cursor:
+ async with db_conn.execute(
+ sql_query, (provider_track_id, provider)
+ ) as cursor:
result = await cursor.fetchone()
if result:
return result[0]
return None
async def __async_add_metadata(
- self, item_id: int, media_type: MediaType, metadata: dict, db_conn: sqlite3.Connection
+ self,
+ item_id: int,
+ media_type: MediaType,
+ metadata: dict,
+ db_conn: sqlite3.Connection,
):
"""Add or update metadata."""
for key, value in metadata.items():
) -> dict:
"""Get metadata for media item."""
metadata = {}
- sql_query = "SELECT key, value FROM metadata WHERE item_id = ? AND media_type = ?"
+ sql_query = (
+ "SELECT key, value FROM metadata WHERE item_id = ? AND media_type = ?"
+ )
if filter_key:
sql_query += ' AND key = "%s"' % filter_key
async with db_conn.execute(sql_query, (item_id, media_type)) as cursor:
return metadata
async def __async_add_tags(
- self, item_id: int, media_type: MediaType, tags: List[str], db_conn: sqlite3.Connection
+ self,
+ item_id: int,
+ media_type: MediaType,
+ tags: List[str],
+ db_conn: sqlite3.Connection,
):
- """add tags to db"""
+ """Add tags to db."""
for tag in tags:
sql_query = "INSERT or IGNORE INTO tags (name) VALUES(?);"
async with db_conn.execute(sql_query, (tag,)) as cursor:
async def __async_get_tags(
self, item_id: int, media_type: MediaType, db_conn: sqlite3.Connection
) -> List[str]:
- """get tags for media item"""
+ """Get tags for media item."""
tags = []
sql_query = """SELECT name FROM tags INNER JOIN media_tags ON
tags.tag_id = media_tags.tag_id WHERE item_id = ? AND media_type = ?"""
async def __async_add_album_labels(
self, album_id: int, labels: List[str], db_conn: sqlite3.Connection
):
- """add labels to album in db"""
+ """Add labels to album in db."""
for label in labels:
sql_query = "INSERT or IGNORE INTO labels (name) VALUES(?);"
async with db_conn.execute(sql_query, (label,)) as cursor:
label_id = cursor.lastrowid
- sql_query = "INSERT or IGNORE INTO album_labels (album_id, label_id) VALUES(?,?);"
+ sql_query = (
+ "INSERT or IGNORE INTO album_labels (album_id, label_id) VALUES(?,?);"
+ )
await db_conn.execute(sql_query, (album_id, label_id))
async def __async_get_album_labels(
self, album_id: int, db_conn: sqlite3.Connection
) -> List[str]:
- """get labels for album item"""
+ """Get labels for album item."""
labels = []
sql_query = """SELECT name FROM labels INNER JOIN album_labels
ON labels.label_id = album_labels.label_id WHERE album_id = ?"""
async def __async_get_track_artists(
self, track_id: int, db_conn: sqlite3.Connection, fulldata: bool = False
) -> List[Artist]:
- """get artists for track"""
+ """Get artists for track."""
sql_query = (
"WHERE artist_id in (SELECT artist_id FROM track_artists WHERE track_id = %s)"
% track_id
)
return [
item
- async for item in self.async_get_artists(sql_query, fulldata=fulldata, db_conn=db_conn)
+ async for item in self.async_get_artists(
+ sql_query, fulldata=fulldata, db_conn=db_conn
+ )
]
async def __async_add_external_ids(
- self, item_id: int, media_type: MediaType, external_ids: dict, db_conn: sqlite3.Connection
+ self,
+ item_id: int,
+ media_type: MediaType,
+ external_ids: dict,
+ db_conn: sqlite3.Connection,
):
- """add or update external_ids"""
+ """Add or update external_ids."""
for key, value in external_ids.items():
sql_query = """INSERT or REPLACE INTO external_ids
(item_id, media_type, key, value) VALUES(?,?,?,?);"""
async def __async_get_external_ids(
self, item_id: int, media_type: MediaType, db_conn: sqlite3.Connection
) -> dict:
- """get external_ids for media item"""
+ """Get external_ids for media item."""
external_ids = {}
- sql_query = "SELECT key, value FROM external_ids WHERE item_id = ? AND media_type = ?"
+ sql_query = (
+ "SELECT key, value FROM external_ids WHERE item_id = ? AND media_type = ?"
+ )
for db_row in await db_conn.execute_fetchall(sql_query, (item_id, media_type)):
external_ids[db_row[0]] = db_row[1]
return external_ids
) -> List[str]:
"""Get the providers that have this media_item added to the library."""
providers = []
- sql_query = "SELECT provider FROM library_items WHERE item_id = ? AND media_type = ?"
- for db_row in await db_conn.execute_fetchall(sql_query, (db_item_id, media_type)):
+ sql_query = (
+ "SELECT provider FROM library_items WHERE item_id = ? AND media_type = ?"
+ )
+ for db_row in await db_conn.execute_fetchall(
+ sql_query, (db_item_id, media_type)
+ ):
providers.append(db_row[0])
return providers
) -> int:
"""Try to get existing item in db by matching the new item's external id's."""
for key, value in media_item.external_ids.items():
- sql_query = (
- "SELECT (item_id) FROM external_ids WHERE media_type=? AND key=? AND value=?;"
- )
+ sql_query = "SELECT (item_id) FROM external_ids \
+ WHERE media_type=? AND key=? AND value=?;"
for db_row in await db_conn.execute_fetchall(
sql_query, (media_item.media_type, key, value)
):
"""
- HTTPStreamer: handles all audio streaming to players,
- either by sending tracks one by one or send one continuous stream
- of music with crossfade/gapless support (queue stream).
+HTTPStreamer: handles all audio streaming to players.
+
+Either by sending tracks one by one or send one continuous stream
+of music with crossfade/gapless support (queue stream).
"""
import asyncio
-import concurrent
import gc
import io
import logging
import signal
import subprocess
import threading
-from asyncio import CancelledError
from contextlib import suppress
import pyloudnorm
"""Built-in streamer using sox and webserver."""
def __init__(self, mass):
+ """Initialize class."""
self.mass = mass
self.local_ip = get_ip()
self.analyze_jobs = {}
@require_local_subnet
async def async_stream(self, http_request):
- """
- start stream for a player
- """
+ """Start stream for a player."""
# make sure we have valid params
player_id = http_request.match_info.get("player_id", "")
player_queue = self.mass.player_manager.get_player_queue(player_id)
if not queue_item:
return web.Response(status=404, reason="Invalid Queue item Id")
# prepare headers as audio/flac content
- resp = web.StreamResponse(status=200, reason="OK", headers={"Content-Type": "audio/flac"})
+ resp = web.StreamResponse(
+ status=200, reason="OK", headers={"Content-Type": "audio/flac"}
+ )
await resp.prepare(http_request)
# run the streamer in executor to prevent the subprocess locking up our eventloop
cancelled = threading.Event()
)
else:
bg_task = self.mass.loop.run_in_executor(
- None, self.__get_queue_item_stream, player_id, queue_item, resp, cancelled
+ None,
+ self.__get_queue_item_stream,
+ player_id,
+ queue_item,
+ resp,
+ cancelled,
)
# let the streaming begin!
try:
return resp
def __get_queue_item_stream(self, player_id, queue_item, buffer, cancelled):
- """start streaming single queue track"""
+ """Start streaming single queue track."""
# pylint: disable=unused-variable
LOGGER.debug(
"stream single queue track started for track %s on player %s",
queue_item.name,
player_id,
)
- for is_last_chunk, audio_chunk in self.__get_audio_stream(player_id, queue_item, cancelled):
+ for is_last_chunk, audio_chunk in self.__get_audio_stream(
+ player_id, queue_item, cancelled
+ ):
if cancelled.is_set():
# http session ended
# we must consume the data to prevent hanging subprocess instances
continue
# put chunk in buffer
with suppress((BrokenPipeError, ConnectionResetError)):
- asyncio.run_coroutine_threadsafe(buffer.write(audio_chunk), self.mass.loop).result()
+ asyncio.run_coroutine_threadsafe(
+ buffer.write(audio_chunk), self.mass.loop
+ ).result()
# all chunks received: streaming finished
if cancelled.is_set():
LOGGER.debug(
else:
# indicate EOF if no more data
with suppress((BrokenPipeError, ConnectionResetError)):
- asyncio.run_coroutine_threadsafe(buffer.write_eof(), self.mass.loop).result()
+ asyncio.run_coroutine_threadsafe(
+ buffer.write_eof(), self.mass.loop
+ ).result()
LOGGER.debug(
- "stream single track finished for track %s on player %s", queue_item.name, player_id
+ "stream single track finished for track %s on player %s",
+ queue_item.name,
+ player_id,
)
def __get_queue_stream(self, player_id, buffer, cancelled):
def fill_buffer():
while True:
- chunk = sox_proc.stdout.read(128000)
+ chunk = sox_proc.stdout.read(128000) # noqa
if not chunk:
break
if chunk and not cancelled.is_set():
- with suppress(
- (
- BrokenPipeError,
- ConnectionResetError,
- )
- ):
+ with suppress((BrokenPipeError, ConnectionResetError)):
asyncio.run_coroutine_threadsafe(
buffer.write(chunk), self.mass.loop
).result()
# indicate EOF if no more data
if not cancelled.is_set():
with suppress((BrokenPipeError, ConnectionResetError)):
- asyncio.run_coroutine_threadsafe(buffer.write_eof(), self.mass.loop).result()
+ asyncio.run_coroutine_threadsafe(
+ buffer.write_eof(), self.mass.loop
+ ).result()
# start fill buffer task in background
fill_buffer_thread = threading.Thread(target=fill_buffer)
# get the (next) track in queue
if is_start:
# report start of queue playback so we can calculate current track/duration etc.
- queue_track = self.mass.add_job(player_queue.async_start_queue_stream()).result()
+ queue_track = self.mass.add_job(
+ player_queue.async_start_queue_stream()
+ ).result()
is_start = False
else:
queue_track = player_queue.next_item
# so we just use the entire original data
last_part = prev_chunk + chunk
if len(last_part) < fade_bytes:
- LOGGER.warning("Not enough data for crossfade: %s", len(last_part))
- if not player_queue.crossfade_enabled or len(last_part) < fade_bytes:
+ LOGGER.warning(
+ "Not enough data for crossfade: %s", len(last_part)
+ )
+ if (
+ not player_queue.crossfade_enabled
+ or len(last_part) < fade_bytes
+ ):
# crossfading is not enabled so just pass the (stripped) audio data
sox_proc.stdin.write(last_part)
bytes_written += len(last_part)
else:
LOGGER.info("streaming of queue for player %s completed", player_id)
- def __get_audio_stream(self, player_id, queue_item, cancelled, chunksize=128000, resample=None):
+ def __get_audio_stream(
+ self, player_id, queue_item, cancelled, chunksize=128000, resample=None
+ ):
"""Get audio stream from provider and apply additional effects/processing if needed."""
+ # pylint: disable=subprocess-popen-preexec-fn
player_queue = self.mass.player_manager.get_player_queue(player_id)
streamdetails = self.mass.add_job(
player_queue.async_get_stream_details(player_id, queue_item)
sox_options,
)
process = subprocess.Popen(
- args, shell=True, stdout=subprocess.PIPE, bufsize=chunksize, preexec_fn=os.setsid
+ args,
+ shell=True,
+ stdout=subprocess.PIPE,
+ bufsize=chunksize,
+ preexec_fn=os.setsid,
)
elif streamdetails.type in [StreamType.URL, StreamType.FILE]:
args = 'sox -t %s "%s" -t %s - %s' % (
)
args = shlex.split(args)
process = subprocess.Popen(
- args, shell=False, stdout=subprocess.PIPE, bufsize=chunksize, preexec_fn=os.setsid
+ args,
+ shell=False,
+ stdout=subprocess.PIPE,
+ bufsize=chunksize,
+ preexec_fn=os.setsid,
)
elif streamdetails.type == StreamType.EXECUTABLE:
args = "%s | sox -t %s - -t %s - %s" % (
sox_options,
)
process = subprocess.Popen(
- args, shell=True, stdout=subprocess.PIPE, bufsize=chunksize, preexec_fn=os.setsid
+ args,
+ shell=True,
+ stdout=subprocess.PIPE,
+ bufsize=chunksize,
+ preexec_fn=os.setsid,
)
else:
LOGGER.warning("no streaming options for %s", queue_item.name)
if queue_item.media_type == MediaType.Track:
self.mass.loop.run_in_executor(None, self.__analyze_audio, streamdetails)
- def __get_player_sox_options(self, player_id: str, streamdetails: StreamDetails) -> str:
+ def __get_player_sox_options(
+ self, player_id: str, streamdetails: StreamDetails
+ ) -> str:
"""Get player specific sox effect options."""
sox_options = []
player_conf = self.mass.config.get_player_config(player_id)
@staticmethod
def __crossfade_pcm_parts(fade_in_part, fade_out_part, pcm_args, fade_length):
- """crossfade two chunks of audio using sox"""
+ """Crossfade two chunks of audio using sox."""
# create fade-in part
fadeinfile = create_tempfile()
args = "sox --ignore-length -t %s - -t %s %s fade t %s" % (
fade_length,
)
args = shlex.split(args)
- process = subprocess.Popen(args, shell=False, stdout=subprocess.PIPE, stdin=subprocess.PIPE)
+ process = subprocess.Popen(
+ args, shell=False, stdout=subprocess.PIPE, stdin=subprocess.PIPE
+ )
process.communicate(fade_out_part)
# create crossfade using sox and some temp files
# TODO: figure out how to make this less complex and without the tempfiles
pcm_args,
)
args = shlex.split(args)
- process = subprocess.Popen(args, shell=False, stdout=subprocess.PIPE, stdin=subprocess.PIPE)
+ process = subprocess.Popen(
+ args, shell=False, stdout=subprocess.PIPE, stdin=subprocess.PIPE
+ )
crossfade_part, _ = process.communicate()
fadeinfile.close()
fadeoutfile.close()
from music_assistant.player_manager import PlayerManager
from music_assistant.utils import callback, get_hostname, get_ip_pton, is_callback
from music_assistant.web import Web
-from zeroconf import DNSPointer, DNSRecord
-from zeroconf import Error as ZeroconfError
-from zeroconf import (
- InterfaceChoice,
- IPVersion,
- NonUniqueNameException,
- ServiceBrowser,
- ServiceInfo,
- ServiceStateChange,
- Zeroconf,
-)
+from zeroconf import NonUniqueNameException, ServiceInfo, Zeroconf
LOGGER = logging.getLogger("mass")
def __init__(self, datapath):
"""
- Create an instance of MusicAssistant
+ Create an instance of MusicAssistant.
+
:param datapath: file location to store the data
"""
await self.__async_setup_discovery()
async def async_stop(self):
- """stop running the music assistant server"""
+ """Stop running the music assistant server."""
LOGGER.info("Application shutdown")
self.signal_event(EVENT_SHUTDOWN)
self._exit = True
@callback
def get_provider(self, provider_id: str) -> Provider:
"""Return provider/plugin by id."""
- if not provider_id in self._providers:
+ if provider_id not in self._providers:
LOGGER.warning("Provider %s is not available", provider_id)
return None
return self._providers[provider_id]
@callback
- def get_providers(self, filter_type: Optional[ProviderType] = None) -> List[Provider]:
+ def get_providers(
+ self, filter_type: Optional[ProviderType] = None
+ ) -> List[Provider]:
"""Return all providers, optionally filtered by type."""
return [
item
def signal_event(self, event_msg: str, event_details: Any = None):
"""
Signal (systemwide) event.
+
:param event_msg: the eventmessage to signal
:param event_details: optional details to send with the event.
"""
) -> Callable:
"""
Add callback to event listeners.
+
Returns function to remove the listener.
:param cb_func: callback function or coroutine
:param event_filter: Optionally only listen for these events
return remove_listener
- def add_job(self, target: Callable[..., Any], *args: Any) -> Optional[asyncio.Future]:
+ def add_job(
+ self, target: Callable[..., Any], *args: Any
+ ) -> Optional[asyncio.Future]:
"""Add a job/task to the event loop.
+
target: target to call.
args: parameters for method to call.
"""
if threading.current_thread() is not threading.main_thread():
# called from other thread
if asyncio.iscoroutine(check_target):
- task = asyncio.run_coroutine_threadsafe(target, self.loop) # type: ignore
+ task = asyncio.run_coroutine_threadsafe(
+ target, self.loop
+ ) # type: ignore
elif asyncio.iscoroutinefunction(check_target):
task = asyncio.run_coroutine_threadsafe(target(*args), self.loop)
elif is_callback(check_target):
"""All logic for metadata retrieval."""
+# TODO: split up into (optional) providers
import json
import logging
-# TODO: split up into (optional) providers
import re
from typing import Optional
class MetaData:
- """several helpers to search and store metadata for mediaitems"""
+ """Several helpers to search and store metadata for mediaitems."""
# TODO: create periodic task to search for missing metadata
def __init__(self, mass):
+ """Initialize class."""
self.mass = mass
self.musicbrainz = MusicBrainz(mass)
self.fanarttv = FanartTv(mass)
async def async_setup(self):
- """async initialize of metadata module"""
+ """Async setup of metadata module."""
await self.musicbrainz.async_setup()
await self.fanarttv.async_setup()
async def async_get_artist_metadata(self, mb_artist_id, cur_metadata):
- """get/update rich metadata for an artist by providing the musicbrainz artist id"""
+ """Get/update rich metadata for an artist by providing the musicbrainz artist id."""
metadata = cur_metadata
- if not "fanart" in metadata:
+ if "fanart" not in metadata:
res = await self.fanarttv.async_get_artist_images(mb_artist_id)
if res:
self.merge_metadata(cur_metadata, res)
trackname=None,
track_isrc=None,
):
- """retrieve musicbrainz artist id for the given details"""
+ """Retrieve musicbrainz artist id for the given details."""
LOGGER.debug(
"searching musicbrainz for %s \
(albumname: %s - album_upc: %s - trackname: %s - track_isrc: %s)",
@staticmethod
def merge_metadata(cur_metadata, new_values):
- """merge new info into the metadata dict without overwiteing existing values"""
+ """Merge new info into the metadata dict without overwriting existing values."""
for key, value in new_values.items():
if not cur_metadata.get(key):
cur_metadata[key] = value
class MusicBrainz:
+ """Handle getting Id's from MusicBrainz."""
+
def __init__(self, mass):
+ """Initialize class."""
self.mass = mass
self.cache = mass.cache
self.throttler = None
self._http_session = None
async def async_setup(self):
- """perform async setup"""
+ """Perform async setup."""
self._http_session = aiohttp.ClientSession(
loop=self.mass.loop, connector=aiohttp.TCPConnector()
)
self.throttler = Throttler(rate_limit=1, period=1)
- async def async_search_artist_by_album(self, artistname, albumname=None, album_upc=None):
- """retrieve musicbrainz artist id by providing the artist name and albumname or upc"""
+ async def async_search_artist_by_album(
+ self, artistname, albumname=None, album_upc=None
+ ):
+ """Retrieve musicbrainz artist id by providing the artist name and albumname or upc."""
for searchartist in [
re.sub(LUCENE_SPECIAL, r"\\\1", artistname),
get_compare_string(artistname),
return artist["id"]
return ""
- async def async_search_artist_by_track(self, artistname, trackname=None, track_isrc=None):
- """retrieve artist id by providing the artist name and trackname or track isrc"""
+ async def async_search_artist_by_track(
+ self, artistname, trackname=None, track_isrc=None
+ ):
+ """Retrieve artist id by providing the artist name and trackname or track isrc."""
endpoint = "recording"
searchartist = re.sub(LUCENE_SPECIAL, r"\\\1", artistname)
# searchartist = searchartist.replace('/','').replace('\\','').replace('-', '')
) as response:
try:
result = await response.json()
- except Exception as exc:
+ except (
+ aiohttp.client_exceptions.ContentTypeError,
+ json.decoder.JSONDecodeError,
+ ) as exc:
msg = await response.text()
LOGGER.exception("%s - %s", str(exc), msg)
result = None
class FanartTv:
+ """FanartTv support for metadata retrieval."""
+
def __init__(self, mass):
+ """Initialize class."""
self.mass = mass
self.cache = mass.cache
self._http_session = None
self.throttler = None
async def async_setup(self):
- """perform async setup"""
+ """Perform async setup."""
self._http_session = aiohttp.ClientSession(
loop=self.mass.loop, connector=aiohttp.TCPConnector()
)
self.throttler = Throttler(rate_limit=1, period=2)
async def async_get_artist_images(self, mb_artist_id):
- """retrieve images by musicbrainz artist id"""
+ """Retrieve images by musicbrainz artist id."""
metadata = {}
data = await self.async_get_data("music/%s" % mb_artist_id)
if data:
metadata[key] = item["url"]
if data.get("artistthumb"):
url = data["artistthumb"][0]["url"]
- if not "2a96cbd8b46e442fc41c2b86b821562f" in url:
+ if "2a96cbd8b46e442fc41c2b86b821562f" not in url:
metadata["image"] = url
if data.get("musicbanner"):
metadata["banner"] = data["musicbanner"][0]["url"]
@async_use_cache(30)
async def async_get_data(self, endpoint, params=None):
- """get data from api"""
+ """Get data from api."""
if params is None:
params = {}
url = "http://webservice.fanart.tv/v3/%s" % endpoint
) as response:
try:
result = await response.json()
- except (aiohttp.client_exceptions.ContentTypeError, json.decoder.JSONDecodeError):
+ except (
+ aiohttp.client_exceptions.ContentTypeError,
+ json.decoder.JSONDecodeError,
+ ):
LOGGER.error("Failed to retrieve %s", endpoint)
text_result = await response.text()
LOGGER.debug(text_result)
HEADER = "header"
-
@dataclass
class ConfigEntry:
"""Model for a Config Entry."""
depends_on: str = "" # entry_key that needs to be set before this setting shows up in frontend
hidden: bool = False # hide from UI
value: Optional[Any] = None # set by the configuration manager
- store_hashed: bool = False # value will be hashed, non reversible
+ store_hashed: bool = False # value will be hashed, non reversible
"""Models and helpers for media items."""
from dataclasses import dataclass, field
-from enum import Enum, Enum
+from enum import Enum
from typing import List, Optional
class MediaType(int, Enum):
"""Enum for MediaType."""
+
Artist = 1
Album = 2
Track = 3
class ContributorRole(int, Enum):
"""Enum for Contributor Role."""
+
Artist = 1
Writer = 2
Producer = 3
class AlbumType(int, Enum):
"""Enum for Album type."""
+
Album = 1
Single = 2
Compilation = 3
class TrackQuality(int, Enum):
"""Enum for Track Quality."""
+
LOSSY_MP3 = 0
LOSSY_OGG = 1
LOSSY_AAC = 2
@dataclass
-class MediaItemProviderId():
+class MediaItemProviderId:
"""Model for a MediaItem's provider id."""
+
provider: str
item_id: str
quality: Optional[TrackQuality] = TrackQuality.UNKNOWN
class ExternalId(str, Enum):
"""Enum with external id's."""
+
MUSICBRAINZ = "musicbrainz"
UPC = "upc"
ISRC = "isrc"
-
@dataclass
class MediaItem(object):
"""Representation of a media item."""
+
item_id: str = ""
provider: str = ""
name: str = ""
@dataclass
class Artist(MediaItem):
- """Model for an artist"""
+ """Model for an artist."""
+
media_type: MediaType = MediaType.Artist
sort_name: str = ""
@dataclass
class Album(MediaItem):
- """Model for an album"""
+ """Model for an album."""
+
media_type: MediaType = MediaType.Album
version: str = ""
year: int = 0
@dataclass
class Track(MediaItem):
- """Model for a track"""
+ """Model for a track."""
+
media_type: MediaType = MediaType.Track
duration: int = 0
version: str = ""
@dataclass
class Playlist(MediaItem):
- """Model for a playlist"""
+ """Model for a playlist."""
+
media_type: MediaType = MediaType.Playlist
owner: str = ""
checksum: [Optional[str]] = None # some value to detect playlist track changes
@dataclass
class Radio(MediaItem):
- """Model for a radio station"""
+ """Model for a radio station."""
+
media_type: MediaType = MediaType.Radio
duration: int = 86400
@dataclass
-class SearchResult():
+class SearchResult:
"""Model for Media Item Search result."""
+
artists: List[Artist] = field(default_factory=list)
albums: List[Album] = field(default_factory=list)
tracks: List[Track] = field(default_factory=list)
SearchResult,
Track,
)
-from music_assistant.models.streamdetails import StreamDetails
from music_assistant.models.provider import Provider, ProviderType
+from music_assistant.models.streamdetails import StreamDetails
@dataclass
class MusicProvider(Provider):
"""
Base class for a Musicprovider.
+
Should be overriden in the provider specific implementation.
"""
) -> SearchResult:
"""
Perform search on musicprovider.
+
:param search_query: Search query.
:param media_types: A list of media_types to include. All types if None.
:param limit: Number of items to return in the search (per type).
@abstractmethod
async def async_get_artist(self, prov_artist_id: str) -> Artist:
- """Get full artist details by id"""
+ """Get full artist details by id."""
raise NotImplementedError
@abstractmethod
@abstractmethod
async def async_get_playlist(self, prov_playlist_id: str) -> Playlist:
- """Get full playlist details by id"""
+ """Get full playlist details by id."""
raise NotImplementedError
@abstractmethod
async def async_get_radio(self, prov_radio_id: str) -> Radio:
- """Get full radio details by id"""
+ """Get full radio details by id."""
raise NotImplementedError
@abstractmethod
raise NotImplementedError
@abstractmethod
- async def async_library_remove(self, prov_item_id: str, media_type: MediaType) -> bool:
+ async def async_library_remove(
+ self, prov_item_id: str, media_type: MediaType
+ ) -> bool:
"""Remove item from provider's library. Return true on succes."""
raise NotImplementedError
from typing import Any, Awaitable, Callable, List, Optional, Union
from music_assistant.models.config_entry import ConfigEntry
-from music_assistant.constants import EVENT_PLAYER_CONTROL_UPDATED, EVENT_PLAYER_CHANGED
class PlayerState(str, Enum):
config_entries: List[ConfigEntry] = field(default_factory=list)
updated_at: datetime = datetime.utcnow() # managed by playermanager!
active_queue: str = "" # managed by playermanager!
- group_parents: List[str] = field(default_factory=list) # managed by playermanager!
- cur_queue_item_id: str = None # managed by playermanager!
+ group_parents: List[str] = field(default_factory=list) # managed by playermanager!
+ cur_queue_item_id: str = None # managed by playermanager!
def __setattr__(self, name, value):
- """Event when control is updated. Do not override"""
+ """Event when control is updated. Do not override."""
if name == "updated_at":
# updated at is set by the on_update callback
# make sure we do not hit an endless loop
@dataclass
class PlayerControl:
- """Model for a player control which allows for a
- plugin-like structure to override common player commands."""
+ """
+ Model for a player control.
+
+ Allows for a plugin-like
+ structure to override common player commands.
+ """
type: PlayerControlType = PlayerControlType.UNKNOWN
id: str = ""
-"""
- Models and helpers for a player queue.
-"""
+"""Models and helpers for a player queue."""
import logging
import random
EVENT_QUEUE_ITEMS_UPDATED,
EVENT_QUEUE_UPDATED,
)
-from music_assistant.models.media_types import Track, MediaType
+from music_assistant.models.media_types import MediaType, Track
from music_assistant.models.player import PlayerFeature, PlayerState
from music_assistant.models.streamdetails import StreamDetails
-from music_assistant.utils import callback, json_serializer
+from music_assistant.utils import callback
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-public-methods
class QueueOption(str, Enum):
- """Enum representation of the queue (play) options"""
+ """Enum representation of the queue (play) options."""
Play = "play"
Replace = "replace"
queue_item_id: str = ""
def __init__(self, media_item=None):
+ """Initialize class."""
super().__init__()
self.queue_item_id = str(uuid.uuid4())
# if existing media_item given, load those values
class PlayerQueue:
- """
- Class that holds the queue items for a player.
- """
+ """Class that holds the queue items for a player."""
def __init__(self, mass, player_id):
+ """Initialize class."""
self.mass = mass
self.player_id = player_id
self._items = []
self.mass.add_job(self.__async_restore_saved_state())
async def async_close(self):
- """Call on shutdown/close."""
+ """Handle shutdown/close."""
# pylint: disable=unused-argument
await self.__async_save_state()
@property
def shuffle_enabled(self):
- """Shuffle enabled property"""
+ """Return shuffle enabled property."""
return self._shuffle_enabled
@shuffle_enabled.setter
def shuffle_enabled(self, enable_shuffle: bool):
- """enable/disable shuffle"""
+ """Set shuffle."""
if not self._shuffle_enabled and enable_shuffle:
# shuffle requested
self._shuffle_enabled = True
@property
def repeat_enabled(self):
- """Returns if crossfade is enabled for this player."""
+ """Return if crossfade is enabled for this player."""
return self._repeat_enabled
@repeat_enabled.setter
@property
def crossfade_enabled(self):
- """Returns if crossfade is enabled for this player's queue."""
- return self.mass.config.player_settings[self.player_id]["crossfade_duration"] > 0
+ """Return if crossfade is enabled for this player's queue."""
+ return (
+ self.mass.config.player_settings[self.player_id]["crossfade_duration"] > 0
+ )
@property
def cur_index(self):
"""
- Returns the current index of the queue.
+ Return the current index of the queue.
+
Returns None if queue is empty.
"""
if not self._items:
def cur_item_id(self):
"""
Return the queue item id of the current item in the queue.
+
Returns None if queue is empty.
"""
if self.cur_index is None or not len(self.items) > self.cur_index:
def cur_item(self):
"""
Return the current item in the queue.
+
Returns None if queue is empty.
"""
if self.cur_index is None or not len(self.items) > self.cur_index:
@property
def cur_item_time(self):
- """Returns the time (progress) for current (playing) item."""
+ """Return the time (progress) for current (playing) item."""
return self._cur_item_time
@property
def next_index(self):
- """
- Returns the next index for this player's queue.
- Returns None if queue is empty or no more items.
+ """Return the next index for this player's queue.
+
+ Return None if queue is empty or no more items.
"""
if not self.items:
# queue is empty
@property
def next_item(self):
- """
- Returns the next item in the queue.
+ """Return the next item in the queue.
+
Returns None if queue is empty or no more items.
"""
if self.next_index is not None:
@property
def items(self):
- """
- Returns all queue items for this player's queue.
- """
+ """Return all queue items for this player's queue."""
return self._items
@property
def use_queue_stream(self):
"""
- bool to indicate that we need to use the queue stream
- for example if crossfading is requested but a player doesn't natively support it
- it will send a constant stream of audio to the player with all tracks
+ Indicate that we need to use the queue stream.
+
+ For example if crossfading is requested but a player doesn't natively support it
+ it will send a constant stream of audio to the player with all tracks.
"""
supports_crossfade = PlayerFeature.CROSSFADE in self.player.features
return self.crossfade_enabled and not supports_crossfade
@callback
def get_item(self, index):
- """get item by index from queue"""
+ """Get item by index from queue."""
if index is not None and len(self.items) > index:
return self.items[index]
return None
@callback
def by_item_id(self, queue_item_id: str):
- """get item by queue_item_id from queue"""
+ """Get item by queue_item_id from queue."""
if not queue_item_id:
return None
for item in self.items:
else:
# at this point we don't know if the queue is synced with the player
# so just to be safe we send the queue_items to the player
- player_provider = self.mass.player_manager.get_player_provider(self.player_id)
+ player_provider = self.mass.player_manager.get_player_provider(
+ self.player_id
+ )
await player_provider.async_cmd_queue_load(self.player_id, self.items)
await self.async_play_index(prev_index)
else:
- LOGGER.warning("resume queue requested for %s but queue is empty", self.player.name)
+ LOGGER.warning(
+ "resume queue requested for %s but queue is empty", self.player.name
+ )
async def async_play_index(self, index):
"""Play item at index X in queue."""
self.mass.web.internal_url,
self.player.player_id,
)
- return await player_prov.async_cmd_play_uri(self.player_id, queue_stream_uri)
+ return await player_prov.async_cmd_play_uri(
+ self.player_id, queue_stream_uri
+ )
elif supports_queue:
try:
- return await player_prov.async_cmd_queue_play_index(self.player_id, index)
+ return await player_prov.async_cmd_queue_play_index(
+ self.player_id, index
+ )
except NotImplementedError:
# not supported by player, use load queue instead
LOGGER.debug(
self._items = self._items[index:]
await player_prov.async_cmd_queue_load(self.player_id, self._items)
else:
- return await player_prov.async_cmd_play_uri(self.player_id, self._items[index].uri)
+ return await player_prov.async_cmd_play_uri(
+ self.player_id, self._items[index].uri
+ )
async def async_move_item(self, queue_item_id, pos_shift=1):
"""
- move queue item x up/down the queue
+ Move queue item x up/down the queue.
+
param pos_shift: move item x positions down if positive value
move item x positions up if negative value
move item to top of queue as next item
await self.async_play_index(new_index)
async def async_load(self, queue_items: List[QueueItem]):
- """load (overwrite) queue with new items"""
+ """Load (overwrite) queue with new items."""
supports_queue = PlayerFeature.QUEUE in self.player.features
for index, item in enumerate(queue_items):
item.sort_index = index
async def async_insert(self, queue_items: List[QueueItem], offset=0):
"""
- insert new items at offset x from current position
- keeps remaining items in queue
+ Insert new items at offset x from current position.
+
+ Keeps remaining items in queue.
if offset 0, will start playing newly added item(s)
:param queue_items: a list of QueueItem
:param offset: offset from current queue position
item.sort_index = insert_at_index + index
if self.shuffle_enabled:
queue_items = self.__shuffle_items(queue_items)
- self._items = self._items[:insert_at_index] + queue_items + self._items[insert_at_index:]
+ self._items = (
+ self._items[:insert_at_index] + queue_items + self._items[insert_at_index:]
+ )
if self.use_queue_stream or not supports_queue:
if offset == 0:
await self.async_play_index(insert_at_index)
self.mass.add_job(self.__async_save_state())
async def async_append(self, queue_items: List[QueueItem]):
- """
- append new items at the end of the queue
- """
+ """Append new items at the end of the queue."""
supports_queue = PlayerFeature.QUEUE in self.player.features
for index, item in enumerate(queue_items):
item.sort_index = len(self.items) + index
self.mass.add_job(self.__async_save_state())
async def async_update(self, queue_items: List[QueueItem]):
- """
- update the existing queue items, mostly caused by reordering
- """
+ """Update the existing queue items, mostly caused by reordering."""
supports_queue = PlayerFeature.QUEUE in self.player.features
self._items = queue_items
if supports_queue and not self.use_queue_stream:
self.mass.add_job(self.__async_save_state())
async def async_clear(self):
- """
- clear all items in the queue
- """
+ """Clear all items in the queue."""
supports_queue = PlayerFeature.QUEUE in self.player.features
await self.mass.player_manager.async_cmd_stop(self.player_id)
self._items = []
self.mass.signal_event(EVENT_QUEUE_UPDATED, self.to_dict())
async def async_start_queue_stream(self):
- """called by the queue streamer when it starts playing the queue stream"""
+ """Call when queue_streamer starts playing the queue stream."""
self._last_queue_startindex = self._next_queue_startindex
return self.get_item(self._next_queue_startindex)
) -> StreamDetails:
"""
Get streamdetails for the given queue_item.
+
This is called just-in-time when a player/queue wants a QueueItem to be played.
Do not try to request streamdetails in advance as this is expiring data.
param player_id: The id of the player that will be playing the stream.
queue_item.item_id, queue_item.provider, lazy=True, refresh=True
)
# sort by quality and check track availability
- for prov_media in sorted(full_track.provider_ids, key=lambda x: x.quality, reverse=True):
+ for prov_media in sorted(
+ full_track.provider_ids, key=lambda x: x.quality, reverse=True
+ ):
# get streamdetails from provider
music_prov = self.mass.get_provider(prov_media.provider)
if not music_prov:
continue # provider temporary unavailable ?
- streamdetails = await music_prov.async_get_stream_details(prov_media.item_id)
+ streamdetails = await music_prov.async_get_stream_details(
+ prov_media.item_id
+ )
if streamdetails:
# set streamdetails as attribute on the queue_item
return None
def to_dict(self):
- """instance attributes as dict so it can be serialized to json"""
+ """Instance attributes as dict so it can be serialized to json."""
return {
"player_id": self.player.player_id,
"shuffle_enabled": self.shuffle_enabled,
@callback
def __get_queue_stream_index(self):
+ """Get index of queue stream."""
# player is playing a constant stream of the queue so we need to do this the hard way
queue_index = 0
elapsed_time_queue = self.player.elapsed_time
total_time = 0
track_time = 0
if self.items and len(self.items) > self._last_queue_startindex:
- queue_index = self._last_queue_startindex # holds the last starting position
+ queue_index = (
+ self._last_queue_startindex
+ ) # holds the last starting position
queue_track = None
while len(self.items) > queue_index:
queue_track = self.items[queue_index]
return queue_index, track_time
async def async_process_queue_update(self, new_index, track_time):
- """compare the queue index to determine if playback changed"""
+ """Compare the queue index to determine if playback changed."""
new_track = self.get_item(new_index)
if (not self._last_track and new_track) or self._last_track != new_track:
# queue track updated
# account for track changing state so trigger track change after 1 second
if self._last_track and self._last_track.streamdetails:
self._last_track.streamdetails.seconds_played = self._last_item_time
- self.mass.signal_event(EVENT_PLAYBACK_STOPPED, self._last_track.streamdetails)
+ self.mass.signal_event(
+ EVENT_PLAYBACK_STOPPED, self._last_track.streamdetails
+ )
if new_track and new_track.streamdetails:
self.mass.signal_event(EVENT_PLAYBACK_STARTED, new_track.streamdetails)
self._last_track = new_track
]:
# player stopped playing
if self._last_track:
- self.mass.signal_event(EVENT_PLAYBACK_STOPPED, self._last_track.streamdetails)
+ self.mass.signal_event(
+ EVENT_PLAYBACK_STOPPED, self._last_track.streamdetails
+ )
# update vars
if track_time > 2:
# account for track changing state so keep this a few seconds behind
@staticmethod
def __shuffle_items(queue_items):
- """shuffle a list of tracks"""
+ """Shuffle a list of tracks."""
# for now we use default python random function
# can be extended with some more magic last_played and stuff
return random.sample(queue_items, len(queue_items))
def __index_by_id(self, queue_item_id):
- """get index by queue_item_id"""
+ """Get index by queue_item_id."""
item_index = None
for index, item in enumerate(self.items):
if item.queue_item_id == queue_item_id:
return item_index
async def __async_restore_saved_state(self):
- """try to load the saved queue for this player from cache file"""
+ """Try to load the saved queue for this player from cache file."""
cache_str = "queue_state_%s" % self.player.player_id
cache_data = await self.mass.cache.async_get(cache_str)
if cache_data:
# pylint: enable=unused-argument
async def __async_save_state(self):
- """save current queue settings to file"""
+ """Save current queue settings to file."""
cache_str = "queue_state_%s" % self.player_id
cache_data = {
"shuffle_enabled": self._shuffle_enabled,
from music_assistant.models.player_queue import QueueItem
from music_assistant.models.provider import Provider, ProviderType
+
@dataclass
class PlayerProvider(Provider):
"""
- Base class for a Playerprovider
- Should be overridden/subclassed by provider specific implementation.
+ Base class for a Playerprovider.
+
+ Should be overridden/subclassed by provider specific implementation.
"""
+
type: ProviderType = ProviderType.PLAYER_PROVIDER
# SERVICE CALLS / PLAYER COMMANDS
@abstractmethod
async def async_cmd_play_uri(self, player_id: str, uri: str):
"""
- Play the specified uri/url on the goven player.
- :param player_id: player_id of the player to handle the command.
+ Play the specified uri/url on the given player.
+
+ :param player_id: player_id of the player to handle the command.
"""
raise NotImplementedError
@abstractmethod
async def async_cmd_stop(self, player_id: str):
"""
- Send STOP command to given player.
- :param player_id: player_id of the player to handle the command.
+ Send STOP command to given player.
+
+ :param player_id: player_id of the player to handle the command.
"""
raise NotImplementedError
@abstractmethod
async def async_cmd_play(self, player_id: str):
"""
- Send STOP command to given player.
- :param player_id: player_id of the player to handle the command.
+ Send PLAY command to given player.
+
+ :param player_id: player_id of the player to handle the command.
"""
raise NotImplementedError
@abstractmethod
async def async_cmd_pause(self, player_id: str):
"""
- Send PAUSE command to given player.
- :param player_id: player_id of the player to handle the command.
+ Send PAUSE command to given player.
+
+ :param player_id: player_id of the player to handle the command.
"""
raise NotImplementedError
@abstractmethod
async def async_cmd_next(self, player_id: str):
"""
- Send NEXT TRACK command to given player.
- :param player_id: player_id of the player to handle the command.
+ Send NEXT TRACK command to given player.
+
+ :param player_id: player_id of the player to handle the command.
"""
raise NotImplementedError
@abstractmethod
async def async_cmd_previous(self, player_id: str):
"""
- Send PREVIOUS TRACK command to given player.
- :param player_id: player_id of the player to handle the command.
+ Send PREVIOUS TRACK command to given player.
+
+ :param player_id: player_id of the player to handle the command.
"""
raise NotImplementedError
@abstractmethod
async def async_cmd_power_on(self, player_id: str):
"""
- Send POWER ON command to given player.
- :param player_id: player_id of the player to handle the command.
+ Send POWER ON command to given player.
+
+ :param player_id: player_id of the player to handle the command.
"""
raise NotImplementedError
@abstractmethod
async def async_cmd_power_off(self, player_id: str):
"""
- Send POWER OFF command to given player.
- :param player_id: player_id of the player to handle the command.
+ Send POWER OFF command to given player.
+
+ :param player_id: player_id of the player to handle the command.
"""
raise NotImplementedError
@abstractmethod
async def async_cmd_volume_set(self, player_id: str, volume_level: int):
"""
- Send volume level command to given player.
- :param player_id: player_id of the player to handle the command.
- :param volume_level: volume level to set (0..100).
+ Send volume level command to given player.
+
+ :param player_id: player_id of the player to handle the command.
+ :param volume_level: volume level to set (0..100).
"""
raise NotImplementedError
@abstractmethod
async def async_cmd_volume_mute(self, player_id: str, is_muted=False):
"""
- Send volume MUTE command to given player.
- :param player_id: player_id of the player to handle the command.
- :param is_muted: bool with new mute state.
+ Send volume MUTE command to given player.
+
+ :param player_id: player_id of the player to handle the command.
+ :param is_muted: bool with new mute state.
"""
raise NotImplementedError
async def async_cmd_queue_play_index(self, player_id: str, index: int):
"""
- Play item at index X on player's queue
- :param player_id: player_id of the player to handle the command.
- :param index: (int) index of the queue item that should start playing
+ Play item at index X on player's queue.
+
+ :param player_id: player_id of the player to handle the command.
+ :param index: (int) index of the queue item that should start playing
"""
raise NotImplementedError
async def async_cmd_queue_load(self, player_id: str, queue_items: List[QueueItem]):
"""
- Load/overwrite given items in the player's queue implementation
- :param player_id: player_id of the player to handle the command.
- :param queue_items: a list of QueueItems
+ Load/overwrite given items in the player's queue implementation.
+
+ :param player_id: player_id of the player to handle the command.
+ :param queue_items: a list of QueueItems
"""
raise NotImplementedError
- async def async_cmd_queue_insert(self,
- player_id: str,
- queue_items: List[QueueItem],
- insert_at_index: int):
+ async def async_cmd_queue_insert(
+ self, player_id: str, queue_items: List[QueueItem], insert_at_index: int
+ ):
"""
- Insert new items at position X into existing queue.
- If insert_at_index 0 or None, will start playing newly added item(s)
- :param player_id: player_id of the player to handle the command.
- :param queue_items: a list of QueueItems
- :param insert_at_index: queue position to insert new items
+ Insert new items at position X into existing queue.
+
+ If insert_at_index 0 or None, will start playing newly added item(s)
+ :param player_id: player_id of the player to handle the command.
+ :param queue_items: a list of QueueItems
+ :param insert_at_index: queue position to insert new items
"""
+ # pylint: disable=abstract-method
raise NotImplementedError
- async def async_cmd_queue_append(self, player_id: str, queue_items: List[QueueItem]):
+ async def async_cmd_queue_append(
+ self, player_id: str, queue_items: List[QueueItem]
+ ):
"""
- Append new items at the end of the queue.
- :param player_id: player_id of the player to handle the command.
- :param queue_items: a list of QueueItems
+ Append new items at the end of the queue.
+
+ :param player_id: player_id of the player to handle the command.
+ :param queue_items: a list of QueueItems
"""
+ # pylint: disable=abstract-method
raise NotImplementedError
- async def async_cmd_queue_update(self, player_id: str, queue_items: List[QueueItem]):
+ async def async_cmd_queue_update(
+ self, player_id: str, queue_items: List[QueueItem]
+ ):
"""
- Overwrite the existing items in the queue, used for reordering.
- :param player_id: player_id of the player to handle the command.
- :param queue_items: a list of QueueItems
+ Overwrite the existing items in the queue, used for reordering.
+
+ :param player_id: player_id of the player to handle the command.
+ :param queue_items: a list of QueueItems
"""
+ # pylint: disable=abstract-method
raise NotImplementedError
async def async_cmd_queue_clear(self, player_id: str):
"""
- Clear the player's queue.
- :param player_id: player_id of the player to handle the command.
+ Clear the player's queue.
+
+ :param player_id: player_id of the player to handle the command.
"""
+ # pylint: disable=abstract-method
raise NotImplementedError
from abc import abstractmethod
from dataclasses import dataclass
from enum import Enum
-from typing import TYPE_CHECKING, Any, List, Optional
+from typing import TYPE_CHECKING, List
from music_assistant.models.config_entry import ConfigEntry
@abstractmethod
async def async_on_start(self) -> bool:
- """Called on startup.
+ """
Handle initialization of the provider based on config.
- Return bool if start was succesfull"""
+
+ Return bool if start was succesfull. Called on startup.
+ """
raise NotImplementedError
@abstractmethod
async def async_on_stop(self):
- """Called on shutdown. Handle correct close/cleanup of the provider on exit."""
+ """Handle correct close/cleanup of the provider on exit. Called on shutdown."""
raise NotImplementedError
async def async_on_reload(self):
- """Called on reload. Handle configuration changes for this provider."""
+ """Handle configuration changes for this provider. Called on reload."""
await self.async_on_stop()
await self.async_on_start()
-"""
- Models and helpers for the streamdetails of a MediaItem.
-"""
+"""Models and helpers for the streamdetails of a MediaItem."""
from dataclasses import dataclass
from enum import Enum
class StreamType(str, Enum):
"""Enum with stream types."""
+
EXECUTABLE = "executable"
URL = "url"
FILE = "file"
class ContentType(str, Enum):
"""Enum with stream content types."""
+
OGG = "ogg"
FLAC = "flac"
MP3 = "mp3"
@dataclass
-class StreamDetails():
+class StreamDetails:
"""Model for streamdetails."""
+
type: StreamType
provider: str
item_id: str
from typing import List, Optional
import aiohttp
-from PIL import Image
from music_assistant.cache import async_cached, async_cached_generator
from music_assistant.constants import EVENT_MUSIC_SYNC_STATUS
from music_assistant.models.media_types import (
from music_assistant.models.musicprovider import MusicProvider
from music_assistant.models.provider import ProviderType
from music_assistant.utils import compare_strings, run_periodic
-
+from PIL import Image
LOGGER = logging.getLogger("mass")
def sync_task(desc):
- """Decorator to report a sync task."""
+ """Return decorator to report a sync task."""
def wrapper(func):
@functools.wraps(func)
# check if this sync task is not already running
for sync_prov_id, sync_desc in method_class.running_sync_jobs:
if sync_prov_id == prov_id and sync_desc == desc:
- LOGGER.debug("Syncjob %s for provider %s is already running!", desc, prov_id)
+ LOGGER.debug(
+ "Syncjob %s for provider %s is already running!", desc, prov_id
+ )
return
LOGGER.debug("Start syncjob %s for provider %s.", desc, prov_id)
sync_job = (prov_id, desc)
method_class.running_sync_jobs.append(sync_job)
- method_class.mass.signal_event(EVENT_MUSIC_SYNC_STATUS, method_class.running_sync_jobs)
+ method_class.mass.signal_event(
+ EVENT_MUSIC_SYNC_STATUS, method_class.running_sync_jobs
+ )
await func(*args)
LOGGER.info("Finished syncing %s for provider %s", desc, prov_id)
method_class.running_sync_jobs.remove(sync_job)
- method_class.mass.signal_event(EVENT_MUSIC_SYNC_STATUS, method_class.running_sync_jobs)
+ method_class.mass.signal_event(
+ EVENT_MUSIC_SYNC_STATUS, method_class.running_sync_jobs
+ )
return async_wrapped
"""Several helpers around the musicproviders."""
def __init__(self, mass):
+ """Initialize class."""
self.running_sync_jobs = []
self.mass = mass
self.cache = mass.cache
return await self.async_get_radio(item_id, provider_id)
return None
- async def async_get_artist(self, item_id: str, provider_id: str, lazy: bool = True) -> Artist:
+ async def async_get_artist(
+ self, item_id: str, provider_id: str, lazy: bool = True
+ ) -> Artist:
"""Return artist details for the given provider artist id."""
assert item_id and provider_id
db_id = await self.mass.database.async_get_database_id(
if not provider.available:
return None
cache_key = f"{provider_id}.get_artist.{item_id}"
- artist = await async_cached(self.cache, cache_key, provider.async_get_artist(item_id))
+ artist = await async_cached(
+ self.cache, cache_key, provider.async_get_artist(item_id)
+ )
if not artist:
- raise Exception("Artist %s not found on provider %s" % (item_id, provider_id))
+ raise Exception(
+ "Artist %s not found on provider %s" % (item_id, provider_id)
+ )
if lazy:
self.mass.add_job(self.__async_add_artist(artist))
artist.is_lazy = True
return await self.mass.database.async_get_artist(db_id)
async def async_get_album(
- self, item_id: str, provider_id: str, lazy=True, album_details: Optional[Album] = None
+ self,
+ item_id: str,
+ provider_id: str,
+ lazy=True,
+ album_details: Optional[Album] = None,
) -> Album:
"""Return album details for the given provider album id."""
assert item_id and provider_id
self.cache, cache_key, provider.async_get_album(item_id)
)
if not album_details:
- raise Exception("Album %s not found on provider %s" % (item_id, provider_id))
+ raise Exception(
+ "Album %s not found on provider %s" % (item_id, provider_id)
+ )
if lazy:
self.mass.add_job(self.__async_add_album(album_details))
album_details.is_lazy = True
)
if db_id and refresh:
# in some cases (e.g. at playback time or requesting full track info)
- # it's useful to have the track refreshed from the provider instead of the database cache
- # this is to make sure that the track is available and perhaps
+ # it's useful to have the track refreshed from the provider instead of
+ # the database cache to make sure that the track is available and perhaps
# another or a higher quality version is available.
if lazy:
self.mass.add_job(self.__async_match_track(db_id))
self.cache, cache_key, provider.async_get_track(item_id)
)
if not track_details:
- raise Exception("Track %s not found on provider %s" % (item_id, provider_id))
+ raise Exception(
+ "Track %s not found on provider %s" % (item_id, provider_id)
+ )
if lazy:
self.mass.add_job(self.__async_add_track(track_details))
track_details.is_lazy = True
db_id = await self.mass.database.async_add_radio(item_details)
return await self.mass.database.async_get_radio(db_id)
- async def async_get_album_tracks(self, item_id: str, provider_id: str) -> List[Track]:
- """Return album tracks for the given provider album id. Generator!"""
+ async def async_get_album_tracks(
+ self, item_id: str, provider_id: str
+ ) -> List[Track]:
+ """Return album tracks for the given provider album id. Generator."""
assert item_id and provider_id
if provider_id == "database":
# album tracks are not stored in db, we always fetch them (cached) from the provider.
)
if db_id:
# return database track instead if we have a match
- db_item = await self.mass.database.async_get_track(db_id, fulldata=False)
+ db_item = await self.mass.database.async_get_track(
+ db_id, fulldata=False
+ )
db_item.disc_number = item.disc_number
db_item.track_number = item.track_number
yield db_item
else:
yield item
- async def async_get_playlist_tracks(self, item_id: str, provider_id: str) -> List[Track]:
- """Return playlist tracks for the given provider playlist id. Generator!"""
+ async def async_get_playlist_tracks(
+ self, item_id: str, provider_id: str
+ ) -> List[Track]:
+ """Return playlist tracks for the given provider playlist id. Generator."""
assert item_id and provider_id
if provider_id == "database":
# playlist tracks are not stored in db, we always fetch them (cached) from the provider.
pos += 1
yield item
- async def async_get_artist_toptracks(self, artist_id: str, provider_id: str) -> List[Track]:
- """Return top tracks for an artist. Generator!"""
+ async def async_get_artist_toptracks(
+ self, artist_id: str, provider_id: str
+ ) -> List[Track]:
+ """Return top tracks for an artist. Generator."""
async with self.mass.database.db_conn() as db_conn:
if provider_id == "database":
# tracks from all providers
)
for prov_id in artist.provider_ids:
provider = self.mass.get_provider(prov_id.provider)
- if not provider or not MediaType.Track in provider.supported_mediatypes:
+ if (
+ not provider
+ or MediaType.Track not in provider.supported_mediatypes
+ ):
continue
async for item in self.async_get_artist_toptracks(
prov_id.item_id, prov_id.provider
):
- if not item.item_id in item_ids:
+ if item.item_id not in item_ids:
yield item
item_ids.append(item.item_id)
else:
provider = self.mass.get_provider(provider_id)
cache_key = f"{provider_id}.artist_toptracks.{artist_id}"
async for item in async_cached_generator(
- self.cache, cache_key, provider.async_get_artist_toptracks(artist_id)
+ self.cache,
+ cache_key,
+ provider.async_get_artist_toptracks(artist_id),
):
if item:
assert item.item_id and item.provider
db_id = await self.mass.database.async_get_database_id(
- item.provider, item.item_id, MediaType.Track, db_conn=db_conn
+ item.provider,
+ item.item_id,
+ MediaType.Track,
+ db_conn=db_conn,
)
if db_id:
# return database track instead if we have a match
- yield await self.mass.database.async_get_track(db_id, fulldata=False, db_conn=db_conn)
+ yield await self.mass.database.async_get_track(
+ db_id, fulldata=False, db_conn=db_conn
+ )
else:
yield item
- async def async_get_artist_albums(self, artist_id: str, provider_id: str) -> List[Album]:
- """Return (all) albums for an artist. Generator!"""
+ async def async_get_artist_albums(
+ self, artist_id: str, provider_id: str
+ ) -> List[Album]:
+ """Return (all) albums for an artist. Generator."""
async with self.mass.database.db_conn() as db_conn:
if provider_id == "database":
# albums from all providers
item_ids = []
- artist = await self.mass.database.async_get_artist(artist_id, True, db_conn=db_conn)
+ artist = await self.mass.database.async_get_artist(
+ artist_id, True, db_conn=db_conn
+ )
for prov_id in artist.provider_ids:
provider = self.mass.get_provider(prov_id.provider)
- if not provider or not MediaType.Album in provider.supported_mediatypes:
+ if (
+ not provider
+ or MediaType.Album not in provider.supported_mediatypes
+ ):
continue
async for item in self.async_get_artist_albums(
prov_id.item_id, prov_id.provider
):
- if not item.item_id in item_ids:
+ if item.item_id not in item_ids:
yield item
item_ids.append(item.item_id)
else:
)
if db_id:
# return database album instead if we have a match
- yield await self.mass.database.async_get_album(db_id, db_conn=db_conn)
+ yield await self.mass.database.async_get_album(
+ db_id, db_conn=db_conn
+ )
else:
yield item
async def async_get_library_artists(
self, orderby: str = "name", provider_filter: str = None
) -> List[Artist]:
- """Return all library artists, optionally filtered by provider. Generator!"""
+ """Return all library artists, optionally filtered by provider. Generator."""
async for item in self.mass.database.async_get_library_artists(
provider_id=provider_filter, orderby=orderby
):
async def async_get_library_albums(
self, orderby: str = "name", provider_filter: str = None
) -> List[Album]:
- """Return all library albums, optionally filtered by provider. Generator!"""
+ """Return all library albums, optionally filtered by provider. Generator."""
async for item in self.mass.database.async_get_library_albums(
provider_id=provider_filter, orderby=orderby
):
async def async_get_library_tracks(
self, orderby: str = "name", provider_filter: str = None
) -> List[Track]:
- """Return all library tracks, optionally filtered by provider. Generator!"""
+ """Return all library tracks, optionally filtered by provider. Generator."""
async for item in self.mass.database.async_get_library_tracks(
provider_id=provider_filter, orderby=orderby
):
async def async_get_library_playlists(
self, orderby: str = "name", provider_filter: str = None
) -> List[Playlist]:
- """Return all library playlists, optionally filtered by provider. Generator!"""
+ """Return all library playlists, optionally filtered by provider. Generator."""
async for item in self.mass.database.async_get_library_playlists(
provider_id=provider_filter, orderby=orderby
):
async def async_get_library_radios(
self, orderby: str = "name", provider_filter: str = None
) -> List[Playlist]:
- """Return all library radios, optionally filtered by provider. Generator!"""
+ """Return all library radios, optionally filtered by provider. Generator."""
async for item in self.mass.database.async_get_library_radios(
provider_id=provider_filter, orderby=orderby
):
await self.__async_match_album(db_id)
return db_id
- async def __async_add_track(self, track: Track, album_id: Optional[str] = None) -> int:
+ async def __async_add_track(
+ self, track: Track, album_id: Optional[str] = None
+ ) -> int:
"""Add track to local db and return the new database id."""
track_artists = []
# we need to fetch track artists too
track.artists = track_artists
# fetch album details - prefer optional provided album_id
if album_id:
- album_details = await self.async_get_album(album_id, track.provider, lazy=False)
+ album_details = await self.async_get_album(
+ album_id, track.provider, lazy=False
+ )
if album_details:
track.album = album_details
# make sure we have a database album
async def __async_get_artist_musicbrainz_id(self, artist: Artist):
"""Fetch musicbrainz id by performing search using the artist name, albums and tracks."""
# try with album first
- async for lookup_album in self.async_get_artist_albums(artist.item_id, artist.provider):
+ async for lookup_album in self.async_get_artist_albums(
+ artist.item_id, artist.provider
+ ):
if not lookup_album:
continue
musicbrainz_id = await self.mass.metadata.async_get_mb_artist_id(
if musicbrainz_id:
return musicbrainz_id
# fallback to track
- async for lookup_track in self.async_get_artist_toptracks(artist.item_id, artist.provider):
+ async for lookup_track in self.async_get_artist_toptracks(
+ artist.item_id, artist.provider
+ ):
if not lookup_track:
continue
musicbrainz_id = await self.mass.metadata.async_get_mb_artist_id(
async def __async_match_artist(self, db_artist_id: int):
"""
Try to find matching artists on all providers for the provided (database) artist_id.
+
This is used to link objects of different providers together.
:attrib db_artist_id: Database artist_id.
"""
for provider in self.mass.get_providers(ProviderType.MUSIC_PROVIDER):
if provider.id in cur_providers:
continue
- LOGGER.debug("Trying to match artist %s on provider %s", artist.name, provider.name)
+ LOGGER.debug(
+ "Trying to match artist %s on provider %s", artist.name, provider.name
+ )
match_found = False
# try to get a match with some reference albums of this artist
- async for ref_album in self.async_get_artist_albums(artist.item_id, artist.provider):
+ async for ref_album in self.async_get_artist_albums(
+ artist.item_id, artist.provider
+ ):
if match_found:
break
searchstr = "%s - %s" % (artist.name, ref_album.name)
continue
# double safety check - artist must match exactly !
if not compare_strings(
- search_result_item.artist.name, artist.name, strict=strictness
+ search_result_item.artist.name,
+ artist.name,
+ strict=strictness,
):
continue
# just load this item in the database where it will be strictly matched
if not search_result_item:
continue
if not compare_strings(
- search_result_item.name, search_track.name, strict=strictness
+ search_result_item.name,
+ search_track.name,
+ strict=strictness,
):
continue
# double safety check - artist must match exactly !
continue
# load this item in the database where it will be strictly matched
await self.async_get_artist(
- match_artist.item_id, match_artist.provider, lazy=False
+ match_artist.item_id,
+ match_artist.provider,
+ lazy=False,
)
match_found = True
break
if match_found:
- LOGGER.debug("Found match for Artist %s on provider %s", artist.name, provider.name)
+ LOGGER.debug(
+ "Found match for Artist %s on provider %s",
+ artist.name,
+ provider.name,
+ )
else:
LOGGER.warning(
- "Could not find match for Artist %s on provider %s", artist.name, provider.name
+ "Could not find match for Artist %s on provider %s",
+ artist.name,
+ provider.name,
)
async def __async_match_album(self, db_album_id: int):
"""
Try to find matching album on all providers for the provided (database) album_id.
+
This is used to link objects of different providers/qualities together.
:attrib db_album_id: Database album_id.
"""
for provider in providers:
if provider.id in cur_providers:
continue
- LOGGER.debug("Trying to match album %s on provider %s", album.name, provider.name)
+ LOGGER.debug(
+ "Trying to match album %s on provider %s", album.name, provider.name
+ )
match_found = False
searchstr = "%s - %s" % (album.artist.name, album.name)
if album.version:
)
match_found = True
if match_found:
- LOGGER.debug("Found match for Album %s on provider %s", album.name, provider.name)
+ LOGGER.debug(
+ "Found match for Album %s on provider %s", album.name, provider.name
+ )
else:
LOGGER.warning(
- "Could not find match for Album %s on provider %s", album.name, provider.name
+ "Could not find match for Album %s on provider %s",
+ album.name,
+ provider.name,
)
async def __async_match_track(self, db_track_id: int):
"""
Try to find matching track on all providers for the provided (database) track_id.
+
This is used to link objects of different providers/qualities together.
:attrib db_track_id: Database track_id.
"""
self._match_jobs.append(match_job_id)
track = await self.mass.database.async_get_track(db_track_id, fulldata=False)
for provider in self.mass.get_providers(ProviderType.MUSIC_PROVIDER):
- LOGGER.debug("Trying to match track %s on provider %s", track.name, provider.name)
+ LOGGER.debug(
+ "Trying to match track %s on provider %s", track.name, provider.name
+ )
match_found = False
searchstr = "%s - %s" % (track.artists[0].name, track.name)
if track.version:
if artist_match_found:
break
for search_item_artist in search_result_item.artists:
- if not compare_strings(artist.name, search_item_artist.name, strict=False):
+ if not compare_strings(
+ artist.name, search_item_artist.name, strict=False
+ ):
continue
# just load this item in the database where it will be strictly matched
await self.async_get_track(
artist_match_found = True
break
if match_found:
- LOGGER.debug("Found match for Track %s on provider %s", track.name, provider.name)
+ LOGGER.debug(
+ "Found match for Track %s on provider %s", track.name, provider.name
+ )
else:
LOGGER.warning(
- "Could not find match for Track %s on provider %s", track.name, provider.name
+ "Could not find match for Track %s on provider %s",
+ track.name,
+ provider.name,
)
################ Various convenience/helper methods ################
) -> SearchResult:
"""
Perform search on given provider.
+
:param search_query: Search query
:param provider_id: provider_id of the provider to perform the search on.
:param media_types: A list of media_types to include. All types if None.
provider = self.mass.get_provider(provider_id)
cache_key = f"{provider_id}.search.{search_query}.{media_types}.{limit}"
return await async_cached(
- self.cache, cache_key, provider.async_search(search_query, media_types, limit)
+ self.cache,
+ cache_key,
+ provider.async_search(search_query, media_types, limit),
)
async def async_global_search(
) -> SearchResult:
"""
Perform global search for media items on all providers.
+
:param search_query: Search query.
:param media_types: A list of media_types to include. All types if None.
:param limit: number of items to return in the search (per type).
for media_item in media_items:
# make sure we have a database item
db_item = await self.async_get_item(
- media_item.item_id, media_item.provider, media_item.media_type, lazy=False
+ media_item.item_id,
+ media_item.provider,
+ media_item.media_type,
+ lazy=False,
)
if not db_item:
continue
for prov in db_item.provider_ids:
provider = self.mass.get_provider(prov.provider)
if provider:
- result = await provider.async_library_add(prov.item_id, media_item.media_type)
+ result = await provider.async_library_add(
+ prov.item_id, media_item.media_type
+ )
# mark as library item in internal db
await self.mass.database.async_add_to_library(
db_item.item_id, db_item.media_type, prov.provider
for media_item in media_items:
# make sure we have a database item
db_item = await self.async_get_item(
- media_item.item_id, media_item.provider, media_item.media_type, lazy=False
+ media_item.item_id,
+ media_item.provider,
+ media_item.media_type,
+ lazy=False,
)
if not db_item:
continue
# this should all be handled in the frontend but these checks are here just to be safe
# a track can contain multiple versions on the same provider
# simply sort by quality and just add the first one (assuming track is still available)
- for track_version in sorted(track.provider_ids, key=lambda x: x.quality, reverse=True):
+ for track_version in sorted(
+ track.provider_ids, key=lambda x: x.quality, reverse=True
+ ):
if track_version.provider == playlist_prov.provider:
track_ids_to_add.append(track_version.item_id)
break
)
# return result of the action on the provider
provider = self.mass.get_provider(playlist_prov.provider)
- return await provider.async_add_playlist_tracks(playlist_prov.item_id, track_ids_to_add)
+ return await provider.async_add_playlist_tracks(
+ playlist_prov.item_id, track_ids_to_add
+ )
return False
async def async_remove_playlist_tracks(self, db_playlist_id, tracks: List[Track]):
img_url = ""
# we only retrieve items that we already have in cache
item = None
- if await self.mass.database.async_get_database_id(provider_id, item_id, media_type):
+ if await self.mass.database.async_get_database_id(
+ provider_id, item_id, media_type
+ ):
item = await self.async_get_item(item_id, provider_id, media_type)
if not item:
return ""
async def async_music_provider_sync(self, prov_id: str):
"""
Sync a music provider.
+
param prov_id: {string} -- provider id to sync
"""
provider = self.mass.get_provider(prov_id)
music_provider = self.mass.get_provider(provider_id)
prev_db_ids = [
item.item_id
- async for item in self.async_get_library_artists(provider_filter=provider_id)
+ async for item in self.async_get_library_artists(
+ provider_filter=provider_id
+ )
]
cur_db_ids = []
async for item in music_provider.async_get_library_artists():
db_item = await self.async_get_artist(item.item_id, provider_id, lazy=False)
cur_db_ids.append(db_item.item_id)
- if not db_item.item_id in prev_db_ids:
+ if db_item.item_id not in prev_db_ids:
await self.mass.database.async_add_to_library(
db_item.item_id, MediaType.Artist, provider_id
)
if not db_album:
LOGGER.error("provider %s album: %s", provider_id, str(item))
cur_db_ids.append(db_album.item_id)
- if not db_album.item_id in prev_db_ids:
+ if db_album.item_id not in prev_db_ids:
await self.mass.database.async_add_to_library(
db_album.item_id, MediaType.Album, provider_id
)
]
cur_db_ids = []
async for item in music_provider.async_get_library_tracks():
- db_item = await self.async_get_track(item.item_id, provider_id=provider_id, lazy=False)
+ db_item = await self.async_get_track(
+ item.item_id, provider_id=provider_id, lazy=False
+ )
cur_db_ids.append(db_item.item_id)
- if not db_item.item_id in prev_db_ids:
+ if db_item.item_id not in prev_db_ids:
await self.mass.database.async_add_to_library(
db_item.item_id, MediaType.Track, provider_id
)
music_provider = self.mass.get_provider(provider_id)
prev_db_ids = [
item.item_id
- async for item in self.async_get_library_playlists(provider_filter=provider_id)
+ async for item in self.async_get_library_playlists(
+ provider_filter=provider_id
+ )
]
cur_db_ids = []
async for playlist in music_provider.async_get_library_playlists():
# always add to db because playlist attributes could have changed
db_id = await self.mass.database.async_add_playlist(playlist)
cur_db_ids.append(db_id)
- if not db_id in prev_db_ids:
+ if db_id not in prev_db_ids:
await self.mass.database.async_add_to_library(
db_id, MediaType.Playlist, playlist.provider
)
@sync_task("radios")
async def async_library_radios_sync(self, provider_id: str):
- """sync library radios for given provider"""
+ """Sync library radios for given provider."""
music_provider = self.mass.get_provider(provider_id)
prev_db_ids = [
item.item_id
if not db_id:
db_id = await self.mass.database.async_add_radio(item)
cur_db_ids.append(db_id)
- if not db_id in prev_db_ids:
- await self.mass.database.async_add_to_library(db_id, MediaType.Radio, provider_id)
+ if db_id not in prev_db_ids:
+ await self.mass.database.async_add_to_library(
+ db_id, MediaType.Radio, provider_id
+ )
# process deletions
for db_id in prev_db_ids:
if db_id not in cur_db_ids:
-"""
- PlayerManager:
- Orchestrates all players from player providers and forwarding of commands and states.
-"""
+"""PlayerManager: Orchestrates all players from player providers."""
-from datetime import datetime
import logging
-from typing import List, Optional, Any
+from datetime import datetime
+from typing import Any, List, Optional
from music_assistant.constants import (
CONF_ENABLED,
class PlayerManager:
- """several helpers to handle playback through player providers"""
+ """Several helpers to handle playback through player providers."""
def __init__(self, mass):
+ """Initialize class."""
self.mass = mass
self._players = {}
self._org_players = {}
self._player_controls_config_entries = []
async def async_setup(self):
- """Async initialize of module"""
+ """Async initialize of module."""
self.mass.add_job(self.poll_task())
async def async_close(self):
async def poll_task(self):
"""Check for updates on players that need to be polled."""
for player in self._org_players.values():
- if (player.should_poll
- and (self._poll_ticks >= POLL_INTERVAL or player.state == PlayerState.Playing)
+ if player.should_poll and (
+ self._poll_ticks >= POLL_INTERVAL or player.state == PlayerState.Playing
):
# Just request update, value checking for changes is handled
await self.async_update_player(player)
@callback
def get_player_queue(self, player_id: str) -> PlayerQueue:
"""Return player's queue by player_id or None if player does not exist."""
- if not player_id in self._players:
+ if player_id not in self._players:
return None
player = self._players[player_id]
return self._player_queues.get(player.active_queue)
@callback
def get_player_control(self, control_id: str) -> PlayerControl:
"""Return PlayerControl by id."""
- if not control_id in self._controls:
+ if control_id not in self._controls:
LOGGER.warning("PlayerControl %s is not available", control_id)
return None
return self._controls[control_id]
await self.__async_create_player_state(player)
if is_new_player:
# create player queue
- if not player.player_id in self._player_queues:
- self._player_queues[player.player_id] = PlayerQueue(self.mass, player.player_id)
+ if player.player_id not in self._player_queues:
+ self._player_queues[player.player_id] = PlayerQueue(
+ self.mass, player.player_id
+ )
# TODO: turn on player if it was previously turned on ?
LOGGER.info(
- "New player added: %s/%s", player.provider_id, self._players[player.player_id].name
+ "New player added: %s/%s",
+ player.provider_id,
+ self._players[player.player_id].name,
)
self.mass.signal_event(EVENT_PLAYER_ADDED, self._players[player.player_id])
"""Update an existing player (or register as new if non existing)."""
if not player:
return
- if not player.player_id in self._players:
+ if player.player_id not in self._players:
return await self.async_add_player(player)
await self.__async_create_player_state(player)
# update all players using this playercontrol
for player_id, player in self._players.items():
conf = self.mass.config.player_settings[player_id]
- if control.id in [conf.get(CONF_POWER_CONTROL), conf.get(CONF_VOLUME_CONTROL)]:
+ if control.id in [
+ conf.get(CONF_POWER_CONTROL),
+ conf.get(CONF_VOLUME_CONTROL),
+ ]:
self.mass.add_job(self.async_update_player(player))
# SERVICE CALLS / PLAYER COMMANDS
queue_opt: QueueOption = QueueOption.Play,
):
"""
- Play media item(s) on the given player
+ Play media item(s) on the given player.
+
:param player_id: player_id of the player to handle the command.
:param media_item: media item(s) that should be played (single item or list of items)
:param queue_opt:
async def async_cmd_stop(self, player_id: str):
"""
Send STOP command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
# TODO: redirect playback related commands to parent player?
async def async_cmd_play(self, player_id: str):
"""
Send PLAY command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
# power on at play request
async def async_cmd_pause(self, player_id: str):
"""
Send PAUSE command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
return await self.get_player_provider(player_id).async_cmd_pause(player_id)
async def async_cmd_play_pause(self, player_id: str):
"""
Toggle play/pause on given player.
+
:param player_id: player_id of the player to handle the command.
"""
player = self.get_player(player_id)
async def async_cmd_next(self, player_id: str):
"""
Send NEXT TRACK command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
return await self.get_player_queue(player_id).async_next()
async def async_cmd_previous(self, player_id: str):
"""
Send PREVIOUS TRACK command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
return await self.get_player_queue(player_id).async_previous()
async def async_cmd_power_on(self, player_id: str):
"""
Send POWER ON command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
player = self._players[player_id]
async def async_cmd_power_off(self, player_id: str):
"""
Send POWER OFF command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
player = self._players[player_id]
async def async_cmd_power_toggle(self, player_id: str):
"""
Send POWER TOGGLE command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
player = self._players[player_id]
async def async_cmd_volume_set(self, player_id: str, volume_level: int):
"""
Send volume level command to given player.
+
:param player_id: player_id of the player to handle the command.
:param volume_level: volume level to set (0..100).
"""
child_player = self._players.get(child_player_id)
if child_player and child_player.available and child_player.powered:
cur_child_volume = child_player.volume_level
- new_child_volume = cur_child_volume + (cur_child_volume * volume_dif_percent)
+ new_child_volume = cur_child_volume + (
+ cur_child_volume * volume_dif_percent
+ )
await self.async_cmd_volume_set(child_player_id, new_child_volume)
# regular volume command
else:
async def async_cmd_volume_up(self, player_id: str):
"""
Send volume UP command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
player = self._players[player_id]
async def async_cmd_volume_down(self, player_id: str):
"""
Send volume DOWN command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
player = self._players[player_id]
async def async_cmd_volume_mute(self, player_id: str, is_muted=False):
"""
Send MUTE command to given player.
+
:param player_id: player_id of the player to handle the command.
:param is_muted: bool with the new mute state.
"""
# OTHER/HELPER FUNCTIONS
- async def async_get_gain_correct(self, player_id: str, item_id: str, provider_id: str):
+ async def async_get_gain_correct(
+ self, player_id: str, item_id: str, provider_id: str
+ ):
"""Get gain correction for given player / track combination."""
player_conf = self.mass.config.get_player_config(player_id)
if not player_conf["volume_normalisation"]:
return 0
target_gain = int(player_conf["target_volume"])
fallback_gain = int(player_conf["fallback_gain_correct"])
- track_loudness = await self.mass.database.async_get_track_loudness(item_id, provider_id)
+ track_loudness = await self.mass.database.async_get_track_loudness(
+ item_id, provider_id
+ )
if track_loudness is None:
gain_correct = fallback_gain
else:
async def __async_create_player_state(self, player: Player):
"""Create/update internal Player object with all calculated properties."""
self._org_players[player.player_id] = player
- player_enabled = bool(self.mass.config.get_player_config(player.player_id)[CONF_ENABLED])
+ player_enabled = bool(
+ self.mass.config.get_player_config(player.player_id)[CONF_ENABLED]
+ )
if player.player_id in self._players:
player_state = self._players[player.player_id]
else:
player_state.config_entries = self.__get_player_config_entries(player)
player_state.active_queue = active_queue
if active_queue in self._player_queues:
- player_state.cur_queue_item_id = self._player_queues[active_queue].cur_item_id
+ player_state.cur_queue_item_id = self._player_queues[
+ active_queue
+ ].cur_item_id
@callback
def __get_player_name(self, player: Player):
for group_player in self._players.values():
if not group_player.is_group_player:
continue
- if not player.player_id in group_player.group_childs:
+ if player.player_id not in group_player.group_childs:
continue
result.append(group_player.player_id)
return result
# append power control config entries
power_controls = self.get_player_controls(PlayerControlType.POWER)
if power_controls:
- controls = [{"text": item.name, "value": item.id} for item in power_controls]
+ controls = [
+ {"text": item.name, "value": item.id} for item in power_controls
+ ]
entries.append(
ConfigEntry(
entry_key=CONF_POWER_CONTROL,
# append volume control config entries
volume_controls = self.get_player_controls(PlayerControlType.VOLUME)
if volume_controls:
- controls = [{"text": item.name, "value": item.id} for item in volume_controls]
+ controls = [
+ {"text": item.name, "value": item.id} for item in volume_controls
+ ]
entries.append(
ConfigEntry(
entry_key=CONF_VOLUME_CONTROL,
@callback
def __player_updated(self, player_id: str, changed_value: str):
"""Call when player is updated."""
- if not player_id in self._players:
+ if player_id not in self._players:
return
player = self._players[player_id]
if not player.available and changed_value != "available":
self.mass.add_job(self.async_update_player(child_player))
if player_id in self._player_queues and player.active_queue == player_id:
self.mass.add_job(self._player_queues[player_id].async_update_state())
-
-
+"""Providers package."""
-#!/usr/bin/env python3
-# -*- coding:utf-8 -*-
+"""ChromeCast playerprovider."""
-import asyncio
import logging
-import time
-import types
import uuid
-from typing import List, Optional, Tuple
+from typing import List
-import aiohttp
-import attr
import pychromecast
-import zeroconf
-from music_assistant.constants import CONF_ENABLED, CONF_HOSTNAME, CONF_PORT
-from music_assistant.mass import MusicAssistant
-from music_assistant.models.config_entry import ConfigEntry, ConfigEntryType
-from music_assistant.models.player import DeviceInfo, Player, PlayerState
+from music_assistant.models.config_entry import ConfigEntry
from music_assistant.models.player_queue import QueueItem
from music_assistant.models.playerprovider import PlayerProvider
-from music_assistant.utils import try_parse_int
-from pychromecast.controllers.multizone import MultizoneController, MultizoneManager
-from pychromecast.socket_client import (
- CONNECTION_STATUS_CONNECTED,
- CONNECTION_STATUS_DISCONNECTED,
-)
+from pychromecast.controllers.multizone import MultizoneManager
from .const import PROV_ID, PROV_NAME, PROVIDER_CONFIG_ENTRIES
from .models import ChromecastInfo
class ChromecastProvider(PlayerProvider):
"""Support for ChromeCast Audio PlayerProvider."""
+ # pylint: disable=abstract-method
+
def __init__(self, *args, **kwargs):
"""Initialize."""
self.mz_mgr = MultizoneManager()
return PROVIDER_CONFIG_ENTRIES
async def async_on_start(self) -> bool:
- """Called on startup. Handle initialization of the provider based on config."""
+ """Handle initialization of the provider based on config."""
self._listener = pychromecast.CastListener(
self.__chromecast_add_update_callback,
self.__chromecast_remove_callback,
self.__chromecast_add_update_callback,
)
- self._browser = pychromecast.discovery.start_discovery(self._listener, self.mass.zeroconf)
+ self._browser = pychromecast.discovery.start_discovery(
+ self._listener, self.mass.zeroconf
+ )
return True
async def async_on_stop(self):
- """Called on shutdown. Handle correct close/cleanup of the provider on exit."""
+ """Handle correct close/cleanup of the provider on exit."""
if not self._browser:
return
# stop discovery
async def async_cmd_play_uri(self, player_id: str, uri: str):
"""
Play the specified uri/url on the goven player.
+
:param player_id: player_id of the player to handle the command.
"""
self.mass.add_job(self._players[player_id].play_uri, uri)
async def async_cmd_stop(self, player_id: str):
"""
Send STOP command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
self.mass.add_job(self._players[player_id].stop)
async def async_cmd_play(self, player_id: str):
"""
Send STOP command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
self.mass.add_job(self._players[player_id].play)
async def async_cmd_pause(self, player_id: str):
"""
Send PAUSE command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
self.mass.add_job(self._players[player_id].pause)
async def async_cmd_next(self, player_id: str):
"""
Send NEXT TRACK command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
self.mass.add_job(self._players[player_id].next)
async def async_cmd_previous(self, player_id: str):
"""
Send PREVIOUS TRACK command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
self.mass.add_job(self._players[player_id].previous)
async def async_cmd_power_on(self, player_id: str):
"""
Send POWER ON command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
self.mass.add_job(self._players[player_id].power_on)
async def async_cmd_power_off(self, player_id: str):
"""
Send POWER OFF command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
self.mass.add_job(self._players[player_id].power_off)
async def async_cmd_volume_set(self, player_id: str, volume_level: int):
"""
Send volume level command to given player.
+
:param player_id: player_id of the player to handle the command.
:param volume_level: volume level to set (0..100).
"""
async def async_cmd_volume_mute(self, player_id: str, is_muted=False):
"""
Send volume MUTE command to given player.
+
:param player_id: player_id of the player to handle the command.
:param is_muted: bool with new mute state.
"""
async def async_cmd_queue_load(self, player_id: str, queue_items: List[QueueItem]):
"""
- Load/overwrite given items in the player's queue implementation
+ Load/overwrite given items in the player's queue implementation.
+
:param player_id: player_id of the player to handle the command.
:param queue_items: a list of QueueItems
"""
self.mass.add_job(self._players[player_id].queue_load, queue_items)
- async def async_cmd_queue_append(self, player_id: str, queue_items: List[QueueItem]):
+ async def async_cmd_queue_append(
+ self, player_id: str, queue_items: List[QueueItem]
+ ):
"""
Append new items at the end of the queue.
+
:param player_id: player_id of the player to handle the command.
:param queue_items: a list of QueueItems
"""
port=service[5],
)
player_id = cast_info.uuid
- LOGGER.debug("Chromecast discovered: %s (%s)", cast_info.friendly_name, player_id)
+ LOGGER.debug(
+ "Chromecast discovered: %s (%s)", cast_info.friendly_name, player_id
+ )
if player_id in self._players:
# player already added, the player will take care of reconnects itself.
- LOGGER.debug("Player is already added: %s (%s)", cast_info.friendly_name, player_id)
+ LOGGER.debug(
+ "Player is already added: %s (%s)", cast_info.friendly_name, player_id
+ )
else:
player = ChromecastPlayer(self.mass, cast_info)
self._players[player_id] = player
self.mass.add_job(self._players[player_id].set_cast_info, cast_info)
def __chromecast_remove_callback(self, cast_uuid, cast_service_name, cast_service):
+ """Handle a Chromecast removed event."""
# pylint: disable=unused-argument
player_id = str(cast_service[1])
friendly_name = cast_service[3]
LOGGER.debug("Chromecast removed: %s - %s", friendly_name, player_id)
self.mass.add_job(self.mass.player_manager.async_remove_player(player_id))
-
-
-# class StatusListener:
-# def __init__(self, player_id, status_callback, mass):
-# self.__handle_callback = status_callback
-# self.mass = mass
-# self.player_id = player_id
-
-# def new_cast_status(self, status):
-# """chromecast status changed (like volume etc.)"""
-# self.mass.run_task(self.__handle_callback(caststatus=status))
-
-# def new_media_status(self, status):
-# """mediacontroller has new state"""
-# self.mass.run_task(self.__handle_callback(mediastatus=status))
-
-# def new_connection_status(self, status):
-# """will be called when the connection changes"""
-# if status.status == CONNECTION_STATUS_DISCONNECTED:
-# # schedule a new scan which will handle reconnects and group parent changes
-# self.mass.loop.run_in_executor(
-# None, self.mass.player_manager.providers[PROV_ID].run_chromecast_discovery
-# )
-
-
-# class MZListener:
-# def __init__(self, mz, callback, loop):
-# self._mz = mz
-# self._loop = loop
-# self.__handle_group_members_update = callback
-
-# def multizone_member_added(self, uuid):
-# asyncio.run_coroutine_threadsafe(
-# self.__handle_group_members_update(self._mz, added_player=str(uuid)),
-# self._loop,
-# )
-
-# def multizone_member_removed(self, uuid):
-# asyncio.run_coroutine_threadsafe(
-# self.__handle_group_members_update(self._mz, removed_player=str(uuid)),
-# self._loop,
-# )
-
-# def multizone_status_received(self):
-# asyncio.run_coroutine_threadsafe(
-# self.__handle_group_members_update(self._mz), self._loop
-# )
-
-
-# async def async_ __report_progress(self):
-# """report current progress while playing"""
-# # chromecast does not send updates of the player's progress (cur_time)
-# # so we need to send it in periodically
-# while self._state == PlayerState.Playing:
-# self.cur_time = self.media_status.adjusted_current_time
-# await asyncio.sleep(1)
-# self.__cc_report_progress_task = None
-
-# async def async_ handle_player_state(self, caststatus=None, mediastatus=None):
-# """handle a player state message from the socket"""
-# # handle generic cast status
-# if caststatus:
-# self.muted = caststatus.volume_muted
-# self.volume_level = caststatus.volume_level * 100
-# self.name = self._chromecast.name
-# # handle media status
-# if mediastatus:
-# if mediastatus.player_state in ["PLAYING", "BUFFERING"]:
-# self.state = PlayerState.Playing
-# self.powered = True
-# elif mediastatus.player_state == "PAUSED":
-# self.state = PlayerState.Paused
-# else:
-# self.state = PlayerState.Stopped
-# self.current_uri = mediastatus.content_id
-# self.cur_time = mediastatus.adjusted_current_time
-# if (
-# self._state == PlayerState.Playing
-# and self.__cc_report_progress_task is None
-# ):
-# self.__cc_report_progress_task = self.mass.add_job(
-# self.__report_progress()
-# )
-
-# def __chromecast_discovered(self, cast_info):
-# """callback when a (new) chromecast device is discovered"""
-# player_id = cast_info.uuid
-# player = self.mass.player_manager.get_player_sync(player_id)
-# if self.mass.player_manager.get_player_sync(player_id):
-# # player already added, the player will take care of reconnects itself.
-# LOGGER.warning("Player is already added: %s", player_id)
-# self.mass.add_job(player.async_set_cast_info(cast_info))
-# else:
-# player = ChromecastPlayer(self.mass, cast_info)
-# # player.cc = chromecast
-# # player.mz = None
-
-# # # register status listeners
-# # status_listener = StatusListener(
-# # player_id, player.handle_player_state, self.mass
-# # )
-# # if chromecast.cast_type == "group":
-# # mz = MultizoneController(chromecast.uuid)
-# # mz.register_listener(
-# # MZListener(mz, self.__handle_group_members_update, self.mass.loop)
-# # )
-# # chromecast.register_handler(mz)
-# # player.mz = mz
-# # chromecast.register_connection_listener(status_listener)
-# # chromecast.register_status_listener(status_listener)
-# # chromecast.media_controller.register_status_listener(status_listener)
-# # player.cc.wait()
-# self.mass.run_task(self.add_player(player))
-
-# def __update_group_players(self):
-# """update childs of all group players"""
-# for player in self.players:
-# if player.cc.cast_type == "group":
-# player.mz.update_members()
-
-# async def async_ __handle_group_members_update(
-# self, mz, added_player=None, removed_player=None
-# ):
-# """handle callback from multizone manager"""
-# group_player_id = str(uuid.UUID(mz._uuid))
-# group_player = await self.get_player(group_player_id)
-# if added_player:
-# player_id = str(uuid.UUID(added_player))
-# child_player = await self.get_player(player_id)
-# if child_player and player_id != group_player_id:
-# group_player.add_group_child(player_id)
-# LOGGER.debug("%s added to %s", child_player.name, group_player.name)
-# elif removed_player:
-# player_id = str(uuid.UUID(removed_player))
-# group_player.remove_group_child(player_id)
-# LOGGER.debug("%s removed from %s", player_id, group_player.name)
-# else:
-# for member in mz.members:
-# player_id = str(uuid.UUID(member))
-# child_player = await self.get_player(player_id)
-# if not child_player or player_id == group_player_id:
-# continue
-# if not player_id in group_player.group_childs:
-# group_player.add_group_child(player_id)
-# LOGGER.debug("%s added to %s", child_player.name, group_player.name)
"""Constants for the implementation."""
-from music_assistant.constants import CONF_ENABLED
-from music_assistant.models.config_entry import ConfigEntry, ConfigEntryType
PROV_ID = "chromecast"
PROV_NAME = "Chromecast"
+"""
+Class to hold all data about a chromecast for creating connections.
-"""Class to hold all data about a chromecast for creating connections.
- This also has the same attributes as the mDNS fields by zeroconf.
+This also has the same attributes as the mDNS fields by zeroconf.
"""
import logging
from dataclasses import dataclass, field
from typing import Optional, Tuple
from pychromecast.const import CAST_MANUFACTURERS
-from pychromecast.controllers.multizone import MultizoneController
-
from .const import PROV_ID
@dataclass()
class ChromecastInfo:
"""Class to hold all data about a chromecast for creating connections.
+
This also has the same attributes as the mDNS fields by zeroconf.
"""
friendly_name: Optional[str] = ""
def __post_init__(self):
- """Always convert UUID to string."""
+ """Convert UUID to string."""
self.uuid = str(self.uuid)
@property
class CastStatusListener:
"""Helper class to handle pychromecast status callbacks.
+
Necessary because a CastDevice entity can create a new socket client
and therefore callbacks from multiple chromecast connections can
potentially arrive. This class allows invalidating past chromecast objects.
def added_to_multizone(self, group_uuid):
"""Handle the cast added to a group."""
- LOGGER.debug("Player %s is added to group %s", self._cast_device.name, group_uuid)
+ LOGGER.debug(
+ "Player %s is added to group %s", self._cast_device.name, group_uuid
+ )
def removed_from_multizone(self, group_uuid):
"""Handle the cast removed from a group."""
def invalidate(self):
"""Invalidate this status listener.
+
All following callbacks won't be forwarded.
"""
# pylint: disable=protected-access
"""Representation of a Cast device on the network."""
import logging
-from typing import List, Optional
+import uuid
from datetime import datetime
+from typing import List, Optional
+
import pychromecast
-import uuid
-from music_assistant.models.player import DeviceInfo, Player, PlayerFeature, PlayerState
+from music_assistant.models.player import DeviceInfo, PlayerFeature, PlayerState
from music_assistant.models.player_queue import QueueItem
from music_assistant.utils import compare_strings
from pychromecast.controllers.multizone import MultizoneController
CONNECTION_STATUS_DISCONNECTED,
)
-from .const import PLAYER_CONFIG_ENTRIES, PROV_ID, PROVIDER_CONFIG_ENTRIES
+from .const import PLAYER_CONFIG_ENTRIES, PROV_ID
from .models import CastStatusListener, ChromecastInfo
LOGGER = logging.getLogger(PROV_ID)
class ChromecastPlayer:
"""Representation of a Cast device on the network.
+
This class is the holder of the pychromecast.Chromecast object and
handles all reconnects and audio group changing
"elected leader" itself.
self.media_status = None
self.media_status_received = None
self.mz_mgr = None
+ self.mz_manager = None
self._available = False
self._powered = False
self._status_listener: Optional[CastStatusListener] = None
@property
def name(self):
"""Return name of this player."""
- return self._chromecast.name if self._chromecast else self._cast_info.friendly_name
+ return (
+ self._chromecast.name if self._chromecast else self._cast_info.friendly_name
+ )
@property
def powered(self):
@property
def group_childs(self):
"""Return group_childs."""
- if self._cast_info.is_audio_group and self._chromecast and not self._is_speaker_group:
- return [str(uuid.UUID(item)) for item in self._chromecast.mz_controller.members]
+ if (
+ self._cast_info.is_audio_group
+ and self._chromecast
+ and not self._is_speaker_group
+ ):
+ return [
+ str(uuid.UUID(item)) for item in self._chromecast.mz_controller.members
+ ]
return []
@property
"""Disconnect Chromecast object if it is set."""
if self._chromecast is None:
return
- LOGGER.warning("[%s] Disconnecting from chromecast socket", self._cast_info.friendly_name)
+ LOGGER.warning(
+ "[%s] Disconnecting from chromecast socket", self._cast_info.friendly_name
+ )
self._available = False
self.mass.add_job(self._chromecast.disconnect)
self._invalidate()
self.media_status = None
self.media_status_received = None
self.mz_mgr = None
- self.mz_controller = None
if self._status_listener is not None:
self._status_listener.invalidate()
self._status_listener = None
self._cast_info.is_audio_group
and self._chromecast.mz_controller
and self._chromecast.mz_controller.members
- and compare_strings(self._chromecast.mz_controller.members[0], self.player_id)
+ and compare_strings(
+ self._chromecast.mz_controller.members[0], self.player_id
+ )
)
self.mass.add_job(self.mass.player_manager.async_update_player(self))
self._chromecast.play_media(uri, "audio/flac")
def queue_load(self, queue_items: List[QueueItem]):
- """load (overwrite) queue with new items"""
+ """Load (overwrite) queue with new items."""
if not self._chromecast.socket_client.is_connected:
LOGGER.warning("Ignore player command: Socket client is not connected.")
return
self.queue_append(queue_items[51:])
def queue_append(self, queue_items: List[QueueItem]):
- """
- append new items at the end of the queue
- """
+ """Append new items at the end of the queue."""
if not self._chromecast.socket_client.is_connected:
LOGGER.warning("Ignore player command: Socket client is not connected.")
return
self.__send_player_queue(queuedata)
def __create_queue_items(self, tracks):
- """create list of CC queue items from tracks"""
+ """Create list of CC queue items from tracks."""
queue_items = []
for track in tracks:
queue_item = self.__create_queue_item(track)
return queue_items
def __create_queue_item(self, track):
- """create CC queue item from track info"""
+ """Create CC queue item from track info."""
player_queue = self.mass.player_manager.get_player_queue(self.player_id)
return {
"opt_itemId": track.queue_item_id,
}
def __send_player_queue(self, queuedata):
- """Send new data to the CC queue"""
+ """Send new data to the CC queue."""
media_controller = self._chromecast.media_controller
+ # pylint: disable=protected-access
receiver_ctrl = media_controller._socket_client.receiver_controller
def send_queue():
media_controller.send_message(queuedata, inc_session_id=False)
if not media_controller.status.media_session_id:
- receiver_ctrl.launch_app(media_controller.app_id, callback_function=send_queue)
+ receiver_ctrl.launch_app(
+ media_controller.app_id, callback_function=send_queue
+ )
else:
send_queue()
-def chunks(l, n):
- """Yield successive n-sized chunks from l."""
- for i in range(0, len(l), n):
- yield l[i : i + n]
+def chunks(_list, chunk_size):
+ """Yield successive n-sized chunks from list."""
+ for i in range(0, len(_list), chunk_size):
+ yield _list[i : i + chunk_size]
from .demo_playerprovider import DemoPlayerProvider
+
async def async_setup(mass):
"""Perform async setup of this Plugin/Provider."""
prov = DemoPlayerProvider()
- await mass.async_register_provider(prov)
\ No newline at end of file
+ await mass.async_register_provider(prov)
+"""Demo music provider."""
"""Demo/test providers."""
-from abc import abstractmethod
-from dataclasses import dataclass
-from typing import List
import functools
+from typing import List
+import vlc
from music_assistant.models.config_entry import ConfigEntry
from music_assistant.models.player import DeviceInfo, Player, PlayerFeature, PlayerState
from music_assistant.models.player_queue import QueueItem
from music_assistant.models.playerprovider import PlayerProvider
-import vlc
PROV_ID = "demo_player"
PROV_NAME = "Demo/Test players"
class DemoPlayerProvider(PlayerProvider):
- """
- Demo PlayerProvider which provides fake players.
- """
+ """Demo PlayerProvider which provides fake players."""
def __init__(self, *args, **kwargs):
"""Initialize."""
return []
async def async_on_start(self) -> bool:
- """Called on startup. Handle initialization of the provider based on config."""
+ """Handle initialization of the provider based on config."""
# create some fake players
for count in range(5)[1:]:
player_id = f"demo_{count}"
return True
async def async_on_stop(self):
- """Called on shutdown. Handle correct close/cleanup of the provider on exit."""
+ """Handle correct close/cleanup of the provider on exit."""
for player_id, player in self._players.items():
player.vlc_player.release()
player.vlc_instance.release()
self._players = {}
def player_event(self, player_id, event):
- """Called on vlc player events."""
+ """Call on vlc player events."""
+ # pylint: disable = unused-argument
vlc_player: vlc.MediaPlayer = self._players[player_id].vlc_player
self._players[player_id].muted = vlc_player.audio_get_mute()
self._players[player_id].volume_level = vlc_player.audio_get_volume()
else:
self._players[player_id].state = PlayerState.Stopped
self._players[player_id].elapsed_time = int(vlc_player.get_time() / 1000)
- self.mass.add_job(self.mass.player_manager.async_update_player(self._players[player_id]))
+ self.mass.add_job(
+ self.mass.player_manager.async_update_player(self._players[player_id])
+ )
# SERVICE CALLS / PLAYER COMMANDS
async def async_cmd_play_uri(self, player_id: str, uri: str):
"""
Play the specified uri/url on the given player.
+
:param player_id: player_id of the player to handle the command.
"""
# self._players[player_id].current_uri = uri
async def async_cmd_stop(self, player_id: str):
"""
Send STOP command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
self.mass.add_job(self._players[player_id].vlc_player.stop)
async def async_cmd_play(self, player_id: str):
"""
- Send STOP command to given player.
+ Send PLAY command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
if self._players[player_id].vlc_player.get_media():
async def async_cmd_pause(self, player_id: str):
"""
Send PAUSE command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
self.mass.add_job(self._players[player_id].vlc_player.pause)
async def async_cmd_next(self, player_id: str):
"""
Send NEXT TRACK command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
self.mass.add_job(self._players[player_id].vlc_player.next_chapter)
async def async_cmd_previous(self, player_id: str):
"""
Send PREVIOUS TRACK command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
self.mass.add_job(self._players[player_id].vlc_player.previous_chapter)
async def async_cmd_power_on(self, player_id: str):
"""
Send POWER ON command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
self._players[player_id].powered = True
- self.mass.add_job(self.mass.player_manager.async_update_player(self._players[player_id]))
+ self.mass.add_job(
+ self.mass.player_manager.async_update_player(self._players[player_id])
+ )
async def async_cmd_power_off(self, player_id: str):
"""
Send POWER OFF command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
self.mass.add_job(self._players[player_id].vlc_player.stop)
self._players[player_id].powered = False
- self.mass.add_job(self.mass.player_manager.async_update_player(self._players[player_id]))
+ self.mass.add_job(
+ self.mass.player_manager.async_update_player(self._players[player_id])
+ )
async def async_cmd_volume_set(self, player_id: str, volume_level: int):
"""
Send volume level command to given player.
+
:param player_id: player_id of the player to handle the command.
:param volume_level: volume level to set (0..100).
"""
- self.mass.add_job(self._players[player_id].vlc_player.audio_set_volume, volume_level)
+ self.mass.add_job(
+ self._players[player_id].vlc_player.audio_set_volume, volume_level
+ )
async def async_cmd_volume_mute(self, player_id: str, is_muted=False):
"""
Send volume MUTE command to given player.
+
:param player_id: player_id of the player to handle the command.
:param is_muted: bool with new mute state.
"""
async def async_cmd_queue_play_index(self, player_id: str, index: int):
"""
- Play item at index X on player's queue
+ Play item at index X on player's queue.
+
:param player_id: player_id of the player to handle the command.
:param index: (int) index of the queue item that should start playing
"""
async def async_cmd_queue_load(self, player_id: str, queue_items: List[QueueItem]):
"""
- Load/overwrite given items in the player's queue implementation
+ Load/overwrite given items in the player's queue implementation.
+
:param player_id: player_id of the player to handle the command.
:param queue_items: a list of QueueItems
"""
):
"""
Insert new items at position X into existing queue.
+
If insert_at_index 0 or None, will start playing newly added item(s)
:param player_id: player_id of the player to handle the command.
:param queue_items: a list of QueueItems
"""
raise NotImplementedError
- async def async_cmd_queue_append(self, player_id: str, queue_items: List[QueueItem]):
+ async def async_cmd_queue_append(
+ self, player_id: str, queue_items: List[QueueItem]
+ ):
"""
Append new items at the end of the queue.
+
:param player_id: player_id of the player to handle the command.
:param queue_items: a list of QueueItems
"""
raise NotImplementedError
- async def async_cmd_queue_update(self, player_id: str, queue_items: List[QueueItem]):
+ async def async_cmd_queue_update(
+ self, player_id: str, queue_items: List[QueueItem]
+ ):
"""
Overwrite the existing items in the queue, used for reordering.
+
:param player_id: player_id of the player to handle the command.
:param queue_items: a list of QueueItems
"""
async def async_cmd_queue_clear(self, player_id: str):
"""
Clear the player's queue.
+
:param player_id: player_id of the player to handle the command.
"""
raise NotImplementedError
"""Filesystem musicprovider support for MusicAssistant."""
+# pylint: skip-file
+# flake8: noqa
import base64
import os
from typing import List
+import taglib
from music_assistant.constants import CONF_ENABLED
from music_assistant.models.media_types import (
Album,
)
from music_assistant.models.musicprovider import MusicProvider
from music_assistant.utils import LOGGER, parse_title_and_version
-import taglib
PROV_NAME = "Local files and playlists"
PROV_CLASS = "FileProvider"
artist.item_id = prov_item_id
artist.provider = self.prov_id
artist.name = name
- artist.ids.append(
- {"provider": self.prov_id, "item_id": artist.item_id}
- )
+ artist.ids.append({"provider": self.prov_id, "item_id": artist.item_id})
return artist
async def async_get_album(self, prov_item_id) -> Album:
playlist.provider = self.prov_id
playlist.name = itempath.split(os.sep)[-1].replace(".m3u", "")
playlist.is_editable = True
- playlist.ids.append(
- {"provider": self.prov_id, "item_id": prov_item_id}
- )
+ playlist.ids.append({"provider": self.prov_id, "item_id": prov_item_id})
playlist.owner = "disk"
playlist.checksum = os.path.getmtime(itempath)
return playlist
"""Plugin that enables integration with Home Assistant."""
-import asyncio
-import functools
import logging
-import os
from typing import List
import slugify as slug
entry_key=CONF_URL, entry_type=ConfigEntryType.STRING, description_key="hass_url"
)
CONFIG_ENTRY_TOKEN = ConfigEntry(
- entry_key=CONF_TOKEN, entry_type=ConfigEntryType.PASSWORD, description_key="hass_token"
+ entry_key=CONF_TOKEN,
+ entry_type=ConfigEntryType.PASSWORD,
+ description_key="hass_token",
)
CONFIG_ENTRY_PUBLISH_PLAYERS = ConfigEntry(
entry_key=CONF_PUBLISH_PLAYERS,
class HomeAssistantPlugin(Provider):
- """
- Homeassistant plugin
+ """Homeassistant plugin.
+
allows publishing of our players to hass
allows using hass entities (like switches, media_players or gui inputs) to be triggered
"""
return entries
async def async_on_start(self) -> bool:
- """Called on startup. Handle initialization of the provider based on config."""
+ """Handle initialization of the provider based on config."""
config = self.mass.config.get_provider_config(PROV_ID)
if IS_SUPERVISOR:
self._hass = HomeAssistant(loop=self.mass.loop)
else:
- self._hass = HomeAssistant(config[CONF_URL], config[CONF_TOKEN], loop=self.mass.loop)
+ self._hass = HomeAssistant(
+ config[CONF_URL], config[CONF_TOKEN], loop=self.mass.loop
+ )
# register callbacks
self._hass.register_event_callback(self.__async_hass_event)
self.mass.add_event_listener(
return True
async def async_on_stop(self):
- """Called on shutdown. Handle correct close/cleanup of the provider on exit."""
+ """Handle correct close/cleanup of the provider on exit."""
for task in self._tasks:
task.cancel()
if self._hass:
await self._hass.async_close()
async def __async_mass_event(self, event, event_data):
- """Received event from Music Assistant"""
+ """Receive event from Music Assistant."""
if event in [EVENT_PLAYER_CHANGED, EVENT_PLAYER_ADDED]:
await self.__async_publish_player(event_data)
# TODO: player removals
async def __async_hass_event(self, event_type, event_data):
- """Received event from Home Assistant"""
+ """Receive event from Home Assistant."""
if event_type == EVENT_STATE_CHANGED:
if event_data["entity_id"] in self._tracked_entities:
new_state = event_data["new_state"]
await self.mass.player_manager.async_cmd_volume_down(player_id)
elif service == "volume_set":
volume_level = service_data["volume_level"] * 100
- await self.mass.player_manager.async_cmd_volume_set(player_id, volume_level)
+ await self.mass.player_manager.async_cmd_volume_set(
+ player_id, volume_level
+ )
elif service == "media_play":
await self.mass.player_manager.async_cmd_play(player_id)
elif service == "media_pause":
elif service in ["play_media", "select_source"]:
return await self.__async_handle_play_media(player_id, service_data)
else:
- LOGGER.error("%s service is unhandled. Service data: %s", service, service_data)
+ LOGGER.error(
+ "%s service is unhandled. Service data: %s",
+ service,
+ service_data,
+ )
async def __async_handle_play_media(self, player_id, service_data):
"""Handle play media request from homeassistant."""
if not media_content_id:
media_content_id = service_data.get("source")
queue_opt = "add" if service_data.get("enqueue") else "play"
- if not "://" in media_content_id:
+ if "://" not in media_content_id:
media_items = []
for playlist_str in media_content_id.split(","):
playlist_str = playlist_str.strip()
if playlist:
media_items.append(playlist)
else:
- radio = await self.mass.music_manager.async_get_radio_by_name(playlist_str)
+ radio = await self.mass.music_manager.async_get_radio_by_name(
+ playlist_str
+ )
if radio:
media_items.append(radio)
queue_opt = "play"
playlist = await self.mass.music_manager.async_getplaylist(
"spotify", media_content_id.split(":")[-1]
)
- return await self.mass.player_manager.async_play_media(player_id, playlist, queue_opt)
+ return await self.mass.player_manager.async_play_media(
+ player_id, playlist, queue_opt
+ )
async def __async_publish_player(self, player: Player):
"""Publish player details to Home Assistant."""
if not player.available:
return
player_id = player.player_id
- entity_id = "media_player.mass_" + slug.slugify(player.name, separator="_").lower()
+ entity_id = (
+ "media_player.mass_" + slug.slugify(player.name, separator="_").lower()
+ )
player_queue = self.mass.player_manager.get_player_queue(player_id)
cur_item = player_queue.cur_item if player_queue else None
state_attributes = {
- "supported_features": 196541, # https://github.com/home-assistant/core/blob/dev/homeassistant/components/media_player/const.py#L59
+ "supported_features": 196541,
"friendly_name": player.name,
"source_list": self._sources,
"source": "unknown",
"media_duration": cur_item.duration if cur_item else None,
"media_position": player_queue.cur_item_time if player_queue else None,
"media_title": cur_item.name if cur_item else None,
- "media_artist": cur_item.artists[0].name if cur_item and cur_item.artists else None,
- "media_album_name": cur_item.album.name if cur_item and cur_item.album else None,
+ "media_artist": cur_item.artists[0].name
+ if cur_item and cur_item.artists
+ else None,
+ "media_album_name": cur_item.album.name
+ if cur_item and cur_item.album
+ else None,
"entity_picture": "",
"mass_player_id": player_id,
}
if cur_item:
host = self.mass.web.internal_url
item_type = "radio" if cur_item.media_type == MediaType.Radio else "track"
+ # pylint: disable=line-too-long
img_url = f"{host}/api/{item_type}/{cur_item.item_id}/thumb?provider={cur_item.provider}"
state_attributes["entity_picture"] = img_url
self._published_players[entity_id] = player.player_id
async for playlist in self.mass.music_manager.async_get_library_playlists()
]
self._sources += [
- playlist.name async for playlist in self.mass.music_manager.async_get_library_radios()
+ playlist.name
+ async for playlist in self.mass.music_manager.async_get_library_radios()
]
@callback
if entity_id.startswith("media_player.mass_"):
continue
result.append(
- {"value": f"volume_{entity_id}", "text": entity_name, "entity_id": entity_id}
+ {
+ "value": f"volume_{entity_id}",
+ "text": entity_name,
+ "entity_id": entity_id,
+ }
)
return result
continue
cur_state = entity_obj["state"] not in ["off", "unavailable"]
if control_entity.get("source"):
- cur_state = entity_obj["attributes"].get("source") == control_entity["source"]
+ cur_state = (
+ entity_obj["attributes"].get("source") == control_entity["source"]
+ )
await self.mass.player_manager.async_update_player_control(
control_entity["value"], cur_state
)
for control_entity in self.__get_volume_control_entities():
if control_entity["entity_id"] != entity_obj["entity_id"]:
continue
- cur_state = int(try_parse_float(entity_obj["attributes"].get("volume_level")) * 100)
+ cur_state = int(
+ try_parse_float(entity_obj["attributes"].get("volume_level")) * 100
+ )
await self.mass.player_manager.async_update_player_control(
control_entity["value"], cur_state
)
if not control_entity["value"] in enabled_controls:
continue
entity_id = control_entity["entity_id"]
- if not entity_id in self._hass.states:
+ if entity_id not in self._hass.states:
LOGGER.warning("entity not found: %s", entity_id)
continue
state_obj = self._hass.states[entity_id]
cur_state = state_obj["state"] not in ["off", "unavailable"]
source = control_entity.get("source")
if source:
- cur_state = state_obj["attributes"].get("source") == control_entity["source"]
+ cur_state = (
+ state_obj["attributes"].get("source") == control_entity["source"]
+ )
control = PlayerControl(
type=PlayerControlType.POWER,
control.entity_id = entity_id
control.source = source
await self.mass.player_manager.async_register_player_control(control)
- if not entity_id in self._tracked_entities:
+ if entity_id not in self._tracked_entities:
self._tracked_entities.append(entity_id)
async def __async_register_volume_controls(self):
if not control_entity["value"] in enabled_controls:
continue
entity_id = control_entity["entity_id"]
- if not entity_id in self._hass.states:
+ if entity_id not in self._hass.states:
LOGGER.warning("entity not found: %s", entity_id)
continue
- cur_volume = try_parse_float(self._hass.get_state(entity_id, "volume_level")) * 100
+ cur_volume = (
+ try_parse_float(self._hass.get_state(entity_id, "volume_level")) * 100
+ )
control = PlayerControl(
type=PlayerControlType.VOLUME,
id=control_entity["value"],
# store some vars on the control object for convenience
control.entity_id = entity_id
await self.mass.player_manager.async_register_player_control(control)
- if not entity_id in self._tracked_entities:
+ if entity_id not in self._tracked_entities:
self._tracked_entities.append(entity_id)
async def async_power_control_set_state(self, control_id: str, new_state: bool):
import aiohttp
from asyncio_throttle import Throttler
-from music_assistant.app_vars import get_app_var
+from music_assistant.app_vars import get_app_var # noqa
from music_assistant.constants import (
CONF_PASSWORD,
CONF_USERNAME,
CONFIG_ENTRIES = [
ConfigEntry(
- entry_key=CONF_USERNAME, entry_type=ConfigEntryType.STRING, description_key=CONF_USERNAME
+ entry_key=CONF_USERNAME,
+ entry_type=ConfigEntryType.STRING,
+ description_key=CONF_USERNAME,
),
ConfigEntry(
- entry_key=CONF_PASSWORD, entry_type=ConfigEntryType.PASSWORD, description_key=CONF_PASSWORD
+ entry_key=CONF_PASSWORD,
+ entry_type=ConfigEntryType.PASSWORD,
+ description_key=CONF_PASSWORD,
),
]
class QobuzProvider(MusicProvider):
"""Provider for the Qobux music service."""
+ # pylint: disable=abstract-method
+
_http_session = None
+ __user_auth_info = None
@property
def id(self) -> str:
@property
def supported_mediatypes(self) -> List[MediaType]:
"""Return MediaTypes the provider supports."""
- return [
- MediaType.Album,
- MediaType.Artist,
- MediaType.Playlist,
- MediaType.Track,
- ]
+ return [MediaType.Album, MediaType.Artist, MediaType.Playlist, MediaType.Track]
async def async_on_start(self) -> bool:
- """Called on startup. Handle initialization of the provider based on config."""
+ """Handle initialization of the provider based on config."""
# pylint: disable=attribute-defined-outside-init
self._http_session = aiohttp.ClientSession(
loop=self.mass.loop, connector=aiohttp.TCPConnector()
return False
self.__username = config[CONF_USERNAME]
self.__password = config[CONF_PASSWORD]
-
+
self.__user_auth_info = None
self.__logged_in = False
self._throttler = Throttler(rate_limit=4, period=1)
return True
async def async_on_stop(self):
- """Called on shutdown. Handle correct close/cleanup of the provider on exit."""
+ """Handle correct close/cleanup of the provider on exit."""
if self._http_session:
await self._http_session.close()
) -> SearchResult:
"""
Perform search on musicprovider.
+
:param search_query: Search query.
:param media_types: A list of media_types to include. All types if None.
:param limit: Number of items to return in the search (per type).
return await self.__async_parse_artist(artist_obj)
async def async_get_album(self, prov_album_id) -> Album:
- """get full album details by id"""
+ """Get full album details by id."""
params = {"album_id": prov_album_id}
album_obj = await self.__async_get_data("album/get", params)
return await self.__async_parse_album(album_obj)
async def async_get_track(self, prov_track_id) -> Track:
- """get full track details by id"""
+ """Get full track details by id."""
params = {"track_id": prov_track_id}
track_obj = await self.__async_get_data("track/get", params)
return await self.__async_parse_track(track_obj)
async def async_get_playlist(self, prov_playlist_id) -> Playlist:
- """get full playlist details by id"""
+ """Get full playlist details by id."""
params = {"playlist_id": prov_playlist_id}
playlist_obj = await self.__async_get_data("playlist/get", params)
return await self.__async_parse_playlist(playlist_obj)
async def async_get_album_tracks(self, prov_album_id) -> List[Track]:
- """get all album tracks for given album id"""
+ """Get all album tracks for given album id."""
params = {"album_id": prov_album_id}
async for item in self.__async_get_all_items("album/get", params, key="tracks"):
track = await self.__async_parse_track(item)
)
async def async_get_playlist_tracks(self, prov_playlist_id) -> List[Track]:
- """get all playlist tracks for given playlist id"""
+ """Get all playlist tracks for given playlist id."""
params = {"playlist_id": prov_playlist_id, "extra": "tracks"}
endpoint = "playlist/get"
async for item in self.__async_get_all_items(endpoint, params, key="tracks"):
# track version if the original is marked unavailable ?
async def async_get_artist_albums(self, prov_artist_id) -> List[Album]:
- """get a list of albums for the given artist"""
+ """Get a list of albums for the given artist."""
params = {"artist_id": prov_artist_id, "extra": "albums"}
endpoint = "artist/get"
async for item in self.__async_get_all_items(endpoint, params, key="albums"):
yield album
async def async_get_artist_toptracks(self, prov_artist_id) -> List[Track]:
- """get a list of most popular tracks for the given artist"""
+ """Get a list of most popular tracks for the given artist."""
# artist toptracks not supported on Qobuz, so use search instead
# assuming qobuz returns results sorted by popularity
artist = await self.async_get_artist(prov_artist_id)
params = {"query": artist.name, "limit": 25, "type": "tracks"}
searchresult = await self.__async_get_data("catalog/search", params)
for item in searchresult["tracks"]["items"]:
- if "performer" in item and str(item["performer"]["id"]) == str(prov_artist_id):
+ if "performer" in item and str(item["performer"]["id"]) == str(
+ prov_artist_id
+ ):
track = await self.__async_parse_track(item)
if track:
yield track
async def async_library_add(self, prov_item_id, media_type: MediaType):
- """add item to library"""
+ """Add item to library."""
result = None
if media_type == MediaType.Artist:
- result = await self.__async_get_data("favorite/create", {"artist_ids": prov_item_id})
+ result = await self.__async_get_data(
+ "favorite/create", {"artist_ids": prov_item_id}
+ )
elif media_type == MediaType.Album:
- result = await self.__async_get_data("favorite/create", {"album_ids": prov_item_id})
+ result = await self.__async_get_data(
+ "favorite/create", {"album_ids": prov_item_id}
+ )
elif media_type == MediaType.Track:
- result = await self.__async_get_data("favorite/create", {"track_ids": prov_item_id})
+ result = await self.__async_get_data(
+ "favorite/create", {"track_ids": prov_item_id}
+ )
elif media_type == MediaType.Playlist:
result = await self.__async_get_data(
"playlist/subscribe", {"playlist_id": prov_item_id}
return result
async def async_library_remove(self, prov_item_id, media_type: MediaType):
- """remove item from library"""
+ """Remove item from library."""
result = None
if media_type == MediaType.Artist:
- result = await self.__async_get_data("favorite/delete", {"artist_ids": prov_item_id})
+ result = await self.__async_get_data(
+ "favorite/delete", {"artist_ids": prov_item_id}
+ )
elif media_type == MediaType.Album:
- result = await self.__async_get_data("favorite/delete", {"album_ids": prov_item_id})
+ result = await self.__async_get_data(
+ "favorite/delete", {"album_ids": prov_item_id}
+ )
elif media_type == MediaType.Track:
- result = await self.__async_get_data("favorite/delete", {"track_ids": prov_item_id})
+ result = await self.__async_get_data(
+ "favorite/delete", {"track_ids": prov_item_id}
+ )
elif media_type == MediaType.Playlist:
playlist = await self.async_get_playlist(prov_item_id)
if playlist.is_editable:
return result
async def async_add_playlist_tracks(self, prov_playlist_id, prov_track_ids):
- """add track(s) to playlist"""
+ """Add track(s) to playlist."""
params = {
"playlist_id": prov_playlist_id,
"track_ids": ",".join(prov_track_ids),
return await self.__async_get_data("playlist/addTracks", params)
async def async_remove_playlist_tracks(self, prov_playlist_id, prov_track_ids):
- """remove track(s) from playlist"""
+ """Remove track(s) from playlist."""
playlist_track_ids = []
params = {"playlist_id": prov_playlist_id, "extra": "tracks"}
- for track in await self.__async_get_all_items("playlist/get", params, key="tracks"):
+ for track in await self.__async_get_all_items(
+ "playlist/get", params, key="tracks"
+ ):
if track["id"] in prov_track_ids:
playlist_track_ids.append(track["playlist_track_id"])
params = {
}
return await self.__async_get_data("playlist/deleteTracks", params)
- async def async_get_stream_details(self, track_id: str) -> StreamDetails:
+ async def async_get_stream_details(self, item_id: str) -> StreamDetails:
"""Return the content details for the given track when it will be streamed."""
streamdetails = None
for format_id in [27, 7, 6, 5]:
# it seems that simply requesting for highest available quality does not work
# from time to time the api response is empty for this request ?!
- params = {"format_id": format_id, "track_id": track_id, "intent": "stream"}
+ params = {"format_id": format_id, "track_id": item_id, "intent": "stream"}
streamdetails = await self.__async_get_data(
"track/getFileUrl", params, sign_request=True
)
if streamdetails and streamdetails.get("url"):
break
if not streamdetails or not streamdetails.get("url"):
- LOGGER.error("Unable to retrieve stream url for track %s", track_id)
+ LOGGER.error("Unable to retrieve stream url for track %s", item_id)
return None
return StreamDetails(
type=StreamType.URL,
- item_id=str(track_id),
+ item_id=str(item_id),
provider=PROV_ID,
path=streamdetails["url"],
content_type=ContentType(streamdetails["mime_type"].split("/")[1]),
async def async_mass_event(self, msg, msg_details):
"""
- received event from mass
- we use this to report playback start/stop to qobuz
+ Received event from mass.
+
+ We use this to report playback start/stop to qobuz.
"""
if not self.__user_auth_info:
return
await self.__async_get_data("/track/reportStreamingEnd", params)
async def __async_parse_artist(self, artist_obj):
- """parse qobuz artist object to generic layout"""
+ """Parse qobuz artist object to generic layout."""
artist = Artist()
if not artist_obj or not artist_obj.get("id"):
return None
if artist_obj.get("image"):
for key in ["extralarge", "large", "medium", "small"]:
if artist_obj["image"].get(key):
- if not "2a96cbd8b46e442fc41c2b86b821562f" in artist_obj["image"][key]:
+ if (
+ "2a96cbd8b46e442fc41c2b86b821562f"
+ not in artist_obj["image"][key]
+ ):
artist.metadata["image"] = artist_obj["image"][key]
break
if artist_obj.get("biography"):
return artist
async def __async_parse_album(self, album_obj):
- """parse qobuz album object to generic layout"""
+ """Parse qobuz album object to generic layout."""
album = Album()
if (
not album_obj
return album
async def __async_parse_track(self, track_obj):
- """parse qobuz track object to generic layout"""
+ """Parse qobuz track object to generic layout."""
track = Track()
if (
not track_obj
return None
track.item_id = str(track_obj["id"])
track.provider = PROV_ID
- if track_obj.get("performer") and not "Various " in track_obj["performer"]:
+ if track_obj.get("performer") and "Various " not in track_obj["performer"]:
artist = await self.__async_parse_artist(track_obj["performer"])
if artist:
track.artists.append(artist)
if (
track_obj.get("album")
and track_obj["album"].get("artist")
- and not "Various " in track_obj["album"]["artist"]
+ and "Various " not in track_obj["album"]["artist"]
):
artist = await self.__async_parse_artist(track_obj["album"]["artist"])
if artist:
return track
async def __async_parse_playlist(self, playlist_obj):
- """parse qobuz playlist object to generic layout"""
+ """Parse qobuz playlist object to generic layout."""
playlist = Playlist()
if not playlist_obj or not playlist_obj.get("id"):
return None
return playlist
async def __async_auth_token(self):
- """login to qobuz and store the token"""
+ """Login to qobuz and store the token."""
if self.__user_auth_info:
return self.__user_auth_info["user_auth_token"]
params = {
details = await self.__async_get_data("user/login", params)
if details and "user" in details:
self.__user_auth_info = details
- LOGGER.info("Succesfully logged in to Qobuz as %s", details["user"]["display_name"])
+ LOGGER.info(
+ "Succesfully logged in to Qobuz as %s", details["user"]["display_name"]
+ )
return details["user_auth_token"]
async def __async_get_all_items(self, endpoint, params=None, key="tracks"):
- """get all items from a paged list"""
+ """Get all items from a paged list."""
if not params:
params = {}
limit = 50
params["offset"] = offset
result = await self.__async_get_data(endpoint, params=params)
offset += limit
- if not result or not key in result or not "items" in result[key]:
+ if not result or key not in result or "items" not in result[key]:
break
for item in result[key]["items"]:
yield item
break
async def __async_get_data(self, endpoint, params=None, sign_request=False):
- """get data from api"""
+ """Get data from api."""
if not params:
params = {}
url = "http://www.qobuz.com/api.json/0.2/%s" % endpoint
url, headers=headers, params=params, verify_ssl=False
) as response:
result = await response.json()
- if "error" in result or ("status" in result and "error" in result["status"]):
+ if "error" in result or (
+ "status" in result and "error" in result["status"]
+ ):
LOGGER.error("%s - %s", endpoint, result)
return None
return result
async def __async_post_data(self, endpoint, params=None, data=None):
- """post data to api"""
+ """Post data to api."""
if not params:
params = {}
if not data:
url, params=params, json=data, verify_ssl=False
) as response:
result = await response.json()
- if "error" in result or ("status" in result and "error" in result["status"]):
+ if "error" in result or (
+ "status" in result and "error" in result["status"]
+ ):
LOGGER.error("%s - %s", endpoint, result)
return None
return result
async def async_setup(mass):
"""Perform async setup of this Plugin/Provider."""
prov = SonosProvider()
- await mass.async_register_provider(prov)
\ No newline at end of file
+ await mass.async_register_provider(prov)
class SonosProvider(PlayerProvider):
- """Support for Sonos speakers"""
+ """Support for Sonos speakers."""
+
+ # pylint: disable=abstract-method
_discovery_running = False
_tasks = []
return CONFIG_ENTRIES
async def async_on_start(self) -> bool:
- """Called on startup. Handle initialization of the provider."""
+ """Handle initialization of the provider."""
self._tasks.append(self.mass.add_job(self.__async_periodic_discovery()))
async def async_on_stop(self):
- """Called on shutdown. Handle correct close/cleanup of the provider on exit."""
+ """Handle correct close/cleanup of the provider on exit."""
for task in self._tasks:
task.cancel()
async def async_cmd_play_uri(self, player_id: str, uri: str):
"""
Play the specified uri/url on the goven player.
+
:param player_id: player_id of the player to handle the command.
"""
player = self._players.get(player_id)
async def async_cmd_stop(self, player_id: str):
"""
Send STOP command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
player = self._players.get(player_id)
async def async_cmd_play(self, player_id: str):
"""
Send STOP command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
player = self._players.get(player_id)
async def async_cmd_pause(self, player_id: str):
"""
Send PAUSE command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
player = self._players.get(player_id)
async def async_cmd_next(self, player_id: str):
"""
Send NEXT TRACK command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
player = self._players.get(player_id)
async def async_cmd_previous(self, player_id: str):
"""
Send PREVIOUS TRACK command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
player = self._players.get(player_id)
async def async_cmd_power_on(self, player_id: str):
"""
Send POWER ON command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
player = self._players.get(player_id)
async def async_cmd_power_off(self, player_id: str):
"""
Send POWER OFF command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
player = self._players.get(player_id)
async def async_cmd_volume_set(self, player_id: str, volume_level: int):
"""
Send volume level command to given player.
+
:param player_id: player_id of the player to handle the command.
:param volume_level: volume level to set (0..100).
"""
async def async_cmd_volume_mute(self, player_id: str, is_muted=False):
"""
Send volume MUTE command to given player.
+
:param player_id: player_id of the player to handle the command.
:param is_muted: bool with new mute state.
"""
async def async_cmd_queue_play_index(self, player_id: str, index: int):
"""
- Play item at index X on player's queue
+ Play item at index X on player's queue.
+
:param player_id: player_id of the player to handle the command.
:param index: (int) index of the queue item that should start playing
"""
async def async_cmd_queue_load(self, player_id: str, queue_items: List[QueueItem]):
"""
- Load/overwrite given items in the player's queue implementation
+ Load/overwrite given items in the player's queue implementation.
+
:param player_id: player_id of the player to handle the command.
:param queue_items: a list of QueueItems
"""
):
"""
Insert new items at position X into existing queue.
+
If insert_at_index 0 or None, will start playing newly added item(s)
:param player_id: player_id of the player to handle the command.
:param queue_items: a list of QueueItems
player = self._players.get(player_id)
if player:
for pos, item in enumerate(queue_items):
- self.mass.add_job(player.soco.add_uri_to_queue, item.uri, insert_at_index + pos)
+ self.mass.add_job(
+ player.soco.add_uri_to_queue, item.uri, insert_at_index + pos
+ )
else:
LOGGER.warning("Received command for unavailable player: %s", player_id)
- async def async_cmd_queue_append(self, player_id: str, queue_items: List[QueueItem]):
+ async def async_cmd_queue_append(
+ self, player_id: str, queue_items: List[QueueItem]
+ ):
"""
Append new items at the end of the queue.
+
:param player_id: player_id of the player to handle the command.
:param queue_items: a list of QueueItems
"""
async def async_cmd_queue_clear(self, player_id: str):
"""
Clear the player's queue.
+
:param player_id: player_id of the player to handle the command.
"""
player = self._players.get(player_id)
cur_player_ids = [item.player_id for item in self._players.values()]
# remove any disconnected players...
for player in list(self._players.values()):
- if not player.is_group and not player.soco.uid in new_device_ids:
- self.mass.add_job(self.mass.player_manager.async_remove_player(player.player_id))
+ if not player.is_group and player.soco.uid not in new_device_ids:
+ self.mass.add_job(
+ self.mass.player_manager.async_remove_player(player.player_id)
+ )
for sub in player.subscriptions:
sub.unsubscribe()
self._players.pop(player, None)
group_player.is_group_player = True
group_player.name = group.label
group_player.group_childs = [item.uid for item in group.members]
- self.mass.run_task(self.mass.player_manager.async_update_player(group_player))
+ self.mass.run_task(
+ self.mass.player_manager.async_update_player(group_player)
+ )
async def __topology_changed(self, player_id, event=None):
- """
- Received topology changed event
- from one of the sonos players.
- Schedule discovery to work out the changes.
- """
+ """Received topology changed event from one of the sonos players."""
+ # pylint: disable=unused-argument
+ # Schedule discovery to work out the changes.
self.mass.add_job(self.__run_discovery)
async def __async_report_progress(self, player_id: str):
def put(self, item, block=True, timeout=None):
"""Process event."""
+ # pylint: disable=unused-argument
self._callback_handler(self._player_id, item)
import aiohttp
from asyncio_throttle import Throttler
-from music_assistant.app_vars import get_app_var
+from music_assistant.app_vars import get_app_var # noqa
from music_assistant.constants import CONF_PASSWORD, CONF_USERNAME
from music_assistant.models.config_entry import ConfigEntry, ConfigEntryType
from music_assistant.models.media_types import (
CONFIG_ENTRIES = [
ConfigEntry(
- entry_key=CONF_USERNAME, entry_type=ConfigEntryType.STRING, description_key=CONF_USERNAME
+ entry_key=CONF_USERNAME,
+ entry_type=ConfigEntryType.STRING,
+ description_key=CONF_USERNAME,
),
ConfigEntry(
- entry_key=CONF_PASSWORD, entry_type=ConfigEntryType.PASSWORD, description_key=CONF_PASSWORD
+ entry_key=CONF_PASSWORD,
+ entry_type=ConfigEntryType.PASSWORD,
+ description_key=CONF_PASSWORD,
),
]
class SpotifyProvider(MusicProvider):
"""Implementation for the Spotify MusicProvider."""
+ # pylint: disable=abstract-method
+
_http_session = None
__auth_token = None
sp_user = None
]
async def async_on_start(self) -> bool:
- """Called on startup. Handle initialization of the provider based on config."""
+ """Handle initialization of the provider based on config."""
config = self.mass.config.get_provider_config(self.id)
# pylint: disable=attribute-defined-outside-init
self._cur_user = None
return token is not None
async def async_on_stop(self):
- """Called on shutdown. Handle correct close/cleanup of the provider on exit."""
+ """Handle correct close/cleanup of the provider on exit."""
if self._http_session:
await self._http_session.close()
) -> SearchResult:
"""
Perform search on musicprovider.
+
:param search_query: Search query.
:param media_types: A list of media_types to include. All types if None.
:param limit: Number of items to return in the search (per type).
return result
async def async_get_library_artists(self) -> List[Artist]:
- """retrieve library artists from spotify"""
- spotify_artists = await self.__async_get_data("me/following?type=artist&limit=50")
+ """Retrieve library artists from spotify."""
+ spotify_artists = await self.__async_get_data(
+ "me/following?type=artist&limit=50"
+ )
if spotify_artists:
# TODO: use cursor method to retrieve more than 50 artists
for artist_obj in spotify_artists["artists"]["items"]:
yield prov_artist
async def async_get_library_albums(self) -> List[Album]:
- """retrieve library albums from the provider"""
+ """Retrieve library albums from the provider."""
async for item in self.__async_get_all_items("me/albums"):
album = await self.__async_parse_album(item)
if album:
yield album
async def async_get_library_tracks(self) -> List[Track]:
- """retrieve library tracks from the provider"""
+ """Retrieve library tracks from the provider."""
async for item in self.__async_get_all_items("me/tracks"):
track = await self.__async_parse_track(item)
if track:
yield track
async def async_get_library_playlists(self) -> List[Playlist]:
- """retrieve playlists from the provider"""
+ """Retrieve playlists from the provider."""
async for item in self.__async_get_all_items("me/playlists"):
playlist = await self.__async_parse_playlist(item)
if playlist:
yield None # TODO: Return spotify radio
async def async_get_artist(self, prov_artist_id) -> Artist:
- """get full artist details by id"""
+ """Get full artist details by id."""
artist_obj = await self.__async_get_data("artists/%s" % prov_artist_id)
return await self.__async_parse_artist(artist_obj)
async def async_get_album(self, prov_album_id) -> Album:
- """get full album details by id"""
+ """Get full album details by id."""
album_obj = await self.__async_get_data("albums/%s" % prov_album_id)
return await self.__async_parse_album(album_obj)
async def async_get_track(self, prov_track_id) -> Track:
- """get full track details by id"""
+ """Get full track details by id."""
track_obj = await self.__async_get_data("tracks/%s" % prov_track_id)
return await self.__async_parse_track(track_obj)
async def async_get_playlist(self, prov_playlist_id) -> Playlist:
- """get full playlist details by id"""
+ """Get full playlist details by id."""
playlist_obj = await self.__async_get_data(f"playlists/{prov_playlist_id}")
return await self.__async_parse_playlist(playlist_obj)
async def async_get_album_tracks(self, prov_album_id) -> List[Track]:
- """get all album tracks for given album id"""
+ """Get all album tracks for given album id."""
endpoint = f"albums/{prov_album_id}/tracks"
async for track_obj in self.__async_get_all_items(endpoint):
track = await self.__async_parse_track(track_obj)
yield track
async def async_get_playlist_tracks(self, prov_playlist_id) -> List[Track]:
- """get all playlist tracks for given playlist id"""
+ """Get all playlist tracks for given playlist id."""
endpoint = f"playlists/{prov_playlist_id}/tracks"
async for track_obj in self.__async_get_all_items(endpoint):
playlist_track = await self.__async_parse_track(track_obj)
)
async def async_get_artist_albums(self, prov_artist_id) -> List[Album]:
- """get a list of all albums for the given artist"""
+ """Get a list of all albums for the given artist."""
params = {"include_groups": "album,single,compilation"}
endpoint = f"artists/{prov_artist_id}/albums"
async for item in self.__async_get_all_items(endpoint, params):
yield album
async def async_get_artist_toptracks(self, prov_artist_id) -> List[Track]:
- """get a list of 10 most popular tracks for the given artist"""
+ """Get a list of 10 most popular tracks for the given artist."""
artist = await self.async_get_artist(prov_artist_id)
endpoint = f"artists/{prov_artist_id}/top-tracks"
items = await self.__async_get_data(endpoint)
yield track
async def async_library_add(self, prov_item_id, media_type: MediaType):
- """add item to library"""
+ """Add item to library."""
result = False
if media_type == MediaType.Artist:
result = await self.__async_put_data(
return result
async def async_library_remove(self, prov_item_id, media_type: MediaType):
- """remove item from library"""
+ """Remove item from library."""
result = False
if media_type == MediaType.Artist:
result = await self.__async_delete_data(
elif media_type == MediaType.Track:
result = await self.__async_delete_data("me/tracks", {"ids": prov_item_id})
elif media_type == MediaType.Playlist:
- result = await self.__async_delete_data(f"playlists/{prov_item_id}/followers")
+ result = await self.__async_delete_data(
+ f"playlists/{prov_item_id}/followers"
+ )
return result
async def async_add_playlist_tracks(self, prov_playlist_id, prov_track_ids):
- """add track(s) to playlist"""
+ """Add track(s) to playlist."""
track_uris = []
for track_id in prov_track_ids:
track_uris.append("spotify:track:%s" % track_id)
data = {"uris": track_uris}
- return await self.__async_post_data(f"playlists/{prov_playlist_id}/tracks", data=data)
+ return await self.__async_post_data(
+ f"playlists/{prov_playlist_id}/tracks", data=data
+ )
async def async_remove_playlist_tracks(self, prov_playlist_id, prov_track_ids):
- """remove track(s) from playlist"""
+ """Remove track(s) from playlist."""
track_uris = []
for track_id in prov_track_ids:
track_uris.append({"uri": "spotify:track:%s" % track_id})
data = {"tracks": track_uris}
- return await self.__async_delete_data(f"playlists/{prov_playlist_id}/tracks", data=data)
+ return await self.__async_delete_data(
+ f"playlists/{prov_playlist_id}/tracks", data=data
+ )
- async def async_get_stream_details(self, track_id: str) -> StreamDetails:
+ async def async_get_stream_details(self, item_id: str) -> StreamDetails:
"""Return the content details for the given track when it will be streamed."""
# make sure a valid track is requested.
- track = await self.async_get_track(track_id)
+ track = await self.async_get_track(item_id)
if not track:
return None
# make sure that the token is still valid by just requesting it
)
async def __async_parse_artist(self, artist_obj):
- """parse spotify artist object to generic layout"""
+ """Parse spotify artist object to generic layout."""
if not artist_obj:
return None
artist = Artist()
artist.item_id = artist_obj["id"]
artist.provider = self.id
- artist.provider_ids.append(MediaItemProviderId(provider=PROV_ID, item_id=artist_obj["id"]))
+ artist.provider_ids.append(
+ MediaItemProviderId(provider=PROV_ID, item_id=artist_obj["id"])
+ )
artist.name = artist_obj["name"]
if "genres" in artist_obj:
artist.tags = artist_obj["genres"]
if artist_obj.get("images"):
for img in artist_obj["images"]:
img_url = img["url"]
- if not "2a96cbd8b46e442fc41c2b86b821562f" in img_url:
+ if "2a96cbd8b46e442fc41c2b86b821562f" not in img_url:
artist.metadata["image"] = img_url
break
if artist_obj.get("external_urls"):
return artist
async def __async_parse_album(self, album_obj):
- """parse spotify album object to generic layout"""
+ """Parse spotify album object to generic layout."""
if not album_obj:
return None
if "album" in album_obj:
album.metadata["explicit"] = str(album_obj["explicit"]).lower()
album.provider_ids.append(
MediaItemProviderId(
- provider=PROV_ID, item_id=album_obj["id"], quality=TrackQuality.LOSSY_OGG
+ provider=PROV_ID,
+ item_id=album_obj["id"],
+ quality=TrackQuality.LOSSY_OGG,
)
)
return album
async def __async_parse_track(self, track_obj):
- """parse spotify track object to generic layout"""
+ """Parse spotify track object to generic layout."""
if not track_obj:
return None
if "track" in track_obj:
track.metadata["spotify_url"] = track_obj["external_urls"]["spotify"]
track.provider_ids.append(
MediaItemProviderId(
- provider=PROV_ID, item_id=track_obj["id"], quality=TrackQuality.LOSSY_OGG
+ provider=PROV_ID,
+ item_id=track_obj["id"],
+ quality=TrackQuality.LOSSY_OGG,
)
)
return track
async def __async_parse_playlist(self, playlist_obj):
- """parse spotify playlist object to generic layout"""
+ """Parse spotify playlist object to generic layout."""
if not playlist_obj.get("id"):
return None
playlist.name = playlist_obj["name"]
playlist.owner = playlist_obj["owner"]["display_name"]
playlist.is_editable = (
- playlist_obj["owner"]["id"] == self.sp_user["id"] or playlist_obj["collaborative"]
+ playlist_obj["owner"]["id"] == self.sp_user["id"]
+ or playlist_obj["collaborative"]
)
if playlist_obj.get("images"):
playlist.metadata["image"] = playlist_obj["images"][0]["url"]
return playlist
async def async_get_token(self):
- """get auth token on spotify"""
+ """Get auth token on spotify."""
# return existing token if we have one in memory
- if self.__auth_token and (self.__auth_token["expiresAt"] > int(time.time()) + 20):
+ if self.__auth_token and (
+ self.__auth_token["expiresAt"] > int(time.time()) + 20
+ ):
return self.__auth_token
tokeninfo = {}
if not self._username or not self._password:
return tokeninfo
def __get_token(self):
- """get spotify auth token with spotty bin"""
+ """Get spotify auth token with spotty bin."""
# get token with spotty
scopes = [
"user-read-playback-state",
return tokeninfo
async def __async_get_all_items(self, endpoint, params=None, key="items"):
- """get all items from a paged list"""
+ """Get all items from a paged list."""
if not params:
params = {}
limit = 50
params["offset"] = offset
result = await self.__async_get_data(endpoint, params=params)
offset += limit
- if not result or not key in result or not result[key]:
+ if not result or key not in result or not result[key]:
break
for item in result[key]:
yield item
break
async def __async_get_data(self, endpoint, params=None):
- """get data from api"""
+ """Get data from api."""
if not params:
params = {}
url = "https://api.spotify.com/v1/%s" % endpoint
return result
async def __async_delete_data(self, endpoint, params=None, data=None):
- """delete data from api"""
+ """Delete data from api."""
if not params:
params = {}
url = "https://api.spotify.com/v1/%s" % endpoint
return await response.text()
async def __async_put_data(self, endpoint, params=None, data=None):
- """put data on api"""
+ """Put data on api."""
if not params:
params = {}
url = "https://api.spotify.com/v1/%s" % endpoint
return await response.text()
async def __async_post_data(self, endpoint, params=None, data=None):
- """post data on api"""
+ """Post data on api."""
if not params:
params = {}
url = "https://api.spotify.com/v1/%s" % endpoint
@staticmethod
def get_spotty_binary():
- """find the correct spotty binary belonging to the platform"""
+ """Find the correct spotty binary belonging to the platform."""
sp_binary = None
if platform.system() == "Windows":
- sp_binary = os.path.join(os.path.dirname(__file__), "spotty", "windows", "spotty.exe")
+ sp_binary = os.path.join(
+ os.path.dirname(__file__), "spotty", "windows", "spotty.exe"
+ )
elif platform.system() == "Darwin":
# macos binary is x86_64 intel
- sp_binary = os.path.join(os.path.dirname(__file__), "spotty", "darwin", "spotty")
+ sp_binary = os.path.join(
+ os.path.dirname(__file__), "spotty", "darwin", "spotty"
+ )
elif platform.system() == "Linux":
# try to find out the correct architecture by trial and error
architecture = platform.machine()
"""Squeezebox emulated player provider."""
import asyncio
-import decimal
import logging
-import os
-import random
-import socket
-import struct
-import sys
-import time
-from collections import OrderedDict
from typing import List
from music_assistant.constants import CONF_CROSSFADE_DURATION
-from music_assistant.models.config_entry import ConfigEntry, ConfigEntryType
-from music_assistant.models.player import DeviceInfo, Player, PlayerFeature, PlayerState
+from music_assistant.models.config_entry import ConfigEntry
+from music_assistant.models.player import DeviceInfo, PlayerFeature
from music_assistant.models.player_queue import QueueItem
from music_assistant.models.playerprovider import PlayerProvider
-from music_assistant.utils import get_hostname, get_ip, run_periodic, try_parse_int
from .constants import PROV_ID, PROV_NAME
from .discovery import DiscoveryProtocol
class PySqueezeProvider(PlayerProvider):
- """Python implementation of SlimProto server"""
+ """Python implementation of SlimProto server."""
_socket_clients = {}
_tasks = []
return CONFIG_ENTRIES
async def async_on_start(self) -> bool:
- """Called on startup. Handle initialization of the provider."""
+ """Handle initialization of the provider. Called on startup."""
# start slimproto server
self._tasks.append(
- self.mass.add_job(asyncio.start_server(self.__async_client_connected, "0.0.0.0", 3483))
+ self.mass.add_job(
+ asyncio.start_server(self.__async_client_connected, "0.0.0.0", 3483)
+ )
)
# setup discovery
self._tasks.append(self.mass.add_job(self.async_start_discovery()))
async def async_on_stop(self):
- """Called on shutdown. Handle correct close/cleanup of the provider on exit."""
+ """Handle correct close/cleanup of the provider on exit."""
for task in self._tasks:
task.cancel()
for client in self._socket_clients.values():
async def async_cmd_play_uri(self, player_id: str, uri: str):
"""
- Play the specified uri/url on the goven player.
+ Play the specified uri/url on the given player.
+
:param player_id: player_id of the player to handle the command.
"""
socket_client = self._socket_clients.get(player_id)
if socket_client:
- crossfade = self.mass.config.player_settings[player_id][CONF_CROSSFADE_DURATION]
+ crossfade = self.mass.config.player_settings[player_id][
+ CONF_CROSSFADE_DURATION
+ ]
await socket_client.async_cmd_play_uri(
uri, send_flush=True, crossfade_duration=crossfade
)
async def async_cmd_stop(self, player_id: str):
"""
Send STOP command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
socket_client = self._socket_clients.get(player_id)
async def async_cmd_play(self, player_id: str):
"""
- Send STOP command to given player.
+ Send PLAY command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
socket_client = self._socket_clients.get(player_id)
async def async_cmd_pause(self, player_id: str):
"""
Send PAUSE command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
socket_client = self._socket_clients.get(player_id)
async def async_cmd_next(self, player_id: str):
"""
Send NEXT TRACK command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
queue = self.mass.player_manager.get_player_queue(player_id)
async def async_cmd_previous(self, player_id: str):
"""
Send PREVIOUS TRACK command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
queue = self.mass.player_manager.get_player_queue(player_id)
async def async_cmd_power_on(self, player_id: str):
"""
Send POWER ON command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
socket_client = self._socket_clients.get(player_id)
await socket_client.async_cmd_power(True)
# save power and volume state in cache
cache_str = f"squeezebox_player_state_{player_id}"
- await self.mass.cache.async_set(cache_str, (True, socket_client.volume_level))
+ await self.mass.cache.async_set(
+ cache_str, (True, socket_client.volume_level)
+ )
else:
LOGGER.warning("Received command for unavailable player: %s", player_id)
async def async_cmd_power_off(self, player_id: str):
"""
Send POWER OFF command to given player.
+
:param player_id: player_id of the player to handle the command.
"""
socket_client = self._socket_clients.get(player_id)
# store last power state as we need it when the player (re)connects
# save power and volume state in cache
cache_str = f"squeezebox_player_state_{player_id}"
- await self.mass.cache.async_set(cache_str, (False, socket_client.volume_level))
+ await self.mass.cache.async_set(
+ cache_str, (False, socket_client.volume_level)
+ )
else:
LOGGER.warning("Received command for unavailable player: %s", player_id)
async def async_cmd_volume_set(self, player_id: str, volume_level: int):
"""
Send volume level command to given player.
+
:param player_id: player_id of the player to handle the command.
:param volume_level: volume level to set (0..100).
"""
await socket_client.async_cmd_volume_set(volume_level)
# save power and volume state in cache
cache_str = f"squeezebox_player_state_{player_id}"
- await self.mass.cache.async_set(cache_str, (socket_client.powered, volume_level))
+ await self.mass.cache.async_set(
+ cache_str, (socket_client.powered, volume_level)
+ )
else:
LOGGER.warning("Received command for unavailable player: %s", player_id)
async def async_cmd_volume_mute(self, player_id: str, is_muted=False):
"""
Send volume MUTE command to given player.
+
:param player_id: player_id of the player to handle the command.
:param is_muted: bool with new mute state.
"""
async def async_cmd_queue_play_index(self, player_id: str, index: int):
"""
- Play item at index X on player's queue
+ Play item at index X on player's queue.
+
:param player_id: player_id of the player to handle the command.
:param index: (int) index of the queue item that should start playing
"""
async def async_cmd_queue_load(self, player_id: str, queue_items: List[QueueItem]):
"""
- Load/overwrite given items in the player's queue implementation
+ Load/overwrite given items in the player's queue implementation.
+
:param player_id: player_id of the player to handle the command.
:param queue_items: a list of QueueItems
"""
):
"""
Insert new items at position X into existing queue.
+
If insert_at_index 0 or None, will start playing newly added item(s)
:param player_id: player_id of the player to handle the command.
:param queue_items: a list of QueueItems
if queue and insert_at_index == queue.cur_index:
return await self.async_cmd_queue_play_index(player_id, insert_at_index)
- async def async_cmd_queue_append(self, player_id: str, queue_items: List[QueueItem]):
+ async def async_cmd_queue_append(
+ self, player_id: str, queue_items: List[QueueItem]
+ ):
"""
Append new items at the end of the queue.
+
:param player_id: player_id of the player to handle the command.
:param queue_items: a list of QueueItems
"""
pass # automagically handled by built-in queue controller
- async def async_cmd_queue_update(self, player_id: str, queue_items: List[QueueItem]):
+ async def async_cmd_queue_update(
+ self, player_id: str, queue_items: List[QueueItem]
+ ):
"""
Overwrite the existing items in the queue, used for reordering.
+
:param player_id: player_id of the player to handle the command.
:param queue_items: a list of QueueItems
"""
async def async_cmd_queue_clear(self, player_id: str):
"""
Clear the player's queue.
+
:param player_id: player_id of the player to handle the command.
"""
# queue is handled by built-in queue controller but send stop
return await self.async_cmd_stop(player_id)
async def async_start_discovery(self):
+ """Start discovery for players."""
transport, protocol = await self.mass.loop.create_datagram_endpoint(
lambda: DiscoveryProtocol(self.mass.web.http_port),
local_addr=("0.0.0.0", 3483),
if queue:
next_item = queue.next_item
if next_item:
- crossfade = self.mass.config.player_settings[player_id][CONF_CROSSFADE_DURATION]
+ crossfade = self.mass.config.player_settings[player_id][
+ CONF_CROSSFADE_DURATION
+ ]
await self._socket_clients[player_id].async_cmd_play_uri(
next_item.uri, send_flush=False, crossfade_duration=crossfade
)
"""Constants for Squeezebox emulation."""
PROV_ID = "squeezebox"
-PROV_NAME = "Squeezebox emulation"
\ No newline at end of file
+PROV_NAME = "Squeezebox emulation"
"""Squeezebox emulation discovery implementation."""
-from collections import OrderedDict
-import socket
import logging
+import socket
import struct
+from collections import OrderedDict
-from music_assistant.utils import (
- get_hostname,
- get_ip
-)
+from music_assistant.utils import get_hostname, get_ip
LOGGER = logging.getLogger("squeezebox")
-class Datagram():
+
+class Datagram:
"""Description of a discovery datagram."""
+
@classmethod
- def decode(self, data):
+ def decode(cls, data):
+ """Decode a datagram message."""
if data[0] == "e":
return TLVDiscoveryRequestDatagram(data)
elif data[0] == "E":
client = None
def __init__(self, data):
- s = struct.unpack("!cxBB8x6B", data.encode())
- assert s[0] == "d"
- self.device = s[1]
- self.firmware = hex(s[2])
- self.client = ":".join(["%02x" % (x,) for x in s[3:]])
+ """Initialize class."""
+ msg = struct.unpack("!cxBB8x6B", data.encode())
+ assert msg[0] == "d"
+ self.device = msg[1]
+ self.firmware = hex(msg[2])
+ self.client = ":".join(["%02x" % (x,) for x in msg[3:]])
def __repr__(self):
+ """Print the class contents."""
return "<%s device=%r firmware=%r client=%r>" % (
self.__class__.__name__,
self.device,
class DiscoveryResponseDatagram(Datagram):
"""Description of a discovery response datagram."""
+
def __init__(self, hostname, port):
+ """Initialize class."""
+ # pylint: disable=unused-argument
hostname = hostname[:16].encode("UTF-8")
hostname += (16 - len(hostname)) * "\x00"
self.packet = struct.pack("!c16s", "D", hostname).decode()
class TLVDiscoveryRequestDatagram(Datagram):
"""Description of a discovery request datagram."""
+
def __init__(self, data):
+ """Initialize class."""
requestdata = OrderedDict()
assert data[0] == "e"
idx = 1
length = len(data) - 5
while idx <= length:
- typ, l = struct.unpack_from("4sB", data.encode(), idx)
- if l:
- val = data[idx + 5 : idx + 5 + l]
- idx += 5 + l
+ typ, _len = struct.unpack_from("4sB", data.encode(), idx)
+ if _len:
+ val = data[idx + 5 : idx + 5 + _len]
+ idx += 5 + _len
else:
val = None
idx += 5
self.data = requestdata
def __repr__(self):
+ """Pretty print class."""
return "<%s data=%r>" % (self.__class__.__name__, self.data.items())
class TLVDiscoveryResponseDatagram(Datagram):
"""Description of a TLV discovery response datagram."""
+
def __init__(self, responsedata):
+ """Initialize class."""
parts = ["E"] # new discovery format
for typ, value in responsedata.items():
if value is None:
class DiscoveryProtocol:
"""Description of a discovery protocol."""
+
def __init__(self, web_port):
+ """Initialze class."""
self.web_port = web_port
+ self.transport = None
def connection_made(self, transport):
+ """Call on connection."""
self.transport = transport
# Allow receiving multicast broadcasts
sock = self.transport.get_extra_info("socket")
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
def error_received(self, exc):
+ """Call on Error."""
LOGGER.error(exc)
def connection_lost(self, *args, **kwargs):
+ """Call on Connection lost."""
+ # pylint: disable=unused-argument
LOGGER.debug("Connection lost to discovery")
- def build_TLV_response(self, requestdata):
+ def build_tlv_response(self, requestdata):
+ """Build TLV Response message."""
responsedata = OrderedDict()
for typ, value in requestdata.items():
if typ == "NAME":
return responsedata
def datagram_received(self, data, addr):
+ """Datagram received callback."""
+ # pylint: disable=broad-except
try:
data = data.decode()
dgram = Datagram.decode(data)
if isinstance(dgram, ClientDiscoveryDatagram):
- self.sendDiscoveryResponse(addr)
+ self.send_discovery_response(addr)
elif isinstance(dgram, TLVDiscoveryRequestDatagram):
- resonsedata = self.build_TLV_response(dgram.data)
- self.sendTLVDiscoveryResponse(resonsedata, addr)
+ resonsedata = self.build_tlv_response(dgram.data)
+ self.send_tlv_discovery_response(resonsedata, addr)
except Exception as exc:
LOGGER.exception(exc)
- def sendDiscoveryResponse(self, addr):
+ def send_discovery_response(self, addr):
+ """Send discovery response message."""
dgram = DiscoveryResponseDatagram(get_hostname(), 3483)
self.transport.sendto(dgram.packet.encode(), addr)
- def sendTLVDiscoveryResponse(self, resonsedata, addr):
+ def send_tlv_discovery_response(self, resonsedata, addr):
+ """Send TLV discovery response message."""
dgram = TLVDiscoveryResponseDatagram(resonsedata)
self.transport.sendto(dgram.packet.encode(), addr)
class SqueezeSocketClient:
- """Squeezebox socket client"""
+ """Squeezebox socket client."""
def __init__(
self,
@property
def player_id(self) -> str:
- """Return player_id (=mac address) of the player."""
+ """Return player id (=mac address) of the player."""
return self._player_id
@property
@property
def device_address(self) -> str:
- """Return device IP address of the player"""
+ """Return device IP address of the player."""
dev_address = self._writer.get_extra_info("peername")
return dev_address[0] if dev_address else ""
old_gain = self._volume_control.old_gain()
new_gain = self._volume_control.new_gain()
await self.__async_send_frame(
- b"audg", struct.pack("!LLBBLL", old_gain, old_gain, 1, 255, new_gain, new_gain)
+ b"audg",
+ struct.pack("!LLBBLL", old_gain, old_gain, 1, 255, new_gain, new_gain),
)
self._volume_level = volume_level
self._powered = True
enable_crossfade = crossfade_duration > 0
command = b"s"
- autostart = (
- b"3" # we use direct stream for now so let the player do the messy work with buffers
- )
+ # we use direct stream for now so let the player do the messy work with buffers
+ autostart = b"3"
trans_type = b"1" if enable_crossfade else b"0"
formatbyte = b"f" # fixed to flac
uri = "/stream" + uri.split("/stream")[1]
autostart=autostart,
flags=0x00,
formatbyte=formatbyte,
- transType=trans_type,
- transDuration=crossfade_duration,
+ trans_type=trans_type,
+ trans_duration=crossfade_duration,
)
# extract host and port from uri
regex = "(?:http.*://)?(?P<host>[^:/ ]+).?(?P<port>[0-9]*).*"
async def __async_send_heartbeat(self):
"""Send periodic heartbeat message to player."""
timestamp = int(time.time())
- data = self.__pack_stream(b"t", replayGain=timestamp, flags=0)
+ data = self.__pack_stream(b"t", replay_gain=timestamp, flags=0)
await self.__async_send_frame(b"strm", data)
async def __async_send_frame(self, command, data):
pcmargs=(b"?", b"?", b"?", b"?"),
threshold=200,
spdif=b"0",
- transDuration=0,
- transType=b"0",
+ trans_duration=0,
+ trans_type=b"0",
flags=0x40,
- outputThreshold=0,
- replayGain=0,
- serverPort=8095,
- serverIp=0,
+ output_threshold=0,
+ replay_gain=0,
+ server_port=8095,
+ server_ip=0,
):
"""Create stream request message based on given arguments."""
return struct.pack(
*pcmargs,
threshold,
spdif,
- transDuration,
- transType,
+ trans_duration,
+ trans_type,
flags,
- outputThreshold,
+ output_threshold,
0,
- replayGain,
- serverPort,
- serverIp,
+ replay_gain,
+ server_port,
+ server_ip,
)
@callback
@callback
def _process_stat_stmd(self, data):
"""Process incoming stat STMd message (decoder ready)."""
- #pylint: disable=unused-argument
+ # pylint: disable=unused-argument
LOGGER.debug("STMu received - Decoder Ready for next track.")
asyncio.create_task(self._event_callback(Event.EVENT_DECODER_READY, self))
@callback
def _process_stat_stmf(self, data):
"""Process incoming stat STMf message (connection closed)."""
- #pylint: disable=unused-argument
+ # pylint: disable=unused-argument
LOGGER.debug("STMf received - connection closed.")
self._state = State.Stopped
asyncio.create_task(self._event_callback(Event.EVENT_UPDATED, self))
@callback
def _process_stat_stmo(self, data):
- """Process incoming stat STMo message:
- No more decoded (uncompressed) data to play; triggers rebuffering."""
- #pylint: disable=unused-argument
+ """
+ Process incoming stat STMo message.
+
+ No more decoded (uncompressed) data to play; triggers rebuffering.
+ """
+ # pylint: disable=unused-argument
LOGGER.debug("STMo received - output underrun.")
LOGGER.debug("Output Underrun")
@callback
def _process_stat_stmp(self, data):
"""Process incoming stat STMp message: Pause confirmed."""
- #pylint: disable=unused-argument
+ # pylint: disable=unused-argument
LOGGER.debug("STMp received - pause confirmed.")
self._state = State.Paused
asyncio.create_task(self._event_callback(Event.EVENT_UPDATED, self))
@callback
def _process_stat_stmr(self, data):
"""Process incoming stat STMr message: Resume confirmed."""
- #pylint: disable=unused-argument
+ # pylint: disable=unused-argument
LOGGER.debug("STMr received - resume confirmed.")
self._state = State.Playing
asyncio.create_task(self._event_callback(Event.EVENT_UPDATED, self))
@callback
def _process_stat_stms(self, data):
+ # pylint: disable=unused-argument
"""Process incoming stat STMs message: Playback of new track has started."""
LOGGER.debug("STMs received - playback of new track has started.")
- #pylint: disable=unused-argument
self._state = State.Playing
asyncio.create_task(self._event_callback(Event.EVENT_UPDATED, self))
@callback
def _process_stat_stmt(self, data):
- """Process incoming stat STMt message: heartbeat from client"""
+ """Process incoming stat STMt message: heartbeat from client."""
# pylint: disable=unused-variable
timestamp = time.time()
self._last_heartbeat = timestamp
@callback
def _process_stat_stmu(self, data):
"""Process incoming stat STMu message: Buffer underrun: Normal end of playback."""
- #pylint: disable=unused-argument
+ # pylint: disable=unused-argument
LOGGER.debug("STMu received - end of playback.")
self.state = State.Stopped
asyncio.create_task(self._event_callback(Event.EVENT_UPDATED, self))
@callback
def _process_resp(self, data):
"""Process incoming RESP message: Response received at player."""
- #pylint: disable=unused-argument
+ # pylint: disable=unused-argument
# send continue
asyncio.create_task(self.__async_send_frame(b"cont", b"0"))
class PySqueezeVolume(object):
-
- """Represents a sound volume. This is an awful lot more complex than it
- sounds."""
+ """Represents a sound volume. This is an awful lot more complex than it sounds."""
minimum = 0
maximum = 100
# new gain parameters, from the same place
total_volume_range = -50 # dB
- step_point = -1 # Number of steps, up from the bottom, where a 2nd volume ramp kicks in.
- step_fraction = 1 # fraction of totalVolumeRange where alternate volume ramp kicks in.
+ step_point = (
+ -1
+ ) # Number of steps, up from the bottom, where a 2nd volume ramp kicks in.
+ step_fraction = (
+ 1 # fraction of totalVolumeRange where alternate volume ramp kicks in.
+ )
def __init__(self):
+ """Initialize class."""
self.volume = 50
def increment(self):
- """Increment the volume"""
+ """Increment the volume."""
self.volume += self.step
if self.volume > self.maximum:
self.volume = self.maximum
def decrement(self):
- """Decrement the volume"""
+ """Decrement the volume."""
self.volume -= self.step
if self.volume < self.minimum:
self.volume = self.minimum
def old_gain(self):
- """Return the "Old" gain value as required by the squeezebox"""
+ """Return the "Old" gain value as required by the squeezebox."""
return self.old_map[self.volume]
def decibels(self):
"""Return the "new" gain value."""
+ # pylint: disable=invalid-name
step_db = self.total_volume_range * self.step_fraction
max_volume_db = 0 # different on the boom?
return m * (x2 - x1) + y1
def new_gain(self):
- db = self.decibels()
- floatmult = 10 ** (db / 20.0)
+ """Return new gainvalue of the volume control."""
+ decibel = self.decibels()
+ floatmult = 10 ** (decibel / 20.0)
# avoid rounding errors somehow
- if -30 <= db <= 0:
+ if -30 <= decibel <= 0:
return int(floatmult * (1 << 8) + 0.5) * (1 << 8)
- else:
- return int((floatmult * (1 << 16)) + 0.5)
+ return int((floatmult * (1 << 16)) + 0.5)
-"""Tunein musicprovider support for MusicAssistant."""
-import datetime
-import hashlib
+"""Tune-In musicprovider support for MusicAssistant."""
import logging
-import time
from typing import List, Optional
import aiohttp
from asyncio_throttle import Throttler
-from music_assistant.constants import (
- CONF_PASSWORD,
- CONF_USERNAME,
- EVENT_PLAYBACK_STOPPED,
- EVENT_STREAM_STARTED,
-)
+from music_assistant.constants import CONF_PASSWORD, CONF_USERNAME
from music_assistant.models.config_entry import ConfigEntry, ConfigEntryType
from music_assistant.models.media_types import (
- Album,
- AlbumType,
- Artist,
MediaItemProviderId,
MediaType,
- Playlist,
Radio,
SearchResult,
- Track,
TrackQuality,
)
from music_assistant.models.musicprovider import MusicProvider
from music_assistant.models.streamdetails import ContentType, StreamDetails, StreamType
-from music_assistant.utils import parse_title_and_version, try_parse_int
PROV_ID = "tunein"
PROV_NAME = "TuneIn Radio"
CONFIG_ENTRIES = [
ConfigEntry(
- entry_key=CONF_USERNAME, entry_type=ConfigEntryType.STRING, description_key=CONF_USERNAME
+ entry_key=CONF_USERNAME,
+ entry_type=ConfigEntryType.STRING,
+ description_key=CONF_USERNAME,
),
ConfigEntry(
- entry_key=CONF_PASSWORD, entry_type=ConfigEntryType.PASSWORD, description_key=CONF_PASSWORD
+ entry_key=CONF_PASSWORD,
+ entry_type=ConfigEntryType.PASSWORD,
+ description_key=CONF_PASSWORD,
),
]
class TuneInProvider(MusicProvider):
+ """Provider implementation for Tune In."""
+
+ # pylint: disable=abstract-method
_username = None
_password = None
return [MediaType.Radio]
async def async_on_start(self) -> bool:
- """Called on startup. Handle initialization of the provider based on config."""
+ """Handle initialization of the provider based on config."""
# pylint: disable=attribute-defined-outside-init
self._http_session = aiohttp.ClientSession(
loop=self.mass.loop, connector=aiohttp.TCPConnector()
self._throttler = Throttler(rate_limit=1, period=1)
async def async_on_stop(self):
- """Called on shutdown. Handle correct close/cleanup of the provider on exit."""
+ """Handle correct close/cleanup of the provider on exit."""
if self._http_session:
await self._http_session.close()
) -> SearchResult:
"""
Perform search on musicprovider.
+
:param search_query: Search query.
:param media_types: A list of media_types to include. All types if None.
:param limit: Number of items to return in the search (per type).
radio = await self.__async_parse_radio(item)
yield radio
- async def async_get_radio(self, radio_id: str) -> Radio:
+ async def async_get_radio(self, prov_radio_id: str) -> Radio:
"""Get radio station details."""
radio = None
- params = {"c": "composite", "detail": "listing", "id": radio_id}
+ params = {"c": "composite", "detail": "listing", "id": prov_radio_id}
result = await self.__async_get_data("Describe.ashx", params)
if result and result.get("body") and result["body"][0].get("children"):
item = result["body"][0]["children"][0]
return radio
async def __async_parse_radio(self, details: dict) -> Radio:
- """parse Radio object from json obj returned from api"""
+ """Parse Radio object from json obj returned from api."""
radio = Radio(item_id=details["preset_id"], provider=PROV_ID)
if "name" in details:
radio.name = details["name"]
item_id=item_id,
provider=PROV_ID,
path=stream["url"],
- content_type=stream["media_type"],
+ content_type=ContentType(stream["media_type"]),
sample_rate=44100,
bit_depth=16,
details=stream,
)
return None
- async def __async_get_data(self, endpoint, params={}):
- """get data from api"""
+ async def __async_get_data(self, endpoint, params=None):
+ """Get data from api."""
+ if not params:
+ params = {}
url = "https://opml.radiotime.com/%s" % endpoint
params["render"] = "json"
params["formats"] = "ogg,aac,wma,mp3"
params["username"] = self._username
params["partnerId"] = "1"
async with self._throttler:
- async with self._http_session.get(url, params=params, verify_ssl=False) as response:
+ async with self._http_session.get(
+ url, params=params, verify_ssl=False
+ ) as response:
result = await response.json()
if not result or "error" in result:
LOGGER.error(url)
-#!/usr/bin/env python3
-# -*- coding:utf-8 -*-
-
+# pylint: skip-file
+# flake8: noqa
import asyncio
-from collections import OrderedDict
import decimal
import os
import random
import struct
import sys
import time
+from collections import OrderedDict
from typing import List
from music_assistant.constants import CONF_ENABLED
import urllib.request
from datetime import datetime
from enum import Enum
-from types import FunctionType, MethodType
from typing import Any, Callable, TypeVar
import memory_tempfile
def run_periodic(period):
+ """Run a coroutine at interval."""
+
def scheduler(fcn):
async def async_wrapper(*args, **kwargs):
while True:
def get_external_ip():
"""Try to get the external (WAN) IP address."""
+ # pylint: disable=broad-except
try:
return urllib.request.urlopen("https://ident.me").read().decode("utf8")
- except:
+ except Exception:
return None
def filename_from_string(string):
- """create filename from unsafe string"""
+ """Create filename from unsafe string."""
keepcharacters = (" ", ".", "_")
return "".join(c for c in string if c.isalnum() or c in keepcharacters).rstrip()
def run_background_task(corofn, *args, executor=None):
- """run non-async task in background"""
+ """Run non-async task in background."""
return asyncio.get_event_loop().run_in_executor(executor, corofn, *args)
def run_async_background_task(executor, corofn, *args):
- """run async task in background"""
+ """Run async task in background."""
def run_task(corofn, *args):
new_loop = asyncio.new_event_loop()
def get_sort_name(name):
- """create a sort name for an artist/title"""
+ """Create a sort name for an artist/title."""
sort_name = name
for item in ["The ", "De ", "de ", "Les "]:
if name.startswith(item):
def try_parse_int(possible_int):
+ """Try to parse an int."""
try:
return int(possible_int)
except (TypeError, ValueError):
async def async_iter_items(items):
- """fake async iterator for compatability reasons."""
+ """Fake async iterator for compatability reasons."""
if not isinstance(items, list):
yield items
else:
def try_parse_float(possible_float):
+ """Try to parse a float."""
try:
return float(possible_float)
except (TypeError, ValueError):
def try_parse_bool(possible_bool):
+ """Try to parse a bool."""
if isinstance(possible_bool, bool):
return possible_bool
else:
def parse_title_and_version(track_title, track_version=None):
- """try to parse clean track title and version from the title"""
+ """Try to parse clean track title and version from the title."""
title = track_title.lower()
version = ""
for splitter in [" (", " [", " - ", " (", " [", "-"]:
def get_version_substitute(version_str):
- """transform provider version str to universal version type"""
+ """Transform provider version str to universal version type."""
version_str = version_str.lower()
# substitute edit and edition with version
if "edition" in version_str or "edit" in version_str:
return version_str.strip()
-# pylint: disable=broad-except
def get_ip():
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ """Get primary IP-address for this host."""
+ # pylint: disable=broad-except
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
# doesn't even have to be reachable
- s.connect(("10.255.255.255", 1))
- IP = s.getsockname()[0]
+ sock.connect(("10.255.255.255", 1))
+ _ip = sock.getsockname()[0]
except Exception:
- IP = "127.0.0.1"
+ _ip = "127.0.0.1"
finally:
- s.close()
- return IP
+ sock.close()
+ return _ip
def get_ip_pton():
- """Return socket pton for local ip"""
+ """Return socket pton for local ip."""
try:
return socket.inet_pton(socket.AF_INET, get_ip())
except OSError:
def get_folder_size(folderpath):
- """get folder size in gb"""
+ """Return folder size in gb."""
total_size = 0
# pylint: disable=unused-variable
for dirpath, dirnames, filenames in os.walk(folderpath):
- for f in filenames:
- fp = os.path.join(dirpath, f)
- total_size += os.path.getsize(fp)
+ for _file in filenames:
+ _fp = os.path.join(dirpath, _file)
+ total_size += os.path.getsize(_fp)
# pylint: enable=unused-variable
total_size_gb = total_size / float(1 << 30)
return total_size_gb
class EnhancedJSONEncoder(json.JSONEncoder):
+ """Custom JSON decoder."""
+
def default(self, obj):
+ """Return default handler."""
+ # pylint: disable=method-hidden
if dataclasses.is_dataclass(obj):
return dataclasses.asdict(obj)
if isinstance(obj, Enum):
return super().default(obj)
+# pylint: disable=invalid-name
json_serializer = functools.partial(json.dumps, cls=EnhancedJSONEncoder)
-
-# def json_serializer(obj):
-# """Recursively create serializable values for (custom) data types."""
-
-# def get_val(val):
-# if isinstance(val, (int, str, bool, float, tuple)):
-# return val
-# elif isinstance(val, list):
-# new_list = []
-# for item in val:
-# new_list.append(get_val(item))
-# return new_list
-# elif hasattr(val, "to_dict"):
-# return get_val(val.to_dict())
-# elif isinstance(val, dict):
-# new_dict = {}
-# for key, value in val.items():
-# new_dict[key] = get_val(value)
-# return new_dict
-# elif hasattr(val, "__dict__"):
-# new_dict = {}
-# for key, value in val.__dict__.items():
-# new_dict[key] = get_val(value)
-# return new_dict
-
-# return get_val(obj)
+# pylint: enable=invalid-name
def get_compare_string(input_str):
- """get clean lowered string for compare actions"""
+ """Return clean lowered string for compare actions."""
unaccented_string = unidecode.unidecode(input_str)
return re.sub(r"[^a-zA-Z0-9]", "", unaccented_string).lower()
def compare_strings(str1, str2, strict=False):
- """compare strings and return True if we have an (almost) perfect match"""
+ """Compare strings and return True if we have an (almost) perfect match."""
match = str1.lower() == str2.lower()
if not match and not strict:
match = get_compare_string(str1) == get_compare_string(str2)
return match
-# def json_serializer(obj):
-# """json serializer to recursively create serializable values for custom data types"""
-# return json.dumps(json_serializer(obj), skipkeys=True)
-
-
def try_load_json_file(jsonfile):
- """try to load json from file"""
+ """Try to load json from file."""
try:
- with open(jsonfile) as f:
- return json.loads(f.read())
+ with open(jsonfile) as _file:
+ return json.loads(_file.read())
except (FileNotFoundError, json.JSONDecodeError) as exc:
- logging.getLogger().debug("Could not load json from file %s", jsonfile, exc_info=exc)
+ logging.getLogger().debug(
+ "Could not load json from file %s", jsonfile, exc_info=exc
+ )
return None
def create_tempfile():
"""Return a (named) temporary file."""
if platform.system() == "Linux":
- return memory_tempfile.MemoryTempfile(fallback=True).NamedTemporaryFile(buffering=0)
+ return memory_tempfile.MemoryTempfile(fallback=True).NamedTemporaryFile(
+ buffering=0
+ )
return tempfile.NamedTemporaryFile(buffering=0)
-"""The web module handles serving the frontend and the rest/websocket api's"""
+"""The web module handles serving the frontend and the rest/websocket api's."""
import asyncio
-import base64
import datetime
+import functools
import inspect
import ipaddress
-import functools
import json
import logging
import os
import ssl
import aiohttp
+import aiohttp_cors
+import jwt
from aiohttp import web
+from aiohttp_jwt import JWTMiddleware, login_required
from music_assistant.constants import (
CONF_KEY_BASE,
CONF_KEY_PLAYERSETTINGS,
from music_assistant.models.media_types import MediaType, media_type_from_string
from music_assistant.utils import (
EnhancedJSONEncoder,
- get_ip,
- json_serializer,
get_external_ip,
get_hostname,
+ get_ip,
+ json_serializer,
)
-import aiohttp_cors
-import jwt
-from aiohttp_jwt import JWTMiddleware, login_required, check_permissions, match_any
-
-
LOGGER = logging.getLogger("mass")
"""Helper class to add class based routing tables."""
def __repr__(self) -> str:
+ """Print the class contents."""
return "<ClassRouteTableDef count={}>".format(len(self._items))
def route(self, method: str, path: str, **kwargs):
+ """Return the route."""
+ # pylint: disable=missing-function-docstring
def inner(handler):
handler.route_info = (method, path, kwargs)
return handler
return inner
def add_class_routes(self, instance) -> None:
+ """Add class routes."""
+ # pylint: disable=missing-function-docstring
def predicate(member) -> bool:
- return all((inspect.iscoroutinefunction(member), hasattr(member, "route_info")))
+ return all(
+ (inspect.iscoroutinefunction(member), hasattr(member, "route_info"))
+ )
for _, handler in inspect.getmembers(instance, predicate):
method, path, kwargs = handler.route_info
super().route(method, path, **kwargs)(handler)
+# pylint: disable=invalid-name
routes = ClassRouteTableDef()
+# pylint: enable=invalid-name
def require_local_subnet(func):
- """Decorator to specify web method as available locally only."""
+ """Return decorator to specify web method as available locally only."""
@functools.wraps(func)
async def wrapped(*args, **kwargs):
class Web:
- """webserver and json/websocket api"""
+ """Webserver and json/websocket api."""
def __init__(self, mass):
+ """Initialize class."""
self.mass = mass
# load/create/update config
self._local_ip = get_ip()
self.runner = None
enable_ssl = self.config["ssl_certificate"] and self.config["ssl_key"]
- if self.config["ssl_certificate"] and not os.path.isfile(self.config["ssl_certificate"]):
+ if self.config["ssl_certificate"] and not os.path.isfile(
+ self.config["ssl_certificate"]
+ ):
enable_ssl = False
- LOGGER.warning("SSL certificate file not found: %s", self.config["ssl_certificate"])
+ LOGGER.warning(
+ "SSL certificate file not found: %s", self.config["ssl_certificate"]
+ )
if self.config["ssl_key"] and not os.path.isfile(self.config["ssl_key"]):
enable_ssl = False
- LOGGER.warning("SSL certificate key file not found: %s", self.config["ssl_key"])
+ LOGGER.warning(
+ "SSL certificate key file not found: %s", self.config["ssl_key"]
+ )
if not self.config.get("external_url"):
enable_ssl = False
self._enable_ssl = enable_ssl
self._jwt_shared_secret = f"mass_{self._local_ip}_{self.http_port}"
async def async_setup(self):
- """perform async setup"""
+ """Perform async setup."""
routes.add_class_routes(self)
jwt_middleware = JWTMiddleware(
self._jwt_shared_secret, request_property="user", credentials_required=False
LOGGER.info("Started HTTP webserver on port %s", self.http_port)
if self._enable_ssl:
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
- ssl_context.load_cert_chain(self.config["ssl_certificate"], self.config["ssl_key"])
+ ssl_context.load_cert_chain(
+ self.config["ssl_certificate"], self.config["ssl_key"]
+ )
https_site = web.TCPSite(
- self.runner,
- "0.0.0.0",
- self.https_port,
- ssl_context=ssl_context,
+ self.runner, "0.0.0.0", self.https_port, ssl_context=ssl_context
)
await https_site.start()
LOGGER.info(
@routes.post("/login")
async def async_login(self, request):
- """Handler to retrieve a JWT token."""
+ """Handle the retrieval of a JWT token."""
form = await request.json()
username = form.get("username")
password = form.get("password")
@routes.get("/info")
async def async_info(self, request):
+ # pylint: disable=unused-argument
"""Return (discovery) info about this instance."""
return web.json_response(self.discovery_info, dumps=json_serializer)
async def async_index(self, request):
+ """Get the index page, redirect if we do not have a web directory."""
# pylint: disable=unused-argument
webdir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "web/")
if not os.path.isdir(webdir):
- raise web.HTTPFound('https://music-assistant.github.io/app')
+ raise web.HTTPFound("https://music-assistant.github.io/app")
return web.FileResponse(os.path.join(webdir, "index.html"))
@login_required
@login_required
@routes.put("/api/library")
async def async_library_add(self, request):
- """Add item(s) to the library"""
+ """Add item(s) to the library."""
body = await request.json()
media_items = await self.__async_media_items_from_body(body)
result = await self.mass.music_manager.async_library_add(media_items)
@login_required
@routes.delete("/api/library")
async def async_library_remove(self, request):
- """R remove item(s) from the library"""
+ """Remove item(s) from the library."""
body = await request.json()
media_items = await self.__async_media_items_from_body(body)
result = await self.mass.music_manager.async_library_remove(media_items)
@login_required
@routes.get("/api/artists/{item_id}")
async def async_artist(self, request):
- """get full artist details"""
+ """Get full artist details."""
item_id = request.match_info.get("item_id")
provider = request.rel_url.query.get("provider")
lazy = request.rel_url.query.get("lazy", "false") != "false"
if item_id is None or provider is None:
return web.Response(text="invalid item or provider", status=501)
- result = await self.mass.music_manager.async_get_artist(item_id, provider, lazy=lazy)
+ result = await self.mass.music_manager.async_get_artist(
+ item_id, provider, lazy=lazy
+ )
return web.json_response(result, dumps=json_serializer)
@login_required
@routes.get("/api/albums/{item_id}")
async def async_album(self, request):
- """get full album details"""
+ """Get full album details."""
item_id = request.match_info.get("item_id")
provider = request.rel_url.query.get("provider")
lazy = request.rel_url.query.get("lazy", "false") != "false"
if item_id is None or provider is None:
return web.Response(text="invalid item or provider", status=501)
- result = await self.mass.music_manager.async_get_album(item_id, provider, lazy=lazy)
+ result = await self.mass.music_manager.async_get_album(
+ item_id, provider, lazy=lazy
+ )
return web.json_response(result, dumps=json_serializer)
@login_required
@routes.get("/api/tracks/{item_id}")
async def async_track(self, request):
- """get full track details"""
+ """Get full track details."""
item_id = request.match_info.get("item_id")
provider = request.rel_url.query.get("provider")
lazy = request.rel_url.query.get("lazy", "false") != "false"
@login_required
@routes.get("/api/playlists/{item_id}")
async def async_playlist(self, request):
- """get full playlist details"""
+ """Get full playlist details."""
item_id = request.match_info.get("item_id")
provider = request.rel_url.query.get("provider")
if item_id is None or provider is None:
@login_required
@routes.get("/api/radios/{item_id}")
async def async_radio(self, request):
- """get full radio details"""
+ """Get full radio details."""
item_id = request.match_info.get("item_id")
provider = request.rel_url.query.get("provider")
if item_id is None or provider is None:
@routes.get("/api/{media_type}/{media_id}/thumb")
async def async_get_image(self, request):
- """get (resized) thumb image"""
+ """Get (resized) thumb image."""
media_type_str = request.match_info.get("media_type")
media_type = media_type_from_string(media_type_str)
media_id = request.match_info.get("media_id")
@login_required
@routes.get("/api/artists/{item_id}/toptracks")
async def async_artist_toptracks(self, request):
- """get top tracks for given artist"""
+ """Get top tracks for given artist."""
item_id = request.match_info.get("item_id")
provider = request.rel_url.query.get("provider")
if item_id is None or provider is None:
@login_required
@routes.get("/api/artists/{item_id}/albums")
async def async_artist_albums(self, request):
- """get (all) albums for given artist"""
+ """Get (all) albums for given artist."""
item_id = request.match_info.get("item_id")
provider = request.rel_url.query.get("provider")
if item_id is None or provider is None:
@login_required
@routes.get("/api/playlists/{item_id}/tracks")
async def async_playlist_tracks(self, request):
- """get playlist tracks from provider"""
+ """Get playlist tracks from provider."""
item_id = request.match_info.get("item_id")
provider = request.rel_url.query.get("provider")
if item_id is None or provider is None:
item_id = request.match_info.get("item_id")
body = await request.json()
tracks = await self.__async_media_items_from_body(body)
- result = await self.mass.music_manager.async_add_playlist_tracks(item_id, tracks)
+ result = await self.mass.music_manager.async_add_playlist_tracks(
+ item_id, tracks
+ )
return web.json_response(result, dumps=json_serializer)
@login_required
item_id = request.match_info.get("item_id")
body = await request.json()
tracks = await self.__async_media_items_from_body(body)
- result = await self.mass.music_manager.async_remove_playlist_tracks(item_id, tracks)
+ result = await self.mass.music_manager.async_remove_playlist_tracks(
+ item_id, tracks
+ )
return web.json_response(result, dumps=json_serializer)
@login_required
@login_required
@routes.post("/api/players/{player_id}/play_media/{queue_opt}")
async def async_player_play_media(self, request):
- """issue player play_media command"""
+ """Issue player play media command."""
player_id = request.match_info.get("player_id")
player = self.mass.player_manager.get_player(player_id)
if not player:
queue_opt = request.match_info.get("queue_opt", "play")
body = await request.json()
media_items = await self.__async_media_items_from_body(body)
- result = await self.mass.player_manager.async_play_media(player_id, media_items, queue_opt)
+ result = await self.mass.player_manager.async_play_media(
+ player_id, media_items, queue_opt
+ )
return web.json_response(result, dumps=json_serializer)
@login_required
@login_required
@routes.get("/api/players/{player_id}/queue")
async def async_player_queue(self, request):
- """return the player queue details"""
+ """Return the player queue details."""
player_id = request.match_info.get("player_id")
player_queue = self.mass.player_manager.get_player_queue(player_id)
return web.json_response(player_queue, dumps=json_serializer)
@login_required
@routes.put("/api/players/{player_id}/queue/{cmd}")
async def async_player_queue_cmd(self, request):
- """change the player queue details"""
+ """Change the player queue details."""
player_id = request.match_info.get("player_id")
player_queue = self.mass.player_manager.get_player_queue(player_id)
cmd = request.match_info.get("cmd")
@login_required
@routes.get("/api/players/{player_id}")
async def async_player(self, request):
- """get single player."""
+ """Get single player."""
player_id = request.match_info.get("player_id")
player = self.mass.player_manager.get_player(player_id)
if not player:
@routes.get("/api/config")
async def async_get_config(self, request):
# pylint: disable=unused-argument
- """get the config"""
+ """Get the full config."""
conf = {
CONF_KEY_BASE: self.mass.config.base,
CONF_KEY_PROVIDERS: self.mass.config.providers,
@login_required
@routes.get("/api/config/{base}")
async def async_get_config_item(self, request):
- """Get the config."""
+ """Get the config by base type."""
conf_base = request.match_info.get("base")
conf = self.mass.config[conf_base]
return web.json_response(conf, dumps=json_serializer)
@login_required
@routes.put("/api/config/{base}/{key}/{entry_key}")
async def async_put_config(self, request):
- """save (partial) config"""
+ """Save the given config item."""
conf_key = request.match_info.get("key")
conf_base = request.match_info.get("base")
entry_key = request.match_info.get("entry_key")
try:
new_value = await request.json()
except json.decoder.JSONDecodeError:
- new_value = self.mass.config[conf_base][conf_key].get_entry(entry_key).default_value
+ new_value = (
+ self.mass.config[conf_base][conf_key].get_entry(entry_key).default_value
+ )
self.mass.config[conf_base][conf_key][entry_key] = new_value
return web.json_response(True)
async def async_websocket_handler(self, request):
- """websockets handler"""
+ """Handle websockets connection."""
ws_response = None
authenticated = False
remove_callbacks = []
player_id = msg_details.get("player_id")
cmd = msg_details.get("cmd")
cmd_args = msg_details.get("cmd_args")
- player_cmd = getattr(self.mass.player_manager, f"async_cmd_{cmd}", None)
+ player_cmd = getattr(
+ self.mass.player_manager, f"async_cmd_{cmd}", None
+ )
if player_cmd and cmd_args is not None:
result = await player_cmd(player_id, cmd_args)
elif player_cmd:
@require_local_subnet
async def async_json_rpc(self, request):
"""
- implement LMS jsonrpc interface
+ Implement LMS jsonrpc interface.
+
for some compatability with tools that talk to lms
only support for basic commands
"""
return web.Response(text="success")
async def __async_media_items_from_body(self, data):
- """Helper to turn posted body data into media items."""
+ """Convert posted body data into media items."""
if not isinstance(data, list):
data = [data]
media_items = []
return media_items
async def __async_stream_json(self, request, iterator):
- """stream items from async iterator as json object"""
+ """Stream items from async iterator as json object."""
resp = web.StreamResponse(
status=200, reason="OK", headers={"Content-Type": "application/json"}
)
"expires": token_expires,
"scopes": scopes,
}
- return None
\ No newline at end of file
+ return None
W503,
E203,
D202,
- W504
+ W504,
+ E266
[isort]
multi_line_output = 3
PROJECT_URLS = {
"Bug Reports": f"{GITHUB_URL}/issues",
"Website": "https://music-assistant.github.io/",
- "Discord": "https://discord.gg/9xHYFY"
+ "Discord": "https://discord.gg/9xHYFY",
}
PACKAGES = find_packages(exclude=["tests", "tests.*"])
PACKAGE_FILES = []
-for (path, directories, filenames) in os.walk('music_assistant/'):
+for (path, directories, filenames) in os.walk("music_assistant/"):
for filename in filenames:
- PACKAGE_FILES.append(os.path.join('..', path, filename))
+ PACKAGE_FILES.append(os.path.join("..", path, filename))
with open("requirements.txt") as f:
REQUIRES = f.read().splitlines()
install_requires=REQUIRES,
python_requires=f">={mass_const.REQUIRED_PYTHON_VER}",
test_suite="tests",
- entry_points={"console_scripts": ["mass = music_assistant.__main__:main", "musicassistant = music_assistant.__main__:main"]},
- package_data={
- 'music_assistant': PACKAGE_FILES,
+ entry_points={
+ "console_scripts": [
+ "mass = music_assistant.__main__:main",
+ "musicassistant = music_assistant.__main__:main",
+ ]
},
+ package_data={"music_assistant": PACKAGE_FILES},
)