@@ -34,32 +34,45 @@ class DeckInfo():
34
34
playing : bool = False
35
35
36
36
def __post_init__ (self ):
37
+ """Set updated timestamp if not provided."""
37
38
if not self .updated :
38
39
self .updated = datetime .datetime .now (tz = datetime .timezone .utc )
39
40
40
41
def __lt__ (self , other : "DeckInfo" ) -> bool :
42
+ """Compare DeckInfo instances by updated timestamp for sorting."""
41
43
return self .updated < other .updated
42
44
43
45
def copy (self ) -> "DeckInfo" :
46
+ """Create a copy of this DeckInfo instance."""
44
47
return dataclasses .replace (self )
45
48
46
49
def same_content (self , other : "DeckInfo" ) -> bool :
50
+ """Check if this deck has the same track and artist as another deck."""
47
51
if self .track == other .track and self .artist == other .artist :
48
52
return True
49
53
return False
50
54
51
55
52
56
class StagelinqHandler ():
53
- """ stagelinq server """
57
+ """ StagelinQ server """
54
58
55
59
def __init__ (self , event : asyncio .Event ):
60
+ """Initialize the StagelinQ handler.
61
+
62
+ Args:
63
+ event: Asyncio event used to signal shutdown
64
+ """
56
65
self .event = event
57
66
self .device : AsyncDevice | None = None
58
67
self .loop_task : asyncio .Task | None = None
59
68
self .decks : dict [int , DeckInfo ] = {}
60
69
61
70
async def get_device (self ):
62
- """ find a device """
71
+ """Discover and connect to a StagelinQ device.
72
+
73
+ Continuously searches for StagelinQ devices until one is found
74
+ or the event is set to stop searching.
75
+ """
63
76
config = DiscoveryConfig (discovery_timeout = 3.0 )
64
77
while not self .event .is_set () and self .device is None :
65
78
try :
@@ -80,6 +93,12 @@ async def get_device(self):
80
93
logging .exception ("Discovery error: %s" , e )
81
94
82
95
async def loop (self ):
96
+ """Main loop that maintains connection to StagelinQ device and processes updates.
97
+
98
+ This method handles device discovery, connection management, and subscribes
99
+ to track state updates from all decks. It automatically reconnects if
100
+ the connection is lost.
101
+ """
83
102
84
103
# Connect to device with retry loop
85
104
while not self .event .is_set ():
@@ -139,27 +158,46 @@ async def loop(self):
139
158
await asyncio .sleep (5 )
140
159
141
160
def process_state_update (self , temp_decks : dict [int , DeckInfo ], state : "State" ):
142
- """Process a state update and update deck information."""
161
+ """Process a state update and update deck information.
162
+
163
+ Args:
164
+ temp_decks: Dictionary of deck information being built during this update cycle
165
+ state: StagelinQ state object containing updated deck information
166
+ """
143
167
deck_num = next ((i for i in range (1 , 5 ) if f"Deck{ i } " in state .name ), None )
144
168
if deck_num is None :
145
169
return
146
170
171
+ # Initialize deck if it doesn't exist
172
+ if deck_num not in temp_decks :
173
+ temp_decks [deck_num ] = DeckInfo (updated = datetime .datetime .now (tz = datetime .timezone .utc ))
174
+
147
175
# Update deck information based on state type using typed values
148
176
if "ArtistName" in state .name :
149
177
temp_decks [deck_num ].artist = state .get_typed_value () or ""
150
178
elif "SongName" in state .name :
151
179
temp_decks [deck_num ].track = state .get_typed_value () or ""
152
180
elif "CurrentBPM" in state .name :
153
181
# BPM values are already properly typed as float
154
- temp_decks [deck_num ].bpm = state .get_typed_value () or 0.0
182
+ temp_decks [deck_num ].bpm = state .get_typed_value ()
155
183
elif "PlayState" in state .name :
156
184
# Boolean states are already properly typed
157
185
temp_decks [deck_num ].playing = state .get_typed_value ()
158
186
159
187
def update_current_tracks (self , temp_decks : dict [int , DeckInfo ]):
188
+ """Update the current deck states with new information.
189
+
190
+ Args:
191
+ temp_decks: Dictionary of updated deck information from the latest state updates
192
+ """
160
193
this_update = datetime .datetime .now (tz = datetime .timezone .utc )
161
194
for deck_num in range (1 , 5 ):
162
- if not temp_decks [deck_num ].playing and self .decks [deck_num ]:
195
+ # If deck doesn't exist in temp_decks, remove it from self.decks
196
+ if deck_num not in temp_decks :
197
+ self .decks .pop (deck_num , None )
198
+ continue
199
+
200
+ if not temp_decks [deck_num ].playing and deck_num in self .decks :
163
201
del self .decks [deck_num ]
164
202
elif self .decks .get (deck_num ) is None :
165
203
self .decks [deck_num ] = temp_decks [deck_num ].copy ()
@@ -169,15 +207,25 @@ def update_current_tracks(self, temp_decks: dict[int, DeckInfo]):
169
207
self .decks [deck_num ].updated = this_update
170
208
171
209
async def start (self ):
210
+ """Start the StagelinQ handler by creating the main loop task."""
172
211
self .loop_task = asyncio .create_task (self .loop ())
173
212
174
213
async def stop (self ):
214
+ """Stop the StagelinQ handler and cancel the main loop task."""
175
215
self .event .set ()
176
- logging .info ("Shutting down Stagelinq " )
216
+ logging .info ("Shutting down StagelinQ " )
177
217
if self .loop_task :
178
218
self .loop_task .cancel ()
179
219
180
220
async def get_track (self , mixmode : str ) -> DeckInfo | None :
221
+ """Get the currently playing track based on the specified mix mode.
222
+
223
+ Args:
224
+ mixmode: Either "newest" or "oldest" to determine which deck to return
225
+
226
+ Returns:
227
+ DeckInfo for the selected deck, or None if no decks are playing
228
+ """
181
229
sorted_decks = sorted (self .decks .values (), key = lambda deck : deck .updated )
182
230
183
231
if not sorted_decks :
@@ -194,7 +242,7 @@ def __init__(self,
194
242
config : "nowplaying.config.ConfigFile | None" = None ,
195
243
qsettings : "QWidget | None" = None ):
196
244
super ().__init__ (config = config , qsettings = qsettings )
197
- self .displayname = "Stagelinq "
245
+ self .displayname = "StagelinQ "
198
246
self .url : str | None = None
199
247
self .mixmode = "newest"
200
248
self .testmode = False
@@ -204,67 +252,84 @@ def __init__(self,
204
252
#### Additional UI method
205
253
206
254
def desc_settingsui (self , qwidget : "QWidget" ) -> None : # pylint: disable=no-self-use
207
- ''' description of this input '''
208
- qwidget .setText ('Denon Stagelinq compatible equipment' )
255
+ """Set the description text for the settings UI."""
256
+ qwidget .setText ('Denon StagelinQ compatible equipment' )
209
257
210
258
#### Autoinstallation methods ####
211
259
212
260
def install (self ) -> bool : # pylint: disable=no-self-use
213
- ''' if a fresh install, run this '''
261
+ """Install method for fresh installations. Not required for StagelinQ."""
214
262
return False
215
263
216
264
#### Mix Mode menu item methods
217
265
218
266
def validmixmodes (self ) -> list [str ]: # pylint: disable=no-self-use
219
- ''' tell ui valid mixmodes '''
267
+ """Return the list of valid mix modes for the UI."""
220
268
return ['newest' , 'oldest' ]
221
269
222
270
def setmixmode (self , mixmode : str ) -> str : # pylint: disable=no-self-use, unused-argument
223
- ''' handle user switching the mix mode: TBD '''
271
+ """Set the mix mode for determining which deck to use.
272
+
273
+ Args:
274
+ mixmode: Either "newest" or "oldest"
275
+
276
+ Returns:
277
+ The validated mix mode that was set
278
+ """
224
279
if mixmode not in ['newest' , 'oldest' ]:
225
280
mixmode = self .config .cparser .value ('stagelinq/mixmode' )
226
281
227
282
self .config .cparser .setValue ('stagelinq/mixmode' , mixmode )
228
283
return mixmode
229
284
230
285
def getmixmode (self ) -> str : # pylint: disable=no-self-use
231
- ''' return what the current mixmode is set to '''
286
+ """Get the current mix mode setting."""
232
287
return self .config .cparser .value ('stagelinq/mixmode' )
233
288
234
289
#### Data feed methods
235
290
236
291
async def getplayingtrack (self ) -> TrackMetadata | None :
237
- ''' Get the currently playing track '''
292
+ """Get the currently playing track metadata.
293
+
294
+ Returns:
295
+ Dictionary containing track metadata (artist, track, bpm) or None if no handler
296
+ """
238
297
if not self .handler :
239
298
return None
240
299
241
300
deck = await self .handler .get_track (mixmode = self .mixmode )
242
301
metadata : TrackMetadata = {}
243
302
if not deck :
244
303
return metadata
245
- if deck .artist :
304
+ if deck .artist is not None :
246
305
metadata ["artist" ] = deck .artist
247
- if deck .track :
306
+ if deck .track is not None :
248
307
metadata ["track" ] = deck .track
249
- if deck .bpm :
308
+ if deck .bpm is not None :
250
309
metadata ["bpm" ] = str (deck .bpm )
251
310
return metadata
252
311
253
312
async def getrandomtrack (self , playlist : str ) -> str | None :
254
- ''' Get a file associated with a playlist, crate, whatever '''
313
+ """Get a random track from a playlist.
314
+
315
+ Args:
316
+ playlist: Name of the playlist (not implemented for StagelinQ)
317
+
318
+ Raises:
319
+ NotImplementedError: This method is not supported for StagelinQ
320
+ """
255
321
raise NotImplementedError
256
322
257
323
258
324
#### Control methods
259
325
260
326
async def start (self ) -> None :
261
- ''' any initialization before actual polling starts '''
327
+ """Initialize the StagelinQ handler and start listening for devices."""
262
328
self .handler = StagelinqHandler (event = self .event )
263
329
await self .handler .start ()
264
330
265
331
async def stop (self ) -> None :
266
- ''' stopping either the entire program or just this
267
- input '''
332
+ """Stop the StagelinQ handler and clean up resources."""
333
+ self . event . set ()
268
334
if self .handler :
269
- self .event .set ()
270
335
await self .handler .stop ()
0 commit comments