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
logger = logging.getLogger(__name__)
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.streamer_name = streamer_name
self.authenticator = authenticator
self.api = api
def run(self):
chat = twitch.Chat(self.streamer_name, self.authenticator.username, self.authenticator.get_token())
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)
self.api.start_chat(self.streamer_name)
def on_error(self, 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 enum
import logging
import os
import subprocess
import requests
import time
HELIX_STREAM_URL = "https://api.twitch.tv/helix/streams?user_login={0}"
logger = logging.getLogger(__name__)
class TwitchStreamStatus(enum.Enum):
ONLINE = 0
OFFLINE = 1
NOT_FOUND = 2
UNAUTHORIZED = 3
ERROR = 4
class TwitchVideoRecorder:
access_token = None
@@ -49,64 +36,7 @@ class TwitchVideoRecorder:
self.refresh_timeout = 15
logger.warning("check interval set to 15 seconds")
self.loop_check(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
self.record_stream(self.streamer_name, recording_path)
def record_stream(self, channel, recording_path):
filename = self.streamer_name + " - " + datetime.datetime.now() \

24
main.py
View File

@@ -2,18 +2,14 @@ import argparse
import os
import sys
import logging
import threading
from clipper.auth import TwitchAuthenticator
from clipper.chat import TwitchChatRecorder
from clipper.video import TwitchVideoRecorder
from clipper import recorder
def parse_arguments():
parser = argparse.ArgumentParser(description='Twitch highlighter')
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('--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('--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())
@@ -31,23 +27,13 @@ def on_chat_recorded(streamer, filename):
if __name__ == "__main__":
# TODO configure logging
# TODO rework authentication and status check. recorder should only record
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
args = parse_arguments()
authenticator = TwitchAuthenticator(args.tw_client, args.tw_secret, args.tw_username)
video_recorder = TwitchVideoRecorder(authenticator, args.tw_streamer, args.output_path,
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()
config = recorder.RecorderConfig(args.tw_client, args.tw_secret, args.tw_streamer, args.tw_quality, args.output_path)
recorder = recorder.Recorder(config)
recorder.run()
# Twitch downloader
# def main(argv):

View File

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