diff --git a/TwitchActionBase.py b/TwitchActionBase.py deleted file mode 100644 index b627441..0000000 --- a/TwitchActionBase.py +++ /dev/null @@ -1,102 +0,0 @@ -from gi.repository import Gtk, Adw -import gi - -from src.backend.PluginManager.ActionBase import ActionBase - -gi.require_version("Gtk", "4.0") -gi.require_version("Adw", "1") - - -class TwitchActionBase(ActionBase): - def get_config_rows(self) -> list: - authed = self.plugin_base.backend.is_authed() - if not authed: - label = "actions.base.credentials.no-credentials" - css_style = "twitch-controller-red" - else: - label = "actions.base.credentials.authenticated" - css_style = "twitch-controller-green" - - self.status_label = Gtk.Label( - label=self.plugin_base.lm.get(label), css_classes=[css_style]) - self.client_id = Adw.EntryRow( - title=self.plugin_base.lm.get("actions.base.twitch_client_id")) - self.client_secret = Adw.PasswordEntryRow( - title=self.plugin_base.lm.get("actions.base.twitch_client_secret")) - self.auth_button = Gtk.Button(label=self.plugin_base.lm.get( - "actions.base.credentials.validate")) - self.auth_button.set_margin_top(10) - self.auth_button.set_margin_bottom(10) - - self.client_id.connect("notify::text", self._on_change_client_id) - self.client_secret.connect( - "notify::text", self._on_change_client_secret) - self.auth_button.connect("clicked", self._on_auth_clicked) - - group = Adw.PreferencesGroup() - group.set_title(self.plugin_base.lm.get( - "actions.base.credentials.title")) - group.add(self.client_id) - group.add(self.client_secret) - group.add(self.status_label) - group.add(self.auth_button) - - self.load_config() - return [group] - - def get_custom_config_area(self): - label = Gtk.Label( - use_markup=True, - label=f"{self.plugin_base.lm.get('actions.info.link.label')} {self.plugin_base.lm.get('actions.info.link.text')}") - return label - - def load_config(self): - settings = self.plugin_base.get_settings() - client_id = settings.setdefault("client_id", "") - client_secret = settings.setdefault("client_secret", "") - - self.client_id.set_text(client_id) - self.client_secret.set_text(client_secret) - - self.plugin_base.set_settings(settings) - - def _on_change_client_id(self, entry, _): - settings = self.plugin_base.get_settings() - settings["client_id"] = entry.get_text() - self.plugin_base.set_settings(settings) - - def _on_change_client_secret(self, entry, _): - settings = self.plugin_base.get_settings() - settings["client_secret"] = entry.get_text() - self.plugin_base.set_settings(settings) - - def _on_auth_clicked(self, _): - settings = self.plugin_base.get_settings() - client_id = settings.get('client_id') - client_secret = settings.get('client_secret') - if not client_id or not client_secret: - self._set_status(self.plugin_base.lm.get( - "actions.base.credentials.no-credentials"), True) - return - self.auth_button.set_sensitive(False) - self.plugin_base.auth_callback_fn = self.on_auth_completed - self.plugin_base.backend.update_client_credentials( - client_id, client_secret) - - def _set_status(self, message: str, is_error: bool = False): - self.status_label.set_label(message) - if is_error: - self.status_label.remove_css_class("twitch-controller-green") - self.status_label.add_css_class("twitch-controller-red") - else: - self.status_label.remove_css_class("twitch-controller-red") - self.status_label.add_css_class("twitch-controller-green") - - def on_auth_completed(self, success: bool) -> None: - self.auth_button.set_sensitive(True) - if success: - self._set_status(self.plugin_base.lm.get( - "actions.base.credentials.authenticated"), False) - else: - self._set_status(self.plugin_base.lm.get( - "actions.base.credentials.failed"), True) diff --git a/actions/ad_schedule.py b/actions/ad_schedule.py index ecb9b07..f92cfa6 100644 --- a/actions/ad_schedule.py +++ b/actions/ad_schedule.py @@ -2,17 +2,18 @@ import time import os -from loguru import logger as log +from src.backend.PluginManager.ActionBase import ActionBase -from plugins.com_imdevinc_StreamControllerTwitchPlugin.TwitchActionBase import TwitchActionBase +from loguru import logger as log # Currently an issue with TwitchPy that gets the wrong time format, can't use this yet -class NextAd(TwitchActionBase): +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( @@ -24,6 +25,8 @@ def on_ready(self): def ad_thread(self): while True: + if not self.get_is_present(): + return self.get_next_ad() time.sleep(1) diff --git a/actions/chat_mode.py b/actions/chat_mode.py index 455baeb..c2e9ac1 100644 --- a/actions/chat_mode.py +++ b/actions/chat_mode.py @@ -7,7 +7,7 @@ from loguru import logger as log -from plugins.com_imdevinc_StreamControllerTwitchPlugin.TwitchActionBase import TwitchActionBase +from src.backend.PluginManager.ActionBase import ActionBase gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") @@ -20,10 +20,11 @@ } -class ChatMode(TwitchActionBase): +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() @@ -89,6 +90,8 @@ def _on_change_mode(self, *_): 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}') diff --git a/actions/clip.py b/actions/clip.py index d91d155..455e7e9 100644 --- a/actions/clip.py +++ b/actions/clip.py @@ -3,10 +3,10 @@ from loguru import logger as log -from plugins.com_imdevinc_StreamControllerTwitchPlugin.TwitchActionBase import TwitchActionBase +from src.backend.PluginManager.ActionBase import ActionBase -class Clip(TwitchActionBase): +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) diff --git a/actions/marker.py b/actions/marker.py index 1f75c05..7a8fa5c 100644 --- a/actions/marker.py +++ b/actions/marker.py @@ -2,10 +2,10 @@ from loguru import logger as log -from plugins.com_imdevinc_StreamControllerTwitchPlugin.TwitchActionBase import TwitchActionBase +from src.backend.PluginManager.ActionBase import ActionBase -class Marker(TwitchActionBase): +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) diff --git a/actions/message.py b/actions/message.py index ffc58b1..004f682 100644 --- a/actions/message.py +++ b/actions/message.py @@ -5,13 +5,16 @@ from loguru import logger as log -from plugins.com_imdevinc_StreamControllerTwitchPlugin.TwitchActionBase import TwitchActionBase +from src.backend.PluginManager.ActionBase import ActionBase gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") -class SendMessage(TwitchActionBase): +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( diff --git a/actions/play_ad.py b/actions/play_ad.py index fd01aa7..50bf637 100644 --- a/actions/play_ad.py +++ b/actions/play_ad.py @@ -4,7 +4,7 @@ from loguru import logger as log -from plugins.com_imdevinc_StreamControllerTwitchPlugin.TwitchActionBase import TwitchActionBase +from src.backend.PluginManager.ActionBase import ActionBase gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") @@ -12,9 +12,13 @@ options = [30, 60, 90, 120] -class PlayAd(TwitchActionBase): +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) diff --git a/actions/snooze_ad.py b/actions/snooze_ad.py index 7e6c060..67bf7e4 100644 --- a/actions/snooze_ad.py +++ b/actions/snooze_ad.py @@ -3,10 +3,10 @@ from loguru import logger as log -from plugins.com_imdevinc_StreamControllerTwitchPlugin.TwitchActionBase import TwitchActionBase +from src.backend.PluginManager.ActionBase import ActionBase -class SnoozeAd(TwitchActionBase): +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) diff --git a/actions/viewers.py b/actions/viewers.py index 4d4b6a0..56c4d99 100644 --- a/actions/viewers.py +++ b/actions/viewers.py @@ -4,10 +4,10 @@ from loguru import logger as log -from plugins.com_imdevinc_StreamControllerTwitchPlugin.TwitchActionBase import TwitchActionBase +from src.backend.PluginManager.ActionBase import ActionBase -class Viewers(TwitchActionBase): +class Viewers(ActionBase): __viewer_thread: threading.Thread = None def __init__(self, *args, **kwargs): @@ -33,6 +33,8 @@ def on_key_down(self): def viewers_thread(self): while True: + if not self.get_is_present(): + return self.show_current_viewers() time.sleep(10) diff --git a/main.py b/main.py index 0973063..54b75e7 100644 --- a/main.py +++ b/main.py @@ -1,11 +1,17 @@ import os import globals as gl +import json + +from loguru import logger # Import StreamController modules from src.backend.PluginManager.PluginBase import PluginBase from src.backend.PluginManager.ActionHolder import ActionHolder +from src.backend.DeckManagement.InputIdentifier import Input +from src.backend.PluginManager.ActionInputSupport import ActionInputSupport # Import actions +from .settings import PluginSettings from .actions.message import SendMessage from .actions.chat_mode import ChatMode from .actions.clip import Clip @@ -20,9 +26,13 @@ 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 @@ -50,6 +60,11 @@ def __init__(self): action_base=SendMessage, action_id="com_imdevinc_StreamControllerTwitchPlugin::SendMessage", action_name="Send Chat Message", + action_support={ + Input.Key: ActionInputSupport.SUPPORTED, + Input.Dial: ActionInputSupport.UNTESTED, + Input.Touchscreen: ActionInputSupport.UNTESTED, + } ) self.add_action_holder(self.message_action_holder) @@ -58,6 +73,11 @@ def __init__(self): action_base=Viewers, action_id="com_imdevinc_StreamControllerTwitchPlugin::Viewers", action_name="Show Viewers", + action_support={ + Input.Key: ActionInputSupport.SUPPORTED, + Input.Dial: ActionInputSupport.UNTESTED, + Input.Touchscreen: ActionInputSupport.UNTESTED, + } ) self.add_action_holder(self.viewer_action_holder) @@ -65,7 +85,12 @@ def __init__(self): plugin_base=self, action_base=Marker, action_id="com_imdevinc_StreamControllerTwitchPlugin::Marker", - action_name="Create Stream Marker" + action_name="Create Stream Marker", + action_support={ + Input.Key: ActionInputSupport.SUPPORTED, + Input.Dial: ActionInputSupport.UNTESTED, + Input.Touchscreen: ActionInputSupport.UNTESTED, + } ) self.add_action_holder(self.marker_action_holder) @@ -73,7 +98,12 @@ def __init__(self): plugin_base=self, action_base=ChatMode, action_id="com_imdevinc_StreamControllerTwitchPlugin::ChatMode", - action_name="Toggle Chat Mode" + action_name="Toggle Chat Mode", + action_support={ + Input.Key: ActionInputSupport.SUPPORTED, + Input.Dial: ActionInputSupport.UNTESTED, + Input.Touchscreen: ActionInputSupport.UNTESTED, + } ) self.add_action_holder(self.chat_mode_action_holder) @@ -81,7 +111,12 @@ def __init__(self): plugin_base=self, action_base=Clip, action_id="com_imdevinc_StreamControllerTwitchPlugin::Clip", - action_name="Create Clip" + action_name="Create Clip", + action_support={ + Input.Key: ActionInputSupport.SUPPORTED, + Input.Dial: ActionInputSupport.UNTESTED, + Input.Touchscreen: ActionInputSupport.UNTESTED, + } ) self.add_action_holder(self.clip_action_holder) @@ -89,7 +124,12 @@ def __init__(self): plugin_base=self, action_base=SnoozeAd, action_id="com_imdevinc_StreamControllerTwitchPlugin::SnoozeAd", - action_name="Snooze Ad" + action_name="Snooze Ad", + action_support={ + Input.Key: ActionInputSupport.SUPPORTED, + Input.Dial: ActionInputSupport.UNTESTED, + Input.Touchscreen: ActionInputSupport.UNTESTED, + } ) self.add_action_holder(self.snooze_ad_action_holder) @@ -97,7 +137,12 @@ def __init__(self): plugin_base=self, action_base=PlayAd, action_id="com_imdevinc_StreamControllerTwitchPlugin::PlayAd", - action_name="Play Ad" + action_name="Play Ad", + action_support={ + Input.Key: ActionInputSupport.SUPPORTED, + Input.Dial: ActionInputSupport.UNTESTED, + Input.Touchscreen: ActionInputSupport.UNTESTED, + } ) self.add_action_holder(self.play_ad_action_holder) @@ -105,16 +150,32 @@ def __init__(self): # plugin_base=self, # action_base=NextAd, # action_id="com_imdevinc_StreamControllerTwitchPlugin::NextAd", - # action_name="Next Ad" + # 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) + try: + with open(os.path.join(self.PATH, "manifest.json"), "r", encoding="UTF-8") as f: + data = json.load(f) + except Exception as ex: + logger.error(ex) + data = {} + app_manifest = { + "plugin_version": data.get("version", "0.0.0"), + "app_version": data.get("app-version", "0.0.0") + } + # Register plugin self.register( plugin_name="Twitch Integration", github_repo="https://github.com/imdevinc/StreamControllerTwitchPlugin", - plugin_version="1.0.0", - app_version="1.1.1-alpha" + plugin_version=app_manifest.get("plugin_version"), + app_version=app_manifest.get("app_version") ) self.add_css_stylesheet(os.path.join(self.PATH, "style.css")) @@ -125,6 +186,9 @@ def save_auth_settings(self, client_id: str, client_secret: str, auth_code: str) settings['auth_code'] = auth_code self.set_settings(settings) - def on_auth_callback(self, success: bool) -> None: + def on_auth_callback(self, success: bool, message: str = "") -> None: if self.auth_callback_fn: - self.auth_callback_fn(success) + self.auth_callback_fn(success, message) + + def get_settings_area(self): + return self._settings_manager.get_settings_area() diff --git a/manifest.json b/manifest.json index 80ed573..328b973 100644 --- a/manifest.json +++ b/manifest.json @@ -1,5 +1,5 @@ { - "version": "1.0.1", + "version": "1.4.0", "thumbnail": "store/thumbnail.png", "id": "com_imdevinc_StreamControllerTwitchPlugin", "name": "Twitch Integration", @@ -10,7 +10,7 @@ "twitch" ], "minimum-app-version": "1.5.0-beta.4", - "app-version": "1.5.0-beta.4", + "app-version": "1.5.0-beta.8", "description": "Adds different controls for interacting with your Twitch stream", "short-description": "Control Twitch" } diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..e082a09 --- /dev/null +++ b/settings.py @@ -0,0 +1,113 @@ +from gi.repository import Gtk, Adw +import gi + +from loguru import logger + +from src.backend.PluginManager import PluginBase + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") + +KEY_CLIENT_SECRET = "client_secret" +KEY_CLIENT_ID = "client_id" + + +class PluginSettings: + _status_label: Gtk.Label + _client_id: Adw.EntryRow + _client_secret: Adw.PasswordEntryRow + _auth_button: Gtk.Button + + def __init__(self, plugin_base: PluginBase): + self._plugin_base = plugin_base + + def get_settings_area(self) -> Adw.PreferencesGroup: + if not self._plugin_base.backend.is_authed(): + self._status_label = Gtk.Label(label=self._plugin_base.lm.get( + "actions.base.credentials.failed"), css_classes=["twitch-controller-red"]) + else: + self._status_label = Gtk.Label(label=self._plugin_base.lm.get( + "actions.base.credentials.authenticated"), css_classes=["twitch-controller-green"]) + self._client_id = Adw.EntryRow( + title=self._plugin_base.lm.get("actions.base.twitch_client_id")) + self._client_secret = Adw.PasswordEntryRow( + title=self._plugin_base.lm.get("actions.base.twitch_client_secret")) + self._auth_button = Gtk.Button( + label=self._plugin_base.lm.get("actions.base.credentials.validate")) + self._auth_button.set_margin_top(10) + self._auth_button.set_margin_bottom(10) + self._client_id.connect("notify::text", self._on_change_client_id) + self._client_secret.connect( + "notify::text", self._on_change_client_secret) + self._auth_button.connect("clicked", self._on_auth_clicked) + + gh_link_label = self._plugin_base.lm.get("actions.info.link.label") + gh_link_text = self._plugin_base.lm.get("actions.info.link.text") + gh_label = Gtk.Label( + use_markup=True, label=f"{gh_link_label} {gh_link_text}") + + self._load_settings() + self._enable_auth() + + pref_group = Adw.PreferencesGroup() + pref_group.set_title(self._plugin_base.lm.get( + "actions.base.credentials.title")) + pref_group.add(self._status_label) + pref_group.add(self._client_id) + pref_group.add(self._client_secret) + pref_group.add(self._auth_button) + pref_group.add(gh_label) + return pref_group + + def _load_settings(self): + settings = self._plugin_base.get_settings() + client_id = settings.get(KEY_CLIENT_ID, "") + client_secret = settings.get(KEY_CLIENT_SECRET, "") + self._client_id.set_text(client_id) + self._client_secret.set_text(client_secret) + + def _update_status(self, message: str, is_error: bool): + style = "twitch-controller-red" if is_error else "twitch-controller-green" + self._status_label.set_text(message) + self._status_label.set_css_classes([style]) + + def _update_settings(self, key: str, value: str): + settings = self._plugin_base.get_settings() + settings[key] = value + self._plugin_base.set_settings(settings) + + def _on_change_client_id(self, entry, _): + val = entry.get_text().strip() + self._update_settings(KEY_CLIENT_ID, val) + self._enable_auth() + + def _on_change_client_secret(self, entry, _): + val = entry.get_text().strip() + self._update_settings(KEY_CLIENT_SECRET, val) + self._enable_auth() + + def _on_auth_clicked(self, _): + if not self._plugin_base.backend: + self._update_status("Failed to load backend", True) + return + settings = self._plugin_base.get_settings() + client_id = settings.get(KEY_CLIENT_ID) + client_secret = settings.get(KEY_CLIENT_SECRET) + self._plugin_base.auth_callback_fn = self._on_auth_completed + self._plugin_base.backend.update_client_credentials( + client_id, client_secret) + + def _enable_auth(self): + settings = self._plugin_base.get_settings() + client_secret = settings.get(KEY_CLIENT_SECRET, "") + client_id = settings.get(KEY_CLIENT_ID, "") + self._auth_button.set_sensitive( + len(client_id) > 0 and len(client_secret) > 0) + + def _on_auth_completed(self, success: bool, message: str = ""): + self._enable_auth() + if not message: + lm_key = "authenticated" if success else "failed" + message = self._plugin_base.lm.get( + f"actions.base.credentials.{lm_key}") + self._update_status(message, not success) diff --git a/twitch_backend.py b/twitch_backend.py index e37966a..b6861a4 100644 --- a/twitch_backend.py +++ b/twitch_backend.py @@ -172,8 +172,8 @@ def update_client_credentials(self, client_id: str, client_secret: str) -> None: url = f'https://id.twitch.tv/oauth2/authorize?{encoded_params}' check = requests.get(url, timeout=5) if check.status_code != 200: - print('invalid client ID') - self.auth_failed() + message = check.json().get("message") if check.json() else "Incorrect Client ID" + self.auth_failed(message) return webbrowser.open( @@ -185,7 +185,7 @@ def new_code(self, auth_code: str) -> None: def validate_auth(self) -> None: try: _ = self.twitch.get_streams(first=1, user_id=self.user_id) - except ClientError: + except Exception as ex: self.auth_with_code( self.client_id, self.client_secret, self.auth_code) @@ -205,9 +205,9 @@ def auth_with_code(self, client_id: str, client_secret: str, auth_code: str) -> log.error("failed to authenticate", e) self.auth_failed() - def auth_failed(self) -> None: + def auth_failed(self, message: str = "") -> None: self.user_id = None - self.frontend.on_auth_callback(False) + self.frontend.on_auth_callback(False, message) def is_authed(self) -> bool: return self.user_id != None