def parse_message(self, message): try: data = json.loads(message) logging.debug(data) if "metadata" in data: logging.error(data["metadata"]) md = Metadata() map_attributes(data["metadata"], md.__dict__, VOLSPOTIFY_ATTRIBUTE_MAP) md.artUrl = self.cover_url(data["metadata"]["albumartId"]) md.playerName = MYNAME self.control.metadata = md elif "position_ms" in data: pos = float(data["position_ms"]) / 1000 self.control.metadata.set_position(pos) elif "volume" in data: logging.debug("ignoring volume data") elif "token" in data: logging.info("got access_token update") self.control.access_token = data["token"] else: logging.warn("don't know how to handle %s", data) except Exception as e: logging.error("error while parsing %s (%s)", message, e)
def test_get_cover(self): md = Metadata() # A Rush of Blood to the Head, Coldplay md.artist = "Coldplay" # Necessary as unknown song won't be retrieved md.albummbid = "219b202d-290e-3960-b626-bf852a63bc50" self.assertIsNone(md.artUrl) self.assertIsNone(md.externalArtUrl) coverartarchive.enrich_metadata(md) self.assertIsNone(md.artUrl) self.assertIsNotNone(md.externalArtUrl)
def test_same_song(self): md1=Metadata("artist1","song1") md2=Metadata("artist1","song1", albumTitle="album1") md3=Metadata("artist1","song1", albumTitle="album2") md4=Metadata("artist2","song1") md5=Metadata("","song1") self.assertTrue(md1.sameSong(md2)) self.assertTrue(md1.sameSong(md3)) self.assertTrue(md2.sameSong(md3)) self.assertFalse(md1.sameSong(md4)) self.assertFalse(md1.sameSong(md5))
def get_meta(self): state = self.get_state() song = None if state in [STATE_PLAYING, STATE_PAUSED]: song = self.client.currentsong() md = Metadata() md.playerName = "mpd" if song is not None: map_attributes(song, md.__dict__, MPD_ATTRIBUTE_MAP) return md
def __init__(self, auto_pause=True, loop_delay=1, ignore_players=[]): self.state_table = {} self.auto_pause = auto_pause self.metadata_displays = [] self.last_update = None self.loop_delay = loop_delay self.active_player = None self.ignore_players = ignore_players self.metadata = {} self.playing = False self.connect_dbus() self.metadata = Metadata() self.metadata_lock = threading.Lock() self.volume_control = None
def demo(): from ac2.metadata import Metadata lp = LaMetricPush() time.sleep(15) md = Metadata(artist="demo artist", title="demo title") lp.notify(md)
def test_enrich(self): # We should be able to get some metadata for this one md = Metadata("Bruce Springsteen", "The River") lastfm.enrich_metadata(md) self.assertIsNotNone(md.externalArtUrl) self.assertIsNotNone(md.mbid) self.assertIsNotNone(md.artistmbid)
def __init__(self, state="unknown", metadata=None, failed=0): self.state = state self.failed = failed if metadata is not None: self.metadata = metadata else: self.metadata = Metadata() self.supported_commands = []
def test_get_cover(self): md = Metadata() # A Rush of Blood to the Head, Coldplay md.artist = "Coldplay" md.mbid = "58b961e1-a2ef-4e92-a82b-199b15bb3cd8" md.albummbid = "219b202d-290e-3960-b626-bf852a63bc50" self.assertIsNone(md.artUrl) self.assertIsNone(md.externalArtUrl) hifiberry.enrich_metadata(md) # Cover might be be in cache at the HiFiBerry musicdb, # in this case try again a few seconds later if md.externalArtUrl is None: sleep(5) hifiberry.enrich_metadata(md) self.assertIsNone(md.artUrl) self.assertIsNotNone(md.externalArtUrl)
def test_enrich(self): # We should be able to get some metadata for this one md=Metadata("Bruce Springsteen","The River") self.md_updated = False self.updates = None song_id = md.songId() self.song_id = None self.assertIsNone(md.artUrl) self.assertIsNone(md.externalArtUrl) self.assertFalse(MetaDataTest.md_updated) enrich_metadata(md, callback=self) self.assertIsNotNone(md.externalArtUrl) self.assertIsNotNone(md.mbid) self.assertIsNotNone(self.updates) self.assertIn("externalArtUrl", self.updates) self.assertIn("mbid",self.updates) self.assertIn("artistmbid",self.updates) self.assertIn("albummbid",self.updates) self.assertEqual(self.song_id, song_id)
def test_tags(self): md1=Metadata("artist1","song1") md1.add_tag("tag1") md1.add_tag("tag2") md1.add_tag("tag3") self.assertIn("tag1", md1.tags) self.assertIn("tag2", md1.tags) self.assertIn("tag3", md1.tags)
def __init__(self, port=80, host='0.0.0.0', authtoken=None, debug=False): super().__init__() self.port = port self.host = host self.debug = debug self.authtoken = authtoken self.bottle = Bottle() self.route() self.system_control = SystemControl() self.player_control = None self.lastfm_network = None self.volume_control = None self.volume = 0 self.thread = None self.lovers = [] self.updaters = [] self.artwork = ExpiringDict(max_len=100, max_age_seconds=36000000) self.notify(Metadata("Artist", "Title", "Album"))
def __init__(self, args={}): self.client = None self.playername = MYNAME self.state = STATE_STOPPED self.metadata = Metadata() if "port" in args: self.port = args["port"] else: self.port = 5030 if "host" in args: self.host = args["host"] else: self.host = "localhost" self.lastupdated = 0 self.tokenupdated = 0 self.token = None self.access_token = None
def test_init(self): md = Metadata( artist="artist", title="title", albumArtist="albumartist", albumTitle="albumtitle", artUrl="http://test", discNumber=1, trackNumber=2, playerName="player", playerState="unknown", streamUrl="http://stream") self.assertEqual(md.artist, "artist") self.assertEqual(md.title, "title") self.assertEqual(md.albumArtist, "albumartist") self.assertEqual(md.albumTitle, "albumtitle") self.assertEqual(md.artUrl, "http://test") self.assertEqual(md.externalArtUrl, None) self.assertEqual(md.discNumber, 1) self.assertEqual(md.tracknumber, 2) self.assertEqual(md.playerName, "player") self.assertEqual(md.playerState, "unknown")
def test_same_artwork(self): md1=Metadata("artist1","song1") md1.artUrl = "http://art1" md2=Metadata("artist1","song1") md2.artUrl = "http://art1" md2.externalArtUrl = "http://art2" md3=Metadata("artist1","song1") md3.artUrl = "http://art3" md3.externalArtUrl = "http://art1" self.assertTrue(md1.sameArtwork(md1)) self.assertTrue(md1.sameArtwork(md2)) self.assertFalse(md1.sameArtwork(md3)) self.assertFalse(md2.sameArtwork(md3))
def test_guess(self): md=Metadata("","Bruce Springsteen - The River") md.fix_problems(guess=True) self.assertEqual(md.artist,"Bruce Springsteen") self.assertEqual(md.title,"The River") md=Metadata("","The River - Bruce Springsteen") md.fix_problems(guess=True) self.assertEqual(md.artist,"Bruce Springsteen") self.assertEqual(md.title,"The River") md=Metadata("","Michael Kiwanuka - You Ain't The Problem") md.fix_problems(guess=True) self.assertEqual(md.artist,"Michael Kiwanuka") self.assertEqual(md.title,"You Ain't The Problem")
def test_unknown(self): md1=Metadata() md2=Metadata("","") md3=Metadata("None","None") md4=Metadata("unknown artist","unknown title") md5=Metadata("unknown","unknown") md6=Metadata("artist","") md7=Metadata(None,"name") md8=Metadata("Unknown","song") md9=Metadata("artist","unknown") md10=Metadata("artist","unknown song") md11=Metadata("artist","songs") self.assertTrue(md1.is_unknown()) self.assertTrue(md2.is_unknown()) self.assertTrue(md3.is_unknown()) self.assertTrue(md4.is_unknown()) self.assertTrue(md5.is_unknown()) self.assertTrue(md6.is_unknown()) self.assertTrue(md7.is_unknown()) self.assertTrue(md8.is_unknown()) self.assertTrue(md9.is_unknown()) self.assertTrue(md10.is_unknown()) self.assertFalse(md11.is_unknown())
def get_meta(self, name): """ Return the metadata for the given player instance """ try: device_prop = self.dbus_get_device_prop_interface(name) prop = device_prop.Get( "org.mpris.MediaPlayer2.Player", "Metadata") try: artist = array_to_string(prop.get("xesam:artist")) except: artist = None try: title = prop.get("xesam:title") except: title = None try: albumArtist = array_to_string(prop.get("xesam:albumArtist")) except: albumArtist = None try: albumTitle = prop.get("xesam:album") except: albumTitle = None try: artURL = prop.get("mpris:artUrl") except: artURL = None try: discNumber = prop.get("xesam:discNumber") except: discNumber = None try: trackNumber = prop.get("xesam:trackNumber") except: trackNumber = None md = Metadata(artist, title, albumArtist, albumTitle, artURL, discNumber, trackNumber) try: md.streamUrl = prop.get("xesam:url") except: pass try: md.trackId = prop.get("mpris:trackid") except: pass if (name.startswith(MPRIS_PREFIX)): md.playerName = name[len(MPRIS_PREFIX):] else: md.playerName = name return md except dbus.exceptions.DBusException as e: if "ServiceUnknown" in e.__class__.__name__: # unfortunately we can't do anything about this and # logging doesn't help, therefore just ignoring this case pass # Â logging.warning("service %s disappered, cleaning up", e) else: logging.warning("no mpris data received %s", e.__class__.__name__) md = Metadata() md.playerName = self.playername(name) return md
def test_unknown(self): md = Metadata() coverartarchive.enrich_metadata(md) self.assertIsNone(md.artUrl) self.assertIsNone(md.externalArtUrl)
def main_loop(self): """ Main loop: - monitors state of all players - pauses players if a new player starts playback """ finished = False md = Metadata() active_players = [] MAX_FAIL = 3 # Workaround for spotifyd problems # spotify_stopped = 0 # Workaround for squeezelite mute squeezelite_active = 0 previous_state = "" ts = datetime.datetime.now() while not (finished): additional_delay = 0 new_player_started = None metadata_notified = False playing = False new_song = False state = "unknown" last_ts = ts ts = datetime.datetime.now() duration = (ts - last_ts).total_seconds() for p in self.all_players(): if self.playername(p) in self.ignore_players: continue if p not in self.state_table: ps = PlayerState() ps.supported_commands = self.get_supported_commands(p) logging.debug("Player %s supports %s", p, ps.supported_commands) self.state_table[p] = ps thisplayer_state = "unknown" try: thisplayer_state = self.get_player_state(p).lower() self.state_table[p].failed = 0 except: logging.info("Got no state from " + p) state = "unknown" self.state_table[p].failed = \ self.state_table[p].failed + 1 if self.state_table[p].failed >= MAX_FAIL: playername = self.playername(p) logging.warning("%s failed, trying to restart", playername) watchdog.restart_service(playername) self.state_table[p].failed = 0 self.state_table[p].state = thisplayer_state # Check if playback started on a player that wasn't # playing before if thisplayer_state == STATE_PLAYING: playing = True state = "playing" # if self.playername(p) == SPOTIFY_NAME: # spotify_stopped = 0 if self.playername(p) == LMS_NAME: squeezelite_active = 2 report_usage( "audiocontrol_playing_{}".format(self.playername(p)), duration) md = self.get_meta(p) if (p not in active_players): new_player_started = p active_players.insert(0, p) md.playerState = thisplayer_state # MPRIS delivers only very few metadata, these will be # enriched with external sources if (md.sameSong(self.metadata)): md.fill_undefined(self.metadata) else: new_song = True self.state_table[p].metadata = md if not (md.sameSong(self.metadata)): logging.debug("updated metadata: \nold %s\nnew %s", self.metadata, md) # Store this as "current" with self.metadata_lock: self.metadata = md self.metadata_notify(md) logging.debug("notifications about new metadata sent") elif state != previous_state: logging.debug("changed state to playing") self.metadata_notify(md) # Some players deliver artwork after initial metadata if md.artUrl != self.metadata.artUrl: logging.debug("artwork changes from %s to %s", self.metadata.artUrl, md.artUrl) self.metadata_notify(md) # Add metadata if this is a new song if new_song: enrich_metadata_bg(md, callback=self) logging.debug("metadata updater thread started") # Even if we din't send metadata, this is still # flagged metadata_notified = True else: # always keep one player in the active_players # list if len(active_players) > 1: if p in active_players: active_players.remove(p) # update metadata for stopped players from time to time i = randint(0, 600) if (i == 0): md = self.get_meta(p) md.playerState = thisplayer_state self.state_table[p].metadata = md self.playing = playing # Find active (or last paused) player if len(active_players) > 0: self.active_player = active_players[0] else: self.active_player = None # # Workaround for wrong state messages by Spotify # # Assume Spotify is still playing for 10 seconds if it's the # # active (or last stopped) player # if self.playername(self.active_player) == SPOTIFY_NAME: # # Less aggressive metadata polling on Spotify as each polling will # # result in an API request # additional_delay = 4 # if not(playing): # spotify_stopped += 1 + additional_delay # if spotify_stopped < 26: # if (spotify_stopped % 5) == 0: # logging.debug("spotify workaround %s", spotify_stopped) # playing = True # # Workaround for LMS muting the output after stopping the # player if self.volume_control is not None: if self.playername(self.active_player) != LMS_NAME: if squeezelite_active > 0: squeezelite_active = squeezelite_active - 1 logging.debug( "squeezelite was active before, unmuting") self.volume_control.set_mute(False) if not (playing) and squeezelite_active > 0: squeezelite_active = squeezelite_active - 1 logging.debug("squeezelite was active before, unmuting") self.volume_control.set_mute(False) # There might be no active player, but one that is paused # or stopped if not (playing) and len(active_players) > 0: p = active_players[0] md = self.get_meta(p) md.playerState = self.state_table[p].state state = md.playerState if state != previous_state: logging.debug("state transition %s -> %s", previous_state, state) if not metadata_notified: self.metadata_notify(md) for sd in self.state_displays: sd.update_playback_state(state) previous_state = state if new_player_started is not None: if self.auto_pause: logging.info( "new player %s started, pausing other active players", self.playername(active_players[0])) self.pause_inactive(new_player_started) else: logging.debug("auto-pause disabled") self.last_update = datetime.datetime.now() time.sleep(self.loop_delay + additional_delay)
def tmux_scraper(self): logging.info('tidalcontrol::tmux_scraper') cmd = 'docker exec -ti tidal_connect /usr/bin/tmux capture-pane -pS -10' stdout = subprocess.check_output(cmd.split()) WINDOW_SIZE = 40 WINDOW_COUNT = 2 VALUE_MAP = {} for line in stdout.decode('utf-8').splitlines(): if line.startswith('PlaybackState::'): VALUE_MAP['state'] = line.split('::')[1] # parse props if line.startswith('xx', WINDOW_SIZE - 1): for window_cnt in range(WINDOW_COUNT): str_keyvals = (line[(WINDOW_SIZE * window_cnt) + 1:(WINDOW_SIZE * (window_cnt + 1)) - 1].strip()) ar_props = str_keyvals.split(':') if len(ar_props) > 1: key = (ar_props[0].replace(' ', '_')) value = ''.join(ar_props[1:]).strip() sess_state_prefix = 'SessionState' if value.startswith(sess_state_prefix): value = value[len(sess_state_prefix):] VALUE_MAP[key] = value # parse volume if line.endswith('#k'): value = line.strip() VALUE_MAP["volume"] = value.count("#") self.state = VALUE_MAP["state"] md = Metadata() md.playerName = "Tidal" md.artist = VALUE_MAP['artists'] md.title = VALUE_MAP['title'] md.albumTitle = VALUE_MAP['album_name'] md.duration = VALUE_MAP['duration'] md.artUrl = None md.externalArtUrl = None ''' self.artist = artist self.title = title self.albumArtist = albumArtist self.albumTitle = albumTitle self.artUrl = artUrl self.externalArtUrl = None self.discNumber = discNumber self.tracknumber = trackNumber self.playerName = playerName self.playerState = playerState self.streamUrl = streamUrl self.playCount = None self.mbid = None self.artistmbid = None self.albummbid = None self.loved = None self.wiki = None self.loveSupported = Metadata.loveSupportedDefault self.tags = [] self.skipped = False self.host_uuid = None self.releaseDate = None self.trackid = None self.hifiberry_cover_found=False self.duration=0 self.time=0 self.position=0 # poosition in seconds self.positionupdate=time() # last time position has been updated ''' self.meta = md
class MPRISController(): """ Controller for MPRIS enabled media players """ def __init__(self, auto_pause=True, loop_delay=1, ignore_players=[]): self.state_table = {} self.auto_pause = auto_pause self.metadata_displays = [] self.last_update = None self.loop_delay = loop_delay self.active_player = None self.ignore_players = ignore_players self.metadata = {} self.playing = False self.connect_dbus() self.metadata = Metadata() self.metadata_lock = threading.Lock() self.volume_control = None def register_metadata_display(self, mddisplay): self.metadata_displays.append(mddisplay) def set_volume_control(self, volume_control): self.volume_control = volume_control def metadata_notify(self, metadata): if metadata.is_unknown() and metadata.playerState == "playing": logging.error("Got empty metadata - what's wrong here? %s", metadata) for md in self.metadata_displays: try: logging.debug("metadata_notify: %s %s", md, metadata) md.notify_async(copy.copy(metadata)) except Exception as e: logging.warn("could not notify %s: %s", md, e) logging.exception(e) self.metadata = metadata def connect_dbus(self): self.bus = dbus.SystemBus() self.device_prop_interfaces = {} def dbus_get_device_prop_interface(self, name): proxy = self.bus.get_object(name, "/org/mpris/MediaPlayer2") device_prop = dbus.Interface(proxy, "org.freedesktop.DBus.Properties") return device_prop def retrievePlayers(self): """ Returns a list of all MPRIS enabled players that are active in the system """ return [ name for name in self.bus.list_names() if name.startswith("org.mpris") ] def retrieveState(self, name): """ Returns the playback state for the given player instance """ try: device_prop = self.dbus_get_device_prop_interface(name) state = device_prop.Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus") return state except Exception as e: logging.warn("got exception %s", e) def retrieveCommands(self, name): commands = { "pause": "CanPause", "next": "CanGoNext", "previous": "CanGoPrevious", "play": "CanPlay", "seek": "CanSeek" } try: supported_commands = ["stop"] # Stop must always be supported device_prop = self.dbus_get_device_prop_interface(name) for command in commands: supported = device_prop.Get("org.mpris.MediaPlayer2.Player", commands[command]) if supported: supported_commands.append(command) except Exception as e: logging.warn("got exception %s", e) return supported_commands def retrieveMeta(self, name): """ Return the metadata for the given player instance """ try: device_prop = self.dbus_get_device_prop_interface(name) prop = device_prop.Get("org.mpris.MediaPlayer2.Player", "Metadata") try: artist = array_to_string(prop.get("xesam:artist")) except: artist = None try: title = prop.get("xesam:title") except: title = None try: albumArtist = array_to_string(prop.get("xesam:albumArtist")) except: albumArtist = None try: albumTitle = prop.get("xesam:album") except: albumTitle = None try: artURL = prop.get("mpris:artUrl") except: artURL = None try: discNumber = prop.get("xesam:discNumber") except: discNumber = None try: trackNumber = prop.get("xesam:trackNumber") except: trackNumber = None md = Metadata(artist, title, albumArtist, albumTitle, artURL, discNumber, trackNumber) try: md.streamUrl = prop.get("xesam:url") except: pass try: md.trackId = prop.get("mpris:trackid") except: pass md.playerName = self.playername(name) md.fix_problems() return md except dbus.exceptions.DBusException as e: if "ServiceUnknown" in e.__class__.__name__: # unfortunately we can't do anything about this and # logging doesn't help, therefore just ignoring this case pass # Â logging.warning("service %s disappered, cleaning up", e) else: logging.warning("no mpris data received %s", e.__class__.__name__) md = Metadata() md.playerName = self.playername(name) return md def mpris_command(self, playername, command): try: if command in mpris_commands: proxy = self.bus.get_object(playername, "/org/mpris/MediaPlayer2") player = dbus.Interface( proxy, dbus_interface='org.mpris.MediaPlayer2.Player') run_command = getattr(player, command, lambda: "Unknown command") return run_command() else: logging.error("MPRIS command %s not supported", command) except Exception as e: logging.error("exception %s while sending MPRIS command %s to %s", e, command, playername) return False def pause_inactive(self, active_player): """ Automatically pause other player if playback was started on a new player """ for p in self.state_table: if (p != active_player) and \ (self.state_table[p].state == PLAYING): logging.info("Pausing " + self.playername(p)) self.mpris_command(p, MPRIS_PAUSE) def pause_all(self): for player in self.state_table: self.mpris_command(player, MPRIS_PAUSE) def print_players(self): for p in self.state_table: print(self.playername(p)) def playername(self, mprisname): if mprisname is None: return if (mprisname.startswith(MPRIS_PREFIX)): return mprisname[len(MPRIS_PREFIX):] else: return mprisname def send_command(self, command, playerName=None): res = None if playerName is None: if self.active_player is None: logging.info("No active player, ignoring %s", command) return else: playerName = self.active_player if playerName.startswith(MPRIS_PREFIX): res = self.mpris_command(playerName, command) else: res = self.mpris_command(MPRIS_PREFIX + playerName, command) logging.info("sent %s to %s", command, playerName) return res def activate_player(self, playername): command = MPRIS_PLAY if playername.startswith(MPRIS_PREFIX): res = self.mpris_command(playername, command) else: res = self.mpris_command(MPRIS_PREFIX + playername, command) return res def update_metadata_attributes(self, updates, songId): logging.debug("received metadata update: %s", updates) if self.metadata is None: logging.warn("ooops, got an update, but don't have metadata") return if self.metadata.songId() != songId: logging.debug("received update for previous song, ignoring") return # TODO: Check if this is the same song! # Otherwise it might be a delayed update with self.metadata_lock: for attribute in updates: self.metadata.__dict__[attribute] = updates[attribute] self.metadata_notify(self.metadata) def main_loop(self): """ Main loop: - monitors state of all players - pauses players if a new player starts playback """ finished = False md = Metadata() active_players = [] MAX_FAIL = 3 # Workaround for spotifyd problems spotify_stopped = 0 # Workaround for squeezelite mute squeezelite_active = 0 previous_state = "" ts = datetime.datetime.now() while not (finished): additional_delay = 0 new_player_started = None metadata_notified = False playing = False new_song = False state = "unknown" last_ts = ts ts = datetime.datetime.now() duration = (ts - last_ts).total_seconds() for p in self.retrievePlayers(): if self.playername(p) in self.ignore_players: continue if p not in self.state_table: ps = PlayerState() ps.supported_commands = self.retrieveCommands(p) logging.debug("Player %s supports %s", p, ps.supported_commands) self.state_table[p] = ps thisplayer_state = "unknown" try: thisplayer_state = self.retrieveState(p).lower() self.state_table[p].failed = 0 except: logging.info("Got no state from " + p) state = "unknown" self.state_table[p].failed = \ self.state_table[p].failed + 1 if self.state_table[p].failed >= MAX_FAIL: playername = self.playername(p) logging.warning("%s failed, trying to restart", playername) watchdog.restart_service(playername) self.state_table[p].failed = 0 self.state_table[p].state = thisplayer_state # Check if playback started on a player that wasn't # playing before if thisplayer_state == PLAYING: playing = True state = "playing" if self.playername(p) == SPOTIFY_NAME: spotify_stopped = 0 if self.playername(p) == LMS_NAME: squeezelite_active = 2 report_usage( "audiocontrol_playing_{}".format(self.playername(p)), duration) md = self.retrieveMeta(p) if (p not in active_players): new_player_started = p active_players.insert(0, p) md.playerState = thisplayer_state # MPRIS delivers only very few metadata, these will be # enriched with external sources if (md.sameSong(self.metadata)): md.fill_undefined(self.metadata) else: new_song = True self.state_table[p].metadata = md if not (md.sameSong(self.metadata)): logging.debug("updated metadata: \nold %s\nnew %s", self.metadata, md) # Store this as "current" with self.metadata_lock: self.metadata = md self.metadata_notify(md) logging.debug("notifications about new metadata sent") elif state != previous_state: logging.debug("changed state to playing") self.metadata_notify(md) # Add metadata if this is a new song if new_song: enrich_metadata_bg(md, callback=self) logging.debug("metadata updater thread started") # Even if we din't send metadata, this is still # flagged metadata_notified = True else: # always keep one player in the active_players # list if len(active_players) > 1: if p in active_players: active_players.remove(p) # update metadata for stopped players from time to time i = randint(0, 600) if (i == 0): md = self.retrieveMeta(p) md.playerState = thisplayer_state self.state_table[p].metadata = md self.playing = playing # Find active (or last paused) player if len(active_players) > 0: self.active_player = active_players[0] else: self.active_player = None # Workaround for wrong state messages by Spotify # Assume Spotify is still playing for 10 seconds if it's the # active (or last stopped) player if self.playername(self.active_player) == SPOTIFY_NAME: # Less aggressive metadata polling on Spotify as each polling will # result in an API request additional_delay = 4 if not (playing): spotify_stopped += 1 + additional_delay if spotify_stopped < 26: if (spotify_stopped % 5) == 0: logging.debug("spotify workaround %s", spotify_stopped) playing = True # Workaround for LMS muting the output after stopping the # player if self.volume_control is not None: if self.playername(self.active_player) != LMS_NAME: if squeezelite_active > 0: squeezelite_active = squeezelite_active - 1 logging.debug( "squeezelite was active before, unmuting") self.volume_control.set_mute(False) if not (playing) and squeezelite_active > 0: squeezelite_active = squeezelite_active - 1 logging.debug("squeezelite was active before, unmuting") self.volume_control.set_mute(False) # There might be no active player, but one that is paused # or stopped if not (playing) and len(active_players) > 0: p = active_players[0] md = self.retrieveMeta(p) md.playerState = self.state_table[p].state state = md.playerState if state != previous_state: logging.debug("state transition %s -> %s", previous_state, state) if not metadata_notified: self.metadata_notify(md) previous_state = state if new_player_started is not None: if self.auto_pause: logging.info( "new player %s started, pausing other active players", self.playername(active_players[0])) self.pause_inactive(new_player_started) else: logging.debug("auto-pause disabled") self.last_update = datetime.datetime.now() time.sleep(self.loop_delay + additional_delay) # ## # ## controller functions # ## def previous(self): self.send_command(MPRIS_PREV) def next(self): self.send_command(MPRIS_NEXT) def playpause(self, pause=None): command = None if pause is None: if self.playing: command = MPRIS_PAUSE else: command = MPRIS_PLAY elif pause: command = MPRIS_PAUSE else: command = MPRIS_PLAY self.send_command(command) def stop(self): self.send_command(MPRIS_STOP) # ## # ## end controller functions # ## def __str__(self): return "mpris" def states(self): players = [] for p in self.state_table: player = {} player["name"] = self.playername(p) player["state"] = self.state_table[p].state player["artist"] = self.state_table[p].metadata.artist player["title"] = self.state_table[p].metadata.title player["supported_commands"] = self.state_table[ p].supported_commands players.append(player) return {"players": players, "last_updated": str(self.last_update)}
def test_song_id(self): md1=Metadata("artist1","song1",albumTitle="abum1") md2=Metadata("artist1","song1",albumTitle="abum2") md3=Metadata("artist2","song1") md4=Metadata("artist2","song1",albumTitle="abum1") self.assertEqual(md1.songId(),md2.songId()) self.assertEqual(md3.songId(),md4.songId()) self.assertNotEqual(md1.songId(),md3.songId()) self.assertNotEqual(md2.songId(),md3.songId()) self.assertNotEqual(md1.songId(),md4.songId())
class AudioController(): """ Controller for MPRIS and non-MPRIS media players """ def __init__(self, auto_pause=True, loop_delay=1, ignore_players=[]): self.state_table = {} self.auto_pause = auto_pause self.metadata_displays = [] self.last_update = None self.loop_delay = loop_delay self.active_player = None self.ignore_players = ignore_players self.metadata = {} self.playing = False self.metadata = Metadata() self.metadata_lock = threading.Lock() self.volume_control = None self.metadata_processors = [] self.state_displays = [] self.players = {} self.mpris = MPRIS() self.mpris.connect_dbus() """ Register a non-mpris player controls """ def register_nonmpris_player(self, name, controller): self.players[name] = controller def register_metadata_display(self, mddisplay): self.metadata_displays.append(mddisplay) def register_state_display(self, statedisplay): self.state_displays.append(statedisplay) def register_metadata_processor(self, mdproc): self.metadata_processors.append(mdproc) def set_volume_control(self, volume_control): self.volume_control = volume_control def metadata_notify(self, metadata): if metadata.is_unknown() and metadata.playerState == "playing": logging.warning( "Metadata without artist, album or title - what's wrong here? %s", metadata) for md in self.metadata_displays: try: logging.debug("metadata_notify: %s %s", md, metadata) md.notify_async(copy.copy(metadata)) except Exception as e: logging.warning("could not notify %s: %s", md, e) logging.exception(e) self.metadata = metadata def all_players(self): """ Returns a list of MPRIS and non-MPRIS players """ players = list(self.players.keys()) + self.mpris.retrieve_players() logging.debug("players: %s", players) return players def get_player_state(self, name): """ Returns the playback state for the given player instance It can handle both MPRIS and non-MPRIS players """ if name in self.players.keys(): return self.players[name].get_state() else: return self.mpris.retrieve_state(name) def get_supported_commands(self, name): if name in self.players.keys(): return self.players[name].get_supported_commands() else: return self.mpris.get_supported_commands(name) def send_command_to_player(self, name, command): if name in self.players.keys(): self.players[name].send_command(command) else: self.mpris.send_command(name, command) def pause_inactive(self, active_player): """ Automatically pause other player if playback was started on a new player """ for p in self.state_table: if (p != active_player) and \ (self.state_table[p].state == STATE_PLAYING): logging.info("Pausing " + self.playername(p)) self.send_command(p, CMD_PAUSE) def pause_all(self): for player in self.state_table: self.send_command(player, CMD_PAUSE) def print_players(self): for p in self.state_table: print(self.playername(p)) def playername(self, name): if name is None: return if (name.startswith(MPRIS_PREFIX)): return name[len(MPRIS_PREFIX):] else: return name def send_command(self, command, playerName=None): if playerName is None: if self.active_player is None: logging.info("No active player, ignoring %s", command) return else: playerName = self.active_player res = self.send_command_to_player(playerName, command) logging.info("sent %s to %s", command, playerName) return res def activate_player(self, playername): command = CMD_PLAY if playername.startswith(MPRIS_PREFIX): res = self.send_command_to_player(playername, command) else: res = self.mpris_command(MPRIS_PREFIX + playername, command) return res def get_meta(self, name): if name in self.players.keys(): md = self.players[name].get_meta() else: md = self.mpris.get_meta(name) if md is None: return None md.fix_problems() for p in self.metadata_processors: p.process_metadata(md) return md def update_metadata_attributes(self, updates, songId): logging.debug("received metadata update: %s", updates) if self.metadata is None: logging.warning("ooops, got an update, but don't have metadata") return if self.metadata.songId() != songId: logging.debug("received update for previous song, ignoring") return # TODO: Check if this is the same song! # Otherwise it might be a delayed update with self.metadata_lock: for attribute in updates: self.metadata.__dict__[attribute] = updates[attribute] self.metadata_notify(self.metadata) def main_loop(self): """ Main loop: - monitors state of all players - pauses players if a new player starts playback """ finished = False md = Metadata() active_players = [] MAX_FAIL = 3 # Workaround for spotifyd problems # spotify_stopped = 0 # Workaround for squeezelite mute squeezelite_active = 0 previous_state = "" ts = datetime.datetime.now() while not (finished): additional_delay = 0 new_player_started = None metadata_notified = False playing = False new_song = False state = "unknown" last_ts = ts ts = datetime.datetime.now() duration = (ts - last_ts).total_seconds() for p in self.all_players(): if self.playername(p) in self.ignore_players: continue if p not in self.state_table: ps = PlayerState() ps.supported_commands = self.get_supported_commands(p) logging.debug("Player %s supports %s", p, ps.supported_commands) self.state_table[p] = ps thisplayer_state = "unknown" try: thisplayer_state = self.get_player_state(p).lower() self.state_table[p].failed = 0 except: logging.info("Got no state from " + p) state = "unknown" self.state_table[p].failed = \ self.state_table[p].failed + 1 if self.state_table[p].failed >= MAX_FAIL: playername = self.playername(p) logging.warning("%s failed, trying to restart", playername) watchdog.restart_service(playername) self.state_table[p].failed = 0 self.state_table[p].state = thisplayer_state # Check if playback started on a player that wasn't # playing before if thisplayer_state == STATE_PLAYING: playing = True state = "playing" # if self.playername(p) == SPOTIFY_NAME: # spotify_stopped = 0 if self.playername(p) == LMS_NAME: squeezelite_active = 2 report_usage( "audiocontrol_playing_{}".format(self.playername(p)), duration) md = self.get_meta(p) if (p not in active_players): new_player_started = p active_players.insert(0, p) md.playerState = thisplayer_state # MPRIS delivers only very few metadata, these will be # enriched with external sources if (md.sameSong(self.metadata)): md.fill_undefined(self.metadata) else: new_song = True self.state_table[p].metadata = md if not (md.sameSong(self.metadata)): logging.debug("updated metadata: \nold %s\nnew %s", self.metadata, md) # Store this as "current" with self.metadata_lock: self.metadata = md self.metadata_notify(md) logging.debug("notifications about new metadata sent") elif state != previous_state: logging.debug("changed state to playing") self.metadata_notify(md) # Some players deliver artwork after initial metadata if md.artUrl != self.metadata.artUrl: logging.debug("artwork changes from %s to %s", self.metadata.artUrl, md.artUrl) self.metadata_notify(md) # Add metadata if this is a new song if new_song: enrich_metadata_bg(md, callback=self) logging.debug("metadata updater thread started") # Even if we din't send metadata, this is still # flagged metadata_notified = True else: # always keep one player in the active_players # list if len(active_players) > 1: if p in active_players: active_players.remove(p) # update metadata for stopped players from time to time i = randint(0, 600) if (i == 0): md = self.get_meta(p) md.playerState = thisplayer_state self.state_table[p].metadata = md self.playing = playing # Find active (or last paused) player if len(active_players) > 0: self.active_player = active_players[0] else: self.active_player = None # # Workaround for wrong state messages by Spotify # # Assume Spotify is still playing for 10 seconds if it's the # # active (or last stopped) player # if self.playername(self.active_player) == SPOTIFY_NAME: # # Less aggressive metadata polling on Spotify as each polling will # # result in an API request # additional_delay = 4 # if not(playing): # spotify_stopped += 1 + additional_delay # if spotify_stopped < 26: # if (spotify_stopped % 5) == 0: # logging.debug("spotify workaround %s", spotify_stopped) # playing = True # # Workaround for LMS muting the output after stopping the # player if self.volume_control is not None: if self.playername(self.active_player) != LMS_NAME: if squeezelite_active > 0: squeezelite_active = squeezelite_active - 1 logging.debug( "squeezelite was active before, unmuting") self.volume_control.set_mute(False) if not (playing) and squeezelite_active > 0: squeezelite_active = squeezelite_active - 1 logging.debug("squeezelite was active before, unmuting") self.volume_control.set_mute(False) # There might be no active player, but one that is paused # or stopped if not (playing) and len(active_players) > 0: p = active_players[0] md = self.get_meta(p) md.playerState = self.state_table[p].state state = md.playerState if state != previous_state: logging.debug("state transition %s -> %s", previous_state, state) if not metadata_notified: self.metadata_notify(md) for sd in self.state_displays: sd.update_playback_state(state) previous_state = state if new_player_started is not None: if self.auto_pause: logging.info( "new player %s started, pausing other active players", self.playername(active_players[0])) self.pause_inactive(new_player_started) else: logging.debug("auto-pause disabled") self.last_update = datetime.datetime.now() time.sleep(self.loop_delay + additional_delay) # ## # ## controller functions # ## def previous(self): self.send_command(CMD_PREV) def next(self): self.send_command(CMD_NEXT) def playpause(self, pause=None, ignore=None): if ignore is not None: if self.active_player.lower() == ignore.lower(): logging.info( "Got a playpquse request that should be ignored (%s)", ignore) return command = None if pause is None: if self.playing: command = CMD_PAUSE else: command = CMD_PLAY elif pause: command = CMD_PAUSE else: command = CMD_PLAY self.send_command(command) def stop(self): self.send_command(CMD_STOP) # ## # ## end controller functions # ## def __str__(self): return "mpris" def states(self): players = [] for p in self.state_table: player = {} player["name"] = self.playername(p) player["state"] = self.state_table[p].state player["artist"] = self.state_table[p].metadata.artist player["title"] = self.state_table[p].metadata.title player["supported_commands"] = self.state_table[ p].supported_commands players.append(player) return {"players": players, "last_updated": str(self.last_update)}