diff --git a/analyser/chat.py b/analyser/chat.py deleted file mode 100644 index 4507312..0000000 --- a/analyser/chat.py +++ /dev/null @@ -1,3 +0,0 @@ -class ChatAnalyser: - def __init__(self, streamer_name, ): - pass diff --git a/clipper/auth.py b/clipper/auth.py index b1122b7..2cdeaf5 100644 --- a/clipper/auth.py +++ b/clipper/auth.py @@ -1,23 +1,41 @@ +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): + 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 @@ -26,3 +44,5 @@ class TwitchAuthenticator: 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 new file mode 100644 index 0000000..3eb6795 --- /dev/null +++ b/clipper/chat.py @@ -0,0 +1,22 @@ +import twitch +import logging + +logger = logging.getLogger(__name__) + + +class TwitchChatRecorder: + def __init__(self, authenticator, streamer_name, on_finish=None): + self.on_finish = on_finish + self.streamer_name = streamer_name + self.authenticator = authenticator + + 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) + + def on_error(self, error): + logger.error(error) + + def on_message(self, msg): + logger.info("New message %s", msg) diff --git a/clipper/recorder.py b/clipper/video.py similarity index 79% rename from clipper/recorder.py rename to clipper/video.py index 1c4b219..30b4b32 100644 --- a/clipper/recorder.py +++ b/clipper/video.py @@ -11,7 +11,7 @@ HELIX_STREAM_URL = "https://api.twitch.tv/helix/streams?user_login={0}" logger = logging.getLogger(__name__) -class TwitchStreamerStatus(enum.Enum): +class TwitchStreamStatus(enum.Enum): ONLINE = 0 OFFLINE = 1 NOT_FOUND = 2 @@ -19,16 +19,16 @@ class TwitchStreamerStatus(enum.Enum): ERROR = 4 -class TwitchRecorder: +class TwitchVideoRecorder: access_token = None - def __init__(self, authenticator, streamer_name, output_path, quality="480p", on_download=None): + def __init__(self, authenticator, streamer_name, output_path, quality="480p", on_finish=None): # global configuration self.disable_ffmpeg = False self.refresh_timeout = 15 self.output_path = output_path self.stream_uid = None - self.on_download = on_download + self.on_finish = on_finish # twitch configuration self.streamer_name = streamer_name @@ -56,38 +56,40 @@ class TwitchRecorder: self.streamer_name, self.refresh_timeout, self.quality) while True: status, info = self.check_user() - if status == TwitchStreamerStatus.NOT_FOUND: + if status == TwitchStreamStatus.NOT_FOUND: logger.error("streamer_name not found, invalid streamer_name or typo") time.sleep(self.refresh_timeout) - elif status == TwitchStreamerStatus.ERROR: + 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 == TwitchStreamerStatus.OFFLINE: + 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 == TwitchStreamerStatus.UNAUTHORIZED: + elif status == TwitchStreamStatus.UNAUTHORIZED: logger.info("unauthorized, will attempt to log back in immediately") self.access_token = self.authenticator.refresh_token() - elif status == TwitchStreamerStatus.ONLINE: + 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.download_stream(channel, recording_path) + recorded_filename = self.record_stream(channel, recording_path) logger.info("recording stream is done") - if self.on_download is not None: - self.on_download(recorded_filename) + 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 = TwitchStreamerStatus.ERROR + status = TwitchStreamStatus.ERROR try: headers = {"Client-ID": self.authenticator.client_id, "Authorization": "Bearer {}".format(self.authenticator.get_token())} @@ -95,18 +97,18 @@ class TwitchRecorder: r.raise_for_status() info = r.json() if info is None or not info["data"]: - status = TwitchStreamerStatus.OFFLINE + status = TwitchStreamStatus.OFFLINE else: - status = TwitchStreamerStatus.ONLINE + status = TwitchStreamStatus.ONLINE except requests.exceptions.RequestException as e: if e.response: if e.response.status_code == 401: - status = TwitchStreamerStatus.UNAUTHORIZED + status = TwitchStreamStatus.UNAUTHORIZED if e.response.status_code == 404: - status = TwitchStreamerStatus.NOT_FOUND + status = TwitchStreamStatus.NOT_FOUND return status, info - def download_stream(self, channel, recording_path): + def record_stream(self, channel, recording_path): filename = self.streamer_name + " - " + datetime.datetime.now() \ .strftime("%Y-%m-%d %Hh%Mm%Ss") + " - " + channel.get("title") + ".mp4" diff --git a/main.py b/main.py index 844193d..de7c6d4 100644 --- a/main.py +++ b/main.py @@ -2,23 +2,30 @@ import argparse import os import sys import logging +import threading from clipper.auth import TwitchAuthenticator -from clipper.recorder import TwitchRecorder +from clipper.chat import TwitchChatRecorder +from clipper.video import TwitchVideoRecorder 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 streamer username', required=True, dest="tw_streamer") + 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()) return parser.parse_args() -def on_downloaded(): +def on_video_recorded(streamer, filename): + pass + + +def on_chat_recorded(streamer, filename): pass @@ -28,11 +35,19 @@ if __name__ == "__main__": args = parse_arguments() - authenticator = TwitchAuthenticator(args.tw_client, args.tw_secret) - rec = TwitchRecorder(authenticator, args.tw_streamer, args.output_path, - args.tw_quality, on_download=on_downloaded) + authenticator = TwitchAuthenticator(args.tw_client, args.tw_secret, args.tw_username) - rec.run() + 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() # Twitch downloader # def main(argv):