-
-
Notifications
You must be signed in to change notification settings - Fork 88
/
player_state.py
150 lines (118 loc) · 4.89 KB
/
player_state.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
"""Module responsible for keeping track of media player states."""
import asyncio
import logging
from datetime import datetime
from pyatv.mrp import protobuf
_LOGGER = logging.getLogger(__name__)
def _cocoa_to_timestamp(time):
delta = datetime(2001, 1, 1) - datetime(1970, 1, 1)
timestamp = datetime.fromtimestamp(time) + delta
return timestamp
class PlayerState:
"""Represent what is currently playing on a device."""
def __init__(self):
"""Initialize a new PlayerState instance."""
self.playback_state = None
self.supported_commands = []
self.timestamp = None
self.items = []
self.location = 0
@property
def metadata(self):
"""Metadata of currently playing item."""
if len(self.items) >= (self.location + 1):
return self.items[self.location].metadata
return None
def metadata_field(self, field):
"""Return a specific metadata field or None if missing."""
metadata = self.metadata
if metadata and metadata.HasField(field):
return getattr(metadata, field)
return None
def handle_set_state(self, setstate):
"""Update current state with new data from SetStateMessage."""
if setstate.HasField('playbackState'):
self.playback_state = setstate.playbackState
if setstate.HasField('supportedCommands'):
self.supported_commands = \
setstate.supportedCommands.supportedCommands
if setstate.HasField('playbackStateTimestamp'):
self.timestamp = _cocoa_to_timestamp(
int(setstate.playbackStateTimestamp))
if setstate.HasField('playbackQueue'):
queue = setstate.playbackQueue
self.items = queue.contentItems
self.location = queue.location
def handle_content_item_update(self, item_update):
"""Update current state with new data from ContentItemUpdate."""
for updated_item in item_update.contentItems:
for existing in self.items:
if updated_item.identifier == existing.identifier:
existing.CopyFrom(updated_item)
class PlayerStateManager: # pylint: disable=too-few-public-methods
"""Manage state of all media players."""
def __init__(self, protocol, loop):
"""Initialize a new PlayerStateManager instance."""
self.protocol = protocol
self.loop = loop
self.states = {}
self.active = None
self._listener = None
self._add_listeners()
def _add_listeners(self):
self.protocol.add_listener(
self._handle_set_state, protobuf.SET_STATE_MESSAGE)
self.protocol.add_listener(
self._handle_content_item_update,
protobuf.UPDATE_CONTENT_ITEM_MESSAGE)
self.protocol.add_listener(
self._handle_set_now_playing_client,
protobuf.SET_NOW_PLAYING_CLIENT_MESSAGE)
@property
def listener(self):
"""Return current listener."""
return self._listener
@listener.setter
def listener(self, new_listener):
"""Change current listener."""
self._listener = new_listener
if self.listener:
asyncio.ensure_future(
self.listener.state_updated(), loop=self.loop)
@property
def playing(self):
"""Player state for active media player."""
if self.active:
return self.states[self.active]
return PlayerState()
async def _handle_set_state(self, message, _):
setstate = message.inner()
identifier = setstate.playerPath.client.bundleIdentifier
if identifier not in self.states:
self.states[identifier] = PlayerState()
self.states[identifier].handle_set_state(setstate)
# Only trigger callback if current state changed
if identifier == self.active:
if self.listener:
await self.listener.state_updated()
async def _handle_content_item_update(self, message, _):
item_update = message.inner()
identifier = item_update.playerPath.client.bundleIdentifier
if identifier in self.states:
state = self.states[identifier]
state.handle_content_item_update(item_update)
# Only trigger callback if current state changed
if identifier == self.active:
if self.listener:
await self.listener.state_updated()
else:
_LOGGER.warning(
'Received ContentItemUpdate for unknown player %s',
identifier)
async def _handle_set_now_playing_client(self, message, _):
identifier = message.inner().client.bundleIdentifier
if identifier != self.active:
self.active = identifier
_LOGGER.debug('Active player is now %s', self.active)
if self.listener:
await self.listener.state_updated()