From 1b075e2fea901c247013478911754093b6b06adc Mon Sep 17 00:00:00 2001 From: Devin Collins Date: Wed, 16 Apr 2025 19:14:31 -0700 Subject: [PATCH] feat(refactor): Refactor to new ActionCore --- actions/AdSchedule.py | 125 +++++++++++++++++++++++++++++ actions/ChatMode.py | 97 ++++++++++++++++++++++ actions/Clip.py | 37 +++++++++ actions/Marker.py | 37 +++++++++ actions/PlayAd.py | 54 +++++++++++++ actions/SendMessage.py | 62 ++++++++++++++ actions/ShowViewers.py | 33 ++++++++ actions/TwitchCore.py | 78 ++++++++++++++++++ actions/ad_schedule.py | 42 ---------- actions/chat_mode.py | 106 ------------------------ actions/clip.py | 19 ----- actions/marker.py | 18 ----- actions/message.py | 62 -------------- actions/play_ad.py | 69 ---------------- actions/snooze_ad.py | 19 ----- actions/viewers.py | 52 ------------ assets/requirements.txt | 2 +- locales.csv | 16 ++++ locales/en_US.json | 16 ---- main.py | 174 ++++++++++++++++++++-------------------- manifest.json | 6 +- twitch_backend.py | 9 +-- 22 files changed, 636 insertions(+), 497 deletions(-) create mode 100644 actions/AdSchedule.py create mode 100644 actions/ChatMode.py create mode 100644 actions/Clip.py create mode 100644 actions/Marker.py create mode 100644 actions/PlayAd.py create mode 100644 actions/SendMessage.py create mode 100644 actions/ShowViewers.py create mode 100644 actions/TwitchCore.py delete mode 100644 actions/ad_schedule.py delete mode 100644 actions/chat_mode.py delete mode 100644 actions/clip.py delete mode 100644 actions/marker.py delete mode 100644 actions/message.py delete mode 100644 actions/play_ad.py delete mode 100644 actions/snooze_ad.py delete mode 100644 actions/viewers.py create mode 100644 locales.csv delete mode 100644 locales/en_US.json diff --git a/actions/AdSchedule.py b/actions/AdSchedule.py new file mode 100644 index 0000000..b2c61fa --- /dev/null +++ b/actions/AdSchedule.py @@ -0,0 +1,125 @@ +from enum import StrEnum, Enum +from datetime import datetime, timedelta +from threading import Thread +from time import sleep + +from GtkHelper.GenerativeUI.SwitchRow import SwitchRow +from .TwitchCore import TwitchCore +from src.backend.PluginManager.EventAssigner import EventAssigner +from src.backend.PluginManager.InputBases import Input +from src.backend.PluginManager.PluginSettings.Asset import Color + +from loguru import logger as log + + +class Icons(StrEnum): + DELAY = "delay" + + +class Colors(StrEnum): + DEFAULT = "default" + WARNING = "warning" + ALERT = "alert" + + +class AdSchedule(TwitchCore): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.icon_keys = [Icons.DELAY] + self.current_icon = self.get_icon(Icons.DELAY) + self.icon_name = Icons.DELAY + self.color_keys = [Colors.DEFAULT, Colors.WARNING, Colors.ALERT] + self.current_color = self.get_color(Colors.DEFAULT) + self.has_configuration = True + self._next_ad: datetime = datetime.now() + self._snoozes: int = -1 + + def on_ready(self): + super().on_ready() + Thread( + target=self._get_ad_schedule, daemon=True, name="get_ad_schedule").start() + Thread( + target=self._update_ad_timer, daemon=True, name="update_ad_timer").start() + + def create_event_assigners(self): + self.event_manager.add_event_assigner( + EventAssigner( + id="snooze-ad", + ui_label="Snooze Ad", + default_event=Input.Key.Events.DOWN, + callback=self._on_snooze_ad, + ) + ) + + def create_generative_ui(self): + self._skip_ad_switch = SwitchRow( + action_core=self, + var_name="ad.snooze", + default_value=True, + title="ad-snooze", + subtitle="Snoozes ad for 5 minutes when pushed", + complex_var_name=True + ) + + def get_config_rows(self): + return [self._skip_ad_switch.widget] + + def _update_background_color(self, color: str): + self.current_color = self.get_color(color) + self.display_color() + + def _update_ad_timer(self): + while self.get_is_present(): + self.display_color() + now = datetime.now() + self.set_bottom_label( + str(self._snoozes) if (self._snoozes >= 0 and self._skip_ad_switch.get_active()) else "") + try: + if self._next_ad < now: + self._update_background_color(Colors.DEFAULT) + self.set_center_label("") + continue + diff = (self._next_ad - now).total_seconds() + self.set_center_label(self._convert_seconds_to_hh_mm_ss(diff)) + if diff <= 60: + self._update_background_color(Colors.ALERT) + continue + if diff <= 300: + self._update_background_color(Colors.WARNING) + continue + self._update_background_color(Colors.DEFAULT) + except TypeError: + # There is a known issue where the default timestamp returned from + # the twitch API is an invalid datetime object and causes an error. + # Ignoring it here + pass + except Exception as ex: + log.error(ex) + sleep(1) + + def _get_ad_schedule(self): + while self.get_is_present(): + try: + schedule, snoozes = self.backend.get_next_ad() + self._next_ad = schedule + self._snoozes = snoozes + self._update_ad_timer() + except Exception as ex: + log.error(ex) + self.show_error(3) + sleep(30) + + def _convert_seconds_to_hh_mm_ss(self, seconds) -> str: + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + remaining_seconds = seconds % 60 + return f"{int(hours):02}:{int(minutes):02}:{int(remaining_seconds):02}" + + def _on_snooze_ad(self, _): + if not self._skip_ad_switch.get_active(): + return + try: + self.backend.snooze_ad() + except Exception as ex: + log.error(ex) + self.show_error(3) diff --git a/actions/ChatMode.py b/actions/ChatMode.py new file mode 100644 index 0000000..a91c2c5 --- /dev/null +++ b/actions/ChatMode.py @@ -0,0 +1,97 @@ +from enum import StrEnum, Enum +from threading import Thread +from time import sleep + +from .TwitchCore import TwitchCore +from src.backend.PluginManager.EventAssigner import EventAssigner +from src.backend.PluginManager.InputBases import Input +from GtkHelper.GenerativeUI.ComboRow import ComboRow +from GtkHelper.ComboRow import SimpleComboRowItem, BaseComboRowItem + +from loguru import logger as log + + +class Icons(StrEnum): + FOLLOWER = "follower_mode" + SUBSCRIBER = "subscriber_mode" + EMOTE = "emote_mode" + SLOW = "slow_mode" + + +class ChatModeOptions(Enum): + FOLLOWER = SimpleComboRowItem("follower_mode", "Follower Only") + SUBSCRIBER = SimpleComboRowItem("subscriber_mode", "Subscriber Only") + EMOTE = SimpleComboRowItem("emote_mode", "Emote Only") + SLOW = SimpleComboRowItem("slow_mode", "Slow Mode") + + +class ChatMode(TwitchCore): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.icon_keys = [Icons.FOLLOWER, + Icons.SUBSCRIBER, Icons.EMOTE, Icons.SLOW] + self.current_icon = self.get_icon(Icons.FOLLOWER) + self.icon_name = Icons.FOLLOWER + + def create_event_assigners(self): + self.event_manager.add_event_assigner( + EventAssigner( + id="chat-toggle", + ui_label="Chat Toggle", + default_event=Input.Key.Events.DOWN, + callback=self._on_toggle_chat, + ) + ) + + def create_generative_ui(self): + self._chat_select_row = ComboRow( + action_core=self, + var_name="chat.mode", + default_value=ChatModeOptions.FOLLOWER.value, + items=[ + ChatModeOptions.FOLLOWER.value, + ChatModeOptions.SUBSCRIBER.value, + ChatModeOptions.EMOTE.value, + ChatModeOptions.SLOW.value, + ], + title="chat-toggle-dropdown", + complex_var_name=True, + on_change=self._change_chat_mode + ) + + def on_ready(self): + Thread( + target=self._update_chat_mode, daemon=True, name="update_chat_mode").start() + + def get_config_rows(self): + return [self._chat_select_row.widget] + + def _change_chat_mode(self, _, new, __): + self.icon_name = Icons(new) + self.current_icon = self.get_icon(self.icon_name) + self.display_icon() + + def _update_icon(self, mode: str, enabled: bool): + # TODO: Custom icons for enabled/disabled + self.set_center_label("Enabled" if enabled else "Disabled") + + def _update_chat_mode(self): + while self.get_is_present(): + try: + chat_settings = self.backend.get_chat_settings() + mode = self._chat_select_row.get_selected_item().get_value() + enabled = chat_settings.get(mode) + self._update_icon(mode, enabled) + except Exception as ex: + log.error(ex) + self.show_error(3) + sleep(5) + + def _on_toggle_chat(self, _): + item = self._chat_select_row.get_selected_item().get_value() + try: + resp = self.backend.toggle_chat_mode(item) + self._update_icon(item, resp) + except Exception as ex: + log.error(ex) + self.show_error(3) diff --git a/actions/Clip.py b/actions/Clip.py new file mode 100644 index 0000000..8b23c80 --- /dev/null +++ b/actions/Clip.py @@ -0,0 +1,37 @@ +from enum import StrEnum + +from loguru import logger as log + +from .TwitchCore import TwitchCore +from src.backend.PluginManager.EventAssigner import EventAssigner +from src.backend.PluginManager.InputBases import Input + + +class Icons(StrEnum): + CLIP = "camera" + + +class Clip(TwitchCore): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.icon_keys = [Icons.CLIP] + self.current_icon = self.get_icon(Icons.CLIP) + self.icon_name = Icons.CLIP + self.has_configuration = False + + def create_event_assigners(self): + self.event_manager.add_event_assigner( + EventAssigner( + id="clip", + ui_label="Clip", + default_event=Input.Key.Events.DOWN, + callback=self._on_clip + ) + ) + + def _on_clip(self, _): + try: + self.backend.create_clip() + except Exception as ex: + log.error(ex) + self.show_error(3) diff --git a/actions/Marker.py b/actions/Marker.py new file mode 100644 index 0000000..05a178b --- /dev/null +++ b/actions/Marker.py @@ -0,0 +1,37 @@ +from enum import StrEnum + +from loguru import logger as log + +from .TwitchCore import TwitchCore +from src.backend.PluginManager.EventAssigner import EventAssigner +from src.backend.PluginManager.InputBases import Input + + +class Icons(StrEnum): + MARKER = "bookmark" + + +class Marker(TwitchCore): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.icon_keys = [Icons.MARKER] + self.current_icon = self.get_icon(Icons.MARKER) + self.icon_name = Icons.MARKER + self.has_configuration = False + + def create_event_assigners(self): + self.event_manager.add_event_assigner( + EventAssigner( + id="marker", + ui_label="Marker", + default_event=Input.Key.Events.DOWN, + callback=self._on_marker + ) + ) + + def _on_marker(self, _): + try: + self.backend.create_marker() + except Exception as ex: + log.error(ex) + self.show_error(3) diff --git a/actions/PlayAd.py b/actions/PlayAd.py new file mode 100644 index 0000000..917e338 --- /dev/null +++ b/actions/PlayAd.py @@ -0,0 +1,54 @@ +from enum import StrEnum, Enum + +from loguru import logger as log + +from .TwitchCore import TwitchCore +from src.backend.PluginManager.EventAssigner import EventAssigner +from src.backend.PluginManager.InputBases import Input +from GtkHelper.GenerativeUI.ComboRow import ComboRow +from GtkHelper.ComboRow import SimpleComboRowItem, BaseComboRowItem + + +class Icons(StrEnum): + AD = "money" + + +class PlayAd(TwitchCore): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.icon_keys = [Icons.AD] + self.current_icon = self.get_icon(Icons.AD) + self.icon_name = Icons.AD + self.has_configuration = True + + def create_generative_ui(self): + options = [SimpleComboRowItem(x, f"{x} seconds") + for x in [30, 60, 90, 120]] + self._time_row = ComboRow( + action_core=self, + var_name="ad.duration", + default_value=options[0], + items=options, + title="ad-options-dropdown", + complex_var_name=True, + ) + + def get_config_rows(self): + return [self._time_row.widget] + + def create_event_assigners(self): + self.event_manager.add_event_assigner( + EventAssigner( + id="play-ad", + ui_label="Play Ad", + default_event=Input.Key.Events.DOWN, + callback=self._on_play_ad + ) + ) + + def _on_play_ad(self, _): + try: + self.backend.play_ad(10) + except Exception as ex: + log.error(ex) + self.show_error(3) diff --git a/actions/SendMessage.py b/actions/SendMessage.py new file mode 100644 index 0000000..b099368 --- /dev/null +++ b/actions/SendMessage.py @@ -0,0 +1,62 @@ +from enum import StrEnum + +from .TwitchCore import TwitchCore +from src.backend.PluginManager.EventAssigner import EventAssigner +from src.backend.PluginManager.InputBases import Input + +from GtkHelper.GenerativeUI.EntryRow import EntryRow + +from loguru import logger as log + + +class Icons(StrEnum): + CHAT = "chat" + + +class SendMessage(TwitchCore): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.icon_keys = [Icons.CHAT] + self.current_icon = self.get_icon(Icons.CHAT) + self.icon_name = Icons.CHAT + self.has_configuration = True + + def create_event_assigners(self): + self.event_manager.add_event_assigner( + EventAssigner( + id="chat", + ui_label="Message", + default_event=Input.Key.Events.DOWN, + callback=self._on_chat + ) + ) + + def create_generative_ui(self): + self.message_row = EntryRow( + action_core=self, + var_name="chat.message_text", + default_value="", + title="chat-message-text", + auto_add=False, + complex_var_name=True, + ) + self.channel_row = EntryRow( + action_core=self, + var_name="chat.channel_id", + default_value="", + title="chat-channel-id", + auto_add=False, + complex_var_name=True, + ) + + def get_config_rows(self): + return [self.message_row.widget, self.channel_row.widget] + + def _on_chat(self, _): + message = self.message_row.get_value() + channel = self.channel_row.get_value() + try: + self.backend.send_message(message, channel) + except Exception as ex: + log.error(ex) + self.show_error(3) diff --git a/actions/ShowViewers.py b/actions/ShowViewers.py new file mode 100644 index 0000000..3893bec --- /dev/null +++ b/actions/ShowViewers.py @@ -0,0 +1,33 @@ +from enum import StrEnum +from threading import Thread +from time import sleep + +from .TwitchCore import TwitchCore +from src.backend.PluginManager.EventAssigner import EventAssigner +from src.backend.PluginManager.InputBases import Input + +from loguru import logger as log + + +class Icons(StrEnum): + VIEWERS = "view" + + +class ShowViewers(TwitchCore): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.icon_keys = [Icons.VIEWERS] + self.current_icon = self.get_icon(Icons.VIEWERS) + self.icon_name = Icons.VIEWERS + + def on_ready(self): + Thread( + target=self._update_viewers, daemon=True, name="update_viewers").start() + + def _update_viewers(self): + while self.get_is_present(): + count = self.backend.get_viewers() + if not count: + count = "-" + self.set_center_label(str(count)) + sleep(10) diff --git a/actions/TwitchCore.py b/actions/TwitchCore.py new file mode 100644 index 0000000..3f0bcd3 --- /dev/null +++ b/actions/TwitchCore.py @@ -0,0 +1,78 @@ +from loguru import logger as log +from src.backend.PluginManager.ActionCore import ActionCore +from src.backend.DeckManagement.InputIdentifier import InputEvent, Input +from src.backend.PluginManager.PluginSettings.Asset import Color, Icon + +from gi.repository import Gtk, Adw +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") + + +class TwitchCore(ActionCore): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Setup AssetManager values + self.icon_keys = [] + self.color_keys = [] + self.current_icon: Icon = None + self.current_color: Color = None + self.icon_name = "" + self.color_name = "" + self.backend: 'Backend' = self.plugin_base.backend + + self.plugin_base.asset_manager.icons.add_listener(self._icon_changed) + self.plugin_base.asset_manager.colors.add_listener(self._color_changed) + + # Setup action related stuff + self.create_generative_ui() + self.create_event_assigners() + + def on_ready(self): + super().on_ready() + self.display_icon() + self.display_color() + + def create_generative_ui(self): + pass + + def create_event_assigners(self): + pass + + def display_icon(self): + if not self.current_icon: + return + _, rendered = self.current_icon.get_values() + if rendered: + self.set_media(image=rendered) + + async def _icon_changed(self, event: str, key: str, asset): + if not key in self.icon_keys: + return + if key != self.icon_name: + return + self.current_icon = asset + self.icon_name = key + self.display_icon() + + def display_color(self): + if not self.current_color: + return + color = self.current_color.get_values() + try: + self.set_background_color(color) + except: + # Sometimes we try to call this too early, and it leads to + # console errors, but no real errors. Ignoring this for now + pass + + async def _color_changed(self, event: str, key: str, asset): + if not key in self.color_keys: + return + if key != self.color_name: + return + self.current_color = asset + self.color_name = key + self.display_color() diff --git a/actions/ad_schedule.py b/actions/ad_schedule.py deleted file mode 100644 index f92cfa6..0000000 --- a/actions/ad_schedule.py +++ /dev/null @@ -1,42 +0,0 @@ -import threading -import time -import os - -from src.backend.PluginManager.ActionBase import ActionBase - -from loguru import logger as log - -# Currently an issue with TwitchPy that gets the wrong time format, can't use this yet - - -class NextAd(ActionBase): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.__ad_thread: threading.Thread = None - self.has_configuration = True - - def on_ready(self): - self.set_media(media_path=os.path.join( - self.plugin_base.PATH, "assets", "view.png"), size=0.85) - if not self.__ad_thread or not self.__ad_thread.is_alive(): - self.__ad_thread = threading.Thread( - target=self.ad_thread, daemon=True, name="ad_thread") - self.__ad_thread.start() - - def ad_thread(self): - while True: - if not self.get_is_present(): - return - self.get_next_ad() - time.sleep(1) - - def get_next_ad(self): - try: - next_ad = self.plugin_base.backend.get_next_ad() - if not next_ad: - next_ad = "-" - self.set_bottom_label(str(next_ad)) - self.hide_error() - except Exception as ex: - log.error(ex) - self.show_error() diff --git a/actions/chat_mode.py b/actions/chat_mode.py deleted file mode 100644 index c2e9ac1..0000000 --- a/actions/chat_mode.py +++ /dev/null @@ -1,106 +0,0 @@ -import time -import threading -import os - -import gi -from gi.repository import Gtk, Adw - -from loguru import logger as log - -from src.backend.PluginManager.ActionBase import ActionBase - -gi.require_version("Gtk", "4.0") -gi.require_version("Adw", "1") - -icons = { - 'Follower Mode': 'follower.png', - 'Subscriber Mode': 'subscriber.png', - 'Emote Mode': 'emote.png', - 'Slow Mode': 'slow.png', -} - - -class ChatMode(ActionBase): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._mode: str = None - self.has_configuration = True - - def on_ready(self): - self._load_config() - self.set_media(media_path=os.path.join( - self.plugin_base.PATH, "assets", icons[self._mode]), size=0.85) - threading.Thread(target=self.get_mode_status, daemon=True, - name="get_mode_status").start() - - def get_config_rows(self): - super_rows = super().get_config_rows() - self.action_model = Gtk.StringList() - self.mode_row = Adw.ComboRow(model=self.action_model, title=self.plugin_base.lm.get( - "actions.chat_mode.mode_row.label")) - - index = 0 - found = -1 - for key in icons: - self.action_model.append(key) - if key == self._mode: - found = index - index += 1 - - if found < 0: - self.mode_row.set_selected(Gtk.INVALID_LIST_POSITION) - - self.mode_row.connect("notify::selected", self._on_change_mode) - self.mode_row.set_selected(found) - super_rows.append(self.mode_row) - return super_rows - - def on_key_down(self): - try: - settings = self.get_settings() - mode = settings['mode'] - parsed_mode = mode.lower().replace(' ', '_') - resp = self.plugin_base.backend.toggle_chat_mode(parsed_mode) - if resp: - self.set_bottom_label(resp) - except Exception as ex: - log.error(ex) - self.show_error(3) - - def _load_config(self): - settings = self.get_settings() - mode = settings.get('mode') - for key in icons: - if key == mode: - self._mode = key - return - self._mode = 'Follower Mode' - - def _on_change_mode(self, *_): - settings = self.get_settings() - selected_index = self.mode_row.get_selected() - if selected_index == Gtk.INVALID_LIST_POSITION: - return - settings['mode'] = self.action_model[selected_index].get_string() - self._mode = settings['mode'] - self.set_media(media_path=os.path.join( - self.plugin_base.PATH, "assets", icons[settings['mode']]), size=0.85) - self.set_settings(settings) - self.set_top_label(self._mode.split(' ')[0]) - - def get_mode_status(self): - while True: - if not self.get_is_present(): - return - try: - if self._mode is None: - raise Exception(f'no config: {self._mode}') - existing_modes = self.plugin_base.backend.get_chat_settings() - parsed_mode = self._mode.lower().replace(' ', '_') - self.set_bottom_label(str(existing_modes[parsed_mode])) - self.set_top_label(self._mode.split(' ')[0]) - except Exception as ex: - log.error(ex) - self.set_bottom_label('-') - self.show_error(3) - time.sleep(3) diff --git a/actions/clip.py b/actions/clip.py deleted file mode 100644 index 455e7e9..0000000 --- a/actions/clip.py +++ /dev/null @@ -1,19 +0,0 @@ - -import os - -from loguru import logger as log - -from src.backend.PluginManager.ActionBase import ActionBase - - -class Clip(ActionBase): - def on_ready(self): - self.set_media(media_path=os.path.join( - self.plugin_base.PATH, "assets", "camera.png"), size=0.85) - - def on_key_down(self): - try: - self.plugin_base.backend.create_clip() - except Exception as ex: - log.error(ex) - self.show_error(3) diff --git a/actions/marker.py b/actions/marker.py deleted file mode 100644 index 7a8fa5c..0000000 --- a/actions/marker.py +++ /dev/null @@ -1,18 +0,0 @@ -import os - -from loguru import logger as log - -from src.backend.PluginManager.ActionBase import ActionBase - - -class Marker(ActionBase): - def on_ready(self): - self.set_media(media_path=os.path.join( - self.plugin_base.PATH, "assets", "bookmark.png"), size=0.85) - - def on_key_down(self): - try: - self.plugin_base.backend.create_marker() - except Exception as ex: - log.error(ex) - self.show_error(3) diff --git a/actions/message.py b/actions/message.py deleted file mode 100644 index 004f682..0000000 --- a/actions/message.py +++ /dev/null @@ -1,62 +0,0 @@ - -import os -import gi -from gi.repository import Adw - -from loguru import logger as log - -from src.backend.PluginManager.ActionBase import ActionBase - -gi.require_version("Gtk", "4.0") -gi.require_version("Adw", "1") - - -class SendMessage(ActionBase): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.has_configuration = True - - def on_ready(self): - self.set_media(media_path=os.path.join( - self.plugin_base.PATH, "assets", "chat.png"), size=0.85) - - def get_config_rows(self): - self.message_row = Adw.EntryRow( - title=self.plugin_base.lm.get("actions.message.message")) - self.channel_row = Adw.EntryRow( - title=self.plugin_base.lm.get("actions.message.channel")) - - self.message_row.connect("notify::text", self._on_message_change) - self.channel_row.connect("notify::text", self._on_channel_change) - - self._load_config() - - super_rows = super().get_config_rows() - super_rows.append(self.message_row) - super_rows.append(self.channel_row) - return super_rows - - def _load_config(self): - settings = self.get_settings() - self.message_row.set_text(settings.get('message', '')) - self.channel_row.set_text(settings.get('channel', '')) - - def _on_message_change(self, entry, _): - settings = self.get_settings() - settings['message'] = entry.get_text() - self.set_settings(settings) - - def _on_channel_change(self, entry, _): - settings = self.get_settings() - settings['channel'] = entry.get_text() - self.set_settings(settings) - - def on_key_down(self): - settings = self.get_settings() - message = settings.get('message', '') - channel = settings.get('channel', '') - try: - self.plugin_base.backend.send_message(message, channel) - except Exception as ex: - log.error(ex) - self.show_error(3) diff --git a/actions/play_ad.py b/actions/play_ad.py deleted file mode 100644 index 50bf637..0000000 --- a/actions/play_ad.py +++ /dev/null @@ -1,69 +0,0 @@ -import os -import gi -from gi.repository import Adw, Gtk - -from loguru import logger as log - -from src.backend.PluginManager.ActionBase import ActionBase - -gi.require_version("Gtk", "4.0") -gi.require_version("Adw", "1") - -options = [30, 60, 90, 120] - - -class PlayAd(ActionBase): - _time: int = 30 - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.has_configuration = True - - def on_ready(self): - self.set_media(media_path=os.path.join( - self.plugin_base.PATH, "assets", "money.png"), size=0.85) - - def _load_config(self): - settings = self.get_settings() - time = settings.get("time") - for value in options: - if value == time: - self._time = value - return - self._time = 30 - - def _on_change_time(self, *_): - settings = self.get_settings() - selected_index = self.time_row.get_selected() - time = self.action_model[selected_index].get_string() - settings["time"] = int(time) - self.set_settings(settings) - - def get_config_rows(self): - super_rows = super().get_config_rows() - self.action_model = Gtk.StringList() - self.time_row = Adw.ComboRow( - model=self.action_model, title=self.plugin_base.lm.get("actions.play_ad.time.label")) - self._load_config() - - index = 0 - found = -1 - for value in options: - self.action_model.append(str(value)) - if value == self._time: - found = index - index += 1 - if found < 0: - self.time_row.set_selected(0) - - self.time_row.connect("notify::selected", self._on_change_time) - self.time_row.set_selected(found) - super_rows.append(self.time_row) - return super_rows - - def on_key_down(self): - try: - self.plugin_base.backend.play_ad(self._time) - except Exception as ex: - log.error(ex) - self.show_error(3) diff --git a/actions/snooze_ad.py b/actions/snooze_ad.py deleted file mode 100644 index 67bf7e4..0000000 --- a/actions/snooze_ad.py +++ /dev/null @@ -1,19 +0,0 @@ - -import os - -from loguru import logger as log - -from src.backend.PluginManager.ActionBase import ActionBase - - -class SnoozeAd(ActionBase): - def on_ready(self): - self.set_media(media_path=os.path.join( - self.plugin_base.PATH, "assets", "delay.png"), size=0.85) - - def on_key_down(self): - try: - self.plugin_base.backend.snooze_ad() - except Exception as ex: - log.error(ex) - self.show_error(3) diff --git a/actions/viewers.py b/actions/viewers.py deleted file mode 100644 index 56c4d99..0000000 --- a/actions/viewers.py +++ /dev/null @@ -1,52 +0,0 @@ -import threading -import time -import os - -from loguru import logger as log - -from src.backend.PluginManager.ActionBase import ActionBase - - -class Viewers(ActionBase): - __viewer_thread: threading.Thread = None - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.current_count = 0 - self._view_hidden = False - self.__viewer_thread = None - - def on_ready(self): - self.set_media(media_path=os.path.join( - self.plugin_base.PATH, "assets", "view.png"), size=0.85) - if not self.__viewer_thread or not self.__viewer_thread.is_alive(): - self.__viewer_thread = threading.Thread(target=self.viewers_thread, - daemon=True, name="viewers_thread") - self.__viewer_thread.start() - - def on_key_down(self): - self._view_hidden = not self._view_hidden - image = "view-hidden.png" if self._view_hidden else "view.png" - self.set_media(media_path=os.path.join( - self.plugin_base.PATH, "assets", image), size=0.85) - self.show_current_viewers() - - def viewers_thread(self): - while True: - if not self.get_is_present(): - return - self.show_current_viewers() - time.sleep(10) - - def show_current_viewers(self): - if self._view_hidden: - self.set_bottom_label("-") - return - try: - self.current_count = self.plugin_base.backend.get_viewers() - if not self.current_count: - self.current_count = "-" - self.set_bottom_label(str(self.current_count)) - except Exception as ex: - log.error(ex) - self.show_error(30) diff --git a/assets/requirements.txt b/assets/requirements.txt index c41443c..5dc8c8e 100644 --- a/assets/requirements.txt +++ b/assets/requirements.txt @@ -6,5 +6,5 @@ plumbum==1.9.0 requests==2.32.3 rpyc==6.0.1 streamcontroller-plugin-tools==2.0.1 -twitchpy==1.3.1 +twitchpy==1.3.2 urllib3==2.3.0 diff --git a/locales.csv b/locales.csv new file mode 100644 index 0000000..24f5a30 --- /dev/null +++ b/locales.csv @@ -0,0 +1,16 @@ +key;en_US +ad-options-dropdown;Ad Durations +ad-snooze;Snooze Ads +ad-snooze-subtitle;Snoozes ad for 5 minutes when pushed +chat-channel-id;Twitch Channel to send message to +chat-message-text;Text to send +chat-toggle-dropdown;Chat Mode +;; +actions.base.credentials.authenticated;Authenticated successfully +actions.base.credentials.failed;Authenication failed +actions.base.credentials.title;Twitch Credentials +actions.base.credentials.validate;Validate +actions.base.twitch_client_id;Twitch Client ID +actions.base.twitch_client_secret;Twitch Client Secret +actions.info.link.label;Checkout how to configure this plugin on +actions.info.link.text;GitHub diff --git a/locales/en_US.json b/locales/en_US.json deleted file mode 100644 index 3f57611..0000000 --- a/locales/en_US.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "plugin.name": "Twitch", - "actions.base.credentials.no-credentials": "Missing credentials", - "actions.base.credentials.title": "Twitch Credentials", - "actions.base.credentials.validate": "Validate", - "actions.base.twitch_client_id": "Twitch Client ID", - "actions.base.twitch_client_secret": "Twitch Client Secret", - "actions.base.credentials.authenticated": "Authenticated successfully", - "actions.base.credentials.failed": "Authentication failed", - "actions.message.message": "Message", - "actions.message.channel": "Channel", - "actions.chat_mode.mode_row.label": "Chat Mode", - "actions.info.link.label": "Checkout how to configure this plugin on", - "actions.info.link.text": "GitHub", - "actions.play_ad.time.label": "Ad Length" -} diff --git a/main.py b/main.py index 54b75e7..e258c9b 100644 --- a/main.py +++ b/main.py @@ -12,53 +12,38 @@ # Import actions from .settings import PluginSettings -from .actions.message import SendMessage -from .actions.chat_mode import ChatMode -from .actions.clip import Clip -from .actions.marker import Marker -from .actions.viewers import Viewers -from .actions.play_ad import PlayAd -from .actions.snooze_ad import SnoozeAd -from .actions.ad_schedule import NextAd +from .actions.SendMessage import SendMessage +from .actions.Clip import Clip +from .actions.ShowViewers import ShowViewers +from .actions.Marker import Marker +from .actions.ChatMode import ChatMode +from .actions.PlayAd import PlayAd +from .actions.AdSchedule import AdSchedule class PluginTemplate(PluginBase): - def __init__(self): - super().__init__() - - self.has_plugin_settings = True - - self.lm = self.locale_manager - self.lm.set_to_os_default() - - self._settings_manager = PluginSettings(self) - - self.auth_callback_fn: callable = None - - # Launch backend - backend_path = os.path.join(self.PATH, "twitch_backend.py") - self.launch_backend(backend_path=backend_path, open_in_terminal=False, - venv_path=os.path.join(self.PATH, ".venv")) - self.wait_for_backend(tries=5) - - settings = self.get_settings() - client_id = settings.get('client_id', '') - client_secret = settings.get('client_secret', '') - auth_code = settings.get('auth_code', '') - - settings_path = os.path.join( - gl.DATA_PATH, 'settings', 'plugins', self.get_plugin_id_from_folder_name()) - os.makedirs(settings_path, exist_ok=True) - self.backend.set_token_path(os.path.join(settings_path, "keys.json")) - if client_id and client_secret and auth_code: - self.backend.auth_with_code( - client_id, client_secret, auth_code) - - # Register actions + def _add_icons(self): + self.add_icon("chat", self.get_asset_path("chat.png")) + self.add_icon("camera", self.get_asset_path("camera.png")) + self.add_icon("bookmark", self.get_asset_path("bookmark.png")) + self.add_icon("view", self.get_asset_path("view.png")) + self.add_icon("follower_mode", self.get_asset_path("follower.png")) + self.add_icon("subscriber_mode", self.get_asset_path("subscriber.png")) + self.add_icon("emote_mode", self.get_asset_path("emote.png")) + self.add_icon("slow_mode", self.get_asset_path("slow.png")) + self.add_icon("money", self.get_asset_path("money.png")) + self.add_icon("delay", self.get_asset_path("delay.png")) + + def _add_colors(self): + self.add_color("default", [0, 0, 0, 0]) + self.add_color("warning", [255, 244, 79, 255]) + self.add_color("alert", [224, 102, 102, 255]) + + def _register_actions(self): self.message_action_holder = ActionHolder( plugin_base=self, action_base=SendMessage, - action_id="com_imdevinc_StreamControllerTwitchPlugin::SendMessage", + action_id_suffix="SendMessage", action_name="Send Chat Message", action_support={ Input.Key: ActionInputSupport.SUPPORTED, @@ -68,96 +53,116 @@ def __init__(self): ) self.add_action_holder(self.message_action_holder) - self.viewer_action_holder = ActionHolder( + self.clip_action_holder = ActionHolder( plugin_base=self, - action_base=Viewers, - action_id="com_imdevinc_StreamControllerTwitchPlugin::Viewers", - action_name="Show Viewers", + action_base=Clip, + action_id_suffix="Clip", + action_name="Create clip", action_support={ Input.Key: ActionInputSupport.SUPPORTED, Input.Dial: ActionInputSupport.UNTESTED, Input.Touchscreen: ActionInputSupport.UNTESTED, } ) - self.add_action_holder(self.viewer_action_holder) + self.add_action_holder(self.clip_action_holder) - self.marker_action_holder = ActionHolder( + self.viewers_action_holder = ActionHolder( plugin_base=self, - action_base=Marker, - action_id="com_imdevinc_StreamControllerTwitchPlugin::Marker", - action_name="Create Stream Marker", + action_base=ShowViewers, + action_id_suffix="ShowViewers", + action_name="Show Viewers", action_support={ Input.Key: ActionInputSupport.SUPPORTED, Input.Dial: ActionInputSupport.UNTESTED, Input.Touchscreen: ActionInputSupport.UNTESTED, } ) - self.add_action_holder(self.marker_action_holder) + self.add_action_holder(self.viewers_action_holder) - self.chat_mode_action_holder = ActionHolder( + self.marker_actions_holder = ActionHolder( plugin_base=self, - action_base=ChatMode, - action_id="com_imdevinc_StreamControllerTwitchPlugin::ChatMode", - action_name="Toggle Chat Mode", + action_base=Marker, + action_id_suffix="Marker", + action_name="Create Marker", action_support={ Input.Key: ActionInputSupport.SUPPORTED, Input.Dial: ActionInputSupport.UNTESTED, Input.Touchscreen: ActionInputSupport.UNTESTED, } ) - self.add_action_holder(self.chat_mode_action_holder) + self.add_action_holder(self.marker_actions_holder) - self.clip_action_holder = ActionHolder( + self.chatmode_actions_holder = ActionHolder( plugin_base=self, - action_base=Clip, - action_id="com_imdevinc_StreamControllerTwitchPlugin::Clip", - action_name="Create Clip", + action_base=ChatMode, + action_id_suffix="ChatMode", + action_name="Chat Mode", action_support={ Input.Key: ActionInputSupport.SUPPORTED, Input.Dial: ActionInputSupport.UNTESTED, Input.Touchscreen: ActionInputSupport.UNTESTED, } ) - self.add_action_holder(self.clip_action_holder) + self.add_action_holder(self.chatmode_actions_holder) - self.snooze_ad_action_holder = ActionHolder( + self.playad_action_holder = ActionHolder( plugin_base=self, - action_base=SnoozeAd, - action_id="com_imdevinc_StreamControllerTwitchPlugin::SnoozeAd", - action_name="Snooze Ad", + action_base=PlayAd, + action_id_suffix="PlayAd", + action_name="Play Ad", action_support={ Input.Key: ActionInputSupport.SUPPORTED, Input.Dial: ActionInputSupport.UNTESTED, Input.Touchscreen: ActionInputSupport.UNTESTED, } ) - self.add_action_holder(self.snooze_ad_action_holder) + self.add_action_holder(self.playad_action_holder) - self.play_ad_action_holder = ActionHolder( + self.ad_schedule_action_holder = ActionHolder( plugin_base=self, - action_base=PlayAd, - action_id="com_imdevinc_StreamControllerTwitchPlugin::PlayAd", - action_name="Play Ad", + action_base=AdSchedule, + action_id_suffix="AdSchedule", + action_name="Ad Schedule", action_support={ Input.Key: ActionInputSupport.SUPPORTED, Input.Dial: ActionInputSupport.UNTESTED, Input.Touchscreen: ActionInputSupport.UNTESTED, } ) - self.add_action_holder(self.play_ad_action_holder) - - # self.next_ad_action_holder = ActionHolder( - # plugin_base=self, - # action_base=NextAd, - # action_id="com_imdevinc_StreamControllerTwitchPlugin::NextAd", - # action_name="Next Ad", - # action_support={ - # Input.Key: ActionInputSupport.SUPPORTED, - # Input.Dial: ActionInputSupport.UNTESTED, - # Input.Touchscreen: ActionInputSupport.UNTESTED, - # } - # ) - # self.add_action_holder(self.next_ad_action_holder) + self.add_action_holder(self.ad_schedule_action_holder) + + def _setup_backend(self): + backend_path = os.path.join(self.PATH, "twitch_backend.py") + self.launch_backend(backend_path=backend_path, open_in_terminal=False, + venv_path=os.path.join(self.PATH, ".venv")) + self.wait_for_backend(tries=5) + + settings = self.get_settings() + client_id = settings.get('client_id', '') + client_secret = settings.get('client_secret', '') + auth_code = settings.get('auth_code', '') + + settings_path = os.path.join( + gl.DATA_PATH, 'settings', 'plugins', self.get_plugin_id_from_folder_name()) + os.makedirs(settings_path, exist_ok=True) + self.backend.set_token_path(os.path.join(settings_path, "keys.json")) + if client_id and client_secret and auth_code: + self.backend.auth_with_code( + client_id, client_secret, auth_code) + + def __init__(self): + super().__init__(use_legacy_locale=False) + + self.has_plugin_settings = True + self.lm = self.locale_manager + self.lm.set_to_os_default() + self._settings_manager = PluginSettings(self) + self.auth_callback_fn: callable = None + + self._add_icons() + self._add_colors() + self._setup_backend() + self._register_actions() try: with open(os.path.join(self.PATH, "manifest.json"), "r", encoding="UTF-8") as f: @@ -170,7 +175,6 @@ def __init__(self): "app_version": data.get("app-version", "0.0.0") } - # Register plugin self.register( plugin_name="Twitch Integration", github_repo="https://github.com/imdevinc/StreamControllerTwitchPlugin", diff --git a/manifest.json b/manifest.json index 328b973..547353f 100644 --- a/manifest.json +++ b/manifest.json @@ -1,5 +1,5 @@ { - "version": "1.4.0", + "version": "1.5.0", "thumbnail": "store/thumbnail.png", "id": "com_imdevinc_StreamControllerTwitchPlugin", "name": "Twitch Integration", @@ -9,8 +9,8 @@ "streaming", "twitch" ], - "minimum-app-version": "1.5.0-beta.4", - "app-version": "1.5.0-beta.8", + "minimum-app-version": "1.5.0-beta.9", + "app-version": "1.5.0-beta.9", "description": "Adds different controls for interacting with your Twitch stream", "short-description": "Control Twitch" } diff --git a/twitch_backend.py b/twitch_backend.py index b6861a4..7924562 100644 --- a/twitch_backend.py +++ b/twitch_backend.py @@ -7,7 +7,6 @@ from loguru import logger as log from twitchpy.client import Client -from twitchpy.errors import ClientError from streamcontroller_plugin_tools import BackendBase @@ -101,7 +100,7 @@ def get_viewers(self) -> str: return 'Not Live' return str(streams[0].viewer_count) - def toggle_chat_mode(self, mode: str) -> str: + def toggle_chat_mode(self, mode: str) -> bool: if not self.twitch: return self.validate_auth() @@ -109,7 +108,7 @@ def toggle_chat_mode(self, mode: str) -> str: updated = not getattr(current, mode) self.twitch.update_chat_settings( self.user_id, self.user_id, **{mode: updated}) - return str(updated) + return updated def get_chat_settings(self) -> dict: if not self.twitch: @@ -142,12 +141,12 @@ def play_ad(self, length: int) -> None: self.validate_auth() self.twitch.start_commercial(self.user_id, length) - def get_next_ad(self) -> datetime: + def get_next_ad(self) -> tuple[datetime, int]: if not self.twitch: return "Not Live" self.validate_auth() schedule = self.twitch.get_ad_schedule(self.user_id) - return schedule.next_ad_at + return schedule.next_ad_at, schedule.snooze_count def update_client_credentials(self, client_id: str, client_secret: str) -> None: if None in (client_id, client_secret) or "" in (client_id, client_secret):