From eae7edc37588616014f50da912b18a70e3cce893 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 12 May 2022 02:07:14 +0200 Subject: [PATCH] Refactor config and setup (#309) * Allow sync to be started manually --- examples/full.py | 37 +++--- music_assistant/controllers/music/__init__.py | 122 +++++++++--------- .../music}/providers/__init__.py | 0 .../music}/providers/filesystem.py | 50 +++---- .../librespot/linux/librespot-aarch64 | Bin .../providers}/librespot/linux/librespot-arm | Bin .../librespot/linux/librespot-armhf | Bin .../librespot/linux/librespot-armv7 | Bin .../librespot/linux/librespot-x86_64 | Bin .../music/providers}/librespot/osx/librespot | Bin .../librespot/windows/librespot.exe | Bin .../music}/providers/qobuz.py | 59 +++++---- .../music/providers/spotify.py} | 59 +++++---- .../music}/providers/tunein.py | 24 ++-- music_assistant/controllers/stream.py | 7 +- music_assistant/helpers/database.py | 5 +- music_assistant/helpers/util.py | 19 --- music_assistant/mass.py | 13 +- music_assistant/models/config.py | 35 +++++ music_assistant/models/enums.py | 4 - music_assistant/models/provider.py | 6 +- 21 files changed, 234 insertions(+), 206 deletions(-) rename music_assistant/{ => controllers/music}/providers/__init__.py (100%) rename music_assistant/{ => controllers/music}/providers/filesystem.py (95%) rename music_assistant/{providers/spotify => controllers/music/providers}/librespot/linux/librespot-aarch64 (100%) rename music_assistant/{providers/spotify => controllers/music/providers}/librespot/linux/librespot-arm (100%) rename music_assistant/{providers/spotify => controllers/music/providers}/librespot/linux/librespot-armhf (100%) rename music_assistant/{providers/spotify => controllers/music/providers}/librespot/linux/librespot-armv7 (100%) rename music_assistant/{providers/spotify => controllers/music/providers}/librespot/linux/librespot-x86_64 (100%) rename music_assistant/{providers/spotify => controllers/music/providers}/librespot/osx/librespot (100%) rename music_assistant/{providers/spotify => controllers/music/providers}/librespot/windows/librespot.exe (100%) rename music_assistant/{ => controllers/music}/providers/qobuz.py (95%) rename music_assistant/{providers/spotify/__init__.py => controllers/music/providers/spotify.py} (95%) rename music_assistant/{ => controllers/music}/providers/tunein.py (91%) create mode 100644 music_assistant/models/config.py diff --git a/examples/full.py b/examples/full.py index 72a6f5ff..a01b739c 100644 --- a/examples/full.py +++ b/examples/full.py @@ -5,12 +5,10 @@ import logging import os from music_assistant.mass import MusicAssistant +from music_assistant.models.config import MassConfig from music_assistant.models.player import Player, PlayerState from music_assistant.models.player_queue import RepeatMode -from music_assistant.providers.filesystem import FileSystemProvider -from music_assistant.providers.qobuz import QobuzProvider -from music_assistant.providers.spotify import SpotifyProvider -from music_assistant.providers.tunein import TuneInProvider + parser = argparse.ArgumentParser(description="MusicAssistant") parser.add_argument( @@ -76,15 +74,20 @@ if not os.path.isdir(data_dir): db_file = os.path.join(data_dir, "music_assistant.db") -providers = [] -if args.spotify_username and args.spotify_password: - providers.append(SpotifyProvider(args.spotify_username, args.spotify_password)) -if args.qobuz_username and args.qobuz_password: - providers.append(QobuzProvider(args.qobuz_username, args.qobuz_password)) -if args.tunein_username: - providers.append(TuneInProvider(args.tunein_username)) -if args.musicdir: - providers.append(FileSystemProvider(args.musicdir, args.playlistdir)) +mass_conf = MassConfig( + database_url=f"sqlite:///{db_file}", + spotify_enabled=args.spotify_username and args.spotify_password, + spotify_username=args.spotify_username, + spotify_password=args.spotify_password, + qobuz_enabled=args.qobuz_username and args.qobuz_password, + qobuz_username=args.qobuz_username, + qobuz_password=args.qobuz_password, + tunein_enabled=args.tunein_username is not None, + tunein_username=args.tunein_username, + filesystem_enabled=args.musicdir is not None, + filesystem_music_dir=args.musicdir, + filesystem_playlists_dir=args.playlistdir, +) class TestPlayer(Player): @@ -147,11 +150,11 @@ async def main(): asyncio.get_event_loop().set_debug(args.debug) - async with MusicAssistant(f"sqlite:///{db_file}") as mass: + async with MusicAssistant(mass_conf) as mass: + + # start sync + await mass.music.start_sync() - # register music provider(s) - for prov in providers: - await mass.music.register_provider(prov) # get some data artists = await mass.music.artists.count() print(f"Got {artists} artists in library") diff --git a/music_assistant/controllers/music/__init__.py b/music_assistant/controllers/music/__init__.py index 1546b61b..02a36328 100755 --- a/music_assistant/controllers/music/__init__.py +++ b/music_assistant/controllers/music/__init__.py @@ -19,14 +19,8 @@ from music_assistant.helpers.database import ( ) from music_assistant.helpers.datetime import utc_timestamp from music_assistant.helpers.uri import parse_uri -from music_assistant.helpers.util import run_periodic -from music_assistant.models.enums import EventType, MediaType -from music_assistant.models.errors import ( - AlreadyRegisteredError, - MusicAssistantError, - SetupFailedError, -) -from music_assistant.models.event import MassEvent +from music_assistant.models.enums import MediaType +from music_assistant.models.errors import MusicAssistantError, SetupFailedError from music_assistant.models.media_items import ( MediaItem, MediaItemProviderId, @@ -34,9 +28,16 @@ from music_assistant.models.media_items import ( ) from music_assistant.models.provider import MusicProvider +from .providers.filesystem import FileSystemProvider +from .providers.qobuz import QobuzProvider +from .providers.spotify import SpotifyProvider +from .providers.tunein import TuneInProvider + if TYPE_CHECKING: from music_assistant.mass import MusicAssistant +PROVIDERS = (FileSystemProvider, QobuzProvider, SpotifyProvider, TuneInProvider) + class MusicController: """Several helpers around the musicproviders.""" @@ -54,7 +55,26 @@ class MusicController: async def setup(self): """Async initialize of module.""" - self.mass.create_task(self.__periodic_sync) + # register providers + for prov in PROVIDERS: + await self._register_provider(prov()) + + async def start_sync(self, schedule: Optional[float] = 3) -> None: + """ + Start running the sync of all registred providers. + + schedule: schedule syncjob every X hours, set to None for just a manual sync run. + """ + + async def do_sync(): + while True: + for prov in self.providers: + await self.run_provider_sync(prov.id) + if schedule is None: + return + await asyncio.sleep(3600 * schedule) + + self.mass.create_task(do_sync()) @property def provider_count(self) -> int: @@ -73,32 +93,6 @@ class MusicController: self.logger.warning("Provider %s is not available", provider_id) return prov - async def register_provider(self, provider: MusicProvider) -> None: - """Register a music provider.""" - if provider.id in self._providers: - raise AlreadyRegisteredError( - f"Provider {provider.id} is already registered" - ) - try: - provider.mass = self.mass - provider.cache = self.mass.cache - provider.logger = self.logger.getChild(provider.id) - await provider.setup() - except Exception as err: # pylint: disable=broad-except - raise SetupFailedError( - f"Setup failed of provider {provider.id}: {str(err)}" - ) from err - else: - self._providers[provider.id] = provider - self.mass.signal_event( - MassEvent( - EventType.PROVIDER_REGISTERED, - object_id=provider.id, - data=provider.id, - ) - ) - self.mass.create_task(self.run_provider_sync(provider.id)) - async def search( self, search_query, media_types: List[MediaType], limit: int = 10 ) -> List[MediaItemType]: @@ -359,13 +353,8 @@ class MusicController: job_desc, ) - async def trigger_sync(self) -> None: - """Trigger sync of all providers.""" - for prov in self.providers: - await self.run_provider_sync(prov.id) - async def run_provider_sync(self, provider_id: str) -> None: - """Run library sync for a provider.""" + """Run/schedule library sync for a provider.""" provider = self.get_provider(provider_id) if not provider: return @@ -379,6 +368,21 @@ class MusicController: allow_duplicate=False, ) + def get_controller( + self, media_type: MediaType + ) -> ArtistsController | AlbumsController | TracksController | RadioController | PlaylistController: + """Return controller for MediaType.""" + if media_type == MediaType.ARTIST: + return self.artists + if media_type == MediaType.ALBUM: + return self.albums + if media_type == MediaType.TRACK: + return self.tracks + if media_type == MediaType.RADIO: + return self.radio + if media_type == MediaType.PLAYLIST: + return self.playlists + async def _library_items_sync( self, media_type: MediaType, provider_id: str ) -> None: @@ -431,29 +435,19 @@ class MusicController: if provider_id == "filesystem": if db_item := await controller.get_db_item(item_id): db_item.provider_ids = { - x - for x in db_item.provider_ids - if not (x.provider == provider_id) + x for x in db_item.provider_ids if x.provider != provider_id } await controller.update_db_item(item_id, db_item, True) - def get_controller( - self, media_type: MediaType - ) -> ArtistsController | AlbumsController | TracksController | RadioController | PlaylistController: - """Return controller for MediaType.""" - if media_type == MediaType.ARTIST: - return self.artists - if media_type == MediaType.ALBUM: - return self.albums - if media_type == MediaType.TRACK: - return self.tracks - if media_type == MediaType.RADIO: - return self.radio - if media_type == MediaType.PLAYLIST: - return self.playlists - - @run_periodic(3 * 3600, True) - async def __periodic_sync(self): - """Periodically sync all providers.""" - for prov in self.providers: - await self.run_provider_sync(prov.id) + async def _register_provider(self, provider: MusicProvider) -> None: + """Register a music provider.""" + try: + provider.mass = self.mass + provider.cache = self.mass.cache + provider.logger = self.logger.getChild(provider.id) + if await provider.setup(): + self._providers[provider.id] = provider + except Exception as err: # pylint: disable=broad-except + raise SetupFailedError( + f"Setup failed of provider {provider.id}: {str(err)}" + ) from err diff --git a/music_assistant/providers/__init__.py b/music_assistant/controllers/music/providers/__init__.py similarity index 100% rename from music_assistant/providers/__init__.py rename to music_assistant/controllers/music/providers/__init__.py diff --git a/music_assistant/providers/filesystem.py b/music_assistant/controllers/music/providers/filesystem.py similarity index 95% rename from music_assistant/providers/filesystem.py rename to music_assistant/controllers/music/providers/filesystem.py index 0fea27f3..a9a1707a 100644 --- a/music_assistant/providers/filesystem.py +++ b/music_assistant/controllers/music/providers/filesystem.py @@ -57,35 +57,37 @@ class FileSystemProvider(MusicProvider): Should be compatible with LMS """ - def __init__(self, music_dir: str, playlist_dir: Optional[str] = None) -> None: - """ - Initialize the Filesystem provider. - - music_dir: Directory on disk containing music files - playlist_dir: Directory on disk containing playlist files (optional) + _attr_id = "filesystem" + _attr_name = "Filesystem" + _playlists_dir = "" + _music_dir = "" + _attr_supported_mediatypes = [ + MediaType.ARTIST, + MediaType.ALBUM, + MediaType.TRACK, + MediaType.PLAYLIST, + ] + + async def setup(self) -> bool: + """Handle async initialization of the provider.""" + if not self.mass.config.filesystem_enabled: + return False - """ - self._attr_id = "filesystem" - self._attr_name = "Filesystem" - self._playlists_dir = playlist_dir - self._music_dir = music_dir - self._attr_supported_mediatypes = [ - MediaType.ARTIST, - MediaType.ALBUM, - MediaType.TRACK, - ] - if playlist_dir is not None: - self._attr_supported_mediatypes.append(MediaType.PLAYLIST) - self._cached_tracks: List[Track] = [] + self._music_dir = self.mass.config.filesystem_music_dir + self._playlists_dir = ( + self.mass.config.filesystem_playlists_dir or self._music_dir + ) - async def setup(self) -> None: - """Handle async initialization of the provider.""" if not os.path.isdir(self._music_dir): - raise FileNotFoundError(f"Music Directory {self._music_dir} does not exist") - if self._playlists_dir is not None and not os.path.isdir(self._playlists_dir): - raise FileNotFoundError( + raise MediaNotFoundError( + f"Music Directory {self._music_dir} does not exist" + ) + + if not os.path.isdir(self._playlists_dir): + raise MediaNotFoundError( f"Playlist Directory {self._playlists_dir} does not exist" ) + return True async def search( self, search_query: str, media_types=Optional[List[MediaType]], limit: int = 5 diff --git a/music_assistant/providers/spotify/librespot/linux/librespot-aarch64 b/music_assistant/controllers/music/providers/librespot/linux/librespot-aarch64 similarity index 100% rename from music_assistant/providers/spotify/librespot/linux/librespot-aarch64 rename to music_assistant/controllers/music/providers/librespot/linux/librespot-aarch64 diff --git a/music_assistant/providers/spotify/librespot/linux/librespot-arm b/music_assistant/controllers/music/providers/librespot/linux/librespot-arm similarity index 100% rename from music_assistant/providers/spotify/librespot/linux/librespot-arm rename to music_assistant/controllers/music/providers/librespot/linux/librespot-arm diff --git a/music_assistant/providers/spotify/librespot/linux/librespot-armhf b/music_assistant/controllers/music/providers/librespot/linux/librespot-armhf similarity index 100% rename from music_assistant/providers/spotify/librespot/linux/librespot-armhf rename to music_assistant/controllers/music/providers/librespot/linux/librespot-armhf diff --git a/music_assistant/providers/spotify/librespot/linux/librespot-armv7 b/music_assistant/controllers/music/providers/librespot/linux/librespot-armv7 similarity index 100% rename from music_assistant/providers/spotify/librespot/linux/librespot-armv7 rename to music_assistant/controllers/music/providers/librespot/linux/librespot-armv7 diff --git a/music_assistant/providers/spotify/librespot/linux/librespot-x86_64 b/music_assistant/controllers/music/providers/librespot/linux/librespot-x86_64 similarity index 100% rename from music_assistant/providers/spotify/librespot/linux/librespot-x86_64 rename to music_assistant/controllers/music/providers/librespot/linux/librespot-x86_64 diff --git a/music_assistant/providers/spotify/librespot/osx/librespot b/music_assistant/controllers/music/providers/librespot/osx/librespot similarity index 100% rename from music_assistant/providers/spotify/librespot/osx/librespot rename to music_assistant/controllers/music/providers/librespot/osx/librespot diff --git a/music_assistant/providers/spotify/librespot/windows/librespot.exe b/music_assistant/controllers/music/providers/librespot/windows/librespot.exe similarity index 100% rename from music_assistant/providers/spotify/librespot/windows/librespot.exe rename to music_assistant/controllers/music/providers/librespot/windows/librespot.exe diff --git a/music_assistant/providers/qobuz.py b/music_assistant/controllers/music/providers/qobuz.py similarity index 95% rename from music_assistant/providers/qobuz.py rename to music_assistant/controllers/music/providers/qobuz.py index 29179d5a..0f8f10a9 100644 --- a/music_assistant/providers/qobuz.py +++ b/music_assistant/controllers/music/providers/qobuz.py @@ -40,33 +40,36 @@ from music_assistant.models.provider import MusicProvider class QobuzProvider(MusicProvider): """Provider for the Qobux music service.""" - def __init__(self, username: str, password: str) -> None: - """Initialize the Spotify provider.""" - self._attr_id = "qobuz" - self._attr_name = "Qobuz" - self._attr_supported_mediatypes = [ - MediaType.ARTIST, - MediaType.ALBUM, - MediaType.TRACK, - MediaType.PLAYLIST, - ] - self._username = username - self._password = password - self.__user_auth_info = None - self._throttler = Throttler(rate_limit=4, period=1) - - async def setup(self) -> None: + _attr_id = "qobuz" + _attr_name = "Qobuz" + _attr_supported_mediatypes = [ + MediaType.ARTIST, + MediaType.ALBUM, + MediaType.TRACK, + MediaType.PLAYLIST, + ] + _user_auth_info = None + _throttler = Throttler(rate_limit=4, period=1) + + async def setup(self) -> bool: """Handle async initialization of the provider.""" + if not self.mass.config.qobuz_enabled: + return False + if not self.mass.config.qobuz_username or not self.mass.config.qobuz_password: + raise LoginFailed("Invalid login credentials") # try to get a token, raise if that fails token = await self._auth_token() if not token: - raise LoginFailed(f"Login failed for user {self._username}") + raise LoginFailed( + f"Login failed for user {self.mass.config.qobuz_username}" + ) # subscribe to stream events so we can report playback to Qobuz self.mass.subscribe( self.on_stream_event, (EventType.STREAM_STARTED, EventType.STREAM_ENDED), id_filter=self.id, ) + return True async def search( self, search_query: str, media_types=Optional[List[MediaType]], limit: int = 5 @@ -385,16 +388,16 @@ class QobuzProvider(MusicProvider): We use this to report playback start/stop to qobuz. """ - if not self.__user_auth_info: + if not self._user_auth_info: return # TODO: need to figure out if the streamed track is purchased by user # https://www.qobuz.com/api.json/0.2/purchase/getUserPurchasesIds?limit=5000&user_id=xxxxxxx # {"albums":{"total":0,"items":[]},"tracks":{"total":0,"items":[]},"user":{"id":xxxx,"login":"xxxxx"}} if event.type == EventType.STREAM_STARTED: # 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"] + 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 = event.data.details["format_id"] timestamp = int(time.time()) events = [ @@ -415,7 +418,7 @@ class QobuzProvider(MusicProvider): await self._post_data("track/reportStreamingStart", data=events) elif event.type == EventType.STREAM_ENDED: # report streaming ended to qobuz - user_id = self.__user_auth_info["user"]["id"] + user_id = self._user_auth_info["user"]["id"] await self._get_data( "/track/reportStreamingEnd", user_id=user_id, @@ -618,7 +621,7 @@ class QobuzProvider(MusicProvider): ) ) playlist.is_editable = ( - playlist_obj["owner"]["id"] == self.__user_auth_info["user"]["id"] + playlist_obj["owner"]["id"] == self._user_auth_info["user"]["id"] or playlist_obj["is_collaborative"] ) if img := self.__get_image(playlist_obj): @@ -628,16 +631,16 @@ class QobuzProvider(MusicProvider): async def _auth_token(self): """Login to qobuz and store the token.""" - if self.__user_auth_info: - return self.__user_auth_info["user_auth_token"] + if self._user_auth_info: + return self._user_auth_info["user_auth_token"] params = { - "username": self._username, - "password": self._password, + "username": self.mass.config.qobuz_username, + "password": self.mass.config.qobuz_password, "device_manufacturer_id": "music_assistant", } details = await self._get_data("user/login", **params) if details and "user" in details: - self.__user_auth_info = details + self._user_auth_info = details self.logger.info( "Succesfully logged in to Qobuz as %s", details["user"]["display_name"] ) diff --git a/music_assistant/providers/spotify/__init__.py b/music_assistant/controllers/music/providers/spotify.py similarity index 95% rename from music_assistant/providers/spotify/__init__.py rename to music_assistant/controllers/music/providers/spotify.py index 5ef92cd4..7d51066c 100644 --- a/music_assistant/providers/spotify/__init__.py +++ b/music_assistant/controllers/music/providers/spotify.py @@ -43,32 +43,36 @@ CACHE_DIR = gettempdir() class SpotifyProvider(MusicProvider): """Implementation of a Spotify MusicProvider.""" - def __init__(self, username: str, password: str) -> None: - """Initialize the Spotify provider.""" - self._attr_id = "spotify" - self._attr_name = "Spotify" - self._attr_supported_mediatypes = [ - MediaType.ARTIST, - MediaType.ALBUM, - MediaType.TRACK, - MediaType.PLAYLIST - # TODO: Return spotify radio - ] - self._username = username - self._password = password - self._auth_token = None - self._sp_user = None - self._librespot_bin = None - self._throttler = Throttler(rate_limit=4, period=1) - - async def setup(self) -> None: + _attr_id = "spotify" + _attr_name = "Spotify" + _attr_supported_mediatypes = [ + MediaType.ARTIST, + MediaType.ALBUM, + MediaType.TRACK, + MediaType.PLAYLIST + # TODO: Return spotify radio + ] + _auth_token = None + _sp_user = None + _librespot_bin = None + _throttler = Throttler(rate_limit=4, period=1) + + async def setup(self) -> bool: """Handle async initialization of the provider.""" - if not self._username or not self._password: + if not self.mass.config.spotify_enabled: + return False + if ( + not self.mass.config.spotify_username + or not self.mass.config.spotify_password + ): raise LoginFailed("Invalid login credentials") # try to get a token, raise if that fails token = await self.get_token() if not token: - raise LoginFailed(f"Login failed for user {self._username}") + raise LoginFailed( + f"Login failed for user {self.mass.config.spotify_username}" + ) + return True async def search( self, search_query: str, media_types=Optional[List[MediaType]], limit: int = 5 @@ -439,7 +443,10 @@ class SpotifyProvider(MusicProvider): ): return self._auth_token tokeninfo = {} - if not self._username or not self._password: + if ( + not self.mass.config.spotify_username + or not self.mass.config.spotify_password + ): return tokeninfo # retrieve token with librespot tokeninfo = await self._get_token() @@ -452,7 +459,9 @@ class SpotifyProvider(MusicProvider): ) self._auth_token = tokeninfo else: - self.logger.error("Login failed for user %s", self._username) + self.logger.error( + "Login failed for user %s", self.mass.config.spotify_username + ) return tokeninfo async def _get_token(self): @@ -465,9 +474,9 @@ class SpotifyProvider(MusicProvider): CACHE_DIR, "-a", "-u", - self._username, + self.mass.config.spotify_username, "-p", - self._password, + self.mass.config.spotify_password, ] librespot = await asyncio.create_subprocess_exec(*args) await librespot.wait() diff --git a/music_assistant/providers/tunein.py b/music_assistant/controllers/music/providers/tunein.py similarity index 91% rename from music_assistant/providers/tunein.py rename to music_assistant/controllers/music/providers/tunein.py index a79aa712..2700e1ac 100644 --- a/music_assistant/providers/tunein.py +++ b/music_assistant/controllers/music/providers/tunein.py @@ -7,6 +7,7 @@ from asyncio_throttle import Throttler from music_assistant.helpers.cache import use_cache from music_assistant.helpers.util import create_sort_name +from music_assistant.models.errors import LoginFailed from music_assistant.models.media_items import ( ContentType, ImageType, @@ -25,17 +26,20 @@ from music_assistant.models.provider import MusicProvider class TuneInProvider(MusicProvider): """Provider implementation for Tune In.""" - def __init__(self, username: Optional[str]) -> None: - """Initialize the provider.""" - self._attr_id = "tunein" - self._attr_name = "Tune-in Radio" - self._attr_supported_mediatypes = [MediaType.RADIO] - self._username = username - self._throttler = Throttler(rate_limit=1, period=1) + _attr_id = "tunein" + _attr_name = "Tune-in Radio" + _attr_supported_mediatypes = [MediaType.RADIO] + _throttler = Throttler(rate_limit=1, period=1) - async def setup(self) -> None: + async def setup(self) -> bool: """Handle async initialization of the provider.""" - # we have nothing to setup + if not self.mass.config.tunein_enabled: + return False + if not self.mass.config.tunein_username: + raise LoginFailed("Username is invalid") + if "@" in self.mass.config.tunein_username: + raise LoginFailed("You must provide the TuneIn username, not email") + return True async def search( self, search_query: str, media_types=Optional[List[MediaType]], limit: int = 5 @@ -166,7 +170,7 @@ class TuneInProvider(MusicProvider): else: url = f"https://opml.radiotime.com/{endpoint}" kwargs["formats"] = "ogg,aac,wma,mp3" - kwargs["username"] = self._username + kwargs["username"] = self.mass.config.tunein_username kwargs["partnerId"] = "1" kwargs["render"] = "json" async with self._throttler: diff --git a/music_assistant/controllers/stream.py b/music_assistant/controllers/stream.py index a963a15a..ae4b2de7 100644 --- a/music_assistant/controllers/stream.py +++ b/music_assistant/controllers/stream.py @@ -19,7 +19,6 @@ from music_assistant.helpers.audio import ( strip_silence, ) from music_assistant.helpers.process import AsyncProcess -from music_assistant.helpers.util import get_ip, select_stream_port from music_assistant.models.enums import ( ContentType, CrossFadeMode, @@ -37,12 +36,12 @@ if TYPE_CHECKING: class StreamController: """Controller to stream audio to players.""" - def __init__(self, mass: MusicAssistant, port: Optional[int] = None): + def __init__(self, mass: MusicAssistant): """Initialize instance.""" self.mass = mass self.logger = mass.logger.getChild("stream") - self._port = port or select_stream_port() - self._ip: str = get_ip() + self._port = mass.config.stream_port + self._ip = mass.config.stream_ip self._subscribers: Dict[str, Set[str]] = {} self._client_queues: Dict[str, Dict[str, asyncio.Queue]] = {} self._stream_tasks: Dict[str, Task] = {} diff --git a/music_assistant/helpers/database.py b/music_assistant/helpers/database.py index 3f807a0c..6a8801a1 100755 --- a/music_assistant/helpers/database.py +++ b/music_assistant/helpers/database.py @@ -5,7 +5,6 @@ from contextlib import asynccontextmanager from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional from databases import Database as Db -from databases import DatabaseURL if TYPE_CHECKING: from music_assistant.mass import MusicAssistant @@ -28,9 +27,9 @@ TABLE_SETTINGS = "settings" class Database: """Class that holds the (logic to the) database.""" - def __init__(self, mass: MusicAssistant, url: DatabaseURL): + def __init__(self, mass: MusicAssistant): """Initialize class.""" - self.url = url + self.url = mass.config.database_url self.mass = mass self.logger = mass.logger.getChild("db") diff --git a/music_assistant/helpers/util.py b/music_assistant/helpers/util.py index 2060de83..8ef1b29a 100755 --- a/music_assistant/helpers/util.py +++ b/music_assistant/helpers/util.py @@ -1,7 +1,6 @@ """Helper and utility functions.""" from __future__ import annotations -import asyncio import os import platform import socket @@ -18,24 +17,6 @@ CALLBACK_TYPE = Callable[[], None] # pylint: enable=invalid-name -def run_periodic(delay: float, later: bool = False): - """Run a coroutine at interval.""" - - def scheduler(fcn): - async def wrapper(*args, **kwargs): - while True: - if later: - await asyncio.sleep(delay) - await fcn(*args, **kwargs) - else: - await fcn(*args, **kwargs) - await asyncio.sleep(delay) - - return wrapper - - return scheduler - - def filename_from_string(string): """Create filename from unsafe string.""" keepcharacters = (" ", ".", "_") diff --git a/music_assistant/mass.py b/music_assistant/mass.py index 6455c8d1..2a7b0ce2 100644 --- a/music_assistant/mass.py +++ b/music_assistant/mass.py @@ -13,7 +13,6 @@ from typing import Any, Callable, Coroutine, Deque, List, Optional, Tuple, Type, from uuid import uuid4 import aiohttp -from databases import DatabaseURL from music_assistant.controllers.metadata import MetaDataController from music_assistant.controllers.music import MusicController @@ -22,6 +21,7 @@ from music_assistant.controllers.stream import StreamController from music_assistant.helpers.cache import Cache from music_assistant.helpers.database import Database from music_assistant.models.background_job import BackgroundJob +from music_assistant.models.config import MassConfig from music_assistant.models.enums import EventType, JobStatus from music_assistant.models.event import MassEvent @@ -30,25 +30,24 @@ EventSubscriptionType = Tuple[ EventCallBackType, Optional[Tuple[EventType]], Optional[Tuple[str]] ] -MAX_SIMULTANEOUS_JOBS = 5 - class MusicAssistant: """Main MusicAssistant object.""" def __init__( self, - db_url: DatabaseURL, + config: MassConfig, session: Optional[aiohttp.ClientSession] = None, ) -> None: """ Create an instance of MusicAssistant. - db_url: Database connection string/url. + conf: Music Assistant runtimestartup Config stream_port: TCP port used for streaming audio. session: Optionally provide an aiohttp clientsession """ + self.config = config self.loop: asyncio.AbstractEventLoop = None self.http_session: aiohttp.ClientSession = session self.http_session_provided = session is not None @@ -59,7 +58,7 @@ class MusicAssistant: self._jobs_event = asyncio.Event() # init core controllers - self.database = Database(self, db_url) + self.database = Database(self) self.cache = Cache(self) self.metadata = MetaDataController(self) self.music = MusicController(self) @@ -212,7 +211,7 @@ class MusicAssistant: self._jobs_event.clear() # make sure we're not running more jobs than allowed running_jobs = tuple(x for x in self._jobs if x.status == JobStatus.RUNNING) - slots_available = MAX_SIMULTANEOUS_JOBS - len(running_jobs) + slots_available = self.config.max_simultaneous_jobs - len(running_jobs) count = 0 while count <= slots_available: count += 1 diff --git a/music_assistant/models/config.py b/music_assistant/models/config.py new file mode 100644 index 00000000..7cb3df16 --- /dev/null +++ b/music_assistant/models/config.py @@ -0,0 +1,35 @@ +"""Model for the Music Assisant runtime config.""" + +from dataclasses import dataclass +from typing import Optional + +from databases import DatabaseURL + +from music_assistant.helpers.util import get_ip, select_stream_port + + +@dataclass(frozen=True) +class MassConfig: + """Model for the Music Assisant runtime config.""" + + database_url: DatabaseURL + + spotify_enabled: bool = False + spotify_username: Optional[str] = None + spotify_password: Optional[str] = None + + qobuz_enabled: bool = False + qobuz_username: Optional[str] = None + qobuz_password: Optional[str] = None + + tunein_enabled: bool = False + tunein_username: Optional[str] = None + + filesystem_enabled: bool = False + filesystem_music_dir: Optional[str] = None + filesystem_playlists_dir: Optional[str] = None + + # advanced settings + max_simultaneous_jobs: int = 10 + stream_port: int = select_stream_port() + stream_ip: str = get_ip() diff --git a/music_assistant/models/enums.py b/music_assistant/models/enums.py index 52b8a259..288e655f 100644 --- a/music_assistant/models/enums.py +++ b/music_assistant/models/enums.py @@ -187,11 +187,9 @@ class EventType(Enum): """Enum with possible Events.""" PLAYER_ADDED = "player added" - PLAYER_REMOVED = "player removed" PLAYER_UPDATED = "player updated" STREAM_STARTED = "streaming started" STREAM_ENDED = "streaming ended" - MUSIC_SYNC_STATUS = "music sync status" QUEUE_ADDED = "queue_added" QUEUE_UPDATED = "queue updated" QUEUE_ITEMS_UPDATED = "queue items updated" @@ -203,8 +201,6 @@ class EventType(Enum): PLAYLIST_ADDED = "playlist added" PLAYLIST_UPDATED = "playlist updated" RADIO_ADDED = "radio added" - TASK_UPDATED = "task updated" - PROVIDER_REGISTERED = "provider registered" BACKGROUND_JOB_UPDATED = "background_job_updated" diff --git a/music_assistant/models/provider.py b/music_assistant/models/provider.py index e69bd921..f1dabbc2 100644 --- a/music_assistant/models/provider.py +++ b/music_assistant/models/provider.py @@ -32,7 +32,7 @@ class MusicProvider: logger: Logger = None # set by setup @abstractmethod - async def setup(self) -> None: + async def setup(self) -> bool: """ Handle async initialization of the provider. @@ -193,3 +193,7 @@ class MusicProvider: return await self.get_playlist(prov_item_id) if media_type == MediaType.RADIO: return await self.get_radio(prov_item_id) + + async def sync(self) -> None: + """Run/schedule sync for this provider.""" + await self.mass.music.run_provider_sync(self.id) -- 2.34.1