Skip to content

Commit 8d09aa5

Browse files
committed
branding; bugs; tests
1 parent 5138bc2 commit 8d09aa5

File tree

3 files changed

+842
-24
lines changed

3 files changed

+842
-24
lines changed

nowplaying/inputs/remote.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import nowplaying.config
1717
from PySide6.QtCore import QSettings
1818

19+
1920
class Plugin(InputPlugin): # pylint: disable=too-many-instance-attributes
2021
''' handler for NowPlaying '''
2122

@@ -26,8 +27,8 @@ def __init__(self,
2627
self.displayname = "Remote"
2728
# Set default path
2829
default_path = pathlib.Path(
29-
QStandardPaths.standardLocations(
30-
QStandardPaths.CacheLocation)[0]).joinpath('remotedb').joinpath("remote.db")
30+
QStandardPaths.standardLocations(
31+
QStandardPaths.CacheLocation)[0]).joinpath('remotedb').joinpath("remote.db")
3132

3233
# Use configured path if available, otherwise use default
3334
if self.config and self.config.cparser.value('remote/remotedb'):

nowplaying/inputs/stagelinq.py

Lines changed: 87 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -34,32 +34,45 @@ class DeckInfo():
3434
playing: bool = False
3535

3636
def __post_init__(self):
37+
"""Set updated timestamp if not provided."""
3738
if not self.updated:
3839
self.updated = datetime.datetime.now(tz=datetime.timezone.utc)
3940

4041
def __lt__(self, other: "DeckInfo") -> bool:
42+
"""Compare DeckInfo instances by updated timestamp for sorting."""
4143
return self.updated < other.updated
4244

4345
def copy(self) -> "DeckInfo":
46+
"""Create a copy of this DeckInfo instance."""
4447
return dataclasses.replace(self)
4548

4649
def same_content(self, other: "DeckInfo") -> bool:
50+
"""Check if this deck has the same track and artist as another deck."""
4751
if self.track == other.track and self.artist == other.artist:
4852
return True
4953
return False
5054

5155

5256
class StagelinqHandler():
53-
""" stagelinq server """
57+
""" StagelinQ server """
5458

5559
def __init__(self, event: asyncio.Event):
60+
"""Initialize the StagelinQ handler.
61+
62+
Args:
63+
event: Asyncio event used to signal shutdown
64+
"""
5665
self.event = event
5766
self.device: AsyncDevice | None = None
5867
self.loop_task: asyncio.Task | None = None
5968
self.decks: dict[int, DeckInfo] = {}
6069

6170
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+
"""
6376
config = DiscoveryConfig(discovery_timeout=3.0)
6477
while not self.event.is_set() and self.device is None:
6578
try:
@@ -80,6 +93,12 @@ async def get_device(self):
8093
logging.exception("Discovery error: %s", e)
8194

8295
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+
"""
83102

84103
# Connect to device with retry loop
85104
while not self.event.is_set():
@@ -139,27 +158,46 @@ async def loop(self):
139158
await asyncio.sleep(5)
140159

141160
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+
"""
143167
deck_num = next((i for i in range(1, 5) if f"Deck{i}" in state.name), None)
144168
if deck_num is None:
145169
return
146170

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+
147175
# Update deck information based on state type using typed values
148176
if "ArtistName" in state.name:
149177
temp_decks[deck_num].artist = state.get_typed_value() or ""
150178
elif "SongName" in state.name:
151179
temp_decks[deck_num].track = state.get_typed_value() or ""
152180
elif "CurrentBPM" in state.name:
153181
# 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()
155183
elif "PlayState" in state.name:
156184
# Boolean states are already properly typed
157185
temp_decks[deck_num].playing = state.get_typed_value()
158186

159187
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+
"""
160193
this_update = datetime.datetime.now(tz=datetime.timezone.utc)
161194
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:
163201
del self.decks[deck_num]
164202
elif self.decks.get(deck_num) is None:
165203
self.decks[deck_num] = temp_decks[deck_num].copy()
@@ -169,15 +207,25 @@ def update_current_tracks(self, temp_decks: dict[int, DeckInfo]):
169207
self.decks[deck_num].updated = this_update
170208

171209
async def start(self):
210+
"""Start the StagelinQ handler by creating the main loop task."""
172211
self.loop_task = asyncio.create_task(self.loop())
173212

174213
async def stop(self):
214+
"""Stop the StagelinQ handler and cancel the main loop task."""
175215
self.event.set()
176-
logging.info("Shutting down Stagelinq")
216+
logging.info("Shutting down StagelinQ")
177217
if self.loop_task:
178218
self.loop_task.cancel()
179219

180220
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+
"""
181229
sorted_decks = sorted(self.decks.values(), key=lambda deck: deck.updated)
182230

183231
if not sorted_decks:
@@ -194,7 +242,7 @@ def __init__(self,
194242
config: "nowplaying.config.ConfigFile | None" = None,
195243
qsettings: "QWidget | None" = None):
196244
super().__init__(config=config, qsettings=qsettings)
197-
self.displayname = "Stagelinq"
245+
self.displayname = "StagelinQ"
198246
self.url: str | None = None
199247
self.mixmode = "newest"
200248
self.testmode = False
@@ -204,67 +252,84 @@ def __init__(self,
204252
#### Additional UI method
205253

206254
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')
209257

210258
#### Autoinstallation methods ####
211259

212260
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."""
214262
return False
215263

216264
#### Mix Mode menu item methods
217265

218266
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."""
220268
return ['newest', 'oldest']
221269

222270
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+
"""
224279
if mixmode not in ['newest', 'oldest']:
225280
mixmode = self.config.cparser.value('stagelinq/mixmode')
226281

227282
self.config.cparser.setValue('stagelinq/mixmode', mixmode)
228283
return mixmode
229284

230285
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."""
232287
return self.config.cparser.value('stagelinq/mixmode')
233288

234289
#### Data feed methods
235290

236291
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+
"""
238297
if not self.handler:
239298
return None
240299

241300
deck = await self.handler.get_track(mixmode=self.mixmode)
242301
metadata: TrackMetadata = {}
243302
if not deck:
244303
return metadata
245-
if deck.artist:
304+
if deck.artist is not None:
246305
metadata["artist"] = deck.artist
247-
if deck.track:
306+
if deck.track is not None:
248307
metadata["track"] = deck.track
249-
if deck.bpm:
308+
if deck.bpm is not None:
250309
metadata["bpm"] = str(deck.bpm)
251310
return metadata
252311

253312
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+
"""
255321
raise NotImplementedError
256322

257323

258324
#### Control methods
259325

260326
async def start(self) -> None:
261-
''' any initialization before actual polling starts '''
327+
"""Initialize the StagelinQ handler and start listening for devices."""
262328
self.handler = StagelinqHandler(event=self.event)
263329
await self.handler.start()
264330

265331
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()
268334
if self.handler:
269-
self.event.set()
270335
await self.handler.stop()

0 commit comments

Comments
 (0)