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(
self._config['core']['data_dir'], name)
file_name += '.state'
logger.info('Save state to "%s"', file_name)
data = {}
self.tracklist._state_export(data)

View File

@ -60,13 +60,17 @@ class HistoryController(object):
def _state_export(self, data):
"""Internal method for :class:`mopidy.Core`."""
data['history'] = {}
data['history']['history'] = self._history
history_list = []
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):
"""Internal method for :class:`mopidy.Core`."""
if 'history' not in data:
return
if 'history' in coverage:
if 'history' in data['history']:
self._history = data['history']['history']
if 'history' in data:
hstate = data['history']
if 'history' in coverage:
self._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.internal import validation
from mopidy.models import MixerState
logger = logging.getLogger(__name__)
@ -102,13 +103,11 @@ class MixerController(object):
def _state_export(self, data):
"""Internal method for :class:`mopidy.Core`."""
data['mixer'] = {}
data['mixer']['volume'] = self.get_volume()
data['mixer'] = MixerState(volume=self.get_volume())
def _state_import(self, data, coverage):
"""Internal method for :class:`mopidy.Core`."""
if 'mixer' not in data:
return
if 'volume' in coverage:
if 'volume' in data['mixer']:
self.set_volume(data['mixer']['volume'])
if 'mixer' in data:
ms = data['mixer']
if 'volume' in coverage:
self.set_volume(ms.volume)

View File

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

View File

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

View File

@ -1,6 +1,7 @@
from __future__ import absolute_import, unicode_literals
from mopidy import compat
from mopidy.internal import validation
from mopidy.models import fields
from mopidy.models.immutable import ImmutableObject, ValidatedImmutableObject
from mopidy.models.serialize import ModelJSONEncoder, model_json_decoder
@ -8,7 +9,8 @@ from mopidy.models.serialize import ModelJSONEncoder, model_json_decoder
__all__ = [
'ImmutableObject', 'Ref', 'Image', 'Artist', 'Album', 'track', 'TlTrack',
'Playlist', 'SearchResult', 'model_json_decoder', 'ModelJSONEncoder',
'ValidatedImmutableObject']
'ValidatedImmutableObject', 'HistoryTrack', 'HistoryState', 'MixerState',
'PlaybackState', 'TracklistState']
class Ref(ValidatedImmutableObject):
@ -360,3 +362,104 @@ class SearchResult(ValidatedImmutableObject):
# The albums matching the search query. Read-only.
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
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:`Field` for storing collections of a given type.

View File

@ -4,7 +4,9 @@ import json
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):

View File

@ -173,6 +173,27 @@ class IntegerTest(unittest.TestCase):
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):
def test_container_instance_is_default(self):
instance = create_instance(Collection(type=int, container=frozenset))

View File

@ -4,8 +4,9 @@ import json
import unittest
from mopidy.models import (
Album, Artist, Image, ModelJSONEncoder, Playlist, Ref, SearchResult,
TlTrack, Track, model_json_decoder)
Album, Artist, HistoryState, HistoryTrack, Image, MixerState,
ModelJSONEncoder, PlaybackState, Playlist,
Ref, SearchResult, TlTrack, Track, TracklistState, model_json_decoder)
class InheritanceTest(unittest.TestCase):
@ -1168,3 +1169,164 @@ class SearchResultTest(unittest.TestCase):
self.assertDictEqual(
{'__model__': 'SearchResult', 'uri': 'uri'},
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)