Compare commits

..

No commits in common. "83936e01b1d376a0cc6433fd23d06a388f1f6059" and "9e8d05fc90096796b3d5605dc4e56d26ac3ccd64" have entirely different histories.

7 changed files with 64 additions and 158 deletions

View File

@ -2,11 +2,6 @@ from abc import ABC, abstractmethod
from multiprocessing import Process, Pipe, Queue from multiprocessing import Process, Pipe, Queue
from aenum import Enum, auto from aenum import Enum, auto
from traceback import format_exception from traceback import format_exception
import time
class GracefulShutdownException(Exception):
pass
class LIFECYCLE_MESSAGES(Enum): class LIFECYCLE_MESSAGES(Enum):
@ -14,20 +9,14 @@ class LIFECYCLE_MESSAGES(Enum):
class ChatProcess(Process, ABC): class ChatProcess(Process, ABC):
def __init__(self, event_poll_frequency): def __init__(self):
super().__init__() super().__init__()
self._event_poll_frequency = event_poll_frequency
self._message_queue = Queue() self._message_queue = Queue()
self._pipe, self._caller_pipe = Pipe() self._pipe, self._caller_pipe = Pipe()
self._state = None self._state = None
self._next_state = None self._next_state = None
@abstractmethod
def keybinds(self):
return []
@abstractmethod @abstractmethod
def loop(self, next_state): def loop(self, next_state):
pass pass
@ -94,38 +83,22 @@ class ChatProcess(Process, ABC):
else: else:
self.state = response self.state = response
while timeout is None or timeout > 0: if self._pipe.poll(timeout):
if timeout is None: incoming_message = self._pipe.recv()
period = self._event_poll_frequency if incoming_message['type'] == LIFECYCLE_MESSAGES.SHUTDOWN:
break
message_type = incoming_message['type']
args = {k: v for k, v in incoming_message.items() if k != 'type'}
response = self.process_messages(message_type, args, self._next_state)
if response is None:
pass
elif type(response) is tuple:
self.state, self._next_state = response
else: else:
period = min(self._event_poll_frequency, timeout) self.state = response
if self._pipe.poll():
incoming_message = self._pipe.recv()
if incoming_message['type'] == LIFECYCLE_MESSAGES.SHUTDOWN:
raise GracefulShutdownException()
message_type = incoming_message['type']
args = {k: v for k, v in incoming_message.items() if k != 'type'}
response = self.process_messages(message_type, args, self._next_state)
if response is None:
continue
elif type(response) is tuple:
self.state, self._next_state = response
else:
self.state = response
if timeout is None:
timeout = 0
time.sleep(period)
if timeout is not None:
timeout -= period
if timeout is not None and timeout > 0:
time.sleep(timeout)
except GracefulShutdownException:
return 0
except Exception as e: except Exception as e:
print(f'Failure in {self.__class__.__name__}: {e}') print(f'Failure in {self.__class__.__name__}: {e}')
print(''.join(format_exception(None, e, e.__traceback__))) print(''.join(format_exception(None, e, e.__traceback__)))
return -1 return -1
return 0

View File

@ -1,3 +1,4 @@
import time
import random import random
from enum import Enum, auto from enum import Enum, auto
@ -11,38 +12,24 @@ class CONTROL_MESSAGES(Enum):
START_STOP = auto() START_STOP = auto()
class STATES(Enum):
PAUSED = auto()
RUNNING = auto()
class FakeChat(ChatProcess): class FakeChat(ChatProcess):
def __init__(self, *args): def __init__(self):
super().__init__(*args) super().__init__()
self._max_messages_per_chunk = 1 self._max_messages_per_chunk = 1
self._max_delay = 10 self._max_delay = 10
self._running = False
@property
def keybinds(self):
return {
ord('f'): {'type': CONTROL_MESSAGES.START_STOP}
}
def process_messages(self, message_type, args, next_state): def process_messages(self, message_type, args, next_state):
if message_type == CONTROL_MESSAGES.START_STOP: if message_type == CONTROL_MESSAGES.START_STOP:
running = not self.state == STATES.RUNNING self._running = not self._running
text = 'Fake chat activated!' if running else 'Disabled fake chat' text = 'Fake chat activated!' if self._running else 'Disabled fake chat'
self._message_queue.put({ self._message_queue.put({
'text': text, 'text': text,
'author_name': 'Fake Chat', 'author_id': self.__class__.__name__, 'author_type': AUTHOR_TYPES.SYSTEM 'author_name': 'Fake Chat', 'author_id': self.__class__.__name__, 'author_type': AUTHOR_TYPES.SYSTEM
}) })
return STATES.RUNNING if running else STATES.PAUSED
def loop(self, next_state): def loop(self, next_state):
if self.state is None: if not self._running:
return STATES.PAUSED
elif self.state == STATES.PAUSED:
return None return None
while range(int(random.random() * (self._max_messages_per_chunk + 1))): while range(int(random.random() * (self._max_messages_per_chunk + 1))):

View File

@ -27,8 +27,8 @@ class YoutubeLiveProcess(ChatProcess):
GOOGLE_OAUTH_TOKEN_URL = 'https://accounts.google.com/o/oauth2/token' GOOGLE_OAUTH_TOKEN_URL = 'https://accounts.google.com/o/oauth2/token'
YOUTUBE_API_URL = 'https://www.googleapis.com/youtube/v3' YOUTUBE_API_URL = 'https://www.googleapis.com/youtube/v3'
def __init__(self, client_secrets, *args): def __init__(self, client_secrets):
super().__init__(*args) super().__init__()
self._client_secrets = client_secrets self._client_secrets = client_secrets
self._state_machine = self.bind_to_states(STATES) self._state_machine = self.bind_to_states(STATES)
@ -39,10 +39,22 @@ class YoutubeLiveProcess(ChatProcess):
self._live_chat_id = None self._live_chat_id = None
self._page_token = None self._page_token = None
@property @classmethod
def keybinds(self): def normalize_message(cls, message):
if message['authorDetails']['isChatOwner']:
author_type = AUTHOR_TYPES.OWNER
elif message['authorDetails']['isChatModerator']:
author_type = AUTHOR_TYPES.MODERATOR
elif message['authorDetails']['isChatSponsor']:
author_type = AUTHOR_TYPES.PATRON
else:
author_type = AUTHOR_TYPES.USER
return { return {
ord('r'): {'type': CONTROL_MESSAGES.RESTART} 'text': message['snippet']['displayMessage'],
'author_name': message['authorDetails']['displayName'],
'author_id': message['authorDetails']['channelId'],
'author_type': author_type,
} }
def loop(self, next_state): def loop(self, next_state):
@ -144,24 +156,6 @@ class YoutubeLiveProcess(ChatProcess):
}) })
return None, None return None, None
@classmethod
def normalize_message(cls, message):
if message['authorDetails']['isChatOwner']:
author_type = AUTHOR_TYPES.OWNER
elif message['authorDetails']['isChatModerator']:
author_type = AUTHOR_TYPES.MODERATOR
elif message['authorDetails']['isChatSponsor']:
author_type = AUTHOR_TYPES.PATRON
else:
author_type = AUTHOR_TYPES.USER
return {
'text': message['snippet']['displayMessage'],
'author_name': message['authorDetails']['displayName'],
'author_id': message['authorDetails']['channelId'],
'author_type': author_type,
}
def request_oauth_consent(self): def request_oauth_consent(self):
params = { params = {
'client_id': self._client_secrets['client_id'], 'client_id': self._client_secrets['client_id'],

15
main.py
View File

@ -1,29 +1,25 @@
import json import json
import os import os
import time
from chats import YoutubeLive from chats import YoutubeLive
from chats.ChatProcess import LIFECYCLE_MESSAGES from chats.ChatProcess import LIFECYCLE_MESSAGES
from chats.FakeChat import FakeChat from chats.FakeChat import FakeChat
from ui.App import App from ui.App import App
from plugins.PogCount import PogCounter
EVENT_POLL_FREQ = 0.1
GOOGLE_API_SECRETS_PATH = 'googleapi.secret.json' GOOGLE_API_SECRETS_PATH = 'googleapi.secret.json'
if __name__ == '__main__': if __name__ == '__main__':
chat_processes = [FakeChat(EVENT_POLL_FREQ)] chat_processes = [FakeChat()]
if os.path.exists(GOOGLE_API_SECRETS_PATH): if os.path.exists(GOOGLE_API_SECRETS_PATH):
with open(GOOGLE_API_SECRETS_PATH, 'r') as f: with open(GOOGLE_API_SECRETS_PATH, 'r') as f:
client_secrets = json.load(f)['installed'] client_secrets = json.load(f)['installed']
chat_processes.append(YoutubeLive.YoutubeLiveProcess(client_secrets, EVENT_POLL_FREQ))
chat_processes.append(YoutubeLive.YoutubeLiveProcess(client_secrets))
else: else:
print('No client secrets - disabling youtube chat client. Hit "f" to enable a testing client') print('No client secrets - disabling youtube chat client. Hit "f" to enable a testing client')
plugins = [PogCounter('../live-status.txt', prefix='Pog count: ')] app = App(chat_processes)
app = App(chat_processes, plugins)
for process in chat_processes: for process in chat_processes:
process.start() process.start()
app.start() app.start()
@ -41,7 +37,6 @@ if __name__ == '__main__':
'type': YoutubeLive.CONTROL_MESSAGES.OAUTH_CONSENT_TOKEN, 'type': YoutubeLive.CONTROL_MESSAGES.OAUTH_CONSENT_TOKEN,
'token': code 'token': code
}) })
time.sleep(EVENT_POLL_FREQ)
for process in chat_processes: for process in chat_processes:
process.control_pipe.send({'type': LIFECYCLE_MESSAGES.SHUTDOWN}) process.control_pipe.send({'type': LIFECYCLE_MESSAGES.SHUTDOWN})

View File

@ -1,36 +0,0 @@
class PogCounter:
def __init__(self, out, prefix=None):
self.out = out
self.prefix = prefix
self._count = 0
self._timer = 0
self._dirty = True
def normalize(self, text):
def remove_dups(text):
last_char = None
for char in list(text):
if last_char == char:
continue
last_char = char
yield char
return ''.join(remove_dups(text.lower()))
def tick(self, dt):
self._timer += dt
if self._timer > 1 and self._dirty:
with open(self.out, 'w') as f:
f.write(f"{self.prefix}{self._count}")
self._timer = 0
self._dirty = False
def handle_event(self, event):
pass
def on_message(self, message):
normalized = self.normalize(message['text'])
if any(phrase in normalized for phrase in ['pog', 'p o g']):
self._count += 1
self._dirty = True
return message

View File

@ -1 +0,0 @@
from chats import AUTHOR_TYPES

View File

@ -11,20 +11,19 @@ from chats import FakeChat, YoutubeLive
class App(Process): class App(Process):
def __init__(self, chat_processes, plugins): def __init__(self, chat_processes):
super().__init__() super().__init__()
self._chat_processes = chat_processes self._running = False
self._plugins = plugins self._display_surf = None
self.size = self.width, self.height = 500, 1200
self._title = 'Chat monitor' self._title = 'Chat monitor'
self._chat_processes = {proc.__class__: proc for proc in chat_processes}
self._log_scroll = 0
self._scroll_speed = 20 self._scroll_speed = 20
self._max_fps = 60 self._max_fps = 60
self.chat_log = deque(maxlen=100) self.chat_log = deque(maxlen=100)
self._log_scroll = 0 self.size = self.width, self.height = 500, 1200
self._running = False
self._display_surf = None
@property @property
def deamon(self): def deamon(self):
@ -39,30 +38,25 @@ class App(Process):
def on_event(self, event): def on_event(self, event):
if event.type == pygame.QUIT: if event.type == pygame.QUIT:
self._running = False self._running = False
elif event.type == pygame.KEYDOWN: elif event.type == pygame.KEYDOWN and event.key == ord('r'):
for process in self._chat_processes: self._chat_processes[YoutubeLive.YoutubeLiveProcess].control_pipe.send(
bound_action = process.keybinds.get(event.key) {'type': YoutubeLive.CONTROL_MESSAGES.RESTART}
if bound_action is not None: )
process.control_pipe.send(bound_action) elif event.type == pygame.KEYDOWN and event.key == ord('f'):
self._chat_processes[FakeChat.FakeChat].control_pipe.send(
{'type': FakeChat.CONTROL_MESSAGES.START_STOP}
)
def tick(self, dt): def tick(self, dt):
for process in self._chat_processes:
if not process.message_queue.empty():
message = process.message_queue.get()
for plugin in self._plugins:
message = plugin.on_message(message)
if message is None:
break
else:
message_view = MessageView(message, self.size)
self.chat_log.append(message_view)
self._log_scroll += message_view.rect.height
for message in self.chat_log: for message in self.chat_log:
message.tick(dt) message.tick(dt)
for plugin in self._plugins: for name, process in self._chat_processes.items():
plugin.tick(dt) if not process.message_queue.empty():
new_message = process.message_queue.get()
message = MessageView(new_message, self.size)
self.chat_log.append(message)
self._log_scroll += message.rect.height
if (self._log_scroll > 0): if (self._log_scroll > 0):
self._log_scroll -= min(max((dt / 1000) * (self._log_scroll) * self._scroll_speed, 0.25), self.height / 4) self._log_scroll -= min(max((dt / 1000) * (self._log_scroll) * self._scroll_speed, 0.25), self.height / 4)