From 3cf1c5721601118e32f1bd4c40341723c2d4c053 Mon Sep 17 00:00:00 2001 From: Devin Collins Date: Fri, 28 Feb 2025 19:30:04 -0800 Subject: [PATCH] feat(ads): Add ad actions --- actions/ad_schedule.py | 39 +++++++++++++++++++++++++ actions/play_ad.py | 65 +++++++++++++++++++++++++++++++++++++++++ actions/snooze_ad.py | 19 ++++++++++++ assets/delay.png | Bin 0 -> 2764 bytes assets/money.png | Bin 0 -> 4445 bytes locales/en_US.json | 3 +- main.py | 27 +++++++++++++++++ twitch_backend.py | 22 +++++++++++++- 8 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 actions/ad_schedule.py create mode 100644 actions/play_ad.py create mode 100644 actions/snooze_ad.py create mode 100644 assets/delay.png create mode 100644 assets/money.png diff --git a/actions/ad_schedule.py b/actions/ad_schedule.py new file mode 100644 index 0000000..ecb9b07 --- /dev/null +++ b/actions/ad_schedule.py @@ -0,0 +1,39 @@ +import threading +import time +import os + +from loguru import logger as log + +from plugins.com_imdevinc_StreamControllerTwitchPlugin.TwitchActionBase import TwitchActionBase + +# Currently an issue with TwitchPy that gets the wrong time format, can't use this yet + + +class NextAd(TwitchActionBase): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__ad_thread: threading.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.__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: + 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/play_ad.py b/actions/play_ad.py new file mode 100644 index 0000000..fd01aa7 --- /dev/null +++ b/actions/play_ad.py @@ -0,0 +1,65 @@ +import os +import gi +from gi.repository import Adw, Gtk + +from loguru import logger as log + +from plugins.com_imdevinc_StreamControllerTwitchPlugin.TwitchActionBase import TwitchActionBase + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") + +options = [30, 60, 90, 120] + + +class PlayAd(TwitchActionBase): + _time: int = 30 + + 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 new file mode 100644 index 0000000..7e6c060 --- /dev/null +++ b/actions/snooze_ad.py @@ -0,0 +1,19 @@ + +import os + +from loguru import logger as log + +from plugins.com_imdevinc_StreamControllerTwitchPlugin.TwitchActionBase import TwitchActionBase + + +class SnoozeAd(TwitchActionBase): + 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/assets/delay.png b/assets/delay.png new file mode 100644 index 0000000000000000000000000000000000000000..d4ec1479bb2c10fedccb425b517ca22ae41e513b GIT binary patch literal 2764 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 literal 0 HcmV?d00001 diff --git a/locales/en_US.json b/locales/en_US.json index 7837e91..3f57611 100644 --- a/locales/en_US.json +++ b/locales/en_US.json @@ -11,5 +11,6 @@ "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.info.link.text": "GitHub", + "actions.play_ad.time.label": "Ad Length" } diff --git a/main.py b/main.py index c49100d..0973063 100644 --- a/main.py +++ b/main.py @@ -11,6 +11,9 @@ 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 class PluginTemplate(PluginBase): @@ -82,6 +85,30 @@ def __init__(self): ) self.add_action_holder(self.clip_action_holder) + self.snooze_ad_action_holder = ActionHolder( + plugin_base=self, + action_base=SnoozeAd, + action_id="com_imdevinc_StreamControllerTwitchPlugin::SnoozeAd", + action_name="Snooze Ad" + ) + self.add_action_holder(self.snooze_ad_action_holder) + + self.play_ad_action_holder = ActionHolder( + plugin_base=self, + action_base=PlayAd, + action_id="com_imdevinc_StreamControllerTwitchPlugin::PlayAd", + action_name="Play Ad" + ) + 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" + # ) + # self.add_action_holder(self.next_ad_action_holder) + # Register plugin self.register( plugin_name="Twitch Integration", diff --git a/twitch_backend.py b/twitch_backend.py index 237d3b8..e37966a 100644 --- a/twitch_backend.py +++ b/twitch_backend.py @@ -3,6 +3,7 @@ from urllib.parse import urlparse, parse_qs, urlencode import threading import requests +from datetime import datetime from loguru import logger as log from twitchpy.client import Client @@ -129,6 +130,25 @@ def send_message(self, message: str, user_name: str) -> None: channel_id = self.get_channel_id(user_name) or self.user_id self.twitch.send_chat_message(channel_id, self.user_id, message) + def snooze_ad(self) -> None: + if not self.twitch: + return + self.validate_auth() + self.twitch.snooze_next_ad(self.user_id) + + def play_ad(self, length: int) -> None: + if not self.twitch: + return + self.validate_auth() + self.twitch.start_commercial(self.user_id, length) + + def get_next_ad(self) -> datetime: + if not self.twitch: + return "Not Live" + self.validate_auth() + schedule = self.twitch.get_ad_schedule(self.user_id) + return schedule.next_ad_at + 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): return @@ -138,7 +158,7 @@ def update_client_credentials(self, client_id: str, client_secret: str) -> None: 'client_id': client_id, 'redirect_uri': 'http://localhost:3000/auth', 'response_type': 'code', - 'scope': 'user:write:chat channel:manage:broadcast moderator:manage:chat_settings clips:edit channel:read:subscriptions' + 'scope': 'user:write:chat channel:manage:broadcast moderator:manage:chat_settings clips:edit channel:read:subscriptions channel:edit:commercial channel:manage:ads channel:read:ads' } encoded_params = urlencode(params) if not self.httpd: