Compare commits
No commits in common. "83936e01b1d376a0cc6433fd23d06a388f1f6059" and "9e8d05fc90096796b3d5605dc4e56d26ac3ccd64" have entirely different histories.
83936e01b1
...
9e8d05fc90
|
@ -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:
|
|
||||||
period = self._event_poll_frequency
|
|
||||||
else:
|
|
||||||
period = min(self._event_poll_frequency, timeout)
|
|
||||||
if self._pipe.poll():
|
|
||||||
incoming_message = self._pipe.recv()
|
incoming_message = self._pipe.recv()
|
||||||
if incoming_message['type'] == LIFECYCLE_MESSAGES.SHUTDOWN:
|
if incoming_message['type'] == LIFECYCLE_MESSAGES.SHUTDOWN:
|
||||||
raise GracefulShutdownException()
|
break
|
||||||
message_type = incoming_message['type']
|
message_type = incoming_message['type']
|
||||||
args = {k: v for k, v in incoming_message.items() if k != 'type'}
|
args = {k: v for k, v in incoming_message.items() if k != 'type'}
|
||||||
response = self.process_messages(message_type, args, self._next_state)
|
response = self.process_messages(message_type, args, self._next_state)
|
||||||
if response is None:
|
if response is None:
|
||||||
continue
|
pass
|
||||||
elif type(response) is tuple:
|
elif type(response) is tuple:
|
||||||
self.state, self._next_state = response
|
self.state, self._next_state = response
|
||||||
else:
|
else:
|
||||||
self.state = response
|
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
|
||||||
|
|
|
@ -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))):
|
||||||
|
|
|
@ -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
15
main.py
|
@ -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})
|
||||||
|
|
|
@ -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
|
|
|
@ -1 +0,0 @@
|
||||||
from chats import AUTHOR_TYPES
|
|
48
ui/App.py
48
ui/App.py
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue