re-run through ruff formatter

add hatch+ruff badges
This commit is contained in:
Onyx and Iris 2025-01-17 20:43:34 +00:00
parent 3c979b8391
commit 5b4f3753db
11 changed files with 230 additions and 219 deletions

View File

@ -1,7 +1,7 @@
# duckypad twitch # duckypad twitch
[![PyPI - Version](https://img.shields.io/pypi/v/duckypad-twitch.svg)](https://pypi.org/project/duckypad-twitch) [![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg)](https://github.com/pypa/hatch)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/duckypad-twitch.svg)](https://pypi.org/project/duckypad-twitch) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
--- ---

View File

@ -7,71 +7,69 @@ import xair_api
import duckypad_twitch import duckypad_twitch
from duckypad_twitch import configuration from duckypad_twitch import configuration
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.INFO)
def register_hotkeys(duckypad): def register_hotkeys(duckypad):
def audio_hotkeys(): def audio_hotkeys():
keyboard.add_hotkey("F13", duckypad.audio.mute_mics) keyboard.add_hotkey('F13', duckypad.audio.mute_mics)
keyboard.add_hotkey("F14", duckypad.audio.only_discord) keyboard.add_hotkey('F14', duckypad.audio.only_discord)
keyboard.add_hotkey("F15", duckypad.audio.only_stream) keyboard.add_hotkey('F15', duckypad.audio.only_stream)
keyboard.add_hotkey("F16", duckypad.audio.sound_test) keyboard.add_hotkey('F16', duckypad.audio.sound_test)
keyboard.add_hotkey("F17", duckypad.audio.solo_onyx) keyboard.add_hotkey('F17', duckypad.audio.solo_onyx)
keyboard.add_hotkey("F18", duckypad.audio.solo_iris) keyboard.add_hotkey('F18', duckypad.audio.solo_iris)
keyboard.add_hotkey("F19", duckypad.audio.toggle_workstation_to_onyx) keyboard.add_hotkey('F19', duckypad.audio.toggle_workstation_to_onyx)
def scene_hotkeys(): def scene_hotkeys():
keyboard.add_hotkey("ctrl+F13", duckypad.scene.onyx_only) keyboard.add_hotkey('ctrl+F13', duckypad.scene.onyx_only)
keyboard.add_hotkey("ctrl+F14", duckypad.scene.iris_only) keyboard.add_hotkey('ctrl+F14', duckypad.scene.iris_only)
keyboard.add_hotkey("ctrl+F15", duckypad.scene.dual_scene) keyboard.add_hotkey('ctrl+F15', duckypad.scene.dual_scene)
keyboard.add_hotkey("ctrl+F16", duckypad.scene.onyx_big) keyboard.add_hotkey('ctrl+F16', duckypad.scene.onyx_big)
keyboard.add_hotkey("ctrl+F17", duckypad.scene.iris_big) keyboard.add_hotkey('ctrl+F17', duckypad.scene.iris_big)
keyboard.add_hotkey("ctrl+F18", duckypad.scene.start) keyboard.add_hotkey('ctrl+F18', duckypad.scene.start)
keyboard.add_hotkey("ctrl+F19", duckypad.scene.brb) keyboard.add_hotkey('ctrl+F19', duckypad.scene.brb)
keyboard.add_hotkey("ctrl+F20", duckypad.scene.end) keyboard.add_hotkey('ctrl+F20', duckypad.scene.end)
def obsws_hotkeys(): def obsws_hotkeys():
keyboard.add_hotkey("ctrl+alt+F13", duckypad.obsws.start) keyboard.add_hotkey('ctrl+alt+F13', duckypad.obsws.start)
keyboard.add_hotkey("ctrl+alt+F14", duckypad.obsws.brb) keyboard.add_hotkey('ctrl+alt+F14', duckypad.obsws.brb)
keyboard.add_hotkey("ctrl+alt+F15", duckypad.obsws.end) keyboard.add_hotkey('ctrl+alt+F15', duckypad.obsws.end)
keyboard.add_hotkey("ctrl+alt+F16", duckypad.obsws.live) keyboard.add_hotkey('ctrl+alt+F16', duckypad.obsws.live)
keyboard.add_hotkey("ctrl+alt+F17", duckypad.obsws.toggle_mute_mic) keyboard.add_hotkey('ctrl+alt+F17', duckypad.obsws.toggle_mute_mic)
keyboard.add_hotkey("ctrl+alt+F18", duckypad.obsws.toggle_stream) keyboard.add_hotkey('ctrl+alt+F18', duckypad.obsws.toggle_stream)
def streamlabs_controller_hotkeys(): def streamlabs_controller_hotkeys():
keyboard.add_hotkey("ctrl+F22", duckypad.streamlabs_controller.begin_stream) keyboard.add_hotkey('ctrl+F22', duckypad.streamlabs_controller.begin_stream)
keyboard.add_hotkey("ctrl+F23", duckypad.streamlabs_controller.end_stream) keyboard.add_hotkey('ctrl+F23', duckypad.streamlabs_controller.end_stream)
keyboard.add_hotkey( keyboard.add_hotkey('ctrl+alt+F23', duckypad.streamlabs_controller.launch, args=(10,))
"ctrl+alt+F23", duckypad.streamlabs_controller.launch, args=(10,) keyboard.add_hotkey('ctrl+alt+F24', duckypad.streamlabs_controller.shutdown)
)
keyboard.add_hotkey("ctrl+alt+F24", duckypad.streamlabs_controller.shutdown)
def duckypad_hotkeys(): def duckypad_hotkeys():
keyboard.add_hotkey("ctrl+F21", duckypad.reset) keyboard.add_hotkey('ctrl+F21', duckypad.reset)
steps = ( for step in (
audio_hotkeys, audio_hotkeys,
scene_hotkeys, scene_hotkeys,
obsws_hotkeys, obsws_hotkeys,
streamlabs_controller_hotkeys, streamlabs_controller_hotkeys,
duckypad_hotkeys, duckypad_hotkeys,
) ):
[step() for step in steps] step()
def main(): def main():
xair_config = configuration.get("xair") xair_config = configuration.get('xair')
with voicemeeterlib.api("potato") as vm: with voicemeeterlib.api('potato') as vm:
with xair_api.connect("MR18", **xair_config) as mixer: with xair_api.connect('MR18', **xair_config) as mixer:
with duckypad_twitch.connect(vm=vm, mixer=mixer) as duckypad: with duckypad_twitch.connect(vm=vm, mixer=mixer) as duckypad:
vm.apply_config("streaming_extender") # extends the streaming config vm.apply_config('streaming_extender') # extends the streaming config
register_hotkeys(duckypad) register_hotkeys(duckypad)
print("press ctrl+m to quit") print('press ctrl+m to quit')
keyboard.wait("ctrl+m") keyboard.wait('ctrl+m')
if __name__ == "__main__": if __name__ == '__main__':
main() main()

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2023-present onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com> # SPDX-FileCopyrightText: 2023-present onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com>
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
__version__ = "1.0.2" __version__ = '1.0.3'

View File

@ -10,7 +10,7 @@ from .states import AudioState
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
Buttons = IntEnum("Buttons", "mute_mics only_discord only_stream", start=0) Buttons = IntEnum('Buttons', 'mute_mics only_discord only_stream', start=0)
class Audio(ILayer): class Audio(ILayer):
@ -46,12 +46,12 @@ class Audio(ILayer):
self.vm.strip[0].mute = True self.vm.strip[0].mute = True
self.vm.strip[1].mute = True self.vm.strip[1].mute = True
self.vm.strip[4].mute = True self.vm.strip[4].mute = True
self.logger.info("Mics Muted") self.logger.info('Mics Muted')
else: else:
self.vm.strip[0].mute = False self.vm.strip[0].mute = False
self.vm.strip[1].mute = False self.vm.strip[1].mute = False
self.vm.strip[4].mute = False self.vm.strip[4].mute = False
self.logger.info("Mics Unmuted") self.logger.info('Mics Unmuted')
self.vm.button[Buttons.mute_mics].stateonly = self.state.mute_mics self.vm.button[Buttons.mute_mics].stateonly = self.state.mute_mics
def only_discord(self): def only_discord(self):
@ -59,11 +59,11 @@ class Audio(ILayer):
if self.state.only_discord: if self.state.only_discord:
self.mixer.dca[0].on = False self.mixer.dca[0].on = False
self.vm.strip[4].mute = True self.vm.strip[4].mute = True
self.logger.info("Only Discord Enabled") self.logger.info('Only Discord Enabled')
else: else:
self.vm.strip[4].mute = False self.vm.strip[4].mute = False
self.mixer.dca[0].on = True self.mixer.dca[0].on = True
self.logger.info("Only Discord Disabled") self.logger.info('Only Discord Disabled')
self.vm.button[Buttons.only_discord].stateonly = self.state.only_discord self.vm.button[Buttons.only_discord].stateonly = self.state.only_discord
def only_stream(self): def only_stream(self):
@ -74,59 +74,57 @@ class Audio(ILayer):
self.vm.strip[2].gain = -3 self.vm.strip[2].gain = -3
self.vm.strip[3].gain = -3 self.vm.strip[3].gain = -3
self.vm.strip[6].gain = -3 self.vm.strip[6].gain = -3
self.logger.info("Only Stream Enabled") self.logger.info('Only Stream Enabled')
else: else:
self.vm.strip[2].gain = 0 self.vm.strip[2].gain = 0
self.vm.strip[3].gain = 0 self.vm.strip[3].gain = 0
self.vm.strip[6].gain = 0 self.vm.strip[6].gain = 0
self.vm.bus[5].mute = False self.vm.bus[5].mute = False
self.vm.bus[6].mute = False self.vm.bus[6].mute = False
self.logger.info("Only Stream Disabled") self.logger.info('Only Stream Disabled')
self.vm.button[Buttons.only_stream].stateonly = self.state.only_stream self.vm.button[Buttons.only_stream].stateonly = self.state.only_stream
def sound_test(self): def sound_test(self):
def toggle_soundtest(params): def toggle_soundtest(params):
onyx_conn = configuration.get("vban_onyx") onyx_conn = configuration.get('vban_onyx')
iris_conn = configuration.get("vban_iris") iris_conn = configuration.get('vban_iris')
assert all( assert all([onyx_conn, iris_conn]), 'expected configurations for onyx_conn, iris_conn'
[onyx_conn, iris_conn]
), "expected configurations for onyx_conn, iris_conn"
with vban_cmd.api("potato", outbound=True, **onyx_conn) as vban: with vban_cmd.api('potato', outbound=True, **onyx_conn) as vban:
vban.strip[0].apply(params) vban.strip[0].apply(params)
with vban_cmd.api("potato", outbound=True, **iris_conn) as vban: with vban_cmd.api('potato', outbound=True, **iris_conn) as vban:
vban.strip[0].apply(params) vban.strip[0].apply(params)
ENABLE_SOUNDTEST = { ENABLE_SOUNDTEST = {
"A1": True, 'A1': True,
"A2": True, 'A2': True,
"B1": False, 'B1': False,
"B2": False, 'B2': False,
"mono": True, 'mono': True,
} }
DISABLE_SOUNDTEST = { DISABLE_SOUNDTEST = {
"A1": False, 'A1': False,
"A2": False, 'A2': False,
"B1": True, 'B1': True,
"B2": True, 'B2': True,
"mono": False, 'mono': False,
} }
self.state.sound_test = not self.state.sound_test self.state.sound_test = not self.state.sound_test
if self.state.sound_test: if self.state.sound_test:
self.vm.strip[4].apply({"B3": False, "A1": True, "mute": False}) self.vm.strip[4].apply({'B3': False, 'A1': True, 'mute': False})
self.vm.vban.outstream[0].on = True self.vm.vban.outstream[0].on = True
self.vm.vban.outstream[1].on = True self.vm.vban.outstream[1].on = True
self.vm.vban.outstream[0].route = 0 self.vm.vban.outstream[0].route = 0
self.vm.vban.outstream[1].route = 0 self.vm.vban.outstream[1].route = 0
toggle_soundtest(ENABLE_SOUNDTEST) toggle_soundtest(ENABLE_SOUNDTEST)
self.logger.info("Sound Test Enabled") self.logger.info('Sound Test Enabled')
else: else:
toggle_soundtest(DISABLE_SOUNDTEST) toggle_soundtest(DISABLE_SOUNDTEST)
self.vm.vban.outstream[0].route = 5 self.vm.vban.outstream[0].route = 5
self.vm.vban.outstream[1].route = 6 self.vm.vban.outstream[1].route = 6
self.vm.strip[4].apply({"B3": True, "A1": False, "mute": True}) self.vm.strip[4].apply({'B3': True, 'A1': False, 'mute': True})
self.logger.info("Sound Test Disabled") self.logger.info('Sound Test Disabled')
def solo_onyx(self): def solo_onyx(self):
"""placeholder method.""" """placeholder method."""
@ -136,14 +134,14 @@ class Audio(ILayer):
def toggle_workstation_to_onyx(self): def toggle_workstation_to_onyx(self):
self.state.ws_to_onyx = not self.state.ws_to_onyx self.state.ws_to_onyx = not self.state.ws_to_onyx
onyx_conn = configuration.get("vban_onyx") onyx_conn = configuration.get('vban_onyx')
if self.state.ws_to_onyx: if self.state.ws_to_onyx:
with vban_cmd.api("potato", **onyx_conn) as vban: with vban_cmd.api('potato', **onyx_conn) as vban:
vban.vban.instream[0].on = True vban.vban.instream[0].on = True
self.vm.strip[5].gain = -6 self.vm.strip[5].gain = -6
self.vm.vban.outstream[2].on = True self.vm.vban.outstream[2].on = True
else: else:
with vban_cmd.api("potato", **onyx_conn) as vban: with vban_cmd.api('potato', **onyx_conn) as vban:
vban.vban.instream[0].on = False vban.vban.instream[0].on = False
self.vm.strip[5].gain = 0 self.vm.strip[5].gain = 0
self.vm.vban.outstream[2].on = False self.vm.vban.outstream[2].on = False

View File

@ -7,11 +7,11 @@ except ModuleNotFoundError:
configuration = {} configuration = {}
configpath = Path.cwd() / "configs" / "duckypad.toml" configpath = Path.cwd() / 'configs' / 'duckypad.toml'
if not configpath.exists(): if not configpath.exists():
raise OSError(f"unable to locate {configpath}") raise OSError(f'unable to locate {configpath}')
with open(configpath, "rb") as f: with open(configpath, 'rb') as f:
configuration = tomllib.load(f) configuration = tomllib.load(f)

View File

@ -28,32 +28,29 @@ class DuckyPad:
def __exit__(self, exc_value, exc_type, traceback): def __exit__(self, exc_value, exc_type, traceback):
self.streamlabs_controller.conn.disconnect() self.streamlabs_controller.conn.disconnect()
self.obsws.disconnect()
def reset(self): def reset(self):
''' """
apply streaming config, apply streaming config,
then apply current scene settings then apply current scene settings
if stream is live enable both mics over vban if stream is live enable both mics over vban
''' """
self.vm.apply_config("streaming") self.vm.apply_config('streaming')
self.audio.reset_states() self.audio.reset_states()
if self.stream.current_scene: if self.stream.current_scene:
self.logger.debug( self.logger.debug(f'Running function for current scene {self.stream.current_scene}')
f"Running function for current scene {self.stream.current_scene}"
)
fn = getattr( fn = getattr(
self.scene, self.scene,
"_".join([word.lower() for word in self.stream.current_scene.split()]), '_'.join([word.lower() for word in self.stream.current_scene.split()]),
) )
fn() fn()
if self.stream.is_live: if self.stream.is_live:
self.logger.debug("stream is live, enabling both mics over vban") self.logger.debug('stream is live, enabling both mics over vban')
self.vm.vban.outstream[0].on = True self.vm.vban.outstream[0].on = True
self.vm.vban.outstream[1].on = True self.vm.vban.outstream[1].on = True
else: else:
self.logger.debug( self.logger.debug('stream is not live. Leaving both vban outstreams disabled')
"stream is not live. Leaving both vban outstreams disabled"
)
def connect(*args, **kwargs): def connect(*args, **kwargs):

View File

@ -29,15 +29,15 @@ class OBSWS(ILayer):
self._state = val self._state = val
def reset_states(self): def reset_states(self):
resp = self.request.get_input_mute("Mic/Aux") resp = self.request.get_input_mute('Mic/Aux')
self.state.mute_mic = resp.input_muted self.state.mute_mic = resp.input_muted
resp = self.request.get_stream_status() resp = self.request.get_stream_status()
self._duckypad.stream.is_live = resp.output_active self._duckypad.stream.is_live = resp.output_active
def obs_connect(self): def obs_connect(self):
try: try:
conn = configuration.get("obsws") conn = configuration.get('obsws')
assert conn is not None, "expected configuration for obs" assert conn is not None, 'expected configuration for obs'
self.request = obsws.ReqClient(**conn) self.request = obsws.ReqClient(**conn)
self.reset_states() self.reset_states()
self.event = obsws.EventClient(**conn) self.event = obsws.EventClient(**conn)
@ -50,25 +50,28 @@ class OBSWS(ILayer):
] ]
) )
except (ConnectionRefusedError, TimeoutError) as e: except (ConnectionRefusedError, TimeoutError) as e:
self.logger.error(f"{type(e).__name__}: {e}") self.logger.error(f'{type(e).__name__}: {e}')
raise raise
def obs_disconnect(self):
for client in (self.request, self.event):
if client:
client.disconnect()
def on_current_program_scene_changed(self, data): def on_current_program_scene_changed(self, data):
self._duckypad.stream.current_scene = data.scene_name self._duckypad.stream.current_scene = data.scene_name
self.logger.info(f"scene switched to {self._duckypad.stream.current_scene}") self.logger.info(f'scene switched to {self._duckypad.stream.current_scene}')
if self._duckypad.stream.current_scene in ("START", "BRB", "END"): if self._duckypad.stream.current_scene in ('START', 'BRB', 'END'):
self.mute_mic_state(True) self.mute_mic_state(True)
def on_input_mute_state_changed(self, data): def on_input_mute_state_changed(self, data):
if data.input_name == "Mic/Aux": if data.input_name == 'Mic/Aux':
self.state.mute_mic = data.input_muted self.state.mute_mic = data.input_muted
self.logger.info(f"mic was {'muted' if self.state.mute_mic else 'unmuted'}") self.logger.info(f'mic was {"muted" if self.state.mute_mic else "unmuted"}')
def on_stream_state_changed(self, data): def on_stream_state_changed(self, data):
self._duckypad.stream.is_live = data.output_active self._duckypad.stream.is_live = data.output_active
self.logger.info( self.logger.info(f'stream is {"live" if self._duckypad.stream.is_live else "offline"}')
f"stream is {'live' if self._duckypad.stream.is_live else 'offline'}"
)
def on_exit_started(self, _): def on_exit_started(self, _):
self.event.unsubscribe() self.event.unsubscribe()
@ -80,22 +83,22 @@ class OBSWS(ILayer):
return resp return resp
def start(self): def start(self):
self.call("set_current_program_scene", "START") self.call('set_current_program_scene', 'START')
def brb(self): def brb(self):
self.call("set_current_program_scene", "BRB") self.call('set_current_program_scene', 'BRB')
def end(self): def end(self):
self.call("set_current_program_scene", "END") self.call('set_current_program_scene', 'END')
def live(self): def live(self):
self.call("set_current_program_scene", "LIVE") self.call('set_current_program_scene', 'LIVE')
def mute_mic_state(self, val): def mute_mic_state(self, val):
self.call("set_input_mute", "Mic/Aux", val) self.call('set_input_mute', 'Mic/Aux', val)
def toggle_mute_mic(self): def toggle_mute_mic(self):
self.call("toggle_input_mute", "Mic/Aux") self.call('toggle_input_mute', 'Mic/Aux')
def toggle_stream(self): def toggle_stream(self):
self.call("toggle_stream") self.call('toggle_stream')

View File

@ -32,49 +32,49 @@ class Scene(ILayer):
self._state = SceneState() self._state = SceneState()
def onyx_only(self): def onyx_only(self):
if self._duckypad.streamlabs_controller.switch_scene("onyx_only"): if self._duckypad.streamlabs_controller.switch_scene('onyx_only'):
self.vm.strip[2].mute = False self.vm.strip[2].mute = False
self.vm.strip[3].mute = True self.vm.strip[3].mute = True
self.logger.info("Only Onyx Scene enabled, Iris game pc muted") self.logger.info('Only Onyx Scene enabled, Iris game pc muted')
def iris_only(self): def iris_only(self):
if self._duckypad.streamlabs_controller.switch_scene("iris_only"): if self._duckypad.streamlabs_controller.switch_scene('iris_only'):
self.vm.strip[2].mute = True self.vm.strip[2].mute = True
self.vm.strip[3].mute = False self.vm.strip[3].mute = False
self.logger.info("Only Iris Scene enabled, Onyx game pc muted") self.logger.info('Only Iris Scene enabled, Onyx game pc muted')
def dual_scene(self): def dual_scene(self):
if self._duckypad.streamlabs_controller.switch_scene("dual_scene"): if self._duckypad.streamlabs_controller.switch_scene('dual_scene'):
self.vm.strip[2].apply({"mute": False, "gain": 0}) self.vm.strip[2].apply({'mute': False, 'gain': 0})
self.vm.strip[3].apply({"A5": True, "mute": False, "gain": 0}) self.vm.strip[3].apply({'A5': True, 'mute': False, 'gain': 0})
self.logger.info("Dual Scene enabled") self.logger.info('Dual Scene enabled')
def onyx_big(self): def onyx_big(self):
if self._duckypad.streamlabs_controller.switch_scene("onyx_big"): if self._duckypad.streamlabs_controller.switch_scene('onyx_big'):
self.vm.strip[2].apply({"mute": False, "gain": 0}) self.vm.strip[2].apply({'mute': False, 'gain': 0})
self.vm.strip[3].apply({"mute": False, "gain": -3}) self.vm.strip[3].apply({'mute': False, 'gain': -3})
self.logger.info("Onyx Big scene enabled") self.logger.info('Onyx Big scene enabled')
def iris_big(self): def iris_big(self):
if self._duckypad.streamlabs_controller.switch_scene("iris_big"): if self._duckypad.streamlabs_controller.switch_scene('iris_big'):
self.vm.strip[2].apply({"mute": False, "gain": -3}) self.vm.strip[2].apply({'mute': False, 'gain': -3})
self.vm.strip[3].apply({"mute": False, "gain": 0}) self.vm.strip[3].apply({'mute': False, 'gain': 0})
self.logger.info("Iris Big enabled") self.logger.info('Iris Big enabled')
def start(self): def start(self):
if self._duckypad.streamlabs_controller.switch_scene("start"): if self._duckypad.streamlabs_controller.switch_scene('start'):
self.vm.strip[2].mute = True self.vm.strip[2].mute = True
self.vm.strip[3].mute = True self.vm.strip[3].mute = True
self.logger.info("Start scene enabled.. ready to go live!") self.logger.info('Start scene enabled.. ready to go live!')
def brb(self): def brb(self):
if self._duckypad.streamlabs_controller.switch_scene("brb"): if self._duckypad.streamlabs_controller.switch_scene('brb'):
self.vm.strip[2].mute = True self.vm.strip[2].mute = True
self.vm.strip[3].mute = True self.vm.strip[3].mute = True
self.logger.info("BRB: game pcs muted") self.logger.info('BRB: game pcs muted')
def end(self): def end(self):
if self._duckypad.streamlabs_controller.switch_scene("end"): if self._duckypad.streamlabs_controller.switch_scene('end'):
self.vm.strip[2].mute = True self.vm.strip[2].mute = True
self.vm.strip[3].mute = True self.vm.strip[3].mute = True
self.logger.info("End scene enabled.") self.logger.info('End scene enabled.')

View File

@ -4,7 +4,7 @@ from dataclasses import dataclass
@dataclass @dataclass
class StreamState: class StreamState:
is_live: bool = False is_live: bool = False
current_scene: str = "" current_scene: str = ''
@dataclass @dataclass

View File

@ -31,21 +31,17 @@ class StreamlabsController:
def connect(self): def connect(self):
try: try:
conn = configuration.get("streamlabs") conn = configuration.get('streamlabs')
assert conn is not None, "expected configuration for streamlabs" assert conn is not None, 'expected configuration for streamlabs'
self.conn.connect(**conn) self.conn.connect(**conn)
except slobs_websocket.exceptions.ConnectionFailure as e: except slobs_websocket.exceptions.ConnectionFailure as e:
self.logger.error(f"{type(e).__name__}: {e}") self.logger.error(f'{type(e).__name__}: {e}')
raise raise
self._duckypad.scene.scenes = { self._duckypad.scene.scenes = {scene.name: scene.id for scene in self.conn.ScenesService.getScenes()}
scene.name: scene.id for scene in self.conn.ScenesService.getScenes() self.logger.debug(f'registered scenes: {self._duckypad.scene.scenes}')
}
self.logger.debug(f"registered scenes: {self._duckypad.scene.scenes}")
self.conn.ScenesService.sceneSwitched += self.on_scene_switched self.conn.ScenesService.sceneSwitched += self.on_scene_switched
self.conn.StreamingService.streamingStatusChange += ( self.conn.StreamingService.streamingStatusChange += self.on_streaming_status_change
self.on_streaming_status_change
)
def disconnect(self): def disconnect(self):
self.conn.disconnect() self.conn.disconnect()
@ -55,17 +51,15 @@ class StreamlabsController:
#################################################################################### ####################################################################################
def on_streaming_status_change(self, data): def on_streaming_status_change(self, data):
self.logger.debug(f"streaming status changed, now: {data}") self.logger.debug(f'streaming status changed, now: {data}')
if data in ("live", "starting"): if data in ('live', 'starting'):
self._duckypad.stream.is_live = True self._duckypad.stream.is_live = True
else: else:
self._duckypad.stream.is_live = False self._duckypad.stream.is_live = False
def on_scene_switched(self, data): def on_scene_switched(self, data):
self._duckypad.stream.current_scene = data.name self._duckypad.stream.current_scene = data.name
self.logger.debug( self.logger.debug(f'stream.current_scene updated to {self._duckypad.stream.current_scene}')
f"stream.current_scene updated to {self._duckypad.stream.current_scene}"
)
#################################################################################### ####################################################################################
# START/STOP the stream # START/STOP the stream
@ -74,14 +68,14 @@ class StreamlabsController:
@ensure_sl @ensure_sl
def begin_stream(self): def begin_stream(self):
if self._duckypad.stream.is_live: if self._duckypad.stream.is_live:
self.logger.info("Stream is already online") self.logger.info('Stream is already online')
return return
self.conn.StreamingService.toggleStreaming() self.conn.StreamingService.toggleStreaming()
@ensure_sl @ensure_sl
def end_stream(self): def end_stream(self):
if not self._duckypad.stream.is_live: if not self._duckypad.stream.is_live:
self.logger.info("Stream is already offline") self.logger.info('Stream is already offline')
return return
self.conn.StreamingService.toggleStreaming() self.conn.StreamingService.toggleStreaming()
@ -91,9 +85,7 @@ class StreamlabsController:
@ensure_sl @ensure_sl
def switch_scene(self, name): def switch_scene(self, name):
return self.conn.ScenesService.makeSceneActive( return self.conn.ScenesService.makeSceneActive(self._duckypad.scene.scenes[name.upper()])
self._duckypad.scene.scenes[name.upper()]
)
#################################################################################### ####################################################################################
# LAUNCH/SHUTDOWN the streamlabs process # LAUNCH/SHUTDOWN the streamlabs process
@ -102,16 +94,14 @@ class StreamlabsController:
@cached_property @cached_property
def sl_fullpath(self) -> Path: def sl_fullpath(self) -> Path:
try: try:
self.logger.debug("fetching sl_fullpath from the registry") self.logger.debug('fetching sl_fullpath from the registry')
SL_KEY = "029c4619-0385-5543-9426-46f9987161d9" SL_KEY = '029c4619-0385-5543-9426-46f9987161d9'
with winreg.OpenKey( with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'{}'.format('SOFTWARE' + '\\' + SL_KEY)) as regpath:
winreg.HKEY_LOCAL_MACHINE, r"{}".format("SOFTWARE" + "\\" + SL_KEY) slpath = winreg.QueryValueEx(regpath, r'InstallLocation')[0]
) as regpath: return Path(slpath) / 'Streamlabs OBS.exe'
slpath = winreg.QueryValueEx(regpath, r"InstallLocation")[0]
return Path(slpath) / "Streamlabs OBS.exe"
except FileNotFoundError as e: except FileNotFoundError as e:
self.logger.exception(f"{type(e).__name__}: {e}") self.logger.exception(f'{type(e).__name__}: {e}')
raise raise
def launch(self, delay=5): def launch(self, delay=5):
@ -126,4 +116,4 @@ class StreamlabsController:
if self.proc is not None: if self.proc is not None:
self.proc.terminate() self.proc.terminate()
self.proc = None self.proc = None
self._duckypad.stream.current_scene = "" self._duckypad.stream.current_scene = ''

View File

@ -11,7 +11,7 @@ requires-python = ">=3.7"
license = "MIT" license = "MIT"
keywords = [] keywords = []
authors = [ authors = [
{ name = "onyx-and-iris", email = "75868496+onyx-and-iris@users.noreply.github.com" }, { name = "onyx-and-iris", email = "code@onyxandiris.online" },
] ]
classifiers = [ classifiers = [
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",
@ -28,11 +28,11 @@ dependencies = [
"tomli >= 2.0.1;python_version < '3.11'", "tomli >= 2.0.1;python_version < '3.11'",
"websocket-client", "websocket-client",
"keyboard", "keyboard",
"voicemeeter-api", "voicemeeter-api>=2.6.1",
"xair-api", "xair-api>=2.4.1",
"slobs_websocket@git+https://git@github.com/onyx-and-iris/slobs_websocket@v0.1.4#egg=slobs_websocket", "slobs_websocket@git+https://git@github.com/onyx-and-iris/slobs_websocket@v0.1.4#egg=slobs_websocket",
"obsws-python", "obsws-python>=1.7.0",
"vban-cmd", "vban-cmd>=2.5.0",
] ]
[project.urls] [project.urls]
@ -66,61 +66,90 @@ style = ["ruff {args:.}", "black --check --diff {args:.}"]
fmt = ["black {args:.}", "ruff --fix {args:.}", "style"] fmt = ["black {args:.}", "ruff --fix {args:.}", "style"]
all = ["style", "typing"] all = ["style", "typing"]
[tool.black]
target-version = ["py37"]
line-length = 120
skip-string-normalization = true
[tool.ruff] [tool.ruff]
target-version = "py37" exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".mypy_cache",
".nox",
".pants.d",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"venv",
]
# Same as Black.
line-length = 120 line-length = 120
select = [ indent-width = 4
"A",
"ARG", # Assume Python 3.10
"B", target-version = "py310"
"C",
"DTZ", [tool.ruff.lint]
"E", # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
"EM", # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
"F", # McCabe complexity (`C901`) by default.
"FBT", select = ["E4", "E7", "E9", "F"]
"I", ignore = []
"ICN",
"ISC", # Allow fix for all enabled rules (when `--fix`) is provided.
"N", fixable = ["ALL"]
"PLC", unfixable = []
"PLE",
"PLR", # Allow unused variables when underscore-prefixed.
"PLW", dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
"Q",
"RUF",
"S", [tool.ruff.format]
"T", # Unlike Black, use single quotes for strings.
"TID", quote-style = "single"
"UP",
"W", # Like Black, indent with spaces, rather than tabs.
"YTT", indent-style = "space"
]
ignore = [ # Like Black, respect magic trailing commas.
# Allow non-abstract empty methods in abstract base classes skip-magic-trailing-comma = false
"B027",
# Allow boolean positional values in function calls, like `dict.get(... True)` # Like Black, automatically detect the appropriate line ending.
"FBT003", line-ending = "auto"
# Ignore checks for possible passwords
"S105", # Enable auto-formatting of code examples in docstrings. Markdown,
"S106", # reStructuredText code/literal blocks and doctests are all supported.
"S107", #
# Ignore complexity # This is currently disabled by default, but it is planned for this
"C901", # to be opt-out in the future.
"PLR0911", docstring-code-format = false
"PLR0912",
"PLR0913", # Set the line length limit used when formatting code snippets in
"PLR0915", # docstrings.
] #
unfixable = [ # This only has an effect when the `docstring-code-format` setting is
# Don't touch unused imports # enabled.
"F401", docstring-code-line-length = "dynamic"
[tool.ruff.lint.mccabe]
max-complexity = 10
[tool.ruff.lint.per-file-ignores]
"__init__.py" = [
"E402",
"F401",
] ]
# Tests can use magic values, assertions, and relative imports
"tests/**/*" = ["PLR2004", "S101", "TID252"]
[tool.ruff.isort] [tool.ruff.isort]
known-first-party = ["duckypad_twitch"] known-first-party = ["duckypad_twitch"]
@ -128,10 +157,6 @@ known-first-party = ["duckypad_twitch"]
[tool.ruff.flake8-tidy-imports] [tool.ruff.flake8-tidy-imports]
ban-relative-imports = "all" ban-relative-imports = "all"
[tool.ruff.per-file-ignores]
# Tests can use magic values, assertions, and relative imports
"tests/**/*" = ["PLR2004", "S101", "TID252"]
[tool.coverage.run] [tool.coverage.run]
source_pkgs = ["duckypad_twitch", "tests"] source_pkgs = ["duckypad_twitch", "tests"]
branch = true branch = true
@ -143,4 +168,4 @@ duckypad_twitch = ["duckypad_twitch", "*/duckypad-twitch/duckypad_twitch"]
tests = ["tests", "*/duckypad-twitch/tests"] tests = ["tests", "*/duckypad-twitch/tests"]
[tool.coverage.report] [tool.coverage.report]
exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"] exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"]