Added first working chat recorder

This commit is contained in:
Vitalii Lebedynskyi
2022-08-09 13:17:29 +03:00
parent e00fb5bd70
commit 86ac82f28d
7 changed files with 178 additions and 145 deletions

85
clipper/api.py Normal file
View File

@@ -0,0 +1,85 @@
import enum
import logging
import socket
import time
from twitchAPI import Twitch, AuthScope
logger = logging.getLogger(__name__)
TW_CHAT_SERVER = 'irc.chat.twitch.tv'
TW_CHAT_PORT = 6667
class TwitchStreamStatus(enum.Enum):
ONLINE = 0
OFFLINE = 1
NOT_FOUND = 2
UNAUTHORIZED = 3
ERROR = 4
class TwitchApi:
_cached_token = None
def __init__(self, client_id, client_secret):
self.client_id = client_id
self.client_secret = client_secret
self.twitch = Twitch(self.client_id, self.client_secret, target_app_auth_scope=[AuthScope.CHAT_READ])
self.twitch.authenticate_app([AuthScope.CHAT_READ])
def get_user_status(self, streamer):
try:
streams = self.twitch.get_streams(user_login=streamer)
if streams is None or len(streams) < 1:
return TwitchStreamStatus.OFFLINE
else:
return TwitchStreamStatus.ONLINE
except:
return TwitchStreamStatus.ERROR
def start_chat(self, streamer_name):
logger.info("Connecting to %s:%s", TW_CHAT_SERVER, TW_CHAT_PORT)
connection = ChatConnection(streamer_name, self.twitch)
self.twitch.get_app_token()
connection.run()
class ChatConnection:
connection = None
def __init__(self, streamer_name, twitch):
self.twitch = twitch
self.streamer_name = streamer_name
def run(self):
# Need to verify channel name.. case sensative
streams = self.twitch.get_streams(user_login=self.streamer_name)
if streams is None or len(streams) < 1:
return
channel = streams["data"][0]["user_login"]
self.connection = socket.socket()
self.connection.connect((TW_CHAT_SERVER, TW_CHAT_PORT))
self.connection.send(f"PASS sdwrerrwsdawerew\n".encode("utf-8"))
self.connection.send(f"NICK justinfan123\n".encode("utf-8"))
self.connection.send(f"JOIN #{channel}\n".encode("utf-8"))
while True:
resp = self.connection.recv(4096).decode('utf-8')
logger.warning(f"Message: {resp}")
time.sleep(1)
def disconnect(self, msg="I'll be back!"):
logger.info("Disconnected ")
def on_welcome(self, c, e):
logger.info("Joining channel ")
c.join("#" + self.streamer_name)
logger.info("Joined????? ")
def on_pubmsg(self, c, e):
logger.info("On message %s <-> %s", c, e)

View File

@@ -1,48 +0,0 @@
import threading
import requests
import logging
TOKEN_URL = "https://id.twitch.tv/oauth2/token?client_id={0}&client_secret={1}&grant_type=client_credentials"
logger = logging.getLogger(__name__)
def synchronized(func):
func.__lock__ = threading.Lock()
def synced_func(*args, **kws):
with func.__lock__:
return func(*args, **kws)
return synced_func
class TwitchAuthenticator:
cached_token = None
def __init__(self, client_id, client_secret, username):
self.username = username
self.client_id = client_id
self.client_secret = client_secret
self.token_url = TOKEN_URL.format(self.client_id, self.client_secret)
@synchronized
def get_token(self):
if self.cached_token is None:
self._fetch_token()
return self.cached_token
@synchronized
def refresh_token(self):
# TODO what if both will call refresh ?
self._fetch_token()
return self.cached_token
def _fetch_token(self):
token_response = requests.post(self.token_url, timeout=15)
token_response.raise_for_status()
token = token_response.json()
self.cached_token = token["access_token"]
logger.info("Fetched new token %s", self.cached_token)

View File

@@ -1,19 +1,17 @@
import twitch
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class TwitchChatRecorder: class TwitchChatRecorder:
def __init__(self, authenticator, streamer_name, on_finish=None): def __init__(self, api, streamer_name, ignored_prefix=["!"], on_finish=None):
self.ignored_prefix = ignored_prefix
self.on_finish = on_finish self.on_finish = on_finish
self.streamer_name = streamer_name self.streamer_name = streamer_name
self.authenticator = authenticator self.api = api
def run(self): def run(self):
chat = twitch.Chat(self.streamer_name, self.authenticator.username, self.authenticator.get_token()) self.api.start_chat(self.streamer_name)
logger.info("Subscribing to chat for %s as %s", self.streamer_name, self.authenticator.username)
chat.subscribe(on_next=self.on_message, on_error=self.on_error)
def on_error(self, error): def on_error(self, error):
logger.error(error) logger.error(error)

81
clipper/recorder.py Normal file
View File

@@ -0,0 +1,81 @@
import logging
import time
import sys
from clipper.api import TwitchApi, TwitchStreamStatus
from clipper.chat import TwitchChatRecorder
logger = logging.getLogger(__name__)
class RecorderConfig:
def __init__(self, tw_client, tw_secret, tw_streamer, tw_quality, output_path):
self.output_path = output_path
self.tw_quality = tw_quality
self.tw_streamer = tw_streamer
self.tw_secret = tw_secret
self.tw_client = tw_client
class Recorder:
def __init__(self, config):
self.config = config
self.api = TwitchApi(config.tw_client, config.tw_secret)
# self.video_recorder = TwitchVideoRecorder(self.api,)
self.chat_recorder = TwitchChatRecorder(self.api, config.tw_streamer)
def run(self):
while True:
logger.info("Start watching streamer %s", self.config.tw_streamer)
status = self.api.get_user_status(self.config.tw_streamer)
if status == TwitchStreamStatus.ONLINE:
logger.info("Streamer %s is online. Start recording", self.config.tw_streamer)
self.chat_recorder.run()
# TODO run video record and join to it.. Run 2 threads?
logger.info("Streamer %s finished his stream", self.config.tw_streamer)
time.sleep(15)
if status == TwitchStreamStatus.OFFLINE:
logger.info("Streamer %s is offline. Waiting for it 60 sec", self.config.tw_streamer)
time.sleep(60)
if status == TwitchStreamStatus.ERROR:
logger.critical("Error occurred. Exit", self.config.tw_streamer)
sys.exit(1)
# def run(self):
# logger.info("Start watching streamer %s", self.config.tw_streamer)
#
# while True:
# status, info = self.api.check_user_status(self.config.tw_streamer)
# if status == TwitchStreamStatus.NOT_FOUND:
# logger.error("streamer_name not found, invalid streamer_name or typo")
# sys.exit(1)
# elif status == TwitchStreamStatus.ERROR:
# logger.error("%s unexpected error. will try again in 5 minutes",
# datetime.datetime.now().strftime("%Hh%Mm%Ss"))
# time.sleep(300)
# elif status == TwitchStreamStatus.OFFLINE:
# logger.info("%s currently offline, checking again in %s seconds", self.streamer_name,
# self.refresh_timeout)
# time.sleep(self.refresh_timeout)
# elif status == TwitchStreamStatus.UNAUTHORIZED:
# logger.info("unauthorized, will attempt to log back in immediately")
# self.access_token = self.authenticator.refresh_token()
# elif status == TwitchStreamStatus.ONLINE:
# logger.info("%s online, stream recording in session", self.streamer_name)
#
# channels = info["data"]
# channel = next(iter(channels), None)
#
# recorded_filename = self.record_stream(channel, recording_path)
#
# logger.info("recording stream is done")
#
# if self.on_finish is not None:
# self.on_finish(channel, recorded_filename)
#
# time.sleep(self.refresh_timeout)
# def start_record(self):
# pass

View File

@@ -1,24 +1,11 @@
import datetime import datetime
import enum
import logging import logging
import os import os
import subprocess import subprocess
import requests
import time
HELIX_STREAM_URL = "https://api.twitch.tv/helix/streams?user_login={0}"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class TwitchStreamStatus(enum.Enum):
ONLINE = 0
OFFLINE = 1
NOT_FOUND = 2
UNAUTHORIZED = 3
ERROR = 4
class TwitchVideoRecorder: class TwitchVideoRecorder:
access_token = None access_token = None
@@ -49,64 +36,7 @@ class TwitchVideoRecorder:
self.refresh_timeout = 15 self.refresh_timeout = 15
logger.warning("check interval set to 15 seconds") logger.warning("check interval set to 15 seconds")
self.loop_check(recording_path) self.record_stream(self.streamer_name, recording_path)
def loop_check(self, recording_path):
logger.info("checking for %s every %s seconds, recording with %s quality",
self.streamer_name, self.refresh_timeout, self.quality)
while True:
status, info = self.check_user()
if status == TwitchStreamStatus.NOT_FOUND:
logger.error("streamer_name not found, invalid streamer_name or typo")
time.sleep(self.refresh_timeout)
elif status == TwitchStreamStatus.ERROR:
logger.error("%s unexpected error. will try again in 5 minutes",
datetime.datetime.now().strftime("%Hh%Mm%Ss"))
time.sleep(300)
elif status == TwitchStreamStatus.OFFLINE:
logger.info("%s currently offline, checking again in %s seconds", self.streamer_name,
self.refresh_timeout)
time.sleep(self.refresh_timeout)
elif status == TwitchStreamStatus.UNAUTHORIZED:
logger.info("unauthorized, will attempt to log back in immediately")
self.access_token = self.authenticator.refresh_token()
elif status == TwitchStreamStatus.ONLINE:
logger.info("%s online, stream recording in session", self.streamer_name)
channels = info["data"]
channel = next(iter(channels), None)
recorded_filename = self.record_stream(channel, recording_path)
logger.info("recording stream is done")
if self.on_finish is not None:
self.on_finish(channel, recorded_filename)
time.sleep(self.refresh_timeout)
# TODO use twitch library instead of pure requests
def check_user(self):
info = None
status = TwitchStreamStatus.ERROR
try:
headers = {"Client-ID": self.authenticator.client_id,
"Authorization": "Bearer {}".format(self.authenticator.get_token())}
r = requests.get(HELIX_STREAM_URL.format(self.streamer_name), headers=headers, timeout=15)
r.raise_for_status()
info = r.json()
if info is None or not info["data"]:
status = TwitchStreamStatus.OFFLINE
else:
status = TwitchStreamStatus.ONLINE
except requests.exceptions.RequestException as e:
if e.response:
if e.response.status_code == 401:
status = TwitchStreamStatus.UNAUTHORIZED
if e.response.status_code == 404:
status = TwitchStreamStatus.NOT_FOUND
return status, info
def record_stream(self, channel, recording_path): def record_stream(self, channel, recording_path):
filename = self.streamer_name + " - " + datetime.datetime.now() \ filename = self.streamer_name + " - " + datetime.datetime.now() \

24
main.py
View File

@@ -2,18 +2,14 @@ import argparse
import os import os
import sys import sys
import logging import logging
import threading
from clipper.auth import TwitchAuthenticator from clipper import recorder
from clipper.chat import TwitchChatRecorder
from clipper.video import TwitchVideoRecorder
def parse_arguments(): def parse_arguments():
parser = argparse.ArgumentParser(description='Twitch highlighter') parser = argparse.ArgumentParser(description='Twitch highlighter')
parser.add_argument('--client', "-c", help='Twitch client id', required=True, dest="tw_client") parser.add_argument('--client', "-c", help='Twitch client id', required=True, dest="tw_client")
parser.add_argument('--secret', "-s", help='Twitch secret id', required=True, dest="tw_secret") parser.add_argument('--secret', "-s", help='Twitch secret id', required=True, dest="tw_secret")
parser.add_argument('--user', "-u", help='Twitch username id', required=True, dest="tw_username")
parser.add_argument('--streamer', "-t", help='Twitch streamer username', required=True, dest="tw_streamer") parser.add_argument('--streamer', "-t", help='Twitch streamer username', required=True, dest="tw_streamer")
parser.add_argument('--quality', "-q", help='Video downloading quality', dest="tw_quality", default="360p") parser.add_argument('--quality', "-q", help='Video downloading quality', dest="tw_quality", default="360p")
parser.add_argument('--output_path', "-o", help='Video download folder', dest="output_path", default=os.getcwd()) parser.add_argument('--output_path', "-o", help='Video download folder', dest="output_path", default=os.getcwd())
@@ -31,23 +27,13 @@ def on_chat_recorded(streamer, filename):
if __name__ == "__main__": if __name__ == "__main__":
# TODO configure logging # TODO configure logging
# TODO rework authentication and status check. recorder should only record
logging.basicConfig(stream=sys.stdout, level=logging.INFO) logging.basicConfig(stream=sys.stdout, level=logging.INFO)
args = parse_arguments() args = parse_arguments()
authenticator = TwitchAuthenticator(args.tw_client, args.tw_secret, args.tw_username) config = recorder.RecorderConfig(args.tw_client, args.tw_secret, args.tw_streamer, args.tw_quality, args.output_path)
recorder = recorder.Recorder(config)
video_recorder = TwitchVideoRecorder(authenticator, args.tw_streamer, args.output_path, recorder.run()
args.tw_quality, on_finish=on_video_recorded)
chat_recorder = TwitchChatRecorder(authenticator, args.tw_streamer, on_finish=on_chat_recorded)
# video_thread = threading.Thread(target=video_recorder.run)
# video_thread.start()
chat_thread = threading.Thread(target=chat_recorder.run)
chat_thread.start()
chat_thread.join()
# Twitch downloader # Twitch downloader
# def main(argv): # def main(argv):

View File

@@ -1,3 +1,4 @@
requests==2.28.1 requests==2.28.1
streamlink==4.2.0 streamlink==4.2.0
twitch-python==0.0.20 twitchAPI==2.5.7
irc==20.1.0