Added first working chat recorder
This commit is contained in:
85
clipper/api.py
Normal file
85
clipper/api.py
Normal 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)
|
||||
@@ -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)
|
||||
@@ -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
81
clipper/recorder.py
Normal 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
|
||||
@@ -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
24
main.py
@@ -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):
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user