1
- import ctypes
2
1
import logging
3
2
from settings import DEBUG
4
3
from typing import Any ,Callable
11
10
import io
12
11
13
12
from core .utils .utilities import Singleton
14
- from core .utils .win32 .system_function import KEYEVENTF_EXTENDEDKEY ,KEYEVENTF_KEYUP
15
13
16
14
from winsdk .windows .media .control import (GlobalSystemMediaTransportControlsSessionManager as SessionManager ,
17
15
GlobalSystemMediaTransportControlsSession as Session ,
@@ -98,12 +96,19 @@ def _run_setup(self):
98
96
# Manually trigger the callback on startup
99
97
self ._on_current_session_changed (self ._session_manager ,None ,is_setup = True )
100
98
101
- def _on_current_session_changed (self ,manager :SessionManager ,args :SessionsChangedEventArgs ,is_setup = False ):
99
+ def _on_current_session_changed (
100
+ self ,
101
+ manager :SessionManager ,
102
+ args :SessionsChangedEventArgs ,
103
+ is_setup = False ,
104
+ is_overridden = False ,
105
+ ):
102
106
if DEBUG :
103
107
self ._log .debug ('MediaCallback: _on_current_session_changed' )
104
108
105
109
with self ._current_session_lock :
106
- self ._current_session = manager .get_current_session ()
110
+ if not is_overridden :
111
+ self ._current_session = manager .get_current_session ()
107
112
108
113
if self ._current_session is not None :
109
114
@@ -121,7 +126,19 @@ def _on_current_session_changed(self, manager: SessionManager, args: SessionsCha
121
126
122
127
for callback in callbacks :
123
128
callback (self ._current_session is not None )
129
+
130
+ def _current_session_only (fn ):
131
+ """
132
+ Decorator to ensure that the function is only called if the session is the same as the current session
133
+ """
124
134
135
+ def wrapper (self :"WindowsMedia" ,session :Session ,* args ,** kwargs ):
136
+ with self ._current_session_lock :
137
+ if self ._are_same_sessions (session ,self ._current_session ):
138
+ return fn (self ,session ,* args ,** kwargs )
139
+ return wrapper
140
+
141
+ @_current_session_only
125
142
def _on_playback_info_changed (self ,session :Session ,args :PlaybackInfoChangedEventArgs ):
126
143
if DEBUG :
127
144
self ._log .info ('MediaCallback: _on_playback_info_changed' )
@@ -136,6 +153,7 @@ def _on_playback_info_changed(self, session: Session, args: PlaybackInfoChangedE
136
153
for callback in callbacks :
137
154
callback (self ._playback_info )
138
155
156
+ @_current_session_only
139
157
def _on_timeline_properties_changed (self ,session :Session ,args :TimelinePropertiesChangedEventArgs ):
140
158
if DEBUG :
141
159
self ._log .info ('MediaCallback: _on_timeline_properties_changed' )
@@ -150,18 +168,25 @@ def _on_timeline_properties_changed(self, session: Session, args: TimelineProper
150
168
for callback in callbacks :
151
169
callback (self ._timeline_info )
152
170
171
+ @_current_session_only
153
172
def _on_media_properties_changed (self ,session :Session ,args :MediaPropertiesChangedEventArgs ):
154
173
if DEBUG :
155
174
self ._log .debug ('MediaCallback: _on_media_properties_changed' )
156
175
try :
157
- asyncio .get_event_loop ()
176
+ # Only for the initial timer based update, because it is called from an event loop
177
+ asyncio .create_task (self ._update_media_properties (session ))
158
178
except RuntimeError :
159
179
with self ._media_info_lock :
160
180
self ._event_loop .run_until_complete (self ._update_media_properties (session ))
161
- else :
162
- # Only for the initial timer based update, because it is called from an event loop
163
- asyncio .create_task (self ._update_media_properties (session ))
164
181
182
+ if self ._media_info and self ._is_media_info_empty (self ._media_info ):
183
+ sessions = self ._session_manager .get_sessions ()
184
+
185
+ # If current session isn't in the list of sessions, switch the session
186
+ if not any (self ._are_same_sessions (sessions [i ],self ._current_session )for i in range (sessions .size )):
187
+ self .switch_session (1 )
188
+
189
+ @_current_session_only
165
190
async def _update_media_properties (self ,session :Session ):
166
191
if DEBUG :
167
192
self ._log .debug ('MediaCallback: Attempting media info update' )
@@ -171,14 +196,9 @@ async def _update_media_properties(self, session: Session):
171
196
172
197
media_info = self ._properties_2_dict (media_info )
173
198
174
- # Skip initial change calls where the thumbnail is None. This prevents processing multiple updates.
175
- # Might prevent showing info for no-thumbnail media
176
- if media_info ['thumbnail' ]is None :
177
- if DEBUG :
178
- self ._log .debug ('MediaCallback: Skipping media info update: no thumbnail' )
179
- return
199
+ if media_info ['thumbnail' ]is not None :
200
+ media_info ['thumbnail' ]= await self .get_thumbnail (media_info ['thumbnail' ])
180
201
181
- media_info ['thumbnail' ]= await self .get_thumbnail (media_info ['thumbnail' ])
182
202
except Exception as e :
183
203
self ._log .error (f'MediaCallback: Error occurred whilst fetching media properties and thumbnail:{ e } ' )
184
204
return
@@ -228,21 +248,48 @@ async def get_thumbnail(thumbnail_stream_reference: IRandomAccessStreamReference
228
248
finally :
229
249
# Close the stream
230
250
readable_stream .close ()
231
-
251
+
232
252
@staticmethod
233
- def play_pause ():
234
- user32 = ctypes .windll .user32
235
- user32 .keybd_event (VK_MEDIA_PLAY_PAUSE ,0 ,KEYEVENTF_EXTENDEDKEY ,0 )
236
- user32 .keybd_event (VK_MEDIA_PLAY_PAUSE ,0 ,KEYEVENTF_KEYUP ,0 )
253
+ def _is_media_info_empty (media_info :dict [str ,Any ])-> bool :
254
+ keys = ['album_artist' ,'album_title' ,'album_track_count' ,'artist' ,'playback_type' ,'subtitle' ,'title' ,'track_number' ]
255
+ # Check if all keys have 'zero' values
256
+ return all (not media_info .get (key )for key in keys )
257
+
258
+ def _are_same_sessions (self ,session1 :Session ,session2 :Session )-> bool :
259
+ return session1 .source_app_user_model_id == session2 .source_app_user_model_id
260
+
261
+ def switch_session (self ,direction :int ):
262
+ sessions = self ._session_manager .get_sessions ()
263
+ if len (sessions )== 0 :
264
+ return
237
265
238
- @staticmethod
239
- def prev ():
240
- user32 = ctypes .windll .user32
241
- user32 .keybd_event (VK_MEDIA_PREV_TRACK ,0 ,KEYEVENTF_EXTENDEDKEY ,0 )
242
- user32 .keybd_event (VK_MEDIA_PREV_TRACK ,0 ,KEYEVENTF_KEYUP ,0 )
266
+ with self ._current_session_lock :
267
+ current_session_idx = - 1
268
+ for i ,session in enumerate (sessions ):
269
+ if self ._current_session is None or self ._are_same_sessions (session ,self ._current_session ):
270
+ current_session_idx = i
271
+ break
272
+
273
+ idx = (current_session_idx + direction )% len (sessions )
274
+ if self ._are_same_sessions (sessions [idx ],self ._current_session ):
275
+ return
276
+ if DEBUG :
277
+ self ._log .info (f"Switching to session{ idx } ({ sessions [idx ].source_app_user_model_id } )" )
278
+ self ._current_session = sessions [idx ]
243
279
244
- @staticmethod
245
- def next ():
246
- user32 = ctypes .windll .user32
247
- user32 .keybd_event (VK_MEDIA_NEXT_TRACK ,0 ,KEYEVENTF_EXTENDEDKEY ,0 )
248
- user32 .keybd_event (VK_MEDIA_NEXT_TRACK ,0 ,KEYEVENTF_KEYUP ,0 )
280
+ self ._on_current_session_changed (self ._session_manager ,None ,is_overridden = True )
281
+
282
+ def play_pause (self ):
283
+ with self ._current_session_lock :
284
+ if self ._current_session is not None :
285
+ self ._current_session .try_toggle_play_pause_async ()
286
+
287
+ def prev (self ):
288
+ with self ._current_session_lock :
289
+ if self ._current_session is not None :
290
+ self ._current_session .try_skip_previous_async ()
291
+
292
+ def next (self ):
293
+ with self ._current_session_lock :
294
+ if self ._current_session is not None :
295
+ self ._current_session .try_skip_next_async ()