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.players = {} self.mpris = MPRIS() self.mpris.connect_dbus()
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)}