Ejemplo n.º 1
0
    def __init__(self, mixer=None, backends=None, audio=None):
        super(Core, self).__init__()

        self.backends = Backends(backends)

        self.library = LibraryController(backends=self.backends, core=self)
        self.history = HistoryController()
        self.mixer = MixerController(mixer=mixer)
        self.playback = PlaybackController(backends=self.backends, core=self)
        self.playlists = PlaylistsController(backends=self.backends, core=self)
        self.tracklist = TracklistController(core=self)

        self.audio = audio
Ejemplo n.º 2
0
    def __init__(self, mixer=None, backends=None, audio=None):
        super(Core, self).__init__()

        self.backends = Backends(backends)

        self.library = LibraryController(backends=self.backends, core=self)
        self.history = HistoryController()
        self.mixer = MixerController(mixer=mixer)
        self.playback = PlaybackController(backends=self.backends, core=self)
        self.playlists = PlaylistsController(backends=self.backends, core=self)
        self.tracklist = TracklistController(core=self)

        self.audio = audio
Ejemplo n.º 3
0
    def __init__(self, config=None, mixer=None, backends=None, audio=None):
        super().__init__()

        self._config = config

        self.backends = Backends(backends)

        self.library = pykka.traversable(
            LibraryController(backends=self.backends, core=self))
        self.history = pykka.traversable(HistoryController())
        self.mixer = pykka.traversable(MixerController(mixer=mixer))
        self.playback = pykka.traversable(
            PlaybackController(audio=audio, backends=self.backends, core=self))
        self.playlists = pykka.traversable(
            PlaylistsController(backends=self.backends, core=self))
        self.tracklist = pykka.traversable(TracklistController(core=self))

        self.audio = audio
Ejemplo n.º 4
0
class Core(pykka.ThreadingActor, audio.AudioListener, backend.BackendListener,
           mixer.MixerListener):

    library = None
    """The library controller. An instance of
    :class:`mopidy.core.LibraryController`."""

    history = None
    """The playback history controller. An instance of
    :class:`mopidy.core.HistoryController`."""

    mixer = None
    """The mixer controller. An instance of
    :class:`mopidy.core.MixerController`."""

    playback = None
    """The playback controller. An instance of
    :class:`mopidy.core.PlaybackController`."""

    playlists = None
    """The playlists controller. An instance of
    :class:`mopidy.core.PlaylistsController`."""

    tracklist = None
    """The tracklist controller. An instance of
    :class:`mopidy.core.TracklistController`."""
    def __init__(self, mixer=None, backends=None, audio=None):
        super(Core, self).__init__()

        self.backends = Backends(backends)

        self.library = LibraryController(backends=self.backends, core=self)
        self.history = HistoryController()
        self.mixer = MixerController(mixer=mixer)
        self.playback = PlaybackController(backends=self.backends, core=self)
        self.playlists = PlaylistsController(backends=self.backends, core=self)
        self.tracklist = TracklistController(core=self)

        self.audio = audio

    def get_uri_schemes(self):
        """Get list of URI schemes we can handle"""
        futures = [b.uri_schemes for b in self.backends]
        results = pykka.get_all(futures)
        uri_schemes = itertools.chain(*results)
        return sorted(uri_schemes)

    uri_schemes = deprecated_property(get_uri_schemes)
    """
    .. deprecated:: 1.0
        Use :meth:`get_uri_schemes` instead.
    """

    def get_version(self):
        """Get version of the Mopidy core API"""
        return versioning.get_version()

    version = deprecated_property(get_version)
    """
    .. deprecated:: 1.0
        Use :meth:`get_version` instead.
    """

    def reached_end_of_stream(self):
        self.playback._on_end_of_track()

    def stream_changed(self, uri):
        self.playback._on_stream_changed(uri)

    def state_changed(self, old_state, new_state, target_state):
        # XXX: This is a temporary fix for issue #232 while we wait for a more
        # permanent solution with the implementation of issue #234. When the
        # Spotify play token is lost, the Spotify backend pauses audio
        # playback, but mopidy.core doesn't know this, so we need to update
        # mopidy.core's state to match the actual state in mopidy.audio. If we
        # don't do this, clients will think that we're still playing.

        # We ignore cases when target state is set as this is buffering
        # updates (at least for now) and we need to get #234 fixed...
        if (new_state == PlaybackState.PAUSED and not target_state
                and self.playback.state != PlaybackState.PAUSED):
            self.playback.state = new_state
            self.playback._trigger_track_playback_paused()

    def playlists_loaded(self):
        # Forward event from backend to frontends
        CoreListener.send('playlists_loaded')

    def volume_changed(self, volume):
        # Forward event from mixer to frontends
        CoreListener.send('volume_changed', volume=volume)

    def mute_changed(self, mute):
        # Forward event from mixer to frontends
        CoreListener.send('mute_changed', mute=mute)

    def tags_changed(self, tags):
        if not self.audio or 'title' not in tags:
            return

        tags = self.audio.get_current_tags().get()
        if not tags:
            return

        # TODO: this limits us to only streams that set organization, this is
        # a hack to make sure we don't emit stream title changes for plain
        # tracks. We need a better way to decide if something is a stream.
        if 'title' in tags and tags['title'] and 'organization' in tags:
            title = tags['title'][0]
            self.playback._stream_title = title
            CoreListener.send('stream_title_changed', title=title)
Ejemplo n.º 5
0
class Core(pykka.ThreadingActor, audio.AudioListener, backend.BackendListener,
           mixer.MixerListener):

    library = None
    """An instance of :class:`~mopidy.core.LibraryController`"""

    history = None
    """An instance of :class:`~mopidy.core.HistoryController`"""

    mixer = None
    """An instance of :class:`~mopidy.core.MixerController`"""

    playback = None
    """An instance of :class:`~mopidy.core.PlaybackController`"""

    playlists = None
    """An instance of :class:`~mopidy.core.PlaylistsController`"""

    tracklist = None
    """An instance of :class:`~mopidy.core.TracklistController`"""
    def __init__(self, config=None, mixer=None, backends=None, audio=None):
        super(Core, self).__init__()

        self._config = config

        self.backends = Backends(backends)

        self.library = LibraryController(backends=self.backends, core=self)
        self.history = HistoryController()
        self.mixer = MixerController(mixer=mixer)
        self.playback = PlaybackController(audio=audio,
                                           backends=self.backends,
                                           core=self)
        self.playlists = PlaylistsController(backends=self.backends, core=self)
        self.tracklist = TracklistController(core=self)

        self.audio = audio

    def get_uri_schemes(self):
        """Get list of URI schemes we can handle"""
        futures = [b.uri_schemes for b in self.backends]
        results = pykka.get_all(futures)
        uri_schemes = itertools.chain(*results)
        return sorted(uri_schemes)

    uri_schemes = deprecated_property(get_uri_schemes)
    """
    .. deprecated:: 1.0
        Use :meth:`get_uri_schemes` instead.
    """

    def get_version(self):
        """Get version of the Mopidy core API"""
        return versioning.get_version()

    version = deprecated_property(get_version)
    """
    .. deprecated:: 1.0
        Use :meth:`get_version` instead.
    """

    def reached_end_of_stream(self):
        self.playback._on_end_of_stream()

    def stream_changed(self, uri):
        self.playback._on_stream_changed(uri)

    def position_changed(self, position):
        self.playback._on_position_changed(position)

    def state_changed(self, old_state, new_state, target_state):
        # XXX: This is a temporary fix for issue #232 while we wait for a more
        # permanent solution with the implementation of issue #234. When the
        # Spotify play token is lost, the Spotify backend pauses audio
        # playback, but mopidy.core doesn't know this, so we need to update
        # mopidy.core's state to match the actual state in mopidy.audio. If we
        # don't do this, clients will think that we're still playing.

        # We ignore cases when target state is set as this is buffering
        # updates (at least for now) and we need to get #234 fixed...
        if (new_state == PlaybackState.PAUSED and not target_state
                and self.playback.state != PlaybackState.PAUSED):
            self.playback.state = new_state
            self.playback._trigger_track_playback_paused()

    def playlists_loaded(self):
        # Forward event from backend to frontends
        CoreListener.send('playlists_loaded')

    def volume_changed(self, volume):
        # Forward event from mixer to frontends
        CoreListener.send('volume_changed', volume=volume)

    def mute_changed(self, mute):
        # Forward event from mixer to frontends
        CoreListener.send('mute_changed', mute=mute)

    def tags_changed(self, tags):
        if not self.audio or 'title' not in tags:
            return

        tags = self.audio.get_current_tags().get()
        if not tags:
            return

        # TODO: this limits us to only streams that set organization, this is
        # a hack to make sure we don't emit stream title changes for plain
        # tracks. We need a better way to decide if something is a stream.
        if 'title' in tags and tags['title'] and 'organization' in tags:
            title = tags['title'][0]
            self.playback._stream_title = title
            CoreListener.send('stream_title_changed', title=title)

    def setup(self):
        """Do not call this function. It is for internal use at startup."""
        try:
            coverage = []
            if self._config and 'restore_state' in self._config['core']:
                if self._config['core']['restore_state']:
                    coverage = [
                        'tracklist', 'mode', 'play-last', 'mixer', 'history'
                    ]
            if len(coverage):
                self._load_state(coverage)
        except Exception as e:
            logger.warn('Restore state: Unexpected error: %s', str(e))

    def teardown(self):
        """Do not call this function. It is for internal use at shutdown."""
        try:
            if self._config and 'restore_state' in self._config['core']:
                if self._config['core']['restore_state']:
                    self._save_state()
        except Exception as e:
            logger.warn('Unexpected error while saving state: %s', str(e))

    def _get_data_dir(self):
        # get or create data director for core
        data_dir_path = os.path.join(self._config['core']['data_dir'], b'core')
        path.get_or_create_dir(data_dir_path)
        return data_dir_path

    def _save_state(self):
        """
        Save current state to disk.
        """

        file_name = os.path.join(self._get_data_dir(), b'state.json.gz')
        logger.info('Saving state to %s', file_name)

        data = {}
        data['version'] = mopidy.__version__
        data['state'] = CoreState(tracklist=self.tracklist._save_state(),
                                  history=self.history._save_state(),
                                  playback=self.playback._save_state(),
                                  mixer=self.mixer._save_state())
        storage.dump(file_name, data)
        logger.debug('Saving state done')

    def _load_state(self, coverage):
        """
        Restore state from disk.

        Load state from disk and restore it. Parameter ``coverage``
        limits the amount of data to restore. Possible
        values for ``coverage`` (list of one or more of):

            - 'tracklist' fill the tracklist
            - 'mode' set tracklist properties (consume, random, repeat, single)
            - 'play-last' restore play state ('tracklist' also required)
            - 'mixer' set mixer volume and mute state
            - 'history' restore history

        :param coverage: amount of data to restore
        :type coverage: list of strings
        """

        file_name = os.path.join(self._get_data_dir(), b'state.json.gz')
        logger.info('Loading state from %s', file_name)

        data = storage.load(file_name)

        try:
            # Try only once. If something goes wrong, the next start is clean.
            os.remove(file_name)
        except OSError:
            logger.info('Failed to delete %s', file_name)

        if 'state' in data:
            core_state = data['state']
            validation.check_instance(core_state, CoreState)
            self.history._load_state(core_state.history, coverage)
            self.tracklist._load_state(core_state.tracklist, coverage)
            self.mixer._load_state(core_state.mixer, coverage)
            # playback after tracklist
            self.playback._load_state(core_state.playback, coverage)
        logger.debug('Loading state done')
Ejemplo n.º 6
0
class Core(
        pykka.ThreadingActor, audio.AudioListener, backend.BackendListener,
        mixer.MixerListener):

    library = None
    """An instance of :class:`~mopidy.core.LibraryController`"""

    history = None
    """An instance of :class:`~mopidy.core.HistoryController`"""

    mixer = None
    """An instance of :class:`~mopidy.core.MixerController`"""

    playback = None
    """An instance of :class:`~mopidy.core.PlaybackController`"""

    playlists = None
    """An instance of :class:`~mopidy.core.PlaylistsController`"""

    tracklist = None
    """An instance of :class:`~mopidy.core.TracklistController`"""

    def __init__(self, config=None, mixer=None, backends=None, audio=None):
        super(Core, self).__init__()

        self._config = config

        self.backends = Backends(backends)

        self.library = LibraryController(backends=self.backends, core=self)
        self.history = HistoryController()
        self.mixer = MixerController(mixer=mixer)
        self.playback = PlaybackController(backends=self.backends, core=self)
        self.playlists = PlaylistsController(backends=self.backends, core=self)
        self.tracklist = TracklistController(core=self)

        self.audio = audio

    def get_uri_schemes(self):
        """Get list of URI schemes we can handle"""
        futures = [b.uri_schemes for b in self.backends]
        results = pykka.get_all(futures)
        uri_schemes = itertools.chain(*results)
        return sorted(uri_schemes)

    uri_schemes = deprecated_property(get_uri_schemes)
    """
    .. deprecated:: 1.0
        Use :meth:`get_uri_schemes` instead.
    """

    def get_version(self):
        """Get version of the Mopidy core API"""
        return versioning.get_version()

    version = deprecated_property(get_version)
    """
    .. deprecated:: 1.0
        Use :meth:`get_version` instead.
    """

    def reached_end_of_stream(self):
        self.playback._on_end_of_track()

    def stream_changed(self, uri):
        self.playback._on_stream_changed(uri)

    def state_changed(self, old_state, new_state, target_state):
        # XXX: This is a temporary fix for issue #232 while we wait for a more
        # permanent solution with the implementation of issue #234. When the
        # Spotify play token is lost, the Spotify backend pauses audio
        # playback, but mopidy.core doesn't know this, so we need to update
        # mopidy.core's state to match the actual state in mopidy.audio. If we
        # don't do this, clients will think that we're still playing.

        # We ignore cases when target state is set as this is buffering
        # updates (at least for now) and we need to get #234 fixed...
        if (new_state == PlaybackState.PAUSED and not target_state and
                self.playback.state != PlaybackState.PAUSED):
            self.playback.state = new_state
            self.playback._trigger_track_playback_paused()

    def playlists_loaded(self):
        # Forward event from backend to frontends
        CoreListener.send('playlists_loaded')

    def volume_changed(self, volume):
        # Forward event from mixer to frontends
        CoreListener.send('volume_changed', volume=volume)

    def mute_changed(self, mute):
        # Forward event from mixer to frontends
        CoreListener.send('mute_changed', mute=mute)

    def tags_changed(self, tags):
        if not self.audio or 'title' not in tags:
            return

        tags = self.audio.get_current_tags().get()
        if not tags:
            return

        # TODO: this limits us to only streams that set organization, this is
        # a hack to make sure we don't emit stream title changes for plain
        # tracks. We need a better way to decide if something is a stream.
        if 'title' in tags and tags['title'] and 'organization' in tags:
            title = tags['title'][0]
            self.playback._stream_title = title
            CoreListener.send('stream_title_changed', title=title)
Ejemplo n.º 7
0
class Core(pykka.ThreadingActor, audio.AudioListener, backend.BackendListener,
           mixer.MixerListener):

    library = None
    """The library controller. An instance of
    :class:`mopidy.core.LibraryController`."""

    playback = None
    """The playback controller. An instance of
    :class:`mopidy.core.PlaybackController`."""

    playlists = None
    """The playlists controller. An instance of
    :class:`mopidy.core.PlaylistsController`."""

    tracklist = None
    """The tracklist controller. An instance of
    :class:`mopidy.core.TracklistController`."""
    def __init__(self, mixer=None, backends=None):
        super(Core, self).__init__()

        self.backends = Backends(backends)

        self.library = LibraryController(backends=self.backends, core=self)

        self.playback = PlaybackController(mixer=mixer,
                                           backends=self.backends,
                                           core=self)

        self.playlists = PlaylistsController(backends=self.backends, core=self)

        self.tracklist = TracklistController(core=self)

    def get_uri_schemes(self):
        futures = [b.uri_schemes for b in self.backends]
        results = pykka.get_all(futures)
        uri_schemes = itertools.chain(*results)
        return sorted(uri_schemes)

    uri_schemes = property(get_uri_schemes)
    """List of URI schemes we can handle"""

    def get_version(self):
        return versioning.get_version()

    version = property(get_version)
    """Version of the Mopidy core API"""

    def reached_end_of_stream(self):
        self.playback.on_end_of_track()

    def state_changed(self, old_state, new_state, target_state):
        # XXX: This is a temporary fix for issue #232 while we wait for a more
        # permanent solution with the implementation of issue #234. When the
        # Spotify play token is lost, the Spotify backend pauses audio
        # playback, but mopidy.core doesn't know this, so we need to update
        # mopidy.core's state to match the actual state in mopidy.audio. If we
        # don't do this, clients will think that we're still playing.

        # We ignore cases when target state is set as this is buffering
        # updates (at least for now) and we need to get #234 fixed...
        if (new_state == PlaybackState.PAUSED and not target_state
                and self.playback.state != PlaybackState.PAUSED):
            self.playback.state = new_state
            self.playback._trigger_track_playback_paused()

    def playlists_loaded(self):
        # Forward event from backend to frontends
        CoreListener.send('playlists_loaded')

    def volume_changed(self, volume):
        # Forward event from mixer to frontends
        CoreListener.send('volume_changed', volume=volume)

    def mute_changed(self, mute):
        # Forward event from mixer to frontends
        CoreListener.send('mute_changed', mute=mute)
Ejemplo n.º 8
0
class Core(
        pykka.ThreadingActor, audio.AudioListener, backend.BackendListener,
        mixer.MixerListener):

    library = None
    """An instance of :class:`~mopidy.core.LibraryController`"""

    history = None
    """An instance of :class:`~mopidy.core.HistoryController`"""

    mixer = None
    """An instance of :class:`~mopidy.core.MixerController`"""

    playback = None
    """An instance of :class:`~mopidy.core.PlaybackController`"""

    playlists = None
    """An instance of :class:`~mopidy.core.PlaylistsController`"""

    tracklist = None
    """An instance of :class:`~mopidy.core.TracklistController`"""

    def __init__(self, config=None, mixer=None, backends=None, audio=None):
        super(Core, self).__init__()

        self._config = config

        self.backends = Backends(backends)

        self.library = LibraryController(backends=self.backends, core=self)
        self.history = HistoryController()
        self.mixer = MixerController(mixer=mixer)
        self.playback = PlaybackController(
            audio=audio, backends=self.backends, core=self)
        self.playlists = PlaylistsController(backends=self.backends, core=self)
        self.tracklist = TracklistController(core=self)

        self.audio = audio

    def get_uri_schemes(self):
        """Get list of URI schemes we can handle"""
        futures = [b.uri_schemes for b in self.backends]
        results = pykka.get_all(futures)
        uri_schemes = itertools.chain(*results)
        return sorted(uri_schemes)

    uri_schemes = deprecated_property(get_uri_schemes)
    """
    .. deprecated:: 1.0
        Use :meth:`get_uri_schemes` instead.
    """

    def get_version(self):
        """Get version of the Mopidy core API"""
        return versioning.get_version()

    version = deprecated_property(get_version)
    """
    .. deprecated:: 1.0
        Use :meth:`get_version` instead.
    """

    def reached_end_of_stream(self):
        self.playback._on_end_of_stream()

    def stream_changed(self, uri):
        self.playback._on_stream_changed(uri)

    def position_changed(self, position):
        self.playback._on_position_changed(position)

    def state_changed(self, old_state, new_state, target_state):
        # XXX: This is a temporary fix for issue #232 while we wait for a more
        # permanent solution with the implementation of issue #234. When the
        # Spotify play token is lost, the Spotify backend pauses audio
        # playback, but mopidy.core doesn't know this, so we need to update
        # mopidy.core's state to match the actual state in mopidy.audio. If we
        # don't do this, clients will think that we're still playing.

        # We ignore cases when target state is set as this is buffering
        # updates (at least for now) and we need to get #234 fixed...
        if (new_state == PlaybackState.PAUSED and not target_state and
                self.playback.state != PlaybackState.PAUSED):
            self.playback.state = new_state
            self.playback._trigger_track_playback_paused()

    def playlists_loaded(self):
        # Forward event from backend to frontends
        CoreListener.send('playlists_loaded')

    def volume_changed(self, volume):
        # Forward event from mixer to frontends
        CoreListener.send('volume_changed', volume=volume)

    def mute_changed(self, mute):
        # Forward event from mixer to frontends
        CoreListener.send('mute_changed', mute=mute)

    def tags_changed(self, tags):
        if not self.audio or 'title' not in tags:
            return

        tags = self.audio.get_current_tags().get()
        if not tags:
            return

        # TODO: this limits us to only streams that set organization, this is
        # a hack to make sure we don't emit stream title changes for plain
        # tracks. We need a better way to decide if something is a stream.
        if 'title' in tags and tags['title'] and 'organization' in tags:
            title = tags['title'][0]
            self.playback._stream_title = title
            CoreListener.send('stream_title_changed', title=title)

    def setup(self):
        """Do not call this function. It is for internal use at startup."""
        try:
            coverage = []
            if self._config and 'restore_state' in self._config['core']:
                if self._config['core']['restore_state']:
                    coverage = ['tracklist', 'mode', 'play-last', 'mixer',
                                'history']
            if len(coverage):
                self._load_state(coverage)
        except Exception as e:
            logger.warn('Restore state: Unexpected error: %s', str(e))

    def teardown(self):
        """Do not call this function. It is for internal use at shutdown."""
        try:
            if self._config and 'restore_state' in self._config['core']:
                if self._config['core']['restore_state']:
                    self._save_state()
        except Exception as e:
            logger.warn('Unexpected error while saving state: %s', str(e))

    def _get_data_dir(self):
        # get or create data director for core
        data_dir_path = os.path.join(self._config['core']['data_dir'], b'core')
        path.get_or_create_dir(data_dir_path)
        return data_dir_path

    def _save_state(self):
        """
        Save current state to disk.
        """

        file_name = os.path.join(self._get_data_dir(), b'state.json.gz')
        logger.info('Saving state to %s', file_name)

        data = {}
        data['version'] = mopidy.__version__
        data['state'] = CoreState(
            tracklist=self.tracklist._save_state(),
            history=self.history._save_state(),
            playback=self.playback._save_state(),
            mixer=self.mixer._save_state())
        storage.dump(file_name, data)
        logger.debug('Saving state done')

    def _load_state(self, coverage):
        """
        Restore state from disk.

        Load state from disk and restore it. Parameter ``coverage``
        limits the amount of data to restore. Possible
        values for ``coverage`` (list of one or more of):

            - 'tracklist' fill the tracklist
            - 'mode' set tracklist properties (consume, random, repeat, single)
            - 'play-last' restore play state ('tracklist' also required)
            - 'mixer' set mixer volume and mute state
            - 'history' restore history

        :param coverage: amount of data to restore
        :type coverage: list of strings
        """

        file_name = os.path.join(self._get_data_dir(), b'state.json.gz')
        logger.info('Loading state from %s', file_name)

        data = storage.load(file_name)

        try:
            # Try only once. If something goes wrong, the next start is clean.
            os.remove(file_name)
        except OSError:
            logger.info('Failed to delete %s', file_name)

        if 'state' in data:
            core_state = data['state']
            validation.check_instance(core_state, CoreState)
            self.history._load_state(core_state.history, coverage)
            self.tracklist._load_state(core_state.tracklist, coverage)
            self.mixer._load_state(core_state.mixer, coverage)
            # playback after tracklist
            self.playback._load_state(core_state.playback, coverage)
        logger.debug('Loading state done')
Ejemplo n.º 9
0
class Core(
        pykka.ThreadingActor, audio.AudioListener, backend.BackendListener,
        mixer.MixerListener):

    library = None
    """The library controller. An instance of
    :class:`mopidy.core.LibraryController`."""

    playback = None
    """The playback controller. An instance of
    :class:`mopidy.core.PlaybackController`."""

    playlists = None
    """The playlists controller. An instance of
    :class:`mopidy.core.PlaylistsController`."""

    tracklist = None
    """The tracklist controller. An instance of
    :class:`mopidy.core.TracklistController`."""

    def __init__(self, mixer=None, backends=None):
        super(Core, self).__init__()

        self.backends = Backends(backends)

        self.library = LibraryController(backends=self.backends, core=self)

        self.playback = PlaybackController(
            mixer=mixer, backends=self.backends, core=self)

        self.playlists = PlaylistsController(
            backends=self.backends, core=self)

        self.tracklist = TracklistController(core=self)

    def get_uri_schemes(self):
        futures = [b.uri_schemes for b in self.backends]
        results = pykka.get_all(futures)
        uri_schemes = itertools.chain(*results)
        return sorted(uri_schemes)

    uri_schemes = property(get_uri_schemes)
    """List of URI schemes we can handle"""

    def get_version(self):
        return versioning.get_version()

    version = property(get_version)
    """Version of the Mopidy core API"""

    def reached_end_of_stream(self):
        self.playback.on_end_of_track()

    def state_changed(self, old_state, new_state, target_state):
        # XXX: This is a temporary fix for issue #232 while we wait for a more
        # permanent solution with the implementation of issue #234. When the
        # Spotify play token is lost, the Spotify backend pauses audio
        # playback, but mopidy.core doesn't know this, so we need to update
        # mopidy.core's state to match the actual state in mopidy.audio. If we
        # don't do this, clients will think that we're still playing.

        # We ignore cases when target state is set as this is buffering
        # updates (at least for now) and we need to get #234 fixed...
        if (new_state == PlaybackState.PAUSED and not target_state
                and self.playback.state != PlaybackState.PAUSED):
            self.playback.state = new_state
            self.playback._trigger_track_playback_paused()

    def playlists_loaded(self):
        # Forward event from backend to frontends
        CoreListener.send('playlists_loaded')

    def volume_changed(self, volume):
        # Forward event from mixer to frontends
        CoreListener.send('volume_changed', volume=volume)

    def mute_changed(self, mute):
        # Forward event from mixer to frontends
        CoreListener.send('mute_changed', mute=mute)