diff --git a/clipper/api.py b/clipper/api.py new file mode 100644 index 0000000..c87edee --- /dev/null +++ b/clipper/api.py @@ -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) diff --git a/clipper/auth.py b/clipper/auth.py deleted file mode 100644 index 2cdeaf5..0000000 --- a/clipper/auth.py +++ /dev/null @@ -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) diff --git a/clipper/chat.py b/clipper/chat.py index 3eb6795..e4a53a9 100644 --- a/clipper/chat.py +++ b/clipper/chat.py @@ -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) diff --git a/clipper/recorder.py b/clipper/recorder.py new file mode 100644 index 0000000..e2f5e94 --- /dev/null +++ b/clipper/recorder.py @@ -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 diff --git a/clipper/video.py b/clipper/video.py index 30b4b32..fc71b9b 100644 --- a/clipper/video.py +++ b/clipper/video.py @@ -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() \ diff --git a/main.py b/main.py index de7c6d4..adf6dd1 100644 --- a/main.py +++ b/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): diff --git a/requirements.txt b/requirements.txt index 431f62f..1febc67 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ requests==2.28.1 streamlink==4.2.0 -twitch-python==0.0.20 \ No newline at end of file +twitchAPI==2.5.7 +irc==20.1.0 \ No newline at end of file