Skip to content

Commit f93dda8

Browse files
committed
feat(media): add session switching functionality
1 parent e3e3fdd commit f93dda8

File tree

2 files changed

+75
-28
lines changed

2 files changed

+75
-28
lines changed

src/core/utils/win32/media.py

+60-24
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,19 @@ def _run_setup(self):
9898
# Manually trigger the callback on startup
9999
self._on_current_session_changed(self._session_manager, None, is_setup=True)
100100

101-
def _on_current_session_changed(self, manager: SessionManager, args: SessionsChangedEventArgs, is_setup=False):
101+
def _on_current_session_changed(
102+
self,
103+
manager: SessionManager,
104+
args: SessionsChangedEventArgs,
105+
is_setup=False,
106+
is_overridden=False,
107+
):
102108
if DEBUG:
103109
self._log.debug('MediaCallback: _on_current_session_changed')
104110

105111
with self._current_session_lock:
106-
self._current_session = manager.get_current_session()
112+
if not is_overridden:
113+
self._current_session = manager.get_current_session()
107114

108115
if self._current_session is not None:
109116

@@ -121,7 +128,19 @@ def _on_current_session_changed(self, manager: SessionManager, args: SessionsCha
121128

122129
for callback in callbacks:
123130
callback(self._current_session is not None)
131+
132+
def _current_session_only(fn):
133+
"""
134+
Decorator to ensure that the function is only called if the session is the same as the current session
135+
"""
136+
137+
def wrapper(self: "WindowsMedia", session: Session, *args, **kwargs):
138+
with self._current_session_lock:
139+
if self._are_same_sessions(session, self._current_session):
140+
return fn(self, session, *args, **kwargs)
141+
return wrapper
124142

143+
@_current_session_only
125144
def _on_playback_info_changed(self, session: Session, args: PlaybackInfoChangedEventArgs):
126145
if DEBUG:
127146
self._log.info('MediaCallback: _on_playback_info_changed')
@@ -136,6 +155,7 @@ def _on_playback_info_changed(self, session: Session, args: PlaybackInfoChangedE
136155
for callback in callbacks:
137156
callback(self._playback_info)
138157

158+
@_current_session_only
139159
def _on_timeline_properties_changed(self, session: Session, args: TimelinePropertiesChangedEventArgs):
140160
if DEBUG:
141161
self._log.info('MediaCallback: _on_timeline_properties_changed')
@@ -150,6 +170,7 @@ def _on_timeline_properties_changed(self, session: Session, args: TimelineProper
150170
for callback in callbacks:
151171
callback(self._timeline_info)
152172

173+
@_current_session_only
153174
def _on_media_properties_changed(self, session: Session, args: MediaPropertiesChangedEventArgs):
154175
if DEBUG:
155176
self._log.debug('MediaCallback: _on_media_properties_changed')
@@ -162,6 +183,7 @@ def _on_media_properties_changed(self, session: Session, args: MediaPropertiesCh
162183
# Only for the initial timer based update, because it is called from an event loop
163184
asyncio.create_task(self._update_media_properties(session))
164185

186+
@_current_session_only
165187
async def _update_media_properties(self, session: Session):
166188
if DEBUG:
167189
self._log.debug('MediaCallback: Attempting media info update')
@@ -171,14 +193,9 @@ async def _update_media_properties(self, session: Session):
171193

172194
media_info = self._properties_2_dict(media_info)
173195

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
196+
if media_info['thumbnail'] is not None:
197+
media_info['thumbnail'] = await self.get_thumbnail(media_info['thumbnail'])
180198

181-
media_info['thumbnail'] = await self.get_thumbnail(media_info['thumbnail'])
182199
except Exception as e:
183200
self._log.error(f'MediaCallback: Error occurred whilst fetching media properties and thumbnail: {e}')
184201
return
@@ -228,21 +245,40 @@ async def get_thumbnail(thumbnail_stream_reference: IRandomAccessStreamReference
228245
finally:
229246
# Close the stream
230247
readable_stream.close()
248+
249+
def _are_same_sessions(self, session1: Session, session2: Session) -> bool:
250+
return session1.source_app_user_model_id == session2.source_app_user_model_id
251+
252+
def switch_session(self, direction: int):
253+
sessions = self._session_manager.get_sessions()
254+
if len(sessions) == 0:
255+
return
231256

232-
@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)
257+
with self._current_session_lock:
258+
current_session_idx = -1
259+
for i, session in enumerate(sessions):
260+
if self._current_session is None or self._are_same_sessions(session, self._current_session):
261+
current_session_idx = i
262+
break
263+
264+
idx = (current_session_idx + direction) % len(sessions)
265+
if self._are_same_sessions(sessions[idx], self._current_session):
266+
return
267+
self._log.info(f"Switching to session {idx} ({sessions[idx].source_app_user_model_id})")
268+
self._current_session = sessions[idx]
269+
self._on_current_session_changed(self._session_manager, None, is_overridden=True)
237270

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)
271+
def play_pause(self):
272+
with self._current_session_lock:
273+
if self._current_session is not None:
274+
self._current_session.try_toggle_play_pause_async()
243275

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)
276+
def prev(self):
277+
with self._current_session_lock:
278+
if self._current_session is not None:
279+
self._current_session.try_skip_previous_async()
280+
281+
def next(self):
282+
with self._current_session_lock:
283+
if self._current_session is not None:
284+
self._current_session.try_skip_next_async()

src/core/widgets/yasb/media.py

+15-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from PIL.ImageQt import QPixmap
77
from PyQt6 import QtCore
88
from PyQt6.QtCore import Qt
9+
from PyQt6.QtGui import QWheelEvent
910
from PIL.ImageQt import ImageQt
1011
from winsdk.windows.media.control import GlobalSystemMediaTransportControlsSessionPlaybackInfo
1112

@@ -193,6 +194,11 @@ def _on_media_properties_changed(self, media_info: Optional[dict[str, Any]]):
193194
if not self._show_thumbnail:
194195
return
195196

197+
# If no media in session, hide thumbnail and stop here
198+
if media_info['thumbnail'] is None and not media_info['title']:
199+
self._thumbnail_label.hide()
200+
return
201+
196202
# Only update the thumbnail if the title/artist changes or if we did a toggle (resize)
197203
try:
198204
if media_info['thumbnail'] is not None:
@@ -248,16 +254,21 @@ def _create_media_button(self, icon, action):
248254
return label
249255

250256
def _create_media_buttons(self):
251-
return (self._create_media_button(self._media_button_icons['prev_track'], WindowsMedia.prev),
252-
self._create_media_button(
253-
self._media_button_icons['play'], WindowsMedia.play_pause), self._create_media_button(
254-
self._media_button_icons['next_track'], WindowsMedia.next))
257+
return (self._create_media_button(self._media_button_icons['prev_track'], WindowsMedia().prev),
258+
self._create_media_button(self._media_button_icons['play'], WindowsMedia().play_pause),
259+
self._create_media_button( self._media_button_icons['next_track'], WindowsMedia().next))
255260

256261
def execute_code(self, func):
257262
try:
258263
func()
259264
except Exception as e:
260265
logging.error(f"Error executing code: {e}")
266+
267+
def wheelEvent(self, event: QWheelEvent):
268+
if event.angleDelta().y() > 0:
269+
self.media.switch_session(+1) # Next
270+
elif event.angleDelta().y() < 0:
271+
self.media.switch_session(-1) # Prev
261272

262273
class ClickableLabel(QLabel):
263274
def __init__(self, parent=None):

0 commit comments

Comments
 (0)