from modules.metadata import MetaData
from modules.cache import Cache
from modules.music import Music
-from modules.player import Player
+from modules.playermanager import PlayerManager
from modules.http_streamer import HTTPStreamer
from modules.homeassistant import setup as hass_setup
from modules.web import setup as web_setup
self.web = web_setup(self)
self.hass = hass_setup(self)
self.music = Music(self)
- self.player = Player(self)
+ self.player = PlayerManager(self)
self.http_streamer = HTTPStreamer(self)
# agent = stackimpact.start(
+++ /dev/null
-#!/usr/bin/env python3
-# -*- coding:utf-8 -*-
-
-from enum import Enum, IntEnum
-from typing import List
-import sys
-sys.path.append("..")
-from utils import run_periodic, LOGGER, parse_track_title
-from difflib import SequenceMatcher as Matcher
-import asyncio
-from modules.cache import use_cache
-
-
-class MediaType(IntEnum):
- Artist = 1
- Album = 2
- Track = 3
- 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':
- return MediaType.Artist
- 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':
- return MediaType.Track
- 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':
- 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
-
-
-class Artist(object):
- ''' representation of an artist '''
- def __init__(self):
- self.item_id = None
- self.provider = 'database'
- self.name = ''
- self.sort_name = ''
- self.metadata = {}
- self.tags = []
- self.external_ids = []
- self.provider_ids = []
- self.media_type = MediaType.Artist
- self.in_library = []
- self.is_lazy = False
-
-class Album(object):
- ''' representation of an album '''
- def __init__(self):
- self.item_id = None
- self.provider = 'database'
- self.name = ''
- self.metadata = {}
- self.version = ''
- self.external_ids = []
- self.tags = []
- self.albumtype = AlbumType.Album
- self.year = 0
- self.artist = None
- self.labels = []
- self.provider_ids = []
- self.media_type = MediaType.Album
- self.in_library = []
- self.is_lazy = False
-
-class Track(object):
- ''' representation of a track '''
- def __init__(self):
- self.item_id = None
- self.provider = 'database'
- self.name = ''
- self.duration = 0
- self.version = ''
- self.external_ids = []
- self.metadata = { }
- self.tags = []
- self.artists = []
- self.provider_ids = []
- self.album = None
- self.disc_number = 1
- self.track_number = 1
- self.media_type = MediaType.Track
- self.in_library = []
- self.is_lazy = False
- self.uri = ""
- def __eq__(self, other):
- if not isinstance(other, self.__class__):
- return NotImplemented
- return (self.name == other.name and
- self.version == other.version and
- self.item_id == other.item_id and
- self.provider == other.provider)
- def __ne__(self, other):
- return not self.__eq__(other)
-
-class Playlist(object):
- ''' representation of a playlist '''
- def __init__(self):
- self.item_id = None
- self.provider = 'database'
- self.name = ''
- self.owner = ''
- self.provider_ids = []
- self.metadata = {}
- self.media_type = MediaType.Playlist
- self.in_library = []
- self.is_editable = False
-
-class Radio(Track):
- ''' representation of a radio station '''
- def __init__(self):
- super().__init__()
- self.item_id = None
- self.provider = 'database'
- self.name = ''
- self.provider_ids = []
- self.metadata = {}
- self.media_type = MediaType.Radio
- self.in_library = []
- self.is_editable = False
- self.duration = 0
-
-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
- '''
-
- name = 'My great Music provider' # display name
- prov_id = 'my_provider' # used as id
- icon = ''
-
- def __init__(self, mass):
- self.mass = mass
- self.cache = mass.cache
-
- ### Common methods and properties ####
-
- async def artist(self, prov_item_id, lazy=True) -> Artist:
- ''' return artist details for the given provider artist id '''
- item_id = await self.mass.db.get_database_id(self.prov_id, prov_item_id, MediaType.Artist)
- if not item_id:
- # artist not yet in local database so fetch details
- artist_details = await self.get_artist(prov_item_id)
- if not artist_details:
- 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)
- return await self.mass.db.artist(item_id)
-
- async def add_artist(self, artist_details) -> int:
- ''' add artist to local db and return the new database id'''
- musicbrainz_id = None
- for item in artist_details.external_ids:
- if item.get("musicbrainz"):
- musicbrainz_id = item["musicbrainz"]
- if not musicbrainz_id:
- musicbrainz_id = await self.get_artist_musicbrainz_id(artist_details)
- 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)
- 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 = await self.get_artist_toptracks(artist_details.item_id)
- if new_artist_toptracks:
- 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_toptracks)
- return item_id
-
- async def get_artist_musicbrainz_id(self, artist_details:Artist):
- ''' 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 = await self.get_artist_albums(artist_details.item_id)
- for lookup_album in lookup_albums[:5]:
- lookup_album_upc = None
- if not lookup_album:
- continue
- for item in lookup_album.external_ids:
- if item.get("upc"):
- lookup_album_upc = item["upc"]
- break
- musicbrainz_id = await self.mass.metadata.get_mb_artist_id(artist_details.name,
- albumname=lookup_album.name, album_upc=lookup_album_upc)
- if musicbrainz_id:
- break
- # fallback to track
- if not musicbrainz_id:
- lookup_tracks = await self.get_artist_toptracks(artist_details.item_id)
- for lookup_track in lookup_tracks:
- if not lookup_track:
- continue
- lookup_track_isrc = None
- for item in lookup_track.external_ids:
- if item.get("isrc"):
- lookup_track_isrc = item["isrc"]
- break
- musicbrainz_id = await self.mass.metadata.get_mb_artist_id(artist_details.name,
- trackname=lookup_track.name, track_isrc=lookup_track_isrc)
- if musicbrainz_id:
- break
- if not musicbrainz_id:
- LOGGER.warning("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:
- ''' return album details for the given provider album id'''
- item_id = await self.mass.db.get_database_id(self.prov_id, prov_item_id, MediaType.Album)
- if not item_id:
- # album not yet in local database so fetch details
- album_details = await self.get_album(prov_item_id)
- if not album_details:
- raise Exception('album not found: %s' % prov_item_id)
- if lazy:
- asyncio.create_task(self.add_album(album_details))
- album_details.is_lazy = True
- return album_details
- item_id = await self.add_album(album_details)
- return await self.mass.db.album(item_id)
-
- 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)
- 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]
- 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) -> Track:
- ''' return track details for the given provider track id '''
- item_id = await self.mass.db.get_database_id(self.prov_id, prov_item_id, MediaType.Track)
- if not item_id:
- # album not yet in local database so fetch details
- if not track_details:
- track_details = await self.get_track(prov_item_id)
- if not track_details:
- raise Exception('track not found: %s' % prov_item_id)
- if lazy:
- asyncio.create_task(self.add_track(track_details))
- track_details.is_lazy = True
- return track_details
- item_id = await self.add_track(track_details)
- return await self.mass.db.track(item_id)
-
- async def add_track(self, track_details, prov_album_id=None) -> int:
- ''' add track to local db and return the new database id'''
- 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)
- if db_track_artist:
- track_artists.append(db_track_artist)
- track_details.artists = track_artists
- if not prov_album_id:
- prov_album_id = track_details.album.item_id
- track_details.album = await self.album(prov_album_id, lazy=False)
- 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)
- for prov_id, provider in self.mass.music.providers.items():
- await provider.match_track(new_track)
- return item_id
-
- async def playlist(self, prov_playlist_id) -> Playlist:
- ''' return playlist details for the given provider playlist id '''
- db_id = await self.mass.db.get_database_id(self.prov_id, prov_playlist_id, MediaType.Playlist)
- if db_id:
- # synced playlist, return database details
- return await self.mass.db.playlist(db_id)
- else:
- return await self.get_playlist(prov_playlist_id)
-
- async def radio(self, prov_radio_id) -> Radio:
- ''' return radio details for the given provider playlist id '''
- db_id = await self.mass.db.get_database_id(self.prov_id, prov_radio_id, MediaType.Radio)
- if db_id:
- # synced radio, return database details
- return await self.mass.db.radio(db_id)
- else:
- return 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'''
- items = []
- album = await self.get_album(prov_album_id)
- for prov_track in await self.get_album_tracks(prov_album_id):
- db_id = await self.mass.db.get_database_id(self.prov_id, prov_track.item_id, MediaType.Track)
- if db_id:
- items.append( await self.mass.db.track(db_id) )
- else:
- prov_track.album = album
- items.append(prov_track)
- return items
-
- async def playlist_tracks(self, prov_playlist_id, limit=100, offset=0) -> List[Track]:
- ''' return playlist tracks for the given provider playlist id'''
- items = []
- for prov_track in await self.get_playlist_tracks(prov_playlist_id, limit=limit, offset=offset):
- for prov_mapping in prov_track.provider_ids:
- item_prov_id = prov_mapping["provider"]
- prov_item_id = prov_mapping["item_id"]
- db_id = await self.mass.db.get_database_id(item_prov_id, prov_item_id, MediaType.Track)
- if db_id:
- items.append( await self.mass.db.track(db_id) )
- else:
- items.append(prov_track)
- return items
-
- async def artist_toptracks(self, prov_item_id) -> List[Track]:
- ''' return top tracks for an artist '''
- items = []
- for prov_track in await self.get_artist_toptracks(prov_item_id):
- db_id = await self.mass.db.get_database_id(self.prov_id, prov_track.item_id, MediaType.Track)
- if db_id:
- items.append( await self.mass.db.track(db_id) )
- else:
- items.append(prov_track)
- return items
-
- async def artist_albums(self, prov_item_id) -> List[Track]:
- ''' return (all) albums for an artist '''
- items = []
- for prov_album in await self.get_artist_albums(prov_item_id):
- db_id = await self.mass.db.get_database_id(self.prov_id, prov_album.item_id, MediaType.Album)
- if db_id:
- items.append( await self.mass.db.album(db_id) )
- else:
- items.append(prov_album)
- return items
-
- async def match_artist(self, searchartist:Artist, searchtracks:List[Track]):
- ''' try to match artist in this provider by supplying db artist '''
- for searchtrack in searchtracks:
- searchstr = "%s - %s" %(searchartist.name, searchtrack.name)
- search_results = await self.search(searchstr, [MediaType.Track], limit=5)
- for item in search_results["tracks"]:
- if item.name == searchtrack.name and item.version == searchtrack.version and item.album.name == searchtrack.album.name:
- # double safety check - artist must match exactly !
- for artist in item.artists:
- if artist.name == searchartist.name:
- # just load this item in the database, it will be matched automagically ;-)
- return await self.artist(artist.item_id, lazy=False)
-
- async def match_album(self, searchalbum:Album):
- ''' try to match album in this provider by supplying db album '''
- searchstr = "%s - %s %s" %(searchalbum.artist.name, searchalbum.name, searchalbum.version)
- search_results = await self.search(searchstr, [MediaType.Album], limit=5)
- for item in search_results["albums"]:
- if item.name == searchalbum.name and item.version == searchalbum.version and item.artist.name == searchalbum.artist.name:
- # just load this item in the database, it will be matched automagically ;-)
- await self.album(item.item_id, lazy=False)
-
- async def match_track(self, searchtrack:Track):
- ''' try to match track in this provider by supplying db track '''
- searchstr = "%s - %s" %(searchtrack.artists[0].name, searchtrack.name)
- searchartists = [item.name for item in searchtrack.artists]
- search_results = await self.search(searchstr, [MediaType.Track], limit=5)
- for item in search_results["tracks"]:
- if item.name == searchtrack.name and item.version == searchtrack.version and item.album.name == searchtrack.album.name:
- # double safety check - artist must match exactly !
- for artist in item.artists:
- if artist.name in searchartists:
- # just load this item in the database, it will be matched automagically ;-)
- await self.track(item.item_id, lazy=False)
-
- ### Provider specific implementation #####
-
- async def search(self, searchstring, media_types=List[MediaType], limit=5):
- ''' perform search on the provider '''
- raise NotImplementedError
-
- async def get_library_artists(self) -> List[Artist]:
- ''' retrieve library artists from the provider '''
- raise NotImplementedError
-
- async def get_library_albums(self) -> List[Album]:
- ''' retrieve library albums from the provider '''
- raise NotImplementedError
-
- async def get_library_tracks(self) -> List[Track]:
- ''' retrieve library tracks from the provider '''
- raise NotImplementedError
-
- async def get_playlists(self) -> List[Playlist]:
- ''' retrieve library/subscribed playlists from the provider '''
- raise NotImplementedError
-
- async def get_radios(self) -> List[Radio]:
- ''' retrieve library/subscribed radio stations from the provider '''
- raise NotImplementedError
-
- async def get_artist(self, prov_item_id) -> Artist:
- ''' get full artist details by id '''
- raise NotImplementedError
-
- async def get_artist_albums(self, prov_item_id) -> List[Album]:
- ''' get a list of albums for the given artist '''
- raise NotImplementedError
-
- async def get_artist_toptracks(self, prov_item_id) -> List[Track]:
- ''' get a list of most popular tracks for the given artist '''
- raise NotImplementedError
-
- async def get_album(self, prov_item_id) -> Album:
- ''' get full album details by id '''
- raise NotImplementedError
-
- async def get_track(self, prov_item_id) -> Track:
- ''' get full track details by id '''
- raise NotImplementedError
-
- async def get_playlist(self, prov_item_id) -> Playlist:
- ''' get full playlist details by id '''
- raise NotImplementedError
-
- async def get_radio(self, prov_item_id) -> Radio:
- ''' get full radio details by id '''
- raise NotImplementedError
-
- async def get_album_tracks(self, prov_item_id, limit=100, offset=0) -> List[Track]:
- ''' get album tracks for given album id '''
- raise NotImplementedError
-
- async def get_playlist_tracks(self, prov_item_id, limit=100, offset=0) -> List[Track]:
- ''' get playlist tracks for given playlist id '''
- raise NotImplementedError
-
- async def add_library(self, prov_item_id, media_type:MediaType):
- ''' add item to library '''
- raise NotImplementedError
-
- async def remove_library(self, prov_item_id, media_type:MediaType):
- ''' remove item from library '''
- raise NotImplementedError
-
- async def add_playlist_tracks(self, prov_playlist_id, prov_track_ids):
- ''' add track(s) to playlist '''
- raise NotImplementedError
-
- async def remove_playlist_tracks(self, prov_playlist_id, prov_track_ids):
- ''' remove track(s) from playlist '''
- raise NotImplementedError
-
- async def get_stream_content_type(self, track_id):
- ''' return the content type for the given track when it will be streamed'''
- raise NotImplementedError
-
- async def get_stream(self, track_id):
- ''' get audio stream for a track '''
- raise NotImplementedError
-
-
-class PlayerState(str, Enum):
- Off = "off"
- Stopped = "stopped"
- Paused = "paused"
- Playing = "playing"
-
-class MusicPlayer():
- ''' representation of a musicplayer '''
- def __init__(self):
- self.player_id = None
- self.player_provider = None
- self.name = ''
- self.state = PlayerState.Stopped
- self.powered = False
- self.cur_item = None
- self.cur_item_time = 0
- self.volume_level = 0
- self.shuffle_enabled = True
- self.repeat_enabled = False
- self.muted = False
- self.group_parent = None
- self.is_group = False
- self.settings = {}
- self.enabled = True
-
-class PlayerProvider():
- '''
- Model for a Playerprovider
- Common methods usable for every provider
- Provider specific __get methods shoud be overriden in the provider specific implementation
- '''
- name = 'My great Musicplayer provider' # display name
- prov_id = 'my_provider' # used as id
- icon = ''
- supported_musicproviders = ['qobuz', 'file', 'spotify', 'http'] # list of supported music provider uri's this playerprovider supports NATIVELY
-
- def __init__(self, mass):
- self.mass = mass
-
- ### Common methods and properties ####
-
-
- async def play_media(self, player_id, media_items:List[Track], queue_opt='play'):
- '''
- play media on a player
- params:
- - player_id: id of the player
- - media_items: List of Tracks to play, each Track will contain uri attribute (e.g. spotify:track:1234 or http://pathtostream)
- - queue_opt:
- replace: replace whatever is currently playing with this media
- next: the given media will be played after the currently playing track
- add: add to the end of the queue
- play: keep existing queue but play the given item(s) now first
- '''
- raise NotImplementedError
-
-
- ### Provider specific implementation #####
-
- async def player_command(self, player_id, cmd:str, cmd_args=None):
- ''' issue command on player (play, pause, next, previous, stop, power, volume) '''
- raise NotImplementedError
-
- async def player_queue(self, player_id, offset=0, limit=50):
- ''' return the items in the player's queue '''
- raise NotImplementedError
-
-
--- /dev/null
+#!/usr/bin/env python3
+# -*- coding:utf-8 -*-
+
+from enum import Enum, IntEnum
+
+class MediaType(IntEnum):
+ Artist = 1
+ Album = 2
+ Track = 3
+ 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':
+ return MediaType.Artist
+ 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':
+ return MediaType.Track
+ 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':
+ 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
+
+
+class Artist(object):
+ ''' representation of an artist '''
+ def __init__(self):
+ self.item_id = None
+ self.provider = 'database'
+ self.name = ''
+ self.sort_name = ''
+ self.metadata = {}
+ self.tags = []
+ self.external_ids = []
+ self.provider_ids = []
+ self.media_type = MediaType.Artist
+ self.in_library = []
+ self.is_lazy = False
+
+class Album(object):
+ ''' representation of an album '''
+ def __init__(self):
+ self.item_id = None
+ self.provider = 'database'
+ self.name = ''
+ self.metadata = {}
+ self.version = ''
+ self.external_ids = []
+ self.tags = []
+ self.albumtype = AlbumType.Album
+ self.year = 0
+ self.artist = None
+ self.labels = []
+ self.provider_ids = []
+ self.media_type = MediaType.Album
+ self.in_library = []
+ self.is_lazy = False
+
+class Track(object):
+ ''' representation of a track '''
+ def __init__(self):
+ self.item_id = None
+ self.provider = 'database'
+ self.name = ''
+ self.duration = 0
+ self.version = ''
+ self.external_ids = []
+ self.metadata = { }
+ self.tags = []
+ self.artists = []
+ self.provider_ids = []
+ self.album = None
+ self.disc_number = 1
+ self.track_number = 1
+ self.media_type = MediaType.Track
+ self.in_library = []
+ self.is_lazy = False
+ self.uri = ""
+ def __eq__(self, other):
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+ return (self.name == other.name and
+ self.version == other.version and
+ self.item_id == other.item_id and
+ self.provider == other.provider)
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+class Playlist(object):
+ ''' representation of a playlist '''
+ def __init__(self):
+ self.item_id = None
+ self.provider = 'database'
+ self.name = ''
+ self.owner = ''
+ self.provider_ids = []
+ self.metadata = {}
+ self.media_type = MediaType.Playlist
+ self.in_library = []
+ self.is_editable = False
+
+class Radio(Track):
+ ''' representation of a radio station '''
+ def __init__(self):
+ super().__init__()
+ self.item_id = None
+ self.provider = 'database'
+ self.name = ''
+ self.provider_ids = []
+ self.metadata = {}
+ self.media_type = MediaType.Radio
+ self.in_library = []
+ self.is_editable = False
+ self.duration = 0
+
+
--- /dev/null
+#!/usr/bin/env python3
+# -*- coding:utf-8 -*-
+
+from typing import List
+from ..utils import run_periodic, LOGGER, parse_track_title
+import asyncio
+from ..modules.cache import use_cache
+from media_types import *
+
+
+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
+ '''
+
+ name = 'My great Music provider' # display name
+ prov_id = 'my_provider' # used as id
+ icon = ''
+
+ def __init__(self, mass):
+ self.mass = mass
+ self.cache = mass.cache
+
+ ### Common methods and properties ####
+
+ async def artist(self, prov_item_id, lazy=True) -> Artist:
+ ''' return artist details for the given provider artist id '''
+ item_id = await self.mass.db.get_database_id(self.prov_id, prov_item_id, MediaType.Artist)
+ if not item_id:
+ # artist not yet in local database so fetch details
+ artist_details = await self.get_artist(prov_item_id)
+ if not artist_details:
+ 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)
+ return await self.mass.db.artist(item_id)
+
+ async def add_artist(self, artist_details) -> int:
+ ''' add artist to local db and return the new database id'''
+ musicbrainz_id = None
+ for item in artist_details.external_ids:
+ if item.get("musicbrainz"):
+ musicbrainz_id = item["musicbrainz"]
+ if not musicbrainz_id:
+ musicbrainz_id = await self.get_artist_musicbrainz_id(artist_details)
+ 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)
+ 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 = await self.get_artist_toptracks(artist_details.item_id)
+ if new_artist_toptracks:
+ 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_toptracks)
+ return item_id
+
+ async def get_artist_musicbrainz_id(self, artist_details:Artist):
+ ''' 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 = await self.get_artist_albums(artist_details.item_id)
+ for lookup_album in lookup_albums[:5]:
+ lookup_album_upc = None
+ if not lookup_album:
+ continue
+ for item in lookup_album.external_ids:
+ if item.get("upc"):
+ lookup_album_upc = item["upc"]
+ break
+ musicbrainz_id = await self.mass.metadata.get_mb_artist_id(artist_details.name,
+ albumname=lookup_album.name, album_upc=lookup_album_upc)
+ if musicbrainz_id:
+ break
+ # fallback to track
+ if not musicbrainz_id:
+ lookup_tracks = await self.get_artist_toptracks(artist_details.item_id)
+ for lookup_track in lookup_tracks:
+ if not lookup_track:
+ continue
+ lookup_track_isrc = None
+ for item in lookup_track.external_ids:
+ if item.get("isrc"):
+ lookup_track_isrc = item["isrc"]
+ break
+ musicbrainz_id = await self.mass.metadata.get_mb_artist_id(artist_details.name,
+ trackname=lookup_track.name, track_isrc=lookup_track_isrc)
+ if musicbrainz_id:
+ break
+ if not musicbrainz_id:
+ LOGGER.warning("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:
+ ''' return album details for the given provider album id'''
+ item_id = await self.mass.db.get_database_id(self.prov_id, prov_item_id, MediaType.Album)
+ if not item_id:
+ # album not yet in local database so fetch details
+ album_details = await self.get_album(prov_item_id)
+ if not album_details:
+ raise Exception('album not found: %s' % prov_item_id)
+ if lazy:
+ asyncio.create_task(self.add_album(album_details))
+ album_details.is_lazy = True
+ return album_details
+ item_id = await self.add_album(album_details)
+ return await self.mass.db.album(item_id)
+
+ 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)
+ 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]
+ 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) -> Track:
+ ''' return track details for the given provider track id '''
+ item_id = await self.mass.db.get_database_id(self.prov_id, prov_item_id, MediaType.Track)
+ if not item_id:
+ # album not yet in local database so fetch details
+ if not track_details:
+ track_details = await self.get_track(prov_item_id)
+ if not track_details:
+ raise Exception('track not found: %s' % prov_item_id)
+ if lazy:
+ asyncio.create_task(self.add_track(track_details))
+ track_details.is_lazy = True
+ return track_details
+ item_id = await self.add_track(track_details)
+ return await self.mass.db.track(item_id)
+
+ async def add_track(self, track_details, prov_album_id=None) -> int:
+ ''' add track to local db and return the new database id'''
+ 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)
+ if db_track_artist:
+ track_artists.append(db_track_artist)
+ track_details.artists = track_artists
+ if not prov_album_id:
+ prov_album_id = track_details.album.item_id
+ track_details.album = await self.album(prov_album_id, lazy=False)
+ 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)
+ for prov_id, provider in self.mass.music.providers.items():
+ await provider.match_track(new_track)
+ return item_id
+
+ async def playlist(self, prov_playlist_id) -> Playlist:
+ ''' return playlist details for the given provider playlist id '''
+ db_id = await self.mass.db.get_database_id(self.prov_id, prov_playlist_id, MediaType.Playlist)
+ if db_id:
+ # synced playlist, return database details
+ return await self.mass.db.playlist(db_id)
+ else:
+ return await self.get_playlist(prov_playlist_id)
+
+ async def radio(self, prov_radio_id) -> Radio:
+ ''' return radio details for the given provider playlist id '''
+ db_id = await self.mass.db.get_database_id(self.prov_id, prov_radio_id, MediaType.Radio)
+ if db_id:
+ # synced radio, return database details
+ return await self.mass.db.radio(db_id)
+ else:
+ return 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'''
+ items = []
+ album = await self.get_album(prov_album_id)
+ for prov_track in await self.get_album_tracks(prov_album_id):
+ db_id = await self.mass.db.get_database_id(self.prov_id, prov_track.item_id, MediaType.Track)
+ if db_id:
+ items.append( await self.mass.db.track(db_id) )
+ else:
+ prov_track.album = album
+ items.append(prov_track)
+ return items
+
+ async def playlist_tracks(self, prov_playlist_id, limit=100, offset=0) -> List[Track]:
+ ''' return playlist tracks for the given provider playlist id'''
+ items = []
+ for prov_track in await self.get_playlist_tracks(prov_playlist_id, limit=limit, offset=offset):
+ for prov_mapping in prov_track.provider_ids:
+ item_prov_id = prov_mapping["provider"]
+ prov_item_id = prov_mapping["item_id"]
+ db_id = await self.mass.db.get_database_id(item_prov_id, prov_item_id, MediaType.Track)
+ if db_id:
+ items.append( await self.mass.db.track(db_id) )
+ else:
+ items.append(prov_track)
+ return items
+
+ async def artist_toptracks(self, prov_item_id) -> List[Track]:
+ ''' return top tracks for an artist '''
+ items = []
+ for prov_track in await self.get_artist_toptracks(prov_item_id):
+ db_id = await self.mass.db.get_database_id(self.prov_id, prov_track.item_id, MediaType.Track)
+ if db_id:
+ items.append( await self.mass.db.track(db_id) )
+ else:
+ items.append(prov_track)
+ return items
+
+ async def artist_albums(self, prov_item_id) -> List[Track]:
+ ''' return (all) albums for an artist '''
+ items = []
+ for prov_album in await self.get_artist_albums(prov_item_id):
+ db_id = await self.mass.db.get_database_id(self.prov_id, prov_album.item_id, MediaType.Album)
+ if db_id:
+ items.append( await self.mass.db.album(db_id) )
+ else:
+ items.append(prov_album)
+ return items
+
+ async def match_artist(self, searchartist:Artist, searchtracks:List[Track]):
+ ''' try to match artist in this provider by supplying db artist '''
+ for searchtrack in searchtracks:
+ searchstr = "%s - %s" %(searchartist.name, searchtrack.name)
+ search_results = await self.search(searchstr, [MediaType.Track], limit=5)
+ for item in search_results["tracks"]:
+ if item.name == searchtrack.name and item.version == searchtrack.version and item.album.name == searchtrack.album.name:
+ # double safety check - artist must match exactly !
+ for artist in item.artists:
+ if artist.name == searchartist.name:
+ # just load this item in the database, it will be matched automagically ;-)
+ return await self.artist(artist.item_id, lazy=False)
+
+ async def match_album(self, searchalbum:Album):
+ ''' try to match album in this provider by supplying db album '''
+ searchstr = "%s - %s %s" %(searchalbum.artist.name, searchalbum.name, searchalbum.version)
+ search_results = await self.search(searchstr, [MediaType.Album], limit=5)
+ for item in search_results["albums"]:
+ if item.name == searchalbum.name and item.version == searchalbum.version and item.artist.name == searchalbum.artist.name:
+ # just load this item in the database, it will be matched automagically ;-)
+ await self.album(item.item_id, lazy=False)
+
+ async def match_track(self, searchtrack:Track):
+ ''' try to match track in this provider by supplying db track '''
+ searchstr = "%s - %s" %(searchtrack.artists[0].name, searchtrack.name)
+ searchartists = [item.name for item in searchtrack.artists]
+ search_results = await self.search(searchstr, [MediaType.Track], limit=5)
+ for item in search_results["tracks"]:
+ if item.name == searchtrack.name and item.version == searchtrack.version and item.album.name == searchtrack.album.name:
+ # double safety check - artist must match exactly !
+ for artist in item.artists:
+ if artist.name in searchartists:
+ # just load this item in the database, it will be matched automagically ;-)
+ await self.track(item.item_id, lazy=False)
+
+ ### Provider specific implementation #####
+
+ async def search(self, searchstring, media_types=List[MediaType], limit=5):
+ ''' perform search on the provider '''
+ raise NotImplementedError
+
+ async def get_library_artists(self) -> List[Artist]:
+ ''' retrieve library artists from the provider '''
+ raise NotImplementedError
+
+ async def get_library_albums(self) -> List[Album]:
+ ''' retrieve library albums from the provider '''
+ raise NotImplementedError
+
+ async def get_library_tracks(self) -> List[Track]:
+ ''' retrieve library tracks from the provider '''
+ raise NotImplementedError
+
+ async def get_playlists(self) -> List[Playlist]:
+ ''' retrieve library/subscribed playlists from the provider '''
+ raise NotImplementedError
+
+ async def get_radios(self) -> List[Radio]:
+ ''' retrieve library/subscribed radio stations from the provider '''
+ raise NotImplementedError
+
+ async def get_artist(self, prov_item_id) -> Artist:
+ ''' get full artist details by id '''
+ raise NotImplementedError
+
+ async def get_artist_albums(self, prov_item_id) -> List[Album]:
+ ''' get a list of albums for the given artist '''
+ raise NotImplementedError
+
+ async def get_artist_toptracks(self, prov_item_id) -> List[Track]:
+ ''' get a list of most popular tracks for the given artist '''
+ raise NotImplementedError
+
+ async def get_album(self, prov_item_id) -> Album:
+ ''' get full album details by id '''
+ raise NotImplementedError
+
+ async def get_track(self, prov_item_id) -> Track:
+ ''' get full track details by id '''
+ raise NotImplementedError
+
+ async def get_playlist(self, prov_item_id) -> Playlist:
+ ''' get full playlist details by id '''
+ raise NotImplementedError
+
+ async def get_radio(self, prov_item_id) -> Radio:
+ ''' get full radio details by id '''
+ raise NotImplementedError
+
+ async def get_album_tracks(self, prov_item_id, limit=100, offset=0) -> List[Track]:
+ ''' get album tracks for given album id '''
+ raise NotImplementedError
+
+ async def get_playlist_tracks(self, prov_item_id, limit=100, offset=0) -> List[Track]:
+ ''' get playlist tracks for given playlist id '''
+ raise NotImplementedError
+
+ async def add_library(self, prov_item_id, media_type:MediaType):
+ ''' add item to library '''
+ raise NotImplementedError
+
+ async def remove_library(self, prov_item_id, media_type:MediaType):
+ ''' remove item from library '''
+ raise NotImplementedError
+
+ async def add_playlist_tracks(self, prov_playlist_id, prov_track_ids):
+ ''' add track(s) to playlist '''
+ raise NotImplementedError
+
+ async def remove_playlist_tracks(self, prov_playlist_id, prov_track_ids):
+ ''' remove track(s) from playlist '''
+ raise NotImplementedError
+
+ async def get_stream_content_type(self, track_id):
+ ''' return the content type for the given track when it will be streamed'''
+ raise NotImplementedError
+
+ async def get_stream(self, track_id):
+ ''' get audio stream for a track '''
+ raise NotImplementedError
+
+
+class PlayerProvider():
+ '''
+ Model for a Playerprovider
+ Common methods usable for every provider
+ Provider specific __get methods shoud be overriden in the provider specific implementation
+ '''
+ name = 'My great Musicplayer provider' # display name
+ prov_id = 'my_provider' # used as id
+ icon = ''
+
+ def __init__(self, mass):
+ self.mass = mass
+
+ ### Common methods and properties ####
+
+ async def players(self):
+ ''' return all players for this provider '''
+ return self.mass.provider_players(self.prov_id)
+
+ async def get_player(self, player_id):
+ ''' return player by id '''
+ return self.mass.get_player(player_id)
+
+ async def add_player(self, player_id, name='', is_group=False):
+ ''' register a new player '''
+ return self.mass.player.add_player(player_id,
+ self.prov_id, name=name, is_group=is_group)
+
+ async def remove_player(self, player_id):
+ ''' remove a player '''
+ return self.mass.player.remove_player(player_id)
+
+ ### Provider specific implementation #####
+
+ async def player_config_entries(self):
+ ''' get the player config entries for this provider (list with key/value pairs)'''
+ return [
+ (CONF_ENABLED, True, CONF_ENABLED)
+ ]
+
+ async def play_media(self, player_id, media_items:List[Track], queue_opt='play'):
+ '''
+ play media on a player
+ params:
+ - player_id: id of the player
+ - media_items: List of Tracks to play, each Track will contain uri attribute (e.g. spotify:track:1234 or http://pathtostream)
+ - queue_opt:
+ replace: replace whatever is currently playing with this media
+ next: the given media will be played after the currently playing track
+ add: add to the end of the queue
+ play: keep existing queue but play the given item(s) now first
+ '''
+ raise NotImplementedError
+
+ async def player_command(self, player_id, cmd:str, cmd_args=None):
+ ''' issue command on player (play, pause, next, previous, stop, power, volume, mute) '''
+ raise NotImplementedError
+
+
+
--- /dev/null
+#!/usr/bin/env python3
+# -*- coding:utf-8 -*-
+
+from enum import Enum
+from typing import List
+from ..utils import run_periodic, LOGGER, parse_track_title, try_parse_int, try_parse_bool, try_parse_float
+from ..constants import CONF_ENABLED
+from ..modules.cache import use_cache
+from media_types import Track, MediaType
+from player_queue import PlayerQueue, QueueItem
+
+
+class PlayerState(str, Enum):
+ Off = "off"
+ Stopped = "stopped"
+ Paused = "paused"
+ Playing = "playing"
+
+class Player():
+ ''' representation of a player '''
+
+ #### Provider specific implementation, should be overridden ####
+
+ async def get_config_entries(self):
+ ''' [MAY OVERRIDE] get the player-specific config entries for this player (list with key/value pairs)'''
+ return []
+
+ async def __stop(self):
+ ''' [MUST OVERRIDE] send stop command to player '''
+ raise NotImplementedError
+
+ async def __play(self):
+ ''' [MUST OVERRIDE] send play (unpause) command to player '''
+ raise NotImplementedError
+
+ async def __pause(self):
+ ''' [MUST OVERRIDE] send pause command to player '''
+ raise NotImplementedError
+
+ async def __power_on(self):
+ ''' [MUST OVERRIDE] send power ON command to player '''
+ raise NotImplementedError
+
+ async def __power_off(self):
+ ''' [MUST OVERRIDE] send power TOGGLE command to player '''
+ raise NotImplementedError
+
+ async def __volume_set(self, volume_level):
+ ''' [MUST OVERRIDE] send new volume level command to player '''
+ raise NotImplementedError
+
+ async def __volume_mute(self, is_muted=False):
+ ''' [MUST OVERRIDE] send mute command to player '''
+ raise NotImplementedError
+
+ async def __play_queue(self):
+ ''' [MUST OVERRIDE] tell player to start playing the queue '''
+ raise NotImplementedError
+
+ #### Common implementation, should NOT be overrridden #####
+
+ def __init__(self, mass, player_id, prov_id):
+ self.mass = mass
+ self._player_id = player_id
+ self._prov_id = prov_id
+ self._name = ''
+ self._is_group = False
+ self._state = PlayerState.Stopped
+ self._powered = False
+ self._cur_time = 0
+ self._volume_level = 0
+ self._muted = False
+ self._group_parent = None
+ self._queue = PlayerQueue(mass, self)
+
+ @property
+ def player_id(self):
+ ''' [PROTECTED] player_id of this player '''
+ return self._player_id
+
+ @property
+ def player_provider(self):
+ ''' [PROTECTED] provider id of this player '''
+ return self._prov_id
+
+ @property
+ def name(self):
+ ''' [PROTECTED] name of this player '''
+ if self.settings.get('name'):
+ return self.settings['name']
+ else:
+ return self._name
+
+ @name.setter
+ def name(self, name):
+ ''' [PROTECTED] set (real) name of this player '''
+ if name != self._name:
+ self._name = name
+ self.mass.event_loop.create_task(self.update())
+
+ @property
+ def is_group(self):
+ ''' [PROTECTED] is_group property of this player '''
+ return self._is_group
+
+ @is_group.setter
+ def is_group(self, is_group:bool):
+ ''' [PROTECTED] set is_group property of this player '''
+ if is_group != self._is_group:
+ self._is_group = is_group
+ self.mass.event_loop.create_task(self.update())
+
+ @property
+ def state(self):
+ ''' [PROTECTED] state property of this player '''
+ if not self.powered:
+ return PlayerState.Off
+ if self.group_parent:
+ group_player = self.mass.event_loop.run_until_complete(
+ self.mass.player.get_player(self.group_parent))
+ if group_player:
+ return group_player.state
+ return self._state
+
+ @state.setter
+ def state(self, state:PlayerState):
+ ''' [PROTECTED] set state property of this player '''
+ if state != self.state:
+ self._state = state
+ self.mass.event_loop.create_task(self.update())
+
+ @property
+ def powered(self):
+ ''' [PROTECTED] return power state for this player '''
+ # homeassistant integration
+ if self.mass.hass and self.settings.get('hass_power_entity') and self.settings.get('hass_power_entity_source'):
+ hass_state = self.mass.event_loop.run_until_complete(
+ self.mass.hass.get_state(
+ self.settings['hass_power_entity'],
+ attribute='source',
+ register_listener=self.update()))
+ return hass_state == self.settings['hass_power_entity_source']
+ elif self.settings.get('hass_power_entity'):
+ hass_state = self.mass.event_loop.run_until_complete(
+ self.mass.hass.get_state(
+ self.settings['hass_power_entity'],
+ attribute='state',
+ register_listener=self.update()))
+ return hass_state != 'off'
+ # mute as power
+ elif self.settings.get('mute_as_power'):
+ return self.muted
+ else:
+ return self._powered
+
+ @powered.setter
+ def powered(self, powered):
+ ''' [PROTECTED] set (real) power state for this player '''
+ self._powered = powered
+
+ @property
+ def cur_time(self):
+ ''' [PROTECTED] cur_time (player's elapsed time) property of this player '''
+ # handle group player
+ if self.group_parent:
+ group_player = self.mass.event_loop.run_until_complete(
+ self.mass.player.get_player(self.group_parent))
+ if group_player:
+ return group_player.cur_time
+ return self._cur_time
+
+ @cur_time.setter
+ def cur_time(self, cur_time:int):
+ ''' [PROTECTED] set cur_time (player's elapsed time) property of this player '''
+ if cur_time != self._cur_time:
+ self._cur_time = cur_time
+ self.mass.event_loop.create_task(self.update())
+
+ @property
+ def volume_level(self):
+ ''' [PROTECTED] volume_level property of this player '''
+ # handle group volume
+ if self.is_group:
+ group_volume = 0
+ active_players = 0
+ for child_player in self.group_childs:
+ if child_player.enabled and child_player.powered:
+ group_volume += child_player.volume_level
+ active_players += 1
+ if active_players:
+ group_volume = group_volume / active_players
+ return group_volume
+ # handle hass integration
+ elif self.mass.hass and self.settings.get('hass_volume_entity'):
+ hass_state = self.mass.event_loop.run_until_complete(
+ self.mass.hass.get_state(
+ self.settings['hass_volume_entity'],
+ attribute='volume_level',
+ register_listener=self.update()))
+ return int(try_parse_float(hass_state)*100)
+ else:
+ return self._volume_level
+
+ @volume_level.setter
+ def volume_level(self, volume_level:int):
+ ''' [PROTECTED] set volume_level property of this player '''
+ volume_level = try_parse_int(volume_level)
+ if volume_level != self._volume_level:
+ self._volume_level = volume_level
+ self.mass.event_loop.create_task(self.update())
+
+ @property
+ def muted(self):
+ ''' [PROTECTED] muted property of this player '''
+ return self._muted
+
+ @muted.setter
+ def muted(self, is_muted:bool):
+ ''' [PROTECTED] set muted property of this player '''
+ is_muted = try_parse_bool(is_muted)
+ if is_muted != self._muted:
+ self._muted = is_muted
+ self.mass.event_loop.create_task(self.update())
+
+ @property
+ def group_parent(self):
+ ''' [PROTECTED] group_parent property of this player '''
+ return self._group_parent
+
+ @group_parent.setter
+ def group_parent(self, group_parent:str):
+ ''' [PROTECTED] set muted property of this player '''
+ if group_parent != self._group_parent:
+ self._group_parent = group_parent
+ self.mass.create_task(self.update())
+
+ @property
+ def group_childs(self):
+ ''' [PROTECTED] return group childs '''
+ if not self.is_group:
+ return []
+ return [item for item in self.mass.player.players if item.group_parent == self.player_id]
+
+ @property
+ def settings(self):
+ ''' [PROTECTED] get the player config settings '''
+ player_settings = self.mass.config['player_settings'].get(self.player_id)
+ if not player_settings:
+ return self.mass.event_loop.run_until_complete(self.__update_player_settings())
+
+ @property
+ def enabled(self):
+ ''' [PROTECTED] player enabled config setting '''
+ return self.settings.get('enabled')
+
+ @property
+ def queue(self):
+ ''' [PROTECTED] player's queue '''
+ # handle group player
+ if self.group_parent:
+ group_player = self.mass.event_loop.run_until_complete(
+ self.mass.player.get_player(self.group_parent))
+ if group_player:
+ return group_player.queue
+ return self._queue
+
+ async def stop(self):
+ ''' [PROTECTED] send stop command to player '''
+ if self.group_parent:
+ # redirect playback related commands to parent player
+ group_player = await self.mass.player.get(self.group_parent)
+ if group_player:
+ return await group_player.stop()
+ else:
+ return await self.__stop()
+
+ async def play(self):
+ ''' [PROTECTED] send play (unpause) command to player '''
+ if self.group_parent:
+ # redirect playback related commands to parent player
+ group_player = await self.mass.player.get_player(self.group_parent)
+ if group_player:
+ return await group_player.play()
+ elif self.state == PlayerState.Paused:
+ return await self.__play()
+ elif self.state != PlayerState.Playing:
+ return await self.play_queue()
+
+ async def pause(self):
+ ''' [PROTECTED] send pause command to player '''
+ if self.group_parent:
+ # redirect playback related commands to parent player
+ group_player = await self.mass.player.get_player(self.group_parent)
+ if group_player:
+ return await group_player.pause()
+ else:
+ return await self.__pause()
+
+ async def power_on(self):
+ ''' [PROTECTED] send power ON command to player '''
+ self.__power_on()
+ # handle mute as power
+ if self.settings['mute_as_power']:
+ self.volume_mute(False)
+ # handle hass integration
+ if self.mass.hass and self.settings.get('hass_power_entity') and self.settings.get('hass_power_entity_source'):
+ cur_source = await self.mass.hass.get_state(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']
+ }
+ await self.mass.hass.call_service('media_player', 'select_source', service_data)
+ elif 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['play_power_on']:
+ self.play()
+ # handle group power
+ if self.group_parent:
+ # player has a group parent, check if it should be turned on
+ group_player = await self.mass.player.get_player(self.group_parent)
+ if group_player and not group_player.powered:
+ return await group_player.power_on()
+
+ async def power_off(self):
+ ''' [PROTECTED] send power TOGGLE command to player '''
+ self.__power_off()
+ # handle mute as power
+ if self.settings['mute_as_power']:
+ self.volume_mute(True)
+ # handle hass integration
+ if self.mass.hass and self.settings.get('hass_power_entity') and self.settings.get('hass_power_entity_source'):
+ cur_source = await self.mass.hass.get_state(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 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_ff', service_data)
+ # handle group power
+ if self.is_group:
+ # player is group, turn off all childs
+ for item in self.group_childs:
+ if item.powered:
+ await item.power_off()
+ elif self.group_parent:
+ # player has a group parent, check if it should be turned off
+ group_player = await self.mass.player.get_player(self.group_parent)
+ if group_player.powered:
+ needs_power = False
+ for child_player in group_player.group_childs:
+ if child_player.player_id != self.player_id and child_player.powered:
+ needs_power = True
+ break
+ if not needs_power:
+ await group_player.power_off()
+
+ async def power_toggle(self):
+ ''' [PROTECTED] send toggle power command to player '''
+ if self.powered:
+ return await self.power_off()
+ else:
+ return await self.power_on()
+
+ async def volume_set(self, volume_level):
+ ''' [PROTECTED] send new volume level command to player '''
+ volume_level = try_parse_int(volume_level)
+ # handle group volume
+ if self.is_group:
+ cur_volume = self.volume_level
+ new_volume = volume_level
+ volume_dif = new_volume - cur_volume
+ if cur_volume == 0:
+ volume_dif_percent = 1+(new_volume/100)
+ else:
+ volume_dif_percent = volume_dif/cur_volume
+ for child_player in self.group_childs:
+ if 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)
+ await child_player.volume_set(new_child_volume)
+ # handle hass integration
+ elif self.mass.hass and self.settings.get('hass_volume_entity'):
+ service_data = {
+ '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.__volume_set(100) # just force full volume on actual player if volume is outsourced to hass
+ else:
+ await self.__volume_set(volume_level)
+
+ async def volume_up(self):
+ ''' [MAY OVERRIDE] send volume up command to player '''
+ new_level = self.volume_level + 1
+ return await self.volume_set(new_level)
+
+ async def volume_down(self):
+ ''' [MAY OVERRIDE] send volume down command to player '''
+ new_level = self.volume_level - 1
+ if new_level < 0:
+ new_level = 0
+ return await self.volume_set(new_level)
+
+ async def volume_mute(self, is_muted=False):
+ ''' [MUST OVERRIDE] send mute command to player '''
+ return await self.__volume_mute(is_muted)
+
+ async def play_queue(self):
+ ''' [PROTECTED] send play_queue (start stream) command to player '''
+ if self.group_parent:
+ # redirect playback related commands to parent player
+ group_player = await self.mass.player.get_player(self.group_parent)
+ if group_player:
+ return await group_player.play_queue()
+ elif self.queue.items:
+ return await self.__play_queue()
+
+ async def play_media(self, media_item, queue_opt='play'):
+ '''
+ play media item(s) on this player
+ media_item: media item(s) that should be played (Track, Album, Artist, Playlist, Radio)
+ single item or list of items
+ queue_opt:
+ play -> insert new items in queue and start playing at the inserted position
+ replace -> replace queue contents with these items
+ next -> play item(s) after current playing item
+ add -> append new items at end of the queue
+ '''
+ # a single item or list of items may be provided
+ media_items = media_item if isinstance(media_item, list) else [media_item]
+ queue_tracks = []
+ for media_item in media_items:
+ # collect tracks to play
+ if media_item.media_type == MediaType.Artist:
+ tracks = await self.mass.music.artist_toptracks(media_item.item_id,
+ provider=media_item.provider)
+ elif media_item.media_type == MediaType.Album:
+ tracks = await self.mass.music.album_tracks(media_item.item_id,
+ provider=media_item.provider)
+ elif media_item.media_type == MediaType.Playlist:
+ tracks = await self.mass.music.playlist_tracks(media_item.item_id,
+ provider=media_item.provider, offset=0, limit=0)
+ else:
+ tracks = [media_item] # single track
+ for track in tracks:
+ queue_item = QueueItem()
+ queue_item.name = track.name
+ queue_item.artists = track.artists
+ queue_item.album = track.album
+ queue_item.duration = track.duration
+ queue_item.version = track.version
+ queue_item.metadata = track.metadata
+ queue_item.media_type = track.media_type
+ queue_item.uri = 'http://%s:%s/stream_queue?player_id=%s'% (
+ self.local_ip, self.mass.config['base']['web']['http_port'], player_id)
+ # sort by quality and check track availability
+ for prov_media in sorted(track.provider_ids, key=operator.itemgetter('quality'), reverse=True):
+ media_provider = prov_media['provider']
+ media_item_id = prov_media['item_id']
+ player_supported_provs = player_prov.supported_musicproviders
+ if media_provider in player_supported_provs and not self.mass.config['player_settings'][player_id]['force_http_streamer']:
+ # the provider can handle this media_type directly !
+ track.uri = await self.get_track_uri(media_item_id, media_provider, player_id, is_radio=is_radio)
+ playable_tracks.append(track)
+ match_found = True
+ elif 'http' in player_prov.supported_musicproviders:
+ # fallback to http streaming if supported
+ track.uri = await self.get_track_uri(media_item_id, media_provider, player_id, True, is_radio=is_radio)
+ queue_tracks.append(track)
+ match_found = True
+ if match_found:
+ break
+ if queue_tracks:
+ if self._players[player_id].shuffle_enabled:
+ random.shuffle(playable_tracks)
+ if queue_opt in ['next', 'play'] and len(playable_tracks) > 1:
+ queue_opt = 'replace' # always assume playback of multiple items as new queue
+ return await player_prov.play_media(player_id, playable_tracks, queue_opt)
+ else:
+ raise Exception("Musicprovider and/or media not supported by player %s !" % (player_id) )
+
+ async def update(self):
+ ''' [PROTECTED] signal player updated '''
+ self.__update_player_settings()
+ LOGGER.info("player updated: %s" % self.name)
+ self.mass.signal_event('player changed', self)
+
+ async def __update_player_settings(self):
+ ''' [PROTECTED] get (or create) player config settings '''
+ config_entries = [ # default config entries for a player
+ ("enabled", False, "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')
+ ]
+ # append player specific settings
+ config_entries += await self.get_config_entries()
+ if self.is_group or not self.group_parent:
+ config_entries += [ # play on power on setting
+ ("play_power_on", False, "player_power_play"),
+ ]
+ 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")]
+ player_settings = self.mass.config['player_settings'].get(self.player_id,{})
+ for key, def_value, desc in config_entries:
+ if not key in player_settings:
+ if (isinstance(def_value, str) and def_value.startswith('<')):
+ player_settings[key] = None
+ else:
+ player_settings[key] = def_value
+ self.mass.config['player_settings'][self.player_id] = player_settings
+ self.mass.config['player_settings'][self.player_id]['__desc__'] = config_entries
+ return player_settings
+
\ No newline at end of file
--- /dev/null
+#!/usr/bin/env python3
+# -*- coding:utf-8 -*-
+
+from ..utils import LOGGER
+from ..constants import CONF_ENABLED
+from typing import List
+from player import PlayerState
+from media_types import Track, TrackQuality
+import operator
+import random
+
+class QueueItem(object):
+ ''' representation of a queue item, simplified version of track '''
+ def __init__(self):
+ self.item_id = None
+ self.provider = None
+ self.name = ''
+ self.duration = 0
+ self.version = ''
+ self.quality = TrackQuality.FLAC_LOSSLESS
+ self.metadata = {}
+ self.artists = []
+ self.album = None
+ self.uri = ""
+ self.is_radio = False
+ def __eq__(self, other):
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+ return (self.name == other.name and
+ self.version == other.version and
+ self.item_id == other.item_id and
+ self.provider == other.provider)
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+class PlayerQueue():
+ '''
+ basic implementation of a queue for a player
+ if no player specific queue exists, this will be used
+ '''
+ # TODO: Persistent storage in DB
+
+ def __init__(self, mass, player):
+ self.mass = mass
+ self._player = player
+ self._items = []
+ self._shuffle_enabled = True
+ self._repeat_enabled = True
+ self._cur_index = None
+
+ @property
+ def shuffle_enabled(self):
+ return self._shuffle_enabled
+
+ @property
+ def repeat_enabled(self):
+ return self._repeat_enabled
+
+ @property
+ def cur_index(self):
+ return self._cur_index
+
+ @property
+ def cur_item(self):
+ if self._cur_index == None:
+ return None
+ return self.mass.event_loop.run_until_complete(self.get_item(self._cur_index))
+
+ @property
+ async def next_index(self):
+ '''
+ return the next queue index for this player
+ '''
+ if not self.items:
+ # queue is empty
+ return None
+ if self.cur_index == None:
+ # playback started
+ return 0
+ else:
+ # player already playing (or paused) so return the next item
+ if len(self.items) > (self.cur_index + 1):
+ return self.cur_index + 1
+ elif self._repeat_enabled:
+ # repeat enabled, start queue at beginning
+ return 0
+ return None
+
+ @property
+ async def next_item(self):
+ '''
+ return the next item in the queue
+ '''
+ return self.mass.event_loop.run_until_complete(
+ self.get_item(self.next_index))
+
+ @property
+ async def items(self):
+ '''
+ return all queue items for this player
+ '''
+ return self._items
+
+ async def get_item(self, index):
+ ''' get item by index from queue '''
+ if len(self._items) > index:
+ return self._items[index]
+ return None
+
+ async def shuffle(self, enable_shuffle:bool):
+ ''' enable/disable shuffle '''
+ if not self._shuffle_enabled and enable_shuffle:
+ # shuffle requested
+ self._shuffle_enabled = True
+ self._items = await self.__shuffle_items(self._items)
+ self._cur_index = None
+ await self._player.play_queue()
+ self.mass.event_loop.create_task(self._player.update())
+ elif self._shuffle_enabled and not enable_shuffle:
+ self._shuffle_enabled = False
+ # TODO: Unshuffle the list ?
+ self.mass.event_loop.create_task(self._player.update())
+
+ async def load(self, queue_items:List[QueueItem]):
+ ''' load (overwrite) queue with new items '''
+ if self._shuffle_enabled:
+ queue_items = await self.__shuffle_items(queue_items)
+ self._items = queue_items
+ self._cur_index = None
+ await self._player.play_queue()
+
+ async def insert(self, queue_items:List[QueueItem], offset=0):
+ '''
+ insert new items at offset x from current position
+ keeps remaining items in queue
+ if offset 0 or None, will start playing newly added item(s)
+ '''
+ insert_at_index = self.cur_index + offset
+ if not self.items or insert_at_index >= len(self.items):
+ return await self.load(queue_items)
+ if self.shuffle_enabled:
+ queue_items = await self.__shuffle_items(queue_items)
+ self._items = self._items[:insert_at_index] + queue_items + self._items[insert_at_index:]
+ if not offset:
+ await self._player.stop()
+ await self._player.play_queue()
+
+ async def append(self, queue_items:List[QueueItem]):
+ '''
+ append new items at the end of the queue
+ '''
+ if self.shuffle_enabled:
+ queue_items = await self.__shuffle_items(queue_items)
+ self._items = self._items + queue_items
+
+ async def __shuffle_items(self, queue_items):
+ ''' shuffle a list of tracks '''
+ # for now we use default python random function
+ # can be extended with some more magic last last_played and stuff
+ return random.sample(queue_items, len(queue_items))
\ No newline at end of file
--- /dev/null
+#!/usr/bin/env python3
+# -*- coding:utf-8 -*-
+
+from enum import Enum
+from typing import List
+from ..utils import run_periodic, LOGGER, parse_track_title
+from ..constants import CONF_ENABLED
+from ..modules.cache import use_cache
+from player_queue import PlayerQueue
+from media_types import Track
+from player import Player
+
+
+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):
+ self.mass = mass
+ self.name = 'My great Musicplayer provider' # display name
+ self.prov_id = 'my_provider' # used as id
+
+ ### Common methods and properties ####
+
+ @property
+ async def players(self):
+ ''' return all players for this provider '''
+ return self.mass.player.get_provider_players(self.prov_id)
+
+ async def get_player(self, player_id:str):
+ ''' return player by id '''
+ return self.mass.player.get_player(player_id)
+
+ async def add_player(self, player:Player):
+ ''' register a new player '''
+ return self.mass.player.add_player(player)
+
+ async def remove_player(self, player_id:str):
+ ''' remove a player '''
+ return self.mass.player.remove_player(player_id)
+
+ ### Provider specific implementation #####
+
+
+
+
+
self.create_config_entries()
self.local_ip = get_ip()
self.analyze_jobs = {}
-
- def create_config_entries(self):
- ''' sets the config entries for this module (list with key/value pairs)'''
- config_entries = [
- ('volume_normalisation', True, 'enable_r128_volume_normalisation'),
- ('target_volume', '-23', 'target_volume_lufs'),
- ('fallback_gain_correct', '-12', 'fallback_gain_correct')
- ]
- if not self.mass.config['base'].get('http_streamer'):
- self.mass.config['base']['http_streamer'] = {}
- self.mass.config['base']['http_streamer']['__desc__'] = config_entries
- for key, def_value, desc in config_entries:
- if not key in self.mass.config['base']['http_streamer']:
- self.mass.config['base']['http_streamer'][key] = def_value
async def stream_track(self, http_request):
''' start streaming track from provider '''
raise asyncio.CancelledError()
return resp
- async def stream_queue(self, http_request):
+ async def stream(self, http_request):
'''
- stream all tracks in queue from player with http
- loads large part of audiodata in memory so only recommended for high performance servers
- use case is enable crossfade/gapless support for chromecast devices
+ stream queue track(s) for player with http
'''
- player_id = http_request.query.get('player_id')
- startindex = int(http_request.query.get('startindex'))
+ player_id = request.match_info.get('player_id','')
+ #startindex = int(http_request.query.get('startindex'))
cancelled = threading.Event()
resp = web.StreamResponse(status=200,
reason='OK',
break
await resp.write(chunk)
queue.task_done()
- LOGGER.info("stream_queue fininished for %s" % player_id)
+ LOGGER.info("stream fininished for %s" % player_id)
except asyncio.CancelledError:
cancelled.set()
- LOGGER.info("stream_queue interrupted for %s" % player_id)
+ LOGGER.info("stream interrupted for %s" % player_id)
raise asyncio.CancelledError()
return resp
LOGGER.info("Start Queue Stream for player %s at index %s" %(player.name, queue_index))
last_fadeout_data = b''
# report start of queue playback so we can calculate current track/duration etc.
- self.mass.event_loop.create_task(self.mass.player.player_queue_stream_update(player_id, queue_index, True))
+ # self.mass.event_loop.create_task(self.mass.player.player_queue_stream_update(player_id, queue_index, True))
while True:
# get the (next) track in queue
try:
+++ /dev/null
-#!/usr/bin/env python3
-# -*- coding:utf-8 -*-
-
-import asyncio
-import os
-from utils import run_periodic, run_async_background_task, LOGGER, try_parse_int, try_supported
-import aiohttp
-from difflib import SequenceMatcher as Matcher
-from models import MediaType, Track, Artist, Album, Playlist, Radio
-from typing import List
-import toolz
-import operator
-
-
-BASE_DIR = os.path.dirname(os.path.abspath(__file__))
-MODULES_PATH = os.path.join(BASE_DIR, "musicproviders" )
-
-class Music():
- ''' several helpers around the musicproviders '''
-
- def __init__(self, mass):
- self.sync_running = False
- self.mass = mass
- self.providers = {}
- # dynamically load musicprovider modules
- self.load_music_providers()
- # schedule sync task
- mass.event_loop.create_task(self.sync_music_providers())
-
- 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:
- return await self.album(item_id, provider, lazy=lazy)
- elif media_type == MediaType.Track:
- return await self.track(item_id, provider, lazy=lazy)
- elif media_type == MediaType.Playlist:
- return await self.playlist(item_id, provider)
- elif media_type == MediaType.Radio:
- return await self.radio(item_id, provider)
- else:
- return None
-
- async def library_artists(self, limit=0, offset=0, orderby='name', provider_filter=None) -> List[Artist]:
- ''' return all library artists, optionally filtered by provider '''
- return await self.mass.db.library_artists(provider=provider_filter, limit=limit, offset=offset, orderby=orderby)
-
- async def library_albums(self, limit=0, offset=0, orderby='name', provider_filter=None) -> List[Album]:
- ''' return all library albums, optionally filtered by provider '''
- return await self.mass.db.library_albums(provider=provider_filter, limit=limit, offset=offset, orderby=orderby)
-
- async def library_tracks(self, limit=0, offset=0, orderby='name', provider_filter=None) -> List[Track]:
- ''' return all library tracks, optionally filtered by provider '''
- return await self.mass.db.library_tracks(provider=provider_filter, limit=limit, offset=offset, orderby=orderby)
-
- async def playlists(self, limit=0, offset=0, orderby='name', provider_filter=None) -> List[Playlist]:
- ''' return all library playlists, optionally filtered by provider '''
- return await self.mass.db.playlists(provider=provider_filter, limit=limit, offset=offset, orderby=orderby)
-
- async def radios(self, limit=0, offset=0, orderby='name', provider_filter=None) -> List[Playlist]:
- ''' return all library radios, optionally filtered by provider '''
- return await self.mass.db.radios(provider=provider_filter, limit=limit, offset=offset, orderby=orderby)
-
- async def library_items(self, media_type:MediaType, limit=0, offset=0, orderby='name', provider_filter=None) -> List[object]:
- ''' get multiple music items in library'''
- if media_type == MediaType.Artist:
- return await self.library_artists(limit=limit, offset=offset, orderby=orderby, provider_filter=provider_filter)
- elif media_type == MediaType.Album:
- return await self.library_albums(limit=limit, offset=offset, orderby=orderby, provider_filter=provider_filter)
- elif media_type == MediaType.Track:
- return await self.library_tracks(limit=limit, offset=offset, orderby=orderby, provider_filter=provider_filter)
- elif media_type == MediaType.Playlist:
- return await self.playlists(limit=limit, offset=offset, orderby=orderby, provider_filter=provider_filter)
- elif media_type == MediaType.Radio:
- return await self.radios(limit=limit, offset=offset, orderby=orderby, provider_filter=provider_filter)
-
- 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':
- 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:
- ''' 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)
-
- 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':
- 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 '''
- for playlist in await self.playlists():
- if playlist.name == name:
- return playlist
- return None
-
- async def radio_by_name(self, name) -> Radio:
- ''' get radio by name '''
- for radio in await self.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 '''
- artist = await self.artist(artist_id, provider)
- # always append database tracks
- items = await self.mass.db.artist_tracks(artist.item_id)
- for prov_mapping in artist.provider_ids:
- prov_id = prov_mapping['provider']
- prov_item_id = prov_mapping['item_id']
- prov_obj = self.providers[prov_id]
- items += await prov_obj.artist_toptracks(prov_item_id)
- items = list(toolz.unique(items, key=operator.attrgetter('item_id')))
- items.sort(key=lambda x: x.name, reverse=False)
- return items
-
- async def artist_albums(self, artist_id, provider='database') -> List[Album]:
- ''' get (all) albums for given artist '''
- artist = await self.artist(artist_id, provider)
- # always append database tracks
- items = await self.mass.db.artist_albums(artist.item_id)
- for prov_mapping in artist.provider_ids:
- prov_id = prov_mapping['provider']
- prov_item_id = prov_mapping['item_id']
- prov_obj = self.providers[prov_id]
- items += await prov_obj.artist_albums(prov_item_id)
- items = list(toolz.unique(items, key=operator.attrgetter('item_id')))
- items.sort(key=lambda x: x.name, reverse=False)
- return items
-
- async def album_tracks(self, album_id, provider='database') -> List[Track]:
- ''' get the album tracks for given album '''
- items = []
- album = await self.album(album_id, provider)
- for prov_mapping in album.provider_ids:
- prov_id = prov_mapping['provider']
- prov_item_id = prov_mapping['item_id']
- prov_obj = self.providers[prov_id]
- items += await prov_obj.album_tracks(prov_item_id)
- items = list(toolz.unique(items, key=operator.attrgetter('item_id')))
- items = sorted(items, key=operator.attrgetter('disc_number'), reverse=False)
- items = sorted(items, key=operator.attrgetter('track_number'), reverse=False)
- return items
-
- async def playlist_tracks(self, playlist_id, provider='database', offset=0, limit=50) -> List[Track]:
- ''' get the tracks for given playlist '''
- playlist = None
- if not provider or provider == 'database':
- playlist = await self.mass.db.playlist(playlist_id)
- if playlist and playlist.is_editable:
- # database synced playlist, return tracks from db...
- return await self.mass.db.playlist_tracks(
- playlist.item_id, offset=offset, limit=limit)
- else:
- # return playlist tracks from provider
- playlist = await self.playlist(playlist_id, provider)
- prov = playlist.provider_ids[0]
- return await self.providers[prov['provider']].playlist_tracks(
- prov['item_id'], offset=offset, limit=limit)
-
- 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, limit)
- if online:
- # include results from music providers
- for prov in self.providers.values():
- 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
- else:
- result[item_type] += items
- # filter out duplicates
- for item_type, items in result.items():
- items = list(toolz.unique(items, key=operator.attrgetter('item_id')))
- return result
-
- async def item_action(self, item_id, media_type, provider, action, action_details=None):
- ''' perform action on item (such as library add/remove) '''
- result = None
- item = await self.item(item_id, media_type, provider)
- if item and action in ['library_add', 'library_remove']:
- # remove or add item to the library
- for prov_mapping in result.provider_ids:
- prov_id = prov_mapping['provider']
- prov_item_id = prov_mapping['item_id']
- for prov in self.providers.values():
- if prov.prov_id == prov_id:
- if action == 'add':
- result = await prov.add_library(prov_item_id, media_type)
- elif action == 'remove':
- result = await prov.remove_library(prov_item_id, media_type)
- return result
-
- async def add_playlist_tracks(self, playlist_id, tracks:List[Track]):
- ''' 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(playlist_id, 'database')
- if not playlist or not playlist.is_editable:
- LOGGER.warning("Playlist %s is not editable - skip addition of tracks" %(playlist.name))
- return False
- playlist_prov = playlist.provider_ids[0] # playlist can only have one provider (for now)
- cur_playlist_tracks = await self.mass.db.playlist_tracks(playlist_id, limit=0)
- # grab all (database) track ids in the playlist so we can check for duplicates
- cur_playlist_track_ids = [item.item_id for item in cur_playlist_tracks]
- track_ids_to_add = []
- for track in tracks:
- if not track.provider == 'database':
- # make sure we have a database track
- track = await self.track(track.item_id, track.provider, lazy=False)
- if track.item_id in cur_playlist_track_ids:
- LOGGER.warning("Track %s already in playlist %s - skip addition" %(track.name, playlist.name))
- continue
- # we can only add a track to a provider playlist if the track is available on that provider
- # exception is the file provider which does accept tracks from all providers in the m3u playlist
- # this should all be handled in the frontend but these checks are here just to be safe
- track_playlist_provs = [item['provider'] for item in track.provider_ids]
- if playlist_prov['provider'] in track_playlist_provs:
- # a track can contain multiple versions on the same provider
- # # simply sort by quality and just add the first one (assuming the track is still available)
- track_versions = sorted(track.provider_ids, key=operator.itemgetter('quality'), reverse=True)
- for track_version in track_versions:
- if track_version['provider'] == playlist_prov['provider']:
- track_ids_to_add.append(track_version['item_id'])
- break
- elif playlist_prov['provider'] == 'file':
- # the file provider can handle uri's from all providers in the file so simply add the db id
- track_ids_to_add.append(track.item_id)
- else:
- LOGGER.warning("Track %s not available on provider %s - skip addition to playlist %s" %(track.name, playlist_prov['provider'], playlist.name))
- continue
- # actually add the tracks to the playlist on the provider
- await self.providers[playlist_prov['provider']].add_playlist_tracks(playlist_prov['item_id'], track_ids_to_add)
- # schedule sync
- self.mass.event_loop.create_task(self.sync_playlist_tracks(playlist.item_id, playlist_prov['provider'], playlist_prov['item_id']))
-
- @run_periodic(3600)
- async def sync_music_providers(self):
- ''' periodic sync of all music providers '''
- if self.sync_running:
- return
- self.sync_running = True
- for prov_id in self.providers.keys():
- # sync library artists
- await try_supported(self.sync_library_artists(prov_id))
- await try_supported(self.sync_library_albums(prov_id))
- await try_supported(self.sync_library_tracks(prov_id))
- await try_supported(self.sync_playlists(prov_id))
- await try_supported(self.sync_radios(prov_id))
- self.sync_running = False
-
- async def sync_library_artists(self, prov_id):
- ''' sync library artists for given provider'''
- music_provider = self.providers[prov_id]
- prev_items = await self.library_artists(provider_filter=prov_id)
- prev_db_ids = [item.item_id for item in prev_items]
- cur_items = await music_provider.get_library_artists()
- cur_db_ids = []
- for item in cur_items:
- 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)
- # 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)
- LOGGER.info("Finished syncing Artists for provider %s" % prov_id)
-
- async def sync_library_albums(self, prov_id):
- ''' sync library albums for given provider'''
- music_provider = self.providers[prov_id]
- prev_items = await self.library_albums(provider_filter=prov_id)
- prev_db_ids = [item.item_id for item in prev_items]
- cur_items = await music_provider.get_library_albums()
- cur_db_ids = []
- for item in cur_items:
- db_item = await music_provider.album(item.item_id, lazy=False)
- cur_db_ids.append(db_item.item_id)
- # precache album tracks...
- for album_track in await music_provider.get_album_tracks(item.item_id):
- await music_provider.track(album_track.item_id)
- if not db_item.item_id in prev_db_ids:
- await self.mass.db.add_to_library(db_item.item_id, MediaType.Album, 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.Album, prov_id)
- LOGGER.info("Finished syncing Albums for provider %s" % prov_id)
-
- async def sync_library_tracks(self, prov_id):
- ''' sync library tracks for given provider'''
- music_provider = self.providers[prov_id]
- prev_items = await self.library_tracks(provider_filter=prov_id)
- prev_db_ids = [item.item_id for item in prev_items]
- cur_items = await music_provider.get_library_tracks()
- cur_db_ids = []
- for item in cur_items:
- 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)
- # 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)
- LOGGER.info("Finished syncing Tracks for provider %s" % prov_id)
-
- async def sync_playlists(self, prov_id):
- ''' sync library playlists for given provider'''
- music_provider = self.providers[prov_id]
- prev_items = await self.playlists(provider_filter=prov_id)
- prev_db_ids = [item.item_id for item in prev_items]
- cur_items = await music_provider.get_playlists()
- cur_db_ids = []
- for item in cur_items:
- # always add to db because playlist attributes could have changed
- 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)
- if item.is_editable:
- # precache/sync playlist tracks (user owned playlists only)
- asyncio.create_task( self.sync_playlist_tracks(db_id, prov_id, item.item_id) )
- # 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)
- LOGGER.info("Finished syncing Playlists for provider %s" % prov_id)
-
- async def sync_playlist_tracks(self, db_playlist_id, prov_id, prov_playlist_id):
- ''' sync library playlists tracks for given provider'''
- music_provider = self.providers[prov_id]
- prev_items = await self.playlist_tracks(db_playlist_id)
- prev_db_ids = [item.item_id for item in prev_items]
- cur_items = await music_provider.get_playlist_tracks(prov_playlist_id, limit=0)
- cur_db_ids = []
- pos = 0
- for item in cur_items:
- # we need to do this the complicated way because the file provider can return tracks from other providers
- for prov_mapping in item.provider_ids:
- item_provider = prov_mapping['provider']
- prov_item_id = prov_mapping['item_id']
- db_item = await self.providers[item_provider].track(prov_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_playlist_track(db_playlist_id, db_item.item_id, pos)
- pos += 1
- # process playlist track deletions
- for db_id in prev_db_ids:
- if db_id not in cur_db_ids:
- await self.mass.db.remove_playlist_track(db_playlist_id, db_id)
- LOGGER.info("Finished syncing Playlist %s tracks for provider %s" % (prov_playlist_id, prov_id))
-
- async def sync_radios(self, prov_id):
- ''' sync library radios for given provider'''
- music_provider = self.providers[prov_id]
- prev_items = await self.radios(provider_filter=prov_id)
- prev_db_ids = [item.item_id for item in prev_items]
- cur_items = await music_provider.get_radios()
- cur_db_ids = []
- for item in cur_items:
- 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)
- # 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)
- LOGGER.info("Finished syncing Radios for provider %s" % prov_id)
-
- def load_music_providers(self):
- ''' dynamically load musicproviders '''
- 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('.')):
- module_name = item.replace(".py","")
- LOGGER.debug("Loading musicprovider module %s" % module_name)
- try:
- mod = __import__("modules.musicproviders." + module_name, fromlist=[''])
- if not self.mass.config['musicproviders'].get(module_name):
- self.mass.config['musicproviders'][module_name] = {}
- self.mass.config['musicproviders'][module_name]['__desc__'] = mod.config_entries()
- for key, def_value, desc in mod.config_entries():
- if not key in self.mass.config['musicproviders'][module_name]:
- self.mass.config['musicproviders'][module_name][key] = def_value
- mod = mod.setup(self.mass)
- if mod:
- self.providers[mod.prov_id] = mod
- cls_name = mod.__class__.__name__
- LOGGER.info("Successfully initialized module %s" % cls_name)
- except Exception as exc:
- LOGGER.exception("Error loading module %s: %s" %(module_name, exc))
--- /dev/null
+#!/usr/bin/env python3
+# -*- coding:utf-8 -*-
+
+import asyncio
+from typing import List
+import toolz
+import operator
+import os
+from ..utils import run_periodic, LOGGER, try_supported
+from ..models.media_types import MediaType, Track, Artist, Album, Playlist, Radio
+
+
+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+MODULES_PATH = os.path.join(BASE_DIR, "musicproviders" )
+
+class Music():
+ ''' several helpers around the musicproviders '''
+
+ def __init__(self, mass):
+ self.sync_running = False
+ self.mass = mass
+ self.providers = {}
+ # dynamically load musicprovider modules
+ self.load_music_providers()
+ # schedule sync task
+ mass.event_loop.create_task(self.sync_music_providers())
+
+ 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:
+ return await self.album(item_id, provider, lazy=lazy)
+ elif media_type == MediaType.Track:
+ return await self.track(item_id, provider, lazy=lazy)
+ elif media_type == MediaType.Playlist:
+ return await self.playlist(item_id, provider)
+ elif media_type == MediaType.Radio:
+ return await self.radio(item_id, provider)
+ else:
+ return None
+
+ async def library_artists(self, limit=0, offset=0, orderby='name', provider_filter=None) -> List[Artist]:
+ ''' return all library artists, optionally filtered by provider '''
+ return await self.mass.db.library_artists(provider=provider_filter, limit=limit, offset=offset, orderby=orderby)
+
+ async def library_albums(self, limit=0, offset=0, orderby='name', provider_filter=None) -> List[Album]:
+ ''' return all library albums, optionally filtered by provider '''
+ return await self.mass.db.library_albums(provider=provider_filter, limit=limit, offset=offset, orderby=orderby)
+
+ async def library_tracks(self, limit=0, offset=0, orderby='name', provider_filter=None) -> List[Track]:
+ ''' return all library tracks, optionally filtered by provider '''
+ return await self.mass.db.library_tracks(provider=provider_filter, limit=limit, offset=offset, orderby=orderby)
+
+ async def playlists(self, limit=0, offset=0, orderby='name', provider_filter=None) -> List[Playlist]:
+ ''' return all library playlists, optionally filtered by provider '''
+ return await self.mass.db.playlists(provider=provider_filter, limit=limit, offset=offset, orderby=orderby)
+
+ async def radios(self, limit=0, offset=0, orderby='name', provider_filter=None) -> List[Playlist]:
+ ''' return all library radios, optionally filtered by provider '''
+ return await self.mass.db.radios(provider=provider_filter, limit=limit, offset=offset, orderby=orderby)
+
+ async def library_items(self, media_type:MediaType, limit=0, offset=0, orderby='name', provider_filter=None) -> List[object]:
+ ''' get multiple music items in library'''
+ if media_type == MediaType.Artist:
+ return await self.library_artists(limit=limit, offset=offset, orderby=orderby, provider_filter=provider_filter)
+ elif media_type == MediaType.Album:
+ return await self.library_albums(limit=limit, offset=offset, orderby=orderby, provider_filter=provider_filter)
+ elif media_type == MediaType.Track:
+ return await self.library_tracks(limit=limit, offset=offset, orderby=orderby, provider_filter=provider_filter)
+ elif media_type == MediaType.Playlist:
+ return await self.playlists(limit=limit, offset=offset, orderby=orderby, provider_filter=provider_filter)
+ elif media_type == MediaType.Radio:
+ return await self.radios(limit=limit, offset=offset, orderby=orderby, provider_filter=provider_filter)
+
+ 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':
+ 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:
+ ''' 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)
+
+ 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':
+ 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 '''
+ for playlist in await self.playlists():
+ if playlist.name == name:
+ return playlist
+ return None
+
+ async def radio_by_name(self, name) -> Radio:
+ ''' get radio by name '''
+ for radio in await self.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 '''
+ artist = await self.artist(artist_id, provider)
+ # always append database tracks
+ items = await self.mass.db.artist_tracks(artist.item_id)
+ for prov_mapping in artist.provider_ids:
+ prov_id = prov_mapping['provider']
+ prov_item_id = prov_mapping['item_id']
+ prov_obj = self.providers[prov_id]
+ items += await prov_obj.artist_toptracks(prov_item_id)
+ items = list(toolz.unique(items, key=operator.attrgetter('item_id')))
+ items.sort(key=lambda x: x.name, reverse=False)
+ return items
+
+ async def artist_albums(self, artist_id, provider='database') -> List[Album]:
+ ''' get (all) albums for given artist '''
+ artist = await self.artist(artist_id, provider)
+ # always append database tracks
+ items = await self.mass.db.artist_albums(artist.item_id)
+ for prov_mapping in artist.provider_ids:
+ prov_id = prov_mapping['provider']
+ prov_item_id = prov_mapping['item_id']
+ prov_obj = self.providers[prov_id]
+ items += await prov_obj.artist_albums(prov_item_id)
+ items = list(toolz.unique(items, key=operator.attrgetter('item_id')))
+ items.sort(key=lambda x: x.name, reverse=False)
+ return items
+
+ async def album_tracks(self, album_id, provider='database') -> List[Track]:
+ ''' get the album tracks for given album '''
+ items = []
+ album = await self.album(album_id, provider)
+ for prov_mapping in album.provider_ids:
+ prov_id = prov_mapping['provider']
+ prov_item_id = prov_mapping['item_id']
+ prov_obj = self.providers[prov_id]
+ items += await prov_obj.album_tracks(prov_item_id)
+ items = list(toolz.unique(items, key=operator.attrgetter('item_id')))
+ items = sorted(items, key=operator.attrgetter('disc_number'), reverse=False)
+ items = sorted(items, key=operator.attrgetter('track_number'), reverse=False)
+ return items
+
+ async def playlist_tracks(self, playlist_id, provider='database', offset=0, limit=50) -> List[Track]:
+ ''' get the tracks for given playlist '''
+ playlist = None
+ if not provider or provider == 'database':
+ playlist = await self.mass.db.playlist(playlist_id)
+ if playlist and playlist.is_editable:
+ # database synced playlist, return tracks from db...
+ return await self.mass.db.playlist_tracks(
+ playlist.item_id, offset=offset, limit=limit)
+ else:
+ # return playlist tracks from provider
+ playlist = await self.playlist(playlist_id, provider)
+ prov = playlist.provider_ids[0]
+ return await self.providers[prov['provider']].playlist_tracks(
+ prov['item_id'], offset=offset, limit=limit)
+
+ 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, limit)
+ if online:
+ # include results from music providers
+ for prov in self.providers.values():
+ 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
+ else:
+ result[item_type] += items
+ # filter out duplicates
+ for item_type, items in result.items():
+ items = list(toolz.unique(items, key=operator.attrgetter('item_id')))
+ return result
+
+ async def item_action(self, item_id, media_type, provider, action, action_details=None):
+ ''' perform action on item (such as library add/remove) '''
+ result = None
+ item = await self.item(item_id, media_type, provider)
+ if item and action in ['library_add', 'library_remove']:
+ # remove or add item to the library
+ for prov_mapping in result.provider_ids:
+ prov_id = prov_mapping['provider']
+ prov_item_id = prov_mapping['item_id']
+ for prov in self.providers.values():
+ if prov.prov_id == prov_id:
+ if action == 'add':
+ result = await prov.add_library(prov_item_id, media_type)
+ elif action == 'remove':
+ result = await prov.remove_library(prov_item_id, media_type)
+ return result
+
+ async def add_playlist_tracks(self, playlist_id, tracks:List[Track]):
+ ''' 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(playlist_id, 'database')
+ if not playlist or not playlist.is_editable:
+ LOGGER.warning("Playlist %s is not editable - skip addition of tracks" %(playlist.name))
+ return False
+ playlist_prov = playlist.provider_ids[0] # playlist can only have one provider (for now)
+ cur_playlist_tracks = await self.mass.db.playlist_tracks(playlist_id, limit=0)
+ # grab all (database) track ids in the playlist so we can check for duplicates
+ cur_playlist_track_ids = [item.item_id for item in cur_playlist_tracks]
+ track_ids_to_add = []
+ for track in tracks:
+ if not track.provider == 'database':
+ # make sure we have a database track
+ track = await self.track(track.item_id, track.provider, lazy=False)
+ if track.item_id in cur_playlist_track_ids:
+ LOGGER.warning("Track %s already in playlist %s - skip addition" %(track.name, playlist.name))
+ continue
+ # we can only add a track to a provider playlist if the track is available on that provider
+ # exception is the file provider which does accept tracks from all providers in the m3u playlist
+ # this should all be handled in the frontend but these checks are here just to be safe
+ track_playlist_provs = [item['provider'] for item in track.provider_ids]
+ if playlist_prov['provider'] in track_playlist_provs:
+ # a track can contain multiple versions on the same provider
+ # # simply sort by quality and just add the first one (assuming the track is still available)
+ track_versions = sorted(track.provider_ids, key=operator.itemgetter('quality'), reverse=True)
+ for track_version in track_versions:
+ if track_version['provider'] == playlist_prov['provider']:
+ track_ids_to_add.append(track_version['item_id'])
+ break
+ elif playlist_prov['provider'] == 'file':
+ # the file provider can handle uri's from all providers in the file so simply add the db id
+ track_ids_to_add.append(track.item_id)
+ else:
+ LOGGER.warning("Track %s not available on provider %s - skip addition to playlist %s" %(track.name, playlist_prov['provider'], playlist.name))
+ continue
+ # actually add the tracks to the playlist on the provider
+ await self.providers[playlist_prov['provider']].add_playlist_tracks(playlist_prov['item_id'], track_ids_to_add)
+ # schedule sync
+ self.mass.event_loop.create_task(self.sync_playlist_tracks(playlist.item_id, playlist_prov['provider'], playlist_prov['item_id']))
+
+ @run_periodic(3600)
+ async def sync_music_providers(self):
+ ''' periodic sync of all music providers '''
+ if self.sync_running:
+ return
+ self.sync_running = True
+ for prov_id in self.providers.keys():
+ # sync library artists
+ await try_supported(self.sync_library_artists(prov_id))
+ await try_supported(self.sync_library_albums(prov_id))
+ await try_supported(self.sync_library_tracks(prov_id))
+ await try_supported(self.sync_playlists(prov_id))
+ await try_supported(self.sync_radios(prov_id))
+ self.sync_running = False
+
+ async def sync_library_artists(self, prov_id):
+ ''' sync library artists for given provider'''
+ music_provider = self.providers[prov_id]
+ prev_items = await self.library_artists(provider_filter=prov_id)
+ prev_db_ids = [item.item_id for item in prev_items]
+ cur_items = await music_provider.get_library_artists()
+ cur_db_ids = []
+ for item in cur_items:
+ 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)
+ # 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)
+ LOGGER.info("Finished syncing Artists for provider %s" % prov_id)
+
+ async def sync_library_albums(self, prov_id):
+ ''' sync library albums for given provider'''
+ music_provider = self.providers[prov_id]
+ prev_items = await self.library_albums(provider_filter=prov_id)
+ prev_db_ids = [item.item_id for item in prev_items]
+ cur_items = await music_provider.get_library_albums()
+ cur_db_ids = []
+ for item in cur_items:
+ db_item = await music_provider.album(item.item_id, lazy=False)
+ cur_db_ids.append(db_item.item_id)
+ # precache album tracks...
+ for album_track in await music_provider.get_album_tracks(item.item_id):
+ await music_provider.track(album_track.item_id)
+ if not db_item.item_id in prev_db_ids:
+ await self.mass.db.add_to_library(db_item.item_id, MediaType.Album, 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.Album, prov_id)
+ LOGGER.info("Finished syncing Albums for provider %s" % prov_id)
+
+ async def sync_library_tracks(self, prov_id):
+ ''' sync library tracks for given provider'''
+ music_provider = self.providers[prov_id]
+ prev_items = await self.library_tracks(provider_filter=prov_id)
+ prev_db_ids = [item.item_id for item in prev_items]
+ cur_items = await music_provider.get_library_tracks()
+ cur_db_ids = []
+ for item in cur_items:
+ 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)
+ # 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)
+ LOGGER.info("Finished syncing Tracks for provider %s" % prov_id)
+
+ async def sync_playlists(self, prov_id):
+ ''' sync library playlists for given provider'''
+ music_provider = self.providers[prov_id]
+ prev_items = await self.playlists(provider_filter=prov_id)
+ prev_db_ids = [item.item_id for item in prev_items]
+ cur_items = await music_provider.get_playlists()
+ cur_db_ids = []
+ for item in cur_items:
+ # always add to db because playlist attributes could have changed
+ 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)
+ if item.is_editable:
+ # precache/sync playlist tracks (user owned playlists only)
+ asyncio.create_task( self.sync_playlist_tracks(db_id, prov_id, item.item_id) )
+ # 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)
+ LOGGER.info("Finished syncing Playlists for provider %s" % prov_id)
+
+ async def sync_playlist_tracks(self, db_playlist_id, prov_id, prov_playlist_id):
+ ''' sync library playlists tracks for given provider'''
+ music_provider = self.providers[prov_id]
+ prev_items = await self.playlist_tracks(db_playlist_id)
+ prev_db_ids = [item.item_id for item in prev_items]
+ cur_items = await music_provider.get_playlist_tracks(prov_playlist_id, limit=0)
+ cur_db_ids = []
+ pos = 0
+ for item in cur_items:
+ # we need to do this the complicated way because the file provider can return tracks from other providers
+ for prov_mapping in item.provider_ids:
+ item_provider = prov_mapping['provider']
+ prov_item_id = prov_mapping['item_id']
+ db_item = await self.providers[item_provider].track(prov_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_playlist_track(db_playlist_id, db_item.item_id, pos)
+ pos += 1
+ # process playlist track deletions
+ for db_id in prev_db_ids:
+ if db_id not in cur_db_ids:
+ await self.mass.db.remove_playlist_track(db_playlist_id, db_id)
+ LOGGER.info("Finished syncing Playlist %s tracks for provider %s" % (prov_playlist_id, prov_id))
+
+ async def sync_radios(self, prov_id):
+ ''' sync library radios for given provider'''
+ music_provider = self.providers[prov_id]
+ prev_items = await self.radios(provider_filter=prov_id)
+ prev_db_ids = [item.item_id for item in prev_items]
+ cur_items = await music_provider.get_radios()
+ cur_db_ids = []
+ for item in cur_items:
+ 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)
+ # 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)
+ LOGGER.info("Finished syncing Radios for provider %s" % prov_id)
+
+ def load_music_providers(self):
+ ''' dynamically load musicproviders '''
+ 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('.')):
+ module_name = item.replace(".py","")
+ LOGGER.debug("Loading musicprovider module %s" % module_name)
+ try:
+ mod = __import__("modules.musicproviders." + module_name, fromlist=[''])
+ if not self.mass.config['musicproviders'].get(module_name):
+ self.mass.config['musicproviders'][module_name] = {}
+ self.mass.config['musicproviders'][module_name]['__desc__'] = mod.config_entries()
+ for key, def_value, desc in mod.config_entries():
+ if not key in self.mass.config['musicproviders'][module_name]:
+ self.mass.config['musicproviders'][module_name][key] = def_value
+ mod = mod.setup(self.mass)
+ if mod:
+ self.providers[mod.prov_id] = mod
+ cls_name = mod.__class__.__name__
+ LOGGER.info("Successfully initialized module %s" % cls_name)
+ except Exception as exc:
+ LOGGER.exception("Error loading module %s: %s" %(module_name, exc))
+++ /dev/null
-#!/usr/bin/env python3
-# -*- coding:utf-8 -*-
-
-import asyncio
-import os
-from utils import run_periodic, LOGGER, try_parse_int, try_parse_float, get_ip, run_async_background_task
-from models import MediaType, PlayerState, MusicPlayer, TrackQuality
-import operator
-import random
-from copy import deepcopy
-import functools
-import urllib
-
-BASE_DIR = os.path.dirname(os.path.abspath(__file__))
-MODULES_PATH = os.path.join(BASE_DIR, "playerproviders" )
-
-
-class Player():
- ''' several helpers to handle playback through player providers '''
-
- def __init__(self, mass):
- self.mass = mass
- self.providers = {}
- self._players = {}
- self.local_ip = get_ip()
- # dynamically load provider modules
- self.load_providers()
-
- async def players(self):
- ''' return all players '''
- items = list(self._players.values())
- items.sort(key=lambda x: x.name, reverse=False)
- return items
-
- async def player(self, player_id):
- ''' return players by id '''
- return self._players[player_id]
-
- async def player_command(self, player_id, cmd, cmd_args=None, skip_group_power=False):
- ''' issue command on player (play, pause, next, previous, stop, power, volume, mute) '''
- if player_id not in self._players:
- return
- player = self._players[player_id]
- prov_id = player.player_provider
- prov = self.providers[prov_id]
- LOGGER.debug('received command %s for player %s' %(cmd, player.name))
- # handle some common workarounds
- if (cmd in ['pause', 'play'] and cmd_args == 'toggle') or cmd == 'playpause':
- cmd = 'pause' if player.state == PlayerState.Playing else 'play'
- if cmd == 'power' and (cmd_args == 'toggle' or not cmd_args):
- cmd_args = 'off' if player.powered else 'on'
- if cmd == 'volume' and cmd_args == 'up':
- cmd_args = player.volume_level + 1
- elif cmd == 'volume' and cmd_args == 'down':
- cmd_args = player.volume_level - 1
- elif cmd == 'volume' and '+' in str(cmd_args):
- cmd_args = player.volume_level + try_parse_int(cmd_args.replace('+',''))
- elif cmd == 'volume' and '-' in str(cmd_args):
- cmd_args = player.volume_level - try_parse_int(cmd_args.replace('-',''))
- if cmd == 'mute' and (cmd_args == 'toggle' or not cmd_args):
- cmd_args = 'off' if player.muted else 'on'
- if cmd == 'volume' and cmd_args:
- if try_parse_int(cmd_args) > 100:
- cmd_args = 100
- elif try_parse_int(cmd_args) < 0:
- cmd_args = 0
- if cmd == 'volume' and player.is_group and player.settings['apply_group_volume']:
- # group volume
- return await self.__player_command_group_volume(player, cmd_args)
-
- # redirect playlist related commands to parent player
- if player.group_parent and cmd not in ['power', 'volume', 'mute']:
- return await self.player_command(player.group_parent, cmd, cmd_args)
- # handle hass integration
- await self.__player_command_hass_integration(player, cmd, cmd_args)
- # handle group power for group players
- if not skip_group_power:
- asyncio.create_task(self.__player_command_group_power(player, cmd, cmd_args))
- # handle play on power on
- if cmd == 'power' and cmd_args == 'on' and player.settings['play_power_on']:
- cmd = 'play'
- cmd_args = None
- # handle mute as power
- if cmd == 'power' and player.settings['mute_as_power']:
- cmd = 'mute'
- cmd_args = 'on' if cmd_args == 'off' else 'off' # invert logic (power ON is mute OFF)
- # normal execution of command on player
- await prov.player_command(player_id, cmd, cmd_args)
-
- async def __player_command_hass_integration(self, player, cmd, cmd_args):
- ''' handle hass integration in player command '''
- if not self.mass.hass:
- return
- if cmd == 'power' and player.settings.get('hass_power_entity') and player.settings.get('hass_power_entity_source'):
- cur_source = await self.mass.hass.get_state(player.settings['hass_power_entity'], attribute='source')
- if cmd_args == 'on' and not cur_source:
- service_data = { 'entity_id': player.settings['hass_power_entity'], 'source':player.settings['hass_power_entity_source'] }
- await self.mass.hass.call_service('media_player', 'select_source', service_data)
- elif cmd_args == 'off' and cur_source == player.settings['hass_power_entity_source']:
- service_data = { 'entity_id': player.settings['hass_power_entity'] }
- await self.mass.hass.call_service('media_player', 'turn_off', service_data)
- else:
- LOGGER.debug('Ignoring power command as required source is not active')
- elif cmd == 'power' and player.settings.get('hass_power_entity'):
- domain = player.settings['hass_power_entity'].split('.')[0]
- service_data = { 'entity_id': player.settings['hass_power_entity']}
- await self.mass.hass.call_service(domain, 'turn_%s' % cmd_args, service_data)
- if cmd == 'volume' and player.settings.get('hass_volume_entity'):
- service_data = { 'entity_id': player.settings['hass_power_entity'], 'volume_level': int(cmd_args)/100}
- await self.mass.hass.call_service('media_player', 'volume_set', service_data)
- cmd_args = 100 # just force full volume on actual player if volume is outsourced to hass
-
- async def __player_command_group_volume(self, player, new_volume):
- ''' handle group volume '''
- cur_volume = player.volume_level
- new_volume = try_parse_int(new_volume)
- volume_dif = new_volume - cur_volume
- if cur_volume == 0:
- volume_dif_percent = 1+(new_volume/100)
- else:
- volume_dif_percent = volume_dif/cur_volume
- player_childs = [item for item in self._players.values() if item.group_parent == player.player_id]
- for child_player in player_childs:
- if 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)
- child_player.volume_level = new_child_volume
- await self.player_command(child_player.player_id, 'volume', new_child_volume)
- player.volume_level = new_volume
-
- async def __player_command_group_power(self, player, cmd, cmd_args):
- ''' handle group power command '''
- if player.is_group and player.settings['apply_group_power'] and cmd == 'power' and cmd_args == 'off':
- # turn off all child players
- player_childs = [item for item in self._players.values() if item.group_parent == player.player_id]
- for item in player_childs:
- if item.powered:
- await self.player_command(item.player_id, cmd, cmd_args, True)
- elif player.group_parent and player.group_parent in self._players:
- group_player = self._players[player.group_parent]
- if group_player.settings['apply_group_power']:
- if cmd == 'power' and cmd_args == 'on':
- if not group_player.powered:
- return await self.player_command(group_player.player_id, 'power', 'on', True)
- player_childs = [item for item in self._players.values() if item.group_parent == group_player.player_id]
- if group_player.powered and cmd == 'power' and cmd_args == 'off':
- # check if the group player should (still) be turned on
- needs_power = False
- for child_player in player_childs:
- if child_player.player_id != player.player_id and child_player.powered:
- needs_power = True
- break
- if not needs_power:
- await self.player_command(group_player.player_id, 'power', 'off', True)
-
- async def remove_player(self, player_id):
- ''' handle a player remove '''
- self._players.pop(player_id, None)
- self.mass.signal_event('player removed', player_id)
-
- async def trigger_update(self, player_id):
- ''' manually trigger update for a player '''
- if player_id in self._players:
- await self.update_player(self._players[player_id])
-
- async def update_player(self, player_details):
- ''' update (or add) player '''
- player_details = deepcopy(player_details)
- player_id = player_details.player_id
- player_changed = False
- if not player_id in self._players:
- # first message from player
- self._players[player_id] = MusicPlayer()
- player = self._players[player_id]
- player.player_id = player_id
- player.player_provider = player_details.player_provider
- player_changed = True
- else:
- player = self._players[player_id]
- player.settings = await self.__get_player_settings(player_details)
- # handle basic player settings
- player_details.enabled = player.settings['enabled']
- player_details.name = player.settings['name'] if player.settings['name'] else player_details.name
- # handle hass integration
- await self.__update_player_hass_settings(player_details, player.settings)
- # handle mute as power setting
- if player.settings['mute_as_power']:
- player_details.powered = not player_details.muted
- # combine state of group parent
- if player_details.group_parent and player_details.group_parent in self._players:
- parent_player = self._players[player_details.group_parent]
- player_details.cur_item_time = parent_player.cur_item_time
- player_details.cur_item = parent_player.cur_item
- player_details.state = parent_player.state
-
- # handle group volume/power setting
- if player_details.is_group:
- player_childs = [item for item in self._players.values() if item.group_parent == player_id]
- if player.settings['apply_group_volume']:
- player_details.volume_level = await self.__get_group_volume(player_childs)
- # detect current track changes
- if player.cur_item and player_details.cur_item and player.cur_item.name != player_details.cur_item.name:
- # track changed
- player_changed = True
- if not player.group_parent:
- LOGGER.info("%s -- STOP PLAYING %s -- SECONDS PLAYED: %s" %(player.name, player.cur_item.name, player.cur_item_time))
- LOGGER.info("%s -- START PLAYING %s" %(player.name, player_details.cur_item.name))
- player.cur_item = player_details.cur_item
- elif not player.cur_item and player_details.cur_item:
- # player started playing
- player_changed = True
- if not player.group_parent:
- LOGGER.info("%s -- START PLAYING %s" %(player.name, player_details.cur_item.name))
- player.cur_item = player_details.cur_item
- elif player.cur_item and not player_details.cur_item:
- # player queue cleared
- player_changed = True
- if not player.group_parent:
- LOGGER.info("%s -- STOP PLAYING %s -- SECONDS PLAYED: %s" %(player.name, player.cur_item.name, player.cur_item_time))
- player.cur_item = player_details.cur_item
- # compare values to detect changes
- for key, cur_value in player.__dict__.items():
- if key != 'settings':
- new_value = getattr(player_details, key)
- if new_value != cur_value:
- player_changed = True
- setattr(player, key, new_value)
- LOGGER.debug('key changed: %s for player %s - new value: %s' % (key, player.name, new_value))
- if player_changed:
- # player is added or updated!
- self.mass.signal_event('player updated', player)
- if player_details.is_group:
- # is groupplayer, trigger update of its childs
- player_childs = [item for item in self._players.values() if item.group_parent == player_id]
- for child in player_childs:
- asyncio.create_task(self.trigger_update(child.player_id))
- elif player.group_parent:
- # if child player in a group, trigger update of parent
- asyncio.create_task(self.trigger_update(player.group_parent))
-
- async def __update_player_hass_settings(self, player_details, player_settings):
- ''' handle home assistant integration on a player '''
- if not self.mass.hass:
- return
- player_id = player_details.player_id
- player_settings = self.mass.config['player_settings'][player_id]
- if player_settings.get('hass_power_entity') and player_settings.get('hass_power_entity_source'):
- hass_state = await self.mass.hass.get_state(
- player_settings['hass_power_entity'],
- attribute='source',
- register_listener=functools.partial(self.trigger_update, player_id))
- player_details.powered = hass_state == player_settings['hass_power_entity_source']
- elif player_settings.get('hass_power_entity'):
- hass_state = await self.mass.hass.get_state(
- player_settings['hass_power_entity'],
- attribute='state',
- register_listener=functools.partial(self.trigger_update, player_id))
- player_details.powered = hass_state != 'off'
- if player_settings.get('hass_volume_entity'):
- hass_state = await self.mass.hass.get_state(
- player_settings['hass_volume_entity'],
- attribute='volume_level',
- register_listener=functools.partial(self.trigger_update, player_id))
- player_details.volume_level = int(try_parse_float(hass_state)*100)
-
- async def __get_group_volume(self, player_childs):
- ''' handle group volume '''
- group_volume = 0
- active_players = 0
- for child_player in player_childs:
- if child_player.enabled and child_player.powered:
- group_volume += child_player.volume_level
- active_players += 1
- group_volume = group_volume / active_players if active_players else 0
- return group_volume
-
- async def __get_group_power(self, player_childs):
- ''' handle group volume '''
- group_power = False
- for child_player in player_childs:
- print(child_player.name)
- print(child_player.powered)
- if child_player.enabled and child_player.powered:
- group_power = True
- break
- return group_power
-
- async def __get_player_settings(self, player_details):
- ''' get (or create) player config '''
- player_id = player_details.player_id
- config_entries = [ # default config entries for a player
- ("enabled", False, "player_enabled"),
- ("name", "", "player_name"),
- ("mute_as_power", False, "player_mute_power"),
- ("disable_volume", False, "player_disable_vol"),
- ("sox_effects", '', "http_streamer_sox_effects"),
- ("max_sample_rate", 96000, "max_sample_rate"),
- ("force_http_streamer", False, "force_http_streamer")
- ]
- # append provider specific player settings
- config_entries += await self.mass.player.providers[player_details.player_provider].player_config_entries()
- if player_details.is_group:
- config_entries += [ # group player settings
- ("apply_group_volume", False, "player_group_vol"),
- ("apply_group_power", False, "player_group_pow")
- ]
- if player_details.is_group or not player_details.group_parent:
- config_entries += [ # play on power on setting
- ("play_power_on", False, "player_power_play"),
- ]
- 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")]
- player_settings = self.mass.config['player_settings'].get(player_id,{})
- for key, def_value, desc in config_entries:
- if not key in player_settings:
- if (isinstance(def_value, str) and def_value.startswith('<')):
- player_settings[key] = None
- else:
- player_settings[key] = def_value
- self.mass.config['player_settings'][player_id] = player_settings
- self.mass.config['player_settings'][player_id]['__desc__'] = config_entries
- return player_settings
-
- async def play_media(self, player_id, media_item, queue_opt='play'):
- '''
- play media on a player
- player_id: id of the player
- media_item: media item(s) that should be played (Track, Album, Artist, Playlist, Radio)
- queue_opt: play, replace, next or add
- '''
- if not player_id in self._players:
- LOGGER.warning('Player %s not found' % player_id)
- return False
- player_prov = self.providers[self._players[player_id].player_provider]
- # a single item or list of items may be provided
- media_items = media_item if isinstance(media_item, list) else [media_item]
- playable_tracks = []
- for media_item in media_items:
- # collect tracks to play
- if media_item.media_type == MediaType.Artist:
- tracks = await self.mass.music.artist_toptracks(media_item.item_id, provider=media_item.provider)
- elif media_item.media_type == MediaType.Album:
- tracks = await self.mass.music.album_tracks(media_item.item_id, provider=media_item.provider)
- elif media_item.media_type == MediaType.Playlist:
- tracks = await self.mass.music.playlist_tracks(media_item.item_id, provider=media_item.provider, offset=0, limit=0)
- else:
- tracks = [media_item] # single track
- # check supported music providers by this player and work out how to handle playback...
- for track in tracks:
- # sort by quality
- match_found = False
- is_radio = track.media_type == MediaType.Radio
- for prov_media in sorted(track.provider_ids, key=operator.itemgetter('quality'), reverse=True):
- media_provider = prov_media['provider']
- media_item_id = prov_media['item_id']
- player_supported_provs = player_prov.supported_musicproviders
- if media_provider in player_supported_provs and not self.mass.config['player_settings'][player_id]['force_http_streamer']:
- # the provider can handle this media_type directly !
- track.uri = await self.get_track_uri(media_item_id, media_provider, player_id, is_radio=is_radio)
- playable_tracks.append(track)
- match_found = True
- elif 'http' in player_prov.supported_musicproviders:
- # fallback to http streaming if supported
- track.uri = await self.get_track_uri(media_item_id, media_provider, player_id, True, is_radio=is_radio)
- playable_tracks.append(track)
- match_found = True
- if match_found:
- break
- if playable_tracks:
- if self._players[player_id].shuffle_enabled:
- random.shuffle(playable_tracks)
- if queue_opt in ['next', 'play'] and len(playable_tracks) > 1:
- queue_opt = 'replace' # always assume playback of multiple items as new queue
- return await player_prov.play_media(player_id, playable_tracks, queue_opt)
- else:
- raise Exception("Musicprovider and/or media not supported by player %s !" % (player_id) )
-
- async def get_track_uri(self, item_id, provider, player_id, http_stream=False, is_radio=False):
- ''' generate the URL/URI for a media item '''
- uri = ""
- if http_stream:
- if is_radio:
- params = {"provider": provider, "radio_id": str(item_id), "player_id": str(player_id)}
- params_str = urllib.parse.urlencode(params)
- uri = 'http://%s:%s/stream_radio?%s'% (self.local_ip, self.mass.config['base']['web']['http_port'], params_str)
- else:
- params = {"provider": provider, "track_id": str(item_id), "player_id": str(player_id)}
- params_str = urllib.parse.urlencode(params)
- uri = 'http://%s:%s/stream_track?%s'% (self.local_ip, self.mass.config['base']['web']['http_port'], params_str)
- elif provider == "spotify":
- uri = 'spotify://spotify:track:%s' % item_id
- elif provider == "qobuz":
- uri = 'qobuz://%s.flac' % item_id
- elif provider == "file":
- uri = item_id
- else:
- uri = "%s://%s" %(provider, item_id)
- return uri
-
- async def player_queue(self, player_id, offset=0, limit=50):
- ''' return the items in the player's queue '''
- player = self._players[player_id]
- player_prov = self.providers[player.player_provider]
- return await player_prov.player_queue(player_id, offset=offset, limit=limit)
-
- async def player_queue_index(self, player_id):
- ''' get current index of the player's queue '''
- player = self._players[player_id]
- player_prov = self.providers[player.player_provider]
- return await player_prov.player_queue_index(player_id)
-
- async def player_queue_stream_update(self, player_id, cur_index, is_start=False):
- ''' called by our queue streamer when it started playing the queue from position x '''
- player = self._players[player_id]
- return await self.providers[player.player_provider].player_queue_stream_update(player_id, cur_index, is_start)
-
- def load_providers(self):
- ''' dynamically load providers '''
- 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('.')):
- module_name = item.replace(".py","")
- LOGGER.debug("Loading playerprovider module %s" % module_name)
- try:
- mod = __import__("modules.playerproviders." + module_name, fromlist=[''])
- if not self.mass.config['playerproviders'].get(module_name):
- self.mass.config['playerproviders'][module_name] = {}
- self.mass.config['playerproviders'][module_name]['__desc__'] = mod.config_entries()
- for key, def_value, desc in mod.config_entries():
- if not key in self.mass.config['playerproviders'][module_name]:
- self.mass.config['playerproviders'][module_name][key] = def_value
- mod = mod.setup(self.mass)
- if mod:
- self.providers[mod.prov_id] = mod
- cls_name = mod.__class__.__name__
- LOGGER.info("Successfully initialized module %s" % cls_name)
- except Exception as exc:
- LOGGER.exception("Error loading module %s: %s" %(module_name, exc))
--- /dev/null
+#!/usr/bin/env python3
+# -*- coding:utf-8 -*-
+
+import asyncio
+import os
+from enum import Enum
+from ..utils import run_periodic, LOGGER, try_parse_int, try_parse_float, get_ip, run_async_background_task
+from ..models.media_types import MediaType, TrackQuality
+from ..models.player_queue import QueueItem
+from ..models.player import PlayerState
+import operator
+import random
+import functools
+import urllib
+
+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+MODULES_PATH = os.path.join(BASE_DIR, "playerproviders" )
+
+
+
+class PlayerManager():
+ ''' several helpers to handle playback through player providers '''
+
+ def __init__(self, mass):
+ self.mass = mass
+ self.providers = {}
+ self._players = {}
+ self.local_ip = get_ip()
+ # dynamically load provider modules
+ self.load_providers()
+
+ @property
+ def players(self):
+ ''' all players as property '''
+ return self.mass.event_loop.run_until_complete(self.get_players())
+
+ async def get_players(self):
+ ''' return all players as a list '''
+ items = list(self._players.values())
+ items.sort(key=lambda x: x.name, reverse=False)
+ return items
+
+ async def get_player(self, player_id):
+ ''' return player by id '''
+ return self._players.get(player_id, None)
+
+ async def get_provider_players(self, player_provider):
+ ''' return all players for given provider_id '''
+ return [item for item in self._players.values() if item.player_provider == player_provider]
+
+ async def add_player(self, player):
+ ''' register a new player '''
+ self._players[player.player_id] = player
+ self.mass.signal_event('player added', player)
+ # TODO: turn on player if it was previously turned on ?
+ return player
+
+ async def remove_player(self, player_id):
+ ''' handle a player remove '''
+ self._players.pop(player_id, None)
+ self.mass.signal_event('player removed', player_id)
+
+ async def trigger_update(self, player_id):
+ ''' manually trigger update for a player '''
+ if player_id in self._players:
+ await self._players[player_id].update()
+
+ def load_providers(self):
+ ''' dynamically load providers '''
+ 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('.')):
+ module_name = item.replace(".py","")
+ LOGGER.debug("Loading playerprovider module %s" % module_name)
+ try:
+ mod = __import__("modules.playerproviders." + module_name, fromlist=[''])
+ if not self.mass.config['playerproviders'].get(module_name):
+ self.mass.config['playerproviders'][module_name] = {}
+ self.mass.config['playerproviders'][module_name]['__desc__'] = mod.config_entries()
+ for key, def_value, desc in mod.config_entries():
+ if not key in self.mass.config['playerproviders'][module_name]:
+ self.mass.config['playerproviders'][module_name][key] = def_value
+ mod = mod.setup(self.mass)
+ if mod:
+ self.providers[mod.prov_id] = mod
+ cls_name = mod.__class__.__name__
+ LOGGER.info("Successfully initialized module %s" % cls_name)
+ except Exception as exc:
+ LOGGER.exception("Error loading module %s: %s" %(module_name, exc))
# -*- coding:utf-8 -*-
import asyncio
-import os
-from typing import List
-import random
-import sys
-from utils import run_periodic, run_background_task, LOGGER, parse_track_title, try_parse_int
-from models import PlayerProvider, MusicPlayer, PlayerState, MediaType, TrackQuality, AlbumType, Artist, Album, Track, Playlist
-from constants import CONF_ENABLED, CONF_HOSTNAME, CONF_PORT
-import json
+# import os
+# from typing import List
+# import random
+# import sys
+# import json
import aiohttp
-import time
-import datetime
-import hashlib
+# import time
+# import datetime
+# import hashlib
import pychromecast
from pychromecast.controllers.multizone import MultizoneController
from pychromecast.controllers import BaseController
from pychromecast.controllers.media import MediaController
import types
-import urllib
-import select
+# import urllib
+# import select
+from ...utils import run_periodic, LOGGER, try_parse_int
+from ...models.playerprovider import PlayerProvider
+from ...models.player import Player, PlayerState
+from ...constants import CONF_ENABLED, CONF_HOSTNAME, CONF_PORT
def setup(mass):
''' setup the provider'''
(CONF_ENABLED, True, CONF_ENABLED),
]
+class ChromecastPlayer(Player):
+ ''' Chromecast player object '''
+ cc = None
+
+ async def __stop(self):
+ ''' send stop command to player '''
+ self.cc.media_controller.stop()
+
+ async def __play(self):
+ ''' send play command to player '''
+ self.cc.media_controller.play()
+
+ async def __pause(self):
+ ''' send pause command to player '''
+ self.cc.media_controller.pause()
+
+ async def __power_on(self):
+ ''' send power ON command to player '''
+ self.powered = True
+
+ async def __power_off(self):
+ ''' send power OFF command to player '''
+ self.powered = False
+ # power is not supported so send quit_app instead
+ if not self.group_parent:
+ self.cc.quit_app()
+
+ async def __volume_set(self, volume_level):
+ ''' send new volume level command to player '''
+ self.cc.set_volume(volume_level/100)
+ self.volume_level = volume_level
+
+ async def __volume_mute(self, is_muted=False):
+ ''' send mute command to player '''
+ self.cc.set_volume_muted(is_muted)
+
+
class ChromecastProvider(PlayerProvider):
''' support for ChromeCast Audio '''
-
+
def __init__(self, mass):
self.prov_id = 'chromecast'
self.name = 'Chromecast'
- self.icon = ''
self.mass = mass
- self._players = {}
- self._chromecasts = {}
- self._player_queue = {}
- self._player_queue_index = {}
- self._player_queue_stream_startindex = {}
self._discovery_running = False
- self.supported_musicproviders = ['http']
self.mass.event_loop.create_task(self.__periodic_chromecast_discovery())
-
- ### Provider specific implementation #####
-
- async def player_config_entries(self):
- '''
- get the player config entries for this provider
- (list with key/value pairs)
- '''
- return [
- ("crossfade_duration", 0, "crossfade_duration"),
- ]
-
- async def player_command(self, player_id, cmd:str, cmd_args=None):
- ''' issue command on player (play, pause, next, previous, stop, power, volume, mute) '''
- if (not player_id in self._chromecasts or
- not self._chromecasts[player_id].socket_client or
- not self._chromecasts[player_id].socket_client.is_connected):
- LOGGER.warning("command %s failed - %s is disconnected, rescan triggered" %(cmd, self._players[player_id].name))
- self.mass.event_loop.create_task(self.__chromecast_discovery())
- return
- if cmd == 'play':
- self._players[player_id].powered = True
- if self._chromecasts[player_id].media_controller.status.player_is_playing:
- pass
- elif self._chromecasts[player_id].media_controller.status.player_is_paused:
- self._chromecasts[player_id].media_controller.play()
- else:
- await self.__resume_queue(player_id)
- await self.mass.player.update_player(self._players[player_id])
- elif cmd == 'pause':
- self._chromecasts[player_id].media_controller.pause()
- elif cmd == 'stop':
- self._chromecasts[player_id].media_controller.stop()
- elif cmd == 'next':
- enable_crossfade = self.mass.config['player_settings'][player_id]["crossfade_duration"] > 0
- if enable_crossfade:
- await self.__play_stream_queue(player_id, self._player_queue_index[player_id]+1)
- else:
- self._chromecasts[player_id].media_controller.queue_next()
- elif cmd == 'previous':
- enable_crossfade = self.mass.config['player_settings'][player_id]["crossfade_duration"] > 0
- if enable_crossfade:
- await self.__play_stream_queue(player_id, self._player_queue_index[player_id]-1)
- else:
- self._chromecasts[player_id].media_controller.queue_prev()
- elif cmd == 'power' and cmd_args == 'off':
- self._players[player_id].powered = False
- if not self._players[player_id].group_parent:
- self._chromecasts[player_id].quit_app() # power is not supported so send quit_app instead
- await self.mass.player.update_player(self._players[player_id])
- elif cmd == 'power':
- self._players[player_id].powered = True
- await self.mass.player.update_player(self._players[player_id])
- elif cmd == 'volume':
- new_volume = try_parse_int(cmd_args)
- self._chromecasts[player_id].set_volume(new_volume/100)
- self._players[player_id].volume_level = new_volume
- await self.mass.player.update_player(self._players[player_id])
- elif cmd == 'mute' and cmd_args == 'off':
- self._chromecasts[player_id].set_volume_muted(False)
- elif cmd == 'mute':
- self._chromecasts[player_id].set_volume_muted(True)
-
- async def player_queue(self, player_id, offset=0, limit=50):
- ''' return the current items in the player's queue '''
- return self._player_queue[player_id][offset:limit]
-
- async def player_queue_index(self, player_id):
- ''' get current index of the player's queue '''
- return self._player_queue_index[player_id]
-
- async def play_media(self, player_id, media_items, queue_opt='play'):
- '''
- play media on a player
- '''
- if (not player_id in self._chromecasts or
- not self._chromecasts[player_id].socket_client or
- not self._chromecasts[player_id].socket_client.is_connected):
- LOGGER.warning("play_media failed - %s is disconnected, rescan triggered" %(self._players[player_id].name))
- self.mass.event_loop.create_task(self.__chromecast_discovery())
- return
-
- castplayer = self._chromecasts[player_id]
- cur_queue_index = self._player_queue_index.get(player_id, 0)
- enable_crossfade = self.mass.config['player_settings'][player_id]["crossfade_duration"] > 0
- is_radio = media_items and media_items[0].media_type == MediaType.Radio
-
- if queue_opt == 'replace' or not self._player_queue[player_id]:
- # overwrite queue with new items
- self._player_queue[player_id] = media_items
- if enable_crossfade and not is_radio:
- await self.__play_stream_queue(player_id, 0)
- else:
- await self.__queue_load(player_id, self._player_queue[player_id], 0)
- elif queue_opt == 'play':
- # replace current item with new item(s)
- self._player_queue[player_id] = self._player_queue[player_id][:cur_queue_index] + media_items + self._player_queue[player_id][cur_queue_index+1:]
- if enable_crossfade and not is_radio:
- await self.__play_stream_queue(player_id, cur_queue_index)
- else:
- await self.__queue_load(player_id, self._player_queue[player_id], cur_queue_index)
- elif queue_opt == 'next':
- # insert new items at current index +1
- if len(self._player_queue[player_id]) > cur_queue_index+1:
- old_next_uri = self._player_queue[player_id][cur_queue_index+1].uri
- else:
- old_next_uri = None
- self._player_queue[player_id] = self._player_queue[player_id][:cur_queue_index+1] + media_items + self._player_queue[player_id][cur_queue_index+1:]
- if not enable_crossfade or is_radio:
- # find out the itemID of the next item in CC queue
- insert_at_item_id = None
- if old_next_uri:
- for item in castplayer.media_controller.queue_items:
- if item['media']['contentId'] == old_next_uri:
- insert_at_item_id = item['itemId']
- await self.__queue_insert(player_id, media_items, insert_at_item_id)
- elif queue_opt == 'add':
- # add new items at end of queue
- self._player_queue[player_id] = self._player_queue[player_id] + media_items
- if not enable_crossfade or is_radio:
- await self.__queue_insert(player_id, media_items)
-
- async def player_queue_stream_update(self, player_id, cur_index, is_start=False):
- ''' called by our queue streamer when it started playing a track in the queue at index X '''
- if is_start:
- self._player_queue_stream_startindex[player_id] = cur_index
- self._player_queue_index[player_id] = cur_index
- # schedule update a few times as we don't know how much time is prebuffered
- for i in range(0, 20):
- castplayer = self._chromecasts[player_id]
- status = castplayer.media_controller.status
- await self.__handle_player_state(castplayer, mediastatus=status)
- await asyncio.sleep(2)
-
- ### Provider specific (helper) methods #####
-
- async def __get_cur_queue_index(self, player_id, current_uri):
- ''' retrieve index of current item in the player queue '''
- cur_index = 0
- for index, track in enumerate(self._player_queue[player_id]):
- if track.uri == current_uri:
- cur_index = index
- break
- return cur_index
async def __queue_load(self, player_id, new_tracks, startindex=None):
''' load queue on player with given queue items '''
async def __handle_player_state(self, chromecast, caststatus=None, mediastatus=None):
''' handle a player state message from the socket '''
player_id = str(chromecast.uuid)
- player = self._players[player_id]
+ player = self.get_player(player_id)
# always update player details that may change
player.name = chromecast.name
if caststatus:
player.cur_item = queue_track
player.cur_item_time = track_time
self._player_queue_index[player_id] = queue_index
- await self.mass.player.update_player(player)
-
- async def __parse_track(self, mediastatus):
- ''' parse track in CC to our internal format '''
- track = await self.__track_from_uri(mediastatus.content_id)
- if not track:
- # TODO: match this info manually in the DB!!
- track = Track()
- artist = mediastatus.artist
- album = mediastatus.album_name
- title = mediastatus.title
- track.name = "%s - %s" %(artist, title)
- track.duration = try_parse_int(mediastatus.duration)
- if mediastatus.media_metadata and mediastatus.media_metadata.get('images'):
- track.metadata.image = mediastatus.media_metadata['images'][-1]['url']
- return track
-
- async def __track_from_uri(self, uri):
- ''' try to parse uri loaded in CC to a track we understand '''
- track = None
- if uri.startswith('spotify://track:') and 'spotify' in self.mass.music.providers:
- track_id = uri.replace('spotify:track:','')
- track = await self.mass.music.providers['spotify'].track(track_id)
- elif uri.startswith('qobuz://') and 'qobuz' in self.mass.music.providers:
- track_id = uri.replace('qobuz://','').replace('.flac','')
- track = await self.mass.music.providers['qobuz'].track(track_id)
- elif uri.startswith('http') and '/stream_track' in uri:
- params = urllib.parse.parse_qs(uri.split('?')[1])
- track_id = params['track_id'][0]
- provider = params['provider'][0]
- track = await self.mass.music.providers[provider].track(track_id)
- return track
async def __handle_group_members_update(self, mz, added_player=None, removed_player=None):
''' callback when cast group members update '''
if added_player:
- if added_player in self._players:
- self._players[added_player].group_parent = str(mz._uuid)
- LOGGER.debug("player %s added to group %s" %(self._players[added_player].name, self._players[str(mz._uuid)].name))
- self.mass.event_loop.create_task(self.mass.player.update_player(self._players[added_player]))
+ player = self.get_player(added_player)
+ group_player = self.get_player(str(mz._uuid))
+ if player and group_player:
+ player.group_parent = str(mz._uuid)
+ LOGGER.debug("player %s added to group %s" %(player.name, group_player.name))
elif removed_player:
- if removed_player in self._players:
- self._players[removed_player].group_parent = None
- LOGGER.debug("player %s removed from group %s" %(self._players[removed_player].name, self._players[str(mz._uuid)].name))
- self.mass.event_loop.create_task(self.mass.player.update_player(self._players[removed_player]))
+ player = self.get_player(added_player)
+ group_player = self.get_player(str(mz._uuid))
+ if player and group_player:
+ player.group_parent = None
+ LOGGER.debug("player %s removed from group %s" %(player.name, group_player.name))
else:
for member in mz.members:
- if member in self._players:
- self._players[member].group_parent = str(mz._uuid)
- self.mass.event_loop.create_task(self.mass.player.update_player(self._players[member]))
+ player = self.get_player(member)
+ if player:
+ player.group_parent = str(mz._uuid)
@run_periodic(1800)
async def __periodic_chromecast_discovery(self):
LOGGER.info("Chromecast discovery started...")
# remove any disconnected players...
removed_players = []
- for player_id, cast in self._chromecasts.items():
- if not cast.socket_client or not cast.socket_client.is_connected:
- LOGGER.info("%s is disconnected" % cast.name)
- removed_players.append(player_id)
+ for player in self.players:
+ if not player.cc.socket_client or not player.cc.socket_client.is_connected:
+ LOGGER.info("%s is disconnected" % player.name)
+ # cleanup cast object
+ del player.cc
+ removed_players.append(player.player_id)
+ # signal removed players
for player_id in removed_players:
- try:
- self._chromecasts[player_id].disconnect()
- except Exception:
- pass
- del self._chromecasts[player_id]
- await self.mass.player.remove_player(player_id)
+ await self.remove_player(player_id)
# search for available chromecasts
from pychromecast.discovery import start_discovery, stop_discovery
def discovered_callback(name):
discovery_info = listener.services[name]
ip_address, port, uuid, model_name, friendly_name = discovery_info
player_id = str(uuid)
- if not player_id in self._chromecasts:
+ if not self.get_player(player_id):
LOGGER.info("discovered chromecast: %s - %s:%s" % (friendly_name, ip_address, port))
asyncio.run_coroutine_threadsafe(
self.__chromecast_discovered(player_id, discovery_info), self.mass.event_loop)
except ChromecastConnectionError:
LOGGER.warning("Could not connect to device %s" % player_id)
return
- if not player_id in self._players:
- player = MusicPlayer()
- player.player_id = player_id
- player.name = chromecast.name
- player.player_provider = self.prov_id
- self._players[player_id] = player
# patch the receive message method for handling queue status updates
chromecast.media_controller.queue_items = []
chromecast.media_controller.queue_cur_id = None
chromecast.register_status_listener(listenerCast)
listenerMedia = StatusMediaListener(chromecast, self.__handle_player_state, self.mass.event_loop)
chromecast.media_controller.register_status_listener(listenerMedia)
+ player = ChromecastPlayer(self.mass, player_id, self.prov_id)
if chromecast.cast_type == 'group':
- self._players[player_id].is_group = True
+ player.is_group = True
mz = MultizoneController(chromecast.uuid)
mz.register_listener(MZListener(mz, self.__handle_group_members_update, self.mass.event_loop))
chromecast.register_handler(mz)
chromecast.register_connection_listener(MZConnListener(mz))
chromecast.mz = mz
- chromecast.wait()
- self._chromecasts[player_id] = chromecast
- if not player_id in self._player_queue:
- # TODO: persistant storage of player queue ?
- self._player_queue[player_id] = []
- self._player_queue_index[player_id] = 0
+ player.cc = chromecast
+ player.cc.wait()
+ self.add_player(player)
self.update_all_group_members()
- # turn on player if it was previously turned on
- if self._players[player_id].powered:
- self.mass.event_loop.create_task(self.mass.player.player_command(player_id, "power", "on"))
def update_all_group_members(self):
''' force member update of all cast groups '''
- for cast in list(self._chromecasts.values()):
- if cast.cast_type == 'group':
- cast.mz.update_members()
+ for player in self.players:
+ if player.cc.cast_type == 'group':
+ player.cc.mz.update_members()
def chunks(l, n):
"""Yield successive n-sized chunks from l."""
items.append(track)
return items
- async def player_queue_index(self, player_id):
- ''' get current index of the player's queue '''
- raise NotImplementedError()
-
### Provider specific (helper) methods #####
async def __get_players(self):
def __init__(self, mass):
self.prov_id = 'pylms'
self.name = 'Logitech Media Server Emulation'
- self.icon = ''
self.mass = mass
- self._players = {}
self._lmsplayers = {}
- self._player_queue = {}
- self._player_queue_index = {}
self.buffer = b''
self.last_msg_received = 0
self.supported_musicproviders = ['http']
self._lmsplayers[player_id].unmute()
elif cmd == 'mute':
self._lmsplayers[player_id].mute()
-
- async def player_queue(self, player_id, offset=0, limit=50):
- ''' return the current items in the player's queue '''
- return self._player_queue[player_id][offset:limit]
-
- async def player_queue_index(self, player_id):
- ''' get current index of the player's queue '''
- return self._player_queue_index.get(player_id, 0)
async def play_media(self, player_id, media_items, queue_opt='play'):
'''
play media on a player
'''
- cur_queue_index = self._player_queue_index.get(player_id, 0)
+ player = self.get_player(player_id)
+ cur_index = player.cur_queue_index
- if queue_opt == 'replace' or not self._player_queue[player_id]:
+ if queue_opt == 'replace' or not player.queue:
# overwrite queue with new items
- self._player_queue[player_id] = media_items
+ player.queue = media_items
await self.__queue_play(player_id, 0, send_flush=True)
elif queue_opt == 'play':
# replace current item with new item(s)
- self._player_queue[player_id] = self._player_queue[player_id][:cur_queue_index] + media_items + self._player_queue[player_id][cur_queue_index+1:]
- await self.__queue_play(player_id, cur_queue_index, send_flush=True)
+ player.queue = player.queue[player_id][:cur_index] + media_items + player.queue[player_id][cur_index+1:]
+ await self.__queue_play(player_id, cur_index, send_flush=True)
elif queue_opt == 'next':
# insert new items at current index +1
- self._player_queue[player_id] = self._player_queue[player_id][:cur_queue_index+1] + media_items + self._player_queue[player_id][cur_queue_index+1:]
+ player.queue[player_id] = player.queue[player_id][:cur_index+1] + media_items + player.queue[player_id][cur_index+1:]
elif queue_opt == 'add':
# add new items at end of queue
- self._player_queue[player_id] = self._player_queue[player_id] + media_items
+ player.queue[player_id] = player.queue[player_id] + media_items
### Provider specific (helper) methods #####
async def __queue_play(self, player_id, index, send_flush=False):
''' send play command to player '''
- if not player_id in self._player_queue or not player_id in self._player_queue_index:
+ if not player_id in player.queue or not player_id in player.queue_index:
return
- if not self._player_queue[player_id]:
+ if not player.queue[player_id]:
return
if index == None:
- index = self._player_queue_index[player_id]
- if len(self._player_queue[player_id]) >= index:
- track = self._player_queue[player_id][index]
+ index = player.queue_index[player_id]
+ if len(player.queue[player_id]) >= index:
+ track = player.queue[player_id][index]
if send_flush:
self._lmsplayers[player_id].flush()
self._lmsplayers[player_id].play(track.uri)
- self._player_queue_index[player_id] = index
+ player.queue_index[player_id] = index
async def __queue_next(self, player_id):
''' request next track from queue '''
- if not player_id in self._player_queue or not player_id in self._player_queue:
+ if not player_id in player.queue or not player_id in player.queue:
return
- cur_queue_index = self._player_queue_index[player_id]
- if len(self._player_queue[player_id]) > cur_queue_index:
+ cur_queue_index = player.queue_index[player_id]
+ if len(player.queue[player_id]) > cur_queue_index:
new_queue_index = cur_queue_index + 1
elif self._players[player_id].repeat_enabled:
new_queue_index = 0
async def __queue_previous(self, player_id):
''' request previous track from queue '''
- if not player_id in self._player_queue:
+ if not player_id in player.queue:
return
- cur_queue_index = self._player_queue_index[player_id]
- if cur_queue_index == 0 and len(self._player_queue[player_id]) > 1:
- new_queue_index = len(self._player_queue[player_id]) -1
+ cur_queue_index = player.queue_index[player_id]
+ if cur_queue_index == 0 and len(player.queue[player_id]) > 1:
+ new_queue_index = len(player.queue[player_id]) -1
elif cur_queue_index == 0:
new_queue_index = cur_queue_index
else:
new_queue_index -= 1
- self._player_queue_index[player_id] = new_queue_index
+ player.queue_index[player_id] = new_queue_index
return await self.__queue_play(player_id, new_queue_index)
async def __handle_player_event(self, player_id, event, event_data=None):
lms_player = self._lmsplayers[player_id]
if event == "next_track":
return await self.__queue_next(player_id)
+ player
if not player_id in self._players:
player = MusicPlayer()
player.player_id = player_id
player.player_provider = self.prov_id
self._players[player_id] = player
- if not player_id in self._player_queue:
- self._player_queue[player_id] = []
- if not player_id in self._player_queue_index:
- self._player_queue_index[player_id] = 0
+ if not player_id in player.queue:
+ player.queue[player_id] = []
+ if not player_id in player.queue_index:
+ player.queue_index[player_id] = 0
else:
player = self._players[player_id]
# update player properties
player.powered = event_data
elif event == "state":
player.state = event_data
- if self._player_queue[player_id]:
- cur_queue_index = self._player_queue_index[player_id]
- player.cur_item = self._player_queue[player_id][cur_queue_index]
+ if player.queue[player_id]:
+ cur_queue_index = player.queue_index[player_id]
+ player.cur_item = player.queue[player_id][cur_queue_index]
# update player details
await self.mass.player.update_player(player)
app.add_routes([web.get('/jsonrpc.js', self.json_rpc)])
app.add_routes([web.post('/jsonrpc.js', self.json_rpc)])
app.add_routes([web.get('/ws', self.websocket_handler)])
- app.add_routes([web.get('/stream_track', self.mass.http_streamer.stream_track)])
- app.add_routes([web.get('/stream_radio', self.mass.http_streamer.stream_radio)])
- app.add_routes([web.get('/stream_queue', self.mass.http_streamer.stream_queue)])
+ # app.add_routes([web.get('/stream_track', self.mass.http_streamer.stream_track)])
+ # app.add_routes([web.get('/stream_radio', self.mass.http_streamer.stream_radio)])
+ app.add_routes([web.get('/stream/{player_id}', self.mass.http_streamer.stream_queue)])
app.add_routes([web.get('/api/search', self.search)])
app.add_routes([web.get('/api/config', self.get_config)])
app.add_routes([web.post('/api/config', self.save_config)])
async def player_command(self, request):
''' issue player command'''
+ result = False
player_id = request.match_info.get('player_id')
- cmd = request.match_info.get('cmd')
- cmd_args = request.match_info.get('cmd_args')
- result = await self.mass.player.player_command(player_id, cmd, cmd_args)
+ player = await self.mass.player.get_player(player_id)
+ if player:
+ cmd = request.match_info.get('cmd')
+ cmd_args = request.match_info.get('cmd_args')
+ player_cmd = getattr(player, cmd, None)
+ if player_cmd and cmd_args:
+ result = await player_cmd(player_id, cmd, cmd_args)
+ elif player_cmd and cmd_args:
+ result = await player_cmd(player_id, cmd, cmd_args)
+ else:
+ LOGGER.error("Received non-existing command %s for player %s" %(cmd, player.name))
+ else:
+ LOGGER.error("Received command dor non-existing player %s" %(player_id))
return web.json_response(result, dumps=json_serializer)
async def play_media(self, request):
try:
return float(possible_float)
except:
- return 0
+ return 0.0
+
+def try_parse_bool(possible_bool):
+ if isinstance(possible_bool, bool):
+ return possible_bool
+ else:
+ return possible_bool in ['true', 'True', '1', 'on', 'ON', 1]
def parse_track_title(track_title):
''' try to parse clean track title and version from the title '''