Use model(s) to save/restore current play state

This commit is contained in:
Jens Luetjen 2016-01-02 15:28:41 +01:00
parent 44841710e0
commit a5a9178b06
10 changed files with 351 additions and 55 deletions

View File

@ -180,6 +180,7 @@ class Core(
file_name = os.path.join( file_name = os.path.join(
self._config['core']['data_dir'], name) self._config['core']['data_dir'], name)
file_name += '.state' file_name += '.state'
logger.info('Save state to "%s"', file_name)
data = {} data = {}
self.tracklist._state_export(data) self.tracklist._state_export(data)

View File

@ -60,13 +60,17 @@ class HistoryController(object):
def _state_export(self, data): def _state_export(self, data):
"""Internal method for :class:`mopidy.Core`.""" """Internal method for :class:`mopidy.Core`."""
data['history'] = {} history_list = []
data['history']['history'] = self._history for timestamp, track in self._history:
history_list.append(models.HistoryTrack(
timestamp=timestamp, track=track))
data['history'] = models.HistoryState(history=history_list)
def _state_import(self, data, coverage): def _state_import(self, data, coverage):
"""Internal method for :class:`mopidy.Core`.""" """Internal method for :class:`mopidy.Core`."""
if 'history' not in data: if 'history' in data:
return hstate = data['history']
if 'history' in coverage: if 'history' in coverage:
if 'history' in data['history']: self._history = []
self._history = data['history']['history'] for htrack in hstate.history:
self._history.append((htrack.timestamp, htrack.track))

View File

@ -5,6 +5,7 @@ import logging
from mopidy import exceptions from mopidy import exceptions
from mopidy.internal import validation from mopidy.internal import validation
from mopidy.models import MixerState
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -102,13 +103,11 @@ class MixerController(object):
def _state_export(self, data): def _state_export(self, data):
"""Internal method for :class:`mopidy.Core`.""" """Internal method for :class:`mopidy.Core`."""
data['mixer'] = {} data['mixer'] = MixerState(volume=self.get_volume())
data['mixer']['volume'] = self.get_volume()
def _state_import(self, data, coverage): def _state_import(self, data, coverage):
"""Internal method for :class:`mopidy.Core`.""" """Internal method for :class:`mopidy.Core`."""
if 'mixer' not in data: if 'mixer' in data:
return ms = data['mixer']
if 'volume' in coverage: if 'volume' in coverage:
if 'volume' in data['mixer']: self.set_volume(ms.volume)
self.set_volume(data['mixer']['volume'])

View File

@ -526,20 +526,18 @@ class PlaybackController(object):
def _state_export(self, data): def _state_export(self, data):
"""Internal method for :class:`mopidy.Core`.""" """Internal method for :class:`mopidy.Core`."""
data['playback'] = {} data['playback'] = models.PlaybackState(
data['playback']['current_tl_track'] = self.get_current_tl_track() tl_track=self.get_current_tl_track(),
data['playback']['position'] = self.get_time_position() position=self.get_time_position(),
# TODO: export/import get_state()? state=self.get_state())
def _state_import(self, data, coverage): def _state_import(self, data, coverage):
"""Internal method for :class:`mopidy.Core`.""" """Internal method for :class:`mopidy.Core`."""
if 'playback' not in data: if 'playback' in data:
return ps = data['playback']
if 'autoplay' in coverage: if 'autoplay' in coverage:
if 'current_tl_track' in data['playback']: tl_track = ps.tl_track
tl_track = data['playback']['current_tl_track']
if tl_track is not None: if tl_track is not None:
self.play(tl_track=tl_track) self.play(tl_track=tl_track)
# TODO: Seek not working. It seeks to early. # TODO: Seek not working. It seeks to early.
# if 'position' in data['playback']: # self.seek(ps.position)
# self.seek(data['playback']['position'])

View File

@ -6,7 +6,7 @@ import random
from mopidy import exceptions from mopidy import exceptions
from mopidy.core import listener from mopidy.core import listener
from mopidy.internal import deprecation, validation from mopidy.internal import deprecation, validation
from mopidy.models import TlTrack, Track from mopidy.models import TlTrack, Track, TracklistState
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -647,32 +647,27 @@ class TracklistController(object):
def _state_export(self, data): def _state_export(self, data):
"""Internal method for :class:`mopidy.Core`.""" """Internal method for :class:`mopidy.Core`."""
data['tracklist'] = {} data['tracklist'] = TracklistState(
data['tracklist']['tl_tracks'] = self._tl_tracks tracks=self._tl_tracks,
data['tracklist']['next_tlid'] = self._next_tlid next_tlid=self._next_tlid,
data['tracklist']['consume'] = self.get_consume() consume=self.get_consume(),
data['tracklist']['random'] = self.get_random() random=self.get_random(),
data['tracklist']['repeat'] = self.get_repeat() repeat=self.get_repeat(),
data['tracklist']['single'] = self.get_single() single=self.get_single())
def _state_import(self, data, coverage): def _state_import(self, data, coverage):
"""Internal method for :class:`mopidy.Core`.""" """Internal method for :class:`mopidy.Core`."""
if 'tracklist' not in data: if 'tracklist' in data:
return tls = data['tracklist']
if 'mode' in coverage: if 'mode' in coverage:
# TODO: only one _trigger_options_changed() for all options self.set_consume(tls.consume)
if 'consume' in data['tracklist']: self.set_random(tls.random)
self.set_consume(data['tracklist']['consume']) self.set_repeat(tls.repeat)
if 'random' in data['tracklist']: self.set_single(tls.single)
self.set_random(data['tracklist']['random']) if 'tracklist' in coverage:
if 'repeat' in data['tracklist']: if tls.next_tlid > self._next_tlid:
self.set_repeat(data['tracklist']['repeat']) self._next_tlid = tls.next_tlid
if 'single' in data['tracklist']: self._tl_tracks = []
self.set_single(data['tracklist']['single']) for track in tls.tracks:
if 'tracklist' in coverage: self._tl_tracks.append(track)
if 'next_tlid' in data['tracklist']:
if data['tracklist']['next_tlid'] > self._next_tlid:
self._next_tlid = data['tracklist']['next_tlid']
if 'tl_tracks' in data['tracklist']:
self._tl_tracks = data['tracklist']['tl_tracks']
self._trigger_tracklist_changed() self._trigger_tracklist_changed()

View File

@ -1,6 +1,7 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from mopidy import compat from mopidy import compat
from mopidy.internal import validation
from mopidy.models import fields from mopidy.models import fields
from mopidy.models.immutable import ImmutableObject, ValidatedImmutableObject from mopidy.models.immutable import ImmutableObject, ValidatedImmutableObject
from mopidy.models.serialize import ModelJSONEncoder, model_json_decoder from mopidy.models.serialize import ModelJSONEncoder, model_json_decoder
@ -8,7 +9,8 @@ from mopidy.models.serialize import ModelJSONEncoder, model_json_decoder
__all__ = [ __all__ = [
'ImmutableObject', 'Ref', 'Image', 'Artist', 'Album', 'track', 'TlTrack', 'ImmutableObject', 'Ref', 'Image', 'Artist', 'Album', 'track', 'TlTrack',
'Playlist', 'SearchResult', 'model_json_decoder', 'ModelJSONEncoder', 'Playlist', 'SearchResult', 'model_json_decoder', 'ModelJSONEncoder',
'ValidatedImmutableObject'] 'ValidatedImmutableObject', 'HistoryTrack', 'HistoryState', 'MixerState',
'PlaybackState', 'TracklistState']
class Ref(ValidatedImmutableObject): class Ref(ValidatedImmutableObject):
@ -360,3 +362,104 @@ class SearchResult(ValidatedImmutableObject):
# The albums matching the search query. Read-only. # The albums matching the search query. Read-only.
albums = fields.Collection(type=Album, container=tuple) albums = fields.Collection(type=Album, container=tuple)
class HistoryTrack(ValidatedImmutableObject):
"""
A history track. Wraps a :class:`Ref` and it's timestamp.
:param timestamp: the timestamp
:type timestamp: int
:param track: the track
:type track: :class:`Ref`
"""
# The timestamp. Read-only.
timestamp = fields.Integer()
# The track. Read-only.
track = fields.Field(type=Ref)
class HistoryState(ValidatedImmutableObject):
"""
State of the history controller.
Internally used for import/export of current state.
:param history: the track history
:type history: list of :class:`HistoryTrack`
"""
# The tracks. Read-only.
history = fields.Collection(type=HistoryTrack, container=tuple)
class MixerState(ValidatedImmutableObject):
"""
State of the mixer controller.
Internally used for import/export of current state.
:param volume: the volume
:type volume: int
"""
# The volume. Read-only.
volume = fields.Integer(min=0, max=100)
class PlaybackState(ValidatedImmutableObject):
"""
State of the playback controller.
Internally used for import/export of current state.
:param tl_track: current track
:type tl_track: :class:`TlTrack`
:param position: play position
:type position: int
:param state: playback state
:type state: :class:`TlTrack`
"""
# The current playing track. Read-only.
tl_track = fields.Field(type=TlTrack)
# The playback position. Read-only.
position = fields.Integer(min=0)
# The playback state. Read-only.
state = fields.Field(choices=validation.PLAYBACK_STATES)
class TracklistState(ValidatedImmutableObject):
"""
State of the tracklist controller.
Internally used for import/export of current state.
:param repeat: the repeat mode
:type repeat: bool
:param consume: the consume mode
:type consume: bool
:param random: the random mode
:type random: bool
:param single: the single mode
:type single: bool
"""
# The repeat mode. Read-only.
repeat = fields.Boolean()
# The consume mode. Read-only.
consume = fields.Boolean()
# The random mode. Read-only.
random = fields.Boolean()
# The single mode. Read-only.
single = fields.Boolean()
# The repeat mode. Read-only.
next_tlid = fields.Integer(min=0)
# The list of tracks. Read-only.
tracks = fields.Collection(type=TlTrack, container=tuple)

View File

@ -135,6 +135,17 @@ class Integer(Field):
return value return value
class Boolean(Field):
"""
:class:`Field` for storing boolean values
:param default: default value for field
"""
def __init__(self, default=None):
super(Boolean, self).__init__(type=bool, default=default)
class Collection(Field): class Collection(Field):
""" """
:class:`Field` for storing collections of a given type. :class:`Field` for storing collections of a given type.

View File

@ -4,7 +4,9 @@ import json
from mopidy.models import immutable from mopidy.models import immutable
_MODELS = ['Ref', 'Artist', 'Album', 'Track', 'TlTrack', 'Playlist'] _MODELS = ['Ref', 'Artist', 'Album', 'Track', 'TlTrack', 'Playlist',
'HistoryTrack', 'HistoryState', 'MixerState', 'PlaybackState',
'TracklistState']
class ModelJSONEncoder(json.JSONEncoder): class ModelJSONEncoder(json.JSONEncoder):

View File

@ -173,6 +173,27 @@ class IntegerTest(unittest.TestCase):
instance.attr = 11 instance.attr = 11
class BooleanTest(unittest.TestCase):
def test_default_handling(self):
instance = create_instance(Boolean(default=True))
self.assertEqual(True, instance.attr)
def test_true_allowed(self):
instance = create_instance(Boolean())
instance.attr = True
self.assertEqual(True, instance.attr)
def test_false_allowed(self):
instance = create_instance(Boolean())
instance.attr = False
self.assertEqual(False, instance.attr)
def test_int_forbidden(self):
instance = create_instance(Boolean())
with self.assertRaises(TypeError):
instance.attr = 1
class CollectionTest(unittest.TestCase): class CollectionTest(unittest.TestCase):
def test_container_instance_is_default(self): def test_container_instance_is_default(self):
instance = create_instance(Collection(type=int, container=frozenset)) instance = create_instance(Collection(type=int, container=frozenset))

View File

@ -4,8 +4,9 @@ import json
import unittest import unittest
from mopidy.models import ( from mopidy.models import (
Album, Artist, Image, ModelJSONEncoder, Playlist, Ref, SearchResult, Album, Artist, HistoryState, HistoryTrack, Image, MixerState,
TlTrack, Track, model_json_decoder) ModelJSONEncoder, PlaybackState, Playlist,
Ref, SearchResult, TlTrack, Track, TracklistState, model_json_decoder)
class InheritanceTest(unittest.TestCase): class InheritanceTest(unittest.TestCase):
@ -1168,3 +1169,164 @@ class SearchResultTest(unittest.TestCase):
self.assertDictEqual( self.assertDictEqual(
{'__model__': 'SearchResult', 'uri': 'uri'}, {'__model__': 'SearchResult', 'uri': 'uri'},
SearchResult(uri='uri').serialize()) SearchResult(uri='uri').serialize())
class HistoryTrackTest(unittest.TestCase):
def test_track(self):
track = Ref.track()
result = HistoryTrack(track=track)
self.assertEqual(result.track, track)
with self.assertRaises(AttributeError):
result.track = None
def test_timestamp(self):
timestamp = 1234
result = HistoryTrack(timestamp=timestamp)
self.assertEqual(result.timestamp, timestamp)
with self.assertRaises(AttributeError):
result.timestamp = None
class HistoryStateTest(unittest.TestCase):
def test_history_list(self):
history = (HistoryTrack(),
HistoryTrack())
result = HistoryState(history=history)
self.assertEqual(result.history, history)
with self.assertRaises(AttributeError):
result.history = None
def test_history_string_fail(self):
history = 'not_a_valid_history'
with self.assertRaises(TypeError):
HistoryState(history=history)
class MixerStateTest(unittest.TestCase):
def test_volume(self):
volume = 37
result = MixerState(volume=volume)
self.assertEqual(result.volume, volume)
with self.assertRaises(AttributeError):
result.volume = None
def test_volume_invalid(self):
volume = 105
with self.assertRaises(ValueError):
MixerState(volume=volume)
class PlaybackStateTest(unittest.TestCase):
def test_position(self):
position = 123456
result = PlaybackState(position=position)
self.assertEqual(result.position, position)
with self.assertRaises(AttributeError):
result.position = None
def test_position_invalid(self):
position = -1
with self.assertRaises(ValueError):
PlaybackState(position=position)
def test_tl_track(self):
tl_track = TlTrack()
result = PlaybackState(tl_track=tl_track)
self.assertEqual(result.tl_track, tl_track)
with self.assertRaises(AttributeError):
result.tl_track = None
def test_tl_track_none(self):
tl_track = None
result = PlaybackState(tl_track=tl_track)
self.assertEqual(result.tl_track, tl_track)
with self.assertRaises(AttributeError):
result.tl_track = None
def test_tl_track_invalid(self):
tl_track = Track()
with self.assertRaises(TypeError):
PlaybackState(tl_track=tl_track)
def test_state(self):
state = 'playing'
result = PlaybackState(state=state)
self.assertEqual(result.state, state)
with self.assertRaises(AttributeError):
result.state = None
def test_state_invalid(self):
state = 'not_a_state'
with self.assertRaises(TypeError):
PlaybackState(state=state)
class TracklistStateTest(unittest.TestCase):
def test_repeat_true(self):
repeat = True
result = TracklistState(repeat=repeat)
self.assertEqual(result.repeat, repeat)
with self.assertRaises(AttributeError):
result.repeat = None
def test_repeat_false(self):
repeat = False
result = TracklistState(repeat=repeat)
self.assertEqual(result.repeat, repeat)
with self.assertRaises(AttributeError):
result.repeat = None
def test_repeat_invalid(self):
repeat = 33
with self.assertRaises(TypeError):
TracklistState(repeat=repeat)
def test_consume_true(self):
val = True
result = TracklistState(consume=val)
self.assertEqual(result.consume, val)
with self.assertRaises(AttributeError):
result.repeat = None
def test_random_true(self):
val = True
result = TracklistState(random=val)
self.assertEqual(result.random, val)
with self.assertRaises(AttributeError):
result.random = None
def test_single_true(self):
val = True
result = TracklistState(single=val)
self.assertEqual(result.single, val)
with self.assertRaises(AttributeError):
result.single = None
def test_next_tlid(self):
val = 654
result = TracklistState(next_tlid=val)
self.assertEqual(result.next_tlid, val)
with self.assertRaises(AttributeError):
result.next_tlid = None
def test_next_tlid_invalid(self):
val = -1
with self.assertRaises(ValueError):
TracklistState(next_tlid=val)
def test_tracks(self):
tracks = (TlTrack(), TlTrack())
result = TracklistState(tracks=tracks)
self.assertEqual(result.tracks, tracks)
with self.assertRaises(AttributeError):
result.tracks = None
def test_tracks_invalid(self):
tracks = (Track(), Track())
with self.assertRaises(TypeError):
TracklistState(tracks=tracks)