diff --git a/mopidy/core/tracklist.py b/mopidy/core/tracklist.py index e0497a9a..01da6019 100644 --- a/mopidy/core/tracklist.py +++ b/mopidy/core/tracklist.py @@ -200,19 +200,52 @@ class TracklistController(object): # Methods - def index(self, tl_track): + def index(self, tl_track=None, tlid=None): """ The position of the given track in the tracklist. + If neither *tl_track* or *tlid* is given we return the index of + the currently playing track. + :param tl_track: the track to find the index of :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None` + :param tlid: TLID of the track to find the index of + :type tlid: :class:`int` or :class:`None` :rtype: :class:`int` or :class:`None` + + .. versionchanged:: 1.1 + Added the *tlid* parameter """ tl_track is None or validation.check_instance(tl_track, TlTrack) - try: - return self._tl_tracks.index(tl_track) - except ValueError: - return None + tlid is None or validation.check_integer(tlid, min=0) + + if tl_track is None and tlid is None: + tl_track = self.core.playback.get_current_tl_track() + + if tl_track is not None: + try: + return self._tl_tracks.index(tl_track) + except ValueError: + pass + elif tlid is not None: + for i, tl_track in enumerate(self._tl_tracks): + if tl_track.tlid == tlid: + return i + return None + + def get_eot_tlid(self): + """ + The TLID of the track that will be played after the given track. + + Not necessarily the same TLID as returned by :meth:`get_next_tlid`. + + :rtype: :class:`int` or :class:`None` + + .. versionadded:: 1.1 + """ + + current_tl_track = self.core.playback.get_current_tl_track() + return getattr(self.eot_track(current_tl_track), 'tlid', None) def eot_track(self, tl_track): """ @@ -224,6 +257,7 @@ class TracklistController(object): :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None` :rtype: :class:`mopidy.models.TlTrack` or :class:`None` """ + deprecation.warn('core.tracklist.eot_track', pending=True) tl_track is None or validation.check_instance(tl_track, TlTrack) if self.get_single() and self.get_repeat(): return tl_track @@ -235,6 +269,23 @@ class TracklistController(object): # shared. return self.next_track(tl_track) + def get_next_tlid(self): + """ + The tlid of the track that will be played if calling + :meth:`mopidy.core.PlaybackController.next()`. + + For normal playback this is the next track in the tracklist. If repeat + is enabled the next track can loop around the tracklist. When random is + enabled this should be a random track, all tracks should be played once + before the tracklist repeats. + + :rtype: :class:`int` or :class:`None` + + .. versionadded:: 1.1 + """ + current_tl_track = self.core.playback.get_current_tl_track() + return getattr(self.next_track(current_tl_track), 'tlid', None) + def next_track(self, tl_track): """ The track that will be played if calling @@ -249,35 +300,51 @@ class TracklistController(object): :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None` :rtype: :class:`mopidy.models.TlTrack` or :class:`None` """ + deprecation.warn('core.tracklist.next_track', pending=True) tl_track is None or validation.check_instance(tl_track, TlTrack) - if not self.get_tl_tracks(): + if not self._tl_tracks: return None if self.get_random() and not self._shuffled: if self.get_repeat() or not tl_track: logger.debug('Shuffling tracks') - self._shuffled = self.get_tl_tracks() + self._shuffled = self._tl_tracks[:] random.shuffle(self._shuffled) if self.get_random(): - try: + if self._shuffled: return self._shuffled[0] - except IndexError: - return None + return None if tl_track is None: - return self.get_tl_tracks()[0] + next_index = 0 + else: + next_index = self.index(tl_track) + 1 - next_index = self.index(tl_track) + 1 if self.get_repeat(): - next_index %= len(self.get_tl_tracks()) - - try: - return self.get_tl_tracks()[next_index] - except IndexError: + next_index %= len(self._tl_tracks) + elif next_index >= len(self._tl_tracks): return None + return self._tl_tracks[next_index] + + def get_previous_tlid(self): + """ + Returns the TLID of the track that will be played if calling + :meth:`mopidy.core.PlaybackController.previous()`. + + For normal playback this is the previous track in the tracklist. If + random and/or consume is enabled it should return the current track + instead. + + :rtype: :class:`int` or :class:`None` + + .. versionadded:: 1.1 + """ + current_tl_track = self.core.playback.get_current_tl_track() + return getattr(self.previous_track(current_tl_track), 'tlid', None) + def previous_track(self, tl_track): """ Returns the track that will be played if calling @@ -291,6 +358,7 @@ class TracklistController(object): :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None` :rtype: :class:`mopidy.models.TlTrack` or :class:`None` """ + deprecation.warn('core.tracklist.previous_track', pending=True) tl_track is None or validation.check_instance(tl_track, TlTrack) if self.get_repeat() or self.get_consume() or self.get_random(): @@ -301,7 +369,9 @@ class TracklistController(object): if position in (None, 0): return None - return self.get_tl_tracks()[position - 1] + # Since we know we are not at zero we have to be somewhere in the range + # 1 - len(tracks) Thus 'position - 1' will always be within the list. + return self._tl_tracks[position - 1] def add(self, tracks=None, at_position=None, uri=None, uris=None): """ @@ -554,7 +624,7 @@ class TracklistController(object): def _trigger_tracklist_changed(self): if self.get_random(): - self._shuffled = self.get_tl_tracks() + self._shuffled = self._tl_tracks[:] random.shuffle(self._shuffled) else: self._shuffled = [] diff --git a/mopidy/utils/deprecation.py b/mopidy/utils/deprecation.py index db263e6d..a650d79e 100644 --- a/mopidy/utils/deprecation.py +++ b/mopidy/utils/deprecation.py @@ -45,13 +45,27 @@ _MESSAGES = { 'core.tracklist.remove:kwargs_criteria': 'tracklist.remove() with "kwargs" as criteria is deprecated', + 'core.tracklist.eot_track': + 'tracklist.eot_track() is pending deprecation, use ' + 'tracklist.get_eot_tlid()', + 'core.tracklist.next_track': + 'tracklist.next_track() is pending deprecation, use ' + 'tracklist.get_next_tlid()', + 'core.tracklist.previous_track': + 'tracklist.previous_track() is pending deprecation, use ' + 'tracklist.get_previous_tlid()', + 'models.immutable.copy': 'ImmutableObject.copy() is deprecated, use ImmutableObject.replace()', } -def warn(msg_id): - warnings.warn(_MESSAGES.get(msg_id, msg_id), DeprecationWarning) +def warn(msg_id, pending=False): + if pending: + category = PendingDeprecationWarning + else: + category = DeprecationWarning + warnings.warn(_MESSAGES.get(msg_id, msg_id), category) @contextlib.contextmanager diff --git a/tests/core/test_tracklist.py b/tests/core/test_tracklist.py index 1ff089cb..6339a18c 100644 --- a/tests/core/test_tracklist.py +++ b/tests/core/test_tracklist.py @@ -5,7 +5,7 @@ import unittest import mock from mopidy import backend, core -from mopidy.models import Track +from mopidy.models import TlTrack, Track from mopidy.utils import deprecation @@ -102,3 +102,66 @@ class TracklistTest(unittest.TestCase): self.core.tracklist.filter({'uri': 'a'}) # TODO Extract tracklist tests from the local backend tests + + +class TracklistIndexTest(unittest.TestCase): + + def setUp(self): # noqa: N802 + self.tracks = [ + Track(uri='dummy1:a', name='foo'), + Track(uri='dummy1:b', name='foo'), + Track(uri='dummy1:c', name='bar'), + ] + + def lookup(uris): + return {u: [t for t in self.tracks if t.uri == u] for u in uris} + + self.core = core.Core(mixer=None, backends=[]) + self.core.library = mock.Mock(spec=core.LibraryController) + self.core.library.lookup.side_effect = lookup + + self.core.playback = mock.Mock(spec=core.PlaybackController) + + self.tl_tracks = self.core.tracklist.add(uris=[ + t.uri for t in self.tracks]) + + def test_index_returns_index_of_track(self): + self.assertEqual(0, self.core.tracklist.index(self.tl_tracks[0])) + self.assertEqual(1, self.core.tracklist.index(self.tl_tracks[1])) + self.assertEqual(2, self.core.tracklist.index(self.tl_tracks[2])) + + def test_index_returns_none_if_item_not_found(self): + tl_track = TlTrack(0, Track()) + self.assertEqual(self.core.tracklist.index(tl_track), None) + + def test_index_returns_none_if_called_with_none(self): + self.assertEqual(self.core.tracklist.index(None), None) + + def test_index_errors_out_for_invalid_tltrack(self): + with self.assertRaises(ValueError): + self.core.tracklist.index('abc') + + def test_index_return_index_when_called_with_tlids(self): + tl_tracks = self.tl_tracks + self.assertEqual(0, self.core.tracklist.index(tlid=tl_tracks[0].tlid)) + self.assertEqual(1, self.core.tracklist.index(tlid=tl_tracks[1].tlid)) + self.assertEqual(2, self.core.tracklist.index(tlid=tl_tracks[2].tlid)) + + def test_index_returns_none_if_tlid_not_found(self): + self.assertEqual(self.core.tracklist.index(tlid=123), None) + + def test_index_returns_none_if_called_with_tlid_none(self): + self.assertEqual(self.core.tracklist.index(tlid=None), None) + + def test_index_errors_out_for_invalid_tlid(self): + with self.assertRaises(ValueError): + self.core.tracklist.index(tlid=-1) + + def test_index_without_args_returns_current_tl_track_index(self): + self.core.playback.get_current_tl_track.side_effect = [ + None, self.tl_tracks[0], self.tl_tracks[1], self.tl_tracks[2]] + + self.assertEqual(None, self.core.tracklist.index()) + self.assertEqual(0, self.core.tracklist.index()) + self.assertEqual(1, self.core.tracklist.index()) + self.assertEqual(2, self.core.tracklist.index()) diff --git a/tests/local/test_tracklist.py b/tests/local/test_tracklist.py index f405f218..a0add637 100644 --- a/tests/local/test_tracklist.py +++ b/tests/local/test_tracklist.py @@ -8,7 +8,7 @@ import pykka from mopidy import core from mopidy.core import PlaybackState from mopidy.local import actor -from mopidy.models import Playlist, TlTrack, Track +from mopidy.models import Playlist, Track from mopidy.utils import deprecation from tests import dummy_audio, path_to_data_dir @@ -176,16 +176,6 @@ class LocalTracklistProviderTest(unittest.TestCase): tl_tracks = self.controller.add(self.controller.tracks[1:2]) self.assertEqual(tl_tracks[0].track, self.controller.tracks[1]) - def test_index_returns_index_of_track(self): - tl_tracks = self.controller.add(self.tracks) - self.assertEqual(0, self.controller.index(tl_tracks[0])) - self.assertEqual(1, self.controller.index(tl_tracks[1])) - self.assertEqual(2, self.controller.index(tl_tracks[2])) - - def test_index_returns_none_if_item_not_found(self): - tl_track = TlTrack(0, Track()) - self.assertEqual(self.controller.index(tl_track), None) - @populate_tracklist def test_move_single(self): self.controller.move(0, 0, 2)