From a1854320f71642369ba6e832c8cdfa908882a687 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 21 Apr 2022 10:39:59 +0200 Subject: [PATCH] fix sync speed (#258) --- music_assistant/constants.py | 4 +- music_assistant/controllers/music/__init__.py | 20 +++---- music_assistant/controllers/music/artists.py | 8 +-- .../controllers/music/playlists.py | 52 ++++++++++++------- music_assistant/mass.py | 15 +++++- music_assistant/models/player.py | 18 ++----- music_assistant/models/player_queue.py | 32 +++++++----- 7 files changed, 88 insertions(+), 61 deletions(-) diff --git a/music_assistant/constants.py b/music_assistant/constants.py index af2a3d50..9a43f0b1 100755 --- a/music_assistant/constants.py +++ b/music_assistant/constants.py @@ -18,6 +18,7 @@ class EventType(Enum): QUEUE_ADDED = "queue_added" QUEUE_UPDATED = "queue updated" QUEUE_ITEMS_UPDATED = "queue items updated" + QUEUE_TIME_UPDATED = "queue time updated" SHUTDOWN = "application shutdown" ARTIST_ADDED = "artist added" ALBUM_ADDED = "album added" @@ -25,7 +26,8 @@ class EventType(Enum): PLAYLIST_ADDED = "playlist added" RADIO_ADDED = "radio added" TASK_UPDATED = "task updated" - PROVIDER_REGISTERED = "PROVIDER_REGISTERED" + PROVIDER_REGISTERED = "provider registered" + BACKGROUND_JOBS_UPDATED = "background_jobs_updated" @dataclass diff --git a/music_assistant/controllers/music/__init__.py b/music_assistant/controllers/music/__init__.py index fced39d9..acc4be91 100755 --- a/music_assistant/controllers/music/__init__.py +++ b/music_assistant/controllers/music/__init__.py @@ -397,9 +397,15 @@ class MusicController: if not db_item and media_type == MediaType.ARTIST: # for artists we need a fully matched item (with musicbrainz id) db_item = await controller.get( - prov_item.item_id, prov_item.provider, details=prov_item + prov_item.item_id, + prov_item.provider, + details=prov_item, + lazy=False, ) - elif not db_item or not db_item.available: + elif db_item and db_item.available != prov_item.available: + # availability changed + db_item = await controller.add_db_item(prov_item) + elif db_item and not db_item.available: # use auto matching magic to find a substitute for missing item db_item = await controller.add(prov_item) elif not db_item: @@ -414,8 +420,6 @@ class MusicController: # sync playlist tracks if media_type == MediaType.PLAYLIST: await self._sync_playlist_tracks(db_item) - # chill a bit otherwise sync is really heavy for the system - await asyncio.sleep(0.1) # process deletions for item_id in prev_ids: @@ -434,7 +438,7 @@ class MusicController: db_track = await self.tracks.get_db_item_by_prov_id( album_track.provider, album_track.item_id ) - if not db_track or not db_track.available: + if db_track and not db_track.available: # use auto matching magic to find a substitute for missing track db_track = await self.tracks.add(album_track) elif not db_track: @@ -447,8 +451,6 @@ class MusicController: album_track.disc_number, album_track.track_number, ) - # chill a bit otherwise sync is really heavy for the system - await asyncio.sleep(0.1) async def _sync_playlist_tracks(self, db_playlist: Playlist) -> None: """Store playlist tracks of in-library playlist in database.""" @@ -462,7 +464,7 @@ class MusicController: db_track = await self.tracks.get_db_item_by_prov_id( playlist_track.provider, playlist_track.item_id ) - if not db_track or not db_track.available: + if db_track and not db_track.available: # use auto matching magic to find a substitute for missing track db_track = await self.tracks.add(playlist_track) elif not db_track: @@ -473,8 +475,6 @@ class MusicController: db_track.item_id, playlist_track.position, ) - # chill a bit otherwise sync is really heavy for the system - await asyncio.sleep(0.1) def _get_controller( self, media_type: MediaType diff --git a/music_assistant/controllers/music/artists.py b/music_assistant/controllers/music/artists.py index 77a5bc37..10f6b62c 100644 --- a/music_assistant/controllers/music/artists.py +++ b/music_assistant/controllers/music/artists.py @@ -222,7 +222,7 @@ class ArtistsController(MediaControllerBase[Artist]): self.logger.warning("Unable to get musicbrainz ID for artist %s !", artist.name) return artist.name - async def _match(self, db_artist: Artist, provider: MusicProvider): + async def _match(self, db_artist: Artist, provider: MusicProvider) -> bool: """Try to find matching artists on given provider for the provided (database) artist.""" self.logger.debug( "Trying to match artist %s on provider %s", db_artist.name, provider.name @@ -247,7 +247,7 @@ class ArtistsController(MediaControllerBase[Artist]): search_item_artist.item_id, search_item_artist.provider ) await self.update_db_artist(db_artist.item_id, prov_artist) - return + return True # try to get a match with some reference albums of this artist artist_albums = await self.albums(db_artist.item_id, db_artist.provider) for ref_album in artist_albums: @@ -267,5 +267,5 @@ class ArtistsController(MediaControllerBase[Artist]): search_result_item.artist.provider, ) await self.update_db_artist(db_artist.item_id, prov_artist) - return - return + return True + return False diff --git a/music_assistant/controllers/music/playlists.py b/music_assistant/controllers/music/playlists.py index 190a6444..4de881ef 100644 --- a/music_assistant/controllers/music/playlists.py +++ b/music_assistant/controllers/music/playlists.py @@ -74,34 +74,39 @@ class PlaylistController(MediaControllerBase[Playlist]): return db_item async def add_playlist_tracks( - self, item_id: str, provider_id: str, tracks: List[Track] + self, playlist_id: str, playlist_provider: str, uris: List[str] ) -> None: """Add multiple tracks to playlist. Creates background tasks to process the action.""" - playlist = await self.get(item_id, provider_id) + playlist = await self.get(playlist_id, playlist_provider) if not playlist: - raise MediaNotFoundError(f"Playlist {item_id} not found") + raise MediaNotFoundError( + f"Playlist {playlist_provider}/{playlist_id} not found" + ) if not playlist.is_editable: raise InvalidDataError(f"Playlist {playlist.name} is not editable") - for track in tracks: - job_desc = f"Add track {track.uri} to playlist {playlist.uri}" + for uri in uris: + job_desc = f"Add track {uri} to playlist {playlist.uri}" self.mass.add_job( - self.add_playlist_track(item_id, provider_id, track), job_desc + self.add_playlist_track(playlist_id, playlist_provider, uri), job_desc ) async def add_playlist_track( - self, item_id: str, provider_id: str, track: Track + self, playlist_id: str, playlist_provider: str, track_uri: str ) -> None: """Add track to playlist - make sure we dont add duplicates.""" # we can only edit playlists that are in the database (marked as editable) - playlist = await self.get(item_id, provider_id) + playlist = await self.get(playlist_id, playlist_provider) if not playlist: - raise MediaNotFoundError(f"Playlist {item_id} not found") + raise MediaNotFoundError( + f"Playlist {playlist_provider}/{playlist_id} not found" + ) if not playlist.is_editable: raise InvalidDataError(f"Playlist {playlist.name} is not editable") # make sure we have recent full track details - track = await self.mass.music.tracks.get( - track.item_id, track.provider, force_refresh=True, lazy=False + track = await self.mass.music.get_item_by_uri( + track_uri, force_refresh=True, lazy=False ) + assert track.media_type == MediaType.TRACK # a playlist can only have one provider (for now) playlist_prov = next(iter(playlist.provider_ids)) # grab all existing track ids in the playlist so we can check for duplicates @@ -155,33 +160,40 @@ class PlaylistController(MediaControllerBase[Playlist]): ) async def remove_playlist_tracks( - self, item_id: str, provider_id: str, tracks: List[Track] + self, playlist_id: str, playlist_provider: str, uris: List[str] ) -> None: """Remove multiple tracks from playlist. Creates background tasks to process the action.""" - playlist = await self.get(item_id, provider_id) + playlist = await self.get(playlist_id, playlist_provider) if not playlist: - raise MediaNotFoundError(f"Playlist {item_id} not found") + raise MediaNotFoundError( + f"Playlist {playlist_provider}/{playlist_id} not found" + ) if not playlist.is_editable: raise InvalidDataError(f"Playlist {playlist.name} is not editable") - for track in tracks: - job_desc = f"Remove track {track.uri} from playlist {playlist.uri}" + for uri in uris: + job_desc = f"Remove track {uri} from playlist {playlist.uri}" self.mass.add_job( - self.remove_playlist_track(item_id, provider_id, track), job_desc + self.remove_playlist_track(playlist_id, playlist_provider, uri), + job_desc, ) async def remove_playlist_track( - self, item_id: str, provider_id: str, track: Track + self, playlist_id: str, playlist_provider: str, track_uri: str ) -> None: """Remove track from playlist.""" # we can only edit playlists that are in the database (marked as editable) - playlist = await self.get(item_id, provider_id) + playlist = await self.get(playlist_id, playlist_provider) if not playlist: - raise MediaNotFoundError(f"Playlist {item_id} not found") + raise MediaNotFoundError( + f"Playlist {playlist_provider}/{playlist_id} not found" + ) if not playlist.is_editable: raise InvalidDataError(f"Playlist {playlist.name} is not editable") # playlist can only have one provider (for now) prov_playlist = next(iter(playlist.provider_ids)) track_ids_to_remove = set() + track = await self.mass.music.get_item_by_uri(track_uri, lazy=True) + assert track.media_type == MediaType.TRACK # 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: diff --git a/music_assistant/mass.py b/music_assistant/mass.py index 599db0b2..83a2ce09 100644 --- a/music_assistant/mass.py +++ b/music_assistant/mass.py @@ -7,7 +7,7 @@ import logging import threading from time import time from types import TracebackType -from typing import Any, Callable, Coroutine, List, Optional, Tuple, Type, Union +from typing import Any, Callable, Coroutine, List, Optional, Set, Tuple, Type, Union import aiohttp from databases import DatabaseURL @@ -50,6 +50,7 @@ class MusicAssistant: self._listeners = [] self._jobs = asyncio.Queue() + self._job_names = set() # init core controllers self.database = Database(self, db_url) @@ -132,7 +133,9 @@ class MusicAssistant: """Add job to be (slowly) processed in the background (one by one).""" if not name: name = job.__qualname__ or job.__name__ + self._job_names.add(name) self._jobs.put_nowait((name, job)) + self.signal_event(MassEvent(EventType.BACKGROUND_JOBS_UPDATED, data=self.jobs)) def create_task( self, @@ -181,6 +184,11 @@ class MusicAssistant: task.add_done_callback(task_done_callback) return task + @property + def jobs(self) -> Set[str]: + """Return the (names of) running background jobs.""" + return self._job_names + async def __process_jobs(self): """Process jobs in the background.""" while True: @@ -198,6 +206,11 @@ class MusicAssistant: else: duration = round(time() - time_start, 2) self.logger.info("Finished job [%s] in %s seconds.", name, duration) + if name in self._job_names: + self._job_names.remove(name) + self.signal_event( + MassEvent(EventType.BACKGROUND_JOBS_UPDATED, data=self.jobs) + ) async def __aenter__(self) -> "MusicAssistant": """Return Context manager.""" diff --git a/music_assistant/models/player.py b/music_assistant/models/player.py index 7fada523..dfc89504 100755 --- a/music_assistant/models/player.py +++ b/music_assistant/models/player.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from abc import ABC from dataclasses import dataclass -from enum import Enum, IntEnum +from enum import Enum from typing import TYPE_CHECKING, Any, Dict, List from mashumaro import DataClassDictMixin @@ -35,14 +35,6 @@ class DeviceInfo(DataClassDictMixin): manufacturer: str = "unknown" -class PlayerFeature(IntEnum): - """Enum for player features.""" - - QUEUE = 0 - GAPLESS = 1 - CROSSFADE = 2 - - class Player(ABC): """Model for a music player.""" @@ -190,10 +182,10 @@ class Player(ABC): async def play_pause(self) -> None: """Toggle play/pause on player.""" - if self.state == PlayerState.PAUSED: - await self.play() - else: + if self.state == PlayerState.PLAYING: await self.pause() + else: + await self.play() async def power_toggle(self) -> None: """Toggle power on player.""" @@ -286,7 +278,7 @@ class Player(ABC): "available": self.available, "is_group": self.is_group, "group_childs": self.group_childs, - "group_parents": self._attr_group_parents, + "group_parents": self.group_parents, "volume_level": int(self.volume_level), "device_info": self.device_info.to_dict(), "active_queue": self.active_queue.queue_id, diff --git a/music_assistant/models/player_queue.py b/music_assistant/models/player_queue.py index 5a15864f..b6597556 100644 --- a/music_assistant/models/player_queue.py +++ b/music_assistant/models/player_queue.py @@ -104,6 +104,11 @@ class PlayerQueue: """Return the player attached to this queue.""" return self.mass.players.get_player(self.queue_id, include_unavailable=True) + @property + def available(self) -> bool: + """Return if player(queue) is available.""" + return self.player.available + @property def active(self) -> bool: """Return bool if the queue is currenty active on the player.""" @@ -356,6 +361,13 @@ class PlayerQueue: # redirect to underlying player await self.player.pause() + async def play_pause(self) -> None: + """Toggle play/pause on queue/player.""" + if self.player.state == PlayerState.PLAYING: + await self.pause() + else: + await self.play() + async def next(self) -> None: """Play the next track in the queue.""" next_index = self.get_next_index(self._current_index) @@ -542,13 +554,9 @@ class PlayerQueue: self._update_task.cancel() self._update_task = None - if not self.update_state(): - # fire event anyway when player updated. - self.mass.signal_event( - MassEvent(EventType.QUEUE_UPDATED, object_id=self.queue_id, data=self) - ) + self.update_state() - def update_state(self) -> bool: + def update_state(self) -> None: """Update queue details, called when player updates.""" if self.player.active_queue.queue_id != self.queue_id: return @@ -580,15 +588,15 @@ class PlayerQueue: if new_item_loaded: self.mass.create_task(self._save_state()) - if ( - new_item_loaded - or abs(prev_item_time - self._current_item_elapsed_time) >= 1 - ): self.mass.signal_event( MassEvent(EventType.QUEUE_UPDATED, object_id=self.queue_id, data=self) ) - return True - return False + if abs(prev_item_time - self._current_item_elapsed_time) >= 1: + self.mass.signal_event( + MassEvent( + EventType.QUEUE_TIME_UPDATED, object_id=self.queue_id, data=self + ) + ) async def queue_stream_prepare(self) -> StreamDetails: """Call when queue_streamer is about to start playing.""" -- 2.34.1