diff --git a/docs/api/core.rst b/docs/api/core.rst index 38cc0f0a..21ff79f5 100644 --- a/docs/api/core.rst +++ b/docs/api/core.rst @@ -37,6 +37,15 @@ Manages everything related to the tracks we are currently playing. :members: +History controller +================== + +Keeps record of what tracks have been played. + +.. autoclass:: mopidy.core.HistoryController + :members: + + Playlists controller ==================== diff --git a/docs/changelog.rst b/docs/changelog.rst index cbaced98..f27ca8cd 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,9 @@ v0.20.0 (UNRELEASED) **Core API** +- Added :class:`mopidy.core.HistoryController` which keeps track of what + tracks have been played. (Fixes: :issue:`423`, PR: :issue:`803`) + - Removed ``clear_current_track`` keyword argument to :meth:`mopidy.core.Playback.stop`. It was a leaky internal abstraction, which was never intended to be used externally. diff --git a/mopidy/core/__init__.py b/mopidy/core/__init__.py index f49bbbe7..019034fc 100644 --- a/mopidy/core/__init__.py +++ b/mopidy/core/__init__.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals # flake8: noqa from .actor import Core +from .history import HistoryController from .library import LibraryController from .listener import CoreListener from .playback import PlaybackController, PlaybackState diff --git a/mopidy/core/actor.py b/mopidy/core/actor.py index 66f2aa82..edf13679 100644 --- a/mopidy/core/actor.py +++ b/mopidy/core/actor.py @@ -7,6 +7,7 @@ import pykka from mopidy import audio, backend, mixer from mopidy.audio import PlaybackState +from mopidy.core.history import HistoryController from mopidy.core.library import LibraryController from mopidy.core.listener import CoreListener from mopidy.core.playback import PlaybackController @@ -23,6 +24,10 @@ class Core( """The library controller. An instance of :class:`mopidy.core.LibraryController`.""" + history = None + """The playback history controller. An instance of + :class:`mopidy.core.HistoryController`.""" + playback = None """The playback controller. An instance of :class:`mopidy.core.PlaybackController`.""" @@ -42,6 +47,8 @@ class Core( self.library = LibraryController(backends=self.backends, core=self) + self.history = HistoryController() + self.playback = PlaybackController( mixer=mixer, backends=self.backends, core=self) diff --git a/mopidy/core/history.py b/mopidy/core/history.py new file mode 100644 index 00000000..6711bcf4 --- /dev/null +++ b/mopidy/core/history.py @@ -0,0 +1,54 @@ +from __future__ import unicode_literals + +import copy +import logging +import time + +from mopidy import models + + +logger = logging.getLogger(__name__) + + +class HistoryController(object): + + def __init__(self): + self._history = [] + + def add(self, track): + """Add track to the playback history. + + :param track: track to add + :type track: :class:`mopidy.models.Track` + """ + if not isinstance(track, models.Track): + raise TypeError('Only Track objects can be added to the history') + + timestamp = int(time.time() * 1000) + + name_parts = [] + if track.artists: + name_parts.append( + ', '.join([artist.name for artist in track.artists])) + if track.name is not None: + name_parts.append(track.name) + name = ' - '.join(name_parts) + ref = models.Ref.track(uri=track.uri, name=name) + + self._history.insert(0, (timestamp, ref)) + + def get_length(self): + """Get the number of tracks in the history. + + :returns: the history length + :rtype: int + """ + return len(self._history) + + def get_history(self): + """Get the track history. + + :returns: the track history + :rtype: list of (timestamp, mopidy.models.Ref) tuples + """ + return copy.copy(self._history) diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index 5cd2b65f..8d5f05da 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -250,6 +250,7 @@ class PlaybackController(object): if success: self.core.tracklist.mark_playing(tl_track) + self.core.history.add(tl_track.track) # TODO: replace with stream-changed self._trigger_track_playback_started() else: diff --git a/mopidy/http/handlers.py b/mopidy/http/handlers.py index 3ef10c3b..3bfc1eff 100644 --- a/mopidy/http/handlers.py +++ b/mopidy/http/handlers.py @@ -41,6 +41,7 @@ def make_jsonrpc_wrapper(core_actor): objects={ 'core.get_uri_schemes': core.Core.get_uri_schemes, 'core.get_version': core.Core.get_version, + 'core.history': core.HistoryController, 'core.library': core.LibraryController, 'core.playback': core.PlaybackController, 'core.playlists': core.PlaylistsController, @@ -51,6 +52,7 @@ def make_jsonrpc_wrapper(core_actor): 'core.describe': inspector.describe, 'core.get_uri_schemes': core_actor.get_uri_schemes, 'core.get_version': core_actor.get_version, + 'core.history': core_actor.history, 'core.library': core_actor.library, 'core.playback': core_actor.playback, 'core.playlists': core_actor.playlists, diff --git a/tests/core/test_history.py b/tests/core/test_history.py new file mode 100644 index 00000000..75b4dc76 --- /dev/null +++ b/tests/core/test_history.py @@ -0,0 +1,47 @@ +from __future__ import unicode_literals + +import unittest + +from mopidy.core import HistoryController +from mopidy.models import Artist, Track + + +class PlaybackHistoryTest(unittest.TestCase): + + def setUp(self): + self.tracks = [ + Track(uri='dummy1:a', name='foo', + artists=[Artist(name='foober'), Artist(name='barber')]), + Track(uri='dummy2:a', name='foo'), + Track(uri='dummy3:a', name='bar') + ] + self.history = HistoryController() + + def test_add_track(self): + self.history.add(self.tracks[0]) + self.assertEqual(self.history.get_length(), 1) + + self.history.add(self.tracks[1]) + self.assertEqual(self.history.get_length(), 2) + + self.history.add(self.tracks[2]) + self.assertEqual(self.history.get_length(), 3) + + def test_non_tracks_are_rejected(self): + with self.assertRaises(TypeError): + self.history.add(object()) + + self.assertEqual(self.history.get_length(), 0) + + def test_history_entry_contents(self): + track = self.tracks[0] + self.history.add(track) + + result = self.history.get_history() + (timestamp, ref) = result[0] + + self.assertIsInstance(timestamp, int) + self.assertEqual(track.uri, ref.uri) + self.assertIn(track.name, ref.name) + for artist in track.artists: + self.assertIn(artist.name, ref.name)