fix sync speed (#258)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Thu, 21 Apr 2022 08:39:59 +0000 (10:39 +0200)
committerGitHub <noreply@github.com>
Thu, 21 Apr 2022 08:39:59 +0000 (10:39 +0200)
music_assistant/constants.py
music_assistant/controllers/music/__init__.py
music_assistant/controllers/music/artists.py
music_assistant/controllers/music/playlists.py
music_assistant/mass.py
music_assistant/models/player.py
music_assistant/models/player_queue.py

index af2a3d50de039d35dc9092c191da3d8a049b2a1b..9a43f0b1289d1b89c8ddb309c400a77b9719015c 100755 (executable)
@@ -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
index fced39d9440daaf9d60514139c485cae45fa487e..acc4be91121c6072c96941942e1cd072ce494e99 100755 (executable)
@@ -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
index 77a5bc377b533ae9d9e60bf7bc8eea9baba59ce1..10f6b62ccf9cc73f26d01b2d94969cbf34873970 100644 (file)
@@ -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
index 190a6444ab1c78060d303c12d2fe56bead5b2335..4de881ef7f988af3184e0607b15a1e6b9a1697d4 100644 (file)
@@ -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:
index 599db0b2be8c4c120cec426c431efb65513134e1..83a2ce098b6d2e18f49fa2063bb8f5d8a7888535 100644 (file)
@@ -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."""
index 7fada5236f49690c843a2d3c51320ff2d25324a7..dfc89504ba07138058b810be8097eaed7aed1f47 100755 (executable)
@@ -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,
index 5a15864fab2b5ed344b6487a5c1a0aff51b04706..b659755686042dad187e04602842fee2d2d71129 100644 (file)
@@ -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."""