diff --git a/clipper/analyser.py b/clipper/analyser.py index 9d4da0d..469f7d2 100644 --- a/clipper/analyser.py +++ b/clipper/analyser.py @@ -5,17 +5,18 @@ import matplotlib.pyplot as plt from datetime import datetime -from clipper.chat import CHAT_DIVIDER - logger = logging.getLogger(__name__) class ChatAnalyser: - def __init__(self, ignore_commands=True, ignored_users=["moobot", "nightbot"]): + def __init__(self, ignore_commands=True, ignored_users=None): + if ignored_users is None: + ignored_users = ["moobot", "nightbot"] + self.ignored_users = ignored_users self.ignore_commands = ignore_commands - def run(self, chat_file, peaks_output_file, peaks_output_chart): + def run(self, chat_file, peaks_output_file, peaks_output_chart, start_time): dates = self._read_message_dates(chat_file) messages_per_minute = self._group_dates(dates) peaks = self._find_peeks(messages_per_minute, peaks_output_file, peaks_output_chart) @@ -31,19 +32,23 @@ class ChatAnalyser: if not line: break - message_data = line.split(CHAT_DIVIDER) + message_data = line.split("<~|~>") if len(message_data) != 3: # Wrong line format continue - if message_data[1].lower() in self.ignore_commands: + if message_data[1].lower() in self.ignored_users: continue if self.ignore_commands and message_data[2].startswith("!"): continue date = message_data[0] - dates.append(self._parse_date(date)) + try: + dates.append(self._parse_date(date)) + except BaseException as e: + logger.error(e) + return dates def _parse_date(self, date_str): @@ -64,20 +69,39 @@ class ChatAnalyser: def _find_peeks(self, messages_per_minute, peaks_output_file, peaks_output_chart): y_coordinates = list(messages_per_minute.values()) x_coordinates = list(messages_per_minute.keys()) - peak_indices = scipy.signal.find_peaks_cwt(np.array(y_coordinates), 1) + peak_indices = scipy.signal.find_peaks_cwt(np.array(y_coordinates), 0.5) - x_hours = [x.split(" ")[1] for x in x_coordinates] fig, ax = plt.subplots() - ax.plot(x_hours, y_coordinates) - fig.autofmt_xdate() - plt.xlabel("Time") - plt.ylabel("Count") + ax.plot(range(0, len(y_coordinates), 1), y_coordinates) + plt.xlabel("Video Minutes") + plt.ylabel("Message count") plt.title("Stream chat reaction") plt.savefig(peaks_output_chart) - peak_values = [x_coordinates[index] for index in peak_indices] + start_time = None + if len(x_coordinates) > 0: + start_time = datetime.strptime(x_coordinates[0], "%Y-%m-%d %H:%M") + + max_value = max(y_coordinates) + trash_hold_value = max_value * 0.75 + filtered_values = [x_coordinates[index] for index in peak_indices if y_coordinates[index] > trash_hold_value] with open(peaks_output_file, "w") as stream: - for peak in peak_values: - stream.writelines(f"{peak}\n") + for peak in filtered_values: + if start_time: + peak_time = datetime.strptime(peak, "%Y-%m-%d %H:%M") + diff = peak_time - start_time + minutes = divmod(diff.total_seconds() / 60, 60) + stream.writelines(f"{peak} -> {minutes}\n") + else: + stream.writelines(f"{peak}\n") return peak_indices + + +if __name__ == "__main__": + anal = ChatAnalyser() + chat_file = "/Users/vetalll/Projects/Python/TwitchClipper/recorded/vovapain/17-08-2022_08-33-23/chat.txt" + out_file = "/Users/vetalll/Projects/Python/TwitchClipper/recorded/vovapain/17-08-2022_08-33-23/chat_peaks.txt" + out_hraph = "/Users/vetalll/Projects/Python/TwitchClipper/recorded/vovapain/17-08-2022_08-33-23/chat_chart.png" + + anal.run(chat_file, out_file, out_hraph, datetime(2022, 8, 15, 20, 38, 49)) diff --git a/clipper/clipper.py b/clipper/clipper.py index 093f90e..676e272 100644 --- a/clipper/clipper.py +++ b/clipper/clipper.py @@ -1,3 +1,72 @@ +import logging +import os +import subprocess +from datetime import datetime +from datetime import timedelta + +logger = logging.getLogger(__name__) + + class Clipper: - def run(self): - pass + def run(self, video_file, chat_peaks_file, output_folder): + try: + self._run(video_file, chat_peaks_file, output_folder) + except BaseException as e: + logger.error(e) + + def _run(self, video_file, chat_peaks_file, output_folder): + if not os.path.isdir(output_folder): + os.mkdir(output_folder) + + with open(chat_peaks_file, "r") as stream: + lines = stream.readlines() + + if not lines: + logger.error("No peaks found") + return + + counter = 1 + for line in lines: + # l = "2022-08-17 10:15 -> (1.0, 42.0)" + time_part = line.split("->")[1].strip() # (1.0, 42.0) + time = time_part.replace("(", "").replace(")", "").split(",") + video_time = datetime(2000, 1, 1, int(float(time[0])), int(float(time[1])), 0, 0) + start_time = video_time - timedelta(minutes=1) + end_time = video_time + timedelta(minutes=1) + + ffmpeg_start_time = start_time.strftime("%H:%M:00") + ffmpeg_end_time = end_time.strftime("%H:%M:00") + output_file = os.path.join(output_folder, f"clip_{counter}.mp4") + self._cut_clip(video_file, ffmpeg_start_time, ffmpeg_end_time, output_file) + counter = counter + 1 + + def _cut_clip(self, video_file, start_time, end_time, output_name): + # ffmpeg -ss 00:01:00 -to 00:02:00 -i input.mp4 -c copy output.mp4 + try: + subprocess.call([ + "ffmpeg", + "-err_detect", + "ignore_err", + "-ss", + start_time, + "-to", + end_time, + "-i", + video_file, + "-c", + "copy", + output_name + ]) + + except BaseException as e: + logger.error("Unable to run streamlink") + logger.error(e) + + +if __name__ == "__main__": + "/Users/vetalll/Projects/Python/TwitchClipper/recorded/" + video = "/Users/vetalll/Projects/Python/TwitchClipper/recorded/icebergdoto/17-08-2022_13-30-20/video.mp4" + peaks = "/Users/vetalll/Projects/Python/TwitchClipper/recorded/icebergdoto/17-08-2022_13-30-20/chat_peaks.txt" + result = "/Users/vetalll/Projects/Python/TwitchClipper/recorded/icebergdoto/17-08-2022_13-30-20/clips" + clipper = Clipper() + clipper.run(video, peaks, result) diff --git a/clipper/recorder.py b/clipper/recorder.py index 4e03b6d..d935634 100644 --- a/clipper/recorder.py +++ b/clipper/recorder.py @@ -7,6 +7,7 @@ from datetime import datetime from clipper.analyser import ChatAnalyser from clipper.api import TwitchApi, TwitchStreamStatus from clipper.chat import TwitchChatRecorder +from clipper.clipper import Clipper from clipper.video import TwitchVideoRecorder logger = logging.getLogger(__name__) @@ -29,6 +30,7 @@ class Recorder: self.video_recorder = TwitchVideoRecorder() self.chat_recorder = TwitchChatRecorder(self.api, debug=True) self.chat_analyser = ChatAnalyser() + self.clipper = Clipper() def run(self): logger.info("Start recording streamer %s", self.config.tw_streamer) @@ -45,13 +47,11 @@ class Recorder: output_video_file = os.path.join(record_folder, "video.mp4") output_chat_file = os.path.join(record_folder, "chat.txt") - output_chat_peaks_file = os.path.join(record_folder, "chat_peaks.txt") - output_chat_chart_file = os.path.join(record_folder, "chat_chart.png") self.chat_recorder.run(self.config.tw_streamer, output_chat_file) self.video_recorder.run(self.config.tw_streamer, output_video_file, quality="160p") self._loop_recording() - self._post_process_video(output_chat_file, output_chat_peaks_file, output_chat_chart_file) + self._post_process_video(record_folder, output_chat_file, output_video_file, start_time) elif status == TwitchStreamStatus.OFFLINE: logger.info("Streamer %s is offline. Waiting for 300 sec", self.config.tw_streamer) @@ -77,7 +77,12 @@ class Recorder: continue break - def _post_process_video(self, output_chat_file, output_chat_peaks_file, output_chat_chart_file): + def _post_process_video(self, record_folder, output_chat_file, output_video_file, start_time): + output_chat_peaks_file = os.path.join(record_folder, "chat_peaks.txt") + output_chat_chart_file = os.path.join(record_folder, "chat_chart.png") + logger.info("Start looking for peaks in file %s", output_chat_file) - peaks = self.chat_analyser.run(output_chat_file, output_chat_peaks_file, output_chat_chart_file) + peaks = self.chat_analyser.run(output_chat_file, output_chat_peaks_file, output_chat_chart_file, start_time) logger.info("Found peaks: %s for file %s", len(peaks), output_chat_file) + + self.clipper.run(output_video_file, output_chat_peaks_file, record_folder)