# '.svn': matches 'pkg/.svn' and all of its children
# 'build/*.o': matches 'build/lib.o' but not 'build/sub/lib.o'
# 'build//*.o': matches 'build/lib.o' and 'build/sub/lib.o'
- prefs['ignored_resources'] = ['*.pyc', '*~', '.ropeproject',
- '.hg', '.svn', '_svn', '.git', '.tox']
+ prefs["ignored_resources"] = [
+ "*.pyc",
+ "*~",
+ ".ropeproject",
+ ".hg",
+ ".svn",
+ "_svn",
+ ".git",
+ ".tox",
+ ]
# Specifies which files should be considered python files. It is
# useful when you have scripts inside your project. Only files
# prefs.add('python_path', '~/python/')
# Should rope save object information or not.
- prefs['save_objectdb'] = True
- prefs['compress_objectdb'] = False
+ prefs["save_objectdb"] = True
+ prefs["compress_objectdb"] = False
# If `True`, rope analyzes each module when it is being saved.
- prefs['automatic_soa'] = True
+ prefs["automatic_soa"] = True
# The depth of calls to follow in static object analysis
- prefs['soa_followed_calls'] = 0
+ prefs["soa_followed_calls"] = 0
# If `False` when running modules or unit tests "dynamic object
# analysis" is turned off. This makes them much faster.
- prefs['perform_doa'] = True
+ prefs["perform_doa"] = True
# Rope can check the validity of its object DB when running.
- prefs['validate_objectdb'] = True
+ prefs["validate_objectdb"] = True
# How many undos to hold?
- prefs['max_history_items'] = 32
+ prefs["max_history_items"] = 32
# Shows whether to save history across sessions.
- prefs['save_history'] = True
- prefs['compress_history'] = False
+ prefs["save_history"] = True
+ prefs["compress_history"] = False
# Set the number spaces used for indenting. According to
# :PEP:`8`, it is best to use 4 spaces. Since most of rope's
# unit-tests use 4 spaces it is more reliable, too.
- prefs['indent_size'] = 4
+ prefs["indent_size"] = 4
# Builtin and c-extension modules that are allowed to be imported
# and inspected by rope.
- prefs['extension_modules'] = []
+ prefs["extension_modules"] = []
# Add all standard c-extensions to extension_modules list.
- prefs['import_dynload_stdmods'] = True
+ prefs["import_dynload_stdmods"] = True
# If `True` modules with syntax errors are considered to be empty.
# The default value is `False`; When `False` syntax errors raise
# `rope.base.exceptions.ModuleSyntaxError` exception.
- prefs['ignore_syntax_errors'] = False
+ prefs["ignore_syntax_errors"] = False
# If `True`, rope ignores unresolvable imports. Otherwise, they
# appear in the importing namespace.
- prefs['ignore_bad_imports'] = False
+ prefs["ignore_bad_imports"] = False
# If `True`, rope will insert new module imports as
# `from <package> import <module>` by default.
- prefs['prefer_module_from_imports'] = False
+ prefs["prefer_module_from_imports"] = False
# If `True`, rope will transform a comma list of imports into
# multiple separate import statements when organizing
# imports.
- prefs['split_imports'] = False
+ prefs["split_imports"] = False
# If `True`, rope will remove all top-level import statements and
# reinsert them at the top of the module when making changes.
- prefs['pull_imports_to_top'] = True
+ prefs["pull_imports_to_top"] = True
# If `True`, rope will sort imports alphabetically by module name instead
# of alphabetically by import statement, with from imports after normal
# imports.
- prefs['sort_imports_alphabetically'] = False
+ prefs["sort_imports_alphabetically"] = False
# Location of implementation of
# rope.base.oi.type_hinting.interfaces.ITypeHintingFactory In general
# listed in module rope.base.oi.type_hinting.providers.interfaces
# For example, you can add you own providers for Django Models, or disable
# the search type-hinting in a class hierarchy, etc.
- prefs['type_hinting_factory'] = (
- 'rope.base.oi.type_hinting.factory.default_type_hinting_factory')
+ prefs[
+ "type_hinting_factory"
+ ] = "rope.base.oi.type_hinting.factory.default_type_hinting_factory"
def project_opened(project):
-"""Init file for Music Assistant."""
\ No newline at end of file
+"""Init file for Music Assistant."""
"""Start Music Assistant."""
import argparse
+import asyncio
+import logging
+import os
import platform
import sys
-import os
-import logging
-import asyncio
-from aiorun import run
+from aiorun import run
from music_assistant.mass import MusicAssistant
# -*- coding: utf-8 -*-
"""provides a simple stateless caching system."""
-import os
import functools
-import time
-import pickle
from functools import reduce
-import aiosqlite
+import os
+import pickle
+import time
-from music_assistant.utils import run_periodic, LOGGER
+import aiosqlite
+from music_assistant.utils import LOGGER, run_periodic
class Cache(object):
"""Initialize our caching class."""
self.mass = mass
if not os.path.isdir(mass.datapath):
- raise FileNotFoundError(
- f"data directory {mass.datapath} does not exist!")
+ raise FileNotFoundError(f"data directory {mass.datapath} does not exist!")
self._dbfile = os.path.join(mass.datapath, "cache.db")
async def setup(self):
"""Async initialize of cache module."""
self._db = await aiosqlite.connect(self._dbfile, timeout=30)
self._db.row_factory = aiosqlite.Row
- await self._db.execute("""CREATE TABLE IF NOT EXISTS simplecache(
- id TEXT UNIQUE, expires INTEGER, data TEXT, checksum INTEGER)""")
+ await self._db.execute(
+ """CREATE TABLE IF NOT EXISTS simplecache(
+ id TEXT UNIQUE, expires INTEGER, data TEXT, checksum INTEGER)"""
+ )
await self._db.commit()
self.mass.event_loop.create_task(self.auto_cleanup())
cur_time = int(time.time())
checksum = self._get_checksum(checksum)
sql_query = "SELECT expires, data, checksum FROM simplecache WHERE id = ?"
- async with self._db.execute(sql_query, (cache_key, )) as cursor:
+ async with self._db.execute(sql_query, (cache_key,)) as cursor:
cache_data = await cursor.fetchone()
if not cache_data:
- LOGGER.debug('no cache data for %s', cache_key)
- elif cache_data['expires'] < cur_time:
- LOGGER.debug('cache expired for %s', cache_key)
- elif checksum and cache_data['checksum'] != checksum:
- LOGGER.debug('cache checksum mismatch for %s', cache_key)
- if cache_data and cache_data['expires'] > cur_time:
- if checksum is None or cache_data['checksum'] == checksum:
- LOGGER.debug('return cache data for %s', cache_key)
+ LOGGER.debug("no cache data for %s", cache_key)
+ elif cache_data["expires"] < cur_time:
+ LOGGER.debug("cache expired for %s", cache_key)
+ elif checksum and cache_data["checksum"] != checksum:
+ LOGGER.debug("cache checksum mismatch for %s", cache_key)
+ if cache_data and cache_data["expires"] > cur_time:
+ if checksum is None or cache_data["checksum"] == checksum:
+ LOGGER.debug("return cache data for %s", cache_key)
result = pickle.loads(cache_data[1])
return result
- async def set(self,
- cache_key,
- data,
- checksum="",
- expiration=(86400*30)):
+ async def set(self, cache_key, data, checksum="", expiration=(86400 * 30)):
"""
set data in cache
"""
(id, expires, data, checksum) VALUES (?, ?, ?, ?)"""
await self._db.execute(sql_query, (cache_key, expires, data, checksum))
await self._db.commit()
-
+
@run_periodic(3600)
async def auto_cleanup(self):
""" (scheduled) auto cleanup task """
async with self._db.execute(sql_query) as cursor:
cache_objects = await cursor.fetchall()
for cache_data in cache_objects:
- cache_id = cache_data['id']
+ cache_id = cache_data["id"]
# clean up db cache object only if expired
- if cache_data['expires'] < cur_timestamp:
+ if cache_data["expires"] < cur_timestamp:
sql_query = "DELETE FROM simplecache WHERE id = ?"
- await self._db.execute(sql_query, (cache_id, ))
+ await self._db.execute(sql_query, (cache_id,))
LOGGER.debug("delete from db %s", cache_id)
# compact db
await self._db.commit()
return reduce(lambda x, y: x + y, map(ord, stringinput))
-async def cached_iterator(cache, iter_func, cache_key, expires=(86400*30), checksum=None):
+async def cached_iterator(
+ cache, iter_func, cache_key, expires=(86400 * 30), checksum=None
+):
"""Helper method to store results of an iterator in the cache."""
cache_result = await cache.get(cache_key, checksum)
if cache_result:
cache_result.append(item)
await cache.set(cache_key, cache_result, checksum, expires)
+
async def cached(cache, cache_key, coro_func, *args, **kwargs):
"""Helper method to store results of a coroutine in the cache."""
cache_result = await cache.get(cache_key)
await cache.set(cache_key, result)
return result
+
def use_cache(cache_days=14, cache_checksum=None):
""" decorator that can be used to cache a method's result."""
+
def wrapper(func):
@functools.wraps(func)
async def wrapped(*args, **kwargs):
cache_str,
result,
checksum=cache_checksum,
- expiration=(86400*cache_days),
+ expiration=(86400 * cache_days),
)
return result
+
return wrapped
+
return wrapper
+
def __cache_id_from_args(*args, **kwargs):
- ''' parse arguments to build cache id '''
- cache_str = ''
+ """ parse arguments to build cache id """
+ cache_str = ""
# append args to cache identifier
for item in args[1:]:
if isinstance(item, dict):
import os
import shutil
-from music_assistant.utils import try_load_json_file, json, LOGGER
-from music_assistant.constants import CONF_KEY_BASE, CONF_KEY_PLAYERSETTINGS, \
- CONF_KEY_MUSICPROVIDERS, CONF_KEY_PLAYERPROVIDERS, EVENT_CONFIG_CHANGED
+from music_assistant.constants import (
+ CONF_KEY_BASE,
+ CONF_KEY_MUSICPROVIDERS,
+ CONF_KEY_PLAYERPROVIDERS,
+ CONF_KEY_PLAYERSETTINGS,
+ EVENT_CONFIG_CHANGED,
+)
+from music_assistant.utils import LOGGER, json, try_load_json_file
class MassConfig(dict):
- ''' Class which holds our configuration '''
+ """ Class which holds our configuration """
def __init__(self, mass):
self.loading = False
@property
def base(self):
- ''' return base config '''
+ """ return base config """
return self[CONF_KEY_BASE]
@property
def players(self):
- ''' return player settings '''
+ """ return player settings """
return self[CONF_KEY_PLAYERSETTINGS]
@property
def playerproviders(self):
- ''' return playerprovider settings '''
+ """ return playerprovider settings """
return self[CONF_KEY_PLAYERPROVIDERS]
@property
def musicproviders(self):
- ''' return musicprovider settings '''
+ """ return musicprovider settings """
return self[CONF_KEY_MUSICPROVIDERS]
def create_module_config(self, conf_key, conf_entries, base_key=CONF_KEY_BASE):
- ''' create (or update) module configuration '''
+ """ create (or update) module configuration """
cur_conf = self[base_key].get(conf_key)
new_conf = {}
for key, def_value, desc in conf_entries:
new_conf[key] = def_value
else:
new_conf[key] = cur_conf[key]
- new_conf['__desc__'] = conf_entries
+ new_conf["__desc__"] = conf_entries
self[base_key][conf_key] = new_conf
return self[base_key][conf_key]
def save(self):
- ''' save config to file '''
+ """ save config to file """
if self.loading:
LOGGER.warning("save already running")
return
self.loading = True
# backup existing file
- conf_file = os.path.join(self.mass.datapath, 'config.json')
- conf_file_backup = os.path.join(self.mass.datapath, 'config.json.backup')
+ conf_file = os.path.join(self.mass.datapath, "config.json")
+ conf_file_backup = os.path.join(self.mass.datapath, "config.json.backup")
if os.path.isfile(conf_file):
shutil.move(conf_file, conf_file_backup)
# remove description keys from config
for subkey, subvalue in value.items():
if subkey != "__desc__":
final_conf[key][subkey] = subvalue
- with open(conf_file, 'w') as f:
+ with open(conf_file, "w") as f:
f.write(json.dumps(final_conf, indent=4))
LOGGER.info("Config saved!")
self.loading = False
-
+
def __load(self):
- '''load config from file'''
+ """load config from file"""
self.loading = True
- conf_file = os.path.join(self.mass.datapath, 'config.json')
+ conf_file = os.path.join(self.mass.datapath, "config.json")
data = try_load_json_file(conf_file)
if not data:
# might be a corrupt config file, retry with backup file
- conf_file_backup = os.path.join(self.mass.datapath, 'config.json.backup')
+ conf_file_backup = os.path.join(self.mass.datapath, "config.json.backup")
data = try_load_json_file(conf_file_backup)
if data:
for key, value in data.items():
CONF_TOKEN = "token"
CONF_URL = "url"
-CONF_TYPE_PASSWORD = '<password>'
+CONF_TYPE_PASSWORD = "<password>"
CONF_KEY_BASE = "base"
CONF_KEY_PLAYERSETTINGS = "player_settings"
EVENT_MUSIC_SYNC_STATUS = "music sync status"
EVENT_QUEUE_UPDATED = "queue updated"
EVENT_QUEUE_ITEMS_UPDATED = "queue items updated"
-
import logging
import os
from typing import List
-import aiosqlite
+import aiosqlite
+from music_assistant.models.media_types import (
+ Album,
+ Artist,
+ MediaType,
+ Playlist,
+ Radio,
+ Track,
+)
from music_assistant.utils import LOGGER, get_sort_name, try_parse_int
-from music_assistant.models.media_types import MediaType, Artist, Album, Track, Playlist, Radio
def commit_guard(func):
""" decorator to guard against multiple db writes """
+
async def wrapped(*args, **kwargs):
method_class = args[0]
while method_class.commit_guard_active:
return wrapped
-class Database():
+class Database:
commit_guard_active = False
def __init__(self, mass):
self.mass = mass
if not os.path.isdir(mass.datapath):
- raise FileNotFoundError(
- f"data directory {mass.datapath} does not exist!")
+ raise FileNotFoundError(f"data directory {mass.datapath} does not exist!")
self._dbfile = os.path.join(mass.datapath, "database.db")
self._db = None
- logging.getLogger('aiosqlite').setLevel(logging.INFO)
+ logging.getLogger("aiosqlite").setLevel(logging.INFO)
async def close(self):
- ''' handle shutdown event, close db connection '''
+ """ handle shutdown event, close db connection """
await self._db.close()
LOGGER.info("db connection closed")
async def setup(self):
- ''' init database '''
+ """ init database """
self._db = await aiosqlite.connect(self._dbfile)
self._db.row_factory = aiosqlite.Row
- await self._db.execute('''CREATE TABLE IF NOT EXISTS library_items(
+ await self._db.execute(
+ """CREATE TABLE IF NOT EXISTS library_items(
item_id INTEGER NOT NULL, provider TEXT NOT NULL,
media_type INTEGER NOT NULL, UNIQUE(item_id, provider, media_type)
- );''')
+ );"""
+ )
- await self._db.execute('''CREATE TABLE IF NOT EXISTS artists(
+ await self._db.execute(
+ """CREATE TABLE IF NOT EXISTS artists(
artist_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL,
- sort_name TEXT, musicbrainz_id TEXT NOT NULL UNIQUE);''')
+ sort_name TEXT, musicbrainz_id TEXT NOT NULL UNIQUE);"""
+ )
- await self._db.execute('''CREATE TABLE IF NOT EXISTS albums(
+ await self._db.execute(
+ """CREATE TABLE IF NOT EXISTS albums(
album_id INTEGER PRIMARY KEY AUTOINCREMENT, artist_id INTEGER NOT NULL,
name TEXT NOT NULL, albumtype TEXT, year INTEGER, version TEXT,
UNIQUE(artist_id, name, version, year)
- );''')
-
- await self._db.execute('''CREATE TABLE IF NOT EXISTS labels(
- label_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE);'''
- )
- await self._db.execute('''CREATE TABLE IF NOT EXISTS album_labels(
- album_id INTEGER, label_id INTEGER, UNIQUE(album_id, label_id));'''
- )
-
- await self._db.execute('''CREATE TABLE IF NOT EXISTS tracks(
+ );"""
+ )
+
+ await self._db.execute(
+ """CREATE TABLE IF NOT EXISTS labels(
+ label_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE);"""
+ )
+ await self._db.execute(
+ """CREATE TABLE IF NOT EXISTS album_labels(
+ album_id INTEGER, label_id INTEGER, UNIQUE(album_id, label_id));"""
+ )
+
+ await self._db.execute(
+ """CREATE TABLE IF NOT EXISTS tracks(
track_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL,
album_id INTEGER, version TEXT, duration INTEGER,
UNIQUE(name, version, album_id, duration)
- );''')
- await self._db.execute('''CREATE TABLE IF NOT EXISTS track_artists(
- track_id INTEGER, artist_id INTEGER, UNIQUE(track_id, artist_id));'''
- )
-
- await self._db.execute('''CREATE TABLE IF NOT EXISTS tags(
- tag_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE);'''
- )
- await self._db.execute('''CREATE TABLE IF NOT EXISTS media_tags(
+ );"""
+ )
+ await self._db.execute(
+ """CREATE TABLE IF NOT EXISTS track_artists(
+ track_id INTEGER, artist_id INTEGER, UNIQUE(track_id, artist_id));"""
+ )
+
+ await self._db.execute(
+ """CREATE TABLE IF NOT EXISTS tags(
+ tag_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE);"""
+ )
+ await self._db.execute(
+ """CREATE TABLE IF NOT EXISTS media_tags(
item_id INTEGER, media_type INTEGER, tag_id,
UNIQUE(item_id, media_type, tag_id)
- );''')
+ );"""
+ )
- await self._db.execute('''CREATE TABLE IF NOT EXISTS provider_mappings(
+ await self._db.execute(
+ """CREATE TABLE IF NOT EXISTS provider_mappings(
item_id INTEGER NOT NULL, media_type INTEGER NOT NULL, prov_item_id TEXT NOT NULL,
provider TEXT NOT NULL, quality INTEGER NOT NULL, details TEXT NULL,
UNIQUE(item_id, media_type, prov_item_id, provider, quality)
- );''')
+ );"""
+ )
- await self._db.execute('''CREATE TABLE IF NOT EXISTS metadata(
+ await self._db.execute(
+ """CREATE TABLE IF NOT EXISTS metadata(
item_id INTEGER NOT NULL, media_type INTEGER NOT NULL, key TEXT NOT NULL,
- value TEXT, UNIQUE(item_id, media_type, key));''')
+ value TEXT, UNIQUE(item_id, media_type, key));"""
+ )
- await self._db.execute('''CREATE TABLE IF NOT EXISTS external_ids(
+ await self._db.execute(
+ """CREATE TABLE IF NOT EXISTS external_ids(
item_id INTEGER NOT NULL, media_type INTEGER NOT NULL, key TEXT NOT NULL,
- value TEXT, UNIQUE(item_id, media_type, key, value));''')
+ value TEXT, UNIQUE(item_id, media_type, key, value));"""
+ )
- await self._db.execute('''CREATE TABLE IF NOT EXISTS playlists(
+ await self._db.execute(
+ """CREATE TABLE IF NOT EXISTS playlists(
playlist_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL,
owner TEXT NOT NULL, is_editable BOOLEAN NOT NULL, checksum TEXT NOT NULL,
UNIQUE(name, owner)
- );''')
+ );"""
+ )
- await self._db.execute('''CREATE TABLE IF NOT EXISTS radios(
- radio_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE);'''
- )
+ await self._db.execute(
+ """CREATE TABLE IF NOT EXISTS radios(
+ radio_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE);"""
+ )
- await self._db.execute('''CREATE TABLE IF NOT EXISTS track_loudness(
+ await self._db.execute(
+ """CREATE TABLE IF NOT EXISTS track_loudness(
provider_track_id INTEGER NOT NULL, provider TEXT NOT NULL, loudness REAL,
- UNIQUE(provider_track_id, provider));''')
+ UNIQUE(provider_track_id, provider));"""
+ )
await self._db.commit()
- await self._db.execute('VACUUM;')
+ await self._db.execute("VACUUM;")
- async def get_database_id(self, provider: str, prov_item_id: str,
- media_type: MediaType):
- ''' get the database id for the given prov_id '''
- if provider == 'database':
+ async def get_database_id(
+ self, provider: str, prov_item_id: str, media_type: MediaType
+ ):
+ """ get the database id for the given prov_id """
+ if provider == "database":
return prov_item_id
- sql_query = '''SELECT item_id FROM provider_mappings
- WHERE prov_item_id = ? AND provider = ? AND media_type = ?;'''
+ sql_query = """SELECT item_id FROM provider_mappings
+ WHERE prov_item_id = ? AND provider = ? AND media_type = ?;"""
async with self._db.execute(
- sql_query, (prov_item_id, provider, media_type)) as cursor:
+ sql_query, (prov_item_id, provider, media_type)
+ ) as cursor:
item_id = await cursor.fetchone()
if item_id:
return item_id[0]
return None
async def search(self, searchquery, media_types: List[MediaType]):
- ''' search library for the given searchphrase '''
+ """ search library for the given searchphrase """
result = {"artists": [], "albums": [], "tracks": [], "playlists": []}
searchquery = "%" + searchquery + "%"
if MediaType.Artist in media_types:
sql_query = ' WHERE name LIKE "%s"' % searchquery
- result["artists"] = [
- item async for item in self.artists(sql_query)
- ]
+ result["artists"] = [item async for item in self.artists(sql_query)]
if MediaType.Album in media_types:
sql_query = ' WHERE name LIKE "%s"' % searchquery
result["albums"] = [item async for item in self.albums(sql_query)]
]
return result
- async def library_artists(self, provider=None,
- orderby='name') -> List[Artist]:
- ''' get all library artists, optionally filtered by provider'''
+ async def library_artists(self, provider=None, orderby="name") -> List[Artist]:
+ """ get all library artists, optionally filtered by provider"""
if provider is not None:
- sql_query = 'WHERE artist_id in (SELECT item_id FROM library_items WHERE provider = "%s" AND media_type = %d)' % (
- provider, MediaType.Artist)
+ sql_query = (
+ 'WHERE artist_id in (SELECT item_id FROM library_items WHERE provider = "%s" AND media_type = %d)'
+ % (provider, MediaType.Artist)
+ )
else:
- sql_query = 'WHERE artist_id in (SELECT item_id FROM library_items WHERE media_type = %d)' % MediaType.Artist
+ sql_query = (
+ "WHERE artist_id in (SELECT item_id FROM library_items WHERE media_type = %d)"
+ % MediaType.Artist
+ )
async for item in self.artists(sql_query, orderby=orderby):
yield item
- async def library_albums(self, provider=None,
- orderby='name') -> List[Album]:
- ''' get all library albums, optionally filtered by provider'''
+ async def library_albums(self, provider=None, orderby="name") -> List[Album]:
+ """ get all library albums, optionally filtered by provider"""
if provider is not None:
- sql_query = ' WHERE album_id in (SELECT item_id FROM library_items WHERE provider = "%s" AND media_type = %d)' % (
- provider, MediaType.Album)
+ sql_query = (
+ ' WHERE album_id in (SELECT item_id FROM library_items WHERE provider = "%s" AND media_type = %d)'
+ % (provider, MediaType.Album)
+ )
else:
- sql_query = ' WHERE album_id in (SELECT item_id FROM library_items WHERE media_type = %d)' % MediaType.Album
+ sql_query = (
+ " WHERE album_id in (SELECT item_id FROM library_items WHERE media_type = %d)"
+ % MediaType.Album
+ )
async for item in self.albums(sql_query, orderby=orderby):
yield item
- async def library_tracks(self, provider=None,
- orderby='name') -> List[Track]:
- ''' get all library tracks, optionally filtered by provider'''
+ async def library_tracks(self, provider=None, orderby="name") -> List[Track]:
+ """ get all library tracks, optionally filtered by provider"""
if provider is not None:
- sql_query = '''SELECT * FROM tracks
+ sql_query = """SELECT * FROM tracks
WHERE track_id in (SELECT item_id FROM library_items WHERE provider = "%s"
- AND media_type = %d)''' % (provider, MediaType.Track)
+ AND media_type = %d)""" % (
+ provider,
+ MediaType.Track,
+ )
else:
- sql_query = '''SELECT * FROM tracks
+ sql_query = (
+ """SELECT * FROM tracks
WHERE track_id in
- (SELECT item_id FROM library_items WHERE media_type = %d)''' % MediaType.Track
+ (SELECT item_id FROM library_items WHERE media_type = %d)"""
+ % MediaType.Track
+ )
async for item in self.tracks(sql_query, orderby=orderby):
yield item
- async def library_playlists(self, provider=None,
- orderby='name') -> List[Playlist]:
- ''' fetch all playlist records from table'''
+ async def library_playlists(self, provider=None, orderby="name") -> List[Playlist]:
+ """ fetch all playlist records from table"""
if provider is not None:
- sql_query = '''WHERE playlist_id in
+ sql_query = """WHERE playlist_id in
(SELECT item_id FROM library_items WHERE provider = "%s"
- AND media_type = %d)''' % (provider, MediaType.Playlist)
+ AND media_type = %d)""" % (
+ provider,
+ MediaType.Playlist,
+ )
else:
- sql_query = '''WHERE playlist_id in
- (SELECT item_id FROM library_items WHERE media_type = %d)''' % MediaType.Playlist
+ sql_query = (
+ """WHERE playlist_id in
+ (SELECT item_id FROM library_items WHERE media_type = %d)"""
+ % MediaType.Playlist
+ )
async for item in self.playlists(sql_query, orderby=orderby):
yield item
- async def library_radios(self, provider=None,
- orderby='name') -> List[Radio]:
- ''' fetch all radio records from table'''
+ async def library_radios(self, provider=None, orderby="name") -> List[Radio]:
+ """ fetch all radio records from table"""
if provider is not None:
- sql_query = '''WHERE radio_id in
+ sql_query = """WHERE radio_id in
(SELECT item_id FROM library_items WHERE provider = "%s"
- AND media_type = %d)''' % (provider, MediaType.Radio)
+ AND media_type = %d)""" % (
+ provider,
+ MediaType.Radio,
+ )
else:
- sql_query = '''WHERE radio_id in
- (SELECT item_id FROM library_items WHERE media_type = %d)''' % MediaType.Radio
+ sql_query = (
+ """WHERE radio_id in
+ (SELECT item_id FROM library_items WHERE media_type = %d)"""
+ % MediaType.Radio
+ )
async for item in self.radios(sql_query, orderby=orderby):
yield item
- async def playlists(self, filter_query=None,
- orderby='name') -> List[Playlist]:
- ''' fetch playlist records from table'''
- sql_query = 'SELECT * FROM playlists'
+ async def playlists(self, filter_query=None, orderby="name") -> List[Playlist]:
+ """ fetch playlist records from table"""
+ sql_query = "SELECT * FROM playlists"
if filter_query:
- sql_query += ' ' + filter_query
- sql_query += ' ORDER BY %s' % orderby
+ sql_query += " " + filter_query
+ sql_query += " ORDER BY %s" % orderby
async with self._db.execute(sql_query) as cursor:
db_rows = await cursor.fetchall()
for db_row in db_rows:
playlist = Playlist()
- playlist.item_id = db_row['playlist_id']
- playlist.name = db_row['name']
- playlist.owner = db_row['owner']
- playlist.is_editable = db_row['is_editable']
- playlist.checksum = db_row['checksum']
+ playlist.item_id = db_row["playlist_id"]
+ playlist.name = db_row["name"]
+ playlist.owner = db_row["owner"]
+ playlist.is_editable = db_row["is_editable"]
+ playlist.checksum = db_row["checksum"]
playlist.metadata = await self.__get_metadata(
- playlist.item_id, MediaType.Playlist)
+ playlist.item_id, MediaType.Playlist
+ )
playlist.provider_ids = await self.__get_prov_ids(
- playlist.item_id, MediaType.Playlist)
+ playlist.item_id, MediaType.Playlist
+ )
playlist.in_library = await self.__get_library_providers(
- playlist.item_id, MediaType.Playlist)
+ playlist.item_id, MediaType.Playlist
+ )
yield playlist
async def playlist(self, playlist_id: int) -> Playlist:
- ''' get playlist record by id '''
+ """ get playlist record by id """
playlist_id = try_parse_int(playlist_id)
- async for item in self.playlists('WHERE playlist_id = %s' %
- playlist_id):
+ async for item in self.playlists("WHERE playlist_id = %s" % playlist_id):
return item
return None
- async def radios(self, filter_query=None,
- orderby='name') -> List[Playlist]:
- ''' fetch radio records from table'''
- sql_query = 'SELECT * FROM radios'
+ async def radios(self, filter_query=None, orderby="name") -> List[Playlist]:
+ """ fetch radio records from table"""
+ sql_query = "SELECT * FROM radios"
if filter_query:
- sql_query += ' ' + filter_query
- sql_query += ' ORDER BY %s' % orderby
+ sql_query += " " + filter_query
+ sql_query += " ORDER BY %s" % orderby
async with self._db.execute(sql_query) as cursor:
db_rows = await cursor.fetchall()
for db_row in db_rows:
radio = Radio()
radio.item_id = db_row[0]
radio.name = db_row[1]
- radio.metadata = await self.__get_metadata(radio.item_id,
- MediaType.Radio)
+ radio.metadata = await self.__get_metadata(radio.item_id, MediaType.Radio)
radio.provider_ids = await self.__get_prov_ids(
- radio.item_id, MediaType.Radio)
+ radio.item_id, MediaType.Radio
+ )
radio.in_library = await self.__get_library_providers(
- radio.item_id, MediaType.Radio)
+ radio.item_id, MediaType.Radio
+ )
yield radio
async def radio(self, radio_id: int) -> Playlist:
- ''' get radio record by id '''
+ """ get radio record by id """
radio_id = try_parse_int(radio_id)
- async for item in self.radios('WHERE radio_id = %s' % radio_id):
+ async for item in self.radios("WHERE radio_id = %s" % radio_id):
return item
return None
@commit_guard
async def add_playlist(self, playlist: Playlist):
- ''' add a new playlist record into table'''
- assert (playlist.name)
+ """ add a new playlist record into table"""
+ assert playlist.name
async with self._db.execute(
- 'SELECT (playlist_id) FROM playlists WHERE name=? AND owner=?;',
- (playlist.name, playlist.owner)) as cursor:
+ "SELECT (playlist_id) FROM playlists WHERE name=? AND owner=?;",
+ (playlist.name, playlist.owner),
+ ) as cursor:
result = await cursor.fetchone()
if result:
playlist_id = result[0]
# update existing
- sql_query = 'UPDATE playlists SET is_editable=?, checksum=? WHERE playlist_id=?;'
+ sql_query = "UPDATE playlists SET is_editable=?, checksum=? WHERE playlist_id=?;"
await self._db.execute(
- sql_query,
- (playlist.is_editable, playlist.checksum, playlist_id))
+ sql_query, (playlist.is_editable, playlist.checksum, playlist_id)
+ )
else:
# insert playlist
- sql_query = 'INSERT INTO playlists (name, owner, is_editable, checksum) VALUES(?,?,?,?);'
+ sql_query = "INSERT INTO playlists (name, owner, is_editable, checksum) VALUES(?,?,?,?);"
async with self._db.execute(
- sql_query,
- (playlist.name, playlist.owner, playlist.is_editable,
- playlist.checksum)) as cursor:
+ sql_query,
+ (
+ playlist.name,
+ playlist.owner,
+ playlist.is_editable,
+ playlist.checksum,
+ ),
+ ) as cursor:
last_row_id = cursor.lastrowid
await self._db.commit()
# get id from newly created item
- sql_query = 'SELECT (playlist_id) FROM playlists WHERE ROWID=?'
- async with self._db.execute(sql_query,
- (last_row_id, )) as cursor:
+ sql_query = "SELECT (playlist_id) FROM playlists WHERE ROWID=?"
+ async with self._db.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.__add_prov_ids(playlist_id, MediaType.Playlist,
- playlist.provider_ids)
- await self.__add_metadata(playlist_id, MediaType.Playlist,
- playlist.metadata)
+ await self.__add_prov_ids(
+ playlist_id, MediaType.Playlist, playlist.provider_ids
+ )
+ await self.__add_metadata(
+ playlist_id, MediaType.Playlist, playlist.metadata
+ )
# save
await self._db.commit()
return playlist_id
@commit_guard
async def add_radio(self, radio: Radio):
- ''' add a new radio record into table'''
- assert (radio.name)
+ """ add a new radio record into table"""
+ assert radio.name
async with self._db.execute(
- 'SELECT (radio_id) FROM radios WHERE name=?;',
- (radio.name, )) as cursor:
+ "SELECT (radio_id) FROM radios WHERE name=?;", (radio.name,)
+ ) as cursor:
result = await cursor.fetchone()
if result:
radio_id = result[0]
else:
# insert radio
- sql_query = 'INSERT INTO radios (name) VALUES(?);'
- async with self._db.execute(sql_query,
- (radio.name, )) as cursor:
+ sql_query = "INSERT INTO radios (name) VALUES(?);"
+ async with self._db.execute(sql_query, (radio.name,)) as cursor:
last_row_id = cursor.lastrowid
await self._db.commit()
# get id from newly created item
- sql_query = 'SELECT (radio_id) FROM radios WHERE ROWID=?'
- async with self._db.execute(sql_query,
- (last_row_id, )) as cursor:
+ sql_query = "SELECT (radio_id) FROM radios WHERE ROWID=?"
+ async with self._db.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.__add_prov_ids(radio_id, MediaType.Radio,
- radio.provider_ids)
- await self.__add_metadata(radio_id, MediaType.Radio,
- radio.metadata)
+ await self.__add_prov_ids(radio_id, MediaType.Radio, radio.provider_ids)
+ await self.__add_metadata(radio_id, MediaType.Radio, radio.metadata)
# save
await self._db.commit()
return radio_id
- async def add_to_library(self, item_id: int, media_type: MediaType,
- provider: str):
- ''' add an item to the library (item must already be present in the db!) '''
+ async def add_to_library(self, item_id: int, media_type: MediaType, provider: str):
+ """ add an item to the library (item must already be present in the db!) """
item_id = try_parse_int(item_id)
- sql_query = 'INSERT or REPLACE INTO library_items (item_id, provider, media_type) VALUES(?,?,?);'
+ sql_query = "INSERT or REPLACE INTO library_items (item_id, provider, media_type) VALUES(?,?,?);"
await self._db.execute(sql_query, (item_id, provider, media_type))
await self._db.commit()
- async def remove_from_library(self, item_id: int, media_type: MediaType,
- provider: str):
- ''' remove item from the library '''
+ async def remove_from_library(
+ self, item_id: int, media_type: MediaType, provider: str
+ ):
+ """ remove item from the library """
item_id = try_parse_int(item_id)
- sql_query = 'DELETE FROM library_items WHERE item_id=? AND provider=? AND media_type=?;'
+ sql_query = (
+ "DELETE FROM library_items WHERE item_id=? AND provider=? AND media_type=?;"
+ )
await self._db.execute(sql_query, (item_id, provider, media_type))
if media_type == MediaType.Playlist:
- sql_query = 'DELETE FROM playlists WHERE playlist_id=?;'
- await self._db.execute(sql_query, (item_id, ))
- sql_query = 'DELETE FROM provider_mappings WHERE item_id=? AND media_type=? AND provider=?;'
+ sql_query = "DELETE FROM playlists WHERE playlist_id=?;"
+ await self._db.execute(sql_query, (item_id,))
+ sql_query = "DELETE FROM provider_mappings WHERE item_id=? AND media_type=? AND provider=?;"
await self._db.execute(sql_query, (item_id, media_type, provider))
await self._db.commit()
- async def artists(self, filter_query=None, orderby='name',
- fulldata=False) -> List[Artist]:
- ''' fetch artist records from table'''
- sql_query = 'SELECT * FROM artists'
+ async def artists(
+ self, filter_query=None, orderby="name", fulldata=False
+ ) -> List[Artist]:
+ """ fetch artist records from table"""
+ sql_query = "SELECT * FROM artists"
if filter_query:
- sql_query += ' ' + filter_query
- sql_query += ' ORDER BY %s' % orderby
+ sql_query += " " + filter_query
+ sql_query += " ORDER BY %s" % orderby
async with self._db.execute(sql_query) as cursor:
db_rows = await cursor.fetchall()
for db_row in db_rows:
artist.name = db_row[1]
artist.sort_name = db_row[2]
artist.provider_ids = await self.__get_prov_ids(
- artist.item_id, MediaType.Artist)
+ artist.item_id, MediaType.Artist
+ )
artist.in_library = await self.__get_library_providers(
- artist.item_id, MediaType.Artist)
+ artist.item_id, MediaType.Artist
+ )
if fulldata:
artist.external_ids = await self.__get_external_ids(
- artist.item_id, MediaType.Artist)
+ artist.item_id, MediaType.Artist
+ )
artist.metadata = await self.__get_metadata(
- artist.item_id, MediaType.Artist)
- artist.tags = await self.__get_tags(artist.item_id,
- MediaType.Artist)
+ artist.item_id, MediaType.Artist
+ )
+ artist.tags = await self.__get_tags(artist.item_id, MediaType.Artist)
artist.metadata = await self.__get_metadata(
- artist.item_id, MediaType.Artist)
+ artist.item_id, MediaType.Artist
+ )
yield artist
async def artist(self, artist_id: int, fulldata=True) -> Artist:
- ''' get artist record by id '''
+ """ get artist record by id """
artist_id = try_parse_int(artist_id)
- async for item in self.artists('WHERE artist_id = %s' % artist_id,
- fulldata=fulldata):
+ async for item in self.artists(
+ "WHERE artist_id = %s" % artist_id, fulldata=fulldata
+ ):
return item
return None
@commit_guard
async def add_artist(self, artist: Artist):
- ''' add a new artist record into table'''
+ """ add a new artist record into table"""
artist_id = None
# always prefer to grab existing artist with external_id (=musicbrainz_id)
artist_id = await self.__get_item_by_external_id(artist)
# insert artist
musicbrainz_id = None
for item in artist.external_ids:
- if item.get('musicbrainz'):
- musicbrainz_id = item['musicbrainz']
+ if item.get("musicbrainz"):
+ musicbrainz_id = item["musicbrainz"]
break
- assert (musicbrainz_id) # musicbrainz id is required
+ assert musicbrainz_id # musicbrainz id is required
if not artist.sort_name:
artist.sort_name = get_sort_name(artist.name)
- sql_query = 'INSERT INTO artists (name, sort_name, musicbrainz_id) VALUES(?,?,?);'
+ sql_query = (
+ "INSERT INTO artists (name, sort_name, musicbrainz_id) VALUES(?,?,?);"
+ )
async with self._db.execute(
- sql_query,
- (artist.name, artist.sort_name, musicbrainz_id)) as cursor:
+ sql_query, (artist.name, artist.sort_name, musicbrainz_id)
+ ) as cursor:
last_row_id = cursor.lastrowid
# get id from (newly created) item
async with self._db.execute(
- 'SELECT artist_id FROM artists WHERE ROWID=?;',
- (last_row_id, )) as cursor:
+ "SELECT artist_id FROM artists WHERE ROWID=?;", (last_row_id,)
+ ) as cursor:
artist_id = await cursor.fetchone()
artist_id = artist_id[0]
# always add metadata and tags etc. because we might have received
# additional info or a match from other provider
- await self.__add_prov_ids(artist_id, MediaType.Artist,
- artist.provider_ids)
+ await self.__add_prov_ids(artist_id, MediaType.Artist, artist.provider_ids)
await self.__add_metadata(artist_id, MediaType.Artist, artist.metadata)
await self.__add_tags(artist_id, MediaType.Artist, artist.tags)
- await self.__add_external_ids(artist_id, MediaType.Artist,
- artist.external_ids)
+ await self.__add_external_ids(artist_id, MediaType.Artist, artist.external_ids)
# save
await self._db.commit()
- LOGGER.debug('added artist %s (%s) to database: %s', artist.name,
- artist.provider_ids, artist_id)
+ LOGGER.debug(
+ "added artist %s (%s) to database: %s",
+ artist.name,
+ artist.provider_ids,
+ artist_id,
+ )
return artist_id
- async def albums(self, filter_query=None, orderby='name',
- fulldata=False) -> List[Album]:
- ''' fetch all album records from table'''
- sql_query = 'SELECT * FROM albums'
+ async def albums(
+ self, filter_query=None, orderby="name", fulldata=False
+ ) -> List[Album]:
+ """ fetch all album records from table"""
+ sql_query = "SELECT * FROM albums"
if filter_query:
- sql_query += ' ' + filter_query
- sql_query += ' ORDER BY %s' % orderby
+ sql_query += " " + filter_query
+ sql_query += " ORDER BY %s" % orderby
async with self._db.execute(sql_query) as cursor:
db_rows = await cursor.fetchall()
for db_row in db_rows:
album.year = db_row[4]
album.version = db_row[5]
album.provider_ids = await self.__get_prov_ids(
- album.item_id, MediaType.Album)
+ album.item_id, MediaType.Album
+ )
album.in_library = await self.__get_library_providers(
- album.item_id, MediaType.Album)
+ album.item_id, MediaType.Album
+ )
album.artist = await self.artist(db_row[1], fulldata=fulldata)
if fulldata:
album.external_ids = await self.__get_external_ids(
- album.item_id, MediaType.Album)
+ album.item_id, MediaType.Album
+ )
album.metadata = await self.__get_metadata(
- album.item_id, MediaType.Album)
- album.tags = await self.__get_tags(album.item_id,
- MediaType.Album)
+ album.item_id, MediaType.Album
+ )
+ album.tags = await self.__get_tags(album.item_id, MediaType.Album)
album.labels = await self.__get_album_labels(album.item_id)
yield album
async def album(self, album_id: int, fulldata=True) -> Album:
- ''' get album record by id '''
+ """ get album record by id """
album_id = try_parse_int(album_id)
- async for item in self.albums('WHERE album_id = %s' % album_id,
- fulldata=fulldata):
+ async for item in self.albums(
+ "WHERE album_id = %s" % album_id, fulldata=fulldata
+ ):
return item
return None
@commit_guard
async def add_album(self, album: Album):
- ''' add a new album record into table'''
- assert (album.name and album.artist)
+ """ add a new album record into table"""
+ assert album.name and album.artist
album_id = None
- assert (album.artist.provider == 'database')
+ assert album.artist.provider == "database"
# always try to grab existing album with external_id
album_id = await self.__get_item_by_external_id(album)
# fallback to matching on artist_id, name and version
if not album_id:
# search exact match first
- sql_query = 'SELECT album_id FROM albums WHERE artist_id=? AND name=? AND version=? AND year=? AND albumtype=?'
+ sql_query = "SELECT album_id FROM albums WHERE artist_id=? AND name=? AND version=? AND year=? AND albumtype=?"
async with self._db.execute(
- sql_query,
- (album.artist.item_id, album.name, album.version, album.year,
- album.albumtype)) as cursor:
+ sql_query,
+ (
+ album.artist.item_id,
+ album.name,
+ album.version,
+ album.year,
+ album.albumtype,
+ ),
+ ) as cursor:
album_id = await cursor.fetchone()
if album_id:
- album_id = album_id['album_id']
+ album_id = album_id["album_id"]
# fallback to almost exact match
- sql_query = 'SELECT album_id, year, version, albumtype FROM albums WHERE artist_id=? AND name=?'
+ sql_query = "SELECT album_id, year, version, albumtype FROM albums WHERE artist_id=? AND name=?"
async with self._db.execute(
- sql_query, (album.artist.item_id, album.name)) as cursor:
+ 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 (album.version
- and result['version'] == album.version)):
- album_id = result['album_id']
+ if (not album.version and result["year"] == album.year) or (
+ album.version and result["version"] == album.version
+ ):
+ album_id = result["album_id"]
break
if not album_id:
# insert album
- sql_query = 'INSERT INTO albums (artist_id, name, albumtype, year, version) VALUES(?,?,?,?,?);'
- query_params = (album.artist.item_id, album.name, album.albumtype,
- album.year, album.version)
+ sql_query = "INSERT INTO albums (artist_id, name, albumtype, year, version) VALUES(?,?,?,?,?);"
+ query_params = (
+ album.artist.item_id,
+ album.name,
+ album.albumtype,
+ album.year,
+ album.version,
+ )
async with self._db.execute(sql_query, query_params) as cursor:
last_row_id = cursor.lastrowid
# get id from newly created item
- sql_query = 'SELECT (album_id) FROM albums WHERE ROWID=?'
- async with self._db.execute(sql_query, (last_row_id, )) as cursor:
+ sql_query = "SELECT (album_id) FROM albums WHERE ROWID=?"
+ async with self._db.execute(sql_query, (last_row_id,)) as cursor:
album_id = await cursor.fetchone()
album_id = album_id[0]
# always add metadata and tags etc. because we might have received
# additional info or a match from other provider
- await self.__add_prov_ids(album_id, MediaType.Album,
- album.provider_ids)
+ await self.__add_prov_ids(album_id, MediaType.Album, album.provider_ids)
await self.__add_metadata(album_id, MediaType.Album, album.metadata)
await self.__add_tags(album_id, MediaType.Album, album.tags)
await self.__add_album_labels(album_id, album.labels)
- await self.__add_external_ids(album_id, MediaType.Album,
- album.external_ids)
+ await self.__add_external_ids(album_id, MediaType.Album, album.external_ids)
# save
await self._db.commit()
- LOGGER.debug('added album %s (%s) to database: %s', album.name,
- album.provider_ids, album_id)
+ LOGGER.debug(
+ "added album %s (%s) to database: %s",
+ album.name,
+ album.provider_ids,
+ album_id,
+ )
return album_id
- async def tracks(self, custom_query=None, orderby='name',
- fulldata=False) -> List[Track]:
- ''' fetch all track records from table'''
- sql_query = 'SELECT * FROM tracks'
+ async def tracks(
+ self, custom_query=None, orderby="name", fulldata=False
+ ) -> List[Track]:
+ """ fetch all track records from table"""
+ sql_query = "SELECT * FROM tracks"
if custom_query:
sql_query = custom_query
- sql_query += ' ORDER BY %s' % orderby
+ sql_query += " ORDER BY %s" % orderby
async with self._db.execute(sql_query) as cursor:
for db_row in await cursor.fetchall():
track = Track()
track.item_id = db_row["track_id"]
track.name = db_row["name"]
- track.album = await self.album(db_row["album_id"],
- fulldata=fulldata)
+ track.album = await self.album(db_row["album_id"], fulldata=fulldata)
track.artists = await self.__get_track_artists(
- track.item_id, fulldata=fulldata)
+ track.item_id, fulldata=fulldata
+ )
track.duration = db_row["duration"]
track.version = db_row["version"]
try: # album tracks only
except IndexError:
pass
track.in_library = await self.__get_library_providers(
- track.item_id, MediaType.Track)
+ track.item_id, MediaType.Track
+ )
track.external_ids = await self.__get_external_ids(
- track.item_id, MediaType.Track)
+ track.item_id, MediaType.Track
+ )
track.provider_ids = await self.__get_prov_ids(
- track.item_id, MediaType.Track)
+ track.item_id, MediaType.Track
+ )
if fulldata:
track.metadata = await self.__get_metadata(
- track.item_id, MediaType.Track)
- track.tags = await self.__get_tags(track.item_id,
- MediaType.Track)
+ track.item_id, MediaType.Track
+ )
+ track.tags = await self.__get_tags(track.item_id, MediaType.Track)
yield track
async def track(self, track_id: int, fulldata=True) -> Track:
- ''' get track record by id '''
+ """ get track record by id """
track_id = try_parse_int(track_id)
sql_query = "SELECT * FROM tracks WHERE track_id = %s" % track_id
async for item in self.tracks(sql_query, fulldata=fulldata):
@commit_guard
async def add_track(self, track: Track):
- ''' add a new track record into table'''
- assert (track.name and track.album)
- assert (track.album.provider == 'database')
- assert (track.artists)
+ """ add a new track record into table"""
+ assert track.name and track.album
+ assert track.album.provider == "database"
+ assert track.artists
for artist in track.artists:
- assert (artist.provider == 'database')
+ assert artist.provider == "database"
# always try to grab existing track with external_id
track_id = await self.__get_item_by_external_id(track)
# 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=?'
+ sql_query = "SELECT track_id, duration, version FROM tracks WHERE album_id=? AND name=?"
async with self._db.execute(
- sql_query, (track.album.item_id, track.name)) as cursor:
+ 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 result['version'] == track.version)
- or
- (not track.version
- and abs(result['duration'] - track.duration) < 3)):
- track_id = result['track_id']
+ if (track.version and result["version"] == track.version) or (
+ not track.version
+ and abs(result["duration"] - track.duration) < 3
+ ):
+ track_id = result["track_id"]
break
if not track_id:
# insert track
- assert (track.name and track.album.item_id)
- sql_query = 'INSERT INTO tracks (name, album_id, duration, version) VALUES(?,?,?,?);'
- query_params = (track.name, track.album.item_id, track.duration,
- track.version)
+ assert track.name and track.album.item_id
+ sql_query = "INSERT INTO tracks (name, album_id, duration, version) VALUES(?,?,?,?);"
+ query_params = (
+ track.name,
+ track.album.item_id,
+ track.duration,
+ track.version,
+ )
async with self._db.execute(sql_query, query_params) as cursor:
last_row_id = cursor.lastrowid
# get id from newly created item (the safe way)
async with self._db.execute(
- 'SELECT track_id FROM tracks WHERE ROWID=?',
- (last_row_id, )) as cursor:
+ "SELECT track_id FROM tracks WHERE ROWID=?", (last_row_id,)
+ ) as cursor:
track_id = await cursor.fetchone()
track_id = track_id[0]
# add track artists
for artist in track.artists:
- sql_query = 'INSERT or IGNORE INTO track_artists (track_id, artist_id) VALUES(?,?);'
+ sql_query = (
+ "INSERT or IGNORE INTO track_artists (track_id, artist_id) VALUES(?,?);"
+ )
await self._db.execute(sql_query, (track_id, artist.item_id))
# always add metadata and tags etc. because we might have received
# additional info or a match from other provider
- await self.__add_prov_ids(track_id, MediaType.Track,
- track.provider_ids)
+ await self.__add_prov_ids(track_id, MediaType.Track, track.provider_ids)
await self.__add_metadata(track_id, MediaType.Track, track.metadata)
await self.__add_tags(track_id, MediaType.Track, track.tags)
- await self.__add_external_ids(track_id, MediaType.Track,
- track.external_ids)
+ await self.__add_external_ids(track_id, MediaType.Track, track.external_ids)
# save to db
await self._db.commit()
- LOGGER.debug('added track %s (%s) to database: %s', track.name,
- track.provider_ids, track_id)
+ LOGGER.debug(
+ "added track %s (%s) to database: %s",
+ track.name,
+ track.provider_ids,
+ track_id,
+ )
return track_id
async def update_track(self, track_id, column_key, column_value):
- ''' update column of existing track '''
- sql_query = 'UPDATE tracks SET %s=? WHERE track_id=?;' % column_key
+ """ update column of existing track """
+ sql_query = "UPDATE tracks SET %s=? WHERE track_id=?;" % column_key
await self._db.execute(sql_query, (column_value, track_id))
await self._db.commit()
async def update_playlist(self, playlist_id, column_key, column_value):
- ''' update column of existing playlist '''
- sql_query = 'UPDATE playlists SET %s=? WHERE playlist_id=?;' % column_key
+ """ update column of existing playlist """
+ sql_query = "UPDATE playlists SET %s=? WHERE playlist_id=?;" % column_key
await self._db.execute(sql_query, (column_value, playlist_id))
await self._db.commit()
- async def artist_tracks(self, artist_id, orderby='name') -> List[Track]:
- ''' get all library tracks for the given artist '''
+ async def artist_tracks(self, artist_id, orderby="name") -> List[Track]:
+ """ get all library tracks for the given artist """
artist_id = try_parse_int(artist_id)
- sql_query = 'SELECT * FROM tracks WHERE track_id in (SELECT track_id FROM track_artists WHERE artist_id = %s)' % artist_id
- async for item in self.tracks(sql_query,
- orderby=orderby,
- fulldata=False):
+ sql_query = (
+ "SELECT * FROM tracks WHERE track_id in (SELECT track_id FROM track_artists WHERE artist_id = %s)"
+ % artist_id
+ )
+ async for item in self.tracks(sql_query, orderby=orderby, fulldata=False):
yield item
- async def artist_albums(self, artist_id, orderby='name') -> List[Album]:
- ''' get all library albums for the given artist '''
- sql_query = ' WHERE artist_id = %s' % artist_id
- async for item in self.albums(sql_query,
- orderby=orderby,
- fulldata=False):
+ async def artist_albums(self, artist_id, orderby="name") -> List[Album]:
+ """ get all library albums for the given artist """
+ sql_query = " WHERE artist_id = %s" % artist_id
+ async for item in self.albums(sql_query, orderby=orderby, fulldata=False):
yield item
async def set_track_loudness(self, provider_track_id, provider, loudness):
- ''' set integrated loudness for a track in db '''
- sql_query = 'INSERT or REPLACE INTO track_loudness (provider_track_id, provider, loudness) VALUES(?,?,?);'
- await self._db.execute(sql_query,
- (provider_track_id, provider, loudness))
+ """ set integrated loudness for a track in db """
+ sql_query = "INSERT or REPLACE INTO track_loudness (provider_track_id, provider, loudness) VALUES(?,?,?);"
+ await self._db.execute(sql_query, (provider_track_id, provider, loudness))
await self._db.commit()
async def get_track_loudness(self, provider_track_id, provider):
- ''' get integrated loudness for a track in db '''
- sql_query = 'SELECT loudness FROM track_loudness WHERE provider_track_id = ? AND provider = ?'
- async with self._db.execute(sql_query,
- (provider_track_id, provider)) as cursor:
+ """ get integrated loudness for a track in db """
+ sql_query = "SELECT loudness FROM track_loudness WHERE provider_track_id = ? AND provider = ?"
+ async with self._db.execute(sql_query, (provider_track_id, provider)) as cursor:
result = await cursor.fetchone()
if result:
return result[0]
return None
async def __add_metadata(self, item_id, media_type, metadata):
- ''' add or update metadata'''
+ """ add or update metadata"""
for key, value in metadata.items():
if value:
- sql_query = 'INSERT or REPLACE INTO metadata (item_id, media_type, key, value) VALUES(?,?,?,?);'
- await self._db.execute(sql_query,
- (item_id, media_type, key, value))
+ sql_query = "INSERT or REPLACE INTO metadata (item_id, media_type, key, value) VALUES(?,?,?,?);"
+ await self._db.execute(sql_query, (item_id, media_type, key, value))
async def __get_metadata(self, item_id, media_type, filter_key=None):
- ''' get metadata for media item '''
+ """ 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 self._db.execute(sql_query,
- (item_id, media_type)) as cursor:
+ async with self._db.execute(sql_query, (item_id, media_type)) as cursor:
db_rows = await cursor.fetchall()
for db_row in db_rows:
key = db_row[0]
return metadata
async def __add_tags(self, item_id, media_type, tags):
- ''' add tags to db '''
+ """ add tags to db """
for tag in tags:
- sql_query = 'INSERT or IGNORE INTO tags (name) VALUES(?);'
- async with self._db.execute(sql_query, (tag, )) as cursor:
+ sql_query = "INSERT or IGNORE INTO tags (name) VALUES(?);"
+ async with self._db.execute(sql_query, (tag,)) as cursor:
tag_id = cursor.lastrowid
- sql_query = 'INSERT or IGNORE INTO media_tags (item_id, media_type, tag_id) VALUES(?,?,?);'
+ sql_query = "INSERT or IGNORE INTO media_tags (item_id, media_type, tag_id) VALUES(?,?,?);"
await self._db.execute(sql_query, (item_id, media_type, tag_id))
async def __get_tags(self, item_id, media_type):
- ''' 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 with self._db.execute(sql_query,
- (item_id, media_type)) as cursor:
+ 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 with self._db.execute(sql_query, (item_id, media_type)) as cursor:
db_rows = await cursor.fetchall()
for db_row in db_rows:
tags.append(db_row[0])
return tags
async def __add_album_labels(self, album_id, labels):
- ''' 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 self._db.execute(sql_query, (label, )) as cursor:
+ sql_query = "INSERT or IGNORE INTO labels (name) VALUES(?);"
+ async with self._db.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 self._db.execute(sql_query, (album_id, label_id))
async def __get_album_labels(self, album_id):
- ''' 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 with self._db.execute(sql_query, (album_id, )) as cursor:
+ sql_query = "SELECT name FROM labels INNER JOIN album_labels on labels.label_id = album_labels.label_id WHERE album_id = ?"
+ async with self._db.execute(sql_query, (album_id,)) as cursor:
db_rows = await cursor.fetchall()
for db_row in db_rows:
labels.append(db_row[0])
return labels
- async def __get_track_artists(self, track_id,
- fulldata=False) -> List[Artist]:
- ''' 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.artists(sql_query, fulldata=fulldata)
- ]
+ async def __get_track_artists(self, track_id, fulldata=False) -> List[Artist]:
+ """ 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.artists(sql_query, fulldata=fulldata)]
async def __add_external_ids(self, item_id, media_type, external_ids):
- ''' add or update external_ids'''
+ """ add or update external_ids"""
for external_id in external_ids:
for key, value in external_id.items():
- sql_query = 'INSERT or REPLACE INTO external_ids (item_id, media_type, key, value) VALUES(?,?,?,?);'
- await self._db.execute(sql_query,
- (item_id, media_type, key, value))
+ sql_query = "INSERT or REPLACE INTO external_ids (item_id, media_type, key, value) VALUES(?,?,?,?);"
+ await self._db.execute(sql_query, (item_id, media_type, key, value))
async def __get_external_ids(self, item_id, media_type):
- ''' 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 = ?'
- async with self._db.execute(sql_query,
- (item_id, media_type)) as cursor:
+ sql_query = (
+ "SELECT key, value FROM external_ids WHERE item_id = ? AND media_type = ?"
+ )
+ async with self._db.execute(sql_query, (item_id, media_type)) as cursor:
db_rows = await cursor.fetchall()
for db_row in db_rows:
external_id = {db_row[0]: db_row[1]}
return external_ids
async def __add_prov_ids(self, item_id, media_type, provider_ids):
- ''' add provider ids for media item to db '''
+ """ add provider ids for media item to db """
for prov_mapping in provider_ids:
- prov_id = prov_mapping['provider']
- prov_item_id = prov_mapping['item_id']
- quality = prov_mapping.get('quality', 0)
- details = prov_mapping.get('details', '')
- sql_query = 'INSERT OR REPLACE INTO provider_mappings (item_id, media_type, prov_item_id, provider, quality, details) VALUES(?,?,?,?,?,?);'
+ prov_id = prov_mapping["provider"]
+ prov_item_id = prov_mapping["item_id"]
+ quality = prov_mapping.get("quality", 0)
+ details = prov_mapping.get("details", "")
+ sql_query = "INSERT OR REPLACE INTO provider_mappings (item_id, media_type, prov_item_id, provider, quality, details) VALUES(?,?,?,?,?,?);"
await self._db.execute(
sql_query,
- (item_id, media_type, prov_item_id, prov_id, quality, details))
+ (item_id, media_type, prov_item_id, prov_id, quality, details),
+ )
async def __get_prov_ids(self, item_id, media_type: MediaType):
- ''' get all provider_ids for media item '''
+ """ get all provider_ids for media item """
provider_ids = []
- sql_query = 'SELECT prov_item_id, provider, quality, details \
+ sql_query = "SELECT prov_item_id, provider, quality, details \
FROM provider_mappings \
- WHERE item_id = ? AND media_type = ?'
+ WHERE item_id = ? AND media_type = ?"
- async with self._db.execute(sql_query,
- (item_id, media_type)) as cursor:
+ async with self._db.execute(sql_query, (item_id, media_type)) as cursor:
db_rows = await cursor.fetchall()
for db_row in db_rows:
prov_mapping = {
"provider": db_row[1],
"item_id": db_row[0],
"quality": db_row[2],
- "details": db_row[3]
+ "details": db_row[3],
}
provider_ids.append(prov_mapping)
return provider_ids
async def __get_library_providers(self, item_id, media_type: MediaType):
- ''' get the providers that have this media_item added to the library '''
+ """ 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 = ?'
- async with self._db.execute(sql_query,
- (item_id, media_type)) as cursor:
+ sql_query = (
+ "SELECT provider FROM library_items WHERE item_id = ? AND media_type = ?"
+ )
+ async with self._db.execute(sql_query, (item_id, media_type)) as cursor:
db_rows = await cursor.fetchall()
for db_row in db_rows:
providers.append(db_row[0])
return providers
async def __get_item_by_external_id(self, media_item):
- ''' try to get existing item in db by matching the new item's external id's '''
+ """ try to get existing item in db by matching the new item's external id's """
item_id = None
for external_id in media_item.external_ids:
if item_id:
break
for key, value in external_id.items():
async with self._db.execute(
- 'SELECT (item_id) FROM external_ids WHERE media_type=? AND key=? AND value=?;',
- (media_item.media_type, key, value)) as cursor:
+ "SELECT (item_id) FROM external_ids WHERE media_type=? AND key=? AND value=?;",
+ (media_item.media_type, key, value),
+ ) as cursor:
result = await cursor.fetchone()
if result:
item_id = result[0]
break
if item_id:
break
- return item_id
\ No newline at end of file
+ return item_id
# -*- coding:utf-8 -*-
import asyncio
+import copy
+import datetime
+import hashlib
+import json
import os
-from typing import List
import random
-import aiohttp
import time
-import datetime
-import hashlib
-from asyncio_throttle import Throttler
+from typing import List
+
from aiocometd import Client, ConnectionType, Extension
-import copy
-import slugify as slug
-import json
-from music_assistant.utils import run_periodic, LOGGER, IS_HASSIO, try_parse_int
+import aiohttp
+from asyncio_throttle import Throttler
+from music_assistant.constants import (
+ CONF_ENABLED,
+ CONF_TOKEN,
+ CONF_URL,
+ EVENT_HASS_ENTITY_CHANGED,
+ EVENT_PLAYER_ADDED,
+ EVENT_PLAYER_CHANGED,
+)
from music_assistant.models.media_types import Track
-from music_assistant.constants import CONF_ENABLED, CONF_URL, CONF_TOKEN, EVENT_PLAYER_CHANGED, EVENT_PLAYER_ADDED, EVENT_HASS_ENTITY_CHANGED
+from music_assistant.utils import IS_HASSIO, LOGGER, run_periodic, try_parse_int
+import slugify as slug
-CONF_KEY = 'homeassistant'
+CONF_KEY = "homeassistant"
CONF_PUBLISH_PLAYERS = "publish_players"
### auto detect hassio for auto config ####
if IS_HASSIO:
CONFIG_ENTRIES = [
(CONF_ENABLED, False, CONF_ENABLED),
- (CONF_PUBLISH_PLAYERS, True, 'hass_publish')]
+ (CONF_PUBLISH_PLAYERS, True, "hass_publish"),
+ ]
else:
CONFIG_ENTRIES = [
(CONF_ENABLED, False, CONF_ENABLED),
- (CONF_URL, 'localhost', 'hass_url'),
- (CONF_TOKEN, '<password>', 'hass_token'),
- (CONF_PUBLISH_PLAYERS, True, 'hass_publish')]
-
+ (CONF_URL, "localhost", "hass_url"),
+ (CONF_TOKEN, "<password>", "hass_token"),
+ (CONF_PUBLISH_PLAYERS, True, "hass_publish"),
+ ]
+
-class HomeAssistant():
- '''
+class HomeAssistant:
+ """
Homeassistant integration
allows publishing of our players to hass
allows using hass entities (like switches, media_players or gui inputs) to be triggered
- '''
+ """
def __init__(self, mass):
self.mass = mass
# load/create/update config
config = self.mass.config.create_module_config(CONF_KEY, CONFIG_ENTRIES)
self.enabled = config[CONF_ENABLED]
- if (self.enabled and not IS_HASSIO and not
- (config[CONF_URL] or config[CONF_TOKEN])):
+ if (
+ self.enabled
+ and not IS_HASSIO
+ and not (config[CONF_URL] or config[CONF_TOKEN])
+ ):
LOGGER.warning("Invalid configuration for Home Assistant")
self.enabled = False
if IS_HASSIO:
- self._token = os.environ['HASSIO_TOKEN']
+ self._token = os.environ["HASSIO_TOKEN"]
self._use_ssl = False
- self._host = 'hassio/homeassistant'
+ self._host = "hassio/homeassistant"
else:
self._token = config[CONF_TOKEN]
url = config[CONF_URL]
- if url.startswith('https://'):
+ if url.startswith("https://"):
self._use_ssl = True
- self._host = url.replace('https://','').split('/')[0]
+ self._host = url.replace("https://", "").split("/")[0]
else:
self._use_ssl = False
- self._host = url.replace('http://','').split('/')[0]
+ self._host = url.replace("http://", "").split("/")[0]
if self.enabled:
- LOGGER.info('Homeassistant integration is enabled')
+ LOGGER.info("Homeassistant integration is enabled")
async def setup(self):
- ''' perform async setup '''
+ """ perform async setup """
if not self.enabled:
return
self.http_session = aiohttp.ClientSession(
- loop=self.mass.event_loop, connector=aiohttp.TCPConnector())
+ loop=self.mass.event_loop, connector=aiohttp.TCPConnector()
+ )
self.mass.event_loop.create_task(self.__hass_websocket())
await self.mass.add_event_listener(self.mass_event, EVENT_PLAYER_CHANGED)
await self.mass.add_event_listener(self.mass_event, EVENT_PLAYER_ADDED)
self.mass.event_loop.create_task(self.__get_sources())
- async def get_state_async(self, entity_id, attribute='state'):
- ''' get state of a hass entity (async)'''
+ async def get_state_async(self, entity_id, attribute="state"):
+ """ get state of a hass entity (async)"""
state = self.get_state(entity_id, attribute)
if not state:
await self.__request_state(entity_id)
state = self.get_state(entity_id, attribute)
return state
- def get_state(self, entity_id, attribute='state'):
- ''' get state of a hass entity'''
+ def get_state(self, entity_id, attribute="state"):
+ """ get state of a hass entity"""
state_obj = self._tracked_entities.get(entity_id)
if state_obj:
- if attribute == 'state':
- return state_obj['state']
+ if attribute == "state":
+ return state_obj["state"]
elif attribute:
- return state_obj['attributes'].get(attribute)
+ return state_obj["attributes"].get(attribute)
else:
return state_obj
else:
return None
async def __request_state(self, entity_id):
- ''' get state of a hass entity'''
- state_obj = await self.__get_data('states/%s' % entity_id)
- if 'state' in state_obj:
+ """ get state of a hass entity"""
+ state_obj = await self.__get_data("states/%s" % entity_id)
+ if "state" in state_obj:
self._tracked_entities[entity_id] = state_obj
await self.mass.signal_event(EVENT_HASS_ENTITY_CHANGED, state_obj)
-
+
async def mass_event(self, msg, msg_details):
- ''' received event from mass '''
+ """ received event from mass """
if msg in [EVENT_PLAYER_CHANGED, EVENT_PLAYER_ADDED]:
await self.publish_player(msg_details)
async def hass_event(self, event_type, event_data):
- ''' received event from hass '''
- if event_type == 'state_changed':
- if event_data['entity_id'] in self._tracked_entities:
- self._tracked_entities[event_data['entity_id']] = event_data['new_state']
+ """ received event from hass """
+ if event_type == "state_changed":
+ if event_data["entity_id"] in self._tracked_entities:
+ self._tracked_entities[event_data["entity_id"]] = event_data[
+ "new_state"
+ ]
self.mass.event_loop.create_task(
- self.mass.signal_event(EVENT_HASS_ENTITY_CHANGED, event_data))
- elif event_type == 'call_service' and event_data['domain'] == 'media_player':
- await self.__handle_player_command(event_data['service'], event_data['service_data'])
+ self.mass.signal_event(EVENT_HASS_ENTITY_CHANGED, event_data)
+ )
+ elif event_type == "call_service" and event_data["domain"] == "media_player":
+ await self.__handle_player_command(
+ event_data["service"], event_data["service_data"]
+ )
async def __handle_player_command(self, service, service_data):
- ''' handle forwarded service call for one of our players '''
- if isinstance(service_data['entity_id'], list):
+ """ handle forwarded service call for one of our players """
+ if isinstance(service_data["entity_id"], list):
# can be a list of entity ids if action fired on multiple items
- entity_ids = service_data['entity_id']
+ entity_ids = service_data["entity_id"]
else:
- entity_ids = [service_data['entity_id']]
+ entity_ids = [service_data["entity_id"]]
for entity_id in entity_ids:
if entity_id in self._published_players:
# call is for one of our players so handle it
player = await self.mass.players.get_player(player_id)
if not player:
return
- if service == 'turn_on':
+ if service == "turn_on":
await player.power_on()
- elif service == 'turn_off':
+ elif service == "turn_off":
await player.power_off()
- elif service == 'toggle':
+ elif service == "toggle":
await player.power_toggle()
- elif service == 'volume_mute':
- await player.volume_mute(service_data['is_volume_muted'])
- elif service == 'volume_up':
+ elif service == "volume_mute":
+ await player.volume_mute(service_data["is_volume_muted"])
+ elif service == "volume_up":
await player.volume_up()
- elif service == 'volume_down':
+ elif service == "volume_down":
await player.volume_down()
- elif service == 'volume_set':
- volume_level = service_data['volume_level']*100
+ elif service == "volume_set":
+ volume_level = service_data["volume_level"] * 100
await player.volume_set(volume_level)
- elif service == 'media_play':
+ elif service == "media_play":
await player.play()
- elif service == 'media_pause':
+ elif service == "media_pause":
await player.pause()
- elif service == 'media_stop':
+ elif service == "media_stop":
await player.stop()
- elif service == 'media_next_track':
+ elif service == "media_next_track":
await player.next()
- elif service == 'media_play_pause':
+ elif service == "media_play_pause":
await player.play_pause()
- elif service == 'play_media':
+ elif service == "play_media":
return await self.__handle_play_media(player_id, service_data)
async def __handle_play_media(self, player_id, service_data):
- ''' handle play_media request from homeassistant'''
- media_content_type = service_data['media_content_type'].lower()
- media_content_id = service_data['media_content_id']
- queue_opt = 'add' if service_data.get('enqueue') else 'play'
- if media_content_type == 'playlist' and not '://' in media_content_id:
+ """ handle play_media request from homeassistant"""
+ media_content_type = service_data["media_content_type"].lower()
+ media_content_id = service_data["media_content_id"]
+ queue_opt = "add" if service_data.get("enqueue") else "play"
+ if media_content_type == "playlist" and not "://" in media_content_id:
media_items = []
- for playlist_str in media_content_id.split(','):
+ for playlist_str in media_content_id.split(","):
playlist_str = playlist_str.strip()
playlist = await self.mass.music.playlist_by_name(playlist_str)
if playlist:
media_items.append(playlist)
return await self.mass.players.play_media(player_id, media_items, queue_opt)
- elif media_content_type == 'playlist' and 'spotify://playlist' in media_content_id:
+ elif (
+ media_content_type == "playlist"
+ and "spotify://playlist" in media_content_id
+ ):
# TODO: handle parsing of other uri's here
- playlist = self.mass.music.providers['spotify'].playlist(media_content_id.split(':')[-1])
+ playlist = self.mass.music.providers["spotify"].playlist(
+ media_content_id.split(":")[-1]
+ )
return await self.mass.players.play_media(player_id, playlist, queue_opt)
- elif media_content_id.startswith('http'):
+ elif media_content_id.startswith("http"):
track = Track()
track.uri = media_content_id
- track.provider = 'http'
+ track.provider = "http"
return await self.mass.players.play_media(player_id, track, queue_opt)
-
+
async def publish_player(self, player_info):
- ''' publish player details to hass'''
- if not self.mass.config['base']['homeassistant']['publish_players']:
+ """ publish player details to hass"""
+ if not self.mass.config["base"]["homeassistant"]["publish_players"]:
return False
if not player_info["name"]:
return
# TODO: throttle updates to home assistant ?
player_id = player_info["player_id"]
- entity_id = 'media_player.mass_' + slug.slugify(player_info["name"], separator='_').lower()
+ entity_id = (
+ "media_player.mass_"
+ + slug.slugify(player_info["name"], separator="_").lower()
+ )
state = player_info["state"]
state_attributes = {
- "supported_features": 65471,
- "friendly_name": player_info["name"],
- "source_list": self._sources,
- "source": 'unknown',
- "volume_level": player_info["volume_level"]/100,
- "is_volume_muted": player_info["muted"],
- "media_position_updated_at": player_info["media_position_updated_at"],
- "media_duration": None,
- "media_position": player_info["cur_time"],
- "media_title": None,
- "media_artist": None,
- "media_album_name": None,
- "entity_picture": None
- }
+ "supported_features": 65471,
+ "friendly_name": player_info["name"],
+ "source_list": self._sources,
+ "source": "unknown",
+ "volume_level": player_info["volume_level"] / 100,
+ "is_volume_muted": player_info["muted"],
+ "media_position_updated_at": player_info["media_position_updated_at"],
+ "media_duration": None,
+ "media_position": player_info["cur_time"],
+ "media_title": None,
+ "media_artist": None,
+ "media_album_name": None,
+ "entity_picture": None,
+ }
if state != "off":
player = await self.mass.players.get_player(player_id)
if player.queue.cur_item:
state_attributes["media_duration"] = player.queue.cur_item.duration
state_attributes["media_title"] = player.queue.cur_item.name
if player.queue.cur_item.artists:
- state_attributes["media_artist"] = player.queue.cur_item.artists[0].name
+ state_attributes["media_artist"] = player.queue.cur_item.artists[
+ 0
+ ].name
if player.queue.cur_item.album:
- state_attributes["media_album_name"] = player.queue.cur_item.album.name
- state_attributes["entity_picture"] = player.queue.cur_item.album.metadata.get("image")
+ state_attributes[
+ "media_album_name"
+ ] = player.queue.cur_item.album.name
+ state_attributes[
+ "entity_picture"
+ ] = player.queue.cur_item.album.metadata.get("image")
self._published_players[entity_id] = player_id
await self.__set_state(entity_id, state, state_attributes)
async def call_service(self, domain, service, service_data=None):
- ''' call service on hass '''
+ """ call service on hass """
if not self.__send_ws:
return False
- msg = {
- "type": "call_service",
- "domain": domain,
- "service": service,
- }
+ msg = {"type": "call_service", "domain": domain, "service": service}
if service_data:
- msg['service_data'] = service_data
+ msg["service_data"] = service_data
return await self.__send_ws(msg)
@run_periodic(120)
async def __get_sources(self):
- ''' we build a list of all playlists to use as player sources '''
- self._sources = [playlist.name async for playlist in self.mass.music.library_playlists()]
- self._sources += [playlist.name async for playlist in self.mass.music.library_radios()]
+ """ we build a list of all playlists to use as player sources """
+ self._sources = [
+ playlist.name async for playlist in self.mass.music.library_playlists()
+ ]
+ self._sources += [
+ playlist.name async for playlist in self.mass.music.library_radios()
+ ]
async def __set_state(self, entity_id, new_state, state_attributes={}):
- ''' set state to hass entity '''
+ """ set state to hass entity """
data = {
"state": new_state,
"entity_id": entity_id,
- "attributes": state_attributes
- }
- return await self.__post_data('states/%s' % entity_id, data)
-
+ "attributes": state_attributes,
+ }
+ return await self.__post_data("states/%s" % entity_id, data)
+
async def __hass_websocket(self):
- ''' Receive events from Hass through websockets '''
+ """ Receive events from Hass through websockets """
while self.mass.event_loop.is_running():
try:
- protocol = 'wss' if self._use_ssl else 'ws'
- async with self.http_session.ws_connect('%s://%s/api/websocket' % (protocol, self._host), verify_ssl=False) as ws:
-
+ protocol = "wss" if self._use_ssl else "ws"
+ async with self.http_session.ws_connect(
+ "%s://%s/api/websocket" % (protocol, self._host), verify_ssl=False
+ ) as ws:
+
async def send_msg(msg):
- ''' callback to send message to the websockets client'''
+ """ callback to send message to the websockets client"""
self.__last_id += 1
- msg['id'] = self.__last_id
+ msg["id"] = self.__last_id
await ws.send_json(msg)
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
- if msg.data == 'close cmd':
+ if msg.data == "close cmd":
await ws.close()
break
else:
data = msg.json()
- if data['type'] == 'auth_required':
+ if data["type"] == "auth_required":
# send auth token
- auth_msg = {"type": "auth", "access_token": self._token}
+ auth_msg = {
+ "type": "auth",
+ "access_token": self._token,
+ }
await ws.send_json(auth_msg)
- elif data['type'] == 'auth_invalid':
+ elif data["type"] == "auth_invalid":
raise Exception(data)
- elif data['type'] == 'auth_ok':
+ elif data["type"] == "auth_ok":
# register callback
self.__send_ws = send_msg
# subscribe to events
- subscribe_msg = {"type": "subscribe_events", "event_type": "state_changed"}
+ subscribe_msg = {
+ "type": "subscribe_events",
+ "event_type": "state_changed",
+ }
await send_msg(subscribe_msg)
- subscribe_msg = {"type": "subscribe_events", "event_type": "call_service"}
+ subscribe_msg = {
+ "type": "subscribe_events",
+ "event_type": "call_service",
+ }
await send_msg(subscribe_msg)
- elif data['type'] == 'event':
- asyncio.create_task(self.hass_event(data['event']['event_type'], data['event']['data']))
- elif data['type'] == 'result' and data.get('result'):
+ elif data["type"] == "event":
+ asyncio.create_task(
+ self.hass_event(
+ data["event"]["event_type"],
+ data["event"]["data"],
+ )
+ )
+ elif data["type"] == "result" and data.get("result"):
# reply to our get_states request
- asyncio.create_task(self.hass_event('all_states', data['result']))
+ asyncio.create_task(
+ self.hass_event("all_states", data["result"])
+ )
# else:
# LOGGER.info(data)
elif msg.type == aiohttp.WSMsgType.ERROR:
await asyncio.sleep(10)
async def __get_data(self, endpoint):
- ''' get data from hass rest api'''
+ """ get data from hass rest api"""
url = "http://%s/api/%s" % (self._host, endpoint)
if self._use_ssl:
url = "https://%s/api/%s" % (self._host, endpoint)
- headers = {"Authorization": "Bearer %s" % self._token, "Content-Type": "application/json"}
- async with self.http_session.get(url, headers=headers, verify_ssl=False) as response:
+ headers = {
+ "Authorization": "Bearer %s" % self._token,
+ "Content-Type": "application/json",
+ }
+ async with self.http_session.get(
+ url, headers=headers, verify_ssl=False
+ ) as response:
return await response.json()
async def __post_data(self, endpoint, data):
- ''' post data to hass rest api'''
+ """ post data to hass rest api"""
url = "http://%s/api/%s" % (self._host, endpoint)
if self._use_ssl:
url = "https://%s/api/%s" % (self._host, endpoint)
- headers = {"Authorization": "Bearer %s" % self._token, "Content-Type": "application/json"}
- async with self.http_session.post(url, headers=headers, json=data, verify_ssl=False) as response:
- return await response.json()
\ No newline at end of file
+ headers = {
+ "Authorization": "Bearer %s" % self._token,
+ "Content-Type": "application/json",
+ }
+ async with self.http_session.post(
+ url, headers=headers, json=data, verify_ssl=False
+ ) as response:
+ return await response.json()
# -*- coding:utf-8 -*-
import asyncio
-import os
-import operator
import concurrent
-from aiohttp import web
+import gc
+import io
+import operator
+import os
+import shlex
+import subprocess
import threading
import urllib
+
+import aiohttp
+from aiohttp import web
from memory_tempfile import MemoryTempfile
-import soundfile
+from music_assistant.constants import EVENT_STREAM_ENDED, EVENT_STREAM_STARTED
+from music_assistant.models.media_types import MediaType, TrackQuality
+from music_assistant.models.playerstate import PlayerState
+from music_assistant.utils import (
+ LOGGER,
+ get_folder_size,
+ get_ip,
+ run_async_background_task,
+ run_periodic,
+ try_parse_int,
+)
import pyloudnorm
-import io
-import aiohttp
-import subprocess
-import gc
-import shlex
+import soundfile
-from music_assistant.constants import EVENT_STREAM_STARTED, EVENT_STREAM_ENDED
-from music_assistant.utils import LOGGER, try_parse_int, get_ip, run_async_background_task, run_periodic, get_folder_size
-from music_assistant.models.media_types import TrackQuality, MediaType
-from music_assistant.models.playerstate import PlayerState
+class HTTPStreamer:
+ """ Built-in streamer using sox and webserver """
-class HTTPStreamer():
- ''' Built-in streamer using sox and webserver '''
def __init__(self, mass):
self.mass = mass
self.local_ip = get_ip()
self.stream_clients = []
async def setup(self):
- ''' async initialize of module '''
+ """ async initialize of module """
pass # we have nothing to initialize
async def stream(self, http_request):
- '''
+ """
start stream for a player
- '''
+ """
# make sure we have valid params
- player_id = http_request.match_info.get('player_id', '')
+ player_id = http_request.match_info.get("player_id", "")
player = await self.mass.players.get_player(player_id)
if not player:
return web.Response(status=404, reason="Player not found")
if not player.queue.use_queue_stream:
- queue_item_id = http_request.match_info.get('queue_item_id')
+ queue_item_id = http_request.match_info.get("queue_item_id")
queue_item = await player.queue.by_item_id(queue_item_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()
if player.queue.use_queue_stream:
bg_task = self.mass.event_loop.run_in_executor(
- None, self.__get_queue_stream, player, resp, cancelled)
+ None, self.__get_queue_stream, player, resp, cancelled
+ )
else:
bg_task = self.mass.event_loop.run_in_executor(
- None, self.__get_queue_item_stream, player, queue_item, resp,
- cancelled)
+ None, self.__get_queue_item_stream, player, queue_item, resp, cancelled
+ )
# let the streaming begin!
try:
await asyncio.gather(bg_task)
return resp
def __get_queue_item_stream(self, player, queue_item, buffer, cancelled):
- ''' start streaming single queue track '''
+ """ start streaming single queue track """
LOGGER.debug(
- "stream single queue track started for track %s on player %s" %
- (queue_item.name, player.name))
+ "stream single queue track started for track %s on player %s"
+ % (queue_item.name, player.name)
+ )
for is_last_chunk, audio_chunk in self.__get_audio_stream(
- player, queue_item, cancelled):
+ player, 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
- self.mass.run_task(buffer.write(audio_chunk),
- wait_for_result=True,
- ignore_exception=(BrokenPipeError,
- ConnectionResetError))
+ self.mass.run_task(
+ buffer.write(audio_chunk),
+ wait_for_result=True,
+ ignore_exception=(BrokenPipeError, ConnectionResetError),
+ )
# all chunks received: streaming finished
if cancelled.is_set():
LOGGER.debug(
- "stream single track interrupted for track %s on player %s" %
- (queue_item.name, player.name))
+ "stream single track interrupted for track %s on player %s"
+ % (queue_item.name, player.name)
+ )
else:
# indicate EOF if no more data
- self.mass.run_task(buffer.write_eof(),
- wait_for_result=True,
- ignore_exception=(BrokenPipeError,
- ConnectionResetError))
+ self.mass.run_task(
+ buffer.write_eof(),
+ wait_for_result=True,
+ ignore_exception=(BrokenPipeError, ConnectionResetError),
+ )
LOGGER.debug(
- "stream single track finished for track %s on player %s" %
- (queue_item.name, player.name))
+ "stream single track finished for track %s on player %s"
+ % (queue_item.name, player.name)
+ )
def __get_queue_stream(self, player, buffer, cancelled):
- ''' start streaming all queue tracks '''
- sample_rate = try_parse_int(player.settings['max_sample_rate'])
+ """ start streaming all queue tracks """
+ sample_rate = try_parse_int(player.settings["max_sample_rate"])
fade_length = try_parse_int(player.settings["crossfade_duration"])
if not sample_rate or sample_rate < 44100 or sample_rate > 384000:
sample_rate = 96000
fade_bytes = int(sample_rate * 4 * 2 * fade_length)
else:
fade_bytes = int(sample_rate * 4 * 2 * 6)
- pcm_args = 'raw -b 32 -c 2 -e signed-integer -r %s' % sample_rate
- args = 'sox -t %s - -t flac -C 0 -' % pcm_args
+ pcm_args = "raw -b 32 -c 2 -e signed-integer -r %s" % sample_rate
+ args = "sox -t %s - -t flac -C 0 -" % pcm_args
# start sox process
args = shlex.split(args)
- sox_proc = subprocess.Popen(args,
- shell=False,
- stdout=subprocess.PIPE,
- stdin=subprocess.PIPE)
+ sox_proc = subprocess.Popen(
+ args, shell=False, stdout=subprocess.PIPE, stdin=subprocess.PIPE
+ )
def fill_buffer():
while True:
buffer.write(chunk),
wait_for_result=True,
ignore_exception=(
- BrokenPipeError, ConnectionResetError,
- concurrent.futures._base.CancelledError))
+ BrokenPipeError,
+ ConnectionResetError,
+ concurrent.futures._base.CancelledError,
+ ),
+ )
del chunk
# indicate EOF if no more data
if not cancelled.is_set():
self.mass.run_task(
buffer.write_eof(),
wait_for_result=True,
- ignore_exception=(BrokenPipeError, ConnectionResetError,
- concurrent.futures._base.CancelledError))
+ ignore_exception=(
+ BrokenPipeError,
+ ConnectionResetError,
+ concurrent.futures._base.CancelledError,
+ ),
+ )
# start fill buffer task in background
fill_buffer_thread = threading.Thread(target=fill_buffer)
LOGGER.info("Start Queue Stream for player %s " % (player.name))
is_start = True
- last_fadeout_data = b''
+ last_fadeout_data = b""
while True:
if cancelled.is_set():
break
if is_start:
# report start of queue playback so we can calculate current track/duration etc.
queue_track = asyncio.run_coroutine_threadsafe(
- player.queue.start_queue_stream(),
- self.mass.event_loop).result()
+ player.queue.start_queue_stream(), self.mass.event_loop
+ ).result()
is_start = False
else:
queue_track = player.queue.next_item
if not queue_track:
LOGGER.debug("no (more) tracks left in queue")
break
- LOGGER.debug("Start Streaming queue track: %s (%s) on player %s" %
- (queue_track.item_id, queue_track.name, player.name))
- fade_in_part = b''
+ LOGGER.debug(
+ "Start Streaming queue track: %s (%s) on player %s"
+ % (queue_track.item_id, queue_track.name, player.name)
+ )
+ fade_in_part = b""
cur_chunk = 0
prev_chunk = None
bytes_written = 0
# handle incoming audio chunks
for is_last_chunk, chunk in self.__get_audio_stream(
- player,
- queue_track,
- cancelled,
- chunksize=fade_bytes,
- resample=sample_rate):
+ player,
+ queue_track,
+ cancelled,
+ chunksize=fade_bytes,
+ resample=sample_rate,
+ ):
cur_chunk += 1
### HANDLE FIRST PART OF TRACK
if cur_chunk == 1 and is_last_chunk:
- LOGGER.warning("Stream error, skip track %s",
- queue_track.item_id)
+ LOGGER.warning("Stream error, skip track %s", queue_track.item_id)
break
if cur_chunk <= 2 and not last_fadeout_data:
# no fadeout_part available so just pass it to the output directly
### HANDLE CROSSFADE OF PREVIOUS TRACK FADE_OUT AND THIS TRACK FADE_IN
elif cur_chunk == 2 and last_fadeout_data:
# combine the first 2 chunks and strip off silence
- args = 'sox --ignore-length -t %s - -t %s - silence 1 0.1 1%%' % (
- pcm_args, pcm_args)
+ args = "sox --ignore-length -t %s - -t %s - silence 1 0.1 1%%" % (
+ pcm_args,
+ pcm_args,
+ )
first_part, std_err = subprocess.Popen(
- args,
- shell=True,
- stdout=subprocess.PIPE,
- stdin=subprocess.PIPE).communicate(prev_chunk + chunk)
+ args, shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE
+ ).communicate(prev_chunk + chunk)
if len(first_part) < fade_bytes:
# part is too short after the strip action?!
# so we just use the full first part
del first_part
# do crossfade
crossfade_part = self.__crossfade_pcm_parts(
- fade_in_part, last_fadeout_data, pcm_args, fade_length)
+ fade_in_part, last_fadeout_data, pcm_args, fade_length
+ )
sox_proc.stdin.write(crossfade_part)
bytes_written += len(crossfade_part)
del crossfade_part
del fade_in_part
- last_fadeout_data = b''
+ last_fadeout_data = b""
# also write the leftover bytes from the strip action
sox_proc.stdin.write(remaining_bytes)
bytes_written += len(remaining_bytes)
elif prev_chunk and is_last_chunk:
# last chunk received so create the last_part with the previous chunk and this chunk
# and strip off silence
- args = 'sox --ignore-length -t %s - -t %s - reverse silence 1 0.1 1%% reverse' % (
- pcm_args, pcm_args)
+ args = (
+ "sox --ignore-length -t %s - -t %s - reverse silence 1 0.1 1%% reverse"
+ % (pcm_args, pcm_args)
+ )
last_part, stderr = subprocess.Popen(
- args,
- shell=True,
- stdout=subprocess.PIPE,
- stdin=subprocess.PIPE).communicate(prev_chunk + chunk)
+ args, shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE
+ ).communicate(prev_chunk + chunk)
if len(last_part) < fade_bytes:
# part is too short after the strip action
# 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)
accurate_duration = bytes_written / int(sample_rate * 4 * 2)
queue_track.duration = accurate_duration
LOGGER.debug(
- "Finished Streaming queue track: %s (%s) on player %s" %
- (queue_track.item_id, queue_track.name, player.name))
+ "Finished Streaming queue track: %s (%s) on player %s"
+ % (queue_track.item_id, queue_track.name, player.name)
+ )
# run garbage collect manually to avoid too much memory fragmentation
gc.collect()
# end of queue reached, pass last fadeout bits to final output
# run garbage collect manually to avoid too much memory fragmentation
gc.collect()
if cancelled.is_set():
- LOGGER.info("streaming of queue for player %s interrupted" %
- player.name)
+ LOGGER.info("streaming of queue for player %s interrupted" % player.name)
else:
- LOGGER.info("streaming of queue for player %s completed" %
- player.name)
+ LOGGER.info("streaming of queue for player %s completed" % player.name)
- def __get_audio_stream(self,
- player,
- queue_item,
- cancelled,
- chunksize=128000,
- resample=None):
- ''' get audio stream from provider and apply additional effects/processing where/if needed'''
+ def __get_audio_stream(
+ self, player, queue_item, cancelled, chunksize=128000, resample=None
+ ):
+ """ get audio stream from provider and apply additional effects/processing where/if needed"""
streamdetails = None
# always request the full db track as there might be other qualities available
- full_track = self.mass.run_task(self.mass.music.track(
- queue_item.item_id,
- queue_item.provider,
- lazy=True,
- track_details=queue_item), wait_for_result=True)
+ full_track = self.mass.run_task(
+ self.mass.music.track(
+ queue_item.item_id,
+ queue_item.provider,
+ lazy=True,
+ track_details=queue_item,
+ ),
+ wait_for_result=True,
+ )
# sort by quality and check track availability
- for prov_media in sorted(full_track.provider_ids,
- key=operator.itemgetter('quality'),
- reverse=True):
- if not prov_media['provider'] in self.mass.music.providers:
+ for prov_media in sorted(
+ full_track.provider_ids, key=operator.itemgetter("quality"), reverse=True
+ ):
+ if not prov_media["provider"] in self.mass.music.providers:
continue
# get stream details from provider
- streamdetails = self.mass.run_task(self.mass.music.providers[
- prov_media['provider']].get_stream_details(
- prov_media['item_id']),
- wait_for_result=True)
+ streamdetails = self.mass.run_task(
+ self.mass.music.providers[prov_media["provider"]].get_stream_details(
+ prov_media["item_id"]
+ ),
+ wait_for_result=True,
+ )
if streamdetails:
- streamdetails['player_id'] = player.player_id
- if not 'item_id' in streamdetails:
- streamdetails['item_id'] = prov_media['item_id']
- if not 'provider' in streamdetails:
- streamdetails['provider'] = prov_media['provider']
- if not 'quality' in streamdetails:
- streamdetails['quality'] = prov_media['quality']
+ streamdetails["player_id"] = player.player_id
+ if not "item_id" in streamdetails:
+ streamdetails["item_id"] = prov_media["item_id"]
+ if not "provider" in streamdetails:
+ streamdetails["provider"] = prov_media["provider"]
+ if not "quality" in streamdetails:
+ streamdetails["quality"] = prov_media["quality"]
queue_item.streamdetails = streamdetails
break
if not streamdetails:
LOGGER.warning("no stream details for %s", queue_item.name)
- yield (True, b'')
+ yield (True, b"")
return
# get sox effects and resample options
sox_options = self.__get_player_sox_options(player, streamdetails)
- outputfmt = 'flac -C 0'
+ outputfmt = "flac -C 0"
if resample:
- outputfmt = 'raw -b 32 -c 2 -e signed-integer'
- sox_options += ' rate -v %s' % resample
- streamdetails['sox_options'] = sox_options
+ outputfmt = "raw -b 32 -c 2 -e signed-integer"
+ sox_options += " rate -v %s" % resample
+ streamdetails["sox_options"] = sox_options
# determine how to proceed based on input file type
- if streamdetails["content_type"] == 'aac':
+ if streamdetails["content_type"] == "aac":
# support for AAC created with ffmpeg in between
args = 'ffmpeg -v quiet -i "%s" -f flac - | sox -t flac - -t %s - %s' % (
- streamdetails["path"], outputfmt, sox_options)
- process = subprocess.Popen(args,
- shell=True,
- stdout=subprocess.PIPE,
- bufsize=chunksize)
- elif streamdetails['type'] in ['url', 'file']:
+ streamdetails["path"],
+ outputfmt,
+ sox_options,
+ )
+ process = subprocess.Popen(
+ args, shell=True, stdout=subprocess.PIPE, bufsize=chunksize
+ )
+ elif streamdetails["type"] in ["url", "file"]:
args = 'sox -t %s "%s" -t %s - %s' % (
- streamdetails["content_type"], streamdetails["path"],
- outputfmt, sox_options)
+ streamdetails["content_type"],
+ streamdetails["path"],
+ outputfmt,
+ sox_options,
+ )
args = shlex.split(args)
- process = subprocess.Popen(args,
- shell=False,
- stdout=subprocess.PIPE,
- bufsize=chunksize)
- elif streamdetails['type'] == 'executable':
- args = '%s | sox -t %s - -t %s - %s' % (
- streamdetails["path"], streamdetails["content_type"],
- outputfmt, sox_options)
- process = subprocess.Popen(args,
- shell=True,
- stdout=subprocess.PIPE,
- bufsize=chunksize)
+ process = subprocess.Popen(
+ args, shell=False, stdout=subprocess.PIPE, bufsize=chunksize
+ )
+ elif streamdetails["type"] == "executable":
+ args = "%s | sox -t %s - -t %s - %s" % (
+ streamdetails["path"],
+ streamdetails["content_type"],
+ outputfmt,
+ sox_options,
+ )
+ process = subprocess.Popen(
+ args, shell=True, stdout=subprocess.PIPE, bufsize=chunksize
+ )
else:
LOGGER.warning("no streaming options for %s", queue_item.name)
- yield (True, b'')
+ yield (True, b"")
return
# fire event that streaming has started for this track
- self.mass.run_task(
- self.mass.signal_event(EVENT_STREAM_STARTED, streamdetails))
+ self.mass.run_task(self.mass.signal_event(EVENT_STREAM_STARTED, streamdetails))
# yield chunks from stdout
# we keep 1 chunk behind to detect end of stream properly
- prev_chunk = b''
+ prev_chunk = b""
while True:
if cancelled.is_set():
# http session ended
yield (False, prev_chunk)
prev_chunk = chunk
# fire event that streaming has ended
- self.mass.run_task(
- self.mass.signal_event(EVENT_STREAM_ENDED, streamdetails))
+ self.mass.run_task(self.mass.signal_event(EVENT_STREAM_ENDED, streamdetails))
# send task to background to analyse the audio
if queue_item.media_type == MediaType.Track:
- self.mass.event_loop.run_in_executor(None, self.__analyze_audio,
- streamdetails)
+ self.mass.event_loop.run_in_executor(
+ None, self.__analyze_audio, streamdetails
+ )
def __get_player_sox_options(self, player, streamdetails):
- ''' get player specific sox effect options '''
+ """ get player specific sox effect options """
sox_options = []
# volume normalisation
- gain_correct = self.mass.run_task(self.mass.players.get_gain_correct(
- player.player_id, streamdetails["item_id"],
- streamdetails["provider"]),
- wait_for_result=True)
+ gain_correct = self.mass.run_task(
+ self.mass.players.get_gain_correct(
+ player.player_id, streamdetails["item_id"], streamdetails["provider"]
+ ),
+ wait_for_result=True,
+ )
if gain_correct != 0:
- sox_options.append('vol %s dB ' % gain_correct)
+ sox_options.append("vol %s dB " % gain_correct)
# downsample if needed
- if player.settings['max_sample_rate']:
- max_sample_rate = try_parse_int(player.settings['max_sample_rate'])
+ if player.settings["max_sample_rate"]:
+ max_sample_rate = try_parse_int(player.settings["max_sample_rate"])
if max_sample_rate:
quality = streamdetails["quality"]
- if quality > TrackQuality.FLAC_LOSSLESS_HI_RES_3 and max_sample_rate == 192000:
- sox_options.append('rate -v 192000')
- elif quality > TrackQuality.FLAC_LOSSLESS_HI_RES_2 and max_sample_rate == 96000:
- sox_options.append('rate -v 96000')
- elif quality > TrackQuality.FLAC_LOSSLESS_HI_RES_1 and max_sample_rate == 48000:
- sox_options.append('rate -v 48000')
- if player.settings.get('sox_options'):
- sox_options.append(player.settings['sox_options'])
+ if (
+ quality > TrackQuality.FLAC_LOSSLESS_HI_RES_3
+ and max_sample_rate == 192000
+ ):
+ sox_options.append("rate -v 192000")
+ elif (
+ quality > TrackQuality.FLAC_LOSSLESS_HI_RES_2
+ and max_sample_rate == 96000
+ ):
+ sox_options.append("rate -v 96000")
+ elif (
+ quality > TrackQuality.FLAC_LOSSLESS_HI_RES_1
+ and max_sample_rate == 48000
+ ):
+ sox_options.append("rate -v 48000")
+ if player.settings.get("sox_options"):
+ sox_options.append(player.settings["sox_options"])
return " ".join(sox_options)
def __analyze_audio(self, streamdetails):
- ''' analyze track audio, for now we only calculate EBU R128 loudness '''
- item_key = '%s%s' % (streamdetails["item_id"],
- streamdetails["provider"])
+ """ analyze track audio, for now we only calculate EBU R128 loudness """
+ item_key = "%s%s" % (streamdetails["item_id"], streamdetails["provider"])
if item_key in self.analyze_jobs:
return # prevent multiple analyze jobs for same track
self.analyze_jobs[item_key] = True
- track_loudness = self.mass.run_task(self.mass.db.get_track_loudness(
- streamdetails["item_id"], streamdetails["provider"]),
- wait_for_result=True)
+ track_loudness = self.mass.run_task(
+ self.mass.db.get_track_loudness(
+ streamdetails["item_id"], streamdetails["provider"]
+ ),
+ wait_for_result=True,
+ )
if track_loudness == None:
# only when needed we do the analyze stuff
- LOGGER.debug('Start analyzing track %s' % item_key)
- if streamdetails['type'] == 'url':
+ LOGGER.debug("Start analyzing track %s" % item_key)
+ if streamdetails["type"] == "url":
import urllib
- audio_data = urllib.request.urlopen(
- streamdetails["path"]).read()
- elif streamdetails['type'] == 'executable':
- audio_data = subprocess.check_output(streamdetails["path"],
- shell=True)
- elif streamdetails['type'] == 'file':
- with open(streamdetails['path'], 'rb') as f:
+
+ audio_data = urllib.request.urlopen(streamdetails["path"]).read()
+ elif streamdetails["type"] == "executable":
+ audio_data = subprocess.check_output(streamdetails["path"], shell=True)
+ elif streamdetails["type"] == "file":
+ with open(streamdetails["path"], "rb") as f:
audio_data = f.read()
# calculate BS.1770 R128 integrated loudness
with io.BytesIO(audio_data) as tmpfile:
loudness = meter.integrated_loudness(data) # measure loudness
del data
self.mass.run_task(
- self.mass.db.set_track_loudness(streamdetails["item_id"],
- streamdetails["provider"],
- loudness))
+ self.mass.db.set_track_loudness(
+ streamdetails["item_id"], streamdetails["provider"], loudness
+ )
+ )
del audio_data
- LOGGER.debug("Integrated loudness of track %s is: %s" %
- (item_key, loudness))
+ LOGGER.debug(
+ "Integrated loudness of track %s is: %s" % (item_key, loudness)
+ )
self.analyze_jobs.pop(item_key, None)
@staticmethod
- def __crossfade_pcm_parts(fade_in_part, fade_out_part, pcm_args,
- fade_length):
- ''' crossfade two chunks of audio using sox '''
+ def __crossfade_pcm_parts(fade_in_part, fade_out_part, pcm_args, fade_length):
+ """ crossfade two chunks of audio using sox """
# create fade-in part
- fadeinfile = MemoryTempfile(fallback=True).NamedTemporaryFile(
- buffering=0)
- args = 'sox --ignore-length -t %s - -t %s %s fade t %s' % (
- pcm_args, pcm_args, fadeinfile.name, fade_length)
+ fadeinfile = MemoryTempfile(fallback=True).NamedTemporaryFile(buffering=0)
+ args = "sox --ignore-length -t %s - -t %s %s fade t %s" % (
+ pcm_args,
+ pcm_args,
+ fadeinfile.name,
+ fade_length,
+ )
args = shlex.split(args)
process = subprocess.Popen(args, shell=False, stdin=subprocess.PIPE)
process.communicate(fade_in_part)
# create fade-out part
- fadeoutfile = MemoryTempfile(fallback=True).NamedTemporaryFile(
- buffering=0)
- args = 'sox --ignore-length -t %s - -t %s %s reverse fade t %s reverse' % (
- pcm_args, pcm_args, fadeoutfile.name, fade_length)
+ fadeoutfile = MemoryTempfile(fallback=True).NamedTemporaryFile(buffering=0)
+ args = "sox --ignore-length -t %s - -t %s %s reverse fade t %s reverse" % (
+ pcm_args,
+ pcm_args,
+ fadeoutfile.name,
+ 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
- args = 'sox -m -v 1.0 -t %s %s -v 1.0 -t %s %s -t %s -' % (
- pcm_args, fadeoutfile.name, pcm_args, fadeinfile.name, pcm_args)
+ args = "sox -m -v 1.0 -t %s %s -v 1.0 -t %s %s -t %s -" % (
+ pcm_args,
+ fadeoutfile.name,
+ pcm_args,
+ fadeinfile.name,
+ 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, stderr = process.communicate()
fadeinfile.close()
fadeoutfile.close()
# -*- coding:utf-8 -*-
import asyncio
-import re
-import os
-import shutil
-import slugify as unicode_slug
-import uuid
import json
-import time
import logging
+import os
+import re
+import shutil
import threading
+import time
+import uuid
-from .database import Database
+import slugify as unicode_slug
+
+from .cache import Cache
from .config import MassConfig
-from .utils import run_periodic, LOGGER, try_parse_bool, serialize_values
+from .database import Database
+from .homeassistant import HomeAssistant
+from .http_streamer import HTTPStreamer
from .metadata import MetaData
-from .cache import Cache
from .music_manager import MusicManager
from .player_manager import PlayerManager
-from .http_streamer import HTTPStreamer
-from .homeassistant import HomeAssistant
+from .utils import LOGGER, run_periodic, serialize_values, try_parse_bool
from .web import Web
-class MusicAssistant():
-
+class MusicAssistant:
def __init__(self, datapath, event_loop):
- '''
+ """
Create an instance of MusicAssistant
:param datapath: file location to store the data
:param event_loop: asyncio event_loop
- '''
+ """
self.event_loop = event_loop
self.event_loop.set_exception_handler(self.handle_exception)
self.datapath = datapath
self.http_streamer = HTTPStreamer(self)
async def start(self):
- ''' start running the music assistant server '''
+ """ start running the music assistant server """
await self.db.setup()
await self.cache.setup()
await self.metadata.setup()
await self.cache.close()
def handle_exception(self, loop, context):
- ''' global exception handler '''
+ """ global exception handler """
LOGGER.debug(f"Caught exception: {context}")
loop.default_exception_handler(context)
async def signal_event(self, msg, msg_details=None):
- ''' signal (systemwide) event '''
+ """ signal (systemwide) event """
if not (msg_details == None or isinstance(msg_details, (str, dict))):
msg_details = serialize_values(msg_details)
listeners = list(self.event_listeners.values())
for callback, eventfilter in listeners:
if not eventfilter or eventfilter in msg:
- if msg == 'shutdown':
+ if msg == "shutdown":
# the shutdown event should be awaited
await callback(msg, msg_details)
else:
self.event_loop.create_task(callback(msg, msg_details))
async def add_event_listener(self, cb, eventfilter=None):
- ''' add callback to our event listeners '''
+ """ add callback to our event listeners """
cb_id = str(uuid.uuid4())
self.event_listeners[cb_id] = (cb, eventfilter)
return cb_id
async def remove_event_listener(self, cb_id):
- ''' remove callback from our event listeners '''
+ """ remove callback from our event listeners """
self.event_listeners.pop(cb_id, None)
def run_task(self, corofcn, wait_for_result=False, ignore_exception=None):
- ''' helper to run a task on the main event loop from another thread '''
+ """ helper to run a task on the main event loop from another thread """
if threading.current_thread() is threading.main_thread():
raise Exception("Can not be called from main event loop!")
future = asyncio.run_coroutine_threadsafe(corofcn, self.event_loop)
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
-import aiohttp
-from asyncio_throttle import Throttler
-from yarl import URL
import re
-from music_assistant.utils import LOGGER, compare_strings, get_compare_string
+import aiohttp
+from asyncio_throttle import Throttler
from music_assistant.cache import use_cache
+from music_assistant.utils import LOGGER, compare_strings, get_compare_string
+from yarl import URL
LUCENE_SPECIAL = r'([+\-&|!(){}\[\]\^"~*?:\\\/])'
-class MetaData():
- ''' several helpers to search and store mediadata for mediaitems '''
+class MetaData:
+ """ several helpers to search and store mediadata for mediaitems """
+
# TODO: create periodic task to search for missing metadata
def __init__(self, mass):
self.mass = mass
self.fanarttv = FanartTv(mass)
async def setup(self):
- ''' async initialize of metadata module '''
+ """ async initialize of metadata module """
await self.musicbrainz.setup()
await self.fanarttv.setup()
async def 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 not "fanart" in metadata:
res = await self.fanarttv.artist_images(mb_artist_id)
if res:
self.merge_metadata(cur_metadata, res)
return metadata
- async def get_mb_artist_id(self,
- artistname,
- albumname=None,
- album_upc=None,
- trackname=None,
- track_isrc=None):
- ''' retrieve musicbrainz artist id for the given details '''
+ async def get_mb_artist_id(
+ self,
+ artistname,
+ albumname=None,
+ album_upc=None,
+ trackname=None,
+ track_isrc=None,
+ ):
+ """ retrieve musicbrainz artist id for the given details """
LOGGER.debug(
- 'searching musicbrainz for %s (albumname: %s - album_upc: %s - trackname: %s - track_isrc: %s)',
- artistname, albumname, album_upc, trackname, track_isrc)
+ "searching musicbrainz for %s (albumname: %s - album_upc: %s - trackname: %s - track_isrc: %s)",
+ artistname,
+ albumname,
+ album_upc,
+ trackname,
+ track_isrc,
+ )
mb_artist_id = None
if album_upc:
mb_artist_id = await self.musicbrainz.search_artist_by_album(
- artistname, None, album_upc)
+ artistname, None, album_upc
+ )
if mb_artist_id:
LOGGER.debug(
- 'Got MusicbrainzArtistId for %s after search on upc %s --> %s',
- artistname, album_upc, mb_artist_id)
+ "Got MusicbrainzArtistId for %s after search on upc %s --> %s",
+ artistname,
+ album_upc,
+ mb_artist_id,
+ )
if not mb_artist_id and track_isrc:
mb_artist_id = await self.musicbrainz.search_artist_by_track(
- artistname, None, track_isrc)
+ artistname, None, track_isrc
+ )
if mb_artist_id:
LOGGER.debug(
- 'Got MusicbrainzArtistId for %s after search on isrc %s --> %s',
- artistname, track_isrc, mb_artist_id)
+ "Got MusicbrainzArtistId for %s after search on isrc %s --> %s",
+ artistname,
+ track_isrc,
+ mb_artist_id,
+ )
if not mb_artist_id and albumname:
mb_artist_id = await self.musicbrainz.search_artist_by_album(
- artistname, albumname)
+ artistname, albumname
+ )
if mb_artist_id:
LOGGER.debug(
- 'Got MusicbrainzArtistId for %s after search on albumname %s --> %s',
- artistname, albumname, mb_artist_id)
+ "Got MusicbrainzArtistId for %s after search on albumname %s --> %s",
+ artistname,
+ albumname,
+ mb_artist_id,
+ )
if not mb_artist_id and trackname:
mb_artist_id = await self.musicbrainz.search_artist_by_track(
- artistname, trackname)
+ artistname, trackname
+ )
if mb_artist_id:
LOGGER.debug(
- 'Got MusicbrainzArtistId for %s after search on trackname %s --> %s',
- artistname, trackname, mb_artist_id)
+ "Got MusicbrainzArtistId for %s after search on trackname %s --> %s",
+ artistname,
+ trackname,
+ mb_artist_id,
+ )
return mb_artist_id
@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 overwiteing existing values """
for key, value in new_values.items():
if not cur_metadata.get(key):
cur_metadata[key] = value
return cur_metadata
-class MusicBrainz():
+class MusicBrainz:
def __init__(self, mass):
self.mass = mass
self.cache = mass.cache
async def setup(self):
- ''' perform async setup '''
+ """ perform async setup """
self.http_session = aiohttp.ClientSession(
- loop=self.mass.event_loop, connector=aiohttp.TCPConnector())
+ loop=self.mass.event_loop, connector=aiohttp.TCPConnector()
+ )
self.throttler = Throttler(rate_limit=1, period=1)
- async def search_artist_by_album(self,
- artistname,
- albumname=None,
- album_upc=None):
- ''' retrieve musicbrainz artist id by providing the artist name and albumname or upc '''
+ async def search_artist_by_album(self, artistname, albumname=None, album_upc=None):
+ """ retrieve musicbrainz artist id by providing the artist name and albumname or upc """
for searchartist in [
- re.sub(LUCENE_SPECIAL, r'\\\1', artistname),
- get_compare_string(artistname)
+ re.sub(LUCENE_SPECIAL, r"\\\1", artistname),
+ get_compare_string(artistname),
]:
if album_upc:
- endpoint = 'release'
- params = {'query': 'barcode:%s' % album_upc}
+ endpoint = "release"
+ params = {"query": "barcode:%s" % album_upc}
else:
- searchalbum = re.sub(LUCENE_SPECIAL, r'\\\1', albumname)
- endpoint = 'release'
+ searchalbum = re.sub(LUCENE_SPECIAL, r"\\\1", albumname)
+ endpoint = "release"
params = {
- 'query':
- 'artist:"%s" AND release:"%s"' %
- (searchartist, searchalbum)
+ "query": 'artist:"%s" AND release:"%s"'
+ % (searchartist, searchalbum)
}
result = await self.get_data(endpoint, params)
- if result and 'releases' in result:
+ if result and "releases" in result:
for strictness in [True, False]:
- for item in result['releases']:
+ for item in result["releases"]:
if album_upc or compare_strings(
- item['title'], albumname, strictness):
- for artist in item['artist-credit']:
- if compare_strings(artist['artist']['name'],
- artistname, strictness):
- return artist['artist']['id']
- for item in artist.get('aliases', []):
- if compare_strings(item['name'],
- artistname, strictness):
- return artist['id']
- return ''
-
- async def search_artist_by_track(self,
- artistname,
- trackname=None,
- track_isrc=None):
- ''' retrieve artist id by providing the artist name and trackname or track isrc '''
- endpoint = 'recording'
- searchartist = re.sub(LUCENE_SPECIAL, r'\\\1', artistname)
- #searchartist = searchartist.replace('/','').replace('\\','').replace('-', '')
+ item["title"], albumname, strictness
+ ):
+ for artist in item["artist-credit"]:
+ if compare_strings(
+ artist["artist"]["name"], artistname, strictness
+ ):
+ return artist["artist"]["id"]
+ for item in artist.get("aliases", []):
+ if compare_strings(
+ item["name"], artistname, strictness
+ ):
+ return artist["id"]
+ return ""
+
+ async def search_artist_by_track(self, artistname, trackname=None, track_isrc=None):
+ """ retrieve artist id by providing the artist name and trackname or track isrc """
+ endpoint = "recording"
+ searchartist = re.sub(LUCENE_SPECIAL, r"\\\1", artistname)
+ # searchartist = searchartist.replace('/','').replace('\\','').replace('-', '')
if track_isrc:
- endpoint = 'isrc/%s' % track_isrc
- params = {'inc': 'artist-credits'}
+ endpoint = "isrc/%s" % track_isrc
+ params = {"inc": "artist-credits"}
else:
- searchtrack = re.sub(LUCENE_SPECIAL, r'\\\1', trackname)
- endpoint = 'recording'
- params = {
- 'query': '"%s" AND artist:"%s"' % (searchtrack, searchartist)
- }
+ searchtrack = re.sub(LUCENE_SPECIAL, r"\\\1", trackname)
+ endpoint = "recording"
+ params = {"query": '"%s" AND artist:"%s"' % (searchtrack, searchartist)}
result = await self.get_data(endpoint, params)
- if result and 'recordings' in result:
+ if result and "recordings" in result:
for strictness in [True, False]:
- for item in result['recordings']:
- if track_isrc or compare_strings(item['title'], trackname,
- strictness):
- for artist in item['artist-credit']:
- if compare_strings(artist['artist']['name'],
- artistname, strictness):
- return artist['artist']['id']
- for item in artist.get('aliases', []):
- if compare_strings(item['name'], artistname,
- strictness):
- return artist['id']
- return ''
+ for item in result["recordings"]:
+ if track_isrc or compare_strings(
+ item["title"], trackname, strictness
+ ):
+ for artist in item["artist-credit"]:
+ if compare_strings(
+ artist["artist"]["name"], artistname, strictness
+ ):
+ return artist["artist"]["id"]
+ for item in artist.get("aliases", []):
+ if compare_strings(
+ item["name"], artistname, strictness
+ ):
+ return artist["id"]
+ return ""
@use_cache(2)
async def get_data(self, endpoint, params={}):
- ''' get data from api'''
- url = 'http://musicbrainz.org/ws/2/%s' % endpoint
- headers = {
- 'User-Agent':
- 'Music Assistant/1.0.0 https://github.com/marcelveldt'
- }
- params['fmt'] = 'json'
+ """ get data from api"""
+ url = "http://musicbrainz.org/ws/2/%s" % endpoint
+ headers = {"User-Agent": "Music Assistant/1.0.0 https://github.com/marcelveldt"}
+ params["fmt"] = "json"
async with self.throttler:
- async with self.http_session.get(url,
- headers=headers,
- params=params,
- verify_ssl=False) as response:
+ async with self.http_session.get(
+ url, headers=headers, params=params, verify_ssl=False
+ ) as response:
try:
result = await response.json()
except Exception as exc:
return result
-class FanartTv():
+class FanartTv:
def __init__(self, mass):
self.mass = mass
self.cache = mass.cache
async def setup(self):
- ''' perform async setup '''
+ """ perform async setup """
self.http_session = aiohttp.ClientSession(
- loop=self.mass.event_loop, connector=aiohttp.TCPConnector())
+ loop=self.mass.event_loop, connector=aiohttp.TCPConnector()
+ )
self.throttler = Throttler(rate_limit=1, period=2)
async def artist_images(self, mb_artist_id):
- ''' retrieve images by musicbrainz artist id '''
+ """ retrieve images by musicbrainz artist id """
metadata = {}
data = await self.get_data("music/%s" % mb_artist_id)
if data:
- if data.get('hdmusiclogo'):
- metadata['logo'] = data['hdmusiclogo'][0]["url"]
- elif data.get('musiclogo'):
- metadata['logo'] = data['musiclogo'][0]["url"]
- if data.get('artistbackground'):
+ if data.get("hdmusiclogo"):
+ metadata["logo"] = data["hdmusiclogo"][0]["url"]
+ elif data.get("musiclogo"):
+ metadata["logo"] = data["musiclogo"][0]["url"]
+ if data.get("artistbackground"):
count = 0
- for item in data['artistbackground']:
+ for item in data["artistbackground"]:
key = "fanart" if count == 0 else "fanart.%s" % count
metadata[key] = item["url"]
- if data.get('artistthumb'):
- url = data['artistthumb'][0]["url"]
- if not '2a96cbd8b46e442fc41c2b86b821562f' in url:
- metadata['image'] = url
- if data.get('musicbanner'):
- metadata['banner'] = data['musicbanner'][0]["url"]
+ if data.get("artistthumb"):
+ url = data["artistthumb"][0]["url"]
+ if not "2a96cbd8b46e442fc41c2b86b821562f" in url:
+ metadata["image"] = url
+ if data.get("musicbanner"):
+ metadata["banner"] = data["musicbanner"][0]["url"]
return metadata
@use_cache(30)
async def get_data(self, endpoint, params={}):
- ''' get data from api'''
- url = 'http://webservice.fanart.tv/v3/%s' % endpoint
- params['api_key'] = '639191cb0774661597f28a47e7e2bad5'
+ """ get data from api"""
+ url = "http://webservice.fanart.tv/v3/%s" % endpoint
+ params["api_key"] = "639191cb0774661597f28a47e7e2bad5"
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:
try:
result = await response.json()
except aiohttp.client_exceptions.ContentTypeError:
except aiohttp.client_exceptions.ClientConnectorError:
LOGGER.error("Failed to retrieve %s", endpoint)
return None
- if 'error' in result and 'limit' in result['error']:
- LOGGER.error(result['error'])
+ if "error" in result and "limit" in result["error"]:
+ LOGGER.error(result["error"])
return None
return result
from enum import Enum, IntEnum
+
class MediaType(IntEnum):
Artist = 1
Album = 2
Playlist = 4
Radio = 5
+
def media_type_from_string(media_type_str):
media_type_str = media_type_str.lower()
- if 'artist' in media_type_str or media_type_str == '1':
+ if "artist" in media_type_str or media_type_str == "1":
return MediaType.Artist
- elif 'album' in media_type_str or media_type_str == '2':
+ elif "album" in media_type_str or media_type_str == "2":
return MediaType.Album
- elif 'track' in media_type_str or media_type_str == '3':
+ elif "track" in media_type_str or media_type_str == "3":
return MediaType.Track
- elif 'playlist' in media_type_str or media_type_str == '4':
+ elif "playlist" in media_type_str or media_type_str == "4":
return MediaType.Playlist
- elif 'radio' in media_type_str or media_type_str == '5':
+ elif "radio" in media_type_str or media_type_str == "5":
return MediaType.Radio
else:
return None
+
class ContributorRole(IntEnum):
Artist = 1
Writer = 2
Producer = 3
+
class AlbumType(IntEnum):
Album = 1
Single = 2
Compilation = 3
+
class TrackQuality(IntEnum):
LOSSY_MP3 = 0
LOSSY_OGG = 1
LOSSY_AAC = 2
- FLAC_LOSSLESS = 6 # 44.1/48khz 16 bits HI-RES
- FLAC_LOSSLESS_HI_RES_1 = 7 # 44.1/48khz 24 bits HI-RES
- FLAC_LOSSLESS_HI_RES_2 = 8 # 88.2/96khz 24 bits HI-RES
- FLAC_LOSSLESS_HI_RES_3 = 9 # 176/192khz 24 bits HI-RES
- FLAC_LOSSLESS_HI_RES_4 = 10 # above 192khz 24 bits HI-RES
+ FLAC_LOSSLESS = 6 # 44.1/48khz 16 bits HI-RES
+ FLAC_LOSSLESS_HI_RES_1 = 7 # 44.1/48khz 24 bits HI-RES
+ FLAC_LOSSLESS_HI_RES_2 = 8 # 88.2/96khz 24 bits HI-RES
+ FLAC_LOSSLESS_HI_RES_3 = 9 # 176/192khz 24 bits HI-RES
+ FLAC_LOSSLESS_HI_RES_4 = 10 # above 192khz 24 bits HI-RES
+
class MediaItem(object):
- ''' representation of a media item '''
+ """ representation of a media item """
+
def __init__(self):
self.item_id = None
- self.provider = 'database'
- self.name = ''
+ self.provider = "database"
+ self.name = ""
self.metadata = {}
self.tags = []
self.external_ids = []
self.in_library = []
self.is_lazy = False
self.available = True
- def __eq__(self, other):
+
+ def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
- return (self.name == other.name and
- self.item_id == other.item_id and
- self.provider == other.provider)
+ return (
+ self.name == other.name
+ and self.item_id == other.item_id
+ and self.provider == other.provider
+ )
+
def __ne__(self, other):
return not self.__eq__(other)
+
class Artist(MediaItem):
- ''' representation of an artist '''
+ """ representation of an artist """
+
def __init__(self):
super().__init__()
- self.sort_name = ''
+ self.sort_name = ""
self.media_type = MediaType.Artist
+
class Album(MediaItem):
- ''' representation of an album '''
+ """ representation of an album """
+
def __init__(self):
super().__init__()
- self.version = ''
+ self.version = ""
self.albumtype = AlbumType.Album
self.year = 0
self.artist = None
self.labels = []
self.media_type = MediaType.Album
+
class Track(MediaItem):
- ''' representation of a track '''
+ """ representation of a track """
+
def __init__(self):
super().__init__()
self.duration = 0
- self.version = ''
+ self.version = ""
self.artists = []
self.album = None
self.disc_number = 1
self.track_number = 1
self.media_type = MediaType.Track
+
class Playlist(MediaItem):
- ''' representation of a playlist '''
+ """ representation of a playlist """
+
def __init__(self):
super().__init__()
- self.owner = ''
+ self.owner = ""
self.media_type = MediaType.Playlist
self.is_editable = False
- self.checksum = '' # some value to detect playlist track changes
+ self.checksum = "" # some value to detect playlist track changes
+
class Radio(MediaItem):
- ''' representation of a radio station '''
+ """ representation of a radio station """
+
def __init__(self):
super().__init__()
self.media_type = MediaType.Radio
self.duration = 86400
-
-
import asyncio
from typing import List
+
+from music_assistant.cache import cached, cached_iterator
+from music_assistant.models.media_types import (
+ Album,
+ Artist,
+ MediaType,
+ Playlist,
+ Radio,
+ Track,
+)
from music_assistant.utils import LOGGER, compare_strings
-from music_assistant.cache import cached_iterator, cached
-from music_assistant.models.media_types import Album, Artist, Track, Playlist, MediaType, Radio
-class MusicProvider():
+class MusicProvider:
"""
Model for a Musicprovider
Common methods usable for every provider
Provider specific get methods shoud be overriden in the provider specific implementation
Uses a form of lazy provisioning to local db as cache
"""
+
def __init__(self, mass):
"""[DO NOT OVERRIDE]"""
- self.prov_id = ''
- self.name = ''
+ self.prov_id = ""
+ self.name = ""
self.mass = mass
self.cache = mass.cache
### Common methods and properties ####
- async def artist(self,
- prov_item_id,
- lazy=True,
- ref_album=None,
- ref_track=None,
- provider=None) -> Artist:
+ async def artist(
+ self, prov_item_id, lazy=True, ref_album=None, ref_track=None, provider=None
+ ) -> Artist:
""" return artist details for the given provider artist id """
if not provider:
provider = self.prov_id
- item_id = await self.mass.db.get_database_id(provider, prov_item_id,
- MediaType.Artist)
+ item_id = await self.mass.db.get_database_id(
+ provider, prov_item_id, MediaType.Artist
+ )
if item_id is not None:
# artist not yet in local database so fetch details
- cache_key = f'{self.prov_id}.get_artist.{prov_item_id}'
- artist_details = await cached(self.cache, cache_key,
- self.get_artist, prov_item_id)
+ cache_key = f"{self.prov_id}.get_artist.{prov_item_id}"
+ artist_details = await cached(
+ self.cache, cache_key, self.get_artist, prov_item_id
+ )
if not artist_details:
- raise Exception('artist not found: %s' % prov_item_id)
+ raise Exception("artist not found: %s" % prov_item_id)
if lazy:
asyncio.create_task(self.add_artist(artist_details))
artist_details.is_lazy = True
return artist_details
- item_id = await self.add_artist(artist_details,
- ref_album=ref_album,
- ref_track=ref_track)
+ item_id = await self.add_artist(
+ artist_details, ref_album=ref_album, ref_track=ref_track
+ )
return await self.mass.db.artist(item_id)
- async def add_artist(self, artist_details, ref_album=None,
- ref_track=None) -> int:
+ async def add_artist(self, artist_details, ref_album=None, ref_track=None) -> int:
""" add artist to local db and return the new database id"""
musicbrainz_id = None
for item in artist_details.external_ids:
musicbrainz_id = item["musicbrainz"]
if not musicbrainz_id:
musicbrainz_id = await self.get_artist_musicbrainz_id(
- artist_details, ref_album=ref_album, ref_track=ref_track)
+ artist_details, ref_album=ref_album, ref_track=ref_track
+ )
if not musicbrainz_id:
return
# grab additional metadata
if musicbrainz_id:
artist_details.external_ids.append({"musicbrainz": musicbrainz_id})
artist_details.metadata = await self.mass.metadata.get_artist_metadata(
- musicbrainz_id, artist_details.metadata)
+ musicbrainz_id, artist_details.metadata
+ )
item_id = await self.mass.db.add_artist(artist_details)
# also fetch same artist on all providers
new_artist = await self.mass.db.artist(item_id)
new_artist_toptracks = [ref_track]
else:
new_artist_toptracks = [
- item async for item in self.get_artist_toptracks(
- artist_details.item_id)
+ item async for item in self.get_artist_toptracks(artist_details.item_id)
]
if ref_album:
new_artist_albums = [ref_album]
else:
new_artist_albums = [
- item async for item in self.get_artist_albums(
- artist_details.item_id)
+ item async for item in self.get_artist_albums(artist_details.item_id)
]
if new_artist_toptracks or new_artist_albums:
- item_provider_keys = [
- item['provider'] for item in new_artist.provider_ids
- ]
+ item_provider_keys = [item["provider"] for item in new_artist.provider_ids]
for prov_id, provider in self.mass.music.providers.items():
if not prov_id in item_provider_keys:
- await provider.match_artist(new_artist, new_artist_albums,
- new_artist_toptracks)
+ await provider.match_artist(
+ new_artist, new_artist_albums, new_artist_toptracks
+ )
return item_id
- async def get_artist_musicbrainz_id(self,
- artist_details: Artist,
- ref_album=None,
- ref_track=None):
+ async def get_artist_musicbrainz_id(
+ self, artist_details: Artist, ref_album=None, ref_track=None
+ ):
""" fetch musicbrainz id by performing search with both the artist and one of it's albums or tracks """
musicbrainz_id = ""
# try with album first
lookup_albums = [ref_album]
else:
lookup_albums = [
- item async for item in self.get_artist_albums(
- artist_details.item_id)
+ item async for item in self.get_artist_albums(artist_details.item_id)
]
for lookup_album in lookup_albums[:10]:
lookup_album_upc = None
musicbrainz_id = await self.mass.metadata.get_mb_artist_id(
artist_details.name,
albumname=lookup_album.name,
- album_upc=lookup_album_upc)
+ album_upc=lookup_album_upc,
+ )
if musicbrainz_id:
break
# fallback to track
lookup_tracks = [ref_track]
else:
lookup_tracks = [
- item async for item in self.get_artist_toptracks(
- artist_details.item_id)
+ item
+ async for item in self.get_artist_toptracks(artist_details.item_id)
]
for lookup_track in lookup_tracks[:25]:
if not lookup_track:
musicbrainz_id = await self.mass.metadata.get_mb_artist_id(
artist_details.name,
trackname=lookup_track.name,
- track_isrc=lookup_track_isrc)
+ track_isrc=lookup_track_isrc,
+ )
if musicbrainz_id:
break
if not musicbrainz_id:
- LOGGER.debug("Unable to get musicbrainz ID for artist %s !",
- artist_details.name)
+ LOGGER.debug(
+ "Unable to get musicbrainz ID for artist %s !", artist_details.name
+ )
musicbrainz_id = artist_details.name
return musicbrainz_id
- async def album(self,
- prov_item_id,
- lazy=True,
- album_details=None,
- provider=None) -> Album:
+ async def album(
+ self, prov_item_id, lazy=True, album_details=None, provider=None
+ ) -> Album:
""" return album details for the given provider album id"""
if not provider:
provider = self.prov_id
- item_id = await self.mass.db.get_database_id(provider, prov_item_id,
- MediaType.Album)
+ item_id = await self.mass.db.get_database_id(
+ provider, prov_item_id, MediaType.Album
+ )
if not item_id:
# album not yet in local database so fetch details
if not album_details:
- cache_key = f'{self.prov_id}.get_album.{prov_item_id}'
- album_details = await cached(self.cache, cache_key,
- self.get_album, prov_item_id)
+ cache_key = f"{self.prov_id}.get_album.{prov_item_id}"
+ album_details = await cached(
+ self.cache, cache_key, self.get_album, prov_item_id
+ )
if not album_details:
- raise Exception('album not found: %s' % prov_item_id)
+ raise Exception("album not found: %s" % prov_item_id)
if lazy:
asyncio.create_task(self.add_album(album_details))
album_details.is_lazy = True
async def add_album(self, album_details) -> int:
""" add album to local db and return the new database id"""
# we need to fetch album artist too
- db_album_artist = await self.artist(album_details.artist.item_id,
- lazy=False,
- ref_album=album_details,
- provider=album_details.artist.provider)
+ db_album_artist = await self.artist(
+ album_details.artist.item_id,
+ lazy=False,
+ ref_album=album_details,
+ provider=album_details.artist.provider,
+ )
album_details.artist = db_album_artist
item_id = await self.mass.db.add_album(album_details)
# also fetch same album on all providers
new_album = await self.mass.db.album(item_id)
- item_provider_keys = [
- item['provider'] for item in new_album.provider_ids
- ]
+ item_provider_keys = [item["provider"] for item in new_album.provider_ids]
for prov_id, provider in self.mass.music.providers.items():
if not prov_id in item_provider_keys:
await provider.match_album(new_album)
return item_id
- async def track(self,
- prov_item_id,
- lazy=True,
- track_details=None,
- provider=None) -> Track:
+ async def track(
+ self, prov_item_id, lazy=True, track_details=None, provider=None
+ ) -> Track:
""" return track details for the given provider track id """
if not provider:
provider = self.prov_id
- item_id = await self.mass.db.get_database_id(provider, prov_item_id,
- MediaType.Track)
+ item_id = await self.mass.db.get_database_id(
+ provider, prov_item_id, MediaType.Track
+ )
if not item_id:
# track not yet in local database so fetch details
if not track_details:
- cache_key = f'{self.prov_id}.get_track.{prov_item_id}'
- track_details = await cached(self.cache, cache_key,
- self.get_track, prov_item_id)
+ cache_key = f"{self.prov_id}.get_track.{prov_item_id}"
+ track_details = await cached(
+ self.cache, cache_key, self.get_track, prov_item_id
+ )
if not track_details:
- LOGGER.error('track not found: %s', prov_item_id)
+ LOGGER.error("track not found: %s", prov_item_id)
return None
if lazy:
asyncio.create_task(self.add_track(track_details))
track_artists = []
# we need to fetch track artists too
for track_artist in track_details.artists:
- db_track_artist = await self.artist(track_artist.item_id,
- lazy=False,
- ref_track=track_details,
- provider=track_artist.provider)
+ db_track_artist = await self.artist(
+ track_artist.item_id,
+ lazy=False,
+ ref_track=track_details,
+ provider=track_artist.provider,
+ )
if db_track_artist:
track_artists.append(db_track_artist)
track_details.artists = track_artists
if album_details:
track_details.album = album_details
# make sure we have a database album
- if track_details.album and track_details.album.provider != 'database':
+ if track_details.album and track_details.album.provider != "database":
track_details.album = await self.album(
track_details.album.item_id,
lazy=False,
- provider=track_details.album.provider)
+ provider=track_details.album.provider,
+ )
item_id = await self.mass.db.add_track(track_details)
# also fetch same track on all providers (will also get other quality versions)
new_track = await self.mass.db.track(item_id)
- item_provider_keys = [
- item['provider'] for item in new_track.provider_ids
- ]
+ item_provider_keys = [item["provider"] for item in new_track.provider_ids]
for prov_id, provider in self.mass.music.providers.items():
if not prov_id in item_provider_keys:
await provider.match_track(new_track)
""" return playlist details for the given provider playlist id """
if not provider:
provider = self.prov_id
- db_id = await self.mass.db.get_database_id(provider, prov_playlist_id,
- MediaType.Playlist)
+ db_id = await self.mass.db.get_database_id(
+ provider, prov_playlist_id, MediaType.Playlist
+ )
if not db_id:
# item not yet in local database so fetch and store details
item_details = await self.get_playlist(prov_playlist_id)
""" return radio details for the given provider playlist id """
if not provider:
provider = self.prov_id
- db_id = await self.mass.db.get_database_id(provider, prov_radio_id,
- MediaType.Radio)
+ db_id = await self.mass.db.get_database_id(
+ provider, prov_radio_id, MediaType.Radio
+ )
if not db_id:
# item not yet in local database so fetch and store details
item_details = await self.get_radio(prov_radio_id)
async def album_tracks(self, prov_album_id) -> List[Track]:
""" return album tracks for the given provider album id"""
- cache_key = f'{self.prov_id}.album_tracks.{prov_album_id}'
- async for item in cached_iterator(self.cache,
- self.get_album_tracks(prov_album_id),
- cache_key):
+ cache_key = f"{self.prov_id}.album_tracks.{prov_album_id}"
+ async for item in cached_iterator(
+ self.cache, self.get_album_tracks(prov_album_id), cache_key
+ ):
if not item:
continue
- db_id = await self.mass.db.get_database_id(item.provider,
- item.item_id,
- MediaType.Track)
+ db_id = await self.mass.db.get_database_id(
+ item.provider, item.item_id, MediaType.Track
+ )
if db_id:
# return database track instead if we have a match
db_item = await self.mass.db.track(db_id, fulldata=False)
""" return playlist tracks for the given provider playlist id"""
playlist = await self.playlist(prov_playlist_id)
cache_checksum = playlist.checksum
- cache_key = f'{self.prov_id}.playlist_tracks.{prov_playlist_id}'
+ cache_key = f"{self.prov_id}.playlist_tracks.{prov_playlist_id}"
pos = 0
async for item in cached_iterator(
- self.cache,
- self.get_playlist_tracks(prov_playlist_id),
- cache_key,
- checksum=cache_checksum):
+ self.cache,
+ self.get_playlist_tracks(prov_playlist_id),
+ cache_key,
+ checksum=cache_checksum,
+ ):
if not item:
continue
- db_id = await self.mass.db.get_database_id(item.provider,
- item.item_id,
- MediaType.Track)
+ db_id = await self.mass.db.get_database_id(
+ item.provider, item.item_id, MediaType.Track
+ )
if db_id:
# return database track instead if we have a match
item = await self.mass.db.track(db_id, fulldata=False)
async def artist_toptracks(self, prov_artist_id) -> List[Track]:
""" return top tracks for an artist """
- cache_key = f'{self.prov_id}.artist_toptracks.{prov_artist_id}'
+ cache_key = f"{self.prov_id}.artist_toptracks.{prov_artist_id}"
async for item in cached_iterator(
- self.cache, self.get_artist_toptracks(prov_artist_id),
- cache_key):
+ self.cache, self.get_artist_toptracks(prov_artist_id), cache_key
+ ):
if item:
db_id = await self.mass.db.get_database_id(
- self.prov_id, item.item_id, MediaType.Track)
+ self.prov_id, item.item_id, MediaType.Track
+ )
if db_id:
# return database track instead if we have a match
yield await self.mass.db.track(db_id)
async def artist_albums(self, prov_artist_id) -> List[Track]:
""" return (all) albums for an artist """
- cache_key = f'{self.prov_id}.artist_albums.{prov_artist_id}'
+ cache_key = f"{self.prov_id}.artist_albums.{prov_artist_id}"
async for item in cached_iterator(
- self.cache, self.get_artist_albums(prov_artist_id), cache_key):
- db_id = await self.mass.db.get_database_id(self.prov_id,
- item.item_id,
- MediaType.Album)
+ self.cache, self.get_artist_albums(prov_artist_id), cache_key
+ ):
+ db_id = await self.mass.db.get_database_id(
+ self.prov_id, item.item_id, MediaType.Album
+ )
if db_id:
# return database album instead if we have a match
yield await self.mass.db.album(db_id)
else:
yield item
- async def match_artist(self, searchartist: Artist,
- searchalbums: List[Album],
- searchtracks: List[Track]):
+ async def match_artist(
+ self, searchartist: Artist, searchalbums: List[Album], searchtracks: List[Track]
+ ):
""" try to match artist in this provider by supplying db artist """
for searchalbum in searchalbums:
searchstr = "%s - %s" % (searchartist.name, searchalbum.name)
- search_results = await self.search(searchstr, [MediaType.Album],
- limit=5)
+ search_results = await self.search(searchstr, [MediaType.Album], limit=5)
for strictness in [True, False]:
for item in search_results["albums"]:
- if (item and compare_strings(
- item.name, searchalbum.name, strict=strictness)):
+ if item and compare_strings(
+ item.name, searchalbum.name, strict=strictness
+ ):
# double safety check - artist must match exactly !
- if compare_strings(item.artist.name,
- searchartist.name,
- strict=strictness):
+ if compare_strings(
+ item.artist.name, searchartist.name, strict=strictness
+ ):
# just load this item in the database where it will be strictly matched
- await self.artist(item.artist.item_id,
- lazy=strictness)
+ await self.artist(item.artist.item_id, lazy=strictness)
return
for searchtrack in searchtracks:
searchstr = "%s - %s" % (searchartist.name, searchtrack.name)
- search_results = await self.search(searchstr, [MediaType.Track],
- limit=5)
+ search_results = await self.search(searchstr, [MediaType.Track], limit=5)
for strictness in [True, False]:
for item in search_results["tracks"]:
- if (item and compare_strings(
- item.name, searchtrack.name, strict=strictness)):
+ if item and compare_strings(
+ item.name, searchtrack.name, strict=strictness
+ ):
# double safety check - artist must match exactly !
for artist in item.artists:
- if compare_strings(artist.name,
- searchartist.name,
- strict=strictness):
+ if compare_strings(
+ artist.name, searchartist.name, strict=strictness
+ ):
# just load this item in the database where it will be strictly matched
# we set skip matching to false to prevent endless recursive matching
await self.artist(artist.item_id, lazy=False)
""" try to match album in this provider by supplying db album """
searchstr = "%s - %s" % (searchalbum.artist.name, searchalbum.name)
if searchalbum.version:
- searchstr += ' ' + searchalbum.version
- search_results = await self.search(searchstr, [MediaType.Album],
- limit=5)
+ searchstr += " " + searchalbum.version
+ search_results = await self.search(searchstr, [MediaType.Album], limit=5)
for item in search_results["albums"]:
- if (item and
- (item.name in searchalbum.name
- or searchalbum.name in item.name) and compare_strings(
- item.artist.name, searchalbum.artist.name, strict=False)):
+ if (
+ item
+ and (item.name in searchalbum.name or searchalbum.name in item.name)
+ and compare_strings(
+ item.artist.name, searchalbum.artist.name, strict=False
+ )
+ ):
# some providers mess up versions in the title, try to fix that situation
- if (searchalbum.version and not item.version
- and searchalbum.name in item.name
- and searchalbum.version in item.name):
+ if (
+ searchalbum.version
+ and not item.version
+ and searchalbum.name in item.name
+ and searchalbum.version in item.name
+ ):
item.name = searchalbum.name
item.version = searchalbum.version
# just load this item in the database where it will be strictly matched
""" try to match track in this provider by supplying db track """
searchstr = "%s - %s" % (searchtrack.artists[0].name, searchtrack.name)
if searchtrack.version:
- searchstr += ' ' + searchtrack.version
+ searchstr += " " + searchtrack.version
searchartists = [item.name for item in searchtrack.artists]
- search_results = await self.search(searchstr, [MediaType.Track],
- limit=5)
+ search_results = await self.search(searchstr, [MediaType.Track], limit=5)
for item in search_results["tracks"]:
if not item or not item.name or not item.album:
continue
- if ((item.name in searchtrack.name
- or searchtrack.name in item.name) and item.album
- and item.album.name == searchtrack.album.name):
+ if (
+ (item.name in searchtrack.name or searchtrack.name in item.name)
+ and item.album
+ and item.album.name == searchtrack.album.name
+ ):
# some providers mess up versions in the title, try to fix that situation
- if (searchtrack.version and not item.version
- and searchtrack.name in item.name
- and searchtrack.version in item.name):
+ if (
+ searchtrack.version
+ and not item.version
+ and searchtrack.name in item.name
+ and searchtrack.version in item.name
+ ):
item.name = searchtrack.name
item.version = searchtrack.version
# double safety check - artist must match exactly !
for artist in item.artists:
for searchartist in searchartists:
- if compare_strings(artist.name,
- searchartist,
- strict=False):
+ if compare_strings(artist.name, searchartist, strict=False):
# just load this item in the database where it will be strictly matched
- await self.track(item.item_id,
- lazy=False,
- track_details=item)
+ await self.track(
+ item.item_id, lazy=False, track_details=item
+ )
break
### Provider specific implementation #####
"""
import time
-from music_assistant.utils import try_parse_int, try_parse_bool, try_parse_float
+
from music_assistant.constants import EVENT_PLAYER_CHANGED
from music_assistant.models.player_queue import PlayerQueue
from music_assistant.models.playerstate import PlayerState
+from music_assistant.utils import try_parse_bool, try_parse_float, try_parse_int
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-public-methods
# pylint: disable=too-few-public-methods
-class Player():
+
+class Player:
"""
Representation of a musicplayer.
Should be subclassed/overriden with provider specific implementation.
async def cmd_next(self):
""" [CAN OVERRIDE] send next track command to player """
- return await self.queue.play_index(self.queue.cur_index+1)
+ return await self.queue.play_index(self.queue.cur_index + 1)
async def cmd_previous(self):
""" [CAN OVERRIDE] send previous track command to player """
- return await self.queue.play_index(self.queue.cur_index-1)
+ return await self.queue.play_index(self.queue.cur_index - 1)
async def cmd_power_on(self):
""" [MUST OVERRIDE] send power ON command to player """
def __init__(self, mass, player_id, prov_id):
# private attributes
self.mass = mass
- self._player_id = player_id # unique id for this player
- self._prov_id = prov_id # unique provider id for the player
- self._name = ''
+ self._player_id = player_id # unique id for this player
+ self._prov_id = prov_id # unique provider id for the player
+ self._name = ""
self._state = PlayerState.Stopped
self._group_childs = []
self._powered = False
self._cur_time = 0
self._media_position_updated_at = 0
- self._cur_uri = ''
+ self._cur_uri = ""
self._volume_level = 0
self._muted = False
self._queue = PlayerQueue(mass, self)
self.__update_player_settings()
self.initialized = False
# public attributes
- self.supports_queue = True # has native support for a queue
- self.supports_gapless = False # has native gapless support
- self.supports_crossfade = False # has native crossfading support
+ self.supports_queue = True # has native support for a queue
+ self.supports_gapless = False # has native gapless support
+ self.supports_crossfade = False # has native crossfading support
@property
def player_id(self):
@property
def enabled(self):
""" [PROTECTED] enabled state of this player """
- if self.settings.get('enabled'):
+ if self.settings.get("enabled"):
return True
else:
return False
@property
def name(self):
""" [PROTECTED] name of this player """
- if self.settings.get('name'):
- return self.settings['name']
+ if self.settings.get("name"):
+ return self.settings["name"]
else:
return self._name
return player_ids
@property
- def group_childs(self)->list:
+ def group_childs(self) -> list:
"""
[PROTECTED]
return all child player ids for this group player as list
self.mass.event_loop.create_task(self.update())
for child_player_id in group_childs:
self.mass.event_loop.create_task(
- self.mass.players.trigger_update(child_player_id))
+ self.mass.players.trigger_update(child_player_id)
+ )
def add_group_child(self, child_player_id):
""" add player as child to this group player """
self._group_childs.append(child_player_id)
self.mass.event_loop.create_task(self.update())
self.mass.event_loop.create_task(
- self.mass.players.trigger_update(child_player_id))
+ self.mass.players.trigger_update(child_player_id)
+ )
def remove_group_child(self, child_player_id):
""" remove player as child from this group player """
self._group_childs.remove(child_player_id)
self.mass.event_loop.create_task(self.update())
self.mass.event_loop.create_task(
- self.mass.players.trigger_update(child_player_id))
+ self.mass.players.trigger_update(child_player_id)
+ )
@property
def state(self):
if not self.enabled:
return False
# homeassistant integration
- if (self.mass.hass.enabled and self.settings.get('hass_power_entity') and
- self.settings.get('hass_power_entity_source')):
- hass_state = self.mass.hass.get_state(
- self.settings['hass_power_entity'],
- attribute='source')
- return hass_state == self.settings['hass_power_entity_source']
- elif self.mass.hass.enabled and self.settings.get('hass_power_entity'):
+ if (
+ self.mass.hass.enabled
+ and self.settings.get("hass_power_entity")
+ and self.settings.get("hass_power_entity_source")
+ ):
hass_state = self.mass.hass.get_state(
- self.settings['hass_power_entity'])
- return hass_state != 'off'
+ self.settings["hass_power_entity"], attribute="source"
+ )
+ return hass_state == self.settings["hass_power_entity_source"]
+ elif self.mass.hass.enabled and self.settings.get("hass_power_entity"):
+ hass_state = self.mass.hass.get_state(self.settings["hass_power_entity"])
+ return hass_state != "off"
# mute as power
- elif self.settings.get('mute_as_power'):
+ elif self.settings.get("mute_as_power"):
return not self.muted
else:
return self._powered
group_volume = group_volume / active_players
return group_volume
# handle hass integration
- elif self.mass.hass.enabled and self.settings.get('hass_volume_entity'):
+ elif self.mass.hass.enabled and self.settings.get("hass_volume_entity"):
hass_state = self.mass.hass.get_state(
- self.settings['hass_volume_entity'],
- attribute='volume_level')
- return int(try_parse_float(hass_state)*100)
+ self.settings["hass_volume_entity"], attribute="volume_level"
+ )
+ return int(try_parse_float(hass_state) * 100)
else:
return self._volume_level
# trigger update on group player
for group_parent_id in self.group_parents:
self.mass.event_loop.create_task(
- self.mass.players.trigger_update(group_parent_id))
+ self.mass.players.trigger_update(group_parent_id)
+ )
@property
def muted(self):
""" [PROTECTED] send power ON command to player """
await self.cmd_power_on()
# handle mute as power
- if self.settings.get('mute_as_power'):
+ if self.settings.get("mute_as_power"):
await self.volume_mute(False)
# handle hass integration
- if (self.mass.hass.enabled and
- self.settings.get('hass_power_entity') and
- self.settings.get('hass_power_entity_source')):
+ if (
+ self.mass.hass.enabled
+ and self.settings.get("hass_power_entity")
+ and self.settings.get("hass_power_entity_source")
+ ):
cur_source = await self.mass.hass.get_state_async(
- self.settings['hass_power_entity'], attribute='source')
+ self.settings["hass_power_entity"], attribute="source"
+ )
if not cur_source:
service_data = {
- 'entity_id': self.settings['hass_power_entity'],
- 'source': self.settings['hass_power_entity_source']
+ "entity_id": self.settings["hass_power_entity"],
+ "source": self.settings["hass_power_entity_source"],
}
- await self.mass.hass.call_service('media_player', 'select_source', service_data)
- elif self.mass.hass.enabled and self.settings.get('hass_power_entity'):
- domain = self.settings['hass_power_entity'].split('.')[0]
- service_data = {'entity_id': self.settings['hass_power_entity']}
- await self.mass.hass.call_service(domain, 'turn_on', service_data)
+ await self.mass.hass.call_service(
+ "media_player", "select_source", service_data
+ )
+ elif self.mass.hass.enabled and self.settings.get("hass_power_entity"):
+ domain = self.settings["hass_power_entity"].split(".")[0]
+ service_data = {"entity_id": self.settings["hass_power_entity"]}
+ await self.mass.hass.call_service(domain, "turn_on", service_data)
# handle play on power on
- if self.settings.get('play_power_on'):
+ if self.settings.get("play_power_on"):
# play player's own queue if it has items
if self._queue.items:
await self.play()
await self.stop()
await self.cmd_power_off()
# handle mute as power
- if self.settings.get('mute_as_power'):
+ if self.settings.get("mute_as_power"):
await self.volume_mute(True)
# handle hass integration
- if (self.mass.hass.enabled and
- self.settings.get('hass_power_entity') and
- self.settings.get('hass_power_entity_source')):
+ if (
+ self.mass.hass.enabled
+ and self.settings.get("hass_power_entity")
+ and self.settings.get("hass_power_entity_source")
+ ):
cur_source = await self.mass.hass.get_state_async(
- self.settings['hass_power_entity'], attribute='source')
- if cur_source == self.settings['hass_power_entity_source']:
- service_data = {'entity_id': self.settings['hass_power_entity']}
- await self.mass.hass.call_service('media_player', 'turn_off', service_data)
- elif self.mass.hass.enabled and self.settings.get('hass_power_entity'):
- domain = self.settings['hass_power_entity'].split('.')[0]
- service_data = {'entity_id': self.settings['hass_power_entity']}
- await self.mass.hass.call_service(domain, 'turn_off', service_data)
+ self.settings["hass_power_entity"], attribute="source"
+ )
+ if cur_source == self.settings["hass_power_entity_source"]:
+ service_data = {"entity_id": self.settings["hass_power_entity"]}
+ await self.mass.hass.call_service(
+ "media_player", "turn_off", service_data
+ )
+ elif self.mass.hass.enabled and self.settings.get("hass_power_entity"):
+ domain = self.settings["hass_power_entity"].split(".")[0]
+ service_data = {"entity_id": self.settings["hass_power_entity"]}
+ await self.mass.hass.call_service(domain, "turn_off", service_data)
# handle group power
if self.is_group:
# player is group, turn off all childs
new_volume = volume_level
volume_dif = new_volume - cur_volume
if cur_volume == 0:
- volume_dif_percent = 1+(new_volume/100)
+ volume_dif_percent = 1 + (new_volume / 100)
else:
- volume_dif_percent = volume_dif/cur_volume
+ volume_dif_percent = volume_dif / cur_volume
for child_player_id in self.group_childs:
child_player = await self.mass.players.get_player(child_player_id)
if child_player and child_player.enabled and child_player.powered:
cur_child_volume = child_player.volume_level
- new_child_volume = cur_child_volume + (cur_child_volume * volume_dif_percent)
+ new_child_volume = cur_child_volume + (
+ cur_child_volume * volume_dif_percent
+ )
await child_player.volume_set(new_child_volume)
# handle hass integration
- elif self.mass.hass.enabled and self.settings.get('hass_volume_entity'):
+ elif self.mass.hass.enabled and self.settings.get("hass_volume_entity"):
service_data = {
- 'entity_id': self.settings['hass_volume_entity'],
- 'volume_level': volume_level/100
+ "entity_id": self.settings["hass_volume_entity"],
+ "volume_level": volume_level / 100,
}
- await self.mass.hass.call_service('media_player', 'volume_set', service_data)
+ await self.mass.hass.call_service(
+ "media_player", "volume_set", service_data
+ )
# just force full volume on actual player if volume is outsourced to hass
await self.cmd_volume_set(100)
else:
@property
def settings(self):
""" [PROTECTED] get player config settings """
- if self.player_id in self.mass.config['player_settings']:
- return self.mass.config['player_settings'][self.player_id]
+ if self.player_id in self.mass.config["player_settings"]:
+ return self.mass.config["player_settings"][self.player_id]
else:
self.__update_player_settings()
- return self.mass.config['player_settings'][self.player_id]
+ return self.mass.config["player_settings"][self.player_id]
def __update_player_settings(self):
""" [PROTECTED] update player config settings """
- player_settings = self.mass.config['player_settings'].get(self.player_id, {})
+ player_settings = self.mass.config["player_settings"].get(self.player_id, {})
# generate config for the player
- config_entries = [ # default config entries for a player
+ config_entries = [ # default config entries for a player
("enabled", True, "player_enabled"),
("name", "", "player_name"),
("mute_as_power", False, "player_mute_power"),
("max_sample_rate", 96000, "max_sample_rate"),
- ('volume_normalisation', True, 'enable_r128_volume_normalisation'),
- ('target_volume', '-23', 'target_volume_lufs'),
- ('fallback_gain_correct', '-12', 'fallback_gain_correct'),
+ ("volume_normalisation", True, "enable_r128_volume_normalisation"),
+ ("target_volume", "-23", "target_volume_lufs"),
+ ("fallback_gain_correct", "-12", "fallback_gain_correct"),
("crossfade_duration", 0, "crossfade_duration"),
("play_power_on", False, "player_power_play"),
]
# append player specific settings
- config_entries += self.mass.players.providers[self._prov_id].player_config_entries
+ config_entries += self.mass.players.providers[
+ self._prov_id
+ ].player_config_entries
# hass integration
- if self.mass.config['base'].get('homeassistant', {}).get("enabled"):
+ if self.mass.config["base"].get("homeassistant", {}).get("enabled"):
# append hass specific config entries
- config_entries += [("hass_power_entity", "", "hass_player_power"),
- ("hass_power_entity_source", "", "hass_player_source"),
- ("hass_volume_entity", "", "hass_player_volume")]
+ config_entries += [
+ ("hass_power_entity", "", "hass_player_power"),
+ ("hass_power_entity_source", "", "hass_player_source"),
+ ("hass_volume_entity", "", "hass_player_volume"),
+ ]
# pylint: disable=unused-variable
for key, def_value, desc in config_entries:
if not key in player_settings:
- if (isinstance(def_value, str) and def_value.startswith('<')):
+ if isinstance(def_value, str) and def_value.startswith("<"):
player_settings[key] = None
else:
player_settings[key] = def_value
# pylint: enable=unused-variable
- self.mass.config['player_settings'][self.player_id] = player_settings
- self.mass.config['player_settings'][self.player_id]['__desc__'] = config_entries
+ self.mass.config["player_settings"][self.player_id] = player_settings
+ self.mass.config["player_settings"][self.player_id]["__desc__"] = config_entries
def to_dict(self):
""" instance attributes as dict so it can be serialized to json """
"group_childs": self.group_childs,
"enabled": self.enabled,
"supports_queue": self.supports_queue,
- "supports_gapless": self.supports_gapless
+ "supports_gapless": self.supports_gapless,
}
"""
import asyncio
-from typing import List
+from enum import Enum
import random
+from typing import List
import uuid
-from enum import Enum
-from music_assistant.utils import LOGGER, serialize_values
-from music_assistant.constants import EVENT_PLAYBACK_STARTED, EVENT_PLAYBACK_STOPPED, \
- EVENT_QUEUE_UPDATED, EVENT_QUEUE_ITEMS_UPDATED
+from music_assistant.constants import (
+ EVENT_PLAYBACK_STARTED,
+ EVENT_PLAYBACK_STOPPED,
+ EVENT_QUEUE_ITEMS_UPDATED,
+ EVENT_QUEUE_UPDATED,
+)
from music_assistant.models.media_types import Track
from music_assistant.models.playerstate import PlayerState
+from music_assistant.utils import LOGGER, serialize_values
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-public-methods
# pylint: disable=too-few-public-methods
+
class QueueOption(str, Enum):
"""Enum representation of the queue (play) options"""
+
Play = "play"
Replace = "replace"
Next = "next"
class QueueItem(Track):
"""Representation of a queue item, extended version of track."""
+
def __init__(self, media_item=None):
super().__init__()
self.streamdetails = {}
for key, value in media_item.__dict__.items():
setattr(self, key, value)
-class PlayerQueue():
+
+class PlayerQueue:
"""
Model for a player's queue.
Can be overriden by custom implementation, but will not be needed
in most cases.
"""
+
def __init__(self, mass, player):
self.mass = mass
self._player = player
self._last_track = None
asyncio.run_coroutine_threadsafe(
self.mass.add_event_listener(self.on_shutdown, "shutdown"),
- self.mass.event_loop)
+ self.mass.event_loop,
+ )
# load previous queue settings from disk
- asyncio.run_coroutine_threadsafe(self.__restore_saved_state(),
- self.mass.event_loop)
+ asyncio.run_coroutine_threadsafe(
+ self.__restore_saved_state(), self.mass.event_loop
+ )
@property
def shuffle_enabled(self):
# shuffle requested
self._shuffle_enabled = True
if self.cur_index is not None:
- played_items = self.items[:self.cur_index]
- next_items = self.__shuffle_items(self.items[self.cur_index +
- 1:])
+ played_items = self.items[: self.cur_index]
+ next_items = self.__shuffle_items(self.items[self.cur_index + 1 :])
items = played_items + [self.cur_item] + next_items
self.mass.event_loop.create_task(self.update(items))
elif self._shuffle_enabled and not enable_shuffle:
# unshuffle
self._shuffle_enabled = False
if self.cur_index is not None:
- played_items = self.items[:self.cur_index]
- next_items = self.items[self.cur_index + 1:]
+ played_items = self.items[: self.cur_index]
+ next_items = self.items[self.cur_index + 1 :]
next_items.sort(key=lambda x: x.sort_index, reverse=False)
items = played_items + [self.cur_item] + next_items
self.mass.event_loop.create_task(self.update(items))
@property
def crossfade_enabled(self):
"""Returns if crossfade is enabled for this player's queue."""
- return self._player.settings.get('crossfade_duration', 0) > 0
+ return self._player.settings.get("crossfade_duration", 0) > 0
@property
def gapless_enabled(self):
"""Returns if gapless support is enabled for this player."""
- return self._player.settings.get('gapless_enabled', True)
+ return self._player.settings.get("gapless_enabled", True)
@property
def cur_index(self):
for example if crossfading is requested but a player doesn't natively support it
it will send a constant stream of audio to the player and all tracks
"""
- return ((self.crossfade_enabled
- and not self._player.supports_crossfade) or
- (self.gapless_enabled and not self._player.supports_gapless))
+ return (self.crossfade_enabled and not self._player.supports_crossfade) or (
+ self.gapless_enabled and not self._player.supports_gapless
+ )
async def get_item(self, index):
"""get item by index from queue"""
await self._player.cmd_queue_load(self.items)
await self.play_index(prev_index)
else:
- LOGGER.warning("resume queue requested for %s but queue is empty",
- self._player.name)
+ LOGGER.warning(
+ "resume queue requested for %s but queue is empty", self._player.name
+ )
async def play_index(self, index):
"""Play item at index X in queue."""
return
if self.use_queue_stream:
self._next_queue_startindex = index
- queue_stream_uri = 'http://%s:%s/stream/%s' % (
- self.mass.web.local_ip, self.mass.web.http_port,
- self._player.player_id)
+ queue_stream_uri = "http://%s:%s/stream/%s" % (
+ self.mass.web.local_ip,
+ self.mass.web.http_port,
+ self._player.player_id,
+ )
return await self._player.cmd_play_uri(queue_stream_uri)
elif self._player.supports_queue:
return await self._player.cmd_queue_play_index(index)
:param offset: offset from current queue position
"""
- if not self.items or self.cur_index is None or self.cur_index + offset > len(
- self.items):
+ if (
+ not self.items
+ or self.cur_index is None
+ or self.cur_index + offset > len(self.items)
+ ):
return await self.load(queue_items)
insert_at_index = self.cur_index + offset
for index, item in enumerate(queue_items):
item.sort_index = insert_at_index + index
if self.shuffle_enabled:
queue_items = self.__shuffle_items(queue_items)
- self._items = self._items[:insert_at_index] + queue_items + self._items[
- insert_at_index:]
+ self._items = (
+ self._items[:insert_at_index] + queue_items + self._items[insert_at_index:]
+ )
if self.use_queue_stream or not self._player.supports_queue:
if offset == 0:
await self.play_index(insert_at_index)
else:
try:
- await self._player.cmd_queue_insert(queue_items,
- insert_at_index)
+ await self._player.cmd_queue_insert(queue_items, insert_at_index)
except NotImplementedError:
# not supported by player, use load queue instead
LOGGER.debug(
"cmd_queue_insert not supported by player, fallback to cmd_queue_load "
)
- self._items = self._items[self.cur_index:]
+ self._items = self._items[self.cur_index :]
await self._player.cmd_queue_load(self._items)
self.mass.event_loop.create_task(
- self.mass.signal_event(EVENT_QUEUE_ITEMS_UPDATED, self.to_dict()))
+ self.mass.signal_event(EVENT_QUEUE_ITEMS_UPDATED, self.to_dict())
+ )
self.mass.event_loop.create_task(self.__save_state())
async def append(self, queue_items: List[QueueItem]):
for index, item in enumerate(queue_items):
item.sort_index = len(self.items) + index
if self.shuffle_enabled:
- played_items = self.items[:self.cur_index]
- next_items = self.items[self.cur_index:] + queue_items
+ played_items = self.items[: self.cur_index]
+ next_items = self.items[self.cur_index :] + queue_items
next_items = self.__shuffle_items(next_items)
items = played_items + next_items
return await self.update(items)
LOGGER.debug(
"cmd_queue_append not supported by player, fallback to cmd_queue_load "
)
- self._items = self._items[self.cur_index:]
+ self._items = self._items[self.cur_index :]
await self._player.cmd_queue_load(self._items)
self.mass.event_loop.create_task(
- self.mass.signal_event(EVENT_QUEUE_ITEMS_UPDATED, self.to_dict()))
+ self.mass.signal_event(EVENT_QUEUE_ITEMS_UPDATED, self.to_dict())
+ )
self.mass.event_loop.create_task(self.__save_state())
async def update(self, queue_items: List[QueueItem]):
LOGGER.debug(
"cmd_queue_update not supported by player, fallback to cmd_queue_load "
)
- self._items = self._items[self.cur_index:]
+ self._items = self._items[self.cur_index :]
await self._player.cmd_queue_load(self._items)
self.mass.event_loop.create_task(
- self.mass.signal_event(EVENT_QUEUE_ITEMS_UPDATED, self.to_dict()))
+ self.mass.signal_event(EVENT_QUEUE_ITEMS_UPDATED, self.to_dict())
+ )
self.mass.event_loop.create_task(self.__save_state())
async def clear(self):
# not supported by player, ignore
pass
self.mass.event_loop.create_task(
- self.mass.signal_event(EVENT_QUEUE_ITEMS_UPDATED, self.to_dict()))
+ self.mass.signal_event(EVENT_QUEUE_ITEMS_UPDATED, self.to_dict())
+ )
async def update_state(self):
"""update queue details, called when player updates"""
"cur_item": serialize_values(self.cur_item),
"cur_item_time": self.cur_item_time,
"next_item": serialize_values(self.next_item),
- "queue_stream_enabled": self.use_queue_stream
+ "queue_stream_enabled": self.use_queue_stream,
}
async def __get_queue_stream_index(self):
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]
async def __process_queue_update(self, new_index, track_time):
"""compare the queue index to determine if playback changed"""
new_track = await self.get_item(new_index)
- if (not self._last_track
- and new_track) or self._last_track != new_track:
+ if (not self._last_track and new_track) or self._last_track != new_track:
# queue track updated
# account for track changing state so trigger track change after 1 second
if self._last_track and self._last_track.streamdetails:
- self._last_track.streamdetails[
- "seconds_played"] = self._last_item_time
- await self.mass.signal_event(EVENT_PLAYBACK_STOPPED,
- self._last_track.streamdetails)
+ self._last_track.streamdetails["seconds_played"] = self._last_item_time
+ await self.mass.signal_event(
+ EVENT_PLAYBACK_STOPPED, self._last_track.streamdetails
+ )
if new_track and new_track.streamdetails:
- await self.mass.signal_event(EVENT_PLAYBACK_STARTED,
- new_track.streamdetails)
+ await self.mass.signal_event(
+ EVENT_PLAYBACK_STARTED, new_track.streamdetails
+ )
self._last_track = new_track
if self._last_player_state != self._player.state:
self._last_player_state = self._player.state
- if (self._player.cur_time == 0 and self._player.state in [
- PlayerState.Stopped, PlayerState.Off
- ]):
+ if self._player.cur_time == 0 and self._player.state in [
+ PlayerState.Stopped,
+ PlayerState.Off,
+ ]:
# player stopped playing
if self._last_track:
await self.mass.signal_event(
- EVENT_PLAYBACK_STOPPED, self._last_track.streamdetails)
+ 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
async def __restore_saved_state(self):
"""try to load the saved queue for this player from cache file"""
- cache_str = 'queue_%s' % self._player.player_id
+ cache_str = "queue_%s" % self._player.player_id
cache_data = await self.mass.cache.get(cache_str)
if cache_data:
self._shuffle_enabled = cache_data["shuffle_enabled"]
async def on_shutdown(self, msg, msg_details):
"""Handle shutdown event, save queue state."""
await self.__save_state()
+
# pylint: enable=unused-argument
async def __save_state(self):
"""save current queue settings to file"""
- cache_str = 'queue_%s' % self._player.player_id
+ cache_str = "queue_%s" % self._player.player_id
cache_data = {
"shuffle_enabled": self._shuffle_enabled,
"repeat_enabled": self._repeat_enabled,
"items": self._items,
"cur_item": self._cur_index,
- "next_queue_index": self._next_queue_startindex
+ "next_queue_index": self._next_queue_startindex,
}
await self.mass.cache.set(cache_str, cache_data)
- LOGGER.info("queue state saved to file for player %s",
- self._player.player_id)
+ LOGGER.info("queue state saved to file for player %s", self._player.player_id)
import asyncio
from enum import Enum
from typing import List
-from music_assistant.utils import run_periodic, LOGGER
+
from music_assistant.constants import CONF_ENABLED
-from music_assistant.models.player_queue import PlayerQueue
from music_assistant.models.media_types import Track
from music_assistant.models.player import Player
+from music_assistant.models.player_queue import PlayerQueue
+from music_assistant.utils import LOGGER, run_periodic
-class PlayerProvider():
- '''
+class PlayerProvider:
+ """
Model for a Playerprovider
Common methods usable for every provider
Provider specific methods should be overriden in the provider specific implementation
- '''
+ """
def __init__(self, mass):
"""[DO NOT OVERRIDE]"""
- self.prov_id = ''
- self.name = ''
+ self.prov_id = ""
+ self.name = ""
self.mass = mass
self.cache = mass.cache
self.player_config_entries = []
@property
def players(self):
- ''' return all players for this provider '''
- return [item for item in self.mass.players.players if item.player_provider == self.prov_id]
-
- async def get_player(self, player_id:str):
- ''' return player by id '''
+ """ return all players for this provider """
+ return [
+ item
+ for item in self.mass.players.players
+ if item.player_provider == self.prov_id
+ ]
+
+ async def get_player(self, player_id: str):
+ """ return player by id """
return await self.mass.players.get_player(player_id)
- async def add_player(self, player:Player):
- ''' register a new player '''
+ async def add_player(self, player: Player):
+ """ register a new player """
return await self.mass.players.add_player(player)
- async def remove_player(self, player_id:str):
- ''' remove a player '''
+ async def remove_player(self, player_id: str):
+ """ remove a player """
return await self.mass.players.remove_player(player_id)
### Provider specific implementation #####
-
-
-
-
-
from enum import Enum
+
class PlayerState(str, Enum):
Off = "off"
Stopped = "stopped"
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
-import operator
-import os
import base64
import functools
+import operator
+import os
import time
from typing import List
-import toolz
+
from PIL import Image
import aiohttp
-
-from music_assistant.utils import run_periodic, LOGGER, load_provider_modules
-from music_assistant.models.media_types import MediaItem, MediaType, Track, Artist, Album, Playlist, Radio
from music_assistant.constants import CONF_KEY_MUSICPROVIDERS, EVENT_MUSIC_SYNC_STATUS
+from music_assistant.models.media_types import (
+ Album,
+ Artist,
+ MediaItem,
+ MediaType,
+ Playlist,
+ Radio,
+ Track,
+)
+from music_assistant.utils import LOGGER, load_provider_modules, run_periodic
+import toolz
def sync_task(desc):
""" decorator to report a sync task """
+
def wrapper(func):
@functools.wraps(func)
async def wrapped(*args):
for sync_prov_id, sync_desc in method_class.running_sync_jobs:
if sync_prov_id == prov_id and sync_desc == desc:
LOGGER.warning(
- "Syncjob %s for provider %s is already running!", desc,
- prov_id)
+ "Syncjob %s for provider %s is already running!", desc, prov_id
+ )
return
sync_job = (prov_id, desc)
method_class.running_sync_jobs.append(sync_job)
await method_class.mass.signal_event(
- EVENT_MUSIC_SYNC_STATUS, method_class.running_sync_jobs)
+ 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)
await method_class.mass.signal_event(
- EVENT_MUSIC_SYNC_STATUS, method_class.running_sync_jobs)
+ EVENT_MUSIC_SYNC_STATUS, method_class.running_sync_jobs
+ )
return wrapped
return wrapper
-class MusicManager():
- ''' several helpers around the musicproviders '''
+class MusicManager:
+ """ several helpers around the musicproviders """
+
def __init__(self, mass):
self.running_sync_jobs = []
self.mass = mass
self.providers = {}
async def setup(self):
- ''' async initialize of module '''
+ """ async initialize of module """
# load providers
await self.load_modules()
# schedule sync task
for player in self.providers[reload_module].players:
await self.mass.players.remove_player(player.player_id)
self.providers.pop(reload_module, None)
- LOGGER.info('Unloaded %s module', reload_module)
+ LOGGER.info("Unloaded %s module", reload_module)
# load all modules (that are not already loaded)
- await load_provider_modules(self.mass, self.providers,
- CONF_KEY_MUSICPROVIDERS)
-
- async def item(self,
- item_id,
- media_type: MediaType,
- provider='database',
- lazy=True):
- ''' get single music item by id and media type'''
+ await load_provider_modules(self.mass, self.providers, CONF_KEY_MUSICPROVIDERS)
+
+ async def item(
+ self, item_id, media_type: MediaType, provider="database", lazy=True
+ ):
+ """ get single music item by id and media type"""
if media_type == MediaType.Artist:
return await self.artist(item_id, provider, lazy=lazy)
elif media_type == MediaType.Album:
else:
return None
- async def library_artists(self, orderby='name',
- provider_filter=None) -> List[Artist]:
- ''' return all library artists, optionally filtered by provider '''
+ async def library_artists(
+ self, orderby="name", provider_filter=None
+ ) -> List[Artist]:
+ """ return all library artists, optionally filtered by provider """
async for item in self.mass.db.library_artists(
- provider=provider_filter, orderby=orderby):
+ provider=provider_filter, orderby=orderby
+ ):
yield item
- async def library_albums(self, orderby='name',
- provider_filter=None) -> List[Album]:
- ''' return all library albums, optionally filtered by provider '''
- async for item in self.mass.db.library_albums(provider=provider_filter,
- orderby=orderby):
+ async def library_albums(self, orderby="name", provider_filter=None) -> List[Album]:
+ """ return all library albums, optionally filtered by provider """
+ async for item in self.mass.db.library_albums(
+ provider=provider_filter, orderby=orderby
+ ):
yield item
- async def library_tracks(self, orderby='name',
- provider_filter=None) -> List[Track]:
- ''' return all library tracks, optionally filtered by provider '''
- async for item in self.mass.db.library_tracks(provider=provider_filter,
- orderby=orderby):
+ async def library_tracks(self, orderby="name", provider_filter=None) -> List[Track]:
+ """ return all library tracks, optionally filtered by provider """
+ async for item in self.mass.db.library_tracks(
+ provider=provider_filter, orderby=orderby
+ ):
yield item
- async def library_playlists(self, orderby='name',
- provider_filter=None) -> List[Playlist]:
- ''' return all library playlists, optionally filtered by provider '''
+ async def library_playlists(
+ self, orderby="name", provider_filter=None
+ ) -> List[Playlist]:
+ """ return all library playlists, optionally filtered by provider """
async for item in self.mass.db.library_playlists(
- provider=provider_filter, orderby=orderby):
+ provider=provider_filter, orderby=orderby
+ ):
yield item
- async def library_radios(self, orderby='name',
- provider_filter=None) -> List[Playlist]:
- ''' return all library radios, optionally filtered by provider '''
- async for item in self.mass.db.library_radios(provider=provider_filter,
- orderby=orderby):
+ async def library_radios(
+ self, orderby="name", provider_filter=None
+ ) -> List[Playlist]:
+ """ return all library radios, optionally filtered by provider """
+ async for item in self.mass.db.library_radios(
+ provider=provider_filter, orderby=orderby
+ ):
yield item
- async def artist(self, item_id, provider='database', lazy=True) -> Artist:
- ''' get artist by id '''
- if not provider or provider == 'database':
+ async def artist(self, item_id, provider="database", lazy=True) -> Artist:
+ """ get artist by id """
+ if not provider or provider == "database":
return await self.mass.db.artist(item_id)
return await self.providers[provider].artist(item_id, lazy=lazy)
- async def album(self, item_id, provider='database', lazy=True) -> Album:
- ''' get album by id '''
- if not provider or provider == 'database':
+ async def album(self, item_id, provider="database", lazy=True) -> Album:
+ """ get album by id """
+ if not provider or provider == "database":
return await self.mass.db.album(item_id)
return await self.providers[provider].album(item_id, lazy=lazy)
- async def track(self,
- item_id,
- provider='database',
- lazy=True,
- track_details=None) -> Track:
- ''' get track by id '''
- if not provider or provider == 'database':
+ async def track(
+ self, item_id, provider="database", lazy=True, track_details=None
+ ) -> Track:
+ """ get track by id """
+ if not provider or provider == "database":
return await self.mass.db.track(item_id)
return await self.providers[provider].track(
- item_id, lazy=lazy, track_details=track_details)
+ item_id, lazy=lazy, track_details=track_details
+ )
- async def playlist(self, item_id, provider='database') -> Playlist:
- ''' get playlist by id '''
- if not provider or provider == 'database':
+ async def playlist(self, item_id, provider="database") -> Playlist:
+ """ get playlist by id """
+ if not provider or provider == "database":
return await self.mass.db.playlist(item_id)
return await self.providers[provider].playlist(item_id)
- async def radio(self, item_id, provider='database') -> Radio:
- ''' get radio by id '''
- if not provider or provider == 'database':
+ async def radio(self, item_id, provider="database") -> Radio:
+ """ get radio by id """
+ if not provider or provider == "database":
return await self.mass.db.radio(item_id)
return await self.providers[provider].radio(item_id)
async def playlist_by_name(self, name) -> Playlist:
- ''' get playlist by name '''
+ """ get playlist by name """
async for playlist in self.library_playlists():
if playlist.name == name:
return playlist
return None
async def radio_by_name(self, name) -> Radio:
- ''' get radio by name '''
+ """ get radio by name """
async for radio in self.library_radios():
if radio.name == name:
return radio
return None
- async def artist_toptracks(self, artist_id,
- provider='database') -> List[Track]:
- ''' get top tracks for given artist '''
+ async def artist_toptracks(self, artist_id, provider="database") -> List[Track]:
+ """ get top tracks for given artist """
track_names = []
artist = await self.artist(artist_id, provider, lazy=False)
# always append database tracks
yield item
track_names.append(item.name + item.version)
for prov_mapping in artist.provider_ids:
- prov_id = prov_mapping['provider']
- prov_item_id = prov_mapping['item_id']
+ prov_id = prov_mapping["provider"]
+ prov_item_id = prov_mapping["item_id"]
prov_obj = self.providers[prov_id]
async for item in prov_obj.artist_toptracks(prov_item_id):
if (item.name + item.version) not in track_names:
yield item
track_names.append(item.name + item.version)
- async def artist_albums(self, artist_id,
- provider='database') -> List[Album]:
- ''' get (all) albums for given artist '''
+ async def artist_albums(self, artist_id, provider="database") -> List[Album]:
+ """ get (all) albums for given artist """
album_names = []
artist = await self.artist(artist_id, provider, lazy=False)
# always append database tracks (if db artist)
yield item
album_names.append(item.name + item.version)
for prov_mapping in artist.provider_ids:
- prov_id = prov_mapping['provider']
- prov_item_id = prov_mapping['item_id']
+ prov_id = prov_mapping["provider"]
+ prov_item_id = prov_mapping["item_id"]
prov_obj = self.providers[prov_id]
async for item in prov_obj.artist_albums(prov_item_id):
if (item.name + item.version) not in album_names:
yield item
album_names.append(item.name + item.version)
- async def album_tracks(self, album_id, provider='database') -> List[Track]:
- ''' get the album tracks for given album '''
+ async def album_tracks(self, album_id, provider="database") -> List[Track]:
+ """ get the album tracks for given album """
album = await self.album(album_id, provider)
# collect the tracks from the first provider
prov = album.provider_ids[0]
- prov_obj = self.providers[prov['provider']]
- async for item in prov_obj.album_tracks(prov['item_id']):
+ prov_obj = self.providers[prov["provider"]]
+ async for item in prov_obj.album_tracks(prov["item_id"]):
yield item
- async def playlist_tracks(self, playlist_id,
- provider='database') -> List[Track]:
- ''' get the tracks for given playlist '''
+ async def playlist_tracks(self, playlist_id, provider="database") -> List[Track]:
+ """ get the tracks for given playlist """
playlist = await self.playlist(playlist_id, provider)
# return playlist tracks from provider
prov = playlist.provider_ids[0]
- async for item in self.providers[prov['provider']].playlist_tracks(
- prov['item_id']):
+ async for item in self.providers[prov["provider"]].playlist_tracks(
+ prov["item_id"]
+ ):
yield item
- async def search(self,
- searchquery,
- media_types: List[MediaType],
- limit=10,
- online=False) -> dict:
- ''' search database or providers '''
+ async def search(
+ self, searchquery, media_types: List[MediaType], limit=10, online=False
+ ) -> dict:
+ """ search database or providers """
# get results from database
result = await self.mass.db.search(searchquery, media_types)
if online:
# include results from music providers
for prov in self.providers.values():
- prov_results = await prov.search(searchquery, media_types,
- limit)
+ prov_results = await prov.search(searchquery, media_types, limit)
for item_type, items in prov_results.items():
if not item_type in result:
result[item_type] = items
result[item_type] += items
# filter out duplicates
for item_type, items in result.items():
- items = list(
- toolz.unique(items, key=operator.attrgetter('item_id')))
+ items = list(toolz.unique(items, key=operator.attrgetter("item_id")))
return result
async def library_add(self, media_items: List[MediaItem]):
- '''Add media item(s) to the library'''
+ """Add media item(s) to the library"""
result = False
for item in media_items:
# make sure we have a database item
- media_item = await self.item(item.item_id,
- item.media_type,
- item.provider,
- lazy=False)
+ media_item = await self.item(
+ item.item_id, item.media_type, item.provider, lazy=False
+ )
if not media_item:
continue
# add to provider's libraries
for prov in item.provider_ids:
- prov_id = prov['provider']
- prov_item_id = prov['item_id']
+ prov_id = prov["provider"]
+ prov_item_id = prov["item_id"]
if prov_id in self.providers:
result = await self.providers[prov_id].add_library(
- prov_item_id, media_item.media_type)
+ prov_item_id, media_item.media_type
+ )
# mark as library item in internal db
- await self.mass.db.add_to_library(media_item.item_id,
- media_item.media_type,
- prov_id)
+ await self.mass.db.add_to_library(
+ media_item.item_id, media_item.media_type, prov_id
+ )
return result
async def library_remove(self, media_items: List[MediaItem]):
- '''Remove media item(s) from the library'''
+ """Remove media item(s) from the library"""
result = False
for item in media_items:
# make sure we have a database item
- media_item = await self.item(item.item_id,
- item.media_type,
- item.provider,
- lazy=False)
+ media_item = await self.item(
+ item.item_id, item.media_type, item.provider, lazy=False
+ )
if not media_item:
continue
# remove from provider's libraries
for prov in item.provider_ids:
- prov_id = prov['provider']
- prov_item_id = prov['item_id']
+ prov_id = prov["provider"]
+ prov_item_id = prov["item_id"]
if prov_id in self.providers:
result = await self.providers[prov_id].remove_library(
- prov_item_id, media_item.media_type)
+ prov_item_id, media_item.media_type
+ )
# mark as library item in internal db
- await self.mass.db.remove_from_library(media_item.item_id,
- media_item.media_type,
- prov_id)
+ await self.mass.db.remove_from_library(
+ media_item.item_id, media_item.media_type, prov_id
+ )
return result
async def add_playlist_tracks(self, db_playlist_id, tracks: List[Track]):
- ''' add tracks to playlist - make sure we dont add dupes '''
+ """ add tracks to playlist - make sure we dont add dupes """
# we can only edit playlists that are in the database (marked as editable)
- playlist = await self.playlist(db_playlist_id, 'database')
+ playlist = await self.playlist(db_playlist_id, "database")
if not playlist or not playlist.is_editable:
return False
# playlist can only have one provider (for now)
playlist_prov = playlist.provider_ids[0]
# grab all existing track ids in the playlist so we can check for duplicates
cur_playlist_track_ids = []
- async for item in self.providers[
- playlist_prov['provider']].playlist_tracks(
- playlist_prov['item_id']):
+ async for item in self.providers[playlist_prov["provider"]].playlist_tracks(
+ playlist_prov["item_id"]
+ ):
cur_playlist_track_ids.append(item.item_id)
- cur_playlist_track_ids += [i['item_id'] for i in item.provider_ids]
+ cur_playlist_track_ids += [i["item_id"] for i in item.provider_ids]
track_ids_to_add = []
for track in tracks:
# check for duplicates
already_exists = track.item_id in cur_playlist_track_ids
for track_prov in track.provider_ids:
- if track_prov['item_id'] in cur_playlist_track_ids:
+ if track_prov["item_id"] in cur_playlist_track_ids:
already_exists = True
if already_exists:
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=operator.itemgetter('quality'),
- reverse=True):
- if track_version['provider'] == playlist_prov['provider']:
- track_ids_to_add.append(track_version['item_id'])
+ for track_version in sorted(
+ track.provider_ids, key=operator.itemgetter("quality"), reverse=True
+ ):
+ if track_version["provider"] == playlist_prov["provider"]:
+ track_ids_to_add.append(track_version["item_id"])
break
- elif playlist_prov['provider'] == 'file':
+ elif playlist_prov["provider"] == "file":
# the file provider can handle uri's from all providers so simply add the uri
uri = f'{track_version["provider"]}://{track_version["item_id"]}'
track_ids_to_add.append(uri)
# actually add the tracks to the playlist on the provider
if track_ids_to_add:
# invalidate cache
- await self.mass.db.update_playlist(playlist.item_id, 'checksum',
- str(time.time()))
+ await self.mass.db.update_playlist(
+ playlist.item_id, "checksum", str(time.time())
+ )
# return result of the action on the provioer
- return await self.providers[playlist_prov['provider']
- ].add_playlist_tracks(
- playlist_prov['item_id'],
- track_ids_to_add)
+ return await self.providers[playlist_prov["provider"]].add_playlist_tracks(
+ playlist_prov["item_id"], track_ids_to_add
+ )
return False
- async def remove_playlist_tracks(self, db_playlist_id,
- tracks: List[Track]):
- ''' remove tracks from playlist '''
+ async def remove_playlist_tracks(self, db_playlist_id, tracks: List[Track]):
+ """ remove tracks from playlist """
# we can only edit playlists that are in the database (marked as editable)
- playlist = await self.playlist(db_playlist_id, 'database')
+ playlist = await self.playlist(db_playlist_id, "database")
if not playlist or not playlist.is_editable:
return False
# playlist can only have one provider (for now)
prov_playlist = playlist.provider_ids[0]
- prov_playlist_playlist_id = prov_playlist['item_id']
- prov_playlist_provider_id = prov_playlist['provider']
+ prov_playlist_playlist_id = prov_playlist["item_id"]
+ prov_playlist_provider_id = prov_playlist["provider"]
track_ids_to_remove = []
for track in tracks:
# a track can contain multiple versions on the same provider, remove all
for track_provider in track.provider_ids:
- if track_provider['provider'] == prov_playlist_provider_id:
- track_ids_to_remove.append(track_provider['item_id'])
+ if track_provider["provider"] == prov_playlist_provider_id:
+ track_ids_to_remove.append(track_provider["item_id"])
# actually remove the tracks from the playlist on the provider
if track_ids_to_remove:
# invalidate cache
- await self.mass.db.update_playlist(playlist.item_id, 'checksum',
- str(time.time()))
- return await self.providers[prov_playlist_provider_id
- ].remove_playlist_tracks(
- prov_playlist_playlist_id,
- track_ids_to_remove)
+ await self.mass.db.update_playlist(
+ playlist.item_id, "checksum", str(time.time())
+ )
+ return await self.providers[
+ prov_playlist_provider_id
+ ].remove_playlist_tracks(prov_playlist_playlist_id, track_ids_to_remove)
@run_periodic(3600 * 3)
async def __sync_music_providers(self):
- ''' periodic sync of all music providers '''
+ """ periodic sync of all music providers """
for prov_id in self.providers:
self.mass.event_loop.create_task(self.sync_music_provider(prov_id))
await self.sync_library_playlists(prov_id)
await self.sync_library_radios(prov_id)
- @sync_task('artists')
+ @sync_task("artists")
async def sync_library_artists(self, prov_id):
- ''' sync library artists for given provider'''
+ """ sync library artists for given provider"""
music_provider = self.providers[prov_id]
prev_db_ids = [
- item.item_id
- async for item in self.library_artists(provider_filter=prov_id)
+ item.item_id async for item in self.library_artists(provider_filter=prov_id)
]
cur_db_ids = []
async for item in music_provider.get_library_artists():
db_item = await music_provider.artist(item.item_id, lazy=False)
cur_db_ids.append(db_item.item_id)
if not db_item.item_id in prev_db_ids:
- await self.mass.db.add_to_library(db_item.item_id,
- MediaType.Artist, prov_id)
+ await self.mass.db.add_to_library(
+ db_item.item_id, MediaType.Artist, prov_id
+ )
# process deletions
for db_id in prev_db_ids:
if db_id not in cur_db_ids:
- await self.mass.db.remove_from_library(db_id, MediaType.Artist,
- prov_id)
+ await self.mass.db.remove_from_library(db_id, MediaType.Artist, prov_id)
- @sync_task('albums')
+ @sync_task("albums")
async def sync_library_albums(self, prov_id):
- ''' sync library albums for given provider'''
+ """ sync library albums for given provider"""
music_provider = self.providers[prov_id]
prev_db_ids = [
- item.item_id
- async for item in self.library_albums(provider_filter=prov_id)
+ item.item_id async for item in self.library_albums(provider_filter=prov_id)
]
cur_db_ids = []
async for item in music_provider.get_library_albums():
- db_album = await music_provider.album(item.item_id,
- album_details=item,
- lazy=False)
+ db_album = await music_provider.album(
+ item.item_id, album_details=item, lazy=False
+ )
if not db_album:
LOGGER.error("provider %s album: %s", prov_id, item.__dict__)
cur_db_ids.append(db_album.item_id)
if not db_album.item_id in prev_db_ids:
- await self.mass.db.add_to_library(db_album.item_id,
- MediaType.Album, prov_id)
+ await self.mass.db.add_to_library(
+ db_album.item_id, MediaType.Album, prov_id
+ )
# precache album tracks
async for item in music_provider.album_tracks(item.item_id):
pass
# process deletions
for db_id in prev_db_ids:
if db_id not in cur_db_ids:
- await self.mass.db.remove_from_library(db_id, MediaType.Album,
- prov_id)
+ await self.mass.db.remove_from_library(db_id, MediaType.Album, prov_id)
- @sync_task('tracks')
+ @sync_task("tracks")
async def sync_library_tracks(self, prov_id):
- ''' sync library tracks for given provider'''
+ """ sync library tracks for given provider"""
music_provider = self.providers[prov_id]
prev_db_ids = [
- item.item_id
- async for item in self.library_tracks(provider_filter=prov_id)
+ item.item_id async for item in self.library_tracks(provider_filter=prov_id)
]
cur_db_ids = []
async for item in music_provider.get_library_tracks():
db_item = await music_provider.track(item.item_id, lazy=False)
cur_db_ids.append(db_item.item_id)
if not db_item.item_id in prev_db_ids:
- await self.mass.db.add_to_library(db_item.item_id,
- MediaType.Track, prov_id)
+ await self.mass.db.add_to_library(
+ db_item.item_id, MediaType.Track, prov_id
+ )
# process deletions
for db_id in prev_db_ids:
if db_id not in cur_db_ids:
- await self.mass.db.remove_from_library(db_id, MediaType.Track,
- prov_id)
+ await self.mass.db.remove_from_library(db_id, MediaType.Track, prov_id)
- @sync_task('playlists')
+ @sync_task("playlists")
async def sync_library_playlists(self, prov_id):
- ''' sync library playlists for given provider'''
+ """ sync library playlists for given provider"""
music_provider = self.providers[prov_id]
prev_db_ids = [
item.item_id
db_id = await self.mass.db.add_playlist(item)
cur_db_ids.append(db_id)
if not db_id in prev_db_ids:
- await self.mass.db.add_to_library(db_id, MediaType.Playlist,
- prov_id)
+ await self.mass.db.add_to_library(db_id, MediaType.Playlist, prov_id)
# precache playlist tracks
async for item in music_provider.playlist_tracks(item.item_id):
pass
# process playlist deletions
for db_id in prev_db_ids:
if db_id not in cur_db_ids:
- await self.mass.db.remove_from_library(db_id,
- MediaType.Playlist,
- prov_id)
+ await self.mass.db.remove_from_library(
+ db_id, MediaType.Playlist, prov_id
+ )
- @sync_task('radios')
+ @sync_task("radios")
async def sync_library_radios(self, prov_id):
- ''' sync library radios for given provider'''
+ """ sync library radios for given provider"""
music_provider = self.providers[prov_id]
prev_db_ids = [
- item.item_id
- async for item in self.library_radios(provider_filter=prov_id)
+ item.item_id async for item in self.library_radios(provider_filter=prov_id)
]
cur_db_ids = []
async for item in music_provider.get_radios():
- db_id = await self.mass.db.get_database_id(prov_id, item.item_id,
- MediaType.Radio)
+ db_id = await self.mass.db.get_database_id(
+ prov_id, item.item_id, MediaType.Radio
+ )
if not db_id:
db_id = await self.mass.db.add_radio(item)
cur_db_ids.append(db_id)
if not db_id in prev_db_ids:
- await self.mass.db.add_to_library(db_id, MediaType.Radio,
- prov_id)
+ await self.mass.db.add_to_library(db_id, MediaType.Radio, prov_id)
# process deletions
for db_id in prev_db_ids:
if db_id not in cur_db_ids:
- await self.mass.db.remove_from_library(db_id, MediaType.Radio,
- prov_id)
-
- async def get_image_thumb(self,
- item_id,
- media_type: MediaType,
- provider,
- size=50):
- ''' get path to (resized) thumb image for given media item '''
- cache_folder = os.path.join(self.mass.datapath, '.thumbs')
- cache_id = f'{item_id}{media_type}{provider}'
- cache_id = base64.b64encode(cache_id.encode('utf-8')).decode('utf-8')
- cache_file_org = os.path.join(cache_folder, f'{cache_id}0.png')
- cache_file_sized = os.path.join(cache_folder, f'{cache_id}{size}.png')
+ await self.mass.db.remove_from_library(db_id, MediaType.Radio, prov_id)
+
+ async def get_image_thumb(self, item_id, media_type: MediaType, provider, size=50):
+ """ get path to (resized) thumb image for given media item """
+ cache_folder = os.path.join(self.mass.datapath, ".thumbs")
+ cache_id = f"{item_id}{media_type}{provider}"
+ cache_id = base64.b64encode(cache_id.encode("utf-8")).decode("utf-8")
+ cache_file_org = os.path.join(cache_folder, f"{cache_id}0.png")
+ cache_file_sized = os.path.join(cache_folder, f"{cache_id}{size}.png")
if os.path.isfile(cache_file_sized):
# return file from cache
return cache_file_sized
# no file in cache so we should get it
- img_url = ''
+ img_url = ""
# we only retrieve items that we already have in cache
item = None
if await self.mass.db.get_database_id(provider, item_id, media_type):
item = await self.item(item_id, media_type, provider)
if not item:
- return ''
- if item and item.metadata.get('image'):
- img_url = item.metadata['image']
+ return ""
+ if item and item.metadata.get("image"):
+ img_url = item.metadata["image"]
elif media_type == MediaType.Track and item.album:
# try album image instead for tracks
- return await self.get_image_thumb(item.album.item_id,
- MediaType.Album,
- item.album.provider, size)
+ return await self.get_image_thumb(
+ item.album.item_id, MediaType.Album, item.album.provider, size
+ )
elif media_type == MediaType.Album and item.artist:
# try artist image instead for albums
- return await self.get_image_thumb(item.artist.item_id,
- MediaType.Artist,
- item.artist.provider, size)
+ return await self.get_image_thumb(
+ item.artist.item_id, MediaType.Artist, item.artist.provider, size
+ )
if not img_url:
return None
# fetch image and store in cache
async with session.get(img_url, verify_ssl=False) as response:
assert response.status == 200
img_data = await response.read()
- with open(cache_file_org, 'wb') as img_file:
+ with open(cache_file_org, "wb") as img_file:
img_file.write(img_data)
if not size:
# return base image
# save resized image
basewidth = size
img = Image.open(cache_file_org)
- wpercent = (basewidth / float(img.size[0]))
+ wpercent = basewidth / float(img.size[0])
hsize = int((float(img.size[1]) * float(wpercent)))
img = img.resize((basewidth, hsize), Image.ANTIALIAS)
img.save(cache_file_sized)
# -*- coding:utf-8 -*-
import asyncio
+import base64
import os
-from typing import List
import sys
import time
-import base64
-import taglib
+from typing import List
-from music_assistant.utils import run_periodic, LOGGER, parse_title_and_version
-from music_assistant.models import MusicProvider, MediaType, TrackQuality, AlbumType, Artist, Album, Track, Playlist
from music_assistant.constants import CONF_ENABLED
+from music_assistant.models.media_types import (
+ Album,
+ AlbumType,
+ Artist,
+ MediaType,
+ Playlist,
+ Track,
+ TrackQuality,
+)
+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'
+PROV_NAME = "Local files and playlists"
+PROV_CLASS = "FileProvider"
CONFIG_ENTRIES = [
(CONF_ENABLED, False, CONF_ENABLED),
- ("music_dir", "", "file_prov_music_path"),
- ("playlists_dir", "", "file_prov_playlists_path")
- ]
+ ("music_dir", "", "file_prov_music_path"),
+ ("playlists_dir", "", "file_prov_playlists_path"),
+]
class FileProvider(MusicProvider):
- '''
+ """
Very basic implementation of a musicprovider for local files
Assumes files are stored on disk in format <artist>/<album>/<track.ext>
Reads ID3 tags from file and falls back to parsing filename
Supports m3u files only for playlists
Supports having URI's from streaming providers within m3u playlist
Should be compatible with LMS
- '''
+ """
+
_music_dir = None
_playlists_dir = None
self._playlists_dir = None
async def search(self, searchstring, media_types=List[MediaType], limit=5):
- ''' perform search on the provider '''
- result = {
- "artists": [],
- "albums": [],
- "tracks": [],
- "playlists": []
- }
+ """ perform search on the provider """
+ result = {"artists": [], "albums": [], "tracks": [], "playlists": []}
return result
-
+
async def get_library_artists(self) -> List[Artist]:
- ''' get artist folders in music directory '''
+ """ get artist folders in music directory """
if not os.path.isdir(self._music_dir):
LOGGER.error("music path does not exist: %s" % self._music_dir)
return
yield
for dirname in os.listdir(self._music_dir):
dirpath = os.path.join(self._music_dir, dirname)
- if os.path.isdir(dirpath) and not dirpath.startswith('.'):
+ if os.path.isdir(dirpath) and not dirpath.startswith("."):
artist = await self.get_artist(dirpath)
if artist:
yield artist
-
+
async def get_library_albums(self) -> List[Album]:
- ''' get album folders recursively '''
+ """ get album folders recursively """
async for artist in self.get_library_artists():
async for album in self.get_artist_albums(artist.item_id):
yield album
async def get_library_tracks(self) -> List[Track]:
- ''' get all tracks recursively '''
- #TODO: support disk subfolders
+ """ get all tracks recursively """
+ # TODO: support disk subfolders
async for album in self.get_library_albums():
async for track in self.get_album_tracks(album.item_id):
yield track
-
+
async def get_library_playlists(self) -> List[Playlist]:
- ''' retrieve playlists from disk '''
+ """ retrieve playlists from disk """
if not self._playlists_dir:
return
yield
for filename in os.listdir(self._playlists_dir):
filepath = os.path.join(self._playlists_dir, filename)
- if os.path.isfile(filepath) and not filename.startswith('.') and filename.lower().endswith('.m3u'):
+ if (
+ os.path.isfile(filepath)
+ and not filename.startswith(".")
+ and filename.lower().endswith(".m3u")
+ ):
playlist = await self.get_playlist(filepath)
if playlist:
yield playlist
async def get_artist(self, prov_item_id) -> Artist:
- ''' get full artist details by id '''
+ """ get full artist details by id """
if not os.sep in prov_item_id:
- itempath = base64.b64decode(prov_item_id).decode('utf-8')
+ itempath = base64.b64decode(prov_item_id).decode("utf-8")
else:
itempath = prov_item_id
- prov_item_id = base64.b64encode(itempath.encode('utf-8')).decode('utf-8')
+ prov_item_id = base64.b64encode(itempath.encode("utf-8")).decode("utf-8")
if not os.path.isdir(itempath):
LOGGER.error("artist path does not exist: %s" % itempath)
return None
artist.item_id = prov_item_id
artist.provider = self.prov_id
artist.name = name
- artist.provider_ids.append({
- "provider": self.prov_id,
- "item_id": artist.item_id
- })
+ artist.provider_ids.append(
+ {"provider": self.prov_id, "item_id": artist.item_id}
+ )
return artist
-
+
async def get_album(self, prov_item_id) -> Album:
- ''' get full album details by id '''
+ """ get full album details by id """
if not os.sep in prov_item_id:
- itempath = base64.b64decode(prov_item_id).decode('utf-8')
+ itempath = base64.b64decode(prov_item_id).decode("utf-8")
else:
itempath = prov_item_id
- prov_item_id = base64.b64encode(itempath.encode('utf-8')).decode('utf-8')
+ prov_item_id = base64.b64encode(itempath.encode("utf-8")).decode("utf-8")
if not os.path.isdir(itempath):
LOGGER.error("album path does not exist: %s" % itempath)
return None
album.artist = await self.get_artist(artistpath)
if not album.artist:
raise Exception("No album artist ! %s" % artistpath)
- album.provider_ids.append({
- "provider": self.prov_id,
- "item_id": prov_item_id
- })
+ album.provider_ids.append({"provider": self.prov_id, "item_id": prov_item_id})
return album
async def get_track(self, prov_item_id) -> Track:
- ''' get full track details by id '''
+ """ get full track details by id """
if not os.sep in prov_item_id:
- itempath = base64.b64decode(prov_item_id).decode('utf-8')
+ itempath = base64.b64decode(prov_item_id).decode("utf-8")
else:
itempath = prov_item_id
if not os.path.isfile(itempath):
return await self.__parse_track(itempath)
async def get_playlist(self, prov_item_id) -> Playlist:
- ''' get full playlist details by id '''
+ """ get full playlist details by id """
if not os.sep in prov_item_id:
- itempath = base64.b64decode(prov_item_id).decode('utf-8')
+ itempath = base64.b64decode(prov_item_id).decode("utf-8")
else:
itempath = prov_item_id
- prov_item_id = base64.b64encode(itempath.encode('utf-8')).decode('utf-8')
+ prov_item_id = base64.b64encode(itempath.encode("utf-8")).decode("utf-8")
if not os.path.isfile(itempath):
LOGGER.error("playlist path does not exist: %s" % itempath)
return None
playlist = Playlist()
playlist.item_id = prov_item_id
playlist.provider = self.prov_id
- playlist.name = itempath.split(os.sep)[-1].replace('.m3u', '')
+ playlist.name = itempath.split(os.sep)[-1].replace(".m3u", "")
playlist.is_editable = True
- playlist.provider_ids.append({
- "provider": self.prov_id,
- "item_id": prov_item_id
- })
- playlist.owner = 'disk'
+ playlist.provider_ids.append(
+ {"provider": self.prov_id, "item_id": prov_item_id}
+ )
+ playlist.owner = "disk"
playlist.checksum = os.path.getmtime(itempath)
return playlist
-
+
async def get_album_tracks(self, prov_album_id) -> List[Track]:
- ''' get album tracks for given album id '''
+ """ get album tracks for given album id """
if not os.sep in prov_album_id:
- albumpath = base64.b64decode(prov_album_id).decode('utf-8')
+ albumpath = base64.b64decode(prov_album_id).decode("utf-8")
else:
albumpath = prov_album_id
if not os.path.isdir(albumpath):
album = await self.get_album(albumpath)
for filename in os.listdir(albumpath):
filepath = os.path.join(albumpath, filename)
- if os.path.isfile(filepath) and not filepath.startswith('.'):
+ if os.path.isfile(filepath) and not filepath.startswith("."):
track = await self.__parse_track(filepath)
if track:
track.album = album
yield track
- async def get_playlist_tracks(self, prov_playlist_id, limit=50, offset=0) -> List[Track]:
- ''' get playlist tracks for given playlist id '''
+ async def get_playlist_tracks(
+ self, prov_playlist_id, limit=50, offset=0
+ ) -> List[Track]:
+ """ get playlist tracks for given playlist id """
if not os.sep in prov_playlist_id:
- itempath = base64.b64decode(prov_playlist_id).decode('utf-8')
+ itempath = base64.b64decode(prov_playlist_id).decode("utf-8")
else:
itempath = prov_playlist_id
if not os.path.isfile(itempath):
with open(itempath) as f:
for line in f.readlines():
line = line.strip()
- if line and not line.startswith('#'):
+ if line and not line.startswith("#"):
counter += 1
if counter > offset:
track = await self.__parse_track_from_uri(line)
break
async def 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 """
if not os.sep in prov_artist_id:
- artistpath = base64.b64decode(prov_artist_id).decode('utf-8')
+ artistpath = base64.b64decode(prov_artist_id).decode("utf-8")
else:
artistpath = prov_artist_id
if not os.path.isdir(artistpath):
return
for dirname in os.listdir(artistpath):
dirpath = os.path.join(artistpath, dirname)
- if os.path.isdir(dirpath) and not dirpath.startswith('.'):
+ if os.path.isdir(dirpath) and not dirpath.startswith("."):
album = await self.get_album(dirpath)
if album:
yield album
async def get_artist_toptracks(self, prov_artist_id) -> List[Track]:
- ''' get a list of random tracks as we have no clue about preference '''
+ """ get a list of random tracks as we have no clue about preference """
async for album in self.get_artist_albums(prov_artist_id):
async for track in self.get_album_tracks(album.item_id):
yield track
async def get_stream_details(self, track_id):
- ''' return the content details for the given track when it will be streamed'''
+ """ return the content details for the given track when it will be streamed"""
if not os.sep in track_id:
- track_id = base64.b64decode(track_id).decode('utf-8')
+ track_id = base64.b64decode(track_id).decode("utf-8")
if not os.path.isfile(track_id):
return None
# TODO: retrieve sanple rate and bitdepth
return {
"type": "file",
"path": track_id,
- "content_type": track_id.split('.')[-1],
+ "content_type": track_id.split(".")[-1],
"sample_rate": 44100,
- "bit_depth": 16
+ "bit_depth": 16,
}
-
+
async def __parse_track(self, filename):
- ''' try to parse a track from a filename with taglib '''
+ """ try to parse a track from a filename with taglib """
track = Track()
try:
song = taglib.File(filename)
except:
- return None # not a media file ?
- prov_item_id = base64.b64encode(filename.encode('utf-8')).decode('utf-8')
+ return None # not a media file ?
+ prov_item_id = base64.b64encode(filename.encode("utf-8")).decode("utf-8")
track.duration = song.length
track.item_id = prov_item_id
track.provider = self.prov_id
- name = song.tags['TITLE'][0]
+ name = song.tags["TITLE"][0]
track.name, track.version = parse_title_and_version(name)
- albumpath = filename.rsplit(os.sep,1)[0]
+ albumpath = filename.rsplit(os.sep, 1)[0]
track.album = await self.get_album(albumpath)
artists = []
- for artist_str in song.tags['ARTIST']:
+ for artist_str in song.tags["ARTIST"]:
local_artist_path = os.path.join(self._music_dir, artist_str)
if os.path.isfile(local_artist_path):
artist = await self.get_artist(local_artist_path)
artist = Artist()
artist.name = artist_str
fake_artistpath = os.path.join(self._music_dir, artist_str)
- artist.item_id = fake_artistpath # temporary id
- artist.provider_ids.append({
+ artist.item_id = fake_artistpath # temporary id
+ artist.provider_ids.append(
+ {
"provider": self.prov_id,
- "item_id": base64.b64encode(fake_artistpath.encode('utf-8')).decode('utf-8')
- })
+ "item_id": base64.b64encode(
+ fake_artistpath.encode("utf-8")
+ ).decode("utf-8"),
+ }
+ )
artists.append(artist)
track.artists = artists
- if 'GENRE' in song.tags:
- track.tags = song.tags['GENRE']
- if 'ISRC' in song.tags:
- track.external_ids.append( {"isrc": song.tags['ISRC'][0]} )
- if 'DISCNUMBER' in song.tags:
- track.disc_number = int(song.tags['DISCNUMBER'][0])
- if 'TRACKNUMBER' in song.tags:
- track.track_number = int(song.tags['TRACKNUMBER'][0])
+ if "GENRE" in song.tags:
+ track.tags = song.tags["GENRE"]
+ if "ISRC" in song.tags:
+ track.external_ids.append({"isrc": song.tags["ISRC"][0]})
+ if "DISCNUMBER" in song.tags:
+ track.disc_number = int(song.tags["DISCNUMBER"][0])
+ if "TRACKNUMBER" in song.tags:
+ track.track_number = int(song.tags["TRACKNUMBER"][0])
quality_details = ""
- if filename.endswith('.flac'):
+ if filename.endswith(".flac"):
# TODO: get bit depth
quality = TrackQuality.FLAC_LOSSLESS
if song.sampleRate > 192000:
quality = TrackQuality.FLAC_LOSSLESS_HI_RES_3
elif song.sampleRate > 48000:
quality = TrackQuality.FLAC_LOSSLESS_HI_RES_2
- quality_details = "%s Khz" % (song.sampleRate/1000)
- elif filename.endswith('.ogg'):
+ quality_details = "%s Khz" % (song.sampleRate / 1000)
+ elif filename.endswith(".ogg"):
quality = TrackQuality.LOSSY_OGG
quality_details = "%s kbps" % (song.bitrate)
- elif filename.endswith('.m4a'):
+ elif filename.endswith(".m4a"):
quality = TrackQuality.LOSSY_AAC
quality_details = "%s kbps" % (song.bitrate)
else:
quality = TrackQuality.LOSSY_MP3
quality_details = "%s kbps" % (song.bitrate)
- track.provider_ids.append({
- "provider": self.prov_id,
- "item_id": prov_item_id,
- "quality": quality,
- "details": quality_details
- })
+ track.provider_ids.append(
+ {
+ "provider": self.prov_id,
+ "item_id": prov_item_id,
+ "quality": quality,
+ "details": quality_details,
+ }
+ )
return track
-
+
async def __parse_track_from_uri(self, uri):
- ''' try to parse a track from an uri found in playlist '''
+ """ try to parse a track from an uri found in playlist """
if "://" in uri:
# track is uri from external provider?
- prov_id = uri.split('://')[0]
- prov_item_id = uri.split('/')[-1].split('.')[0].split(':')[-1]
+ prov_id = uri.split("://")[0]
+ prov_item_id = uri.split("/")[-1].split(".")[0].split(":")[-1]
try:
- return await self.mass.music.providers[prov_id].track(prov_item_id, lazy=False)
+ return await self.mass.music.providers[prov_id].track(
+ prov_item_id, lazy=False
+ )
except Exception as exc:
- LOGGER.warning("Could not parse uri %s to track: %s" %(uri, str(exc)))
+ LOGGER.warning("Could not parse uri %s to track: %s" % (uri, str(exc)))
return None
# try to treat uri as filename
# TODO: filename could be related to musicdir or full path
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
-from typing import List
import datetime
import hashlib
import time
+from typing import List
+
import aiohttp
from asyncio_throttle import Throttler
-
-from music_assistant.utils import LOGGER, parse_title_and_version
from music_assistant.app_vars import get_app_var
-from music_assistant.models.media_types import MediaType, AlbumType, Artist, Album, Track, Playlist, TrackQuality
+from music_assistant.constants import (
+ CONF_ENABLED,
+ CONF_PASSWORD,
+ CONF_TYPE_PASSWORD,
+ CONF_USERNAME,
+ EVENT_PLAYBACK_STOPPED,
+ EVENT_STREAM_STARTED,
+)
+from music_assistant.models.media_types import (
+ Album,
+ AlbumType,
+ Artist,
+ MediaType,
+ Playlist,
+ Track,
+ TrackQuality,
+)
from music_assistant.models.musicprovider import MusicProvider
-from music_assistant.constants import CONF_USERNAME, CONF_PASSWORD, CONF_ENABLED, \
- CONF_TYPE_PASSWORD, EVENT_STREAM_STARTED, EVENT_PLAYBACK_STOPPED
+from music_assistant.utils import LOGGER, parse_title_and_version
-PROV_NAME = 'Qobuz'
-PROV_CLASS = 'QobuzProvider'
+PROV_NAME = "Qobuz"
+PROV_CLASS = "QobuzProvider"
-CONFIG_ENTRIES = [(CONF_ENABLED, False, CONF_ENABLED),
- (CONF_USERNAME, "", CONF_USERNAME),
- (CONF_PASSWORD, CONF_TYPE_PASSWORD, CONF_PASSWORD)]
+CONFIG_ENTRIES = [
+ (CONF_ENABLED, False, CONF_ENABLED),
+ (CONF_USERNAME, "", CONF_USERNAME),
+ (CONF_PASSWORD, CONF_TYPE_PASSWORD, CONF_PASSWORD),
+]
class QobuzProvider(MusicProvider):
__logged_in = None
async def setup(self, conf):
- ''' perform async setup '''
+ """ perform async setup """
self.__username = conf[CONF_USERNAME]
self.__password = conf[CONF_PASSWORD]
if not conf[CONF_USERNAME] or not conf[CONF_PASSWORD]:
self.__user_auth_info = None
self.__logged_in = False
self.http_session = aiohttp.ClientSession(
- loop=self.mass.event_loop, connector=aiohttp.TCPConnector())
+ loop=self.mass.event_loop, connector=aiohttp.TCPConnector()
+ )
self.throttler = Throttler(rate_limit=4, period=1)
- await self.mass.add_event_listener(self.mass_event,
- EVENT_STREAM_STARTED)
- await self.mass.add_event_listener(self.mass_event,
- EVENT_PLAYBACK_STOPPED)
+ await self.mass.add_event_listener(self.mass_event, EVENT_STREAM_STARTED)
+ await self.mass.add_event_listener(self.mass_event, EVENT_PLAYBACK_STOPPED)
async def search(self, searchstring, media_types=List[MediaType], limit=5):
- ''' perform search on the provider '''
+ """ perform search on the provider """
result = {"artists": [], "albums": [], "tracks": [], "playlists": []}
params = {"query": searchstring, "limit": limit}
if len(media_types) == 1:
result["tracks"].append(track)
if "playlists" in searchresult:
for item in searchresult["playlists"]["items"]:
- result["playlists"].append(await
- self.__parse_playlist(item))
+ result["playlists"].append(await self.__parse_playlist(item))
return result
async def get_library_artists(self) -> List[Artist]:
- ''' retrieve all library artists from qobuz '''
- params = {'type': 'artists'}
- endpoint = 'favorite/getUserFavorites'
- async for item in self.__get_all_items(endpoint, params, key='artists'):
+ """ retrieve all library artists from qobuz """
+ params = {"type": "artists"}
+ endpoint = "favorite/getUserFavorites"
+ async for item in self.__get_all_items(endpoint, params, key="artists"):
artist = await self.__parse_artist(item)
if artist:
yield artist
async def get_library_albums(self) -> List[Album]:
- ''' retrieve all library albums from qobuz '''
- params = {'type': 'albums'}
- endpoint = 'favorite/getUserFavorites'
- async for item in self.__get_all_items(endpoint, params, key='albums'):
+ """ retrieve all library albums from qobuz """
+ params = {"type": "albums"}
+ endpoint = "favorite/getUserFavorites"
+ async for item in self.__get_all_items(endpoint, params, key="albums"):
album = await self.__parse_album(item)
if album:
yield album
async def get_library_tracks(self) -> List[Track]:
- ''' retrieve library tracks from qobuz '''
- params = {'type': 'tracks'}
- endpoint = 'favorite/getUserFavorites'
- async for item in self.__get_all_items(endpoint, params, key='tracks'):
+ """ retrieve library tracks from qobuz """
+ params = {"type": "tracks"}
+ endpoint = "favorite/getUserFavorites"
+ async for item in self.__get_all_items(endpoint, params, key="tracks"):
track = await self.__parse_track(item)
if track:
yield track
async def get_library_playlists(self) -> List[Playlist]:
- ''' retrieve all library playlists from the provider '''
- endpoint = 'playlist/getUserPlaylists'
- async for item in self.__get_all_items(endpoint, key='playlists'):
+ """ retrieve all library playlists from the provider """
+ endpoint = "playlist/getUserPlaylists"
+ async for item in self.__get_all_items(endpoint, key="playlists"):
playlist = await self.__parse_playlist(item)
if playlist:
yield playlist
async def get_artist(self, prov_artist_id) -> Artist:
- ''' get full artist details by id '''
- params = {'artist_id': prov_artist_id}
+ """ get full artist details by id """
+ params = {"artist_id": prov_artist_id}
artist_obj = await self.__get_data("artist/get", params)
return await self.__parse_artist(artist_obj)
async def get_album(self, prov_album_id) -> Album:
- ''' get full album details by id '''
- params = {'album_id': prov_album_id}
+ """ get full album details by id """
+ params = {"album_id": prov_album_id}
album_obj = await self.__get_data("album/get", params)
return await self.__parse_album(album_obj)
async def get_track(self, prov_track_id) -> Track:
- ''' get full track details by id '''
- params = {'track_id': prov_track_id}
+ """ get full track details by id """
+ params = {"track_id": prov_track_id}
track_obj = await self.__get_data("track/get", params)
return await self.__parse_track(track_obj)
async def get_playlist(self, prov_playlist_id) -> Playlist:
- ''' get full playlist details by id '''
- params = {'playlist_id': prov_playlist_id}
+ """ get full playlist details by id """
+ params = {"playlist_id": prov_playlist_id}
playlist_obj = await self.__get_data("playlist/get", params)
return await self.__parse_playlist(playlist_obj)
async def get_album_tracks(self, prov_album_id) -> List[Track]:
- ''' get all album tracks for given album id '''
- params = {'album_id': prov_album_id}
- async for item in self.__get_all_items('album/get', params, key='tracks'):
+ """ get all album tracks for given album id """
+ params = {"album_id": prov_album_id}
+ async for item in self.__get_all_items("album/get", params, key="tracks"):
track = await self.__parse_track(item)
if track:
yield track
else:
- LOGGER.warning("Unavailable track found in album %s: %s",
- prov_album_id, item['title'])
+ LOGGER.warning(
+ "Unavailable track found in album %s: %s",
+ prov_album_id,
+ item["title"],
+ )
async def get_playlist_tracks(self, prov_playlist_id) -> List[Track]:
- ''' get all playlist tracks for given playlist id '''
- params = {'playlist_id': prov_playlist_id, 'extra': 'tracks'}
- endpoint = 'playlist/get'
- async for item in self.__get_all_items(endpoint, params, key='tracks'):
+ """ get all playlist tracks for given playlist id """
+ params = {"playlist_id": prov_playlist_id, "extra": "tracks"}
+ endpoint = "playlist/get"
+ async for item in self.__get_all_items(endpoint, params, key="tracks"):
playlist_track = await self.__parse_track(item)
if playlist_track:
yield playlist_track
else:
- LOGGER.warning("Unavailable track found in playlist %s: %s",
- prov_playlist_id, item['title'])
+ LOGGER.warning(
+ "Unavailable track found in playlist %s: %s",
+ prov_playlist_id,
+ item["title"],
+ )
# TODO: should we look for an alternative track version if the original is marked unavailable ?
async def get_artist_albums(self, prov_artist_id) -> List[Album]:
- ''' get a list of albums for the given artist '''
- params = { 'artist_id': prov_artist_id, 'extra': 'albums' }
- endpoint = 'artist/get'
- async for item in self.__get_all_items(endpoint, params, key='albums'):
- if str(item['artist']['id']) == str(prov_artist_id):
+ """ 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.__get_all_items(endpoint, params, key="albums"):
+ if str(item["artist"]["id"]) == str(prov_artist_id):
album = await self.__parse_album(item)
if album:
yield album
async def 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.get_artist(prov_artist_id)
params = {"query": artist.name, "limit": 25, "type": "tracks"}
searchresult = await self.__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.__parse_track(item)
if track:
yield track
async def add_library(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.__get_data('favorite/create',
- {'artist_ids': prov_item_id})
+ result = await self.__get_data(
+ "favorite/create", {"artist_ids": prov_item_id}
+ )
elif media_type == MediaType.Album:
- result = await self.__get_data('favorite/create',
- {'album_ids': prov_item_id})
+ result = await self.__get_data(
+ "favorite/create", {"album_ids": prov_item_id}
+ )
elif media_type == MediaType.Track:
- result = await self.__get_data('favorite/create',
- {'track_ids': prov_item_id})
+ result = await self.__get_data(
+ "favorite/create", {"track_ids": prov_item_id}
+ )
elif media_type == MediaType.Playlist:
- result = await self.__get_data('playlist/subscribe',
- {'playlist_id': prov_item_id})
+ result = await self.__get_data(
+ "playlist/subscribe", {"playlist_id": prov_item_id}
+ )
return result
async def remove_library(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.__get_data('favorite/delete',
- {'artist_ids': prov_item_id})
+ result = await self.__get_data(
+ "favorite/delete", {"artist_ids": prov_item_id}
+ )
elif media_type == MediaType.Album:
- result = await self.__get_data('favorite/delete',
- {'album_ids': prov_item_id})
+ result = await self.__get_data(
+ "favorite/delete", {"album_ids": prov_item_id}
+ )
elif media_type == MediaType.Track:
- result = await self.__get_data('favorite/delete',
- {'track_ids': prov_item_id})
+ result = await self.__get_data(
+ "favorite/delete", {"track_ids": prov_item_id}
+ )
elif media_type == MediaType.Playlist:
playlist = await self.playlist(prov_item_id)
if playlist.is_editable:
- result = await self.__get_data('playlist/delete',
- {'playlist_id': prov_item_id})
+ result = await self.__get_data(
+ "playlist/delete", {"playlist_id": prov_item_id}
+ )
else:
- result = await self.__get_data('playlist/unsubscribe',
- {'playlist_id': prov_item_id})
+ result = await self.__get_data(
+ "playlist/unsubscribe", {"playlist_id": prov_item_id}
+ )
return result
async def 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)
+ "playlist_id": prov_playlist_id,
+ "track_ids": ",".join(prov_track_ids),
}
- return await self.__get_data('playlist/addTracks', params)
+ return await self.__get_data("playlist/addTracks", params)
async def 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.__get_all_items("playlist/get",
- params,
- key='tracks'):
- if track['id'] in prov_track_ids:
- playlist_track_ids.append(track['playlist_track_id'])
+ params = {"playlist_id": prov_playlist_id, "extra": "tracks"}
+ for track in await self.__get_all_items("playlist/get", params, key="tracks"):
+ if track["id"] in prov_track_ids:
+ playlist_track_ids.append(track["playlist_track_id"])
params = {
- 'playlist_id': prov_playlist_id,
- 'track_ids': ",".join(playlist_track_ids)
+ "playlist_id": prov_playlist_id,
+ "track_ids": ",".join(playlist_track_ids),
}
- return await self.__get_data('playlist/deleteTracks', params)
+ return await self.__get_data("playlist/deleteTracks", params)
async def get_stream_details(self, track_id):
- ''' return the content details for the given track when it will be streamed'''
+ """ 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'
- }
- streamdetails = await self.__get_data('track/getFileUrl',
- params,
- sign_request=True)
- if streamdetails and streamdetails.get('url'):
+ params = {"format_id": format_id, "track_id": track_id, "intent": "stream"}
+ streamdetails = await self.__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)
+ if not streamdetails or not streamdetails.get("url"):
+ LOGGER.error("Unable to retrieve stream url for track %s", track_id)
return None
return {
"type": "url",
- "path": streamdetails['url'],
- "content_type": streamdetails['mime_type'].split('/')[1],
- "sample_rate": int(streamdetails['sampling_rate'] * 1000),
- "bit_depth": streamdetails['bit_depth'],
- "details":
- streamdetails # we need these details for reporting playback
+ "path": streamdetails["url"],
+ "content_type": streamdetails["mime_type"].split("/")[1],
+ "sample_rate": int(streamdetails["sampling_rate"] * 1000),
+ "bit_depth": streamdetails["bit_depth"],
+ "details": streamdetails, # we need these details for reporting playback
}
async def mass_event(self, msg, msg_details):
- '''
+ """
received event from mass
we use this to report playback start/stop to qobuz
- '''
+ """
if not self.__user_auth_info:
return
# TODO: need to figure out if the streamed track is purchased by user
- if msg == EVENT_STREAM_STARTED and msg_details[
- "provider"] == self.prov_id:
+ if msg == EVENT_STREAM_STARTED and msg_details["provider"] == self.prov_id:
# report streaming started to qobuz
device_id = self.__user_auth_info["user"]["device"]["id"]
credential_id = self.__user_auth_info["user"]["credential"]["id"]
user_id = self.__user_auth_info["user"]["id"]
format_id = msg_details["details"]["format_id"]
timestamp = int(time.time())
- events = [{
- "online": True,
- "sample": False,
- "intent": "stream",
- "device_id": device_id,
- "track_id": msg_details["item_id"],
- "purchase": False,
- "date": timestamp,
- "credential_id": credential_id,
- "user_id": user_id,
- "local": False,
- "format_id": format_id
- }]
+ events = [
+ {
+ "online": True,
+ "sample": False,
+ "intent": "stream",
+ "device_id": device_id,
+ "track_id": msg_details["item_id"],
+ "purchase": False,
+ "date": timestamp,
+ "credential_id": credential_id,
+ "user_id": user_id,
+ "local": False,
+ "format_id": format_id,
+ }
+ ]
await self.__post_data("track/reportStreamingStart", data=events)
- elif msg == EVENT_PLAYBACK_STOPPED and msg_details[
- "provider"] == self.prov_id:
+ elif msg == EVENT_PLAYBACK_STOPPED and msg_details["provider"] == self.prov_id:
# report streaming ended to qobuz
- if msg_details.get('msg_details',0) < 6:
+ if msg_details.get("msg_details", 0) < 6:
return
user_id = self.__user_auth_info["user"]["id"]
params = {
- 'user_id': user_id,
- 'track_id': msg_details["item_id"],
- 'duration': int(msg_details["seconds_played"])
+ "user_id": user_id,
+ "track_id": msg_details["item_id"],
+ "duration": int(msg_details["seconds_played"]),
}
- await self.__get_data('/track/reportStreamingEnd', params)
+ await self.__get_data("/track/reportStreamingEnd", params)
async def __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'):
+ if not artist_obj or not artist_obj.get("id"):
return None
- artist.item_id = artist_obj['id']
+ artist.item_id = artist_obj["id"]
artist.provider = self.prov_id
- artist.provider_ids.append({
- "provider": self.prov_id,
- "item_id": artist_obj['id']
- })
- artist.name = artist_obj['name']
- 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]:
- artist.metadata["image"] = artist_obj['image'][key]
+ artist.provider_ids.append(
+ {"provider": self.prov_id, "item_id": artist_obj["id"]}
+ )
+ artist.name = artist_obj["name"]
+ 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]
+ ):
+ artist.metadata["image"] = artist_obj["image"][key]
break
- if artist_obj.get('biography'):
- artist.metadata["biography"] = artist_obj['biography'].get(
- 'content', '')
- if artist_obj.get('url'):
- artist.metadata["qobuz_url"] = artist_obj['url']
+ if artist_obj.get("biography"):
+ artist.metadata["biography"] = artist_obj["biography"].get("content", "")
+ if artist_obj.get("url"):
+ artist.metadata["qobuz_url"] = artist_obj["url"]
return artist
async def __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 or not album_obj.get('id') or not album_obj[
- "streamable"] or not album_obj["displayable"]:
+ if (
+ not album_obj
+ or not album_obj.get("id")
+ or not album_obj["streamable"]
+ or not album_obj["displayable"]
+ ):
# do not return unavailable items
return None
- album.item_id = album_obj['id']
+ album.item_id = album_obj["id"]
album.provider = self.prov_id
- if album_obj['maximum_sampling_rate'] > 192:
+ if album_obj["maximum_sampling_rate"] > 192:
quality = TrackQuality.FLAC_LOSSLESS_HI_RES_4
- elif album_obj['maximum_sampling_rate'] > 96:
+ elif album_obj["maximum_sampling_rate"] > 96:
quality = TrackQuality.FLAC_LOSSLESS_HI_RES_3
- elif album_obj['maximum_sampling_rate'] > 48:
+ elif album_obj["maximum_sampling_rate"] > 48:
quality = TrackQuality.FLAC_LOSSLESS_HI_RES_2
- elif album_obj['maximum_bit_depth'] > 16:
+ elif album_obj["maximum_bit_depth"] > 16:
quality = TrackQuality.FLAC_LOSSLESS_HI_RES_1
- elif album_obj.get('format_id', 0) == 5:
+ elif album_obj.get("format_id", 0) == 5:
quality = TrackQuality.LOSSY_AAC
else:
quality = TrackQuality.FLAC_LOSSLESS
- album.provider_ids.append({
- "provider":
- self.prov_id,
- "item_id":
- album_obj['id'],
- "quality":
- quality,
- "details":
- "%skHz %sbit" % (album_obj['maximum_sampling_rate'],
- album_obj['maximum_bit_depth'])
- })
+ album.provider_ids.append(
+ {
+ "provider": self.prov_id,
+ "item_id": album_obj["id"],
+ "quality": quality,
+ "details": "%skHz %sbit"
+ % (album_obj["maximum_sampling_rate"], album_obj["maximum_bit_depth"]),
+ }
+ )
album.name, album.version = parse_title_and_version(
- album_obj['title'], album_obj.get('version'))
- album.artist = await self.__parse_artist(album_obj['artist'])
- if album_obj.get('product_type', '') == 'single':
+ album_obj["title"], album_obj.get("version")
+ )
+ album.artist = await self.__parse_artist(album_obj["artist"])
+ if album_obj.get("product_type", "") == "single":
album.albumtype = AlbumType.Single
- elif album_obj.get(
- 'product_type', ''
- ) == 'compilation' or 'Various' in album_obj['artist']['name']:
+ elif (
+ album_obj.get("product_type", "") == "compilation"
+ or "Various" in album_obj["artist"]["name"]
+ ):
album.albumtype = AlbumType.Compilation
else:
album.albumtype = AlbumType.Album
- if 'genre' in album_obj:
- album.tags = [album_obj['genre']['name']]
- if album_obj.get('image'):
- for key in ['extralarge', 'large', 'medium', 'small']:
- if album_obj['image'].get(key):
- album.metadata["image"] = album_obj['image'][key]
+ if "genre" in album_obj:
+ album.tags = [album_obj["genre"]["name"]]
+ if album_obj.get("image"):
+ for key in ["extralarge", "large", "medium", "small"]:
+ if album_obj["image"].get(key):
+ album.metadata["image"] = album_obj["image"][key]
break
- if len(album_obj['upc']) == 13:
+ if len(album_obj["upc"]) == 13:
# qobuz writes ean as upc ?!
- album.external_ids.append({"ean": album_obj['upc']})
- album.external_ids.append({"upc": album_obj['upc'][1:]})
+ album.external_ids.append({"ean": album_obj["upc"]})
+ album.external_ids.append({"upc": album_obj["upc"][1:]})
else:
- album.external_ids.append({"upc": album_obj['upc']})
- if 'label' in album_obj:
- album.labels = album_obj['label']['name'].split('/')
- if album_obj.get('released_at'):
- album.year = datetime.datetime.fromtimestamp(
- album_obj['released_at']).year
- if album_obj.get('copyright'):
- album.metadata["copyright"] = album_obj['copyright']
- if album_obj.get('hires'):
+ album.external_ids.append({"upc": album_obj["upc"]})
+ if "label" in album_obj:
+ album.labels = album_obj["label"]["name"].split("/")
+ if album_obj.get("released_at"):
+ album.year = datetime.datetime.fromtimestamp(album_obj["released_at"]).year
+ if album_obj.get("copyright"):
+ album.metadata["copyright"] = album_obj["copyright"]
+ if album_obj.get("hires"):
album.metadata["hires"] = "true"
- if album_obj.get('url'):
- album.metadata["qobuz_url"] = album_obj['url']
- if album_obj.get('description'):
- album.metadata["description"] = album_obj['description']
+ if album_obj.get("url"):
+ album.metadata["qobuz_url"] = album_obj["url"]
+ if album_obj.get("description"):
+ album.metadata["description"] = album_obj["description"]
return album
async def __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 or not track_obj.get('id') or not track_obj[
- "streamable"] or not track_obj["displayable"]:
+ if (
+ not track_obj
+ or not track_obj.get("id")
+ or not track_obj["streamable"]
+ or not track_obj["displayable"]
+ ):
# do not return unavailable items
return None
- track.item_id = track_obj['id']
+ track.item_id = track_obj["id"]
track.provider = self.prov_id
- if track_obj.get(
- 'performer') and not 'Various ' in track_obj['performer']:
- artist = await self.__parse_artist(track_obj['performer'])
+ if track_obj.get("performer") and not "Various " in track_obj["performer"]:
+ artist = await self.__parse_artist(track_obj["performer"])
if artist:
track.artists.append(artist)
if not track.artists:
# try to grab artist from album
- if track_obj.get('album') and track_obj['album'].get(
- 'artist'
- ) and not 'Various ' in track_obj['album']['artist']:
- artist = await self.__parse_artist(track_obj['album']['artist']
- )
+ if (
+ track_obj.get("album")
+ and track_obj["album"].get("artist")
+ and not "Various " in track_obj["album"]["artist"]
+ ):
+ artist = await self.__parse_artist(track_obj["album"]["artist"])
if artist:
track.artists.append(artist)
if not track.artists:
# last resort: parse from performers string
- for performer_str in track_obj['performers'].split(' - '):
- role = performer_str.split(', ')[1]
- name = performer_str.split(', ')[0]
- if 'artist' in role.lower():
+ for performer_str in track_obj["performers"].split(" - "):
+ role = performer_str.split(", ")[1]
+ name = performer_str.split(", ")[0]
+ if "artist" in role.lower():
artist = Artist()
artist.name = name
artist.item_id = name
track.artists.append(artist)
# TODO: fix grabbing composer from details
track.name, track.version = parse_title_and_version(
- track_obj['title'], track_obj.get('version'))
- track.duration = track_obj['duration']
- if 'album' in track_obj:
- album = await self.__parse_album(track_obj['album'])
+ track_obj["title"], track_obj.get("version")
+ )
+ track.duration = track_obj["duration"]
+ if "album" in track_obj:
+ album = await self.__parse_album(track_obj["album"])
if album:
track.album = album
- track.disc_number = track_obj['media_number']
- track.track_number = track_obj['track_number']
- if track_obj.get('hires'):
+ track.disc_number = track_obj["media_number"]
+ track.track_number = track_obj["track_number"]
+ if track_obj.get("hires"):
track.metadata["hires"] = "true"
- if track_obj.get('url'):
- track.metadata["qobuz_url"] = track_obj['url']
- if track_obj.get('isrc'):
- track.external_ids.append({"isrc": track_obj['isrc']})
- if track_obj.get('performers'):
- track.metadata["performers"] = track_obj['performers']
- if track_obj.get('copyright'):
- track.metadata["copyright"] = track_obj['copyright']
+ if track_obj.get("url"):
+ track.metadata["qobuz_url"] = track_obj["url"]
+ if track_obj.get("isrc"):
+ track.external_ids.append({"isrc": track_obj["isrc"]})
+ if track_obj.get("performers"):
+ track.metadata["performers"] = track_obj["performers"]
+ if track_obj.get("copyright"):
+ track.metadata["copyright"] = track_obj["copyright"]
# get track quality
- if track_obj['maximum_sampling_rate'] > 192:
+ if track_obj["maximum_sampling_rate"] > 192:
quality = TrackQuality.FLAC_LOSSLESS_HI_RES_4
- elif track_obj['maximum_sampling_rate'] > 96:
+ elif track_obj["maximum_sampling_rate"] > 96:
quality = TrackQuality.FLAC_LOSSLESS_HI_RES_3
- elif track_obj['maximum_sampling_rate'] > 48:
+ elif track_obj["maximum_sampling_rate"] > 48:
quality = TrackQuality.FLAC_LOSSLESS_HI_RES_2
- elif track_obj['maximum_bit_depth'] > 16:
+ elif track_obj["maximum_bit_depth"] > 16:
quality = TrackQuality.FLAC_LOSSLESS_HI_RES_1
- elif track_obj.get('format_id', 0) == 5:
+ elif track_obj.get("format_id", 0) == 5:
quality = TrackQuality.LOSSY_AAC
else:
quality = TrackQuality.FLAC_LOSSLESS
- track.provider_ids.append({
- "provider":
- self.prov_id,
- "item_id":
- track_obj['id'],
- "quality":
- quality,
- "details":
- "%skHz %sbit" % (track_obj['maximum_sampling_rate'],
- track_obj['maximum_bit_depth'])
- })
+ track.provider_ids.append(
+ {
+ "provider": self.prov_id,
+ "item_id": track_obj["id"],
+ "quality": quality,
+ "details": "%skHz %sbit"
+ % (track_obj["maximum_sampling_rate"], track_obj["maximum_bit_depth"]),
+ }
+ )
return track
async def __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'):
+ if not playlist_obj or not playlist_obj.get("id"):
return None
- playlist.item_id = playlist_obj['id']
+ playlist.item_id = playlist_obj["id"]
playlist.provider = self.prov_id
- playlist.provider_ids.append({
- "provider": self.prov_id,
- "item_id": playlist_obj['id']
- })
- playlist.name = playlist_obj['name']
- playlist.owner = playlist_obj['owner']['name']
- playlist.is_editable = playlist_obj['owner'][
- 'id'] == self.__user_auth_info["user"]["id"] or playlist_obj[
- 'is_collaborative']
- if playlist_obj.get('images300'):
- playlist.metadata["image"] = playlist_obj['images300'][0]
- if playlist_obj.get('url'):
- playlist.metadata["qobuz_url"] = playlist_obj['url']
- playlist.checksum = playlist_obj['updated_at']
+ playlist.provider_ids.append(
+ {"provider": self.prov_id, "item_id": playlist_obj["id"]}
+ )
+ playlist.name = playlist_obj["name"]
+ playlist.owner = playlist_obj["owner"]["name"]
+ playlist.is_editable = (
+ playlist_obj["owner"]["id"] == self.__user_auth_info["user"]["id"]
+ or playlist_obj["is_collaborative"]
+ )
+ if playlist_obj.get("images300"):
+ playlist.metadata["image"] = playlist_obj["images300"][0]
+ if playlist_obj.get("url"):
+ playlist.metadata["qobuz_url"] = playlist_obj["url"]
+ playlist.checksum = playlist_obj["updated_at"]
return playlist
async def __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 = {
"username": self.__username,
"password": self.__password,
- "device_manufacturer_id": "music_assistant"
+ "device_manufacturer_id": "music_assistant",
}
details = await self.__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 __get_all_items(self, endpoint, params=None, key='tracks'):
- ''' get all items from a paged list '''
+ async def __get_all_items(self, endpoint, params=None, key="tracks"):
+ """ get all items from a paged list """
if not params:
params = {}
limit = 50
offset += limit
if not result or not key in result or not "items" in result[key]:
break
- for item in result[key]['items']:
+ for item in result[key]["items"]:
yield item
- if len(result[key]['items']) < limit:
+ if len(result[key]["items"]) < limit:
break
async def __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
headers = {"X-App-Id": get_app_var(0)}
- if endpoint != 'user/login':
+ if endpoint != "user/login":
auth_token = await self.__auth_token()
if not auth_token:
LOGGER.debug("Not logged in")
return None
headers["X-User-Auth-Token"] = auth_token
if sign_request:
- signing_data = "".join(endpoint.split('/'))
+ signing_data = "".join(endpoint.split("/"))
keys = list(params.keys())
keys.sort()
for key in keys:
params["app_id"] = get_app_var(0)
params["user_auth_token"] = await self.__auth_token()
async with self.throttler:
- async with self.http_session.get(url,
- headers=headers,
- params=params,
- verify_ssl=False) as response:
+ async with self.http_session.get(
+ 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']):
- LOGGER.error('%s - %s', endpoint, result)
+ 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 __post_data(self, endpoint, params=None, data=None):
- ''' post data to api'''
+ """ post data to api"""
if not params:
params = {}
if not data:
url = "http://www.qobuz.com/api.json/0.2/%s" % endpoint
params["app_id"] = get_app_var(0)
params["user_auth_token"] = await self.__auth_token()
- async with self.http_session.post(url,
- params=params,
- json=data,
- verify_ssl=False) as response:
+ async with self.http_session.post(
+ 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']):
- LOGGER.error('%s - %s', endpoint, result)
+ if "error" in result or (
+ "status" in result and "error" in result["status"]
+ ):
+ LOGGER.error("%s - %s", endpoint, result)
return None
return result
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
+import asyncio
import os
-from typing import List
-import time
-import subprocess
import platform
-import asyncio
-from asyncio_throttle import Throttler
-import aiohttp
+import subprocess
+import time
+from typing import List
-from music_assistant.utils import LOGGER, parse_title_and_version, json
+import aiohttp
+from asyncio_throttle import Throttler
from music_assistant.app_vars import get_app_var
-from music_assistant.models.media_types import MediaType, AlbumType, Artist, Album, Track, Playlist, TrackQuality
+from music_assistant.constants import (
+ CONF_ENABLED,
+ CONF_PASSWORD,
+ CONF_TYPE_PASSWORD,
+ CONF_USERNAME,
+)
+from music_assistant.models.media_types import (
+ Album,
+ AlbumType,
+ Artist,
+ MediaType,
+ Playlist,
+ Track,
+ TrackQuality,
+)
from music_assistant.models.musicprovider import MusicProvider
-from music_assistant.constants import CONF_USERNAME, CONF_PASSWORD, CONF_ENABLED, CONF_TYPE_PASSWORD
+from music_assistant.utils import LOGGER, json, parse_title_and_version
-PROV_NAME = 'Spotify'
-PROV_CLASS = 'SpotifyProvider'
+PROV_NAME = "Spotify"
+PROV_CLASS = "SpotifyProvider"
-CONFIG_ENTRIES = [(CONF_ENABLED, False, CONF_ENABLED),
- (CONF_USERNAME, "", CONF_USERNAME),
- (CONF_PASSWORD, CONF_TYPE_PASSWORD, CONF_PASSWORD)]
+CONFIG_ENTRIES = [
+ (CONF_ENABLED, False, CONF_ENABLED),
+ (CONF_USERNAME, "", CONF_USERNAME),
+ (CONF_PASSWORD, CONF_TYPE_PASSWORD, CONF_PASSWORD),
+]
class SpotifyProvider(MusicProvider):
sp_user = None
async def setup(self, conf):
- ''' perform async setup '''
+ """ perform async setup """
self._cur_user = None
if not conf[CONF_USERNAME] or not conf[CONF_PASSWORD]:
raise Exception("Username and password must not be empty")
self.__auth_token = {}
self.throttler = Throttler(rate_limit=4, period=1)
self.http_session = aiohttp.ClientSession(
- loop=self.mass.event_loop, connector=aiohttp.TCPConnector())
+ loop=self.mass.event_loop, connector=aiohttp.TCPConnector()
+ )
async def search(self, searchstring, media_types=List[MediaType], limit=5):
- ''' perform search on the provider '''
+ """ perform search on the provider """
result = {"artists": [], "albums": [], "tracks": [], "playlists": []}
searchtypes = []
if MediaType.Artist in media_types:
searchtypes.append("playlist")
searchtype = ",".join(searchtypes)
params = {"q": searchstring, "type": searchtype, "limit": limit}
- searchresult = await self.__get_data("search",
- params=params)
+ searchresult = await self.__get_data("search", params=params)
if searchresult:
if "artists" in searchresult:
for item in searchresult["artists"]["items"]:
return result
async def get_library_artists(self) -> List[Artist]:
- ''' retrieve library artists from spotify '''
- spotify_artists = await self.__get_data(
- "me/following?type=artist&limit=50")
+ """ retrieve library artists from spotify """
+ spotify_artists = await self.__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']:
+ for artist_obj in spotify_artists["artists"]["items"]:
prov_artist = await self.__parse_artist(artist_obj)
yield prov_artist
async def get_library_albums(self) -> List[Album]:
- ''' retrieve library albums from the provider '''
+ """ retrieve library albums from the provider """
async for item in self.__get_all_items("me/albums"):
album = await self.__parse_album(item)
if album:
yield album
async def get_library_tracks(self) -> List[Track]:
- ''' retrieve library tracks from the provider '''
+ """ retrieve library tracks from the provider """
async for item in self.__get_all_items("me/tracks"):
track = await self.__parse_track(item)
if track:
yield track
async def get_library_playlists(self) -> List[Playlist]:
- ''' retrieve playlists from the provider '''
+ """ retrieve playlists from the provider """
async for item in self.__get_all_items("me/playlists"):
playlist = await self.__parse_playlist(item)
if playlist:
yield playlist
async def get_artist(self, prov_artist_id) -> Artist:
- ''' get full artist details by id '''
+ """ get full artist details by id """
artist_obj = await self.__get_data("artists/%s" % prov_artist_id)
return await self.__parse_artist(artist_obj)
async def get_album(self, prov_album_id) -> Album:
- ''' get full album details by id '''
+ """ get full album details by id """
album_obj = await self.__get_data("albums/%s" % prov_album_id)
return await self.__parse_album(album_obj)
async def get_track(self, prov_track_id) -> Track:
- ''' get full track details by id '''
+ """ get full track details by id """
track_obj = await self.__get_data("tracks/%s" % prov_track_id)
return await self.__parse_track(track_obj)
async def get_playlist(self, prov_playlist_id) -> Playlist:
- ''' get full playlist details by id '''
- playlist_obj = await self.__get_data(f'playlists/{prov_playlist_id}')
+ """ get full playlist details by id """
+ playlist_obj = await self.__get_data(f"playlists/{prov_playlist_id}")
return await self.__parse_playlist(playlist_obj)
async def 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.__get_all_items(endpoint):
track = await self.__parse_track(track_obj)
yield track
async def 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.__get_all_items(endpoint):
playlist_track = await self.__parse_track(track_obj)
if playlist_track:
yield playlist_track
else:
- LOGGER.warning("Unavailable track found in playlist %s: %s",
- prov_playlist_id,
- track_obj['track']['name'])
+ LOGGER.warning(
+ "Unavailable track found in playlist %s: %s",
+ prov_playlist_id,
+ track_obj["track"]["name"],
+ )
async def get_artist_albums(self, prov_artist_id) -> List[Album]:
- ''' get a list of all albums for the given artist '''
- params = {'include_groups': 'album,single,compilation'}
- endpoint = f'artists/{prov_artist_id}/albums'
+ """ 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.__get_all_items(endpoint, params):
album = await self.__parse_album(item)
if album:
yield album
async def 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.get_artist(prov_artist_id)
- endpoint = f'artists/{prov_artist_id}/top-tracks'
+ endpoint = f"artists/{prov_artist_id}/top-tracks"
items = await self.__get_data(endpoint)
- for item in items['tracks']:
+ for item in items["tracks"]:
track = await self.__parse_track(item)
if track:
track.artists = [artist]
yield track
async def add_library(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.__put_data('me/following', {
- 'ids': prov_item_id,
- 'type': 'artist'
- })
+ result = await self.__put_data(
+ "me/following", {"ids": prov_item_id, "type": "artist"}
+ )
elif media_type == MediaType.Album:
- result = await self.__put_data('me/albums', {'ids': prov_item_id})
+ result = await self.__put_data("me/albums", {"ids": prov_item_id})
elif media_type == MediaType.Track:
- result = await self.__put_data('me/tracks', {'ids': prov_item_id})
+ result = await self.__put_data("me/tracks", {"ids": prov_item_id})
elif media_type == MediaType.Playlist:
- result = await self.__put_data(f'playlists/{prov_item_id}/followers',
- data={'public': False})
+ result = await self.__put_data(
+ f"playlists/{prov_item_id}/followers", data={"public": False}
+ )
return result
async def remove_library(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.__delete_data('me/following', {
- 'ids': prov_item_id,
- 'type': 'artist'
- })
+ result = await self.__delete_data(
+ "me/following", {"ids": prov_item_id, "type": "artist"}
+ )
elif media_type == MediaType.Album:
- result = await self.__delete_data('me/albums',
- {'ids': prov_item_id})
+ result = await self.__delete_data("me/albums", {"ids": prov_item_id})
elif media_type == MediaType.Track:
- result = await self.__delete_data('me/tracks',
- {'ids': prov_item_id})
+ result = await self.__delete_data("me/tracks", {"ids": prov_item_id})
elif media_type == MediaType.Playlist:
- result = await self.__delete_data(f'playlists/{prov_item_id}/followers')
+ result = await self.__delete_data(f"playlists/{prov_item_id}/followers")
return result
async def 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.__post_data(f'playlists/{prov_playlist_id}/tracks',
- data=data)
+ return await self.__post_data(f"playlists/{prov_playlist_id}/tracks", data=data)
async def 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.__delete_data(f'playlists/{prov_playlist_id}/tracks',
- data=data)
+ data = {"tracks": track_uris}
+ return await self.__delete_data(
+ f"playlists/{prov_playlist_id}/tracks", data=data
+ )
async def get_stream_details(self, track_id):
- ''' return the content details for the given track when it will be streamed'''
+ """ return the content details for the given track when it will be streamed"""
# make sure a valid track is requested
track = await self.get_track(track_id)
if not track:
await self.get_token()
spotty = self.get_spotty_binary()
spotty_exec = '%s -n temp -c "%s" --pass-through --single-track %s' % (
- spotty, self.mass.datapath, track.item_id)
+ spotty,
+ self.mass.datapath,
+ track.item_id,
+ )
return {
"type": "executable",
"path": spotty_exec,
"sample_rate": 44100,
"bit_depth": 16,
"provider": self.prov_id,
- "item_id": track.item_id
+ "item_id": track.item_id,
}
async def __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.item_id = artist_obj["id"]
artist.provider = self.prov_id
- artist.provider_ids.append({
- "provider": self.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:
+ artist.provider_ids.append(
+ {"provider": self.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:
artist.metadata["image"] = img_url
break
- if artist_obj.get('external_urls'):
- artist.metadata["spotify_url"] = artist_obj['external_urls'][
- 'spotify']
+ if artist_obj.get("external_urls"):
+ artist.metadata["spotify_url"] = artist_obj["external_urls"]["spotify"]
return artist
async def __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_obj = album_obj['album']
- if not album_obj['id'] or not album_obj.get('is_playable', True):
+ if "album" in album_obj:
+ album_obj = album_obj["album"]
+ if not album_obj["id"] or not album_obj.get("is_playable", True):
return None
album = Album()
- album.item_id = album_obj['id']
+ album.item_id = album_obj["id"]
album.provider = self.prov_id
- album.name, album.version = parse_title_and_version(album_obj['name'])
- for artist in album_obj['artists']:
+ album.name, album.version = parse_title_and_version(album_obj["name"])
+ for artist in album_obj["artists"]:
album.artist = await self.__parse_artist(artist)
if album.artist:
break
- if album_obj['album_type'] == 'single':
+ if album_obj["album_type"] == "single":
album.albumtype = AlbumType.Single
- elif album_obj['album_type'] == 'compilation':
+ elif album_obj["album_type"] == "compilation":
album.albumtype = AlbumType.Compilation
else:
album.albumtype = AlbumType.Album
- if 'genres' in album_obj:
- album.tags = album_obj['genres']
- if album_obj.get('images'):
- album.metadata["image"] = album_obj['images'][0]['url']
- if 'external_ids' in album_obj:
- for key, value in album_obj['external_ids'].items():
+ if "genres" in album_obj:
+ album.tags = album_obj["genres"]
+ if album_obj.get("images"):
+ album.metadata["image"] = album_obj["images"][0]["url"]
+ if "external_ids" in album_obj:
+ for key, value in album_obj["external_ids"].items():
album.external_ids.append({key: value})
- if 'label' in album_obj:
- album.labels = album_obj['label'].split('/')
- if album_obj.get('release_date'):
- album.year = int(album_obj['release_date'].split('-')[0])
- if album_obj.get('copyrights'):
- album.metadata["copyright"] = album_obj['copyrights'][0]['text']
- if album_obj.get('external_urls'):
- album.metadata["spotify_url"] = album_obj['external_urls'][
- 'spotify']
- if album_obj.get('explicit'):
- album.metadata['explicit'] = str(album_obj['explicit']).lower()
- album.provider_ids.append({
- "provider": self.prov_id,
- "item_id": album_obj['id'],
- "quality": TrackQuality.LOSSY_OGG
- })
+ if "label" in album_obj:
+ album.labels = album_obj["label"].split("/")
+ if album_obj.get("release_date"):
+ album.year = int(album_obj["release_date"].split("-")[0])
+ if album_obj.get("copyrights"):
+ album.metadata["copyright"] = album_obj["copyrights"][0]["text"]
+ if album_obj.get("external_urls"):
+ album.metadata["spotify_url"] = album_obj["external_urls"]["spotify"]
+ if album_obj.get("explicit"):
+ album.metadata["explicit"] = str(album_obj["explicit"]).lower()
+ album.provider_ids.append(
+ {
+ "provider": self.prov_id,
+ "item_id": album_obj["id"],
+ "quality": TrackQuality.LOSSY_OGG,
+ }
+ )
return album
async def __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_obj = track_obj['track']
- if track_obj['is_local'] or not track_obj['id'] or not track_obj[
- 'is_playable']:
+ if "track" in track_obj:
+ track_obj = track_obj["track"]
+ if track_obj["is_local"] or not track_obj["id"] or not track_obj["is_playable"]:
# do not return unavailable items
return None
track = Track()
- track.item_id = track_obj['id']
+ track.item_id = track_obj["id"]
track.provider = self.prov_id
- for track_artist in track_obj['artists']:
+ for track_artist in track_obj["artists"]:
artist = await self.__parse_artist(track_artist)
if artist:
track.artists.append(artist)
- track.name, track.version = parse_title_and_version(track_obj['name'])
- track.duration = track_obj['duration_ms'] / 1000
- track.metadata['explicit'] = str(track_obj['explicit']).lower()
- if 'external_ids' in track_obj:
- for key, value in track_obj['external_ids'].items():
+ track.name, track.version = parse_title_and_version(track_obj["name"])
+ track.duration = track_obj["duration_ms"] / 1000
+ track.metadata["explicit"] = str(track_obj["explicit"]).lower()
+ if "external_ids" in track_obj:
+ for key, value in track_obj["external_ids"].items():
track.external_ids.append({key: value})
- if 'album' in track_obj:
- track.album = await self.__parse_album(track_obj['album'])
- if track_obj.get('copyright'):
- track.metadata["copyright"] = track_obj['copyright']
- if track_obj.get('explicit'):
+ if "album" in track_obj:
+ track.album = await self.__parse_album(track_obj["album"])
+ if track_obj.get("copyright"):
+ track.metadata["copyright"] = track_obj["copyright"]
+ if track_obj.get("explicit"):
track.metadata["explicit"] = True
- track.disc_number = track_obj['disc_number']
- track.track_number = track_obj['track_number']
- if track_obj.get('external_urls'):
- track.metadata["spotify_url"] = track_obj['external_urls'][
- 'spotify']
- track.provider_ids.append({
- "provider": self.prov_id,
- "item_id": track_obj['id'],
- "quality": TrackQuality.LOSSY_OGG
- })
+ track.disc_number = track_obj["disc_number"]
+ track.track_number = track_obj["track_number"]
+ if track_obj.get("external_urls"):
+ track.metadata["spotify_url"] = track_obj["external_urls"]["spotify"]
+ track.provider_ids.append(
+ {
+ "provider": self.prov_id,
+ "item_id": track_obj["id"],
+ "quality": TrackQuality.LOSSY_OGG,
+ }
+ )
return track
async def __parse_playlist(self, playlist_obj):
- ''' parse spotify playlist object to generic layout '''
+ """ parse spotify playlist object to generic layout """
playlist = Playlist()
- if not playlist_obj.get('id'):
+ if not playlist_obj.get("id"):
return None
- playlist.item_id = playlist_obj['id']
+ playlist.item_id = playlist_obj["id"]
playlist.provider = self.prov_id
- playlist.provider_ids.append({
- "provider": self.prov_id,
- "item_id": playlist_obj['id']
- })
- 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']
- if playlist_obj.get('images'):
- playlist.metadata["image"] = playlist_obj['images'][0]['url']
- if playlist_obj.get('external_urls'):
- playlist.metadata["spotify_url"] = playlist_obj['external_urls'][
- 'spotify']
- playlist.checksum = playlist_obj['snapshot_id']
+ playlist.provider_ids.append(
+ {"provider": self.prov_id, "item_id": playlist_obj["id"]}
+ )
+ 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"]
+ )
+ if playlist_obj.get("images"):
+ playlist.metadata["image"] = playlist_obj["images"][0]["url"]
+ if playlist_obj.get("external_urls"):
+ playlist.metadata["spotify_url"] = playlist_obj["external_urls"]["spotify"]
+ playlist.checksum = playlist_obj["snapshot_id"]
return playlist
async def 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
# retrieve token with spotty
- tokeninfo = await self.mass.event_loop.run_in_executor(
- None, self.__get_token)
+ tokeninfo = await self.mass.event_loop.run_in_executor(None, self.__get_token)
if tokeninfo:
self.__auth_token = tokeninfo
self.sp_user = await self.__get_data("me")
- LOGGER.info("Succesfully logged in to Spotify as %s",
- self.sp_user["id"])
+ LOGGER.info("Succesfully logged in to Spotify as %s", self.sp_user["id"])
self.__auth_token = tokeninfo
else:
- raise Exception("Can't get Spotify token for user %s" %
- self._username)
+ raise Exception("Can't get Spotify token for user %s" % self._username)
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", "user-read-currently-playing",
- "user-modify-playback-state", "playlist-read-private",
- "playlist-read-collaborative", "playlist-modify-public",
- "playlist-modify-private", "user-follow-modify",
- "user-follow-read", "user-library-read", "user-library-modify",
- "user-read-private", "user-read-email", "user-read-birthdate",
- "user-top-read"
+ "user-read-playback-state",
+ "user-read-currently-playing",
+ "user-modify-playback-state",
+ "playlist-read-private",
+ "playlist-read-collaborative",
+ "playlist-modify-public",
+ "playlist-modify-private",
+ "user-follow-modify",
+ "user-follow-read",
+ "user-library-read",
+ "user-library-modify",
+ "user-read-private",
+ "user-read-email",
+ "user-read-birthdate",
+ "user-top-read",
]
scope = ",".join(scopes)
args = [
- self.get_spotty_binary(), "-t", "--client-id",
- get_app_var(2), "--scope", scope, "-n", "temp-spotty", "-u",
- self._username, "-p", self._password, "-c", self.mass.datapath,
- "--disable-discovery"
+ self.get_spotty_binary(),
+ "-t",
+ "--client-id",
+ get_app_var(2),
+ "--scope",
+ scope,
+ "-n",
+ "temp-spotty",
+ "-u",
+ self._username,
+ "-p",
+ self._password,
+ "-c",
+ self.mass.datapath,
+ "--disable-discovery",
]
- spotty = subprocess.Popen(args,
- stdout=asyncio.subprocess.PIPE,
- stderr=asyncio.subprocess.STDOUT)
+ spotty = subprocess.Popen(
+ args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT
+ )
stdout, stderr = spotty.communicate()
result = json.loads(stdout)
# transform token info to spotipy compatible format
if result and "accessToken" in result:
tokeninfo = result
- tokeninfo['expiresAt'] = tokeninfo['expiresIn'] + int(time.time())
+ tokeninfo["expiresAt"] = tokeninfo["expiresIn"] + int(time.time())
return tokeninfo
- async def __get_all_items(self, endpoint, params=None, key='items'):
- ''' get all items from a paged list '''
+ async def __get_all_items(self, endpoint, params=None, key="items"):
+ """ get all items from a paged list """
if not params:
params = {}
limit = 50
break
async def __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
- params['market'] = 'from_token'
- params['country'] = 'from_token'
+ url = "https://api.spotify.com/v1/%s" % endpoint
+ params["market"] = "from_token"
+ params["country"] = "from_token"
token = await self.get_token()
- headers = {'Authorization': 'Bearer %s' % token["accessToken"]}
+ headers = {"Authorization": "Bearer %s" % token["accessToken"]}
async with self.throttler:
- async with self.http_session.get(url,
- headers=headers,
- params=params,
- verify_ssl=False) as response:
+ async with self.http_session.get(
+ url, headers=headers, params=params, verify_ssl=False
+ ) as response:
result = await response.json()
- if not result or 'error' in result:
- LOGGER.error('%s - %s', endpoint, result)
+ if not result or "error" in result:
+ LOGGER.error("%s - %s", endpoint, result)
result = None
return result
async def __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
+ url = "https://api.spotify.com/v1/%s" % endpoint
token = await self.get_token()
- headers = {'Authorization': 'Bearer %s' % token["accessToken"]}
- async with self.http_session.delete(url,
- headers=headers,
- params=params,
- json=data,
- verify_ssl=False) as response:
+ headers = {"Authorization": "Bearer %s" % token["accessToken"]}
+ async with self.http_session.delete(
+ url, headers=headers, params=params, json=data, verify_ssl=False
+ ) as response:
return await response.text()
async def __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
+ url = "https://api.spotify.com/v1/%s" % endpoint
token = await self.get_token()
- headers = {'Authorization': 'Bearer %s' % token["accessToken"]}
- async with self.http_session.put(url,
- headers=headers,
- params=params,
- json=data,
- verify_ssl=False) as response:
+ headers = {"Authorization": "Bearer %s" % token["accessToken"]}
+ async with self.http_session.put(
+ url, headers=headers, params=params, json=data, verify_ssl=False
+ ) as response:
return await response.text()
async def __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
+ url = "https://api.spotify.com/v1/%s" % endpoint
token = await self.get_token()
- headers = {'Authorization': 'Bearer %s' % token["accessToken"]}
- async with self.http_session.post(url,
- headers=headers,
- params=params,
- json=data,
- verify_ssl=False) as response:
+ headers = {"Authorization": "Bearer %s" % token["accessToken"]}
+ async with self.http_session.post(
+ url, headers=headers, params=params, json=data, verify_ssl=False
+ ) as response:
return await response.text()
@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()
- if architecture.startswith('AMD64') or architecture.startswith(
- 'x86_64'):
+ if architecture.startswith("AMD64") or architecture.startswith("x86_64"):
# generic linux x86_64 binary
- sp_binary = os.path.join(os.path.dirname(__file__), "spotty",
- "x86-linux", "spotty-x86_64")
+ sp_binary = os.path.join(
+ os.path.dirname(__file__), "spotty", "x86-linux", "spotty-x86_64"
+ )
else:
- sp_binary = os.path.join(os.path.dirname(__file__), "spotty",
- "arm-linux", "spotty-muslhf")
+ sp_binary = os.path.join(
+ os.path.dirname(__file__), "spotty", "arm-linux", "spotty-muslhf"
+ )
return sp_binary
# -*- coding:utf-8 -*-
from typing import List
-from asyncio_throttle import Throttler
-import aiohttp
-from music_assistant.utils import LOGGER
-from music_assistant.models.media_types import MediaType, TrackQuality, Radio
+import aiohttp
+from asyncio_throttle import Throttler
+from music_assistant.constants import (
+ CONF_ENABLED,
+ CONF_PASSWORD,
+ CONF_TYPE_PASSWORD,
+ CONF_USERNAME,
+)
+from music_assistant.models.media_types import MediaType, Radio, TrackQuality
from music_assistant.models.musicprovider import MusicProvider
-from music_assistant.constants import CONF_USERNAME, CONF_PASSWORD, CONF_ENABLED, CONF_TYPE_PASSWORD
-
+from music_assistant.utils import LOGGER
-PROV_NAME = 'TuneIn Radio'
-PROV_CLASS = 'TuneInProvider'
+PROV_NAME = "TuneIn Radio"
+PROV_CLASS = "TuneInProvider"
CONFIG_ENTRIES = [
(CONF_ENABLED, False, CONF_ENABLED),
- (CONF_USERNAME, "", CONF_USERNAME),
- (CONF_PASSWORD, CONF_TYPE_PASSWORD, CONF_PASSWORD)
- ]
+ (CONF_USERNAME, "", CONF_USERNAME),
+ (CONF_PASSWORD, CONF_TYPE_PASSWORD, CONF_PASSWORD),
+]
+
class TuneInProvider(MusicProvider):
throttler = None
async def setup(self, conf):
- ''' perform async setup '''
+ """ perform async setup """
if not conf[CONF_USERNAME] or not conf[CONF_PASSWORD]:
raise Exception("Username and password must not be empty")
self._username = conf[CONF_USERNAME]
self._password = conf[CONF_PASSWORD]
self.http_session = aiohttp.ClientSession(
- loop=self.mass.event_loop, connector=aiohttp.TCPConnector())
+ loop=self.mass.event_loop, connector=aiohttp.TCPConnector()
+ )
self.throttler = Throttler(rate_limit=1, period=1)
async def search(self, searchstring, media_types=List[MediaType], limit=5):
- ''' perform search on the provider '''
+ """ perform search on the provider """
result = {
"artists": [],
"albums": [],
"tracks": [],
"playlists": [],
- "radios": []
+ "radios": [],
}
return result
async def get_radios(self):
- ''' get favorited/library radio stations '''
+ """ get favorited/library radio stations """
params = {"c": "presets"}
result = await self.__get_data("Browse.ashx", params)
if result and "body" in result:
yield radio
async def get_radio(self, radio_id):
- ''' get radio station details '''
+ """ get radio station details """
radio = None
params = {"c": "composite", "detail": "listing", "id": radio_id}
result = await self.__get_data("Describe.ashx", params)
return radio
async def __parse_radio(self, details):
- ''' parse Radio object from json obj returned from api '''
+ """ parse Radio object from json obj returned from api """
radio = Radio()
- radio.item_id = details['preset_id']
+ radio.item_id = details["preset_id"]
radio.provider = self.prov_id
if "name" in details:
radio.name = details["name"]
# parse stream urls and format
stream_info = await self.__get_stream_urls(radio.item_id)
for stream in stream_info["body"]:
- if stream["media_type"] == 'aac':
+ if stream["media_type"] == "aac":
quality = TrackQuality.LOSSY_AAC
- elif stream["media_type"] == 'ogg':
+ elif stream["media_type"] == "ogg":
quality = TrackQuality.LOSSY_OGG
else:
quality = TrackQuality.LOSSY_MP3
- radio.provider_ids.append({
- "provider": self.prov_id,
- "item_id": "%s--%s" % (details['preset_id'], stream["media_type"]),
- "quality": quality,
- "details": stream['url']
- })
+ radio.provider_ids.append(
+ {
+ "provider": self.prov_id,
+ "item_id": "%s--%s" % (details["preset_id"], stream["media_type"]),
+ "quality": quality,
+ "details": stream["url"],
+ }
+ )
# image
if "image" in details:
radio.metadata["image"] = details["image"]
return radio
async def __get_stream_urls(self, radio_id):
- ''' get the stream urls for the given radio id '''
+ """ get the stream urls for the given radio id """
params = {"id": radio_id}
res = await self.__get_data("Tune.ashx", params)
return res
async def get_stream_details(self, stream_id):
- ''' return the content details for the given track when it will be streamed'''
- radio_id = stream_id.split('--')[0]
- if len(stream_id.split('--')) > 1:
- media_type = stream_id.split('--')[1]
+ """ return the content details for the given track when it will be streamed"""
+ radio_id = stream_id.split("--")[0]
+ if len(stream_id.split("--")) > 1:
+ media_type = stream_id.split("--")[1]
else:
- media_type = ''
+ media_type = ""
stream_info = await self.__get_stream_urls(radio_id)
for stream in stream_info["body"]:
- if stream['media_type'] == media_type or not media_type:
+ if stream["media_type"] == media_type or not media_type:
return {
"type": "url",
- "path": stream['url'],
- "content_type": stream['media_type'],
+ "path": stream["url"],
+ "content_type": stream["media_type"],
"sample_rate": 44100,
- "bit_depth": 16
+ "bit_depth": 16,
}
return {}
-
+
async def __get_data(self, endpoint, params={}):
- ''' get data from api'''
- url = 'https://opml.radiotime.com/%s' % endpoint
- params['render'] = 'json'
- params['formats'] = 'ogg,aac,wma,mp3'
- params['username'] = self._username
- params['partnerId'] = '1'
+ """ get data from api"""
+ 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:
+ if not result or "error" in result:
LOGGER.error(url)
LOGGER.error(params)
result = None
return result
-
-
\ No newline at end of file
import os
from typing import List
-from music_assistant.constants import CONF_KEY_PLAYERPROVIDERS, EVENT_PLAYER_ADDED, \
- EVENT_PLAYER_REMOVED, EVENT_HASS_ENTITY_CHANGED
-from music_assistant.utils import LOGGER, load_provider_modules, iter_items
+from music_assistant.constants import (
+ CONF_KEY_PLAYERPROVIDERS,
+ EVENT_HASS_ENTITY_CHANGED,
+ EVENT_PLAYER_ADDED,
+ EVENT_PLAYER_REMOVED,
+)
from music_assistant.models.media_types import MediaItem, MediaType
-from music_assistant.models.player_queue import QueueItem, QueueOption
from music_assistant.models.player import Player
+from music_assistant.models.player_queue import QueueItem, QueueOption
+from music_assistant.utils import LOGGER, iter_items, load_provider_modules
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
MODULES_PATH = os.path.join(BASE_DIR, "playerproviders")
-class PlayerManager():
+class PlayerManager:
""" several helpers to handle playback through player providers """
+
def __init__(self, mass):
self.mass = mass
self._players = {}
# load providers
await self.load_modules()
# register state listener
- await self.mass.add_event_listener(self.handle_mass_events,
- EVENT_HASS_ENTITY_CHANGED)
+ await self.mass.add_event_listener(
+ self.handle_mass_events, EVENT_HASS_ENTITY_CHANGED
+ )
async def load_modules(self, reload_module=None):
"""Dynamically (un)load musicprovider modules."""
if reload_module and reload_module in self.providers:
# unload existing module
- if hasattr(self.providers[reload_module], 'http_session'):
+ if hasattr(self.providers[reload_module], "http_session"):
await self.providers[reload_module].http_session.close()
self.providers.pop(reload_module, None)
- LOGGER.info('Unloaded %s module', reload_module)
+ LOGGER.info("Unloaded %s module", reload_module)
# load all modules (that are not already loaded)
- await load_provider_modules(self.mass, self.providers,
- CONF_KEY_PLAYERPROVIDERS)
+ await load_provider_modules(self.mass, self.providers, CONF_KEY_PLAYERPROVIDERS)
@property
def players(self):
self._players[player.player_id] = player
await self.mass.signal_event(EVENT_PLAYER_ADDED, player.to_dict())
# TODO: turn on player if it was previously turned on ?
- LOGGER.info("New player added: %s/%s", player.player_provider,
- player.player_id)
+ LOGGER.info("New player added: %s/%s", player.player_provider, player.player_id)
return player
async def remove_player(self, player_id: str):
""" handle a player remove """
self._players.pop(player_id, None)
- await self.mass.signal_event(EVENT_PLAYER_REMOVED,
- {"player_id": player_id})
+ await self.mass.signal_event(EVENT_PLAYER_REMOVED, {"player_id": player_id})
LOGGER.info("Player removed: %s", player_id)
async def trigger_update(self, player_id: str):
if player_id in self._players:
await self._players[player_id].update(force=True)
- async def play_media(self,
- player_id: str,
- media_items: List[MediaItem],
- queue_opt=QueueOption.Play):
+ async def play_media(
+ self, player_id: str, media_items: List[MediaItem], queue_opt=QueueOption.Play
+ ):
"""
play media item(s) on the given player
:param media_item: media item(s) that should be played (single item or list of items)
# collect tracks to play
if media_item.media_type == MediaType.Artist:
tracks = self.mass.music.artist_toptracks(
- media_item.item_id, provider=media_item.provider)
+ media_item.item_id, provider=media_item.provider
+ )
elif media_item.media_type == MediaType.Album:
tracks = self.mass.music.album_tracks(
- media_item.item_id, provider=media_item.provider)
+ media_item.item_id, provider=media_item.provider
+ )
elif media_item.media_type == MediaType.Playlist:
tracks = self.mass.music.playlist_tracks(
- media_item.item_id, provider=media_item.provider)
+ media_item.item_id, provider=media_item.provider
+ )
else:
tracks = iter_items(media_item) # single track
async for track in tracks:
queue_item = QueueItem(track)
# generate uri for this queue item
- queue_item.uri = 'http://%s:%s/stream/%s/%s' % (
- self.mass.web.local_ip, self.mass.web.http_port, player_id,
- queue_item.queue_item_id)
+ queue_item.uri = "http://%s:%s/stream/%s/%s" % (
+ self.mass.web.local_ip,
+ self.mass.web.http_port,
+ player_id,
+ queue_item.queue_item_id,
+ )
queue_items.append(queue_item)
# load items into the queue
- if (queue_opt == QueueOption.Replace
- or (len(queue_items) > 10
- and queue_opt in [QueueOption.Play, QueueOption.Next])):
+ if queue_opt == QueueOption.Replace or (
+ len(queue_items) > 10 and queue_opt in [QueueOption.Play, QueueOption.Next]
+ ):
return await player.queue.load(queue_items)
elif queue_opt == QueueOption.Next:
return await player.queue.insert(queue_items, 1)
player_ids = list(self._players.keys())
for player_id in player_ids:
player = self._players[player_id]
- if (msg_details['entity_id'] == player.settings.get(
- 'hass_power_entity') or msg_details['entity_id'] ==
- player.settings.get('hass_volume_entity')):
+ if msg_details["entity_id"] == player.settings.get(
+ "hass_power_entity"
+ ) or msg_details["entity_id"] == player.settings.get(
+ "hass_volume_entity"
+ ):
await player.update()
async def get_gain_correct(self, player_id, item_id, provider_id):
""" get gain correction for given player / track combination """
player = self._players[player_id]
- if not player.settings['volume_normalisation']:
+ if not player.settings["volume_normalisation"]:
return 0
- target_gain = int(player.settings['target_volume'])
- fallback_gain = int(player.settings['fallback_gain_correct'])
- track_loudness = await self.mass.db.get_track_loudness(
- item_id, provider_id)
+ target_gain = int(player.settings["target_volume"])
+ fallback_gain = int(player.settings["fallback_gain_correct"])
+ track_loudness = await self.mass.db.get_track_loudness(item_id, provider_id)
if track_loudness is None:
gain_correct = fallback_gain
else:
gain_correct = round(gain_correct, 2)
LOGGER.debug(
"Loudness level for track %s/%s is %s - calculated replayGain is %s",
- provider_id, item_id, track_loudness, gain_correct)
+ provider_id,
+ item_id,
+ track_loudness,
+ gain_correct,
+ )
return gain_correct
# -*- coding:utf-8 -*-
import asyncio
-import aiohttp
-from typing import List
import logging
-import pychromecast
-from pychromecast.controllers.multizone import MultizoneController
-from pychromecast.socket_client import CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_DISCONNECTED
-import types
import time
+import types
+from typing import List
import uuid
-from music_assistant.utils import run_periodic, LOGGER, try_parse_int
-from music_assistant.models.playerprovider import PlayerProvider
-from music_assistant.models.player import Player, PlayerState
-from music_assistant.models.playerstate import PlayerState
-from music_assistant.models.player_queue import QueueItem, PlayerQueue
+import aiohttp
from music_assistant.constants import CONF_ENABLED, CONF_HOSTNAME, CONF_PORT
+from music_assistant.models.player import Player, PlayerState
+from music_assistant.models.player_queue import PlayerQueue, QueueItem
+from music_assistant.models.playerprovider import PlayerProvider
+from music_assistant.utils import LOGGER, run_periodic, try_parse_int
+import pychromecast
+from pychromecast.controllers.multizone import MultizoneController
+from pychromecast.socket_client import (
+ CONNECTION_STATUS_CONNECTED,
+ CONNECTION_STATUS_DISCONNECTED,
+)
+
+PROV_ID = "chromecast"
+PROV_NAME = "Chromecast"
+PROV_CLASS = "ChromecastProvider"
-PROV_ID = 'chromecast'
-PROV_NAME = 'Chromecast'
-PROV_CLASS = 'ChromecastProvider'
+CONFIG_ENTRIES = [(CONF_ENABLED, True, CONF_ENABLED)]
-CONFIG_ENTRIES = [
- (CONF_ENABLED, True, CONF_ENABLED),
- ]
+PLAYER_CONFIG_ENTRIES = [("gapless_enabled", False, "gapless_enabled")]
-PLAYER_CONFIG_ENTRIES = [
- ("gapless_enabled", False, "gapless_enabled"),
- ]
class ChromecastPlayer(Player):
- ''' Chromecast player object '''
-
+ """ Chromecast player object """
+
def __init__(self, *args, **kwargs):
self.__cc_report_progress_task = None
super().__init__(*args, **kwargs)
if self.__cc_report_progress_task:
self.__cc_report_progress_task.cancel()
- async def try_chromecast_command(self, cmd:types.MethodType, *args, **kwargs):
- ''' guard for disconnected socket client '''
- def _try_chromecast_command(_cmd:types.MethodType, *_args, **_kwargs):
+ async def try_chromecast_command(self, cmd: types.MethodType, *args, **kwargs):
+ """ guard for disconnected socket client """
+
+ def _try_chromecast_command(_cmd: types.MethodType, *_args, **_kwargs):
try:
_cmd(*_args, **_kwargs)
except (pychromecast.error.NotConnected, AttributeError):
LOGGER.warning("Chromecast %s is not connected!" % self.name)
except Exception as exc:
LOGGER.warning(exc)
+
return self.mass.event_loop.call_soon_threadsafe(
- _try_chromecast_command, cmd, *args, **kwargs)
-
+ _try_chromecast_command, cmd, *args, **kwargs
+ )
+
async def cmd_stop(self):
- ''' send stop command to player '''
+ """ send stop command to player """
await self.try_chromecast_command(self.cc.media_controller.stop)
async def cmd_play(self):
- ''' send play command to player '''
+ """ send play command to player """
await self.try_chromecast_command(self.cc.media_controller.play)
async def cmd_pause(self):
- ''' send pause command to player '''
+ """ send pause command to player """
await self.try_chromecast_command(self.cc.media_controller.pause)
async def cmd_next(self):
- ''' send next track command to player '''
+ """ send next track command to player """
await self.try_chromecast_command(self.cc.media_controller.queue_next)
async def cmd_previous(self):
- ''' [CAN OVERRIDE] send previous track command to player '''
+ """ [CAN OVERRIDE] send previous track command to player """
await self.try_chromecast_command(self.cc.media_controller.queue_prev)
-
+
async def cmd_power_on(self):
- ''' send power ON command to player '''
+ """ send power ON command to player """
self.powered = True
async def cmd_power_off(self):
- ''' send power OFF command to player '''
+ """ send power OFF command to player """
self.powered = False
async def cmd_volume_set(self, volume_level):
- ''' send new volume level command to player '''
- await self.try_chromecast_command(self.cc.set_volume, volume_level/100)
+ """ send new volume level command to player """
+ await self.try_chromecast_command(self.cc.set_volume, volume_level / 100)
self.volume_level = volume_level
async def cmd_volume_mute(self, is_muted=False):
- ''' send mute command to player '''
+ """ send mute command to player """
await self.try_chromecast_command(self.cc.set_volume_muted, is_muted)
- async def cmd_play_uri(self, uri:str):
- ''' play single uri on player '''
+ async def cmd_play_uri(self, uri: str):
+ """ play single uri on player """
if self.queue.use_queue_stream:
# create CC queue so that skip and previous will work
queue_item = QueueItem()
queue_item.uri = uri
return await self.cmd_queue_load([queue_item, queue_item])
else:
- await self.try_chromecast_command(self.cc.play_media, uri, 'audio/flac')
+ await self.try_chromecast_command(self.cc.play_media, uri, "audio/flac")
- async def cmd_queue_load(self, queue_items:List[QueueItem]):
- ''' load (overwrite) queue with new items '''
+ async def cmd_queue_load(self, queue_items: List[QueueItem]):
+ """ load (overwrite) queue with new items """
cc_queue_items = await self.__create_queue_items(queue_items[:50])
- queuedata = {
- "type": 'QUEUE_LOAD',
- "repeatMode": "REPEAT_ALL" if self.queue.repeat_enabled else "REPEAT_OFF",
- "shuffle": False, # handled by our queue controller
- "queueType": "PLAYLIST",
- "startIndex": 0, # Item index to play after this request or keep same item if undefined
- "items": cc_queue_items # only load 50 tracks at once or the socket will crash
+ queuedata = {
+ "type": "QUEUE_LOAD",
+ "repeatMode": "REPEAT_ALL" if self.queue.repeat_enabled else "REPEAT_OFF",
+ "shuffle": False, # handled by our queue controller
+ "queueType": "PLAYLIST",
+ "startIndex": 0, # Item index to play after this request or keep same item if undefined
+ "items": cc_queue_items, # only load 50 tracks at once or the socket will crash
}
await self.try_chromecast_command(self.__send_player_queue, queuedata)
await asyncio.sleep(0.2)
await self.cmd_queue_append(queue_items[51:])
await asyncio.sleep(0.2)
- async def cmd_queue_insert(self, queue_items:List[QueueItem], insert_at_index):
+ async def cmd_queue_insert(self, queue_items: List[QueueItem], insert_at_index):
# for now we don't support this as google requires a special internal id
# as item id to determine the insert position
# https://developers.google.com/cast/docs/reference/caf_receiver/cast.framework.QueueManager#insertItems
raise NotImplementedError
- async def cmd_queue_append(self, queue_items:List[QueueItem]):
- '''
+ async def cmd_queue_append(self, queue_items: List[QueueItem]):
+ """
append new items at the end of the queue
- '''
+ """
cc_queue_items = await self.__create_queue_items(queue_items)
for chunk in chunks(cc_queue_items, 50):
- queuedata = {
- "type": 'QUEUE_INSERT',
- "insertBefore": None,
- "items": chunk
- }
+ queuedata = {"type": "QUEUE_INSERT", "insertBefore": None, "items": chunk}
await self.try_chromecast_command(self.__send_player_queue, queuedata)
async 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 = await self.__create_queue_item(track)
return queue_items
async def __create_queue_item(self, track):
- '''create CC queue item from track info '''
+ """create CC queue item from track info """
return {
- 'opt_itemId': track.queue_item_id,
- 'autoplay' : True,
- 'preloadTime' : 10,
- 'playbackDuration': int(track.duration),
- 'startTime' : 0,
- 'activeTrackIds' : [],
- 'media': {
- 'contentId': track.uri,
- 'customData': {
- 'provider': track.provider,
- 'uri': track.uri,
- 'item_id': track.queue_item_id
+ "opt_itemId": track.queue_item_id,
+ "autoplay": True,
+ "preloadTime": 10,
+ "playbackDuration": int(track.duration),
+ "startTime": 0,
+ "activeTrackIds": [],
+ "media": {
+ "contentId": track.uri,
+ "customData": {
+ "provider": track.provider,
+ "uri": track.uri,
+ "item_id": track.queue_item_id,
},
- 'contentType': "audio/flac",
- 'streamType': 'LIVE' if self.queue.use_queue_stream else 'BUFFERED',
- 'metadata': {
- 'title': track.name,
- 'artist': track.artists[0].name if track.artists else "",
+ "contentType": "audio/flac",
+ "streamType": "LIVE" if self.queue.use_queue_stream else "BUFFERED",
+ "metadata": {
+ "title": track.name,
+ "artist": track.artists[0].name if track.artists else "",
},
- 'duration': int(track.duration)
- }
+ "duration": int(track.duration),
+ },
}
-
+
def __send_player_queue(self, queuedata):
- '''send new data to the CC queue'''
+ """send new data to the CC queue"""
media_controller = self.cc.media_controller
receiver_ctrl = media_controller._socket_client.receiver_controller
+
def send_queue():
"""Plays media after chromecast has switched to requested app."""
- queuedata['mediaSessionId'] = media_controller.status.media_session_id
+ queuedata["mediaSessionId"] = media_controller.status.media_session_id
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()
async def __report_progress(self):
- ''' report current progress while playing '''
+ """ 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.cc.media_controller.status.adjusted_current_time
await asyncio.sleep(1)
self.__cc_report_progress_task = None
-
- async def handle_player_state(self, caststatus=None,
- mediastatus=None):
- ''' handle a player state message from the socket '''
+
+ async def 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.name = self.cc.name
# handle media status
if mediastatus:
- if mediastatus.player_state in ['PLAYING', 'BUFFERING']:
+ if mediastatus.player_state in ["PLAYING", "BUFFERING"]:
self.state = PlayerState.Playing
self.powered = True
- elif mediastatus.player_state == 'PAUSED':
+ elif mediastatus.player_state == "PAUSED":
self.state = PlayerState.Paused
else:
self.state = PlayerState.Stopped
self.cur_uri = mediastatus.content_id
self.cur_time = mediastatus.adjusted_current_time
- if self._state == PlayerState.Playing and self.__cc_report_progress_task == None:
- self.__cc_report_progress_task = self.mass.event_loop.create_task(self.__report_progress())
+ if (
+ self._state == PlayerState.Playing
+ and self.__cc_report_progress_task == None
+ ):
+ self.__cc_report_progress_task = self.mass.event_loop.create_task(
+ self.__report_progress()
+ )
+
class ChromecastProvider(PlayerProvider):
- ''' support for ChromeCast Audio '''
+ """ support for ChromeCast Audio """
+
_discovery_running = False
-
+
async def setup(self, conf):
- ''' perform async setup '''
+ """ perform async setup """
self._discovery_running = False
- logging.getLogger('pychromecast').setLevel(logging.WARNING)
+ logging.getLogger("pychromecast").setLevel(logging.WARNING)
self.player_config_entries = PLAYER_CONFIG_ENTRIES
- self.mass.event_loop.create_task(
- self.__periodic_chromecast_discovery())
+ self.mass.event_loop.create_task(self.__periodic_chromecast_discovery())
- async def __handle_group_members_update(self, mz, added_player=None, removed_player=None):
- ''' handle callback from multizone manager '''
+ async def __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:
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)
-
+
@run_periodic(1800)
async def __periodic_chromecast_discovery(self):
- ''' run chromecast discovery on interval '''
+ """ run chromecast discovery on interval """
self.mass.event_loop.run_in_executor(None, self.run_chromecast_discovery)
def run_chromecast_discovery(self):
- ''' background non-blocking chromecast discovery and handler '''
+ """ background non-blocking chromecast discovery and handler """
if self._discovery_running:
return
self._discovery_running = True
self.mass.run_task(self.remove_player(player.player_id))
# search for available chromecasts
from pychromecast.discovery import start_discovery, stop_discovery
+
def discovered_callback(name):
"""Called when zeroconf has discovered a (new) chromecast."""
discovery_info = listener.services[name]
if not player_id in self.mass.players._players:
self.__chromecast_discovered(player_id, discovery_info)
self.__update_group_players()
+
listener, browser = start_discovery(discovered_callback)
- time.sleep(30) # run discovery for 30 seconds
+ time.sleep(30) # run discovery for 30 seconds
stop_discovery(browser)
LOGGER.debug("Chromecast discovery completed...")
self._discovery_running = False
-
+
def __chromecast_discovered(self, player_id, discovery_info):
- ''' callback when a (new) chromecast device is discovered '''
+ """ callback when a (new) chromecast device is discovered """
from pychromecast import _get_chromecast_from_host, ChromecastConnectionError
+
try:
- chromecast = _get_chromecast_from_host(discovery_info, tries=2, timeout=5, retry_wait=5)
+ chromecast = _get_chromecast_from_host(
+ discovery_info, tries=2, timeout=5, retry_wait=5
+ )
except ChromecastConnectionError:
LOGGER.warning("Could not connect to device %s" % player_id)
return
self.supports_gapless = False
self.supports_crossfade = False
# register status listeners
- status_listener = StatusListener(player_id,
- player.handle_player_state, self.mass)
- if chromecast.cast_type == 'group':
+ 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.event_loop))
+ mz.register_listener(
+ MZListener(mz, self.__handle_group_members_update, self.mass.event_loop)
+ )
chromecast.register_handler(mz)
player.mz = mz
chromecast.register_connection_listener(status_listener)
self.mass.run_task(self.add_player(player))
def __update_group_players(self):
- '''update childs of all group players'''
+ """update childs of all group players"""
for player in self.players:
- if player.cc.cast_type == 'group':
+ if player.cc.cast_type == "group":
player.mz.update_members()
+
def chunks(l, n):
"""Yield successive n-sized chunks from l."""
for i in range(0, len(l), n):
- yield l[i:i + n]
+ yield l[i : i + n]
class StatusListener:
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))
+ """ 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))
+ """ 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 '''
+ """ 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.event_loop.run_in_executor(None,
- self.mass.players.providers[PROV_ID].run_chromecast_discovery)
+ self.mass.event_loop.run_in_executor(
+ None, self.mass.players.providers[PROV_ID].run_chromecast_discovery
+ )
+
class MZListener:
def __init__(self, mz, callback, loop):
def multizone_member_added(self, uuid):
asyncio.run_coroutine_threadsafe(
- self.__handle_group_members_update(
- self._mz, added_player=str(uuid)), self._loop)
+ 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)
+ 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)
+ self.__handle_group_members_update(self._mz), self._loop
+ )
# -*- coding:utf-8 -*-
import asyncio
-import aiohttp
-from typing import List
import logging
-import types
import time
+import types
+from typing import List
-from music_assistant.utils import run_periodic, LOGGER, try_parse_int
-from music_assistant.models.playerprovider import PlayerProvider
-from music_assistant.models.player import Player, PlayerState
-from music_assistant.models.playerstate import PlayerState
-from music_assistant.models.player_queue import QueueItem, PlayerQueue
+import aiohttp
from music_assistant.constants import CONF_ENABLED, CONF_HOSTNAME, CONF_PORT
+from music_assistant.models.player import Player, PlayerState
+from music_assistant.models.player_queue import PlayerQueue, QueueItem
+from music_assistant.models.playerprovider import PlayerProvider
+from music_assistant.utils import LOGGER, run_periodic, try_parse_int
-PROV_ID = 'sonos'
-PROV_NAME = 'Sonos'
-PROV_CLASS = 'SonosProvider'
+PROV_ID = "sonos"
+PROV_NAME = "Sonos"
+PROV_CLASS = "SonosProvider"
+
+CONFIG_ENTRIES = [(CONF_ENABLED, True, CONF_ENABLED)]
-CONFIG_ENTRIES = [
- (CONF_ENABLED, True, CONF_ENABLED),
- ]
class SonosPlayer(Player):
- ''' Sonos player object '''
+ """ Sonos player object """
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__sonos_report_progress_task.cancel()
async def cmd_stop(self):
- ''' send stop command to player '''
+ """ send stop command to player """
self.soco.stop()
async def cmd_play(self):
- ''' send play command to player '''
+ """ send play command to player """
self.soco.play()
async def cmd_pause(self):
- ''' send pause command to player '''
+ """ send pause command to player """
self.soco.pause()
async def cmd_next(self):
- ''' send next track command to player '''
+ """ send next track command to player """
self.soco.next()
async def cmd_previous(self):
- ''' send previous track command to player '''
+ """ send previous track command to player """
self.soco.previous()
-
+
async def cmd_power_on(self):
- ''' send power ON command to player '''
+ """ send power ON command to player """
self.powered = True
async def cmd_power_off(self):
- ''' send power OFF command to player '''
+ """ send power OFF command to player """
self.powered = False
# power is not supported so send stop instead
self.soco.stop()
async def cmd_volume_set(self, volume_level):
- ''' send new volume level command to player '''
+ """ send new volume level command to player """
self.soco.volume = volume_level
async def cmd_volume_mute(self, is_muted=False):
- ''' send mute command to player '''
+ """ send mute command to player """
self.soco.mute = is_muted
- async def cmd_play_uri(self, uri:str):
- ''' play single uri on player '''
+ async def cmd_play_uri(self, uri: str):
+ """ play single uri on player """
self.soco.play_uri(uri)
- async def cmd_queue_play_index(self, index:int):
- '''
+ async def cmd_queue_play_index(self, index: int):
+ """
play item at index X on player's queue
:attrib index: (int) index of the queue item that should start playing
- '''
+ """
self.soco.play_from_queue(index)
- async def cmd_queue_load(self, queue_items:List[QueueItem]):
- ''' load (overwrite) queue with new items '''
+ async def cmd_queue_load(self, queue_items: List[QueueItem]):
+ """ load (overwrite) queue with new items """
self.soco.clear_queue()
for pos, item in enumerate(queue_items):
self.soco.add_uri_to_queue(item.uri, pos)
- async def cmd_queue_insert(self, queue_items:List[QueueItem], insert_at_index):
+ async def cmd_queue_insert(self, queue_items: List[QueueItem], insert_at_index):
for pos, item in enumerate(queue_items):
- self.soco.add_uri_to_queue(item.uri, insert_at_index+pos)
+ self.soco.add_uri_to_queue(item.uri, insert_at_index + pos)
- async def cmd_queue_append(self, queue_items:List[QueueItem]):
- '''
+ async def cmd_queue_append(self, queue_items: List[QueueItem]):
+ """
append new items at the end of the queue
- '''
+ """
last_index = len(self.queue.items)
for pos, item in enumerate(queue_items):
- self.soco.add_uri_to_queue(item.uri, last_index+pos)
+ self.soco.add_uri_to_queue(item.uri, last_index + pos)
async def __report_progress(self):
- ''' report current progress while playing '''
+ """ report current progress while playing """
# sonos does not send instant updates of the player's progress (cur_time)
# so we need to send it in periodically
while self._state == PlayerState.Playing:
self.cur_time = adjusted_current_time
await asyncio.sleep(1)
self.__sonos_report_progress_task = None
-
+
async def update_state(self, event=None):
- ''' update state, triggerer by event '''
+ """ update state, triggerer by event """
if event:
variables = event.variables
if "volume" in variables:
track_info = self.soco.get_current_track_info()
self.cur_uri = track_info["uri"]
position_info = self.soco.avTransport.GetPositionInfo(
- [("InstanceID", 0), ("Channel", "Master")])
+ [("InstanceID", 0), ("Channel", "Master")]
+ )
rel_time = self.__timespan_secs(position_info.get("RelTime"))
self.cur_time = rel_time
- if self._state == PlayerState.Playing and self.__sonos_report_progress_task == None:
- self.__sonos_report_progress_task = self.mass.event_loop.create_task(self.__report_progress())
+ if (
+ self._state == PlayerState.Playing
+ and self.__sonos_report_progress_task == None
+ ):
+ self.__sonos_report_progress_task = self.mass.event_loop.create_task(
+ self.__report_progress()
+ )
@staticmethod
def __convert_state(sonos_state):
- ''' convert sonos state to internal state '''
- if sonos_state == 'PLAYING':
+ """ convert sonos state to internal state """
+ if sonos_state == "PLAYING":
return PlayerState.Playing
- elif sonos_state == 'PAUSED_PLAYBACK':
+ elif sonos_state == "PAUSED_PLAYBACK":
return PlayerState.Paused
else:
return PlayerState.Stopped
"""Parse a time-span into number of seconds."""
if timespan in ("", "NOT_IMPLEMENTED", None):
return None
- return sum(60 ** x[0] * int(x[1]) for x in enumerate(reversed(timespan.split(":"))))
-
+ return sum(
+ 60 ** x[0] * int(x[1]) for x in enumerate(reversed(timespan.split(":")))
+ )
+
class SonosProvider(PlayerProvider):
- ''' support for Sonos speakers '''
+ """ support for Sonos speakers """
+
_discovery_running = False
async def setup(self, conf):
- ''' perform async setup '''
- self.mass.event_loop.create_task(
- self.__periodic_discovery())
+ """ perform async setup """
+ self.mass.event_loop.create_task(self.__periodic_discovery())
@run_periodic(1800)
async def __periodic_discovery(self):
- ''' run sonos discovery on interval '''
+ """ run sonos discovery on interval """
self.mass.event_loop.run_in_executor(None, self.run_discovery)
def run_discovery(self):
- ''' background sonos discovery and handler '''
+ """ background sonos discovery and handler """
if self._discovery_running:
return
self._discovery_running = True
LOGGER.debug("Sonos discovery started...")
import soco
+
discovered_devices = soco.discover()
if discovered_devices == None:
discovered_devices = []
self.__process_groups([])
def __device_discovered(self, soco_device):
- '''handle new sonos player '''
+ """handle new sonos player """
player = SonosPlayer(self.mass, soco_device.uid, self.prov_id)
player.soco = soco_device
player.name = soco_device.player_name
queue = _ProcessSonosEventQueue(self.mass, action)
sub = service.subscribe(auto_renew=True, event_queue=queue)
player._subscriptions.append(sub)
+
subscribe(soco_device.avTransport, player.update_state)
subscribe(soco_device.renderingControl, player.update_state)
subscribe(soco_device.zoneGroupTopology, self.__topology_changed)
return player
def __process_groups(self, sonos_groups):
- ''' process all sonos groups '''
+ """ process all sonos groups """
all_group_ids = []
for group in sonos_groups:
all_group_ids.append(group.uid)
# check members
group_player.name = group.label
group_player.group_childs = [item.uid for item in group.members]
-
+
async def __topology_changed(self, event=None):
- '''
+ """
received topology changed event
from one of the sonos players
schedule discovery to work out the changes
- '''
+ """
self.mass.event_loop.run_in_executor(None, self.run_discovery)
+
class _ProcessSonosEventQueue:
"""Queue like object for dispatching sonos events."""
try:
self.mass.run_task(self._handler(item), wait_for_result=True)
except Exception as ex:
- LOGGER.warning("Error calling %s: %s", self._handler, ex)
\ No newline at end of file
+ LOGGER.warning("Error calling %s: %s", self._handler, ex)
# -*- coding:utf-8 -*-
import asyncio
-import os
-import struct
from collections import OrderedDict
-import time
import decimal
-from typing import List
+import os
import random
-import sys
import socket
-from music_assistant.utils import run_periodic, LOGGER, try_parse_int, get_ip, get_hostname
-from music_assistant.models import PlayerProvider, Player, PlayerState, MediaType, TrackQuality, AlbumType, Artist, Album, Track, Playlist
-from music_assistant.constants import CONF_ENABLED
+import struct
+import sys
+import time
+from typing import List
+from music_assistant.constants import CONF_ENABLED
+from music_assistant.models.player import Player, PlayerState
+from music_assistant.models.player_queue import PlayerQueue, QueueItem
+from music_assistant.models.playerprovider import PlayerProvider
+from music_assistant.utils import (
+ LOGGER,
+ get_hostname,
+ get_ip,
+ run_periodic,
+ try_parse_int,
+)
-PROV_ID = 'squeezebox'
-PROV_NAME = 'Squeezebox'
-PROV_CLASS = 'PySqueezeProvider'
+PROV_ID = "squeezebox"
+PROV_NAME = "Squeezebox"
+PROV_CLASS = "PySqueezeProvider"
-CONFIG_ENTRIES = [
- (CONF_ENABLED, True, CONF_ENABLED),
- ]
+CONFIG_ENTRIES = [(CONF_ENABLED, True, CONF_ENABLED)]
class PySqueezeProvider(PlayerProvider):
- ''' Python implementation of SlimProto server '''
+ """ Python implementation of SlimProto server """
- ### Provider specific implementation #####
+ ### Provider specific implementation #####
async def setup(self, conf):
- ''' async initialize of module '''
+ """ async initialize of module """
# start slimproto server
self.mass.event_loop.create_task(
- asyncio.start_server(self.__handle_socket_client, '0.0.0.0', 3483))
+ asyncio.start_server(self.__handle_socket_client, "0.0.0.0", 3483)
+ )
# setup discovery
self.mass.event_loop.create_task(self.start_discovery())
async def start_discovery(self):
transport, protocol = await self.mass.event_loop.create_datagram_endpoint(
lambda: DiscoveryProtocol(self.mass.web.http_port),
- local_addr=('0.0.0.0', 3483))
+ local_addr=("0.0.0.0", 3483),
+ )
try:
while True:
await asyncio.sleep(60) # serve forever
transport.close()
async def __handle_socket_client(self, reader, writer):
- ''' handle a client connection on the socket'''
- buffer = b''
+ """ handle a client connection on the socket"""
+ buffer = b""
player = None
- try:
+ try:
# keep reading bytes from the socket
while True:
data = await reader.read(64)
del data
if len(buffer) > 8:
operation, length = buffer[:4], buffer[4:8]
- plen = struct.unpack('!I', length)[0] + 8
+ plen = struct.unpack("!I", length)[0] + 8
if len(buffer) >= plen:
packet, buffer = buffer[8:plen], buffer[plen:]
operation = operation.strip(b"!").strip().decode()
- if operation == 'HELO':
+ if operation == "HELO":
# player connected
- (dev_id, rev, mac) = struct.unpack('BB6s', packet[:8])
- device_mac = ':'.join("%02x" % x for x in mac)
+ (dev_id, rev, mac) = struct.unpack("BB6s", packet[:8])
+ device_mac = ":".join("%02x" % x for x in mac)
player_id = str(device_mac).lower()
- device_type = devices.get(dev_id, 'unknown device')
- player = PySqueezePlayer(self.mass, player_id, self.prov_id, device_type, writer)
+ device_type = devices.get(dev_id, "unknown device")
+ player = PySqueezePlayer(
+ self.mass, player_id, self.prov_id, device_type, writer
+ )
await self.mass.players.add_player(player)
elif player != None:
await player.process_msg(operation, packet)
await self.mass.players.remove_player(player.player_id)
self.mass.config.save()
+
class PySqueezePlayer(Player):
- ''' Squeezebox socket client '''
+ """ Squeezebox socket client """
def __init__(self, mass, player_id, prov_id, dev_type, writer):
super().__init__(mass, player_id, prov_id)
self.supports_gapless = True
self.supports_crossfade = True
self._writer = writer
- self.buffer = b''
- self.name = "%s - %s" %(dev_type, player_id)
+ self.buffer = b""
+ self.name = "%s - %s" % (dev_type, player_id)
self._volume = PySqueezeVolume()
self._last_volume = 0
self._last_heartbeat = 0
self._heartbeat_task = self.mass.event_loop.create_task(self.__send_heartbeat())
async def initialize_player(self):
- ''' set some startup settings for the player '''
+ """ set some startup settings for the player """
# send version
- await self.__send_frame(b'vers', b'7.8')
+ await self.__send_frame(b"vers", b"7.8")
await self.__send_frame(b"setd", struct.pack("B", 0))
await self.__send_frame(b"setd", struct.pack("B", 4))
# TODO: handle display stuff
- #await self.setBrightness()
+ # await self.setBrightness()
# restore last volume and power state
if self.settings.get("last_volume"):
await self.volume_set(self.settings["last_volume"])
await self.power_off()
async def cmd_stop(self):
- ''' send stop command to player '''
+ """ send stop command to player """
data = await self.__pack_stream(b"q", autostart=b"0", flags=0)
await self.__send_frame(b"strm", data)
async def cmd_play(self):
- ''' send play (unpause) command to player '''
+ """ send play (unpause) command to player """
data = await self.__pack_stream(b"u", autostart=b"0", flags=0)
await self.__send_frame(b"strm", data)
async def cmd_pause(self):
- ''' send pause command to player '''
+ """ send pause command to player """
data = await self.__pack_stream(b"p", autostart=b"0", flags=0)
await self.__send_frame(b"strm", data)
-
+
async def cmd_power_on(self):
- ''' send power ON command to player '''
+ """ send power ON command to player """
await self.__send_frame(b"aude", struct.pack("2B", 1, 1))
self.settings["last_power"] = True
self.powered = True
async def cmd_power_off(self):
- ''' send power TOGGLE command to player '''
+ """ send power TOGGLE command to player """
await self.cmd_stop()
await self.__send_frame(b"aude", struct.pack("2B", 0, 0))
self.settings["last_power"] = False
self.powered = False
async def cmd_volume_set(self, volume_level):
- ''' send new volume level command to player '''
+ """ send new volume level command to player """
self._volume.volume = volume_level
og = self._volume.old_gain()
ng = self._volume.new_gain()
await self.__send_frame(b"audg", struct.pack("!LLBBLL", og, og, 1, 255, ng, ng))
self.settings["last_volume"] = volume_level
self.volume_level = volume_level
-
+
async def cmd_volume_mute(self, is_muted=False):
- ''' send mute command to player '''
+ """ send mute command to player """
if is_muted:
await self.__send_frame(b"aude", struct.pack("2B", 0, 0))
else:
await self.__send_frame(b"aude", struct.pack("2B", 1, 1))
self.muted = is_muted
- async def cmd_queue_play_index(self, index:int):
- '''
+ async def cmd_queue_play_index(self, index: int):
+ """
play item at index X on player's queue
:param index: (int) index of the queue item that should start playing
- '''
+ """
new_track = await self.queue.get_item(index)
if new_track:
await self.__send_flush()
await self.__send_play(new_track.uri)
async def cmd_queue_load(self, queue_items):
- '''
+ """
load/overwrite given items in the player's own queue implementation
:param queue_items: a list of QueueItems
- '''
+ """
await self.__send_flush()
if queue_items:
await self.__send_play(queue_items[0].uri)
return await self.cmd_queue_play_index(insert_at_index)
async def cmd_queue_append(self, queue_items):
- pass # automagically handled by built-in queue controller
+ pass # automagically handled by built-in queue controller
- async def cmd_play_uri(self, uri:str):
- '''
+ async def cmd_play_uri(self, uri: str):
+ """
[MUST OVERRIDE]
tell player to start playing a single uri
- '''
+ """
await self.__send_flush()
await self.__send_play(uri)
async def __send_flush(self):
data = await self.__pack_stream(b"f", autostart=b"0", flags=0)
await self.__send_frame(b"strm", data)
-
+
async def __send_play(self, uri):
- ''' play uri '''
+ """ play uri """
self.cur_uri = uri
self.powered = True
enable_crossfade = self.settings["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
- transType= b'1' if enable_crossfade else b'0'
+ command = b"s"
+ autostart = (
+ b"3"
+ ) # we use direct stream for now so let the player do the messy work with buffers
+ transType = b"1" if enable_crossfade else b"0"
transDuration = self.settings["crossfade_duration"]
- formatbyte = b'f' # fixed to flac
- uri = '/stream' + uri.split('/stream')[1]
- data = await self.__pack_stream(command, autostart=autostart, flags=0x00,
- formatbyte=formatbyte, transType=transType,
- transDuration=transDuration)
- headers = "Connection: close\r\nAccept: */*\r\nHost: %s:%s\r\n" %(self.mass.web.local_ip, self.mass.web.http_port)
+ formatbyte = b"f" # fixed to flac
+ uri = "/stream" + uri.split("/stream")[1]
+ data = await self.__pack_stream(
+ command,
+ autostart=autostart,
+ flags=0x00,
+ formatbyte=formatbyte,
+ transType=transType,
+ transDuration=transDuration,
+ )
+ headers = "Connection: close\r\nAccept: */*\r\nHost: %s:%s\r\n" % (
+ self.mass.web.local_ip,
+ self.mass.web.http_port,
+ )
request = "GET %s HTTP/1.0\r\n%s\r\n" % (uri, headers)
data = data + request.encode("utf-8")
- await self.__send_frame(b'strm', data)
+ await self.__send_frame(b"strm", data)
def __delete__(self, instance):
- ''' make sure the heartbeat task is deleted '''
+ """ make sure the heartbeat task is deleted """
if self._heartbeat_task:
self._heartbeat_task.cancel()
@run_periodic(5)
async def __send_heartbeat(self):
- ''' send periodic heartbeat message to player '''
+ """ send periodic heartbeat message to player """
timestamp = int(time.time())
data = await self.__pack_stream(b"t", replayGain=timestamp, flags=0)
await self.__send_frame(b"strm", data)
async def __send_frame(self, command, data):
- ''' send command to Squeeze player'''
- packet = struct.pack('!H', len(data) + 4) + command + data
+ """ send command to Squeeze player"""
+ packet = struct.pack("!H", len(data) + 4) + command + data
self._writer.write(packet)
await self._writer.drain()
- async def __pack_stream(self, command, autostart=b"1", formatbyte = b'o',
- pcmargs = (b'?',b'?',b'?',b'?'), threshold = 200,
- spdif = b'0', transDuration = 0, transType = b'0',
- flags = 0x40, outputThreshold = 0,
- replayGain=0, serverPort = 8095, serverIp = 0):
- return struct.pack("!cccccccBcBcBBBLHL",
- command, autostart, formatbyte, *pcmargs,
- threshold, spdif, transDuration, transType,
- flags, outputThreshold, 0, replayGain, serverPort, serverIp)
-
+ async def __pack_stream(
+ self,
+ command,
+ autostart=b"1",
+ formatbyte=b"o",
+ pcmargs=(b"?", b"?", b"?", b"?"),
+ threshold=200,
+ spdif=b"0",
+ transDuration=0,
+ transType=b"0",
+ flags=0x40,
+ outputThreshold=0,
+ replayGain=0,
+ serverPort=8095,
+ serverIp=0,
+ ):
+ return struct.pack(
+ "!cccccccBcBcBBBLHL",
+ command,
+ autostart,
+ formatbyte,
+ *pcmargs,
+ threshold,
+ spdif,
+ transDuration,
+ transType,
+ flags,
+ outputThreshold,
+ 0,
+ replayGain,
+ serverPort,
+ serverIp
+ )
+
async def displayTrack(self, track):
await self.render("%s by %s" % (track.title, track.artist))
-
+
async def setBrightness(self, level=4):
assert 0 <= level <= 4
await self.__send_frame(b"grfb", struct.pack("!H", level))
await self.__send_frame(b"visu", visualisation.pack())
async def render(self, text):
- #self.display.clear()
- #self.display.renderText(text, "DejaVu-Sans", 16, (0,0))
- #self.updateDisplay(self.display.frame())
+ # self.display.clear()
+ # self.display.renderText(text, "DejaVu-Sans", 16, (0,0))
+ # self.updateDisplay(self.display.frame())
pass
- async def updateDisplay(self, bitmap, transition = 'c', offset=0, param=0):
+ async def updateDisplay(self, bitmap, transition="c", offset=0, param=0):
frame = struct.pack("!Hcb", offset, transition, param) + bitmap
await self.__send_frame(b"grfe", frame)
await handler(packet)
async def process_STAT(self, data):
- '''process incoming event from player'''
+ """process incoming event from player"""
event = data[:4].decode()
event_data = data[4:]
- if event == b'\x00\x00\x00\x00':
+ if event == b"\x00\x00\x00\x00":
# Presumed informational stat message
return
- event_handler = getattr(self, 'stat_%s' %event, None)
+ event_handler = getattr(self, "stat_%s" % event, None)
if event_handler is None:
- LOGGER.debug("Got event %s - event_data: %s" %(event, event_data))
+ LOGGER.debug("Got event %s - event_data: %s" % (event, event_data))
else:
await event_handler(data[4:])
self.state = PlayerState.Stopped
async def stat_STMo(self, data):
- ''' No more decoded (uncompressed) data to play; triggers rebuffering. '''
+ """ No more decoded (uncompressed) data to play; triggers rebuffering. """
LOGGER.debug("Output Underrun")
-
+
async def stat_STMp(self, data):
- '''Pause confirmed'''
+ """Pause confirmed"""
self.state = PlayerState.Paused
async def stat_STMr(self, data):
- '''Resume confirmed'''
+ """Resume confirmed"""
self.state = PlayerState.Playing
async def stat_STMs(self, data):
- '''Playback of new track has started'''
+ """Playback of new track has started"""
self.state = PlayerState.Playing
async def stat_STMt(self, data):
""" heartbeat from client """
timestamp = time.time()
self._last_heartbeat = timestamp
- (num_crlf, mas_initialized, mas_mode, rptr, wptr,
- bytes_received_h, bytes_received_l, signal_strength,
- jiffies, output_buffer_size, output_buffer_fullness,
- elapsed_seconds, voltage, cur_time_milliseconds,
- server_timestamp, error_code) = struct.unpack("!BBBLLLLHLLLLHLLH", data)
+ (
+ num_crlf,
+ mas_initialized,
+ mas_mode,
+ rptr,
+ wptr,
+ bytes_received_h,
+ bytes_received_l,
+ signal_strength,
+ jiffies,
+ output_buffer_size,
+ output_buffer_fullness,
+ elapsed_seconds,
+ voltage,
+ cur_time_milliseconds,
+ server_timestamp,
+ error_code,
+ ) = struct.unpack("!BBBLLLLHLLLLHLLH", data)
if self.state == PlayerState.Playing and elapsed_seconds != self.cur_time:
self.cur_time = elapsed_seconds
self._cur_time_milliseconds = cur_time_milliseconds
async def stat_STMu(self, data):
- ''' Buffer underrun: Normal end of playback'''
+ """ Buffer underrun: Normal end of playback"""
self.state = PlayerState.Stopped
async def process_RESP(self, data):
- ''' response received at player, send continue '''
+ """ response received at player, send continue """
LOGGER.debug("RESP received")
await self.__send_frame(b"cont", b"0")
# LOGGER.info("Unknown IR received: %r, %r" % (time, code))
async def process_SETD(self, data):
- ''' Get/set player firmware settings '''
+ """ Get/set player firmware settings """
LOGGER.debug("SETD received %s" % data)
cmd_id = data[0]
if cmd_id == 0:
# from http://wiki.slimdevices.com/index.php/SlimProtoTCPProtocol#HELO
devices = {
- 2: 'squeezebox',
- 3: 'softsqueeze',
- 4: 'squeezebox2',
- 5: 'transporter',
- 6: 'softsqueeze3',
- 7: 'receiver',
- 8: 'squeezeslave',
- 9: 'controller',
- 10: 'boom',
- 11: 'softboom',
- 12: 'squeezeplay',
- }
+ 2: "squeezebox",
+ 3: "softsqueeze",
+ 4: "squeezebox2",
+ 5: "transporter",
+ 6: "softsqueeze3",
+ 7: "receiver",
+ 8: "squeezeslave",
+ 9: "controller",
+ 10: "boom",
+ 11: "softboom",
+ 12: "squeezeplay",
+}
class PySqueezeVolume(object):
# this map is taken from Slim::Player::Squeezebox2 in the squeezecenter source
# i don't know how much magic it contains, or any way I can test it
old_map = [
- 0, 1, 1, 1, 2, 2, 2, 3, 3, 4,
- 5, 5, 6, 6, 7, 8, 9, 9, 10, 11,
- 12, 13, 14, 15, 16, 16, 17, 18, 19, 20,
- 22, 23, 24, 25, 26, 27, 28, 29, 30, 32,
- 33, 34, 35, 37, 38, 39, 40, 42, 43, 44,
- 46, 47, 48, 50, 51, 53, 54, 56, 57, 59,
- 60, 61, 63, 65, 66, 68, 69, 71, 72, 74,
- 75, 77, 79, 80, 82, 84, 85, 87, 89, 90,
- 92, 94, 96, 97, 99, 101, 103, 104, 106, 108, 110,
- 112, 113, 115, 117, 119, 121, 123, 125, 127, 128
- ];
+ 0,
+ 1,
+ 1,
+ 1,
+ 2,
+ 2,
+ 2,
+ 3,
+ 3,
+ 4,
+ 5,
+ 5,
+ 6,
+ 6,
+ 7,
+ 8,
+ 9,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 16,
+ 17,
+ 18,
+ 19,
+ 20,
+ 22,
+ 23,
+ 24,
+ 25,
+ 26,
+ 27,
+ 28,
+ 29,
+ 30,
+ 32,
+ 33,
+ 34,
+ 35,
+ 37,
+ 38,
+ 39,
+ 40,
+ 42,
+ 43,
+ 44,
+ 46,
+ 47,
+ 48,
+ 50,
+ 51,
+ 53,
+ 54,
+ 56,
+ 57,
+ 59,
+ 60,
+ 61,
+ 63,
+ 65,
+ 66,
+ 68,
+ 69,
+ 71,
+ 72,
+ 74,
+ 75,
+ 77,
+ 79,
+ 80,
+ 82,
+ 84,
+ 85,
+ 87,
+ 89,
+ 90,
+ 92,
+ 94,
+ 96,
+ 97,
+ 99,
+ 101,
+ 103,
+ 104,
+ 106,
+ 108,
+ 110,
+ 112,
+ 113,
+ 115,
+ 117,
+ 119,
+ 121,
+ 123,
+ 125,
+ 127,
+ 128,
+ ]
# 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.
+ 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.
def __init__(self):
self.volume = 50
""" Return the "new" gain value. """
step_db = self.total_volume_range * self.step_fraction
- max_volume_db = 0 # different on the boom?
+ max_volume_db = 0 # different on the boom?
# Equation for a line:
# y = mx+b
slope_high = max_volume_db - step_db / (100.0 - self.step_point)
slope_low = step_db - self.total_volume_range / (self.step_point - 0.0)
x2 = self.volume
- if (x2 > self.step_point):
+ if x2 > self.step_point:
m = slope_high
x1 = 100
y1 = max_volume_db
def new_gain(self):
db = self.decibels()
- floatmult = 10 ** (db/20.0)
+ floatmult = 10 ** (db / 20.0)
# avoid rounding errors somehow
if -30 <= db <= 0:
- return int(floatmult * (1 << 8) + 0.5) * (1<<8)
+ return int(floatmult * (1 << 8) + 0.5) * (1 << 8)
else:
- return int((floatmult * (1<<16)) + 0.5)
+ return int((floatmult * (1 << 16)) + 0.5)
##### UDP DISCOVERY STUFF #############
-class Datagram(object):
+class Datagram(object):
@classmethod
def decode(self, data):
- if data[0] == 'e':
+ if data[0] == "e":
return TLVDiscoveryRequestDatagram(data)
- elif data[0] == 'E':
+ elif data[0] == "E":
return TLVDiscoveryResponseDatagram(data)
- elif data[0] == 'd':
+ elif data[0] == "d":
return ClientDiscoveryDatagram(data)
- elif data[0] == 'h':
- pass # Hello!
- elif data[0] == 'i':
- pass # IR
- elif data[0] == '2':
- pass # i2c?
- elif data[0] == 'a':
- pass # ack!
+ elif data[0] == "h":
+ pass # Hello!
+ elif data[0] == "i":
+ pass # IR
+ elif data[0] == "2":
+ pass # i2c?
+ elif data[0] == "a":
+ pass # ack!
+
class ClientDiscoveryDatagram(Datagram):
client = None
def __init__(self, data):
- s = struct.unpack('!cxBB8x6B', data.encode())
- assert s[0] == 'd'
+ 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:]])
def __repr__(self):
- return "<%s device=%r firmware=%r client=%r>" % (self.__class__.__name__, self.device, self.firmware, self.client)
+ return "<%s device=%r firmware=%r client=%r>" % (
+ self.__class__.__name__,
+ self.device,
+ self.firmware,
+ self.client,
+ )
-class DiscoveryResponseDatagram(Datagram):
+class DiscoveryResponseDatagram(Datagram):
def __init__(self, hostname, port):
hostname = hostname[:16].encode("UTF-8")
- hostname += (16 - len(hostname)) * '\x00'
- self.packet = struct.pack('!c16s', 'D', hostname).decode()
+ hostname += (16 - len(hostname)) * "\x00"
+ self.packet = struct.pack("!c16s", "D", hostname).decode()
+
class TLVDiscoveryRequestDatagram(Datagram):
-
def __init__(self, data):
requestdata = OrderedDict()
- assert data[0] == 'e'
+ assert data[0] == "e"
idx = 1
- length = len(data)-5
+ 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
+ val = data[idx + 5 : idx + 5 + l]
+ idx += 5 + l
else:
val = None
idx += 5
typ = typ.decode()
requestdata[typ] = val
self.data = requestdata
-
+
def __repr__(self):
return "<%s data=%r>" % (self.__class__.__name__, self.data.items())
-class TLVDiscoveryResponseDatagram(Datagram):
+class TLVDiscoveryResponseDatagram(Datagram):
def __init__(self, responsedata):
- parts = ['E'] # new discovery format
+ parts = ["E"] # new discovery format
for typ, value in responsedata.items():
if value is None:
- value = ''
+ value = ""
elif len(value) > 255:
# Response too long, truncating to 255 bytes
value = value[:255]
parts.extend((typ, chr(len(value)), value))
- self.packet = ''.join(parts)
+ self.packet = "".join(parts)
-class DiscoveryProtocol():
+class DiscoveryProtocol:
def __init__(self, web_port):
self.web_port = web_port
-
+
def connection_made(self, transport):
self.transport = transport
# Allow receiving multicast broadcasts
- sock = self.transport.get_extra_info('socket')
- group = socket.inet_aton('239.255.255.250')
- mreq = struct.pack('4sL', group, socket.INADDR_ANY)
+ sock = self.transport.get_extra_info("socket")
+ group = socket.inet_aton("239.255.255.250")
+ mreq = struct.pack("4sL", group, socket.INADDR_ANY)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
def error_received(self, exc):
def connection_lost(self, *args, **kwargs):
LOGGER.debug("Connection lost to discovery")
-
+
def build_TLV_response(self, requestdata):
responsedata = OrderedDict()
for typ, value in requestdata.items():
- if typ == 'NAME':
+ if typ == "NAME":
# send full host name - no truncation
value = get_hostname()
- elif typ == 'IPAD':
+ elif typ == "IPAD":
# send ipaddress as a string only if it is set
value = get_ip()
# :todo: IPv6
- if value == '0.0.0.0':
+ if value == "0.0.0.0":
# do not send back an ip address
typ = None
- elif typ == 'JSON':
+ elif typ == "JSON":
# send port as a string
json_port = self.web_port
value = str(json_port)
- elif typ == 'VERS':
+ elif typ == "VERS":
# send server version
- value = '7.9'
- elif typ == 'UUID':
+ value = "7.9"
+ elif typ == "UUID":
# send server uuid
- value = 'musicassistant'
+ value = "musicassistant"
else:
- LOGGER.debug('Unexpected information request: %r', typ)
+ LOGGER.debug("Unexpected information request: %r", typ)
typ = None
if typ:
responsedata[typ] = value
def sendTLVDiscoveryResponse(self, resonsedata, addr):
dgram = TLVDiscoveryResponseDatagram(resonsedata)
self.transport.sendto(dgram.packet.encode(), addr)
-
# -*- coding:utf-8 -*-
import asyncio
-import os
-import struct
from collections import OrderedDict
-import time
import decimal
-from typing import List
+import os
import random
-import sys
import socket
-from music_assistant.utils import run_periodic, LOGGER, try_parse_int, get_ip, get_hostname
-from music_assistant.models import PlayerProvider, Player, PlayerState, MediaType, TrackQuality, AlbumType, Artist, Album, Track, Playlist
+import struct
+import sys
+import time
+from typing import List
+
from music_assistant.constants import CONF_ENABLED
+from music_assistant.models.player import Player, PlayerState
+from music_assistant.models.player_queue import QueueItem
+from music_assistant.models.playerprovider import PlayerProvider
+from music_assistant.utils import (
+ LOGGER,
+ get_hostname,
+ get_ip,
+ run_periodic,
+ try_parse_int,
+)
+PROV_ID = "webplayer"
+PROV_NAME = "WebPlayer"
+PROV_CLASS = "WebPlayerProvider"
-PROV_ID = 'webplayer'
-PROV_NAME = 'WebPlayer'
-PROV_CLASS = 'WebPlayerProvider'
+CONFIG_ENTRIES = [(CONF_ENABLED, True, CONF_ENABLED)]
-CONFIG_ENTRIES = [
- (CONF_ENABLED, True, CONF_ENABLED),
- ]
+EVENT_WEBPLAYER_CMD = "webplayer command"
+EVENT_WEBPLAYER_STATE = "webplayer state"
+EVENT_WEBPLAYER_REGISTER = "webplayer register"
-EVENT_WEBPLAYER_CMD = 'webplayer command'
-EVENT_WEBPLAYER_STATE = 'webplayer state'
-EVENT_WEBPLAYER_REGISTER = 'webplayer register'
class WebPlayerProvider(PlayerProvider):
- '''
+ """
Implementation of a player using pure HTML/javascript
used in the front-end.
Communication is handled through the websocket connection
and our internal event bus
- '''
+ """
- ### Provider specific implementation #####
+ ### Provider specific implementation #####
async def setup(self, conf):
- ''' async initialize of module '''
- await self.mass.add_event_listener(self.handle_mass_event, EVENT_WEBPLAYER_STATE)
- await self.mass.add_event_listener(self.handle_mass_event, EVENT_WEBPLAYER_REGISTER)
+ """ async initialize of module """
+ await self.mass.add_event_listener(
+ self.handle_mass_event, EVENT_WEBPLAYER_STATE
+ )
+ await self.mass.add_event_listener(
+ self.handle_mass_event, EVENT_WEBPLAYER_REGISTER
+ )
self.mass.event_loop.create_task(self.check_players())
async def handle_mass_event(self, msg, msg_details):
- ''' received event for the webplayer component '''
+ """ received event for the webplayer component """
if msg == EVENT_WEBPLAYER_REGISTER:
# register new player
- player_id = msg_details['player_id']
+ player_id = msg_details["player_id"]
player = WebPlayer(self.mass, player_id, self.prov_id)
player.supports_crossfade = False
player.supports_gapless = False
player.supports_queue = False
- player.name = msg_details['name']
+ player.name = msg_details["name"]
await self.add_player(player)
elif msg == EVENT_WEBPLAYER_STATE:
- player_id = msg_details['player_id']
+ player_id = msg_details["player_id"]
player = await self.get_player(player_id)
if player:
await player.handle_state(msg_details)
@run_periodic(30)
async def check_players(self):
- ''' invalidate players that did not send a heartbeat message in a while '''
+ """ invalidate players that did not send a heartbeat message in a while """
cur_time = time.time()
offline_players = []
for player in self.players:
class WebPlayer(Player):
- ''' Web player object '''
+ """ Web player object """
def __init__(self, mass, player_id, prov_id):
self._last_message = time.time()
super().__init__(mass, player_id, prov_id)
async def cmd_stop(self):
- ''' send stop command to player '''
- data = { 'player_id': self.player_id, 'cmd': 'stop'}
+ """ send stop command to player """
+ data = {"player_id": self.player_id, "cmd": "stop"}
await self.mass.signal_event(EVENT_WEBPLAYER_CMD, data)
async def cmd_play(self):
- ''' send play command to player '''
- data = { 'player_id': self.player_id, 'cmd': 'play'}
+ """ send play command to player """
+ data = {"player_id": self.player_id, "cmd": "play"}
await self.mass.signal_event(EVENT_WEBPLAYER_CMD, data)
async def cmd_pause(self):
- ''' send pause command to player '''
- data = { 'player_id': self.player_id, 'cmd': 'pause'}
+ """ send pause command to player """
+ data = {"player_id": self.player_id, "cmd": "pause"}
await self.mass.signal_event(EVENT_WEBPLAYER_CMD, data)
-
+
async def cmd_power_on(self):
- ''' send power ON command to player '''
- self.powered = True # not supported on webplayer
- data = { 'player_id': self.player_id, 'cmd': 'stop'}
+ """ send power ON command to player """
+ self.powered = True # not supported on webplayer
+ data = {"player_id": self.player_id, "cmd": "stop"}
await self.mass.signal_event(EVENT_WEBPLAYER_CMD, data)
async def cmd_power_off(self):
- ''' send power OFF command to player '''
+ """ send power OFF command to player """
self.powered = False
async def cmd_volume_set(self, volume_level):
- ''' send new volume level command to player '''
- data = { 'player_id': self.player_id, 'cmd': 'volume_set', 'volume_level': volume_level}
+ """ send new volume level command to player """
+ data = {
+ "player_id": self.player_id,
+ "cmd": "volume_set",
+ "volume_level": volume_level,
+ }
await self.mass.signal_event(EVENT_WEBPLAYER_CMD, data)
async def cmd_volume_mute(self, is_muted=False):
- ''' send mute command to player '''
- data = { 'player_id': self.player_id, 'cmd': 'volume_mute', 'is_muted': is_muted}
+ """ send mute command to player """
+ data = {"player_id": self.player_id, "cmd": "volume_mute", "is_muted": is_muted}
await self.mass.signal_event(EVENT_WEBPLAYER_CMD, data)
- async def cmd_play_uri(self, uri:str):
- ''' play single uri on player '''
- data = { 'player_id': self.player_id, 'cmd': 'play_uri', 'uri': uri}
+ async def cmd_play_uri(self, uri: str):
+ """ play single uri on player """
+ data = {"player_id": self.player_id, "cmd": "play_uri", "uri": uri}
await self.mass.signal_event(EVENT_WEBPLAYER_CMD, data)
async def handle_state(self, data):
- ''' handle state event from player '''
- if 'volume_level' in data:
- self.volume_level = data['volume_level']
- if 'muted' in data:
- self.muted = data['muted']
- if 'state' in data:
- self.state = PlayerState(data['state'])
- if 'cur_time' in data:
- self.cur_time = data['cur_time']
- if 'cur_uri' in data:
- self.cur_uri = data['cur_uri']
- if 'powered' in data:
- self.powered = data['powered']
- if 'name' in data:
- self.name = data['name']
+ """ handle state event from player """
+ if "volume_level" in data:
+ self.volume_level = data["volume_level"]
+ if "muted" in data:
+ self.muted = data["muted"]
+ if "state" in data:
+ self.state = PlayerState(data["state"])
+ if "cur_time" in data:
+ self.cur_time = data["cur_time"]
+ if "cur_uri" in data:
+ self.cur_uri = data["cur_uri"]
+ if "powered" in data:
+ self.powered = data["powered"]
+ if "name" in data:
+ self.name = data["name"]
self._last_message = time.time()
-
-
# -*- coding:utf-8 -*-
import asyncio
-import logging
-import socket
import importlib
+import logging
import os
import re
+import socket
+
+from music_assistant.constants import CONF_ENABLED, CONF_KEY_MUSICPROVIDERS
import unidecode
+
try:
import simplejson as json
except ImportError:
import json
-LOGGER = logging.getLogger('music_assistant')
+LOGGER = logging.getLogger("music_assistant")
-from music_assistant.constants import CONF_KEY_MUSICPROVIDERS, CONF_ENABLED
-IS_HASSIO = os.path.isfile('/data/options.json')
+IS_HASSIO = os.path.isfile("/data/options.json")
def run_periodic(period):
def filename_from_string(string):
""" create filename from unsafe string """
- keepcharacters = (' ', '.', '_')
- return "".join(c for c in string
- if c.isalnum() or c in keepcharacters).rstrip()
+ keepcharacters = (" ", ".", "_")
+ return "".join(c for c in string if c.isalnum() or c in keepcharacters).rstrip()
def run_background_task(corofn, *args, executor=None):
def run_async_background_task(executor, corofn, *args):
""" run async task in background """
+
def run_task(corofn, *args):
- LOGGER.debug('running %s in background task', corofn.__name__)
+ LOGGER.debug("running %s in background task", corofn.__name__)
new_loop = asyncio.new_event_loop()
asyncio.set_event_loop(new_loop)
coro = corofn(*args)
res = new_loop.run_until_complete(coro)
new_loop.close()
- LOGGER.debug('completed %s in background task', corofn.__name__)
+ LOGGER.debug("completed %s in background task", corofn.__name__)
return res
- return asyncio.get_event_loop().run_in_executor(executor, run_task, corofn,
- *args)
+ return asyncio.get_event_loop().run_in_executor(executor, run_task, corofn, *args)
def get_sort_name(name):
if isinstance(possible_bool, bool):
return possible_bool
else:
- return possible_bool in ['true', 'True', '1', 'on', 'ON', 1]
+ return possible_bool in ["true", "True", "1", "on", "ON", 1]
def parse_title_and_version(track_title, track_version=None):
""" try to parse clean track title and version from the title """
title = track_title.lower()
- version = ''
+ version = ""
for splitter in [" (", " [", " - ", " (", " [", "-"]:
if splitter in title:
title_parts = title.split(splitter)
if end_splitter in title_part:
title_part = title_part.split(end_splitter)[0]
for ignore_str in [
- "feat.", "featuring", "ft.", "with ", " & ", "explicit"
+ "feat.",
+ "featuring",
+ "ft.",
+ "with ",
+ " & ",
+ "explicit",
]:
if ignore_str in title_part:
title = title.split(splitter + title_part)[0]
for version_str in [
- "version", "live", "edit", "remix", "mix", "acoustic",
- " instrumental", "karaoke", "remaster", "versie",
- "radio", "unplugged", "disco"
+ "version",
+ "live",
+ "edit",
+ "remix",
+ "mix",
+ "acoustic",
+ " instrumental",
+ "karaoke",
+ "remaster",
+ "versie",
+ "radio",
+ "unplugged",
+ "disco",
]:
if version_str in title_part:
version = title_part
""" transform provider version str to universal version type """
version_str = version_str.lower()
# substitute edit and edition with version
- if 'edition' in version_str or 'edit' in version_str:
- version_str = version_str.replace(' edition', ' version')
- version_str = version_str.replace(' edit ', ' version')
- if version_str.startswith('the '):
- version_str = version_str.split('the ')[1]
+ if "edition" in version_str or "edit" in version_str:
+ version_str = version_str.replace(" edition", " version")
+ version_str = version_str.replace(" edit ", " version")
+ if version_str.startswith("the "):
+ version_str = version_str.split("the ")[1]
if "radio mix" in version_str:
version_str = "radio version"
elif "video mix" in version_str:
version_str = "video version"
elif "spanglish" in version_str or "spanish" in version_str:
version_str = "spanish version"
- elif version_str.endswith('remaster'):
- version_str = 'remaster'
+ elif version_str.endswith("remaster"):
+ version_str = "remaster"
return version_str.strip()
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
# doesn't even have to be reachable
- s.connect(('10.255.255.255', 1))
+ s.connect(("10.255.255.255", 1))
IP = s.getsockname()[0]
except Exception:
- IP = '127.0.0.1'
+ IP = "127.0.0.1"
finally:
s.close()
return IP
def serialize_values(obj):
"""Recursively create serializable values for (custom) data types."""
+
def get_val(val):
if isinstance(val, (int, str, bool, float, tuple)):
return val
for item in val:
new_list.append(get_val(item))
return new_list
- elif hasattr(val, 'to_dict'):
+ 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__'):
+ elif hasattr(val, "__dict__"):
new_dict = {}
for key, value in val.__dict__.items():
new_dict[key] = get_val(value)
with open(jsonfile) as f:
return json.loads(f.read())
except Exception as exc:
- LOGGER.debug("Could not load json from file %s",
- jsonfile,
- exc_info=exc)
+ LOGGER.debug("Could not load json from file %s", jsonfile, exc_info=exc)
return None
# pylint: enable=broad-except
-async def load_provider_modules(mass,
- provider_modules,
- prov_type=CONF_KEY_MUSICPROVIDERS):
+async def load_provider_modules(
+ mass, provider_modules, prov_type=CONF_KEY_MUSICPROVIDERS
+):
""" dynamically load music/player providers """
base_dir = os.path.dirname(os.path.abspath(__file__))
modules_path = os.path.join(base_dir, prov_type)
# load modules
for item in os.listdir(modules_path):
- if (os.path.isfile(os.path.join(modules_path, item))
- and not item.startswith("_") and item.endswith('.py')
- and not item.startswith('.')):
+ if (
+ os.path.isfile(os.path.join(modules_path, item))
+ and not item.startswith("_")
+ and item.endswith(".py")
+ and not item.startswith(".")
+ ):
module_name = item.replace(".py", "")
if module_name not in provider_modules:
- prov_mod = await load_provider_module(mass, module_name,
- prov_type)
+ prov_mod = await load_provider_module(mass, module_name, prov_type)
if prov_mod:
provider_modules[module_name] = prov_mod
""" dynamically load music/player provider """
# pylint: disable=broad-except
try:
- prov_mod = importlib.import_module(f".{module_name}",
- f"music_assistant.{prov_type}")
+ prov_mod = importlib.import_module(
+ f".{module_name}", f"music_assistant.{prov_type}"
+ )
prov_conf_entries = prov_mod.CONFIG_ENTRIES
prov_id = module_name
prov_name = prov_mod.PROV_NAME
prov_class = prov_mod.PROV_CLASS
# get/create config for the module
- prov_config = mass.config.create_module_config(prov_id,
- prov_conf_entries,
- prov_type)
+ prov_config = mass.config.create_module_config(
+ prov_id, prov_conf_entries, prov_type
+ )
if prov_config[CONF_ENABLED]:
prov_mod_cls = getattr(prov_mod, prov_class)
provider = prov_mod_cls(mass)
# -*- coding:utf-8 -*-
import asyncio
-import os
-import aiohttp
-import inspect
-import aiohttp_cors
-from aiohttp import web
+import concurrent
from functools import partial
+import inspect
+import os
import ssl
-import concurrent
import threading
-from music_assistant.models.media_types import MediaItem, MediaType, media_type_from_string
-from music_assistant.utils import run_periodic, LOGGER, IS_HASSIO, run_async_background_task, get_ip, json_serializer
-from music_assistant.constants import CONF_KEY_PLAYERSETTINGS, CONF_KEY_MUSICPROVIDERS, CONF_KEY_PLAYERPROVIDERS
-CONF_KEY = 'web'
+import aiohttp
+from aiohttp import web
+import aiohttp_cors
+from music_assistant.constants import (
+ CONF_KEY_MUSICPROVIDERS,
+ CONF_KEY_PLAYERPROVIDERS,
+ CONF_KEY_PLAYERSETTINGS,
+)
+from music_assistant.models.media_types import (
+ MediaItem,
+ MediaType,
+ media_type_from_string,
+)
+from music_assistant.utils import (
+ IS_HASSIO,
+ LOGGER,
+ get_ip,
+ json_serializer,
+ run_async_background_task,
+ run_periodic,
+)
+
+CONF_KEY = "web"
if IS_HASSIO:
# on hassio we use ingress
- CONFIG_ENTRIES = [('https_port', 8096, 'web_https_port'),
- ('ssl_certificate', '', 'web_ssl_cert'),
- ('ssl_key', '', 'web_ssl_key'),
- ('cert_fqdn_host', '', 'cert_fqdn_host')]
+ CONFIG_ENTRIES = [
+ ("https_port", 8096, "web_https_port"),
+ ("ssl_certificate", "", "web_ssl_cert"),
+ ("ssl_key", "", "web_ssl_key"),
+ ("cert_fqdn_host", "", "cert_fqdn_host"),
+ ]
else:
- CONFIG_ENTRIES = [('http_port', 8095, 'web_http_port'),
- ('https_port', 8096, 'web_https_port'),
- ('ssl_certificate', '', 'web_ssl_cert'),
- ('ssl_key', '', 'web_ssl_key'),
- ('cert_fqdn_host', '', 'cert_fqdn_host')]
+ CONFIG_ENTRIES = [
+ ("http_port", 8095, "web_http_port"),
+ ("https_port", 8096, "web_https_port"),
+ ("ssl_certificate", "", "web_ssl_cert"),
+ ("ssl_key", "", "web_ssl_key"),
+ ("cert_fqdn_host", "", "cert_fqdn_host"),
+ ]
class ClassRouteTableDef(web.RouteTableDef):
def add_class_routes(self, instance) -> None:
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
routes = ClassRouteTableDef()
-class Web():
+class Web:
""" webserver and json/websocket api """
+
runner = None
def __init__(self, mass):
self.mass = mass
# load/create/update config
- config = self.mass.config.create_module_config(CONF_KEY,
- CONFIG_ENTRIES)
+ config = self.mass.config.create_module_config(CONF_KEY, CONFIG_ENTRIES)
self.local_ip = get_ip()
self.config = config
if IS_HASSIO:
# retrieve ingress http port
import requests
- url = 'http://hassio/addons/self/info'
- headers = { "X-HASSIO-KEY":os.environ["HASSIO_TOKEN"] }
+
+ url = "http://hassio/addons/self/info"
+ headers = {"X-HASSIO-KEY": os.environ["HASSIO_TOKEN"]}
response = requests.get(url, headers=headers).json()
self.http_port = response["data"]["ingress_port"]
else:
# use settings from config
- self.http_port = config['http_port']
- enable_ssl = config['ssl_certificate'] and config['ssl_key']
- if config['ssl_certificate'] and not os.path.isfile(
- config['ssl_certificate']):
+ self.http_port = config["http_port"]
+ enable_ssl = config["ssl_certificate"] and config["ssl_key"]
+ if config["ssl_certificate"] and not os.path.isfile(config["ssl_certificate"]):
enable_ssl = False
- LOGGER.warning("SSL certificate file not found: %s",
- config['ssl_certificate'])
- if config['ssl_key'] and not os.path.isfile(config['ssl_key']):
+ LOGGER.warning(
+ "SSL certificate file not found: %s", config["ssl_certificate"]
+ )
+ if config["ssl_key"] and not os.path.isfile(config["ssl_key"]):
enable_ssl = False
- LOGGER.warning("SSL certificate key file not found: %s",
- config['ssl_key'])
- self.https_port = config['https_port']
+ LOGGER.warning("SSL certificate key file not found: %s", config["ssl_key"])
+ self.https_port = config["https_port"]
self._enable_ssl = enable_ssl
async def setup(self):
routes.add_class_routes(self)
app = web.Application()
app.add_routes(routes)
- app.add_routes([
- web.get('/stream/{player_id}',
+ app.add_routes(
+ [
+ web.get(
+ "/stream/{player_id}",
self.mass.http_streamer.stream,
- allow_head=False),
- web.get('/stream/{player_id}/{queue_item_id}',
+ allow_head=False,
+ ),
+ web.get(
+ "/stream/{player_id}/{queue_item_id}",
self.mass.http_streamer.stream,
- allow_head=False),
- web.get('/', self.index),
- web.get('/jsonrpc.js', self.json_rpc),
- web.post('/jsonrpc.js', self.json_rpc),
- web.get('/ws', self.websocket_handler)
- ])
- webdir = os.path.join(os.path.dirname(os.path.abspath(__file__)),
- 'web/')
+ allow_head=False,
+ ),
+ web.get("/", self.index),
+ web.get("/jsonrpc.js", self.json_rpc),
+ web.post("/jsonrpc.js", self.json_rpc),
+ web.get("/ws", self.websocket_handler),
+ ]
+ )
+ webdir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "web/")
app.router.add_static("/", webdir)
# Add CORS support to all routes
cors = aiohttp_cors.setup(
app,
defaults={
- "*":
- aiohttp_cors.ResourceOptions(
+ "*": aiohttp_cors.ResourceOptions(
allow_credentials=True,
expose_headers="*",
allow_headers="*",
- allow_methods=["POST", "PUT", "DELETE"])
- })
+ allow_methods=["POST", "PUT", "DELETE"],
+ )
+ },
+ )
for route in list(app.router.routes()):
cors.add(route)
self.runner = web.AppRunner(app, access_log=None)
await self.runner.setup()
- http_site = web.TCPSite(self.runner, '0.0.0.0', self.http_port)
+ http_site = web.TCPSite(self.runner, "0.0.0.0", self.http_port)
await http_site.start()
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'])
- https_site = web.TCPSite(self.runner,
- '0.0.0.0',
- self.config['https_port'],
- ssl_context=ssl_context)
+ ssl_context.load_cert_chain(
+ self.config["ssl_certificate"], self.config["ssl_key"]
+ )
+ https_site = web.TCPSite(
+ self.runner,
+ "0.0.0.0",
+ self.config["https_port"],
+ ssl_context=ssl_context,
+ )
await https_site.start()
- LOGGER.info("Started HTTPS webserver on port %s",
- self.config['https_port'])
+ LOGGER.info("Started HTTPS webserver on port %s", self.config["https_port"])
async def index(self, request):
- index_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),
- 'web/index.html')
+ index_file = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)), "web/index.html"
+ )
return web.FileResponse(index_file)
- @routes.get('/api/library/artists')
+ @routes.get("/api/library/artists")
async def library_artists(self, request):
"""Get all library artists."""
- orderby = request.query.get('orderby', 'name')
- provider_filter = request.rel_url.query.get('provider')
+ orderby = request.query.get("orderby", "name")
+ provider_filter = request.rel_url.query.get("provider")
iterator = self.mass.music.library_artists(
- orderby=orderby, provider_filter=provider_filter)
+ orderby=orderby, provider_filter=provider_filter
+ )
return await self.__stream_json(request, iterator)
- @routes.get('/api/library/albums')
+ @routes.get("/api/library/albums")
async def library_albums(self, request):
"""Get all library albums."""
- orderby = request.query.get('orderby', 'name')
- provider_filter = request.rel_url.query.get('provider')
+ orderby = request.query.get("orderby", "name")
+ provider_filter = request.rel_url.query.get("provider")
iterator = self.mass.music.library_albums(
- orderby=orderby, provider_filter=provider_filter)
+ orderby=orderby, provider_filter=provider_filter
+ )
return await self.__stream_json(request, iterator)
- @routes.get('/api/library/tracks')
+ @routes.get("/api/library/tracks")
async def library_tracks(self, request):
"""Get all library tracks."""
- orderby = request.query.get('orderby', 'name')
- provider_filter = request.rel_url.query.get('provider')
+ orderby = request.query.get("orderby", "name")
+ provider_filter = request.rel_url.query.get("provider")
iterator = self.mass.music.library_tracks(
- orderby=orderby, provider_filter=provider_filter)
+ orderby=orderby, provider_filter=provider_filter
+ )
return await self.__stream_json(request, iterator)
- @routes.get('/api/library/radios')
+ @routes.get("/api/library/radios")
async def library_radios(self, request):
"""Get all library radios."""
- orderby = request.query.get('orderby', 'name')
- provider_filter = request.rel_url.query.get('provider')
+ orderby = request.query.get("orderby", "name")
+ provider_filter = request.rel_url.query.get("provider")
iterator = self.mass.music.library_radios(
- orderby=orderby, provider_filter=provider_filter)
+ orderby=orderby, provider_filter=provider_filter
+ )
return await self.__stream_json(request, iterator)
- @routes.get('/api/library/playlists')
+ @routes.get("/api/library/playlists")
async def library_playlists(self, request):
"""Get all library playlists."""
- orderby = request.query.get('orderby', 'name')
- provider_filter = request.rel_url.query.get('provider')
+ orderby = request.query.get("orderby", "name")
+ provider_filter = request.rel_url.query.get("provider")
iterator = self.mass.music.library_playlists(
- orderby=orderby, provider_filter=provider_filter)
+ orderby=orderby, provider_filter=provider_filter
+ )
return await self.__stream_json(request, iterator)
- @routes.put('/api/library')
+ @routes.put("/api/library")
async def library_add(self, request):
"""Add item(s) to the library"""
body = await request.json()
result = await self.mass.music.library_add(media_items)
return web.json_response(result, dumps=json_serializer)
- @routes.delete('/api/library')
+ @routes.delete("/api/library")
async def library_remove(self, request):
"""R remove item(s) from the library"""
body = await request.json()
result = await self.mass.music.library_remove(media_items)
return web.json_response(result, dumps=json_serializer)
- @routes.get('/api/artists/{item_id}')
+ @routes.get("/api/artists/{item_id}")
async def artist(self, request):
""" 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', 'true') != 'false'
- if (item_id is None or provider is None):
- return web.Response(text='invalid item or provider', status=501)
+ item_id = request.match_info.get("item_id")
+ provider = request.rel_url.query.get("provider")
+ lazy = request.rel_url.query.get("lazy", "true") != "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.artist(item_id, provider, lazy=lazy)
return web.json_response(result, dumps=json_serializer)
- @routes.get('/api/albums/{item_id}')
+ @routes.get("/api/albums/{item_id}")
async def album(self, request):
""" 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', 'true') != 'false'
- if (item_id is None or provider is None):
- return web.Response(text='invalid item or provider', status=501)
+ item_id = request.match_info.get("item_id")
+ provider = request.rel_url.query.get("provider")
+ lazy = request.rel_url.query.get("lazy", "true") != "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.album(item_id, provider, lazy=lazy)
return web.json_response(result, dumps=json_serializer)
- @routes.get('/api/tracks/{item_id}')
+ @routes.get("/api/tracks/{item_id}")
async def track(self, request):
""" 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', 'true') != 'false'
- if (item_id is None or provider is None):
- return web.Response(text='invalid item or provider', status=501)
+ item_id = request.match_info.get("item_id")
+ provider = request.rel_url.query.get("provider")
+ lazy = request.rel_url.query.get("lazy", "true") != "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.track(item_id, provider, lazy=lazy)
return web.json_response(result, dumps=json_serializer)
- @routes.get('/api/playlists/{item_id}')
+ @routes.get("/api/playlists/{item_id}")
async def playlist(self, request):
""" 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):
- return web.Response(text='invalid item or provider', status=501)
+ item_id = request.match_info.get("item_id")
+ provider = request.rel_url.query.get("provider")
+ if item_id is None or provider is None:
+ return web.Response(text="invalid item or provider", status=501)
result = await self.mass.music.playlist(item_id, provider)
return web.json_response(result, dumps=json_serializer)
- @routes.get('/api/radios/{item_id}')
+ @routes.get("/api/radios/{item_id}")
async def radio(self, request):
""" 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):
- return web.Response(text='invalid item_id or provider', status=501)
+ item_id = request.match_info.get("item_id")
+ provider = request.rel_url.query.get("provider")
+ if item_id is None or provider is None:
+ return web.Response(text="invalid item_id or provider", status=501)
result = await self.mass.music.radio(item_id, provider)
return web.json_response(result, dumps=json_serializer)
- @routes.get('/api/{media_type}/{media_id}/thumb')
+ @routes.get("/api/{media_type}/{media_id}/thumb")
async def get_image(self, request):
""" get (resized) thumb image """
- media_type_str = request.match_info.get('media_type')
+ 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')
- provider = request.rel_url.query.get('provider')
- if (media_id is None or provider is None):
- return web.Response(text='invalid media_id or provider',
- status=501)
- size = int(request.rel_url.query.get('size', 0))
+ media_id = request.match_info.get("media_id")
+ provider = request.rel_url.query.get("provider")
+ if media_id is None or provider is None:
+ return web.Response(text="invalid media_id or provider", status=501)
+ size = int(request.rel_url.query.get("size", 0))
img_file = await self.mass.music.get_image_thumb(
- media_id, media_type, provider, size)
+ media_id, media_type, provider, size
+ )
if not img_file or not os.path.isfile(img_file):
return web.Response(status=404)
- headers = {
- 'Cache-Control': 'max-age=86400, public',
- 'Pragma': 'public'
- }
+ headers = {"Cache-Control": "max-age=86400, public", "Pragma": "public"}
return web.FileResponse(img_file, headers=headers)
- @routes.get('/api/artists/{item_id}/toptracks')
+ @routes.get("/api/artists/{item_id}/toptracks")
async def artist_toptracks(self, request):
""" 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):
- return web.Response(text='invalid item_id or provider', status=501)
+ item_id = request.match_info.get("item_id")
+ provider = request.rel_url.query.get("provider")
+ if item_id is None or provider is None:
+ return web.Response(text="invalid item_id or provider", status=501)
iterator = self.mass.music.artist_toptracks(item_id, provider)
return await self.__stream_json(request, iterator)
- @routes.get('/api/artists/{item_id}/albums')
+ @routes.get("/api/artists/{item_id}/albums")
async def artist_albums(self, request):
""" 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):
- return web.Response(text='invalid item_id or provider', status=501)
+ item_id = request.match_info.get("item_id")
+ provider = request.rel_url.query.get("provider")
+ if item_id is None or provider is None:
+ return web.Response(text="invalid item_id or provider", status=501)
iterator = self.mass.music.artist_albums(item_id, provider)
return await self.__stream_json(request, iterator)
- @routes.get('/api/playlists/{item_id}/tracks')
+ @routes.get("/api/playlists/{item_id}/tracks")
async def playlist_tracks(self, request):
""" 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):
- return web.Response(text='invalid item_id or provider', status=501)
+ item_id = request.match_info.get("item_id")
+ provider = request.rel_url.query.get("provider")
+ if item_id is None or provider is None:
+ return web.Response(text="invalid item_id or provider", status=501)
iterator = self.mass.music.playlist_tracks(item_id, provider)
return await self.__stream_json(request, iterator)
- @routes.put('/api/playlists/{item_id}/tracks')
+ @routes.put("/api/playlists/{item_id}/tracks")
async def add_playlist_tracks(self, request):
"""Add tracks to (editable) playlist."""
- item_id = request.match_info.get('item_id')
+ item_id = request.match_info.get("item_id")
body = await request.json()
tracks = await self.__media_items_from_body(body)
result = await self.mass.music.add_playlist_tracks(item_id, tracks)
return web.json_response(result, dumps=json_serializer)
- @routes.delete('/api/playlists/{item_id}/tracks')
+ @routes.delete("/api/playlists/{item_id}/tracks")
async def remove_playlist_tracks(self, request):
"""Remove tracks from (editable) playlist."""
- item_id = request.match_info.get('item_id')
+ item_id = request.match_info.get("item_id")
body = await request.json()
tracks = await self.__media_items_from_body(body)
result = await self.mass.music.remove_playlist_tracks(item_id, tracks)
return web.json_response(result, dumps=json_serializer)
- @routes.get('/api/albums/{item_id}/tracks')
+ @routes.get("/api/albums/{item_id}/tracks")
async def album_tracks(self, request):
""" get album 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):
- return web.Response(text='invalid item_id or provider', status=501)
+ item_id = request.match_info.get("item_id")
+ provider = request.rel_url.query.get("provider")
+ if item_id is None or provider is None:
+ return web.Response(text="invalid item_id or provider", status=501)
iterator = self.mass.music.album_tracks(item_id, provider)
return await self.__stream_json(request, iterator)
- @routes.get('/api/search')
+ @routes.get("/api/search")
async def search(self, request):
""" search database or providers """
- searchquery = request.rel_url.query.get('query')
- media_types_query = request.rel_url.query.get('media_types')
- limit = request.rel_url.query.get('limit', 5)
- online = request.rel_url.query.get('online', False)
+ searchquery = request.rel_url.query.get("query")
+ media_types_query = request.rel_url.query.get("media_types")
+ limit = request.rel_url.query.get("limit", 5)
+ online = request.rel_url.query.get("online", False)
media_types = []
if not media_types_query or "artists" in media_types_query:
media_types.append(MediaType.Artist)
if not media_types_query or "radios" in media_types_query:
media_types.append(MediaType.Radio)
# get results from database
- result = await self.mass.music.search(searchquery,
- media_types,
- limit=limit,
- online=online)
+ result = await self.mass.music.search(
+ searchquery, media_types, limit=limit, online=online
+ )
return web.json_response(result, dumps=json_serializer)
- @routes.get('/api/players')
+ @routes.get("/api/players")
async def players(self, request):
""" get all players """
players = list(self.mass.players.players)
players.sort(key=lambda x: x.name, reverse=False)
return web.json_response(players, dumps=json_serializer)
- @routes.post('/api/players/{player_id}/cmd/{cmd}')
+ @routes.post("/api/players/{player_id}/cmd/{cmd}")
async def player_command(self, request):
""" issue player command"""
result = False
- player_id = request.match_info.get('player_id')
+ player_id = request.match_info.get("player_id")
player = await self.mass.players.get_player(player_id)
if not player:
- return web.Response(text='invalid player', status=404)
- cmd = request.match_info.get('cmd')
+ return web.Response(text="invalid player", status=404)
+ cmd = request.match_info.get("cmd")
cmd_args = await request.json()
player_cmd = getattr(player, cmd, None)
if player_cmd and cmd_args is not None:
elif player_cmd:
result = await player_cmd()
else:
- return web.Response(text='invalid command', status=501)
+ return web.Response(text="invalid command", status=501)
return web.json_response(result, dumps=json_serializer)
- @routes.post('/api/players/{player_id}/play_media/{queue_opt}')
+ @routes.post("/api/players/{player_id}/play_media/{queue_opt}")
async def player_play_media(self, request):
""" issue player play_media command"""
- player_id = request.match_info.get('player_id')
+ player_id = request.match_info.get("player_id")
player = await self.mass.players.get_player(player_id)
if not player:
return web.Response(status=404)
- queue_opt = request.match_info.get('queue_opt', 'play')
+ queue_opt = request.match_info.get("queue_opt", "play")
body = await request.json()
media_items = await self.__media_items_from_body(body)
- result = await self.mass.players.play_media(player_id, media_items,
- queue_opt)
+ result = await self.mass.players.play_media(player_id, media_items, queue_opt)
return web.json_response(result, dumps=json_serializer)
- @routes.get('/api/players/{player_id}/queue/items/{queue_item}')
+ @routes.get("/api/players/{player_id}/queue/items/{queue_item}")
async def player_queue_item(self, request):
""" return item (by index or queue item id) from the player's queue """
- player_id = request.match_info.get('player_id')
- item_id = request.match_info.get('queue_item')
+ player_id = request.match_info.get("player_id")
+ item_id = request.match_info.get("queue_item")
player = await self.mass.players.get_player(player_id)
try:
item_id = int(item_id)
queue_item = await player.queue.by_item_id(item_id)
return web.json_response(queue_item, dumps=json_serializer)
- @routes.get('/api/players/{player_id}/queue/items')
+ @routes.get("/api/players/{player_id}/queue/items")
async def player_queue_items(self, request):
""" return the items in the player's queue """
- player_id = request.match_info.get('player_id')
+ player_id = request.match_info.get("player_id")
player = await self.mass.players.get_player(player_id)
async def queue_tracks_iter():
return await self.__stream_json(request, queue_tracks_iter())
- @routes.get('/api/players/{player_id}/queue')
+ @routes.get("/api/players/{player_id}/queue")
async def player_queue(self, request):
""" return the player queue details """
- player_id = request.match_info.get('player_id')
+ player_id = request.match_info.get("player_id")
player = await self.mass.players.get_player(player_id)
return web.json_response(player.queue, dumps=json_serializer)
- @routes.put('/api/players/{player_id}/queue/{cmd}')
+ @routes.put("/api/players/{player_id}/queue/{cmd}")
async def player_queue_cmd(self, request):
""" change the player queue details """
- player_id = request.match_info.get('player_id')
+ player_id = request.match_info.get("player_id")
player = await self.mass.players.get_player(player_id)
- cmd = request.match_info.get('cmd')
+ cmd = request.match_info.get("cmd")
cmd_args = await request.json()
- if cmd == 'repeat_enabled':
+ if cmd == "repeat_enabled":
player.queue.repeat_enabled = cmd_args
- elif cmd == 'shuffle_enabled':
+ elif cmd == "shuffle_enabled":
player.queue.shuffle_enabled = cmd_args
- elif cmd == 'clear':
+ elif cmd == "clear":
await player.queue.clear()
- elif cmd == 'index':
+ elif cmd == "index":
await player.queue.play_index(cmd_args)
- elif cmd == 'move_up':
+ elif cmd == "move_up":
await player.queue.move_item(cmd_args, -1)
- elif cmd == 'move_down':
+ elif cmd == "move_down":
await player.queue.move_item(cmd_args, 1)
- elif cmd == 'next':
+ elif cmd == "next":
await player.queue.move_item(cmd_args, 0)
return web.json_response(player.queue, dumps=json_serializer)
- @routes.get('/api/players/{player_id}')
+ @routes.get("/api/players/{player_id}")
async def player(self, request):
""" get single player """
- player_id = request.match_info.get('player_id')
+ player_id = request.match_info.get("player_id")
player = await self.mass.players.get_player(player_id)
if not player:
- return web.Response(text='invalid player', status=404)
+ return web.Response(text="invalid player", status=404)
return web.json_response(player, dumps=json_serializer)
- @routes.get('/api/config')
+ @routes.get("/api/config")
async def get_config(self, request):
""" get the config """
return web.json_response(self.mass.config)
- @routes.put('/api/config/{key}/{subkey}')
+ @routes.put("/api/config/{key}/{subkey}")
async def put_config(self, request):
""" save (partial) config """
- conf_key = request.match_info.get('key')
- conf_subkey = request.match_info.get('subkey')
+ conf_key = request.match_info.get("key")
+ conf_subkey = request.match_info.get("subkey")
new_values = await request.json()
LOGGER.debug(
- f'save config called for {conf_key}/{conf_subkey} - new value: {new_values}'
+ f"save config called for {conf_key}/{conf_subkey} - new value: {new_values}"
)
cur_values = self.mass.config[conf_key][conf_subkey]
- result = {
- "success": True,
- "restart_required": False,
- "settings_changed": False
- }
+ result = {"success": True, "restart_required": False, "settings_changed": False}
if cur_values != new_values:
# config changed
result["settings_changed"] = True
if conf_key == CONF_KEY_PLAYERSETTINGS:
# player settings: force update of player
self.mass.event_loop.create_task(
- self.mass.players.trigger_update(conf_subkey))
+ self.mass.players.trigger_update(conf_subkey)
+ )
elif conf_key == CONF_KEY_MUSICPROVIDERS:
# (re)load music provider module
self.mass.event_loop.create_task(
- self.mass.music.load_modules(conf_subkey))
+ self.mass.music.load_modules(conf_subkey)
+ )
elif conf_key == CONF_KEY_PLAYERPROVIDERS:
# (re)load player provider module
self.mass.event_loop.create_task(
- self.mass.players.load_modules(conf_subkey))
+ self.mass.players.load_modules(conf_subkey)
+ )
else:
# other settings need restart
result["restart_required"] = True
# process incoming messages
async for msg in ws:
if msg.type == aiohttp.WSMsgType.ERROR:
- LOGGER.debug('ws connection closed with exception %s' %
- ws.exception())
+ LOGGER.debug(
+ "ws connection closed with exception %s" % ws.exception()
+ )
elif msg.type != aiohttp.WSMsgType.TEXT:
LOGGER.warning(msg.data)
else:
data = msg.json()
# echo the websocket message on event bus
# can be picked up by other modules, e.g. the webplayer
- await self.mass.signal_event(data['message'],
- data['message_details'])
+ await self.mass.signal_event(
+ data["message"], data["message_details"]
+ )
except (Exception, AssertionError, asyncio.CancelledError) as exc:
LOGGER.warning("Websocket disconnected - %s" % str(exc))
finally:
await self.mass.remove_event_listener(cb_id)
- LOGGER.debug('websocket connection closed')
+ LOGGER.debug("websocket connection closed")
return ws
async def json_rpc(self, request):
"""
data = await request.json()
LOGGER.debug("jsonrpc: %s" % data)
- params = data['params']
+ params = data["params"]
player_id = params[0]
cmds = params[1]
cmd_str = " ".join(cmds)
player = await self.mass.players.get_player(player_id)
if not player:
return web.Response(status=404)
- if cmd_str == 'play':
+ if cmd_str == "play":
await player.play()
- elif cmd_str == 'pause':
+ elif cmd_str == "pause":
await player.pause()
- elif cmd_str == 'stop':
+ elif cmd_str == "stop":
await player.stop()
- elif cmd_str == 'next':
+ elif cmd_str == "next":
await player.next()
- elif cmd_str == 'previous':
+ elif cmd_str == "previous":
await player.previous()
- elif 'power' in cmd_str:
+ elif "power" in cmd_str:
args = cmds[1] if len(cmds) > 1 else None
await player.power(args)
- elif cmd_str == 'playlist index +1':
+ elif cmd_str == "playlist index +1":
await player.next()
- elif cmd_str == 'playlist index -1':
+ elif cmd_str == "playlist index -1":
await player.previous()
- elif 'mixer volume' in cmd_str and '+' in cmds[2]:
- volume_level = player.volume_level + int(cmds[2].split('+')[1])
+ elif "mixer volume" in cmd_str and "+" in cmds[2]:
+ volume_level = player.volume_level + int(cmds[2].split("+")[1])
await player.volume_set(volume_level)
- elif 'mixer volume' in cmd_str and '-' in cmds[2]:
- volume_level = player.volume_level - int(cmds[2].split('-')[1])
+ elif "mixer volume" in cmd_str and "-" in cmds[2]:
+ volume_level = player.volume_level - int(cmds[2].split("-")[1])
await player.volume_set(volume_level)
- elif 'mixer volume' in cmd_str:
+ elif "mixer volume" in cmd_str:
await player.volume_set(cmds[2])
- elif cmd_str == 'mixer muting 1':
+ elif cmd_str == "mixer muting 1":
await player.volume_mute(True)
- elif cmd_str == 'mixer muting 0':
+ elif cmd_str == "mixer muting 0":
await player.volume_mute(False)
- elif cmd_str == 'button volup':
+ elif cmd_str == "button volup":
await player.volume_up()
- elif cmd_str == 'button voldown':
+ elif cmd_str == "button voldown":
await player.volume_down()
- elif cmd_str == 'button power':
+ elif cmd_str == "button power":
await player.power_toggle()
else:
- return web.Response(text='command not supported')
- return web.Response(text='success')
+ return web.Response(text="command not supported")
+ return web.Response(text="success")
async def __media_items_from_body(self, data):
"""Helper to turn posted body data into media items."""
data = [data]
media_items = []
for item in data:
- media_item = await self.mass.music.item(item['item_id'],
- item['media_type'],
- item['provider'],
- lazy=True)
+ media_item = await self.mass.music.item(
+ item["item_id"], item["media_type"], item["provider"], lazy=True
+ )
media_items.append(media_item)
return media_items
async def __stream_json(self, request, iterator):
""" stream items from async iterator as json object """
- resp = web.StreamResponse(status=200,
- reason='OK',
- headers={'Content-Type': 'application/json'})
+ resp = web.StreamResponse(
+ status=200, reason="OK", headers={"Content-Type": "application/json"}
+ )
await resp.prepare(request)
# write json open tag
json_response = '{ "items": ['
- await resp.write(json_response.encode('utf-8'))
+ await resp.write(json_response.encode("utf-8"))
count = 0
async for item in iterator:
# write each item into the items object of the json
if count:
- json_response = ',' + json_serializer(item)
+ json_response = "," + json_serializer(item)
else:
json_response = json_serializer(item)
- await resp.write(json_response.encode('utf-8'))
+ await resp.write(json_response.encode("utf-8"))
count += 1
# write json close tag
json_response = '], "count": %s }' % count
- await resp.write((json_response).encode('utf-8'))
+ await resp.write((json_response).encode("utf-8"))
await resp.write_eof()
return resp
[MESSAGES CONTROL]
# Reasons disabled:
-# format - handled by black
# locally-disabled - it spams too much
-# duplicate-code - unavoidable
-# cyclic-import - doesn't test if both import on load
-# abstract-class-little-used - prevents from setting right foundation
-# unused-argument - generic callbacks and setup methods create a lot of warnings
-# global-statement - used for the on-demand requirement installation
-# redefined-variable-type - this is Python, we're duck typing!
# too-many-* - are not enforced for the sake of readability
# too-few-* - same as too-many-*
-# abstract-method - with intro of async there are always methods missing
-# inconsistent-return-statements - doesn't handle raise
-# unnecessary-pass - readability for functions which only contain pass
# import-outside-toplevel - TODO
-# too-many-ancestors - it's too strict.
disable=
- format,
- abstract-class-little-used,
- abstract-method,
- cyclic-import,
- duplicate-code,
- global-statement,
+ bad-continuation,
+ fixme,
import-outside-toplevel,
- inconsistent-return-statements,
locally-disabled,
- not-context-manager,
- redefined-variable-type,
too-few-public-methods,
- too-many-ancestors,
- too-many-arguments,
- too-many-branches,
- too-many-instance-attributes,
- too-many-lines,
- too-many-locals,
too-many-public-methods,
- too-many-return-statements,
- too-many-statements,
- too-many-boolean-expressions,
- unnecessary-pass,
- unused-argument
[REPORTS]
score=no
W504
[isort]
-# https://github.com/timothycrosley/isort
-# https://github.com/timothycrosley/isort/wiki/isort-Settings
-# splits long import on multiple lines indented by 4 spaces
multi_line_output = 3
-include_trailing_comma=True
-force_grid_wrap=0
-use_parentheses=True
-line_length=88
-indent = " "
-# by default isort don't check module indexes
-not_skip = __init__.py
-# will group `import x` and `from x import` of the same module.
-force_sort_within_sections = true
-sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
-default_section = THIRDPARTY
-known_first_party = homeassistant,tests
-forced_separate = tests
-combine_as_imports = true
\ No newline at end of file
+include_trailing_comma = True
+force_grid_wrap = 0
+use_parentheses = True
+line_length = 88
+
+[mypy]
+follow_imports = skip
+ignore_missing_imports = true
+check_untyped_defs = true
+disallow_incomplete_defs = true
+disallow_untyped_calls = true
+disallow_untyped_defs = true
+warn_return_any = true
+warn_unreachable = true
+warn_unused_ignores = true
+warn_incomplete_stub = true
+warn_redundant_casts = true
+warn_unused_configs = true
+
+[pydocstyle]
+add-ignore = D202
\ No newline at end of file
# sudo python3 setup.py sdist bdist_wheel
# sudo python3 -m twine upload dist/*
-import setuptools
import os
+import setuptools
+
VERSION = "0.0.20"
NAME = "music_assistant"
with open("README.md", "r") as fh:
LONG_DESC = fh.read()
-with open('requirements.txt') as f:
+with open("requirements.txt") as f:
INSTALL_REQUIRES = f.read().splitlines()
if os.name != "nt":
INSTALL_REQUIRES.append("uvloop")
setuptools.setup(
name=NAME,
version=VERSION,
- author='Marcel van der Veldt',
- author_email='marcelveldt@users.noreply.github.com',
- description='Music library manager and player based on sox.',
+ author="Marcel van der Veldt",
+ author_email="marcelveldt@users.noreply.github.com",
+ description="Music library manager and player based on sox.",
long_description=LONG_DESC,
long_description_content_type="text/markdown",
- url = 'https://github.com/marcelveldt/musicassistant.git',
- packages=['music_assistant'],
+ url="https://github.com/marcelveldt/musicassistant.git",
+ packages=["music_assistant"],
classifiers=[
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
],
install_requires=INSTALL_REQUIRES,
- )
\ No newline at end of file
+)
--- /dev/null
+[tox]
+envlist = py36, py37, py38, lint, mypy
+skip_missing_interpreters = True
+
+[gh-actions]
+python =
+ 3.6: py36, lint, mypy
+ 3.7: py37
+ 3.8: py38
+
+[testenv]
+commands =
+ pytest --timeout=30 --cov=music_assistant --cov-report= {posargs}
+deps =
+ -rrequirements.txt
+
+[testenv:lint]
+basepython = python3
+ignore_errors = True
+commands =
+ black --check ./
+ flake8 music_assistant test
+ pylint music_assistant test
+ pydocstyle music_assistant test
+deps =
+ -rrequirements_lint.txt
+ -rrequirements_test.txt
+
+[testenv:mypy]
+basepython = python3
+ignore_errors = True
+commands =
+ mypy music_assistant
+deps =
+ -rrequirements_lint.txt