mopidy / mopidy
1 2
import logging
2 2
import random
3

4 2
from mopidy import exceptions
5 2
from mopidy.core import listener
6 2
from mopidy.internal import deprecation, validation
7 2
from mopidy.internal.models import TracklistState
8 2
from mopidy.models import TlTrack, Track
9

10 2
logger = logging.getLogger(__name__)
11

12

13 2
class TracklistController:
14 2
    def __init__(self, core):
15 2
        self.core = core
16 2
        self._next_tlid = 1
17 2
        self._tl_tracks = []
18 2
        self._version = 0
19

20 2
        self._consume = False
21 2
        self._random = False
22 2
        self._shuffled = []
23 2
        self._repeat = False
24 2
        self._single = False
25

26 2
    def get_tl_tracks(self):
27
        """Get tracklist as list of :class:`mopidy.models.TlTrack`."""
28 2
        return self._tl_tracks[:]
29

30 2
    def get_tracks(self):
31
        """Get tracklist as list of :class:`mopidy.models.Track`."""
32 0
        return [tl_track.track for tl_track in self._tl_tracks]
33

34 2
    def get_length(self):
35
        """Get length of the tracklist."""
36 2
        return len(self._tl_tracks)
37

38 2
    def get_version(self):
39
        """
40
        Get the tracklist version.
41

42
        Integer which is increased every time the tracklist is changed. Is not
43
        reset before Mopidy is restarted.
44
        """
45 2
        return self._version
46

47 2
    def _increase_version(self):
48 2
        self._version += 1
49 2
        self.core.playback._on_tracklist_change()
50 2
        self._trigger_tracklist_changed()
51

52 2
    def get_consume(self):
53
        """Get consume mode.
54

55
        :class:`True`
56
            Tracks are removed from the tracklist when they have been played.
57
        :class:`False`
58
            Tracks are not removed from the tracklist.
59
        """
60 2
        return self._consume
61

62 2
    def set_consume(self, value):
63
        """Set consume mode.
64

65
        :class:`True`
66
            Tracks are removed from the tracklist when they have been played.
67
        :class:`False`
68
            Tracks are not removed from the tracklist.
69
        """
70 2
        validation.check_boolean(value)
71 2
        if self.get_consume() != value:
72 2
            self._trigger_options_changed()
73 2
        self._consume = value
74

75 2
    def get_random(self):
76
        """Get random mode.
77

78
        :class:`True`
79
            Tracks are selected at random from the tracklist.
80
        :class:`False`
81
            Tracks are played in the order of the tracklist.
82
        """
83 2
        return self._random
84

85 2
    def set_random(self, value):
86
        """Set random mode.
87

88
        :class:`True`
89
            Tracks are selected at random from the tracklist.
90
        :class:`False`
91
            Tracks are played in the order of the tracklist.
92
        """
93 2
        validation.check_boolean(value)
94 2
        if self.get_random() != value:
95 2
            self._trigger_options_changed()
96 2
        if value:
97 2
            self._shuffled = self.get_tl_tracks()
98 2
            random.shuffle(self._shuffled)
99 2
        self._random = value
100

101 2
    def get_repeat(self):
102
        """
103
        Get repeat mode.
104

105
        :class:`True`
106
            The tracklist is played repeatedly.
107
        :class:`False`
108
            The tracklist is played once.
109
        """
110 2
        return self._repeat
111

112 2
    def set_repeat(self, value):
113
        """
114
        Set repeat mode.
115

116
        To repeat a single track, set both ``repeat`` and ``single``.
117

118
        :class:`True`
119
            The tracklist is played repeatedly.
120
        :class:`False`
121
            The tracklist is played once.
122
        """
123 2
        validation.check_boolean(value)
124 2
        if self.get_repeat() != value:
125 2
            self._trigger_options_changed()
126 2
        self._repeat = value
127

128 2
    def get_single(self):
129
        """
130
        Get single mode.
131

132
        :class:`True`
133
            Playback is stopped after current song, unless in ``repeat`` mode.
134
        :class:`False`
135
            Playback continues after current song.
136
        """
137 2
        return self._single
138

139 2
    def set_single(self, value):
140
        """
141
        Set single mode.
142

143
        :class:`True`
144
            Playback is stopped after current song, unless in ``repeat`` mode.
145
        :class:`False`
146
            Playback continues after current song.
147
        """
148 2
        validation.check_boolean(value)
149 2
        if self.get_single() != value:
150 2
            self._trigger_options_changed()
151 2
        self._single = value
152

153 2
    def index(self, tl_track=None, tlid=None):
154
        """
155
        The position of the given track in the tracklist.
156

157
        If neither *tl_track* or *tlid* is given we return the index of
158
        the currently playing track.
159

160
        :param tl_track: the track to find the index of
161
        :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None`
162
        :param tlid: TLID of the track to find the index of
163
        :type tlid: :class:`int` or :class:`None`
164
        :rtype: :class:`int` or :class:`None`
165

166
        .. versionadded:: 1.1
167
            The *tlid* parameter
168
        """
169 2
        tl_track is None or validation.check_instance(tl_track, TlTrack)
170 2
        tlid is None or validation.check_integer(tlid, min=1)
171

172 2
        if tl_track is None and tlid is None:
173 2
            tl_track = self.core.playback.get_current_tl_track()
174

175 2
        if tl_track is not None:
176 2
            try:
177 2
                return self._tl_tracks.index(tl_track)
178 2
            except ValueError:
179 2
                pass
180 2
        elif tlid is not None:
181 2
            for i, tl_track in enumerate(self._tl_tracks):
182 2
                if tl_track.tlid == tlid:
183 2
                    return i
184 2
        return None
185

186 2
    def get_eot_tlid(self):
187
        """
188
        The TLID of the track that will be played after the current track.
189

190
        Not necessarily the same TLID as returned by :meth:`get_next_tlid`.
191

192
        :rtype: :class:`int` or :class:`None`
193

194
        .. versionadded:: 1.1
195
        """
196

197 0
        current_tl_track = self.core.playback.get_current_tl_track()
198

199 0
        with deprecation.ignore("core.tracklist.eot_track"):
200 0
            eot_tl_track = self.eot_track(current_tl_track)
201

202 0
        return getattr(eot_tl_track, "tlid", None)
203

204 2
    def eot_track(self, tl_track):
205
        """
206
        The track that will be played after the given track.
207

208
        Not necessarily the same track as :meth:`next_track`.
209

210
        .. deprecated:: 3.0
211
            Use :meth:`get_eot_tlid` instead.
212

213
        :param tl_track: the reference track
214
        :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None`
215
        :rtype: :class:`mopidy.models.TlTrack` or :class:`None`
216
        """
217 2
        deprecation.warn("core.tracklist.eot_track")
218 2
        tl_track is None or validation.check_instance(tl_track, TlTrack)
219 2
        if self.get_single() and self.get_repeat():
220 0
            return tl_track
221 2
        elif self.get_single():
222 0
            return None
223

224
        # Current difference between next and EOT handling is that EOT needs to
225
        # handle "single", with that out of the way the rest of the logic is
226
        # shared.
227 2
        return self.next_track(tl_track)
228

229 2
    def get_next_tlid(self):
230
        """
231
        The tlid of the track that will be played if calling
232
        :meth:`mopidy.core.PlaybackController.next()`.
233

234
        For normal playback this is the next track in the tracklist. If repeat
235
        is enabled the next track can loop around the tracklist. When random is
236
        enabled this should be a random track, all tracks should be played once
237
        before the tracklist repeats.
238

239
        :rtype: :class:`int` or :class:`None`
240

241
        .. versionadded:: 1.1
242
        """
243 0
        current_tl_track = self.core.playback.get_current_tl_track()
244

245 0
        with deprecation.ignore("core.tracklist.next_track"):
246 0
            next_tl_track = self.next_track(current_tl_track)
247

248 0
        return getattr(next_tl_track, "tlid", None)
249

250 2
    def next_track(self, tl_track):
251
        """
252
        The track that will be played if calling
253
        :meth:`mopidy.core.PlaybackController.next()`.
254

255
        For normal playback this is the next track in the tracklist. If repeat
256
        is enabled the next track can loop around the tracklist. When random is
257
        enabled this should be a random track, all tracks should be played once
258
        before the tracklist repeats.
259

260
        .. deprecated:: 3.0
261
            Use :meth:`get_next_tlid` instead.
262

263
        :param tl_track: the reference track
264
        :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None`
265
        :rtype: :class:`mopidy.models.TlTrack` or :class:`None`
266
        """
267 2
        deprecation.warn("core.tracklist.next_track")
268 2
        tl_track is None or validation.check_instance(tl_track, TlTrack)
269

270 2
        if not self._tl_tracks:
271 0
            return None
272

273 2
        if self.get_random() and not self._shuffled:
274 2
            if self.get_repeat() or not tl_track:
275 2
                logger.debug("Shuffling tracks")
276 2
                self._shuffled = self._tl_tracks[:]
277 2
                random.shuffle(self._shuffled)
278

279 2
        if self.get_random():
280 2
            if self._shuffled:
281 2
                return self._shuffled[0]
282 0
            return None
283

284 2
        next_index = self.index(tl_track)
285 2
        if next_index is None:
286 2
            next_index = 0
287
        else:
288 2
            next_index += 1
289

290 2
        if self.get_repeat():
291 2
            if self.get_consume() and len(self._tl_tracks) == 1:
292 2
                return None
293
            else:
294 2
                next_index %= len(self._tl_tracks)
295 2
        elif next_index >= len(self._tl_tracks):
296 2
            return None
297

298 2
        return self._tl_tracks[next_index]
299

300 2
    def get_previous_tlid(self):
301
        """
302
        Returns the TLID of the track that will be played if calling
303
        :meth:`mopidy.core.PlaybackController.previous()`.
304

305
        For normal playback this is the previous track in the tracklist. If
306
        random and/or consume is enabled it should return the current track
307
        instead.
308

309
        :rtype: :class:`int` or :class:`None`
310

311
        .. versionadded:: 1.1
312
        """
313 0
        current_tl_track = self.core.playback.get_current_tl_track()
314

315 0
        with deprecation.ignore("core.tracklist.previous_track"):
316 0
            previous_tl_track = self.previous_track(current_tl_track)
317

318 0
        return getattr(previous_tl_track, "tlid", None)
319

320 2
    def previous_track(self, tl_track):
321
        """
322
        Returns the track that will be played if calling
323
        :meth:`mopidy.core.PlaybackController.previous()`.
324

325
        For normal playback this is the previous track in the tracklist. If
326
        random and/or consume is enabled it should return the current track
327
        instead.
328

329
        .. deprecated:: 3.0
330
            Use :meth:`get_previous_tlid` instead.
331

332
        :param tl_track: the reference track
333
        :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None`
334
        :rtype: :class:`mopidy.models.TlTrack` or :class:`None`
335
        """
336 2
        deprecation.warn("core.tracklist.previous_track")
337 2
        tl_track is None or validation.check_instance(tl_track, TlTrack)
338

339 2
        if self.get_repeat() or self.get_consume() or self.get_random():
340 2
            return tl_track
341

342 2
        position = self.index(tl_track)
343

344 2
        if position in (None, 0):
345 2
            return None
346

347
        # Since we know we are not at zero we have to be somewhere in the range
348
        # 1 - len(tracks) Thus 'position - 1' will always be within the list.
349 2
        return self._tl_tracks[position - 1]
350

351 2
    def add(self, tracks=None, at_position=None, uris=None):
352
        """
353
        Add tracks to the tracklist.
354

355
        If ``uris`` is given instead of ``tracks``, the URIs are
356
        looked up in the library and the resulting tracks are added to the
357
        tracklist.
358

359
        If ``at_position`` is given, the tracks are inserted at the given
360
        position in the tracklist. If ``at_position`` is not given, the tracks
361
        are appended to the end of the tracklist.
362

363
        Triggers the :meth:`mopidy.core.CoreListener.tracklist_changed` event.
364

365
        :param tracks: tracks to add
366
        :type tracks: list of :class:`mopidy.models.Track` or :class:`None`
367
        :param at_position: position in tracklist to add tracks
368
        :type at_position: int or :class:`None`
369
        :param uris: list of URIs for tracks to add
370
        :type uris: list of string or :class:`None`
371
        :rtype: list of :class:`mopidy.models.TlTrack`
372

373
        .. versionadded:: 1.0
374
            The ``uris`` argument.
375

376
        .. deprecated:: 1.0
377
            The ``tracks`` argument. Use ``uris``.
378
        """
379 2
        if sum(o is not None for o in [tracks, uris]) != 1:
380 0
            raise ValueError('Exactly one of "tracks" or "uris" must be set')
381

382 2
        tracks is None or validation.check_instances(tracks, Track)
383 2
        uris is None or validation.check_uris(uris)
384 2
        validation.check_integer(at_position or 0)
385

386 2
        if tracks:
387 2
            deprecation.warn("core.tracklist.add:tracks_arg")
388

389 2
        if tracks is None:
390 2
            tracks = []
391 2
            track_map = self.core.library.lookup(uris=uris)
392 2
            for uri in uris:
393 2
                tracks.extend(track_map[uri])
394

395 2
        tl_tracks = []
396 2
        max_length = self.core._config["core"]["max_tracklist_length"]
397

398 2
        for track in tracks:
399 2
            if self.get_length() >= max_length:
400 0
                raise exceptions.TracklistFull(
401
                    f"Tracklist may contain at most {max_length:d} tracks."
402
                )
403

404 2
            tl_track = TlTrack(self._next_tlid, track)
405 2
            self._next_tlid += 1
406 2
            if at_position is not None:
407 2
                self._tl_tracks.insert(at_position, tl_track)
408 2
                at_position += 1
409
            else:
410 2
                self._tl_tracks.append(tl_track)
411 2
            tl_tracks.append(tl_track)
412

413 2
        if tl_tracks:
414 2
            self._increase_version()
415

416 2
        return tl_tracks
417

418 2
    def clear(self):
419
        """
420
        Clear the tracklist.
421

422
        Triggers the :meth:`mopidy.core.CoreListener.tracklist_changed` event.
423
        """
424 2
        self._tl_tracks = []
425 2
        self._increase_version()
426

427 2
    def filter(self, criteria):
428
        """
429
        Filter the tracklist by the given criteria.
430

431
        Each rule in the criteria consists of a model field and a list of
432
        values to compare it against. If the model field matches any of the
433
        values, it may be returned.
434

435
        Only tracks that match all the given criteria are returned.
436

437
        Examples::
438

439
            # Returns tracks with TLIDs 1, 2, 3, or 4 (tracklist ID)
440
            filter({'tlid': [1, 2, 3, 4]})
441

442
            # Returns track with URIs 'xyz' or 'abc'
443
            filter({'uri': ['xyz', 'abc']})
444

445
            # Returns track with a matching TLIDs (1, 3 or 6) and a
446
            # matching URI ('xyz' or 'abc')
447
            filter({'tlid': [1, 3, 6], 'uri': ['xyz', 'abc']})
448

449
        :param criteria: one or more rules to match by
450
        :type criteria: dict, of (string, list) pairs
451
        :rtype: list of :class:`mopidy.models.TlTrack`
452
        """
453 2
        tlids = criteria.pop("tlid", [])
454 2
        validation.check_query(criteria, validation.TRACKLIST_FIELDS)
455 2
        validation.check_instances(tlids, int)
456

457 2
        matches = self._tl_tracks
458 2
        for (key, values) in criteria.items():
459 2
            matches = [ct for ct in matches if getattr(ct.track, key) in values]
460 2
        if tlids:
461 2
            matches = [ct for ct in matches if ct.tlid in tlids]
462 2
        return matches
463

464 2
    def move(self, start, end, to_position):
465
        """
466
        Move the tracks in the slice ``[start:end]`` to ``to_position``.
467

468
        Triggers the :meth:`mopidy.core.CoreListener.tracklist_changed` event.
469

470
        :param start: position of first track to move
471
        :type start: int
472
        :param end: position after last track to move
473
        :type end: int
474
        :param to_position: new position for the tracks
475
        :type to_position: int
476
        """
477 2
        if start == end:
478 0
            end += 1
479

480 2
        tl_tracks = self._tl_tracks
481

482
        # TODO: use validation helpers?
483 2
        if start >= end:
484 0
            raise AssertionError("start must be smaller than end")
485 2
        if start < 0:
486 0
            raise AssertionError("start must be at least zero")
487 2
        if end > len(tl_tracks):
488 0
            raise AssertionError("end can not be larger than tracklist length")
489 2
        if to_position < 0:
490 0
            raise AssertionError("to_position must be at least zero")
491 2
        if to_position > len(tl_tracks):
492 0
            raise AssertionError(
493
                "to_position can not be larger than tracklist length"
494
            )
495

496 2
        new_tl_tracks = tl_tracks[:start] + tl_tracks[end:]
497 2
        for tl_track in tl_tracks[start:end]:
498 2
            new_tl_tracks.insert(to_position, tl_track)
499 2
            to_position += 1
500 2
        self._tl_tracks = new_tl_tracks
501 2
        self._increase_version()
502

503 2
    def remove(self, criteria):
504
        """
505
        Remove the matching tracks from the tracklist.
506

507
        Uses :meth:`filter()` to lookup the tracks to remove.
508

509
        Triggers the :meth:`mopidy.core.CoreListener.tracklist_changed` event.
510

511
        :param criteria: one or more rules to match by
512
        :type criteria: dict, of (string, list) pairs
513
        :rtype: list of :class:`mopidy.models.TlTrack` that were removed
514
        """
515 2
        tl_tracks = self.filter(criteria)
516 2
        for tl_track in tl_tracks:
517 2
            position = self._tl_tracks.index(tl_track)
518 2
            del self._tl_tracks[position]
519 2
        self._increase_version()
520 2
        return tl_tracks
521

522 2
    def shuffle(self, start=None, end=None):
523
        """
524
        Shuffles the entire tracklist. If ``start`` and ``end`` is given only
525
        shuffles the slice ``[start:end]``.
526

527
        Triggers the :meth:`mopidy.core.CoreListener.tracklist_changed` event.
528

529
        :param start: position of first track to shuffle
530
        :type start: int or :class:`None`
531
        :param end: position after last track to shuffle
532
        :type end: int or :class:`None`
533
        """
534 2
        tl_tracks = self._tl_tracks
535

536
        # TOOD: use validation helpers?
537 2
        if start is not None and end is not None:
538 0
            if start >= end:
539 0
                raise AssertionError("start must be smaller than end")
540

541 2
        if start is not None:
542 0
            if start < 0:
543 0
                raise AssertionError("start must be at least zero")
544

545 2
        if end is not None:
546 0
            if end > len(tl_tracks):
547 0
                raise AssertionError(
548
                    "end can not be larger than " + "tracklist length"
549
                )
550

551 2
        before = tl_tracks[: start or 0]
552 2
        shuffled = tl_tracks[start:end]
553 2
        after = tl_tracks[end or len(tl_tracks) :]
554 2
        random.shuffle(shuffled)
555 2
        self._tl_tracks = before + shuffled + after
556 2
        self._increase_version()
557

558 2
    def slice(self, start, end):
559
        """
560
        Returns a slice of the tracklist, limited by the given start and end
561
        positions.
562

563
        :param start: position of first track to include in slice
564
        :type start: int
565
        :param end: position after last track to include in slice
566
        :type end: int
567
        :rtype: :class:`mopidy.models.TlTrack`
568
        """
569
        # TODO: validate slice?
570 0
        return self._tl_tracks[start:end]
571

572 2
    def _mark_playing(self, tl_track):
573
        """Internal method for :class:`mopidy.core.PlaybackController`."""
574 2
        if self.get_random() and tl_track in self._shuffled:
575 0
            self._shuffled.remove(tl_track)
576

577 2
    def _mark_unplayable(self, tl_track):
578
        """Internal method for :class:`mopidy.core.PlaybackController`."""
579 2
        logger.warning("Track is not playable: %s", tl_track.track.uri)
580 2
        if self.get_consume() and tl_track is not None:
581 0
            self.remove({"tlid": [tl_track.tlid]})
582 2
        if self.get_random() and tl_track in self._shuffled:
583 2
            self._shuffled.remove(tl_track)
584

585 2
    def _mark_played(self, tl_track):
586
        """Internal method for :class:`mopidy.core.PlaybackController`."""
587 2
        if self.get_consume() and tl_track is not None:
588 2
            self.remove({"tlid": [tl_track.tlid]})
589 2
            return True
590 2
        return False
591

592 2
    def _trigger_tracklist_changed(self):
593 2
        if self.get_random():
594 2
            self._shuffled = self._tl_tracks[:]
595 2
            random.shuffle(self._shuffled)
596
        else:
597 2
            self._shuffled = []
598

599 2
        logger.debug("Triggering event: tracklist_changed()")
600 2
        listener.CoreListener.send("tracklist_changed")
601

602 2
    def _trigger_options_changed(self):
603 2
        logger.debug("Triggering options changed event")
604 2
        listener.CoreListener.send("options_changed")
605

606 2
    def _save_state(self):
607 2
        return TracklistState(
608
            tl_tracks=self._tl_tracks,
609
            next_tlid=self._next_tlid,
610
            consume=self.get_consume(),
611
            random=self.get_random(),
612
            repeat=self.get_repeat(),
613
            single=self.get_single(),
614
        )
615

616 2
    def _load_state(self, state, coverage):
617 2
        if state:
618 2
            if "mode" in coverage:
619 2
                self.set_consume(state.consume)
620 2
                self.set_random(state.random)
621 2
                self.set_repeat(state.repeat)
622 2
                self.set_single(state.single)
623 2
            if "tracklist" in coverage:
624 2
                self._next_tlid = max(state.next_tlid, self._next_tlid)
625 2
                self._tl_tracks = list(state.tl_tracks)
626 2
                self._increase_version()

Read our documentation on viewing source code .

Loading