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"
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
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:
# 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:
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:
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."""
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:
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
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
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:
search_result_item.artist.provider,
)
await self.update_db_artist(db_artist.item_id, prov_artist)
- return
- return
+ return True
+ return False
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
)
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:
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
self._listeners = []
self._jobs = asyncio.Queue()
+ self._job_names = set()
# init core controllers
self.database = Database(self, db_url)
"""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,
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:
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."""
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
manufacturer: str = "unknown"
-class PlayerFeature(IntEnum):
- """Enum for player features."""
-
- QUEUE = 0
- GAPLESS = 1
- CROSSFADE = 2
-
-
class Player(ABC):
"""Model for a music player."""
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."""
"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,
"""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."""
# 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)
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
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."""