Use model(s) to save/restore current play state
This commit is contained in:
parent
44841710e0
commit
a5a9178b06
@ -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)
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user