mopidy / mopidy
1 2
import collections
2 2
import itertools
3 2
import logging
4

5 2
import pykka
6

7 2
import mopidy
8 2
from mopidy import audio, backend, mixer
9 2
from mopidy.audio import PlaybackState
10 2
from mopidy.core.history import HistoryController
11 2
from mopidy.core.library import LibraryController
12 2
from mopidy.core.listener import CoreListener
13 2
from mopidy.core.mixer import MixerController
14 2
from mopidy.core.playback import PlaybackController
15 2
from mopidy.core.playlists import PlaylistsController
16 2
from mopidy.core.tracklist import TracklistController
17 2
from mopidy.internal import path, storage, validation, versioning
18 2
from mopidy.internal.models import CoreState
19

20 2
logger = logging.getLogger(__name__)
21

22

23 2
class Core(
24
    pykka.ThreadingActor,
25
    audio.AudioListener,
26
    backend.BackendListener,
27
    mixer.MixerListener,
28
):
29

30 2
    library = None
31
    """An instance of :class:`~mopidy.core.LibraryController`"""
32

33 2
    history = None
34
    """An instance of :class:`~mopidy.core.HistoryController`"""
35

36 2
    mixer = None
37
    """An instance of :class:`~mopidy.core.MixerController`"""
38

39 2
    playback = None
40
    """An instance of :class:`~mopidy.core.PlaybackController`"""
41

42 2
    playlists = None
43
    """An instance of :class:`~mopidy.core.PlaylistsController`"""
44

45 2
    tracklist = None
46
    """An instance of :class:`~mopidy.core.TracklistController`"""
47

48 2
    def __init__(self, config=None, mixer=None, backends=None, audio=None):
49 2
        super().__init__()
50

51 2
        self._config = config
52

53 2
        self.backends = Backends(backends)
54

55 2
        self.library = pykka.traversable(
56
            LibraryController(backends=self.backends, core=self)
57
        )
58 2
        self.history = pykka.traversable(HistoryController())
59 2
        self.mixer = pykka.traversable(MixerController(mixer=mixer))
60 2
        self.playback = pykka.traversable(
61
            PlaybackController(audio=audio, backends=self.backends, core=self)
62
        )
63 2
        self.playlists = pykka.traversable(
64
            PlaylistsController(backends=self.backends, core=self)
65
        )
66 2
        self.tracklist = pykka.traversable(TracklistController(core=self))
67

68 2
        self.audio = audio
69

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

77 2
    def get_version(self):
78
        """Get version of the Mopidy core API"""
79 2
        return versioning.get_version()
80

81 2
    def reached_end_of_stream(self):
82 2
        self.playback._on_end_of_stream()
83

84 2
    def stream_changed(self, uri):
85 2
        self.playback._on_stream_changed(uri)
86

87 2
    def position_changed(self, position):
88 2
        self.playback._on_position_changed(position)
89

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

98
        # We ignore cases when target state is set as this is buffering
99
        # updates (at least for now) and we need to get #234 fixed...
100 2
        if (
101
            new_state == PlaybackState.PAUSED
102
            and not target_state
103
            and self.playback.get_state() != PlaybackState.PAUSED
104
        ):
105 2
            self.playback.set_state(new_state)
106 2
            self.playback._trigger_track_playback_paused()
107

108 2
    def playlists_loaded(self):
109
        # Forward event from backend to frontends
110 2
        CoreListener.send("playlists_loaded")
111

112 2
    def volume_changed(self, volume):
113
        # Forward event from mixer to frontends
114 2
        CoreListener.send("volume_changed", volume=volume)
115

116 2
    def mute_changed(self, mute):
117
        # Forward event from mixer to frontends
118 2
        CoreListener.send("mute_changed", mute=mute)
119

120 2
    def tags_changed(self, tags):
121 2
        if not self.audio or "title" not in tags:
122 2
            return
123

124 2
        tags = self.audio.get_current_tags().get()
125 2
        if not tags:
126 0
            return
127

128 2
        self.playback._stream_title = None
129
        # TODO: Do not emit stream title changes for plain tracks. We need a
130
        # better way to decide if something is a stream.
131 2
        if "title" in tags and tags["title"]:
132 2
            title = tags["title"][0]
133 2
            current_track = self.playback.get_current_track()
134 2
            if current_track is not None and current_track.name != title:
135 2
                self.playback._stream_title = title
136 2
                CoreListener.send("stream_title_changed", title=title)
137

138 2
    def _setup(self):
139
        """Do not call this function. It is for internal use at startup."""
140 2
        try:
141 2
            coverage = []
142 2
            if self._config and "restore_state" in self._config["core"]:
143 2
                if self._config["core"]["restore_state"]:
144 2
                    coverage = [
145
                        "tracklist",
146
                        "mode",
147
                        "play-last",
148
                        "mixer",
149
                        "history",
150
                    ]
151 2
            if len(coverage):
152 2
                self._load_state(coverage)
153 0
        except Exception as e:
154 0
            logger.warn("Restore state: Unexpected error: %s", str(e))
155

156 2
    def _teardown(self):
157
        """Do not call this function. It is for internal use at shutdown."""
158 2
        try:
159 2
            if self._config and "restore_state" in self._config["core"]:
160 2
                if self._config["core"]["restore_state"]:
161 2
                    self._save_state()
162 0
        except Exception as e:
163 0
            logger.warn("Unexpected error while saving state: %s", str(e))
164

165 2
    def _get_data_dir(self):
166
        # get or create data director for core
167 2
        data_dir_path = (
168
            path.expand_path(self._config["core"]["data_dir"]) / "core"
169
        )
170 2
        path.get_or_create_dir(data_dir_path)
171 2
        return data_dir_path
172

173 2
    def _get_state_file(self):
174 2
        return self._get_data_dir() / "state.json.gz"
175

176 2
    def _save_state(self):
177
        """
178
        Save current state to disk.
179
        """
180

181 2
        state_file = self._get_state_file()
182 2
        logger.info("Saving state to %s", state_file)
183

184 2
        data = {}
185 2
        data["version"] = mopidy.__version__
186 2
        data["state"] = CoreState(
187
            tracklist=self.tracklist._save_state(),
188
            history=self.history._save_state(),
189
            playback=self.playback._save_state(),
190
            mixer=self.mixer._save_state(),
191
        )
192 2
        storage.dump(state_file, data)
193 2
        logger.debug("Saving state done")
194

195 2
    def _load_state(self, coverage):
196
        """
197
        Restore state from disk.
198

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

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

209
        :param coverage: amount of data to restore
210
        :type coverage: list of strings
211
        """
212

213 2
        state_file = self._get_state_file()
214 2
        logger.info("Loading state from %s", state_file)
215

216 2
        data = storage.load(state_file)
217

218 2
        try:
219
            # Try only once. If something goes wrong, the next start is clean.
220 2
            state_file.unlink()
221 2
        except OSError:
222 2
            logger.info("Failed to delete %s", state_file)
223

224 2
        if "state" in data:
225 2
            core_state = data["state"]
226 2
            validation.check_instance(core_state, CoreState)
227 2
            self.history._load_state(core_state.history, coverage)
228 2
            self.tracklist._load_state(core_state.tracklist, coverage)
229 2
            self.mixer._load_state(core_state.mixer, coverage)
230
            # playback after tracklist
231 2
            self.playback._load_state(core_state.playback, coverage)
232 2
        logger.debug("Loading state done")
233

234

235 2
class Backends(list):
236 2
    def __init__(self, backends):
237 2
        super().__init__(backends)
238

239 2
        self.with_library = collections.OrderedDict()
240 2
        self.with_library_browse = collections.OrderedDict()
241 2
        self.with_playback = collections.OrderedDict()
242 2
        self.with_playlists = collections.OrderedDict()
243

244 2
        backends_by_scheme = {}
245

246 2
        def name(b):
247 2
            return b.actor_ref.actor_class.__name__
248

249 2
        for b in backends:
250 2
            try:
251 2
                has_library = b.has_library().get()
252 2
                has_library_browse = b.has_library_browse().get()
253 2
                has_playback = b.has_playback().get()
254 2
                has_playlists = b.has_playlists().get()
255 0
            except Exception:
256 0
                self.remove(b)
257 0
                logger.exception(
258
                    "Fetching backend info for %s failed",
259
                    b.actor_ref.actor_class.__name__,
260
                )
261

262 2
            for scheme in b.uri_schemes.get():
263 2
                if scheme in backends_by_scheme:
264 2
                    raise AssertionError(
265
                        f"Cannot add URI scheme {scheme!r} for {name(b)}, "
266
                        f"it is already handled by {name(backends_by_scheme[scheme])}"
267
                    )
268 2
                backends_by_scheme[scheme] = b
269

270 2
                if has_library:
271 2
                    self.with_library[scheme] = b
272 2
                if has_library_browse:
273 2
                    self.with_library_browse[scheme] = b
274 2
                if has_playback:
275 2
                    self.with_playback[scheme] = b
276 2
                if has_playlists:
277 2
                    self.with_playlists[scheme] = b

Read our documentation on viewing source code .

Loading