Skip to content

Commit fe66e13

Browse files
authored
feat:spotifyd_integration (#13)
* feat:spotifyd_integration * feat:spotifyd_integration
1 parent c2aa768 commit fe66e13

File tree

4 files changed

+267
-28
lines changed

4 files changed

+267
-28
lines changed

ovos_media_plugin_spotify/__init__.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from ovos_utils.log import LOG
55

66
from ovos_media_plugin_spotify.spotify_client import SpotifyClient
7+
from ovos_media_plugin_spotify.spotifyd import SpotifydHooks
78

89

910
class SpotifyOCPAudioService(AudioPlayerBackend):
@@ -15,8 +16,12 @@ def __init__(self, config, bus=None):
1516
super().__init__(config, bus)
1617
self.spotify = SpotifyClient()
1718
self._paused = False
18-
self.ts = 0
19+
self._last_sync_ts = 0
1920
self.device_name = self.config.get("identifier") # device name in spotify
21+
self.hooks = SpotifydHooks(bus=self.bus,
22+
track_start_callback=self.on_track_start,
23+
track_end_callback=self.on_track_end,
24+
track_error_callback=self.on_track_error)
2025

2126
@property
2227
def device(self):
@@ -32,24 +37,30 @@ def supported_uris(self):
3237
return []
3338
return ['spotify']
3439

35-
def on_track_start(self):
36-
self.ts = time.time()
40+
def on_track_start(self, uri: str = ""):
41+
self._now_playing = uri or self._now_playing
42+
self._last_sync_ts = time.time()
3743
# Indicate to audio service which track is being played
3844
if self._track_start_callback:
3945
self._track_start_callback(self._now_playing)
4046

41-
def on_track_end(self):
47+
def on_track_end(self, uri: str = ""):
48+
if not uri:
49+
self.hooks.reset_metadata()
4250
self._paused = False
43-
self.ts = 0
51+
self._last_sync_ts = 0
4452
if self._track_start_callback:
4553
self._track_start_callback(None)
4654

47-
def on_track_error(self):
55+
def on_track_error(self, uri: str = ""):
56+
if not uri:
57+
self.hooks.reset_metadata()
4858
self._paused = False
49-
self.ts = 0
59+
self._last_sync_ts = 0
5060
self.ocp_error()
5161

5262
def play(self):
63+
self.hooks.preload_uri(self._now_playing)
5364
self.on_track_start()
5465
try:
5566
self.spotify.play([self._now_playing],
@@ -60,7 +71,7 @@ def play(self):
6071

6172
def _wait_until_finished(self):
6273
# pool spotify to see when the player becomes inactive
63-
while self.ts > 0:
74+
while self._last_sync_ts > 0:
6475
time.sleep(2)
6576
for d in self.spotify.devices:
6677
if d["name"] == self.device_name and not d["is_active"]:
@@ -98,17 +109,13 @@ def get_track_length(self) -> int:
98109
"""
99110
getting the duration of the audio in milliseconds
100111
"""
101-
# we only can estimate how much we already played as a minimum value
102-
return self.get_track_position()
112+
return self.hooks.get_track_length()
103113

104114
def get_track_position(self) -> int:
105115
"""
106116
get current position in milliseconds
107117
"""
108-
# approximate given timestamp of playback start
109-
if self.ts:
110-
return int((time.time() - self.ts) * 1000)
111-
return 0
118+
return self.hooks.get_track_position()
112119

113120
def set_track_position(self, milliseconds):
114121
"""

ovos_media_plugin_spotify/audio.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from ovos_utils.log import LOG
55

66
from ovos_media_plugin_spotify.spotify_client import SpotifyClient
7+
from ovos_media_plugin_spotify.spotifyd import SpotifydHooks
78

89

910
class SpotifyAudioService(AudioBackend):
@@ -15,8 +16,12 @@ def __init__(self, config, bus, name='spotify'):
1516
super().__init__(config, bus, name)
1617
self.spotify = SpotifyClient()
1718
self._paused = False
18-
self.ts = 0
19+
self._last_sync_ts = 0
1920
self.device_name = self.config.get("identifier") # device name in spotify
21+
self.hooks = SpotifydHooks(bus=self.bus,
22+
track_start_callback=self.on_track_start,
23+
track_end_callback=self.on_track_end,
24+
track_error_callback=self.on_track_error)
2025

2126
@property
2227
def device(self):
@@ -32,25 +37,31 @@ def supported_uris(self):
3237
return []
3338
return ['spotify']
3439

35-
def on_track_start(self):
36-
self.ts = time.time()
40+
def on_track_start(self, uri: str = ""):
41+
self._now_playing = uri or self._now_playing
42+
self._last_sync_ts = time.time()
3743
# Indicate to audio service which track is being played
3844
if self._track_start_callback:
3945
# TODO why is it None sometimes?
4046
if self._now_playing:
4147
self._track_start_callback(self._now_playing)
4248

43-
def on_track_end(self):
49+
def on_track_end(self, uri: str = ""):
50+
if not uri:
51+
self.hooks.reset_metadata()
4452
self._paused = False
45-
self.ts = 0
53+
self._last_sync_ts = 0
4654
if self._track_start_callback:
4755
self._track_start_callback(None)
4856

49-
def on_track_error(self):
57+
def on_track_error(self, uri: str = ""):
58+
if not uri:
59+
self.hooks.reset_metadata()
5060
self._paused = False
51-
self.ts = 0
61+
self._last_sync_ts = 0
5262

5363
def play(self, repeat=False):
64+
self.hooks.preload_uri(self._now_playing)
5465
self.on_track_start()
5566
try:
5667
self.spotify.play([self._now_playing],
@@ -61,7 +72,7 @@ def play(self, repeat=False):
6172

6273
def _wait_until_finished(self):
6374
# pool spotify to see when the player becomes inactive
64-
while self.ts > 0:
75+
while self._last_sync_ts > 0:
6576
time.sleep(2)
6677
for d in self.spotify.devices:
6778
if d["name"] == self.device_name and not d["is_active"]:
@@ -106,17 +117,13 @@ def get_track_length(self) -> int:
106117
"""
107118
getting the duration of the audio in milliseconds
108119
"""
109-
# we only can estimate how much we already played as a minimum value
110-
return self.get_track_position()
120+
return self.hooks.get_track_length()
111121

112122
def get_track_position(self) -> int:
113123
"""
114124
get current position in milliseconds
115125
"""
116-
# approximate given timestamp of playback start
117-
if self.ts:
118-
return int((time.time() - self.ts) * 1000)
119-
return 0
126+
return self.hooks.get_track_position()
120127

121128
def set_track_position(self, milliseconds):
122129
"""
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import time
2+
3+
from ovos_bus_client.message import Message
4+
5+
6+
class SpotifydHooks:
7+
def __init__(self, bus,
8+
track_start_callback=None,
9+
track_end_callback=None,
10+
track_error_callback=None):
11+
self.current_uri = None
12+
self._last_sync_ts = 0
13+
self._track_len = 0
14+
self._track_pos = 0
15+
self.bus = bus
16+
17+
self.track_start_callback = track_start_callback
18+
self.track_end_callback = track_end_callback
19+
self.track_error_callback = track_error_callback
20+
21+
self.bus.on("spotifyd.start", self.on_spotify_start)
22+
self.bus.on("spotifyd.play", self.on_spotify_play)
23+
self.bus.on("spotifyd.pause", self.on_spotify_pause)
24+
self.bus.on("spotifyd.stop", self.on_spotify_stop)
25+
self.bus.on("spotifyd.load", self.on_spotify_load)
26+
self.bus.on("spotifyd.end_of_track", self.on_spotify_end)
27+
self.bus.on("spotifyd.change", self.on_spotify_change)
28+
self.bus.on("spotifyd.preloading", self.on_spotify_preloading)
29+
30+
def reset_metadata(self):
31+
self.current_uri = None
32+
self._last_sync_ts = 0
33+
self._track_len = 0
34+
self._track_pos = 0
35+
36+
def preload_uri(self, uri: str):
37+
self.reset_metadata()
38+
self.current_uri = uri
39+
self._last_sync_ts = time.time()
40+
41+
def get_track_length(self) -> int:
42+
"""
43+
getting the duration of the audio in milliseconds
44+
"""
45+
return self._track_len or self.get_track_position()
46+
47+
def get_track_position(self) -> int:
48+
"""
49+
get current position in milliseconds
50+
"""
51+
pos = self._track_pos or 0
52+
if self._last_sync_ts: # add the elapsed time since last update of self._track_pos
53+
pos += (time.time() - self._last_sync_ts) * 1000
54+
if not self._track_len:
55+
self._track_len = pos
56+
return min(pos, self._track_len)
57+
58+
##################
59+
# spotifyd hooks
60+
def on_spotify_start(self, message: Message):
61+
self._last_sync_ts = time.time()
62+
self.current_uri = "spotify:track:" + message.data["track_id"]
63+
self._track_pos = message.data["position"] # milliseconds
64+
self.bus.emit(message.forward("ovos.common_play.media.state",
65+
{"state": 2})) # loading media
66+
67+
def on_spotify_play(self, message: Message):
68+
self._last_sync_ts = time.time()
69+
self.current_uri = "spotify:track:" + message.data["track_id"]
70+
self._track_len = message.data["duration"] # milliseconds
71+
self._track_pos = message.data["position"] # milliseconds
72+
self.bus.emit(message.forward("ovos.common_play.player.state",
73+
{"state": 1})) # playing
74+
self.bus.emit(message.forward("ovos.common_play.media.state",
75+
{"state": 6})) # buffered media
76+
if self.track_start_callback is not None:
77+
self.track_start_callback(self.current_uri)
78+
79+
def on_spotify_stop(self, message: Message):
80+
self.current_uri = None
81+
self._track_len = 0
82+
self._track_pos = 0
83+
self._last_sync_ts = 0
84+
self.bus.emit(message.forward("ovos.common_play.media.state",
85+
{"state": 1})) # no media
86+
self.bus.emit(message.forward("ovos.common_play.player.state",
87+
{"state": 0})) # stopped
88+
89+
def on_spotify_pause(self, message: Message):
90+
self.current_uri = "spotify:track:" + message.data["track_id"]
91+
self._track_len = message.data["duration"] # milliseconds
92+
self._track_pos = message.data["position"] # milliseconds
93+
self._last_sync_ts = 0
94+
self.bus.emit(message.forward("ovos.common_play.player.state",
95+
{"state": 2})) # paused
96+
97+
def on_spotify_load(self, message: Message):
98+
self._last_sync_ts = time.time()
99+
self._track_pos = message.data["position"] # milliseconds
100+
self.current_uri = "spotify:track:" + message.data["track_id"]
101+
102+
def on_spotify_preloading(self, message: Message):
103+
# when track is about to end we get info about next song
104+
# we could show a "coming up next" popup
105+
pass
106+
107+
def on_spotify_end(self, message: Message):
108+
self._track_pos = self._track_len
109+
self.bus.emit(message.forward("ovos.common_play.player.state",
110+
{"state": 2})) # paused
111+
self.bus.emit(message.forward("ovos.common_play.media.state",
112+
{"state": 7})) # end of media
113+
if self.track_end_callback is not None:
114+
self.track_end_callback(self.current_uri)
115+
116+
def on_spotify_change(self, message: Message):
117+
self.current_uri = "spotify:track:" + message.data["track_id"]
118+
self._last_sync_ts = time.time()
119+
self.bus.emit(message.forward("ovos.common_play.player.state",
120+
{"state": 2})) # paused
121+
self.bus.emit(message.forward("ovos.common_play.media.state",
122+
{"state": 2})) # loading media

0 commit comments

Comments
 (0)