From 602be593598fa9112cdd0eb5d7f3863a9edc4e89 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Tue, 2 Feb 2021 11:55:39 +0100 Subject: [PATCH 001/117] Save in a .json file the balance history --- .gitignore | 3 ++- .../TwitchChannelPointsMiner.py | 5 +++++ TwitchChannelPointsMiner/classes/WebSocketsPool.py | 7 +++++-- .../classes/entities/Streamer.py | 13 +++++++++++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 0a444db..1bd3575 100644 --- a/.gitignore +++ b/.gitignore @@ -145,4 +145,5 @@ chromedriver* cookies/* logs/* screenshots/* -htmls/* \ No newline at end of file +htmls/* +analytics/* diff --git a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py index c0b4623..6c03f2b 100644 --- a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py +++ b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py @@ -2,6 +2,7 @@ import copy import logging +import os import random import signal import sys @@ -10,6 +11,7 @@ import time import uuid from collections import OrderedDict from datetime import datetime +from pathlib import Path from TwitchChannelPointsMiner.classes.entities.PubsubTopic import PubsubTopic from TwitchChannelPointsMiner.classes.entities.Streamer import ( @@ -52,6 +54,9 @@ class TwitchChannelPointsMiner: # Default values for all streamers streamer_settings: StreamerSettings = StreamerSettings(), ): + Settings.analytics_path = os.path.join(Path().absolute(), "analytics") + Path(Settings.analytics_path).mkdir(parents=True, exist_ok=True) + self.username = username # Set as globally config diff --git a/TwitchChannelPointsMiner/classes/WebSocketsPool.py b/TwitchChannelPointsMiner/classes/WebSocketsPool.py index 1a46173..c4b125e 100644 --- a/TwitchChannelPointsMiner/classes/WebSocketsPool.py +++ b/TwitchChannelPointsMiner/classes/WebSocketsPool.py @@ -128,11 +128,14 @@ class WebSocketsPool: if streamer_index != -1: try: if message.topic == "community-points-user-v1": + if message.type in ["points-earned", "points-spent"]: + balance = message.data["balance"]["balance"] + ws.streamers[streamer_index].channel_points = balance + ws.streamers[streamer_index].persistent_history() + if message.type == "points-earned": earned = message.data["point_gain"]["total_points"] reason_code = message.data["point_gain"]["reason_code"] - balance = message.data["balance"]["balance"] - ws.streamers[streamer_index].channel_points = balance logger.info( f"+{earned} β†’ {ws.streamers[streamer_index]} - Reason: {reason_code}.", extra={"emoji": ":rocket:"}, diff --git a/TwitchChannelPointsMiner/classes/entities/Streamer.py b/TwitchChannelPointsMiner/classes/entities/Streamer.py index 6eef727..4794510 100644 --- a/TwitchChannelPointsMiner/classes/entities/Streamer.py +++ b/TwitchChannelPointsMiner/classes/entities/Streamer.py @@ -1,5 +1,8 @@ +import json import logging +import os import time +from threading import Lock from TwitchChannelPointsMiner.classes.entities.Bet import BetSettings from TwitchChannelPointsMiner.classes.entities.Stream import Stream @@ -57,6 +60,8 @@ class Streamer(object): self.streamer_url = f"{URL}/{self.username}" self.chat_url = f"{URL}/popout/{self.username}/chat?popout=" + self.mutex = Lock() + def __repr__(self): return f"Streamer(username={self.username}, channel_id={self.channel_id}, channel_points={_millify(self.channel_points)})" @@ -101,3 +106,11 @@ class Streamer(object): def stream_up_elapsed(self): return self.stream_up == 0 or ((time.time() - self.stream_up) > 120) + + def persistent_history(self): + fname = os.path.join(Settings.analytics_path, f"{self.username}.json") + with self.mutex: + data = json.load(open(fname)) if os.path.isfile(fname) else [] + data.append([time.time, self.channel_points]) + with open(fname, "w") as outfile: + json.dump(data, outfile, indent=4) From 2187a78b861e334349ebeb44a6522b3ccd7437eb Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Tue, 2 Feb 2021 12:11:20 +0100 Subject: [PATCH 002/117] Instead of copy the entire array of streamers copy only the balance. We can't pickle Lock() --- TwitchChannelPointsMiner/TwitchChannelPointsMiner.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py index 86d323b..aa0d67c 100644 --- a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py +++ b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -import copy import logging import os import random @@ -174,7 +173,9 @@ class TwitchChannelPointsMiner: if streamer.viewer_is_mod is True: streamer.settings.make_predictions = False - self.original_streamers = copy.deepcopy(self.streamers) + self.original_streamers = [ + streamer.channel_points for streamer in self.streamers + ] # If we have at least one streamer with settings = make_predictions True make_predictions = at_least_one_value_in_settings_is( @@ -307,7 +308,7 @@ class TwitchChannelPointsMiner: for streamer_index in range(0, len(self.streamers)): logger.info( - f"{repr(self.streamers[streamer_index])}, Total Points Gained (after farming - before farming): {_millify(self.streamers[streamer_index].channel_points - self.original_streamers[streamer_index].channel_points)}", + f"{repr(self.streamers[streamer_index])}, Total Points Gained (after farming - before farming): {_millify(self.streamers[streamer_index].channel_points - self.original_streamers[streamer_index])}", extra={"emoji": ":robot:"}, ) if self.streamers[streamer_index].history != {}: From 08088e496db3d696d073b8d9ebe9db3e0172d40d Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Tue, 2 Feb 2021 14:38:35 +0100 Subject: [PATCH 003/117] missing () --- TwitchChannelPointsMiner/classes/entities/Streamer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TwitchChannelPointsMiner/classes/entities/Streamer.py b/TwitchChannelPointsMiner/classes/entities/Streamer.py index 4794510..57a4cff 100644 --- a/TwitchChannelPointsMiner/classes/entities/Streamer.py +++ b/TwitchChannelPointsMiner/classes/entities/Streamer.py @@ -111,6 +111,6 @@ class Streamer(object): fname = os.path.join(Settings.analytics_path, f"{self.username}.json") with self.mutex: data = json.load(open(fname)) if os.path.isfile(fname) else [] - data.append([time.time, self.channel_points]) + data.append([time.time(), self.channel_points]) with open(fname, "w") as outfile: json.dump(data, outfile, indent=4) From 06fefd5943567596da4169d7f4149ce261234006 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Tue, 2 Feb 2021 15:35:53 +0100 Subject: [PATCH 004/117] convert time.time in milliseconds, create an example chart.html --- .../classes/entities/Streamer.py | 3 +- charts.html | 83 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 charts.html diff --git a/TwitchChannelPointsMiner/classes/entities/Streamer.py b/TwitchChannelPointsMiner/classes/entities/Streamer.py index 57a4cff..f9c48c9 100644 --- a/TwitchChannelPointsMiner/classes/entities/Streamer.py +++ b/TwitchChannelPointsMiner/classes/entities/Streamer.py @@ -111,6 +111,7 @@ class Streamer(object): fname = os.path.join(Settings.analytics_path, f"{self.username}.json") with self.mutex: data = json.load(open(fname)) if os.path.isfile(fname) else [] - data.append([time.time(), self.channel_points]) + # https://stackoverflow.com/questions/4676195/why-do-i-need-to-multiply-unix-timestamps-by-1000-in-javascript + data.append([round(time.time() * 1000), self.channel_points]) with open(fname, "w") as outfile: json.dump(data, outfile, indent=4) diff --git a/charts.html b/charts.html new file mode 100644 index 0000000..c1aaa8a --- /dev/null +++ b/charts.html @@ -0,0 +1,83 @@ + + + + + + + Twitch-Channel-Points-Miner-v2 + + + + +
+
+ + + + From 05aeb2988dc7f18371d08409b8863846a9a7d274 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Tue, 2 Feb 2021 16:46:51 +0100 Subject: [PATCH 005/117] Analytics function. Check the status of your points with a line-charts (zoomable-timeseries). We need Flask for serve the .json history --- .../TwitchChannelPointsMiner.py | 9 +++- .../classes/AnalyticsServer.py | 41 +++++++++++++++++++ charts.html => assets/charts.html | 33 ++++++++------- requirements.txt | 1 + 4 files changed, 68 insertions(+), 16 deletions(-) create mode 100644 TwitchChannelPointsMiner/classes/AnalyticsServer.py rename charts.html => assets/charts.html (71%) diff --git a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py index aa0d67c..4c10e35 100644 --- a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py +++ b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py @@ -12,6 +12,7 @@ from collections import OrderedDict from datetime import datetime from pathlib import Path +from TwitchChannelPointsMiner.classes.AnalyticsServer import AnalyticsServer from TwitchChannelPointsMiner.classes.entities.PubsubTopic import PubsubTopic from TwitchChannelPointsMiner.classes.entities.Streamer import ( Streamer, @@ -34,9 +35,10 @@ from TwitchChannelPointsMiner.utils import ( ) # Suppress warning for urllib3.connectionpool (selenium close connection) -# Suppress also the selenium logger please +# Suppress Selenium and Flask (werkzeug) logs logging.getLogger("urllib3").setLevel(logging.ERROR) logging.getLogger("selenium").setLevel(logging.ERROR) +logging.getLogger("werkzeug").setLevel(logging.ERROR) logger = logging.getLogger(__name__) @@ -46,6 +48,7 @@ class TwitchChannelPointsMiner: self, username: str, claim_drops_startup: bool = False, + analytics: bool = True, # Settings for logging and selenium as you can see. # This settings will be global shared trought Settings class logger_settings: LoggerSettings = LoggerSettings(), @@ -84,6 +87,10 @@ class TwitchChannelPointsMiner: self.logs_file = configure_loggers(self.username, logger_settings) + if analytics is True: + http_server = AnalyticsServer() + http_server.run() + for sign in [signal.SIGINT, signal.SIGSEGV, signal.SIGTERM]: signal.signal(sign, self.end) diff --git a/TwitchChannelPointsMiner/classes/AnalyticsServer.py b/TwitchChannelPointsMiner/classes/AnalyticsServer.py new file mode 100644 index 0000000..d2934a9 --- /dev/null +++ b/TwitchChannelPointsMiner/classes/AnalyticsServer.py @@ -0,0 +1,41 @@ +import os +from pathlib import Path + +from flask import Flask, Response, render_template + +from TwitchChannelPointsMiner.classes.Settings import Settings + + +def streamers_available(): + path = Settings.analytics_path + return [ + f + for f in os.listdir(path) + if os.path.isfile(os.path.join(path, f)) and f.endswith(".json") + ] + + +def read_json(streamer): + path = Settings.analytics_path + streamer = streamer if streamer.endswith(".json") else f"{streamer}.json" + return Response( + open(os.path.join(path, streamer)) if streamer in streamers_available() else [], + status=200, + mimetype="application/json", + ) + + +def index(): + return render_template("charts.html", streamers=",".join(streamers_available())) + + +class AnalyticsServer(object): + def __init__(self): + self.app = Flask( + __name__, template_folder=os.path.join(Path().absolute(), "assets") + ) + self.app.add_url_rule("/", "index", index) + self.app.add_url_rule("/json/", "json", read_json) + + def run(self): + self.app.run() diff --git a/charts.html b/assets/charts.html similarity index 71% rename from charts.html rename to assets/charts.html index c1aaa8a..969231e 100644 --- a/charts.html +++ b/assets/charts.html @@ -6,6 +6,7 @@ Twitch-Channel-Points-Miner-v2 + @@ -15,21 +16,7 @@ diff --git a/requirements.txt b/requirements.txt index c9b056d..2669b83 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ python-dateutil emoji millify pre-commit +flask From a236c6b9a923a42bfc1e5597d2eb9f425b4f09fa Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Tue, 2 Feb 2021 17:01:25 +0100 Subject: [PATCH 006/117] Insert banner --- .../classes/AnalyticsServer.py | 9 +++++-- assets/charts.html | 25 ++++++++++++++++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/TwitchChannelPointsMiner/classes/AnalyticsServer.py b/TwitchChannelPointsMiner/classes/AnalyticsServer.py index d2934a9..9f61d6d 100644 --- a/TwitchChannelPointsMiner/classes/AnalyticsServer.py +++ b/TwitchChannelPointsMiner/classes/AnalyticsServer.py @@ -1,7 +1,7 @@ import os from pathlib import Path -from flask import Flask, Response, render_template +from flask import Flask, Response, render_template # , cli from TwitchChannelPointsMiner.classes.Settings import Settings @@ -29,10 +29,15 @@ def index(): return render_template("charts.html", streamers=",".join(streamers_available())) +# cli.show_server_banner = lambda *_: None + + class AnalyticsServer(object): def __init__(self): self.app = Flask( - __name__, template_folder=os.path.join(Path().absolute(), "assets") + __name__, + template_folder=os.path.join(Path().absolute(), "assets"), + static_folder=os.path.join(Path().absolute(), "assets"), ) self.app.add_url_rule("/", "index", index) self.app.add_url_rule("/json/", "json", read_json) diff --git a/assets/charts.html b/assets/charts.html index 969231e..d217373 100644 --- a/assets/charts.html +++ b/assets/charts.html @@ -10,8 +10,31 @@ -
+ +
+
+ + -
- banner +
+
- - License - - - Python3 - - - PRsWelcome - - - GitHub Repo stars - - - GitHub closed issues - - - GitHub last commit - +
+ +
+
-
-
From 130e45bf88b986fd20331fe52e49443d7781222e Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Wed, 3 Feb 2021 01:22:38 +0100 Subject: [PATCH 015/117] Just fix the prefix for label -- + --- .../classes/AnalyticsServer.py | 2 +- .../classes/WebSocketsPool.py | 17 +++++------------ .../classes/entities/Streamer.py | 2 +- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/TwitchChannelPointsMiner/classes/AnalyticsServer.py b/TwitchChannelPointsMiner/classes/AnalyticsServer.py index 8abf4fc..d2590e0 100644 --- a/TwitchChannelPointsMiner/classes/AnalyticsServer.py +++ b/TwitchChannelPointsMiner/classes/AnalyticsServer.py @@ -51,7 +51,7 @@ class AnalyticsServer(Process): def run(self): logger.info( - f"Running on http:/{self.host}:{self.port}/", + f"Analytics running on http://{self.host}:{self.port}/", extra={"emoji": ":globe_with_meridians:"}, ) self.app.run(host=self.host, port=self.port, threaded=True) diff --git a/TwitchChannelPointsMiner/classes/WebSocketsPool.py b/TwitchChannelPointsMiner/classes/WebSocketsPool.py index 1ee0883..2cf8f7d 100644 --- a/TwitchChannelPointsMiner/classes/WebSocketsPool.py +++ b/TwitchChannelPointsMiner/classes/WebSocketsPool.py @@ -259,9 +259,8 @@ class WebSocketsPool: if event_id in ws.events_predictions: if message.type == "prediction-result": event_result = message.data["prediction"]["result"] - poinst_placed = ws.events_predictions[ - event_id - ].bet.decision["amount"] + bet = ws.events_predictions[event_id].bet + poinst_placed = bet.decision["amount"] points_prefix = ( "+" if event_result["points_won"] else "-" ) @@ -274,18 +273,12 @@ class WebSocketsPool: f"{ws.events_predictions[event_id]} - Result: {event_result['type']}, Points won: {points_prefix}{_millify(points_won)}", extra={"emoji": ":bar_chart:"}, ) - points_won = ( - event_result["points_won"] - if event_result["points_won"] - else 0 - ) ws.events_predictions[event_id].final_result = { "type": event_result["type"], - "won": points_won, + "won": f"{points_prefix}{_millify(points_won)}", } - ws.events_predictions[ - event_id - ].streamer.persistent_points( + # ws.events_predictions[event_id].streamer + ws.streamers[streamer_index].persistent_points( event_result["type"], f"{points_prefix}{points_won} - {event_result['type']}: {ws.events_predictions[event_id].title}", ) diff --git a/TwitchChannelPointsMiner/classes/entities/Streamer.py b/TwitchChannelPointsMiner/classes/entities/Streamer.py index da1fbdb..5ebfde4 100644 --- a/TwitchChannelPointsMiner/classes/entities/Streamer.py +++ b/TwitchChannelPointsMiner/classes/entities/Streamer.py @@ -139,7 +139,7 @@ class Streamer(object): def __save_json(self, key, data={}): # https://stackoverflow.com/questions/4676195/why-do-i-need-to-multiply-unix-timestamps-by-1000-in-javascript - data.update({"y": self.channel_points, "x": round(time.time() * 1000)}) + data.update({"x": round(time.time() * 1000), "y": self.channel_points}) fname = os.path.join(Settings.analytics_path, f"{self.username}.json") with self.mutex: json_data = json.load(open(fname, "r")) if os.path.isfile(fname) else {} From 415a06641409bda86ae2dacbdaa1a5c5e188984a Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Wed, 3 Feb 2021 11:26:37 +0100 Subject: [PATCH 016/117] -- fix --- TwitchChannelPointsMiner/classes/WebSocketsPool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TwitchChannelPointsMiner/classes/WebSocketsPool.py b/TwitchChannelPointsMiner/classes/WebSocketsPool.py index 2cf8f7d..b3e7d5c 100644 --- a/TwitchChannelPointsMiner/classes/WebSocketsPool.py +++ b/TwitchChannelPointsMiner/classes/WebSocketsPool.py @@ -267,7 +267,7 @@ class WebSocketsPool: points_won = ( event_result["points_won"] if event_result["points_won"] - else (poinst_placed * -1) + else poinst_placed ) logger.info( f"{ws.events_predictions[event_id]} - Result: {event_result['type']}, Points won: {points_prefix}{_millify(points_won)}", From 49490da3b1e5255969c4d857c1bd6810e78787f0 Mon Sep 17 00:00:00 2001 From: Jan Radtke Date: Wed, 3 Feb 2021 18:45:26 +0100 Subject: [PATCH 017/117] add irc chat client list user as viewer in selected chat viewtime increases while connected --- .../TwitchChannelPointsMiner.py | 4 + .../classes/TwitchChat.py | 34 ++++++ .../classes/entities/Chat.py | 18 +++ .../classes/entities/Streamer.py | 17 ++- example.py | 105 ++++++++++-------- requirements.txt | 1 + 6 files changed, 130 insertions(+), 49 deletions(-) create mode 100644 TwitchChannelPointsMiner/classes/TwitchChat.py create mode 100644 TwitchChannelPointsMiner/classes/entities/Chat.py diff --git a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py index 552468b..6f2848e 100644 --- a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py +++ b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py @@ -260,6 +260,10 @@ class TwitchChannelPointsMiner: if self.twitch_browser is not None: self.twitch_browser.browser.quit() + for streamer in self.streamers: + if(streamer is not None): + streamer.leave_chat() + self.running = self.twitch.running = False self.ws_pool.end() diff --git a/TwitchChannelPointsMiner/classes/TwitchChat.py b/TwitchChannelPointsMiner/classes/TwitchChat.py new file mode 100644 index 0000000..a14e306 --- /dev/null +++ b/TwitchChannelPointsMiner/classes/TwitchChat.py @@ -0,0 +1,34 @@ +from TwitchChannelPointsMiner.classes.entities.Chat import Chat + +import requests +from multiprocessing import Process +import logging + +logger = logging.getLogger(__name__) + +class ChatSettings(object): + def __init__( + self, + username: str = None, + token: str = None, + ): + self.username = username + self.token = token + +class TwitchChat(Process): + def __deepcopy__(self, memo): + return None + + def __init__(self, username, token, channel): + super(TwitchChat, self).__init__() + self.token = token + self.username = username + self.channel = channel + + def run(self): + # Create IRC bot connection + try: + IRC = Chat( self.username, self.token, self.channel ) + IRC.start() + except KeyboardInterrupt: + None # dont handle KeyboardInterruption here diff --git a/TwitchChannelPointsMiner/classes/entities/Chat.py b/TwitchChannelPointsMiner/classes/entities/Chat.py new file mode 100644 index 0000000..3e22247 --- /dev/null +++ b/TwitchChannelPointsMiner/classes/entities/Chat.py @@ -0,0 +1,18 @@ + +import irc.bot +import requests + +class Chat(irc.bot.SingleServerIRCBot): + def __init__(self, username, token, channel): + self.token = token + self.channel = '#' + channel + + # Create IRC bot connection + server = 'irc.chat.twitch.tv' + port = 6667 + irc.bot.SingleServerIRCBot.__init__(self, [(server, port, 'oauth:'+token)], username, username) + + + def on_welcome(self, c, e): + # You must request specific capabilities before you can use them + c.join(self.channel) diff --git a/TwitchChannelPointsMiner/classes/entities/Streamer.py b/TwitchChannelPointsMiner/classes/entities/Streamer.py index 6eef727..6f93356 100644 --- a/TwitchChannelPointsMiner/classes/entities/Streamer.py +++ b/TwitchChannelPointsMiner/classes/entities/Streamer.py @@ -1,6 +1,7 @@ import logging import time +from TwitchChannelPointsMiner.classes.TwitchChat import TwitchChat, ChatSettings from TwitchChannelPointsMiner.classes.entities.Bet import BetSettings from TwitchChannelPointsMiner.classes.entities.Stream import Stream from TwitchChannelPointsMiner.classes.Settings import Settings @@ -9,7 +10,6 @@ from TwitchChannelPointsMiner.utils import _millify logger = logging.getLogger(__name__) - class StreamerSettings(object): def __init__( self, @@ -18,12 +18,14 @@ class StreamerSettings(object): claim_drops: bool = None, watch_streak: bool = None, bet: BetSettings = None, + chat_client: ChatSettings = None ): self.make_predictions = make_predictions self.follow_raid = follow_raid self.claim_drops = claim_drops self.watch_streak = watch_streak self.bet = bet + self.chat_client = chat_client def default(self): for name in ["make_predictions", "follow_raid", "claim_drops", "watch_streak"]: @@ -48,6 +50,7 @@ class Streamer(object): self.channel_points = 0 self.minute_watched_requests = None self.viewer_is_mod = False + self.chat = None self.stream = Stream() @@ -71,6 +74,7 @@ class Streamer(object): if self.is_online is True: self.offline_at = time.time() self.is_online = False + self.leave_chat() logger.info(f"{self} is Offline!", extra={"emoji": ":sleeping:"}) @@ -81,6 +85,7 @@ class Streamer(object): self.stream.init_watch_streak() logger.info(f"{self} is Online!", extra={"emoji": ":partying_face:"}) + self.join_chat() def print_history(self): return ", ".join( @@ -101,3 +106,13 @@ class Streamer(object): def stream_up_elapsed(self): return self.stream_up == 0 or ((time.time() - self.stream_up) > 120) + + def leave_chat(self): + if(self.chat is not None): + self.chat.terminate() + + def join_chat(self): + if(self.settings.chat_client is not None): + self.chat = TwitchChat(self.settings.chat_client.username, self.settings.chat_client.token, self.username) + logger.info(f"Connecting to {self.username}'s chat!") + self.chat.start() diff --git a/example.py b/example.py index 4b1ae3f..659f696 100644 --- a/example.py +++ b/example.py @@ -6,57 +6,66 @@ from TwitchChannelPointsMiner.logger import LoggerSettings from TwitchChannelPointsMiner.classes.entities.Bet import Strategy, BetSettings from TwitchChannelPointsMiner.classes.entities.Streamer import Streamer, StreamerSettings from TwitchChannelPointsMiner.classes.TwitchBrowser import Browser, BrowserSettings +from TwitchChannelPointsMiner.classes.TwitchChat import TwitchChat, ChatSettings -twitch_miner = TwitchChannelPointsMiner( - username="your-twitch-username", - claim_drops_startup=False, # If you want to auto claim all drops from Twitch inventory on startup - logger_settings=LoggerSettings( - save=True, # If you want to save logs in file (suggested) - console_level=logging.INFO, # Level of logs - use logging.DEBUG for more info) - file_level=logging.DEBUG, # Level of logs - If you think the log file it's too big use logging.INFO - emoji=True, # On Windows we have a problem to print emoji. Set to false if you have a problem - less=False # If you think that the logs are too much verborse set this to True - ), - browser_settings=BrowserSettings( - browser=Browser.FIREFOX, # Choose if you want to use Chrome or Firefox as browser - show=False, # Show the browser during bet else headless mode - do_screenshot=False, # Do screenshot during the bet - ), - streamer_settings=StreamerSettings( - make_predictions=True, # If you want to Bet / Make prediction - follow_raid=True, # Follow raid to obtain more points - claim_drops=True, # We can't filter rewards base on stream. Set to False for skip viewing counter increase and you will never obtain a drop reward from this script. Issue #21 - watch_streak=True, # If a streamer go online change the priotiry of streamers array and catch the watch screak. Issue #11 - bet=BetSettings( - strategy=Strategy.SMART, # Choose you strategy! - percentage=5, # Place the x% of your channel points - percentage_gap=20, # Gap difference between outcomesA and outcomesB (for SMART stragegy) - max_points=50000, # If the x percentage of your channel points is gt bet_max_points set this value +def main(): + twitch_miner = TwitchChannelPointsMiner( + username="your-twitch-username", + claim_drops_startup=False, # If you want to auto claim all drops from Twitch inventory on startup + logger_settings=LoggerSettings( + save=True, # If you want to save logs in file (suggested) + console_level=logging.INFO, # Level of logs - use logging.DEBUG for more info) + file_level=logging.DEBUG, # Level of logs - If you think the log file it's too big use logging.INFO + emoji=True, # On Windows we have a problem to print emoji. Set to false if you have a problem + less=False # If you think that the logs are too much verborse set this to True + ), + browser_settings=BrowserSettings( + browser=Browser.FIREFOX, # Choose if you want to use Chrome or Firefox as browser + show=False, # Show the browser during bet else headless mode + do_screenshot=False, # Do screenshot during the bet + ), + streamer_settings=StreamerSettings( + make_predictions=True, # If you want to Bet / Make prediction + follow_raid=True, # Follow raid to obtain more points + claim_drops=True, # We can't filter rewards base on stream. Set to False for skip viewing counter increase and you will never obtain a drop reward from this script. Issue #21 + watch_streak=True, # If a streamer go online change the priotiry of streamers array and catch the watch screak. Issue #11 + bet=BetSettings( + strategy=Strategy.SMART, # Choose you strategy! + percentage=5, # Place the x% of your channel points + percentage_gap=20, # Gap difference between outcomesA and outcomesB (for SMART stragegy) + max_points=50000, # If the x percentage of your channel points is gt bet_max_points set this value + ), + chat_client=ChatSettings( # Can be removed for mining channelpoints only + username="your-twitch-username", + token="oauth-token-without-prefix", # generate on https://twitchapps.com/tmi/ - without "oauth:" prefix! + ) ) ) -) -# You can customize the settings for each streamer. If not settings was provided the script will use the streamer_settings from TwitchChannelPointsMiner. -# If no streamer_settings provided in TwitchChannelPointsMiner the script will use default settings. -# The streamers array can be a String -> username or Streamer instance. + # You can customize the settings for each streamer. If not settings was provided the script will use the streamer_settings from TwitchChannelPointsMiner. + # If no streamer_settings provided in TwitchChannelPointsMiner the script will use default settings. + # The streamers array can be a String -> username or Streamer instance. -# The settings priority are: settings in mine function, settings in TwitchChannelPointsMiner instance, default settings. -# For example if in the mine function you don't provide any value for 'make_prediction' but you have set it on TwitchChannelPointsMiner instance the script will take the value from here. -# If you haven't set any value even in the instance the default one will be used + # The settings priority are: settings in mine function, settings in TwitchChannelPointsMiner instance, default settings. + # For example if in the mine function you don't provide any value for 'make_prediction' but you have set it on TwitchChannelPointsMiner instance the script will take the value from here. + # If you haven't set any value even in the instance the default one will be used -twitch_miner.mine( - [ - Streamer("streamer-username01", settings=StreamerSettings(make_predictions=True , follow_raid=False , claim_drops=True , watch_streak=True , bet=BetSettings(strategy=Strategy.SMART , percentage=5 , percentage_gap=20 , max_points=234 ) )), - Streamer("streamer-username02", settings=StreamerSettings(make_predictions=False , follow_raid=True , claim_drops=False , bet=BetSettings(strategy=Strategy.PERCENTAGE , percentage=5 , percentage_gap=20 , max_points=1234 ) )), - Streamer("streamer-username03", settings=StreamerSettings(make_predictions=True , follow_raid=False , watch_streak=True , bet=BetSettings(strategy=Strategy.SMART , percentage=5 , percentage_gap=30 , max_points=50000 ) )), - Streamer("streamer-username04", settings=StreamerSettings(make_predictions=False , follow_raid=True , watch_streak=True )), - Streamer("streamer-username05", settings=StreamerSettings(make_predictions=True , follow_raid=True , claim_drops=True , watch_streak=True , bet=BetSettings(strategy=Strategy.HIGH_ODDS , percentage=7 , percentage_gap=20 , max_points=90 ) )), - Streamer("streamer-username06"), - Streamer("streamer-username07"), - Streamer("streamer-username08"), - "streamer-username09", - "streamer-username10", - "streamer-username11" - ], # Array of streamers (order = priority) - followers=False # Automatic download the list of your followers (unable to set custom settings for you followers list) -) + twitch_miner.mine( + [ + Streamer("streamer-username01", settings=StreamerSettings(make_predictions=True , follow_raid=False , claim_drops=True , watch_streak=True , bet=BetSettings(strategy=Strategy.SMART , percentage=5 , percentage_gap=20 , max_points=234 ) )), + Streamer("streamer-username02", settings=StreamerSettings(make_predictions=False , follow_raid=True , claim_drops=False , bet=BetSettings(strategy=Strategy.PERCENTAGE , percentage=5 , percentage_gap=20 , max_points=1234 ) )), + Streamer("streamer-username03", settings=StreamerSettings(make_predictions=True , follow_raid=False , watch_streak=True , bet=BetSettings(strategy=Strategy.SMART , percentage=5 , percentage_gap=30 , max_points=50000 ) )), + Streamer("streamer-username04", settings=StreamerSettings(make_predictions=False , follow_raid=True , watch_streak=True )), + Streamer("streamer-username05", settings=StreamerSettings(make_predictions=True , follow_raid=True , claim_drops=True , watch_streak=True , bet=BetSettings(strategy=Strategy.HIGH_ODDS , percentage=7 , percentage_gap=20 , max_points=90 ) )), + Streamer("streamer-username06"), + Streamer("streamer-username07"), + Streamer("streamer-username08"), + "streamer-username09", + "streamer-username10", + "streamer-username11" + ], # Array of streamers (order = priority) + followers=False # Automatic download the list of your followers (unable to set custom settings for you followers list) + ) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c9b056d..1e5c3a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ python-dateutil emoji millify pre-commit +irc From cb1cc1d884819f6382affa33bd31abb7fa04087f Mon Sep 17 00:00:00 2001 From: Jan Radtke Date: Wed, 3 Feb 2021 18:53:04 +0100 Subject: [PATCH 018/117] remove unnessessary imports --- TwitchChannelPointsMiner/classes/TwitchChat.py | 7 +++---- TwitchChannelPointsMiner/classes/entities/Chat.py | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/TwitchChannelPointsMiner/classes/TwitchChat.py b/TwitchChannelPointsMiner/classes/TwitchChat.py index a14e306..2d057dd 100644 --- a/TwitchChannelPointsMiner/classes/TwitchChat.py +++ b/TwitchChannelPointsMiner/classes/TwitchChat.py @@ -1,9 +1,8 @@ -from TwitchChannelPointsMiner.classes.entities.Chat import Chat - -import requests -from multiprocessing import Process import logging +from TwitchChannelPointsMiner.classes.entities.Chat import Chat +from multiprocessing import Process + logger = logging.getLogger(__name__) class ChatSettings(object): diff --git a/TwitchChannelPointsMiner/classes/entities/Chat.py b/TwitchChannelPointsMiner/classes/entities/Chat.py index 3e22247..3b58535 100644 --- a/TwitchChannelPointsMiner/classes/entities/Chat.py +++ b/TwitchChannelPointsMiner/classes/entities/Chat.py @@ -1,6 +1,5 @@ import irc.bot -import requests class Chat(irc.bot.SingleServerIRCBot): def __init__(self, username, token, channel): From 1b81d8d9ee5e202c25d83b9942f0358820a8ce0e Mon Sep 17 00:00:00 2001 From: anoviel Date: Wed, 3 Feb 2021 20:46:53 +0100 Subject: [PATCH 019/117] changed format with pre_commit hook --- .gitignore | 2 +- .../TwitchChannelPointsMiner.py | 2 +- TwitchChannelPointsMiner/classes/TwitchChat.py | 10 ++++++---- TwitchChannelPointsMiner/classes/entities/Chat.py | 11 ++++++----- .../classes/entities/Streamer.py | 15 ++++++++++----- example.py | 2 +- 6 files changed, 25 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 0a444db..360b9f1 100644 --- a/.gitignore +++ b/.gitignore @@ -145,4 +145,4 @@ chromedriver* cookies/* logs/* screenshots/* -htmls/* \ No newline at end of file +htmls/* diff --git a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py index 6f2848e..ea2fdcc 100644 --- a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py +++ b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py @@ -261,7 +261,7 @@ class TwitchChannelPointsMiner: self.twitch_browser.browser.quit() for streamer in self.streamers: - if(streamer is not None): + if streamer is not None: streamer.leave_chat() self.running = self.twitch.running = False diff --git a/TwitchChannelPointsMiner/classes/TwitchChat.py b/TwitchChannelPointsMiner/classes/TwitchChat.py index 2d057dd..e0a0bfa 100644 --- a/TwitchChannelPointsMiner/classes/TwitchChat.py +++ b/TwitchChannelPointsMiner/classes/TwitchChat.py @@ -1,10 +1,11 @@ import logging - -from TwitchChannelPointsMiner.classes.entities.Chat import Chat from multiprocessing import Process +from TwitchChannelPointsMiner.classes.entities.Chat import Chat + logger = logging.getLogger(__name__) + class ChatSettings(object): def __init__( self, @@ -14,6 +15,7 @@ class ChatSettings(object): self.username = username self.token = token + class TwitchChat(Process): def __deepcopy__(self, memo): return None @@ -27,7 +29,7 @@ class TwitchChat(Process): def run(self): # Create IRC bot connection try: - IRC = Chat( self.username, self.token, self.channel ) + IRC = Chat(self.username, self.token, self.channel) IRC.start() except KeyboardInterrupt: - None # dont handle KeyboardInterruption here + None # dont handle KeyboardInterruption here diff --git a/TwitchChannelPointsMiner/classes/entities/Chat.py b/TwitchChannelPointsMiner/classes/entities/Chat.py index 3b58535..9263fe1 100644 --- a/TwitchChannelPointsMiner/classes/entities/Chat.py +++ b/TwitchChannelPointsMiner/classes/entities/Chat.py @@ -1,16 +1,17 @@ - import irc.bot + class Chat(irc.bot.SingleServerIRCBot): def __init__(self, username, token, channel): self.token = token - self.channel = '#' + channel + self.channel = "#" + channel # Create IRC bot connection - server = 'irc.chat.twitch.tv' + server = "irc.chat.twitch.tv" port = 6667 - irc.bot.SingleServerIRCBot.__init__(self, [(server, port, 'oauth:'+token)], username, username) - + irc.bot.SingleServerIRCBot.__init__( + self, [(server, port, "oauth:" + token)], username, username + ) def on_welcome(self, c, e): # You must request specific capabilities before you can use them diff --git a/TwitchChannelPointsMiner/classes/entities/Streamer.py b/TwitchChannelPointsMiner/classes/entities/Streamer.py index 6f93356..ab386e5 100644 --- a/TwitchChannelPointsMiner/classes/entities/Streamer.py +++ b/TwitchChannelPointsMiner/classes/entities/Streamer.py @@ -1,15 +1,16 @@ import logging import time -from TwitchChannelPointsMiner.classes.TwitchChat import TwitchChat, ChatSettings from TwitchChannelPointsMiner.classes.entities.Bet import BetSettings from TwitchChannelPointsMiner.classes.entities.Stream import Stream from TwitchChannelPointsMiner.classes.Settings import Settings +from TwitchChannelPointsMiner.classes.TwitchChat import ChatSettings, TwitchChat from TwitchChannelPointsMiner.constants.twitch import URL from TwitchChannelPointsMiner.utils import _millify logger = logging.getLogger(__name__) + class StreamerSettings(object): def __init__( self, @@ -18,7 +19,7 @@ class StreamerSettings(object): claim_drops: bool = None, watch_streak: bool = None, bet: BetSettings = None, - chat_client: ChatSettings = None + chat_client: ChatSettings = None, ): self.make_predictions = make_predictions self.follow_raid = follow_raid @@ -108,11 +109,15 @@ class Streamer(object): return self.stream_up == 0 or ((time.time() - self.stream_up) > 120) def leave_chat(self): - if(self.chat is not None): + if self.chat is not None: self.chat.terminate() def join_chat(self): - if(self.settings.chat_client is not None): - self.chat = TwitchChat(self.settings.chat_client.username, self.settings.chat_client.token, self.username) + if self.settings.chat_client is not None: + self.chat = TwitchChat( + self.settings.chat_client.username, + self.settings.chat_client.token, + self.username, + ) logger.info(f"Connecting to {self.username}'s chat!") self.chat.start() diff --git a/example.py b/example.py index 659f696..bbcd75b 100644 --- a/example.py +++ b/example.py @@ -68,4 +68,4 @@ def main(): ) if __name__ == "__main__": - main() \ No newline at end of file + main() From 312adbc8992fc80b456f777512dc3bc52ac6c9a3 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Fri, 26 Feb 2021 12:49:10 +0100 Subject: [PATCH 020/117] mutex in __slots__, missed after merge --- TwitchChannelPointsMiner/classes/entities/Streamer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/TwitchChannelPointsMiner/classes/entities/Streamer.py b/TwitchChannelPointsMiner/classes/entities/Streamer.py index 00d0025..6083217 100644 --- a/TwitchChannelPointsMiner/classes/entities/Streamer.py +++ b/TwitchChannelPointsMiner/classes/entities/Streamer.py @@ -63,6 +63,7 @@ class Streamer(object): "raid", "history", "streamer_url", + "mutex", ] def __init__(self, username, settings=None): From eb742008b30f6486bc75d6b1703cc43ed0308f38 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Fri, 26 Feb 2021 12:49:31 +0100 Subject: [PATCH 021/117] update charts.html --- assets/charts.html | 46 +++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/assets/charts.html b/assets/charts.html index 0ae4603..5e8f19d 100644 --- a/assets/charts.html +++ b/assets/charts.html @@ -16,25 +16,26 @@
@@ -46,6 +47,13 @@
+ +
+
If you want to help on this project, please leave a star 🌟 and share it with your friends! 😎 - A coffee is always a gesture of LOVE ❀️ + + Buy Me A Coffee + +
From 65da7cde7fa80b72b8a3027e89553fc9245a5799 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Mon, 1 Mar 2021 22:16:00 +0100 Subject: [PATCH 031/117] x-Label with HH:mm:ss dd MMM --- assets/charts.html | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/assets/charts.html b/assets/charts.html index b12ca42..84c40b1 100644 --- a/assets/charts.html +++ b/assets/charts.html @@ -76,6 +76,9 @@ dataLabels: { enabled: false }, + stroke: { + curve: 'smooth', + }, markers: { size: 0, }, @@ -102,7 +105,11 @@ type: 'datetime' }, tooltip: { - shared: false + shared: false, + x: { + show: true, + format: 'HH:mm:ss dd MMM', + }, }, noData: { text: 'Loading...' @@ -121,7 +128,7 @@ $("li").removeClass( "is-active" ) $("li").eq(index - 1).addClass('is-active'); currentStreamer = streamer; - getData(streamer); + getStreamerData(streamer); } function getStreamerData(streamer) { From 25f5f1231fdf735712cc00d665ce530d13c10929 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Mon, 1 Mar 2021 22:46:07 +0100 Subject: [PATCH 032/117] Skip annotation mark for LOSE vent. Save prediction made in yello as marker - Commit not tested --- TwitchChannelPointsMiner/classes/WebSocketsPool.py | 13 +++++++++---- .../classes/entities/Streamer.py | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/TwitchChannelPointsMiner/classes/WebSocketsPool.py b/TwitchChannelPointsMiner/classes/WebSocketsPool.py index 643887f..0387835 100644 --- a/TwitchChannelPointsMiner/classes/WebSocketsPool.py +++ b/TwitchChannelPointsMiner/classes/WebSocketsPool.py @@ -362,12 +362,17 @@ class WebSocketsPool: counter=-1, ) - ws.streamers[streamer_index].persistent_points( - event_result["type"], - f"{points_prefix}{points_won} - {result_type}: {ws.events_predictions[event_id].title}", - ) + if result_type != "LOSE": + ws.streamers[streamer_index].persistent_points( + event_result["type"], + f"{result_type}: {ws.events_predictions[event_id].title}", + ) elif message.type == "prediction-made": event_prediction.bet_confirmed = True + ws.streamers[streamer_index].persistent_points( + "PREDICTION_MADE", + f"PREDICTION MADE: {event_prediction.bet.decision['choice']} {event_prediction.title}", + ) except Exception: logger.error( f"Exception raised for topic: {message.topic} and message: {message}", diff --git a/TwitchChannelPointsMiner/classes/entities/Streamer.py b/TwitchChannelPointsMiner/classes/entities/Streamer.py index 6083217..99c8191 100644 --- a/TwitchChannelPointsMiner/classes/entities/Streamer.py +++ b/TwitchChannelPointsMiner/classes/entities/Streamer.py @@ -156,11 +156,11 @@ class Streamer(object): # === ANALYTICS === # def persistent_points(self, event_type, event_text): event_type = event_type.upper() - if event_type in ["WATCH_STREAK", "WIN", "LOSE"]: + if event_type in ["WATCH_STREAK", "WIN", "PREDICTION_MADE"]: primary_color = ( "#45c1ff" if event_type == "WATCH_STREAK" - else ("#ff4560" if event_type == "LOSE" else "#54ff45") + else ("#ffe045" if event_type == "LOSE" else "#54ff45") ) data = { "marker": { From 6a080c536325df95d822e4864df0d98be95b1fc9 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Wed, 3 Mar 2021 20:36:04 +0100 Subject: [PATCH 033/117] Update assets/charts.html Co-authored-by: Thomas Couchoud --- assets/charts.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/charts.html b/assets/charts.html index 84c40b1..e37e6b9 100644 --- a/assets/charts.html +++ b/assets/charts.html @@ -41,7 +41,7 @@
From b8d8808f3a83cb87bf592a799f5577f5478558ab Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Wed, 3 Mar 2021 20:37:29 +0100 Subject: [PATCH 034/117] Custom tooltips for event, save in 'z' axis the event type and show in custom tooltip - Remove dt. Replace points annotation with vertical annotations --- .../classes/WebSocketsPool.py | 16 +++++--- .../classes/entities/Streamer.py | 37 +++++++++---------- assets/charts.html | 22 ++++++++--- 3 files changed, 45 insertions(+), 30 deletions(-) diff --git a/TwitchChannelPointsMiner/classes/WebSocketsPool.py b/TwitchChannelPointsMiner/classes/WebSocketsPool.py index 0387835..9295cdc 100644 --- a/TwitchChannelPointsMiner/classes/WebSocketsPool.py +++ b/TwitchChannelPointsMiner/classes/WebSocketsPool.py @@ -180,7 +180,11 @@ class WebSocketsPool: if message.type in ["points-earned", "points-spent"]: balance = message.data["balance"]["balance"] ws.streamers[streamer_index].channel_points = balance - ws.streamers[streamer_index].persistent_series() + ws.streamers[streamer_index].persistent_series( + event_type=message.data["point_gain"]["reason_code"] + if message.type == "points-earned" + else "Spent" + ) if message.type == "points-earned": earned = message.data["point_gain"]["total_points"] @@ -197,7 +201,7 @@ class WebSocketsPool: ws.streamers[streamer_index].update_history( reason_code, earned ) - ws.streamers[streamer_index].persistent_points( + ws.streamers[streamer_index].persistent_annotations( reason_code, f"+{earned} - {reason_code}" ) elif message.type == "claim-available": @@ -363,15 +367,15 @@ class WebSocketsPool: ) if result_type != "LOSE": - ws.streamers[streamer_index].persistent_points( + ws.streamers[streamer_index].persistent_annotations( event_result["type"], - f"{result_type}: {ws.events_predictions[event_id].title}", + f"{ws.events_predictions[event_id].title}", ) elif message.type == "prediction-made": event_prediction.bet_confirmed = True - ws.streamers[streamer_index].persistent_points( + ws.streamers[streamer_index].persistent_annotations( "PREDICTION_MADE", - f"PREDICTION MADE: {event_prediction.bet.decision['choice']} {event_prediction.title}", + f"Decision: {event_prediction.bet.decision['choice']} - {event_prediction.title}", ) except Exception: logger.error( diff --git a/TwitchChannelPointsMiner/classes/entities/Streamer.py b/TwitchChannelPointsMiner/classes/entities/Streamer.py index 99c8191..41f633b 100644 --- a/TwitchChannelPointsMiner/classes/entities/Streamer.py +++ b/TwitchChannelPointsMiner/classes/entities/Streamer.py @@ -2,6 +2,7 @@ import json import logging import os import time +from datetime import datetime from threading import Lock from TwitchChannelPointsMiner.classes.entities.Bet import BetSettings @@ -154,39 +155,37 @@ class Streamer(object): ) # === ANALYTICS === # - def persistent_points(self, event_type, event_text): + def persistent_annotations(self, event_type, event_text): event_type = event_type.upper() if event_type in ["WATCH_STREAK", "WIN", "PREDICTION_MADE"]: primary_color = ( "#45c1ff" if event_type == "WATCH_STREAK" - else ("#ffe045" if event_type == "LOSE" else "#54ff45") + else ("#ffe045" if event_type == "PREDICTION_MADE" else "#54ff45") ) data = { - "marker": { - "size": 5, - "fillColor": "#fff", - "strokeColor": primary_color, - "radius": 2, - }, + "borderColor": primary_color, "label": { - "borderColor": primary_color, - "offsetY": 0, - "style": { - "color": "#fff", - "background": primary_color, - }, + "style": {"color": "#fff", "background": primary_color}, "text": event_text, }, } - self.__save_json("points", data) + self.__save_json("annotations", data) - def persistent_series(self): - self.__save_json("series") + def persistent_series(self, event_type="Watch"): + self.__save_json("series", event_type=event_type) - def __save_json(self, key, data={}): + def __save_json(self, key, data={}, event_type="Watch"): # https://stackoverflow.com/questions/4676195/why-do-i-need-to-multiply-unix-timestamps-by-1000-in-javascript - data.update({"x": round(time.time() * 1000), "y": self.channel_points}) + # data.update({"x": round(time.time() * 1000)}) + now = datetime.now().replace(microsecond=0) + data.update({"x": round(datetime.timestamp(now) * 1000)}) + + if key == "series": + data.update({"y": self.channel_points}) + if event_type is not None: + data.update({"z": event_type.replace("_", " ").title()}) + fname = os.path.join(Settings.analytics_path, f"{self.username}.json") with self.mutex: json_data = json.load(open(fname, "r")) if os.path.isfile(fname) else {} diff --git a/assets/charts.html b/assets/charts.html index 84c40b1..d4eb0c5 100644 --- a/assets/charts.html +++ b/assets/charts.html @@ -68,9 +68,6 @@ type: 'x', enabled: true, autoScaleYaxis: true - }, - toolbar: { - autoSelected: 'zoom' } }, dataLabels: { @@ -110,6 +107,21 @@ show: true, format: 'HH:mm:ss dd MMM', }, + custom: ({series, seriesIndex, dataPointIndex, w}) => { + console.log(w.globals) + return (`
+
${w.globals.seriesNames[seriesIndex]}
+
+
+
+ Points: ${series[seriesIndex][dataPointIndex]}
+ Reason: ${w.globals.seriesZ[seriesIndex][dataPointIndex] ? w.globals.seriesZ[seriesIndex][dataPointIndex] : ''} +
+
+
+
` + ) + } }, noData: { text: 'Loading...' @@ -139,8 +151,8 @@ name: streamer.replace(".json", ""), data: response["series"] }], true) - response["points"].forEach(annotation => { - chart.addPointAnnotation(annotation, true) + response["annotations"].forEach(annotation => { + chart.addXaxisAnnotation(annotation, true) }) setTimeout(function(){getStreamerData(streamer);}, 300000); // 5 minutes }); From 5d7478e027ba675b2c3016544be9deaba638efb6 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Wed, 3 Mar 2021 21:55:57 +0100 Subject: [PATCH 035/117] Toggle annotations --- assets/charts.html | 43 ++++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/assets/charts.html b/assets/charts.html index bb4b29e..4de046b 100644 --- a/assets/charts.html +++ b/assets/charts.html @@ -39,13 +39,24 @@

-
- +
+
+
+ +
+
+
+ +
+

@@ -108,7 +119,6 @@ format: 'HH:mm:ss dd MMM', }, custom: ({series, seriesIndex, dataPointIndex, w}) => { - console.log(w.globals) return (`
${w.globals.seriesNames[seriesIndex]}
@@ -130,6 +140,7 @@ var chart = new ApexCharts(document.querySelector("#chart"), options); var currentStreamer = null; + var annotations = []; $(document).ready(function() { chart.render(); @@ -146,19 +157,29 @@ function getStreamerData(streamer) { if(currentStreamer == streamer){ $.getJSON(`./json/${streamer}`, function (response) { - chart.clearAnnotations() chart.updateSeries([{ name: streamer.replace(".json", ""), data: response["series"] }], true) - response["annotations"].forEach(annotation => { - chart.addXaxisAnnotation(annotation, true) - }) + annotations = response["annotations"]; + updateAnnotations() setTimeout(function(){getStreamerData(streamer);}, 300000); // 5 minutes }); } } + function updateAnnotations() { + if($('#annotations').prop("checked") === true){ + chart.clearAnnotations() + if(annotations && annotations.length > 0) + annotations.forEach(annotation => { + chart.addXaxisAnnotation(annotation, true) + }) + } else chart.clearAnnotations() + } + + $('#annotations').click(() => { updateAnnotations(); }); + From 7de7ea62e039f7183785d4289a1a6b551bd0af56 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Wed, 3 Mar 2021 22:24:00 +0100 Subject: [PATCH 036/117] Texmux tutorial in README.md --- README.md | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2fa5e6c..adc51be 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,8 @@ Read more about channels point [here](https://help.twitch.tv/s/article/channel-p - [Example](#example) 6. πŸͺ [Migrating from old repository (the original one)](#migrating-from-old-repository-the-original-one) 7. πŸͺŸ [Windows](#windows) -8. ⚠️ [Disclaimer](#disclaimer) +8. πŸ“± [Termux](#termux) +9. ⚠️ [Disclaimer](#disclaimer) ## Community @@ -408,5 +409,40 @@ Other useful info can be founded here: You can also follow this [video tutorial](https://www.youtube.com/watch?v=0VkM7NOZkuA). +## Termux +Install the requirements + +`pkg install python git rust libjpeg-turbo libcrypt ndk-sysroot clang zlib` + +`LDFLAGS="-L${PREFIX}/lib/" CFLAGS="-I${PREFIX}/include/" pip install --upgrade wheel pillow` + +Clone this repository + +`git clone https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2` + +(2 way): +Download sources from GitHub and put it into your Termux storage + +Now you can enter the directory with our miner, do this by typing this command: + +`cd Twitch-Channel-Points-Miner-v2` + +Configure your miner on your preferences by typing + +`nano example.py` + +When you have configured it now we can rename it (optional): + +`mv example.py run.py` + +We have to also install dependences required to run miner: + +`pip install -r requirements.txt` + +Now when we did everything we can run miner: + +`python run.py` + +Read more at [#92](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/92) [#76](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/76) ## Disclaimer This project comes with no guarantee or warranty. You are responsible for whatever happens from using this project. It is possible to get soft or hard banned by using this project if you are not careful. This is a personal project and is in no way affiliated with Twitch. From ddcd70d6f33c54c7fda7f34e5feff77e8dff2dae Mon Sep 17 00:00:00 2001 From: Nicholas Allam Date: Wed, 3 Mar 2021 18:43:00 +0000 Subject: [PATCH 037/117] Use requests.Session() in Twitch class --- TwitchChannelPointsMiner/classes/Twitch.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/TwitchChannelPointsMiner/classes/Twitch.py b/TwitchChannelPointsMiner/classes/Twitch.py index 08ce5ad..61d1fc1 100644 --- a/TwitchChannelPointsMiner/classes/Twitch.py +++ b/TwitchChannelPointsMiner/classes/Twitch.py @@ -30,15 +30,16 @@ logger = logging.getLogger(__name__) class Twitch(object): - __slots__ = ["cookies_file", "user_agent", "twitch_login", "running"] + __slots__ = ["cookies_file", "session", "twitch_login", "running"] def __init__(self, username, user_agent, password=None): cookies_path = os.path.join(Path().absolute(), "cookies") Path(cookies_path).mkdir(parents=True, exist_ok=True) self.cookies_file = os.path.join(cookies_path, f"{username}.pkl") - self.user_agent = user_agent + self.session = requests.Session() + self.session.headers.update({"User-Agent": user_agent}) self.twitch_login = TwitchLogin( - CLIENT_ID, username, self.user_agent, password=password + CLIENT_ID, username, user_agent, password=password ) self.running = True @@ -86,13 +87,12 @@ class Twitch(object): def get_spade_url(self, streamer): try: - headers = {"User-Agent": self.user_agent} - main_page_request = requests.get(streamer.streamer_url, headers=headers) + main_page_request = self.session.get(streamer.streamer_url) response = main_page_request.text regex_settings = "(https://static.twitchcdn.net/config/settings.*?js)" settings_url = re.search(regex_settings, response).group(1) - settings_request = requests.get(settings_url, headers=headers) + settings_request = self.session.get(settings_url) response = settings_request.text regex_spade = '"spade_url":"(.*?)"' streamer.stream.spade_url = re.search(regex_spade, response).group(1) @@ -217,13 +217,12 @@ class Twitch(object): def post_gql_request(self, json_data): try: - response = requests.post( + response = self.session.post( GQLOperations.url, json=json_data, headers={ "Authorization": f"OAuth {self.twitch_login.get_auth_token()}", "Client-Id": CLIENT_ID, - "User-Agent": self.user_agent, }, ) logger.debug( @@ -320,10 +319,9 @@ class Twitch(object): next_iteration = time.time() + 60 / len(streamers_watching) try: - response = requests.post( + response = self.session.post( streamers[index].stream.spade_url, data=streamers[index].stream.encode_payload(), - headers={"User-Agent": self.user_agent}, timeout=60, ) logger.debug( From 4b0b2f1ab7df616dfbbf6f77e375e739a7566d0f Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Fri, 5 Mar 2021 11:17:22 +0100 Subject: [PATCH 038/117] Correct channel_id from balance - Will be used on points-spent #103 --- TwitchChannelPointsMiner/classes/entities/Message.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/TwitchChannelPointsMiner/classes/entities/Message.py b/TwitchChannelPointsMiner/classes/entities/Message.py index 450764c..cb3330a 100644 --- a/TwitchChannelPointsMiner/classes/entities/Message.py +++ b/TwitchChannelPointsMiner/classes/entities/Message.py @@ -58,7 +58,11 @@ class Message(object): else ( self.data["channel_id"] if "channel_id" in self.data - else self.topic_user + else ( + self.data["balance"]["channel_id"] + if "balance" in self.data + else self.topic_user + ) ) ) ) From f0ec7592319d44800c33cb88c549b05c257e1e98 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Fri, 5 Mar 2021 11:30:49 +0100 Subject: [PATCH 039/117] Customizable refresh time --- README.md | 4 +++- .../TwitchChannelPointsMiner.py | 4 ++-- TwitchChannelPointsMiner/classes/AnalyticsServer.py | 13 +++++++++---- assets/charts.html | 4 +++- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 24468f2..b1f5042 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ If you have any issues or you want to contribute, you are welcome! But please be - Auto claim game drops from Twitch inventory [#21](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/21) Read more about game drops [here](https://help.twitch.tv/s/article/mission-based-drops) - Place the bet / make a prediction and win or lose (πŸ€) your channel points! No browser needed. [#41](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/41) ([@lay295](https://github.com/lay295)) +- Analytics chart ## Logs feature ### Full logs @@ -261,10 +262,11 @@ twitch_miner = TwitchChannelPointsMiner("your-twitch-username") twitch_miner.mine(followers=True, blacklist=["user1", "user2"]) # Blacklist example ``` If you want to use the analytics function [#96](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/96) call the analytics method before start mining. +The chart will be autofreshed each `refresh` minutes. ```python from TwitchChannelPointsMiner import TwitchChannelPointsMiner twitch_miner = TwitchChannelPointsMiner("your-twitch-username") -twitch_miner.analytics(host="127.0.0.1", port=5000) # Analytics web-server +twitch_miner.analytics(host="127.0.0.1", port=5000, refresh=5) # Analytics web-server twitch_miner.mine(followers=True, blacklist=["user1", "user2"]) ``` 4. Start mining! `python run.py` diff --git a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py index ee9db06..7735219 100644 --- a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py +++ b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py @@ -108,8 +108,8 @@ class TwitchChannelPointsMiner: for sign in [signal.SIGINT, signal.SIGSEGV, signal.SIGTERM]: signal.signal(sign, self.end) - def analytics(self, host="127.0.0.1", port=5000): - http_server = AnalyticsServer(host=host, port=port) + def analytics(self, host: str = "127.0.0.1", port: int = 5000, refresh: int = 5): + http_server = AnalyticsServer(host=host, port=port, refresh=refresh) http_server.daemon = True http_server.name = "Analytics Thread" http_server.start() diff --git a/TwitchChannelPointsMiner/classes/AnalyticsServer.py b/TwitchChannelPointsMiner/classes/AnalyticsServer.py index 287388f..0a5c0a1 100644 --- a/TwitchChannelPointsMiner/classes/AnalyticsServer.py +++ b/TwitchChannelPointsMiner/classes/AnalyticsServer.py @@ -30,23 +30,28 @@ def read_json(streamer): ) -def index(): - return render_template("charts.html", streamers=",".join(streamers_available())) +def index(refresh=5): + return render_template( + "charts.html", + refresh=(refresh * 60 * 1000), + streamers=",".join(streamers_available()), + ) class AnalyticsServer(Thread): - def __init__(self, host="127.0.0.1", port=5000): + def __init__(self, host: str = "127.0.0.1", port: int = 5000, refresh: int = 5): super(AnalyticsServer, self).__init__() self.host = host self.port = port + self.refresh = refresh self.app = Flask( __name__, template_folder=os.path.join(Path().absolute(), "assets"), static_folder=os.path.join(Path().absolute(), "assets"), ) - self.app.add_url_rule("/", "index", index) + self.app.add_url_rule("/", "index", index, defaults={"refresh": refresh}) self.app.add_url_rule("/json/", "json", read_json) def run(self): diff --git a/assets/charts.html b/assets/charts.html index 4de046b..0e750c1 100644 --- a/assets/charts.html +++ b/assets/charts.html @@ -44,7 +44,7 @@
@@ -142,6 +142,8 @@ var currentStreamer = null; var annotations = []; + var refresh = parseInt("{{ refresh }}"); + $(document).ready(function() { chart.render(); $("li").eq(0).click(); From 505e1fb54bf134b2b52d8574f72e91e06b86b27f Mon Sep 17 00:00:00 2001 From: Dennis Mulder Date: Sat, 6 Mar 2021 13:02:50 +0100 Subject: [PATCH 040/117] Improved handling of invalid credentials response --- TwitchChannelPointsMiner/classes/Exceptions.py | 4 ++++ TwitchChannelPointsMiner/classes/TwitchLogin.py | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/TwitchChannelPointsMiner/classes/Exceptions.py b/TwitchChannelPointsMiner/classes/Exceptions.py index 50d6abb..9f88b60 100644 --- a/TwitchChannelPointsMiner/classes/Exceptions.py +++ b/TwitchChannelPointsMiner/classes/Exceptions.py @@ -8,3 +8,7 @@ class StreamerIsOfflineException(Exception): class WrongCookiesException(Exception): pass + + +class BadCredentialsException(Exception): + pass diff --git a/TwitchChannelPointsMiner/classes/TwitchLogin.py b/TwitchChannelPointsMiner/classes/TwitchLogin.py index 563a4c6..edff3bc 100644 --- a/TwitchChannelPointsMiner/classes/TwitchLogin.py +++ b/TwitchChannelPointsMiner/classes/TwitchLogin.py @@ -10,7 +10,7 @@ import pickle import browser_cookie3 import requests -from TwitchChannelPointsMiner.classes.Exceptions import WrongCookiesException +from TwitchChannelPointsMiner.classes.Exceptions import WrongCookiesException, BadCredentialsException logger = logging.getLogger(__name__) @@ -104,6 +104,12 @@ class TwitchLogin(object): elif err_code == 3001: # invalid password logger.info("Invalid username or password, please try again.") + + # If the password is loaded from run.py, require the user to fix it there. + if self.password is not None: + raise BadCredentialsException("Username or password is incorrect.") + + # If the user didn't load the password from run.py we can just ask for it again. break elif err_code == 1000: logger.info( From 0911f76356fbdf6385a0f6209fd4b14d8ac261ab Mon Sep 17 00:00:00 2001 From: Dennis Mulder Date: Sat, 6 Mar 2021 15:36:44 +0100 Subject: [PATCH 041/117] fixed styling --- TwitchChannelPointsMiner/classes/TwitchLogin.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/TwitchChannelPointsMiner/classes/TwitchLogin.py b/TwitchChannelPointsMiner/classes/TwitchLogin.py index edff3bc..169d3ad 100644 --- a/TwitchChannelPointsMiner/classes/TwitchLogin.py +++ b/TwitchChannelPointsMiner/classes/TwitchLogin.py @@ -10,7 +10,10 @@ import pickle import browser_cookie3 import requests -from TwitchChannelPointsMiner.classes.Exceptions import WrongCookiesException, BadCredentialsException +from TwitchChannelPointsMiner.classes.Exceptions import ( + BadCredentialsException, + WrongCookiesException, +) logger = logging.getLogger(__name__) @@ -107,7 +110,9 @@ class TwitchLogin(object): # If the password is loaded from run.py, require the user to fix it there. if self.password is not None: - raise BadCredentialsException("Username or password is incorrect.") + raise BadCredentialsException( + "Username or password is incorrect." + ) # If the user didn't load the password from run.py we can just ask for it again. break From e02045e3e4237996bf5ab396914911eb09cff2d5 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Sun, 7 Mar 2021 11:43:13 +0100 Subject: [PATCH 042/117] Revert "Use requests.Session() in Twitch class" --- TwitchChannelPointsMiner/classes/Twitch.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/TwitchChannelPointsMiner/classes/Twitch.py b/TwitchChannelPointsMiner/classes/Twitch.py index 61d1fc1..08ce5ad 100644 --- a/TwitchChannelPointsMiner/classes/Twitch.py +++ b/TwitchChannelPointsMiner/classes/Twitch.py @@ -30,16 +30,15 @@ logger = logging.getLogger(__name__) class Twitch(object): - __slots__ = ["cookies_file", "session", "twitch_login", "running"] + __slots__ = ["cookies_file", "user_agent", "twitch_login", "running"] def __init__(self, username, user_agent, password=None): cookies_path = os.path.join(Path().absolute(), "cookies") Path(cookies_path).mkdir(parents=True, exist_ok=True) self.cookies_file = os.path.join(cookies_path, f"{username}.pkl") - self.session = requests.Session() - self.session.headers.update({"User-Agent": user_agent}) + self.user_agent = user_agent self.twitch_login = TwitchLogin( - CLIENT_ID, username, user_agent, password=password + CLIENT_ID, username, self.user_agent, password=password ) self.running = True @@ -87,12 +86,13 @@ class Twitch(object): def get_spade_url(self, streamer): try: - main_page_request = self.session.get(streamer.streamer_url) + headers = {"User-Agent": self.user_agent} + main_page_request = requests.get(streamer.streamer_url, headers=headers) response = main_page_request.text regex_settings = "(https://static.twitchcdn.net/config/settings.*?js)" settings_url = re.search(regex_settings, response).group(1) - settings_request = self.session.get(settings_url) + settings_request = requests.get(settings_url, headers=headers) response = settings_request.text regex_spade = '"spade_url":"(.*?)"' streamer.stream.spade_url = re.search(regex_spade, response).group(1) @@ -217,12 +217,13 @@ class Twitch(object): def post_gql_request(self, json_data): try: - response = self.session.post( + response = requests.post( GQLOperations.url, json=json_data, headers={ "Authorization": f"OAuth {self.twitch_login.get_auth_token()}", "Client-Id": CLIENT_ID, + "User-Agent": self.user_agent, }, ) logger.debug( @@ -319,9 +320,10 @@ class Twitch(object): next_iteration = time.time() + 60 / len(streamers_watching) try: - response = self.session.post( + response = requests.post( streamers[index].stream.spade_url, data=streamers[index].stream.encode_payload(), + headers={"User-Agent": self.user_agent}, timeout=60, ) logger.debug( From a78d3911ccbaf5f105e3b24c9d091e26264aff94 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Sun, 7 Mar 2021 11:51:41 +0100 Subject: [PATCH 043/117] Update README.md --- README.md | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index adc51be..aa1aaa3 100644 --- a/README.md +++ b/README.md @@ -411,36 +411,30 @@ You can also follow this [video tutorial](https://www.youtube.com/watch?v=0VkM7N ## Termux Install the requirements - -`pkg install python git rust libjpeg-turbo libcrypt ndk-sysroot clang zlib` - -`LDFLAGS="-L${PREFIX}/lib/" CFLAGS="-I${PREFIX}/include/" pip install --upgrade wheel pillow` +``` +pkg install python git rust libjpeg-turbo libcrypt ndk-sysroot clang zlib` +LDFLAGS="-L${PREFIX}/lib/" CFLAGS="-I${PREFIX}/include/" pip install --upgrade wheel pillow +``` Clone this repository - `git clone https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2` (2 way): Download sources from GitHub and put it into your Termux storage Now you can enter the directory with our miner, do this by typing this command: - `cd Twitch-Channel-Points-Miner-v2` Configure your miner on your preferences by typing - `nano example.py` When you have configured it now we can rename it (optional): - `mv example.py run.py` We have to also install dependences required to run miner: - `pip install -r requirements.txt` Now when we did everything we can run miner: - `python run.py` Read more at [#92](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/92) [#76](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/76) From f24826eb93a9bac575c3e843e57aa6c9c4390d3f Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Tue, 9 Mar 2021 13:46:58 +0100 Subject: [PATCH 044/117] Save analytics in username dedicated folder --- TwitchChannelPointsMiner/TwitchChannelPointsMiner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py index 7735219..7bc51d5 100644 --- a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py +++ b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py @@ -73,7 +73,7 @@ class TwitchChannelPointsMiner: # Default values for all streamers streamer_settings: StreamerSettings = StreamerSettings(), ): - Settings.analytics_path = os.path.join(Path().absolute(), "analytics") + Settings.analytics_path = os.path.join(Path().absolute(), "analytics", username) Path(Settings.analytics_path).mkdir(parents=True, exist_ok=True) self.username = username From e310ac141ddac9be6f42743798c22e032a0f3d23 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Tue, 9 Mar 2021 14:31:12 +0100 Subject: [PATCH 045/117] After a won/lose in the bet, show which decision the bot made when he placed the bet. #95 --- .../classes/WebSocketsPool.py | 19 +++++++++++++------ .../classes/entities/Bet.py | 13 ++++++++++--- .../classes/entities/EventPrediction.py | 6 +++--- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/TwitchChannelPointsMiner/classes/WebSocketsPool.py b/TwitchChannelPointsMiner/classes/WebSocketsPool.py index 2e5e25f..c0cb608 100644 --- a/TwitchChannelPointsMiner/classes/WebSocketsPool.py +++ b/TwitchChannelPointsMiner/classes/WebSocketsPool.py @@ -324,8 +324,19 @@ class WebSocketsPool: else "Gained" ) ) + ws.events_predictions[ + event_id + ].result = f"{result_type}, {action}: {points_prefix}{_millify(points_gained)}" + + decision = ws.events_predictions[ + event_id + ].bet.get_decision() + choice = ws.events_predictions[event_id].bet.decision[ + "choice" + ] + logger.info( - f"{ws.events_predictions[event_id]} - Result: {result_type}, {action}: {points_prefix}{_millify(points_gained)}", + f"{ws.events_predictions[event_id]} - Decision: {choice}: {decision['title']} ({decision['color']}) - Result: {ws.events_predictions[event_id].result}", extra={ "emoji": ":bar_chart:", "color": Settings.logger.color_palette.get( @@ -333,11 +344,7 @@ class WebSocketsPool: ), }, ) - ws.events_predictions[event_id].final_result = { - "type": event_result["type"], - "points_won": points_won, - "gained": points_gained, - } + ws.streamers[streamer_index].update_history( "PREDICTION", points_gained ) diff --git a/TwitchChannelPointsMiner/classes/entities/Bet.py b/TwitchChannelPointsMiner/classes/entities/Bet.py index 033227a..cffde0d 100644 --- a/TwitchChannelPointsMiner/classes/entities/Bet.py +++ b/TwitchChannelPointsMiner/classes/entities/Bet.py @@ -153,11 +153,18 @@ class Bet(object): self.__clear_outcomes() def __repr__(self): - return f"Bet(total_users={millify(self.total_users)}, total_points={millify(self.total_points)}), decision={self.decision})\n\t\tOutcome0({self.get_outcome(0)})\n\t\tOutcome1({self.get_outcome(1)})" + return f"Bet(total_users={millify(self.total_users)}, total_points={millify(self.total_points)}), decision={self.decision})\n\t\tOutcome A({self.get_outcome(0)})\n\t\tOutcome B({self.get_outcome(1)})" + + def get_decision(self, parsed=False): + decision = self.outcomes[0 if self.decision["choice"] == "A" else 1] + return decision if parsed is False else Bet.__parse_outcome(decision) + + @staticmethod + def __parse_outcome(outcome): + return f"{outcome['title']} ({outcome['color']}), Points: {millify(outcome[OutcomeKeys.TOTAL_POINTS])}, Users: {millify(outcome[OutcomeKeys.TOTAL_USERS])} ({outcome[OutcomeKeys.PERCENTAGE_USERS]}%), Odds: {outcome[OutcomeKeys.ODDS]} ({outcome[OutcomeKeys.ODDS_PERCENTAGE]}%)" def get_outcome(self, index): - outcome = self.outcomes[index] - return f"{outcome['title']} ({outcome['color']}), Points: {millify(outcome[OutcomeKeys.TOTAL_POINTS])}, Users: {millify(outcome[OutcomeKeys.TOTAL_USERS])} ({outcome[OutcomeKeys.PERCENTAGE_USERS]}%), Odds: {outcome[OutcomeKeys.ODDS]} ({outcome[OutcomeKeys.ODDS_PERCENTAGE]}%)" + return Bet.__parse_outcome(self.outcomes[index]) def __clear_outcomes(self): for index in range(0, len(self.outcomes)): diff --git a/TwitchChannelPointsMiner/classes/entities/EventPrediction.py b/TwitchChannelPointsMiner/classes/entities/EventPrediction.py index 57ea6bc..d23249f 100644 --- a/TwitchChannelPointsMiner/classes/entities/EventPrediction.py +++ b/TwitchChannelPointsMiner/classes/entities/EventPrediction.py @@ -12,7 +12,7 @@ class EventPrediction(object): "created_at", "prediction_window_seconds", "status", - "final_result", + "result", "box_fillable", "bet_confirmed", "bet_placed", @@ -36,7 +36,7 @@ class EventPrediction(object): self.created_at = created_at self.prediction_window_seconds = prediction_window_seconds self.status = status - self.final_result: dict = {} + self.result = "" self.box_fillable = False self.bet_confirmed = False @@ -60,4 +60,4 @@ class EventPrediction(object): return float_round(self.prediction_window_seconds - self.elapsed(timestamp)) def print_recap(self) -> str: - return f"{self}\n\t\t{self.bet}\n\t\tResult: {self.final_result}" + return f"{self}\n\t\t{self.bet}\n\t\tResult: {self.result}" From 0cf13c229a1ff4d226a7907747d02a01b74bcaaa Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Tue, 9 Mar 2021 19:17:05 +0100 Subject: [PATCH 046/117] Parse result in separated method. Save again gains and type. Maybe It will be helpful in the future --- .../classes/WebSocketsPool.py | 51 ++++--------------- .../classes/entities/EventPrediction.py | 35 +++++++++++-- 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/TwitchChannelPointsMiner/classes/WebSocketsPool.py b/TwitchChannelPointsMiner/classes/WebSocketsPool.py index c0cb608..8913bb5 100644 --- a/TwitchChannelPointsMiner/classes/WebSocketsPool.py +++ b/TwitchChannelPointsMiner/classes/WebSocketsPool.py @@ -13,7 +13,6 @@ from TwitchChannelPointsMiner.classes.Settings import Settings from TwitchChannelPointsMiner.classes.TwitchWebSocket import TwitchWebSocket from TwitchChannelPointsMiner.constants import WEBSOCKET from TwitchChannelPointsMiner.utils import ( - _millify, get_streamer_index, internet_connection_available, ) @@ -300,66 +299,38 @@ class WebSocketsPool: message.type == "prediction-result" and event_prediction.bet_confirmed ): - event_result = message.data["prediction"]["result"] - result_type = event_result["type"] - points_placed = event_prediction.bet.decision["amount"] - points_won = ( - event_result["points_won"] - if event_result["points_won"] - or result_type == "REFUND" - else 0 + points = event_prediction.parse_result( + message.data["prediction"]["result"] ) - points_gained = ( - points_won - points_placed - if result_type != "REFUND" - else 0 - ) - points_prefix = "+" if points_gained >= 0 else "" - action = ( - "Lost" - if result_type == "LOSE" - else ( - "Refunded" - if result_type == "REFUND" - else "Gained" - ) - ) - ws.events_predictions[ - event_id - ].result = f"{result_type}, {action}: {points_prefix}{_millify(points_gained)}" - decision = ws.events_predictions[ - event_id - ].bet.get_decision() - choice = ws.events_predictions[event_id].bet.decision[ - "choice" - ] + decision = event_prediction.bet.get_decision() + choice = event_prediction.bet.decision["choice"] logger.info( - f"{ws.events_predictions[event_id]} - Decision: {choice}: {decision['title']} ({decision['color']}) - Result: {ws.events_predictions[event_id].result}", + f"{event_prediction} - Decision: {choice}: {decision['title']} ({decision['color']}) - Result: {event_prediction.result['string']}", extra={ "emoji": ":bar_chart:", "color": Settings.logger.color_palette.get( - f"BET_{result_type}" + f"BET_{event_prediction.result['type']}" ), }, ) ws.streamers[streamer_index].update_history( - "PREDICTION", points_gained + "PREDICTION", points["gained"] ) # Remove duplicate history records from previous message sent in community-points-user-v1 - if result_type == "REFUND": + if event_prediction.result["type"] == "REFUND": ws.streamers[streamer_index].update_history( "REFUND", - -points_placed, + -points["placed"], counter=-1, ) - elif result_type == "WIN": + elif event_prediction.result["type"] == "WIN": ws.streamers[streamer_index].update_history( "PREDICTION", - -points_won, + -points["won"], counter=-1, ) elif message.type == "prediction-made": diff --git a/TwitchChannelPointsMiner/classes/entities/EventPrediction.py b/TwitchChannelPointsMiner/classes/entities/EventPrediction.py index d23249f..7349aa4 100644 --- a/TwitchChannelPointsMiner/classes/entities/EventPrediction.py +++ b/TwitchChannelPointsMiner/classes/entities/EventPrediction.py @@ -1,7 +1,7 @@ from TwitchChannelPointsMiner.classes.entities.Bet import Bet from TwitchChannelPointsMiner.classes.entities.Streamer import Streamer from TwitchChannelPointsMiner.classes.Settings import Settings -from TwitchChannelPointsMiner.utils import float_round +from TwitchChannelPointsMiner.utils import _millify, float_round class EventPrediction(object): @@ -36,7 +36,7 @@ class EventPrediction(object): self.created_at = created_at self.prediction_window_seconds = prediction_window_seconds self.status = status - self.result = "" + self.result: dict = {} self.box_fillable = False self.bet_confirmed = False @@ -60,4 +60,33 @@ class EventPrediction(object): return float_round(self.prediction_window_seconds - self.elapsed(timestamp)) def print_recap(self) -> str: - return f"{self}\n\t\t{self.bet}\n\t\tResult: {self.result}" + return f"{self}\n\t\t{self.bet}\n\t\tResult: {self.result['string']}" + + def parse_result(self, result) -> dict: + result_type = result["type"] + + points = {} + points["placed"] = self.bet.decision["amount"] + points["won"] = ( + result["points_won"] + if result["points_won"] or result_type == "REFUND" + else 0 + ) + points["gained"] = ( + points["won"] - points["placed"] if result_type != "REFUND" else 0 + ) + points["prefix"] = "+" if points["gained"] >= 0 else "" + + action = ( + "Lost" + if result_type == "LOSE" + else ("Refunded" if result_type == "REFUND" else "Gained") + ) + + self.result = { + "string": f"{result_type}, {action}: {points['prefix']}{_millify(points['gained'])}", + "type": result_type, + "gained": points["gained"], + } + + return points From 1cb2231690da57faa0041bc00e824f05ad74d86c Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Tue, 9 Mar 2021 19:30:31 +0100 Subject: [PATCH 047/117] Create indexed id for annotation, clear annotation using removeAnnotation instead of clearAnnotation() --- assets/charts.html | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/assets/charts.html b/assets/charts.html index 0e750c1..399998c 100644 --- a/assets/charts.html +++ b/assets/charts.html @@ -65,6 +65,8 @@ Buy Me A Coffee
+ +
+ + - +
-
+
+
-
+
-
+
If you want to help on this project, please leave a star 🌟 and share it with your friends! 😎 - A coffee is always a gesture of LOVE ❀️ Buy Me A Coffee @@ -81,7 +87,9 @@ type: 'x', enabled: true, autoScaleYaxis: true - } + }, + // background: '#2B2D3E', + foreColor: '#fff' }, dataLabels: { enabled: false @@ -96,6 +104,7 @@ text: 'Channel points', align: 'left' }, + colors: ["#f9826c"], fill: { type: 'gradient', gradient: { @@ -115,6 +124,7 @@ type: 'datetime' }, tooltip: { + theme: 'dark', shared: false, x: { show: true, @@ -190,7 +200,24 @@ }) } + function toggleDarkMode(){ + var darkMode = $('#dark-mode').prop("checked") + $("link[href='{{url_for('static', filename='dark-theme.css')}}']").prop("disabled", !darkMode); + chart.updateOptions({ + colors: darkMode === true ? ["#f9826c"] : ['#008ffb'], + chart: { + foreColor: darkMode === true ? "#fff" : '#373d3f' + }, + tooltip: { + theme: darkMode === true ? "dark" : "light" + } + }) + // if (darkMode === true) $("#chart").addClass("box") + // else $("#chart").removeClass("box") + } + $('#annotations').click(() => { updateAnnotations(); }); + $('#dark-mode').click(() => { toggleDarkMode(); }); diff --git a/assets/dark-theme.css b/assets/dark-theme.css new file mode 100644 index 0000000..f79bdc1 --- /dev/null +++ b/assets/dark-theme.css @@ -0,0 +1,29 @@ +body { + background: #343E59; + color: #fff; +} +.box { + background-color: #2B2D3E; +} +.tabs a { + border-bottom-color: #dbdbdb; + color: #fff; + border-bottom-style: none; +} +.tabs li.is-active a { + border-bottom-color: #f9826c; + color: #fff; + border-bottom-style: solid; +} +.tabs a:hover { + border-bottom-color: #dbdbdb; + color: #dbdbdb; + border-bottom-style: solid; +} +.tabs ul{ + margin-bottom: 5px; + border-bottom-style: none; +} +.checkbox:hover{ + color: #f9826c; +} From 3ad8b4bc790f1bb7462d7ad620ee81aa1faa6c42 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Wed, 10 Mar 2021 13:49:51 +0100 Subject: [PATCH 056/117] Revert https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/pull/96/commits/24bf4c49b8fe002047c9071e34c43f0c232b0301 --- TwitchChannelPointsMiner/classes/entities/Streamer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TwitchChannelPointsMiner/classes/entities/Streamer.py b/TwitchChannelPointsMiner/classes/entities/Streamer.py index 1bd2db3..41f633b 100644 --- a/TwitchChannelPointsMiner/classes/entities/Streamer.py +++ b/TwitchChannelPointsMiner/classes/entities/Streamer.py @@ -178,7 +178,7 @@ class Streamer(object): def __save_json(self, key, data={}, event_type="Watch"): # https://stackoverflow.com/questions/4676195/why-do-i-need-to-multiply-unix-timestamps-by-1000-in-javascript # data.update({"x": round(time.time() * 1000)}) - now = datetime.utcnow().replace(microsecond=0) + now = datetime.now().replace(microsecond=0) data.update({"x": round(datetime.timestamp(now) * 1000)}) if key == "series": From 108702e7a6b4c8f377831a87591f0fcd56fa4a2a Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Wed, 10 Mar 2021 13:54:12 +0100 Subject: [PATCH 057/117] datetimeUTC: false --- assets/charts.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/assets/charts.html b/assets/charts.html index d922428..92ca88b 100644 --- a/assets/charts.html +++ b/assets/charts.html @@ -121,7 +121,10 @@ }, }, xaxis: { - type: 'datetime' + type: 'datetime', + labels: { + datetimeUTC: false + } }, tooltip: { theme: 'dark', From 8d9a839315a451408cbe4d3c284aeb271f1f4a59 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Wed, 10 Mar 2021 22:09:48 +0100 Subject: [PATCH 058/117] Black text in annotations --- TwitchChannelPointsMiner/classes/entities/Streamer.py | 2 +- assets/charts.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/TwitchChannelPointsMiner/classes/entities/Streamer.py b/TwitchChannelPointsMiner/classes/entities/Streamer.py index 41f633b..94080e6 100644 --- a/TwitchChannelPointsMiner/classes/entities/Streamer.py +++ b/TwitchChannelPointsMiner/classes/entities/Streamer.py @@ -166,7 +166,7 @@ class Streamer(object): data = { "borderColor": primary_color, "label": { - "style": {"color": "#fff", "background": primary_color}, + "style": {"color": "#000", "background": primary_color}, "text": event_text, }, } diff --git a/assets/charts.html b/assets/charts.html index 92ca88b..f0e2086 100644 --- a/assets/charts.html +++ b/assets/charts.html @@ -101,7 +101,7 @@ size: 0, }, title: { - text: 'Channel points', + text: 'Channel points (dates are displayed in UTC)', align: 'left' }, colors: ["#f9826c"], From 4ee8ffb17bb504cb56fb261f4eef08d5cc46b22c Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Wed, 10 Mar 2021 22:26:05 +0100 Subject: [PATCH 059/117] Update README.md --- README.md | 36 ++++++++++++++++++++----------- assets/chart-analytics-dark.png | Bin 0 -> 113403 bytes assets/chart-analytics-light.png | Bin 0 -> 109235 bytes 3 files changed, 24 insertions(+), 12 deletions(-) create mode 100644 assets/chart-analytics-dark.png create mode 100644 assets/chart-analytics-light.png diff --git a/README.md b/README.md index 3c648f5..7eb3887 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,11 @@ Read more about channels point [here](https://help.twitch.tv/s/article/channel-p - [Bet strategy](#bet-strategy) - [FilterCondition](#filtercondition) - [Example](#example) -6. πŸͺ [Migrating from old repository (the original one)](#migrating-from-old-repository-the-original-one) -7. πŸͺŸ [Windows](#windows) -8. πŸ“± [Termux](#termux) -9. ⚠️ [Disclaimer](#disclaimer) +6. πŸ“ˆ [Analytics](#analytics) +7. πŸͺ [Migrating from old repository (the original one)](#migrating-from-old-repository-the-original-one) +8. πŸͺŸ [Windows](#windows) +9. πŸ“± [Termux](#termux) +10. ⚠️ [Disclaimer](#disclaimer) ## Community @@ -262,14 +263,6 @@ from TwitchChannelPointsMiner import TwitchChannelPointsMiner twitch_miner = TwitchChannelPointsMiner("your-twitch-username") twitch_miner.mine(followers=True, blacklist=["user1", "user2"]) # Blacklist example ``` -If you want to use the analytics function [#96](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/96) call the analytics method before start mining. -The chart will be autofreshed each `refresh` minutes. -```python -from TwitchChannelPointsMiner import TwitchChannelPointsMiner -twitch_miner = TwitchChannelPointsMiner("your-twitch-username") -twitch_miner.analytics(host="127.0.0.1", port=5000, refresh=5) # Analytics web-server -twitch_miner.mine(followers=True, blacklist=["user1", "user2"]) -``` 4. Start mining! `python run.py` ### Limits @@ -398,6 +391,25 @@ Allowed values for `where` are: `GT, LT, GTE, LTE` - If you want to place the bet ONLY if the highest bet is lower than 2000 `FilterCondition(by=OutcomeKeys.TOP_POINTS, where=Condition.LT, value=2000)` +## Analytics +We have recently introduced a little frontend where you can show with a chart you points trend. The script will spawn a Flask web-server on your machine where you can select binding address and port. +The chart provides some annotation to handle the prediction and watch strike events. Usually annotation are used to notice big increase / decrease of points. If you want to can disable annotations. +On each (x, y) points Its present a tooltip that show points, date time and reason of points gained / lost. This web page was just a funny idea, and it is not intended to use for a professional usage. +If you want you can toggle the dark theme with the dedicated checkbox. + +| Light theme | Dark theme | +| ----------- | ---------- | +| ![Light theme](./assets/chart-analytics-light.png) | ![Dark theme](./assets/chart-analytics-dark.png) | + +For use this feature just call the `analytics` method before start mining. Read more at: [#96](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/96) +The chart will be autofreshed each `refresh` minutes. If you want to connect from one to second machine that have that webpanel you have to use `0.0.0.0` instead of `127.0.0.1`. +```python +from TwitchChannelPointsMiner import TwitchChannelPointsMiner +twitch_miner = TwitchChannelPointsMiner("your-twitch-username") +twitch_miner.analytics(host="127.0.0.1", port=5000, refresh=5) # Analytics web-server +twitch_miner.mine(followers=True, blacklist=["user1", "user2"]) +``` + ## Migrating from an old repository (the original one): If you already have a `twitch-cookies.pkl` and you don't want to log in again, please create a `cookies/` folder in the current directory and then copy the .pkl file with a new name `your-twitch-username.pkl` ``` diff --git a/assets/chart-analytics-dark.png b/assets/chart-analytics-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..d3164cc10eb7d3be8fc0172a5786381e91d1ad1d GIT binary patch literal 113403 zcmX_HbwHHe(_K0wrMsjhmu`@dPC-JtySux)VL@7?rKCZ+yBnmtS@K);_kMq{EbO!Q znVmUz=FGXX5lRYDsK|uK00010`lGlC003tM0Kga_LPP!|vYSf+`GsWvQPT+kKS1sa==UvF|Uz_gQ?;OX=N0z+K;0d*1U_U~?6R}SV%_^@J z*UYC5;XuGHY)>Gj%cD1YL~@rmcb76-5y#f-%JY zJU|1!-g-g*efsBtN93I<`Tt%L3&%;M|92JSr5@x#?Ein4VTdvEe^(*ah_S={{{o*W zGNAukTdX`@F5v$cP)~o(@&EhR0#VOM1L~9#p*SFbsgoRa!gpK&as#Fh|68XK&Q#oG zHQS_Uakx4yU}~lkLmTa~y!AOJ7eg#uJM@3Iwnad}kiu3^t}P!v-dmJaE*n`Aq5Y@{ z|B;>sRb@jVApn~YV()_g|6eZNqq|fb_G#v;Yer@*wx>PXm@2a|4M2&o@N0}J)Ddhe z?8mdao)8Q%VxIqf?w_1LdFqsd>PqDEmqP*M&;U~a`Hn-rdCFu%(J#I&E|}kxks|s4 z02`E&7Qvj5bEof;2SIx93?AghpK1O%B3v7Wew52Lm3A}x0_!(*{jIbGe3BCv8m}!Q z8rD#v4NNp@z8?*|z5&xBSAnEKh+O1z_M1!A_TO(H4i@>JM-bAYphR5Un8}OUj7h~S zF4bxU;LIY_8dm{8ej;}o9Cs}n?iE^AYgi3+2hcBMT3P0S0Bn(Jie6NA0(HkGx?l`9 zVnNU25#RBDk+<&A{6azn7=l5BRgv*PV#xrCrUJ}95OLWo7!sL!C&}MSPAZf^BLet; z*hCWQ_wymCd>6_i0?Hxl${?c#V{9ob?%b7fUC&mdAKqu z=dGkgAe*FB1j=yreHk20U_6hD4J-S`snSj$B0vW$ia%2T4{mZq$ z)fmF*LxtXRMnJoOe~8S@c>B{Ti_c!zjpWf1*4t?=ZrMMuE-P?rKT~}aMEJ9X5Nke+ z%w%%cBmKlpPlomv-{{!e^zz$I5*GaY{yoWT%qf9f;Q||G`ef`czn6}e^$fMG^_M6; z!uPo0h;u*cqhjXHd~O-neP2w>=GAbk*_pj&s2|9O(ILP`Bj@~AoEpg~Z`tR@zkh@q zGJ)A9NEv@8b(zt5r%oa4k=}PbEZ!zT2sd%V~O? zZLE|VWiphXhc+}R%*w+MTcP-0p>(if4;UsfAVS4y)AO6pn>1|bh~E^aJV$Z5TN4Iq z9L99K?A97!zXqWTaeW?fYyV`GB2B&KdWOU!z?E5gh?zJ<@SeAzP^gBk@@AX*mWy@;4jYr4? z1S0}laYJpMtvH8(QqQF;;=I%4xso4+my73^tDE#Yx>7M5 zh$o_+*6{v%!UPjaDYzTPV}Z`#TCgA8L}k#09_p};dSxyCx%VlfC9XIL$&{7e5z|E# z9`c4$?EgOW5d|fsdrt1CiDT38mwqWERDIrWdJG5)yoyJ6!VeoxRl4tuC27~ZLt2Bk z7fJyD*`Yr^L-{YPobS`%?%UIjhVi0&7IpfrdA{-=P~deyKhf~_QKlXH15V6P z_xC@(wg}&E&NI?j&%i6JE|)hRC1`#AT&{5hG7cz2fjHv!FOT_I)1Z3h#s9_eZ0lz2 zP&of+?3G+mcZDw;acSS03&_I}&w}f{u3DH(i2+5-k8nblIXQc##3NxX+QVR>if(^N zF_!_#jyif%4{ws9*iY)4{U!U9h&cd_jDO|Zyjk(??%wWF1y}7K*0a?>{m1t|15}X4 z;FOw)WTvJhI9YmAd#Xov_&5um)7Nt-#eQwKqhMBf1eA5aZ*;!ltlPGYao)&FUcJ@_ zpz!Q!y}7v!&}OvZ_#v(EK=k_P`Z6Q}{OuUD0+rSJ^AiRpVqf}8z|?u(rW{{Ioz|+; z*Xf{*m7=mcGSaIdJ@rLp?6tm8r1!d%MBj(*Y&cA6&&*VpPU*Er@k$DM5y{yjIDjl5 z1utESv~iLMv~k68MFqJ}Wt<-)AZ&Lf_HSa~M#dDfXL;TYYZWVA=Jp1wc!CLY31%A} zg{`KRcb|E4dEbuO>N}j#y^QOj&qyM$M7@74 z>4AM}Y>vQM&YSetX-gu?4cmKF|{QAUubUd0{@UtEhGu(R|B^HH* zk0bFTH6IxirPed%fwz4hE^h0w#_Ri>jAry#BD#_7))Zvi;>UHh8}c?n%1;NF_y?Ru@zzFU1CAqa!Gmi$v?9bhcrDhBDk$uo~- z7|h&iCz9#ILUVL|tGHD)k{tQWkOmK+T-(}%>10cno?QCf1tkJN)au%=K&usxG%lm4 z0g8*ufO81&Nd=sZ-z9t=!*tHkyD}j6UldIB34J)*JM>Va`Aon?U?^1y=L$I5B_hst z?us}vM#f*UY;WE$rL141wFS?oa5Zx%F(e~j)m~v4Vbxm-#u^PmxOdOVe^dYgF##F6 zN{NA(*YzDpp<)!%`X2RsU^R>71L{}4LB8rHTI)*#N6s(LmA0*D4cOcm^1t|DQx>*n zP?+bJG%PDE8s+>e8#tL6L9SENeML6gxGNMBVVQyMGY|^8Qv3q<8ccriCU|vBX!WV zJ>Z#bALz1}Gfnyd&D`iwJa}PS2E{i$c88OgF*9>NGcqGrp?+o^Q$36cLDn(WUqZM* zG`4xnqua8C%APJ-w>!?P`?B32#fqd9>HQ{i?QAd{Ch}v^^k7#)S65yDdBVaMuW6C` z&G+HqJf*9pJzz2g(2F{?56AL@62;g9C>f{Bd`!Zm7<&d@!{7MCdzx9Yjq5YD*v4a6 z9EpsXDwYNf9v+Ii#gW)&pL61P!ovs5 zlUQ~`r9$8I1}=Ky#W2wuzCq;yAnTlKNvTYmG4|tW>!9L1it7UmW{MBrfu+-7qYaF{ zUds@_4wEQ6O*1J8<`wZ8cK+liafVFR1i~YZ8v$laOtOdtRxjv?Chw=bRRv$C-P+kNKR?4d?6WHv8az-@Pg1zbEoui${1VZy#c3Td18GMhP=Pb&q7 z%|6I4PXulT$hKaah)|N7C1d-lLAE=+m7u;R>BkhjrltG}BWnr#fs1}JWhyw88IM_V z>j!8J;ZuC!<^esQgbeOgQ_`igT|{SS8I~RY{hWeQzvp48a-;G>Lv>mObMTvfZX>$}%?U9e8Vqa}WnoU(H z%3!TaD)rxAD$-2nT+GLGju;#hCb*P)f=0D`F;L>rJleT^cP!h~otB?|Lz=|}jXwoC z+-KK9z;R{czHeMBtVA!aZTV)_<|j-Z*r?wt2y&7 zH%tb`6BU^E5Sz#8Q10Nm9WR2gdN_ejUS?F z5M!{_lHDtNd3hnQ)>pVu@TsfS-hD%&WKE(1am9dLPc1pTIh#FuvbgONruVW!3ebqG zeK&N7od$Xq46%Aw$piPioxy>`3YrdWt(gvWx2AM$@&PFpijV(@hL z)9~43+`5fQmoB`R447fED$z$N%Z9%Z2wL1#d4P$-PC|M9#4`=oK zJu$f*COE$Sz9e~YPeIUKZ+*gFnJ@O2&F!ZB*BVI&W$@?u>&wpRY@`fw&TNfgi}?=r?Q#fHcY`72pNCV z+R7OAW1%RJNkBwmyXVu3H@lMG`A$S_nwCN$Tq6%9 zW&Uj!&Dejdg8d5AV#9ZjfVc#)DlfZI!NN zoX)ox9{hfL^3I-*Zes<>R?99noEiD?gf63UbG36+kx)@g%YLV5^q^6bv-Z82qpBRM z8KTPuT(NK%*?;a-6jusvB-vSis&-qaI5`=evzyP}=~J`9Qr;+a%NZ;2em?79^}TiM z=1Z(TRF^1>5%M^^pBR|vi$MZFlkj?6_hdZ2U3?EUD!Dz?bfw?;?B@)x$+brm#=;>< zFjVoyuv6AfZ4X1!)D$`ZA0<+ZA&ou~4ou1@Y#Ol0NOl*0r+@hK`O&72<$+ z6Tl!n-iPzj=n@OXN5=51;BWyW1&|Og2VXgt=NrrfmBahh{xh$~4+lE2h_S0h^J^$s zEfM)7Nk%7JhvE|+TtC+0*2*>a7PRWLEFDBgN58Ye7A!@sH}kRSF>9BV&2AjJ@BRwO z3u$^;@EkL=BO=+o1h<^E44X5ZSwfWD^uGmVi|aRmfHz9V4W^T$660Y}c%}#;pJ=IQ z66wB>V=YENcq~fcOPC^w&%K&uQp@Dl1|{YAo#Kj%x4yZERsH6p61~fHPr~E6s~=_3 zA?$s9$k5CVTcc@r63GH5d_;N{HokJ zY_drjW2NAbp5CkHs2xM%qqZfH1VmVD*4UranMcmKIpxIXl6bVi+JZ}#}M$&`+u8o8VZudLxN$A>ys^C(%qEq=m^}&p^ zT{vS@A{B~f*vKx&uJN|=eQvmBC}iE{P|MyOBvZLE+LomvHyZ&(P-7Uc&=B? z^Me>)(pdSUZ)he+K1PqkoNJ0}tw2@^8Y&Du4lNF48keZ-kMKe}Rq}@+*@Pp4h<>&- zYfF>Ek#x!?<Mngj-ti9MtRrx-l@$+uvK&({l zjS>8UgNcKIt7Tu`Mb3z5!sU~3MQUaSS4Ap?AwUWZ^n@<#Zd5+yYm8m=?$gp~jOOF& z8+b2C`6cA4Z{Oz9?%o2K!uxyK4>u5YY9-S#!`HSai zvfF~Hax=O*LoD1(Y)jF-X+8Cp_F@RC^js?Jvz7-dNHdIN*M;%4!^XC3DJ3MkucWQ= z;R_~t69Gr}$2-kN$JZQV2JM77Z7cR;7gL8JI~0iI`LtxmM+PauJoeFS&q2@KR3Aj` zPo6yhfP_iQ-{%9REsAUu(I6xE0`= zmC;i|-iHvK%@}SX;s%vLHNCt#lgzLD4EQ&hb~btOEF~&76<*J);=yWsKG*jNS9@cP z%LZ`;NDz!L{w?m$J<+BEdhX0Gr$XXP+}Q&Uy-muTW8 z%o83}0J71Gn_gsryN6A{^b(W}Mo^2-9N(s3kTyl_@`b!2d@hmQ%W1COVQP}<9o+l4 za?YHe!d171UhobWbVbNM;7KZ~QfJHBQ8*lB=G?Zu_G#Z=cG`Hn@SVq|eY8aNT4#-O zILa8t(lI*i$GEszgKF^~;UkQ{Vt?O}Y$0(lqa7n()FaGHCzPd8X(zL`vxfe*c&;^S ztT&1Krsj}W^D8#aF7D+_1oE%kljVZhh;kQY8E0^?t&n98Ile zCcuC0;rr0%tPE+x#}l@a z!x@N0Y7}}}pR*xm^B>Mh2dfY3kj#(h93TCj_cV;==3xI^p+5WuM>7awF+#wf@@Kft zc4b>I!4F_TVD)-Yoq`I)HhSAK25pUwiP+OB&EY~jAGPB8)JY~NptV12H@OiQ4soxl zTdtCpL~M6auf)#cqG!{RS9WD96PHsvT;500?p-}&IWktas;S5z2A$h-K z27_8=^<*h{A2Wf?RNwro-&gUvgBu*o8@xAnxlObw3P240hB2sct<9%vo!ZG`7hIXW zA0L3V5#V0e8sXg0qB`{(&)P+E+P6qXt@Wk!Dq3c>aO+oY5A+jSRuyHRTuh=ZT@KrB zrRl%W{eFCmdS@dZ0_kA?sbp?%ce=(C)2Kn@=ASjOnQq}RO|BLvy9@f3ueTz@*)x8R z`&_r_3Te36M^CsiB3tgrxH#1Ge|iv4Mx1isU}e)B!~1a{Cq~~pQg2z%`dOo}67?5O zsWD|!lps%vik6lCP?WQa4hjG6TmA~!&%z$C5#psCYPxjuMDnGzZ^go&ssGBfr?9Xl z7}v_4YSa&dvL6WOsWH4l2ku&pXwhhsvTosy#ShNh&vUk=LhCNV2iU_vG2Fx&xVVQq zT6SG*Y~Nyi&kv4sNrppYw{? zm_NYQH67my%OQ#VHtQcn>O&-t56vyP{w9MKpB8#q$nC$NEzaR>PNzp_{XB>4eRhJr zh6S888jfDzGi7yJu$>=Dsh&N&kbIcv%#%&i;jar(X-}fDpZmlZqh8ylDJ_giUsT_I z`Vvm06qyVSII=%r=gf+1npaHS^XOI4qscmTAciDv!M{2}_%a^m6N(91=?q3j^l!wT z7!mOUwabCY?s*?KkdR9qOKxTG950Xka?@C4#*Jfr^XE;{wQTXi;?L%aBmAz?hKVjBNFh{~Da8t!LBj=*`}KRLo@^O72F4-QziI7~Jrz&|=Z>^ax|W z%A3SuY2&bq9^07)0fdmZwTdtsywKLUYnXdk`Hygiitm0v7r3-n#mkfw)k8G%W2*m= zdJKA-!1t~1lT-Pi9kwx)YdX~4-`J4MzEx(<5^`|=cA{gQL6C5C8irqAaA-)4>=m&m zO&`Z90Ms7GZHD3kauc3d)=zd<&+{KGDdJ*&Y5nLR*XFl{9HP_CLis$q)>zkym+>(Zby0=B2sRo#U%%$1pTXs z&NnQ8*wCH|E~xJul>3(6bRP9ne)sr^q$xzQZQh3J zk5m%-H>C+<#-p8=o!$7}l7K!NJ)5+-xF1A5`n`_vwsa>T-9_)Ozu=Xf(2oavH0s`Yl>^u)EjV)s-`Z93GDts3WAU`~I$h`2&RYF#i)r$e60`zPQ=Q z+oM36Yly-cxFFV647*Yir$2QP*7-_vh#>R%r@(k)cWA zerODHO7#eev#me0wS3Hytzb9#^Hj zs(87QhO`e`0WX1Otqo2f-|P#Oq`+#NImTICu?IBH*pRlCeO`t_ca1?qn~Oia!WV!n$^CN4C7HRTO7Q66CdRlQDX z(B}A55KG=la{1%AQ5JoA6`91Z1(D}|yxFN$`hF$pykesQ_*uPb%dORt0}m{j27|#3 zP73rAk?E6`lI?!oZp<}vOFV1Gi@g%!lcaKxt>ck1|HuP5H*%GE_%lp^`WxTK={~8X zX)RU=oSr669PWeQ)$8q&&~fhlG>hk9V9^rrrGxp1wcVdZ;7p6cZ|meiN!gAyQ6WAp zAdMyo*5(MF3T8Ck&$)6fmZp24?WoUpr!4JcdfB?qRFwPviOvJ`bGCGAWaMTHaaXpCHFGtN9CH12p8Eg1Ow>vfwn-!CK;`~8BcAMvr z3AKh0IG=ShHb2%x7kRR4<%M7ObD-^7Z(j)_zFXM*9!rHtwbuV|QKnkfvGA4UW#GBL zBhJ*S(f)@&ft}paxPZ;;sfq5lr0vy{!D^^IR_!9Y){)nnmPxS15%Fh_j}REA2fp6Zs*vY4d`Y44+2efL zY}|IbvPi=(5*NS%A<5zI@r z62(?8Zu9f;M1qe$J%+9SV9^uBw@cpD_ut^dS}U}H0s>s4M{k*(qdken+anrmo~ISh z52&;l%Y_K+)HF8m=a`tF)`MLHc*$M|xy=Y|?rIfLuxpRJ3T=2U5Bn>&ZI>Tr$Maeo zo^gXdoAz&v?)9%|&UIzQbgtH3;Rc0g(Ps_AiB^wht&C(VP{&N7&igF)%i3AsNu8J$ z^bcY*jmc#@#b`OXoB@nb{9Ri}U`6B14svAaS#fdk77S_wp%0(pHXkR5Tu2t_h0h^w znaOHH|I-sZ{yRNH2WqkTy65bE-^}PP#7?GcyiMJ5EMEWz5^TS*p65a{+IaP1emFgH zUF|AER|>9>TAS>O;p*TEt>F#Ms$(_s&ls2ATK8+KNEQ2LC1vW*q?dWpgHI%dSrM-5 zUIuRQ6T01qP}?Zubkcoz1A}b88Xruvt2LXt+B_0K;FNN&*um@Mo6otj7Zl#99oPCG z%DUFPWZ#IJ)|% zwmf^xp-9WFQ9B@4$ug)H4+MP7TzBIZ_7!di65X}WK~8^w2aZjK+;(3fV{L}xe=Dvn zgK044nEaHssuBQe`VzD?dvQIb$IqP&&J@kzla;w>4oZ8K0bn(ddPpGvYn|-1Z&{q> zu-@GNa{5ea)t!*B>0Z|RW`wwp$F1JOH$P;jIcjIy;^5VViW0sXdRcQ~6Td$7aV`o+ z!s;zlR&48`X7NplzGTy8`=g_L*+X-XM%)N&qX`=T#_s~eyW~g+vc;IS9Y^Ziaf_nP@U4NXj|sn2T>Sxi@$y^GcL$2?Bw5~k-$FwX|XPsT^U<|6VdzRpK}diT0Seyu#$VBZQ8!p zkCWj>x-uTt$vm3em*35*KwLon&cz$$m!*WNY2{dg3mKOI)9HOu`r9w;pi3V+Fj;r= zXU1BIAN~+)S+QzinwQA{fQ3G`J7OuU`bUIJ)uhb4N}I>MM_+7-HZ$aGCj)$_WSOGu zAQuPhkbphLBJ3{UPp@F1mvE193zjUkaHAQjI39y6bIOPNOXBF@oDNZW?-NS~ysh#J^uZsgizH()7> z5gm1)VbNSta#{_GbQy{Pl17n`86CTIj{=blNpvAa^yI3^=I^{ zhU%YABpTc;@zjSf`*?PhUZdppdN}^8cH5D&6&_PkX07D4|1pLJV8$~0azmz*UeS}) zyCLp=oHO!ztEgaEl)Zh{_T%2}^@`dp$c>-;SIgPx(=II<2`M6?`?Jh*qu*JD`a3H) z+YjPH=z$6)B7ixqW?iu`5lJFlzRKyfM3E8u%Bzz$-@}M~cfZH9^^&#pOWmxAw7psH z5A`2<*rZ~!?YPboYrc%M{`|=!+%YiHhrf*)G$cD?83s9BKV$;F-|V&in(LMNysZu(gAaf=@SPope`i{P{=m_!`OP zJF-7fKY}#KLQZF+IE$pA@MyGaoWu)L;q|_(uV*x<6o%J9W711#C8%x`B~?D~mOsgP zScK13eJnk#o0o9(9+ietBRpMkFPI9Cus6sbM3Equu<}bBAvZm3Km0_3GOrNyZFS18+ulDoMBXN==1eFZ9R?YOx7H4vlYkwuzCJY5GH>+>!i-N z%2Fqq{PuE>{)4*u&)<4AikC+x0+a{RjXsEODBnq;1I+7Aw6Q8AhnZMBc-XS{%RN8y zx8c<641#Zei(zZ)WLiGYPS=)-usPKqly8SA6#Cf7qbwq?E?9e2zQx+K@8Dj*OHYX* zX$D$^oNgIM1yBWv(&VX+SMwwF>2N|!aC{3qK^W)m)7J9rL|iuD12vCbLQ$&JF&NZc zvvx1G9dPF3fDVCLr3$u#=7+M5H8bt&$z7+`qmQ-R*H)^1HnqSZ$?vmEOOUoAp$m7F zcrZ}QYgRU7K=GdLY|RbDPkqjfc&K`gym`W)pR=SD=!y2 za)rE#QWVSluFCp9ai7(2ZRo#jHXZ5-aXQ^P)+Pfxt{D4MD53vI&yyN7LKmk~Sa*2p zep1%p;qejQ3j`!2jO^s=plHZ(CRp12HX;xlR}bEQ`gs0bB|?2)*S+8tTw=CHY`5yg0X`BI zbT%i4+K8#?d%X_Bd);9nX5M`o?RR+ML*@gy$-WC!+<3!l zZU<=`@#}P4Og3M#&V$+SPt%=E7kwoiT;QZPF$1SxaRL{xa?Ub7JyJ<3x3(yFfw*vw zJ=Z^Ah~tlX6#Z_FU|qx?Nwr`o42|&aZtFQITepz0pqxSukTUH~8%MDpE~Sfg^Bby% z@$7S+N`@f77VU3oiV+LW6L{Tfxsf8{D*c4ol&);m)LD)ci$jr?pF%Vct>;xG1D;%O z--ynagD#5BPkDT1j3jt+BkL;3jnpIkz$M)qtx|@@XmTMN=S464Fl@%VQ;<%hIDP1N z&|W@NbZ)jDWyLg9QkwilPiEEJVX9V3nNO)GdJ4Hl;umd^d+jKmK#-U^o|E+D)bG8o zFqHMJ+$&*gx+6t5eotG!bvPM}TgO(_wU)5#!Q<$HIwQr`eh*XP1t-WB1@+MJ@)DA& ze0h&QLc>$h;qS?O^jXu`U%^MJ!w#I7;LxXNLrEHgX!RhED_~rj?`=eYimgvvqcsB; z;p(AjpbA7Wq^SMW;!gQA!`~_=^E_`SR{{-^_Xvqbx92KN5fid`U}rR=5Tn?B?xvMz z$ZJ1uYcl?dY{8PG^QyNg)9p!`P-H!XJ>_dAF}SoJzV5-7c0S_=?(4bJEtOtzP6Frb zc{E>fV&>Dey0!$|q_5HlGNw1yej0GWuvhFLE00+iSQn_j@ZN9X6T;8k(A ziq3NEeSew`KG0u1c(|64zr}`jf}EW}xlm0# zlQi5kDlF4jrbf#C*sPNUJ|V7b_scFZy*YrIRbQ3Pcg@dyKcK}n4 z4^|u=nc)MxJ!3US>T3 zM>8TIwA?N*dl;NJy_kuRyAXRPGhwXb3n1&Sv3n_#R!-hyD%k9-v+7tjCefU_<;fi_ zUYFv@gN55XmFh<_^c9>?i?BG&R-j7WH7(QkbzQNIH;hI^$y2RD@(@jF3;wT|z9O;+ z%g0TgaWVAs`9XG$jksWK>k9ar$oxI{>cU)E(ET$O1&-1$JqrO-NK&ph>t89! zIgi+|c=w#*6@(IuTNS&kg$1DO*TAc(^~xOr4pI`3iQ|Uf;|m9d@CBB?5V zY%x@L?YQzw4v!|w@A+Ep*xishyR45^GO>OBfOa58{b@@3hVInWTBls%2G{Ow*g0N1 z_f_ZQOa>M&z`$5fK#z%gE7!9SHr`B)MkxjLtQ;CJbaS6EkBgoXh@iNO_>uZXB?^`` zD5H3HS=hJi5FhaAP`SupIwHj|c(CbqFE^)+XA$~52f?vFENLiSC0X7B7P=-_fjdb> zCL_hKohP>C6J3^(y){!sObI{uT~XyU>P2(9Lq7gBb?TdlRMHsA9}Tv%DXChB5&Ays zmc19Y+BkGYDy7D?i@IZhJF0Yr+@vu^Rv9H)+VyM;dA1EoYsZk8v*~~O1@f;2V!xeU z1u{|R%7eIT7QtaeqnEC%)B3VPoYw+3r&pS8@znm^i@tYQ6b(cyPNrYyb~XCa7eTzO zqKHT7Rby99dl#j&T-o$*Nqshiuoi9Q1Dq~qLqIzDSOIE#oUz!?=z=PA^Gx`6q|zRw zHtu+irfg5QvQcRzA52khJueFENKAYr>qFt+yuZ?v@l!TeWX$TtH+q z-|Od+$KA<1y>gvaFJEdh<{0sgqFmf;SbxclXIEfPJ&Z`hV`xHXN_INB@U^`Kzk_a@ ziJp+28=9Fyx!?C`(bpbxhO}G0dQvQ>iYQf^eA}@=#G=-_m#|B8kQ@vTSMo$8pVLpC z+2n5rG*pxo^}0xBpk>JdDAS%AI5!4)_o5j105wIN8(8;Hg}c&1O{}D(+8Il>E9;MT zoOLFwNo)!hou@y-12QVUTp3=`AIg@YMq+0N)h6s*@ZB04g`w4#`Tm*Wcl|rsx`7I0^4hm4DYzefYZ1!H=$9YeqCn$fMm*wWGDN|+VS=8qr^zD224iO zG4{{#p&5&8EH=O0nf4ag_#4@MPEQX2>(eEYiNi;*;qm9QrY+UH{bNiwN&w=G^eQi) zheu*;jll^o9}$OfYkTxT^a+s+0(Mp8K&0kt8~P1z|I8vl1xlpNVs9Ap12VEL10`KW zKv88b+Hpe-0-O`JrtT1?Gf^rAVxdbFCtR50n!Zex1>?Yoa^Wx6;mO{HOlwRS@na9R z1h~rFfaQ_QI>bB{`Ce2j%1$T>q!P8|d^@e!+Tr;mOwa z6bvsVbBMtR5MOBBZKF+rf9-1gCG!S0(y*Q5jcz@4zB-T4S5n~<;%F{W^NU|ip8QKZ zbf3%nHHx!F8?@Wt@|}2uC+$Bk8s?>Q#jSyOh45MJtyx(K%eV*@sG(7Rx7`WF*N)Ui zO@2K(K_DJoTUX(^^(Tr)-~wsD*hv;CZ_gRrnTH2C4O7%i%1 zj@9pt#GNp3ulM#UnC#L%4d*8|XTS0H12NmQu2KVCrus0OQf$=S#&{c_+Hxe>f{x*f1U7gK`-=FE zZ@hSp{jIMc1(*Bp7}^%RA2h9(-I64T7c_x1e(WgE?#5kiMnfsY zoB4F2Na`>$-mVTF;Vx#Ue;z1x_Uk%=$N;4C;|EDq8jFu&C0SNq`lH8acOSY~m0L*o_L46F zC8;WK;!0Zd_0lB6T+zF&R36(^OmrgnDrKJ7W&3OU(^b39aTT1W~W26XPtko;(pVc(;!vmT$LQjDXCu?puh{$Z_sw!ZN1-_ z?q)Rap5Q3Y4=^9syqqn|VxAge4yI6af9l7js!>$Dg;&hx0)bXDDT*0Sdu3%c4hmCp z$4nj_`H+e_LFQ)D0o?RY8DKY?0zsqcbBOM8Y=I?P`DZr9%TRlet+ew<%3Yc(2y zp~CN?$QY3`H8C+UEtQ-t6l*MU)viWaup#h0k23hcin0*883zAkOca`H&9TIS%HgRr z3^T)|DV9=b`N^ad{*%e4*xnIiQ+8>1f@tvA-H6xOPMDJsIl;RiCz&Q;jNc@;i)Oh_ zcK$UlZF3zx?lv56FxMN=34t9>^qxms8%01=%KOUmtVDvCNN18v+h!KQRh?{7mt;mJ ztmr)7a$TRYwjUY7u{N@y22UFmr)CQ3Yv2j_-RZh^h@HFmce>2;t{k`=tAx2blUqmH7 z`CUl7*hkS&Ho4i3`=}epAfSRevHizR^9z|;_F|3?_#g=<3vK=MdQ+R|-N~@B1v;Z4 zuYA-qU6OQ7;Mp1m{wP^x@$2$y?ZfF%^#oyO0Ch^%$c~v=oq>_0hQaM=&YI72jW39) zQA8@@4QlU<8bY{2cEDh@M1G$11~ETDk+mX0Z4D1l#VS9fww=3YiIK#9)xiv7-z@>g2UuB8a9jH8nQs(G&{)p-mV!XL_4nh06H$l<)3r;MHY7>7m)xYXy6I; z7qcetB-l6c?Deg%Zd!+H!znLppCoOIJrhas?i^?&lBNB|P!;3q8$5u@uP0%f2)Ms} zbG_vHax*?7Tc1xjQ$4$Ms^z1h-MDs`TrdrNj3^1=@8&L?)80w*fnL_8 z?F49FAN|Iw9=1Y*L-R#4&3j+>VQfimsr+6s+Fr}vEN5v;afCvkgF75#lmZ>qYv}Pi z*|SUj1SVbfH87#d0BmY;-ndTaGxN#+R(Fxe^{UQ9+)IDKHq&ph$>%y8Jsoak!k@(2 zbsIqxL9bsI7`VD?+jwogz>TobqDvWFj$E^xxpBVicof_y*DmbIv3e@MtUdSb1}Xr* zfF`-19pzD;=9Um=WyT|rAozp!DpHNtBsY`M8O^C;W8;Q)<#{z{8Zz9iP#00uy4(8V zb2OVwKfqqK;ZDAB_3|60N{&FAx3@=)_xBXvy_hSraS&2jN(=0|I98I;`V-`9F!M(k zGxC=!_<$hY*S6e_$)%O{67)pGz2axP9K>P_VNVxw#|quKWLwrmKvKvuT!#dsy6^;10n(1a}Xv ziw1Xh3lf66dvJG$;O-8=T^Hw)yx;xBujR~ndZwqUx~JOvb)Tg4sgkq>#kTWOzHzO# zBLw?<|J7oZAk5#tziBDj1IftJrs85Tc%a}^0qdx&uAy6dhIy{w01uT!#uHTfKz+KE z>y$NtoA%mx+q1!ur_T8_D54E8XyCQPW{kVz)?-x9CxkW7avSpXG3)%R_M-UhuY${E0jcXV*O6_zfu2;F`J=*m$_mz3;wbZ0j3LpI4Cx02(E3b0E)Ir>za^gr> z!r7y;P>iryDH^;lBL=Sw^Y3E%jj9O|@}j0-l_5z3&RZ;gt}^BU0MA;C${s)HWAM6) zEs8OH>bD>D5(`}g>gnw*N1vWqeaRQg`3%Gpb2e-X?ky&~%L4hB#b|XgVc5}PfdOH| z?vbqag#Gqfl^O-{N|^oj%Fz=wqu0-YDENfohb_s)#3$H8`IrRkqYp1kX%atffA-Om z98vxKI*Ae?|Kj_;EY0Yksb%xAlf?PN%zYQn7lHkNmF~?$jKSuqRas@~q@UTy7{a7i ztJ&&2vb38m=N#VnfA~i^gk|KwBZDvfEw$%HKTVA$S0ZjFJkHVta?WWmSSX~neT0mu z$b#Nm9hD#zfk73=ywO&Xu!@|rw5SXbJ@$9siRg$Ei-Pwf0#$0&tw)0eQND*RUu6dY zzo4yDWdA*SZq?HMf0xIQr~u97VX@w)i_AuX`Xwh`*;5Yfh2{4(|3ocmQygm!kj~cU{^kIfe5=y9(?fzq1^X1@`HXU{|)y0o|7VnCVU4xv_oirB!5D-60G$3vHs$q z@0GUC(M+M<3()@)OgF2SegY?mj0$Iu3+wv(*;PP+`$M=kEX%@7#?Ilx%o$5?Tn$ET zTCWuC{r2`va{;Zd*MJk3T5OSpx_y;~lfg8@(U&BTXZekG^cY^+RXo@pzaW!x@EmM; z{dzr?0q4MhvVa|-2KA=buC!TF+8Je}+p_Dl*;>Rd_7 z%xo)H6#fI-;q<5c9De?`%`t*&$^b>#4)J$Du#43oe%$K`!csEF>G4nJxpu?5F~iHp zkY;X7fiIwi-iz@@KlrgimxYqw{c^bj#fKE9d^nP{1@XJO0_JlE@UnCLs=6ngg`XnT zVR1aDrfz6z1h=`BO5F}-wEo6s{C}wd6>j;vhB;>a2@DDx1>UkZaZA5#Rx^k8Smj;7 zxgIYXmLLf(e`*k*;~Tsf9LZ^BtPOvI*X7~modz}+ee~-{LNi!fB{(s3Tnf77()wwJ zC9DhgQv~ybLu+?y@E@m(pSFD0Q8*%Suz;>JO5ePtcBo>tKVoBeWzF@99!&1hPK4Y6 zVRu4JD?Y2xfRG{F@dR4T{a|7-P?3He+?Su%#*;`+IT5*wPdEG%w>pgrhig-_KiXL+ z-4|_jT7XItA7g=(VP5EhfRKe>LD8FMAx9OCnQP0owGl?$M8k@xj(y@{CxaIl6sk)B z25%wfg{Xi47QRGo$BageJ%^pM@+9{+Pd7!(Y~?9DUEHKfXeLI5UPo~u7z3*ENKOKT zdE*XGm_F~R^V@`J&Z58Bl$c$e_CI8yZ1XB@1S#BxgMI#O4ao?w3=D&32*b0J$|96e z;P|SzEfr^3i=%NK0fYR5jAcgdD?kir(%1bvHQOEY0C-6=<>`uN?Bf9<8@_0 zEN3;09c5EhnDG2~O~N#TrTlznrqYd%9_<+}A%X~@f+ZRq8FwE{Wjg!+iMK0Kcxfsj2u7Z88>8u&LKo(28yymkQ- z3EJOIf=3{ql=OANvMOUb8#v$U3y$^q=c5wtU#_VaLT;Ive|aDGVdHLe@_s}JxSwr$ zwe7rk4L!ZMy)D@<;+v2cFQ`!*ug7YBdwxza+=>Y>hYc&DzqC!)^tNFW1Y%C_>Rp85 z$Oqe8>ie8gi0HsFrJxIS2)@6gSKd4qntR-Hc&e`-09P;+3=7!7oXFHG%4B6uhK6{C z#9FDQ=cbqL?JCHbYY=i`vP3R-!FTzc^WMAQ4v4m6wH(creSaC<=d;%GC8tF~5lxDo zfE}c2?@C%9D|iHxXrhaQd+mSQ7x(1q+3!)WxpZnSjV?o(603zkyk|m8%Sw+<^|FQr z?Nb*s7n?hmLCHb1&=UCk1j$f4sD<0$cw8*)AEWgLE=#DJPbb`5PU;3B>gj{IVfbnB^QAPf!PCvkslg$WnQ_%zK}ksNtF)QCtq)^GaDk(zIPZ$G6cf_Nct5FF*Bn=oN}>h5_PrX_`EzEZSA38wB=MI8n0!qv4F48u{`dAL z`S14M?Kadu3g$&JTAv{1nn@s%N=B>=PQ|0!ks6C^Nn)H$?#6QcgN!3Ns@{yXpHoKF zYn`Jsum}+D(kZ`ZUPXPqB$%LFHf3TjUBdF9`;Bu9GQmLw8aAXxK9D?b!O0+fqJVBX zK`wGyqYrtI#59N10XH4)gK-;#Ai@;E7p@9-bQO=+G9wubo1Hv&h+-E4?hf-pf;*lS zc4A)_-hSUbzWb>}WE)7!$2@7DK3|Ei5*6}uzi!4MFkbI` zKDpb-{ll#Y2LC8fH9w6haWjD3xPV&FQez-sdsUSaZ(H+|3k-%xeA*~9IUW5SOiV+t zt5I9~*LL3G4{R|22q*T(Idve`xJ+$t3SAbiN}Sh$8{3S0 z@7tf|1Po)3>Jn)o{$8(~-RFyC>zsad!pw|=co}com1bUeAxkdA*L*9Of@dZB%To;d z1D!`MlFTDh2;c2*NMWR5@B6w4hrWIit&SSVzZ{(O753-*gTAcGqO|ovf}y!`4rXGI z3k18Fh1MTS+i}Zsy8II`oO`BwrtdM?&Q4T+aC3=A0(62q)3O~i^R_4&MflC`sgnQ# z0)!#Hb@HXS%rjAF)}7-54bA7^TyuCbvX6D!xwuXZynJQTm-+iLhlsQsXh8<((8}&1 z45sB_!j$>Po|(+~n4&CQT6pDA>Z7pDTZDwwu*1(jKdYObKa)8g;vRM*{JtdXEp^cY zsRBMzg3n+8iG^`xvNAne!N{nP{!eQibpHV&1;GFIe@=I{%aPZj#puE|I|d$F*!?)) z(CA`x#<`Bx8K_(KBWzcfI%S8esXcq?nEFOW=^iv9;cZ$`YXM_7XNb-2CYHtm^J>?>ZZ0uLJ|zmHA29TsSt141Ug>rm9&Klc9!8t zG6+%nUJ{*MaYa6*e#cMFtMsM6VLs#bj;0NJ%cZX&>>q;^HS9mdc*os;Vc>?-M5Gm1 zaP}gTl$QD#N$1q8#R(0RI&7J~6)_e303{|y3GL!Z;>XXHZ=6RCGJf8fXq}@gp1pLb z6qOFbGI}ecUT8(cS?K^lhK1kXHN=C(((`+1rk4|m-(lzoMi<>*`fB8E&JD>di!&i# zfl z773rey+X|i_>-y?9!JNcCMBBWd%i34c92w*yaeE5)jf1C`FR{Dy+$t$zAG&o2&l_? zlG=KeD|u#SXVc3Z`Me+K>+)KX(e#MG@R>9KlS5!HQ}4t}{g64nClf6Dj!iq; z-Py2RtuE#=fBY&WCn4$%KQsRXJtJM(7^rb=ZOM|6VSHmUHn4cPSbk#s-ru#Vyo+T4 zkj#VEx4>M9+ittyI%!gyb>yntRtvRp8?DjHuGYS3(eKtyy+I)A7|c2XK)^=Nv=z;v zrz*Xy>Q;iftUFk^6*dcz$V8#|_+DWOqR6MvRe&cWlHmT})_EzyQ{j{bEj%47a+*DJ z5Czs!Ut6-B7vhfHXT)-n1Ji=lZKqlJscAbNlslxExt+UmNMSbs3Tul1{I}jF7Bi8+fq` z-D>XBtV~pTf9}bNt?gSSF;9tMQ&4r>nxkvO*J78Sbr}y<@9) z%R^eC!zLf^HmWn#Qi|jO%5fN|4BJ{dTmv%YyO4KOMN-3OB4lLy+osgiMv@f|JtUZ% z?Ic-QaWid3p>Do?opi!sg=Fd-DP_X{KWgm}Htdr}?!WPAy$mcHOiFzTURE=`$+p{A zPyN+$Y(}k2PDKuymlbN&d)pnbroYs6_hy?UJu;K8*d=wIT|d@Q zqU$Df#9_{;ln@Z`z(DV=T%r2}KK=#MHuwF%*o@29`8K$m#Ki&0;QgtS_5)NLJC5Na zJyd*@Ng+wci7SIL?+reM&xB5jv4+Qyk0{Y!-zk-=@o%VnsjjcqEnB#|Ti4x4%giWM zqklJaqM#cNf;5q8vEMz?#i`wZF=75G2vpv1VnI_|-L7RXx6RTlZ%LJuL|8}?$@3xr z)T�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%0Dk!Ts|6P%?*4^w~>#@#~any{Z&7l5? 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 z zWW1Htpi0jP+xR6w*tUcxEQ=}Pi=4^Df1%}NH^RHbZjltg>r zFSPi^7S4zU)zU1Mf4dFbB4l53QbNqjcnP?ym3y}k{&{<+6{L-mvJH6~)uM}QJ-bG8 z`YGc---yZ(!P3X2qwq2{sQU5f|Gei6lejY`+JL1ml+^xgYWZKU4_z9jp=|SgDMZah zDV3eJ?Ppue*K>Q>U!6T6<~7`3#37t~(Du?miAy}d2g4wMGnBd_2)y#OEzhMZ_12G@ zTnGY{ClkBza0jSGMD~Qx(Xp`~Zx?FRfHGdfX(+SN?6oYw*vsJ`Zj1pPo(0Nod4O+7 z&!?RbzNT*xzzLT$mge3KRDZvk)8qL=U{#eD>gwy)Uu@?WsJYw71iu9oyeojQw_K$p zGhrx@U`)ACz0PQ8*?pRZ>+Cv`Ryj~KjmYT@8MI%Y3^g?bKGEa96P;rOCL#tU|Hg7A zIIE|dB^W?Y0(0*3@GvVRR~c&FpTvik24XG101WWKL>3ViZU;g$S7Du2xlmYiJA};F zM}bBW=mk(F3LP^@=KN?hm8aalySoeMtl`YCzP{qZ0A4jyI8A!AK5}q$)Z5t!wDbwU zOuSp0Vd)QWUrDU6-)r}XRV1mFw>#T-dx35(1GaBVrTfWn916ze}@4Z7=57e^S$L>XQ)aEws7f(rdnX;-M@c-acRkXqLiGJ z)OB}Gu&JqOrC-YdTx7s|3A@r`)oAGDLbEcD1Xt64)C|R*^#(g-$(td!wz487jE9F; zUtj+iyQjOaFNymo@)1Uo~p42PJQ_+X_U|e%!=LXM>fQ)@Hr^McvMwyPnB@;B((6`=m^lCpp1`#_2Wl{xkKkxPth=tR&YuDc|S;9%*{4Ti4H+=_%)QRxJUlb2M6Ufk>S?<@HJ z5%##?e#ElO+S-rt9sUvmg9CNHgMbGa*a>jfJ@v}p>EalGJ0%$P$rIB-GTs+2tlEQ! zfMxm^Pzp&%&MOUX*C3>VPYP; z+Bfm5tE=GH01Fw;`3+LM^*CM)4X}Kb9i`I@jQnLbPOCGdy%wMUxPPhzc9*9`*1!lK zP8Vl*6NZT{m*^A6j$$#vIyJ=%LPUSOCks_d-lh%2_#2mblew zSuvaB2s#`5`V|7=9AG}yC4eNo!sO%)Xz8(J+YR_t3n|IT5g$CbI6a+eaBoPTVUmrw zK6+MdyK(;WaX4_>0G6T^_h5IID#HrQ@9O+$k(Ztx^oTNMjjOG#b)SCsD6s^e(JD1n z`4cIm+C0B;_MPLOb$F@?0l`~bQgUOm0v3R0VcFOG^XH=W+qX2JchHGYg*jgj%>ClH zV@xZ#aKLc6Hy%`YCM)KR#w0IJ+Z%us?I&;-3D}HFt(Q1vW@Z5IJa?j04r?OL;r>2Z zDhN-Ui5;kW@uGdGr}^$PSz!TqY|{?aa7RIJv66m%u1~AKe9+P|rCb#;TL*(VK*NCV zj7M*;_3t0l;a+1%1-utrRDvsk5C2jEbBw8mwczwHG+b8e_o|YR?0|FhOjco-3*s~n z`rY*O0PQ?gX*Fbs3FPx9ZhOc?J>UNNNS?3nqkWdjs;aZIvqUZ%yTSBM9{cOHwKbIn zw=3Xt(_s3^rir4F;~Tt8bOOaA=?)#4{E zAXsmO-+&}uYIhtjSIo)qv!CYr@GJgq)aoQ~G_zX9V*v+lg6JDPzYQ2SuH z7Yqte(wBkB!>$;#mwW8NJtQI|%pWomI9p^nAV)GT%j}qWce(Agx@w~3der-vK>`^C zB`{zrG&))fstIUfIc8nr6B35MH>Uwl2Xy1?!*xDeiU#gJ-7NM1KLZY$3WG?9A4=Y0 zO$!c*`FAjvVY8(^dLD8S)#`1AAYO0xT(>#GZ(;qW-N*MKer zMD(Pjq&lsB!BGqqE?++%B|g=KeOfxW*m2(lI=nafm{guMI?d>af+ie{DES{gKJ-LG%tv~#$8xJK3dS0aJ6=9{H>N^L?hq9&DFq`st`3=4AFlkv z05lw23|uDdyE=E#bR<5cKX1c*6mVZVAk?!{fcfjIw{7pyw*`l|LLcg&!ewF?cj-ite72`v5^0z_v-_D=VNFMn_akY1{sYn z3;mAvH86TC9yuiwg$ z<>h|6Mo zNie?+w(pT_gn4`#BH2PVFBGn`Rm_lb;V3E^5WYmehj@?fHIeOD-sB$(;^Hus+K_SGNoscPSq)HF<~HE7pRi zgKRVFqoSGNX%IfC7HImoE+FW71<~?;pD!ovNAl0&EQ;duFF-(Z-vH6z_@fES3!2FT|*n&SSl|XfEXA}$HqoOT&_jtX}i_&#{ zJL*Jeq6vE$%;g&{M}^xS<#bo+*~={d-Wr{N9!mY*6}R5z4}zLn8jPM;63*04VWYg3 zY^YvzLMEaCn|5K?2 z!LA{Ni&8XdiW}I-v?I6sZ`Ydr!NXXU;F3W|>H9CeqNl{g%-VMsMh&zcOF|>QoH9H z^p%7dM~^Np-vpcagcvd_DKhK%78J;eFghCLAb|+8YxW8vOh;QBSqcN&^-cYXzGT~`d$Q|6`+4zz9ZBhz+VmWI zQ-i;bMyr9Yf^{i7O_O}R+&lKJM{ZsR@}Kz%D;ic+78IFYoJ7^JSQ*RpLG_P_8($uy ziuo(vS8|y%UiZ?&pm={2g|%VkYoJn~bXN`gNqjlYsM^M7T9(AHGGYz2zB24>#R1w6 zDg$?nn+#`jD}t^K9(Z&G;yh8wcrB!YPEL<%?pxUs+fpSA?hJbLgX+xCA1#tw1#OLA z+uBlfH4G9Qn&>%K$gqf2yLm>aW2XiWLyXkqm2)}@!R|Y+kj0fR>GBgo`l>ppxyGoh zArvQmYNGp@Mb(P^T+u#OeImt&QQ_(wd^A=`l>45cIf@YiJ_0G#jkuI=d)VJk@cvq{ z_vB`%(!6x=Z`mIck%jb;`#ug3_GN)|4krcUj_&kS`A0KotR$%Mp;kB{4a?d$RTF*4 zL`d+INs`acALc=gKEX5zWyu@0H%&!O)TZOz)P?=Qq-DzMyKI-wS7IPiX8a6(M))1z zh}-!%xqvNL#JAEAFEs93F=Fubf=pC68LCIGNS>sG05-9msz6tjp4!l9EJidZOGVKW zFGGXY#*Ypc(#zA zrI)U~;R>%Li)3{X%74GuO6tCA8`(uCATVavuZ6XaAE*>YHiU__B22~*95RW)qMT3) z%T6=*uO_-S35vb@gH|iBIJUu=lte?`T_Z$kDNpIl5EK6^-*)F6L`ETmSB{VhZZBgz z!Z6+$THfo$sE>3jh$aF4$-(3sf;mgV=HKOu!^lv-SMf$I3y10A=kG(BCp-yPzamF~i2N`Jvol+2z^J{KOful~n zDpfd$__Rui6+3SZtMHTT^RTHY-u(j`!s+lXqPp5kGVutqzACDQu`XzVB7yZcHwa{c zV}ej0xv~6yswjUo_ML6Id%UVhl@B(CgSOE;y!KwFiP0F<(xBq|2Po6^3OhO|9bQr{ zMA?>)X4$-RKsqRw17r(Df9PYAqx?{`d(DJ>ZO|nQ3^UL#)D7PlT|G%{@$KwyL=I9l zrxqtqxKqyCCH{bbiDKo^Fd1WuRZKiN^_?qPb6XW`Gin1EJB88 zX=?dDSJRw%S*8P35HNI zak8DzPG=l4-<3`d5AFQ9Eu^Xfn>-%oP?p07TmUl}I!TM;!)eu4rpl05Q_&a$eB=9q zG7sWV7uuVPwp7;|s!wz-0??$c2q(gP5g8iGA=L zLSz%8{K2Kne z6IS^zqetJTu%s6cdlgU#dH{ZX{i5Q>9(Pt#@$_(aYz3FR8-hnK9e?0SXge$`Mg)JFj@$@f8Y=z>32X2jSNI?`B2AfN-l&{5$DVqUpD_+eY=^gG`n&qRGw1z|yW4~LN*1NI>`(o~H~#8#er|V; zj9}3-X!u?IGjxI2d;#2KN2(VaZuJ9)j{|*fAMeXZG4i3T6hmQm#ZJ1a(s(65)cfDD z|K$D*Es;upjY%G%d?ih`boamzZ*G9Mu}(5Y3^C~ixioKFOql@xf^IRt^df349E}T7 zQ2gEBG>}$KKRlAT%ac#t=!%?6C5pM71WEu5{alk8rF^3hkguRm*y#+Z~^0JAG`fE+b1Z zlwHhk@Q|3u`z^&RD&vv}5|f|HK>cY z8S>QU;hp1@;l<|q%NU&-DvNq_w`cU`Qt~+0U$@ge-MMGI7}RO@!~~h90-<0%*sZs_ zth2YL)@x5i480^(?ntRY7o?W|p6=Usx)=8MWxx-N=ODex17&MgYLm0GQ%ZX+f|{Lk zWo7g<`b}XC&1e+0MMOD?51BqK9mXpFl|G6$V|I{%woJ>f}bm0&zdE;)jRJC!`Ai9h+R; zWcczTMpmYZ8Z-B;OR{Gt122d?<3P9_R$DGeT*$=b{Fnc*%qDLC`OdR@GVs-3=}p*h z-6RG>?QZGoXMIU^jP^6=UQN#}K&<6A=*NE0k;W$?dw6toHg~1+;>8Fk@dDj5BjcjB ze>E4=`NoG4rbT>wHniW`Lf9Nxn$W$*r95aew7B!~xu|f9j|~K3-o=F$P9l7BFK?fY zy53V(&9hz!64_o1cOEA!t$W!8^I{GZU4>94<1relZ%wc{oov%Q{CVN8-SY|pIF+D)m@F!_VdAvVV`wC+Wq`^i^3vLE=RvN;l;yDo!IM%#fEF{-&s2yL_Bu= zF?*>CL7l`sT81f#$Zy*ywj1_F_jvNs!h05qP1nN3PTCW>vvR-}UC4W8xR?c;*{^U%iforTW2?9H77q5iEtN_V-%j!HbH+S)y+Sv zxdTsy-WkMFhm1VU12Kem*!2zbb`trG_q%BuW2oL*JFWiOE1dQQ5u(j$`~Q!%w+ySY z?bb%8Qc5>S3ew#j(jC$bO1E@}C?zEzCEX?64WfW_cS_f!yY{7z&-&i=?Y)m<{a76Q zxaXYryyunU8si-27{lP?=*ZJ_Hiy?qm_zuzD4AB<+wf?bkLUalsnwJaZT*3O|GqW! z>FZ#qztCEY2;BBD;|>{?+f0?-iGTj|2^)WzS{NIB8L$-Ac2|iBp$N?@o?MJJxnm~ zkq7YPjE8JO7v#1UigycRUxD1sGZn^B^Y=ZOJ%joPVOj3W?a?AI04{E-@M|MMKqF@M zvF#EoNk)4%+Px8M&Q2i4q(95NnU)4+U-F4#?c$aCFX`rdrNu~-p=|<;3Fo-C<~Q|XZtv?Ew}E8NBwnY+ z;HI0xu~+QoEzBR^Bn%Q8dOKu^qBH47`r0qXd}V04dKO9a(fg~h;VW#A(haZQuSwG4 zt*jMr=~ZE3#mdc;rKc+m5RjqWzTNAR`WZht)#K3^y%-W5!as)Rll5D}`n&KO2X=rr zEWFkAKP6nX`D_sKK+#Rn6?>W#+%1Ay~KAqJy&;n#t*`! zpHFuK|F6lX7#bL$ze%ik!w&cHQ@Ppt>`P3P`A@o~3hl1*>fg2`C)NbIY3|}fx9=sa&_$bPYL>4BDqC3?t_zYUBg+S zqsD~%jd+Um{h3JQfAwo#{Um;Xa4Ad)KvNDe_s^y5(uu&C)6Bz4-t|oEKkoZ+a^N@Z z47T&;Hk#wwd9N)hJ6vfXYfM5OI?##!W6iEHCC-tUA?hJ*s@?M8{^zOkLJG>(u)iV% z7e@$H_5ZK$H1}>8OQLdwykP_HHh)~tqIs1BZhD#Gq-b*8s!-YcSMm8HU>D$azbC4m z{XU;x)xq>**JCQiTWj`ML7)ECjsB6r|6FMn{%OY3Rp9KJ#rc&&5=a7ckmnG9)T<@a z{kuATuM_CUvzlGsmbYZH9@oG>a9$X@Ur~$E*|+$`$*yMzw`$C0ii{@VWr?Fy8?K|T z@YFQ*am;^AFbjU~qE_a~t3sE0C6S4)hFjtPID>)0IM3-c*D3#we#N9A@;^5>-cWZq z!F;u)&z})>?fs32lzzYde~no{194aUe=Ila?8TQJ{~V`3JtAM};ic|P-~ZOGAkzwc zxC+o8)KrwYJdH0Pu9Pw=GlWQV+<~HjH`r>nDR=`VFaB#^{I)@qefAp_fR~?N$xm5o zLDA)RJFQvun?y}oBYyy#=Kt)X-&Y1RyS2f^XmoYoG)bkG3>6%0{Qmqbq&?5wg9_>G z*&huCqDE#cg=RPT^I0yTnqWnd8ekW=RCSy|Aciw zAT~vMOECri7)OiwFSQ4m-gR=EIN7fpEV2!^6qZT3xW#2R{oR?RPc=z-IGJmH{q%ff zC^WLXRvxV3W_zl$OQ3Ik8PBHB{vQGlwAp#Z0zpf4Ih+GA)2YBMO2>EqeamOj8O_kQ zUHF^-=loG8k0fMYTibT}J*v&rAtC4CTts2GK+#PJQKWT7JcFurmWh?;OTL4)1$_ZsofBJ1Zf?-#CI;P%Dl0t+Nzl5AlMw0CUH~QWs&1 zFX&pxV4?W4SrWX1lFe-9Ww_YUvxcnH$v;WW0OMNy{S7vcWFI6d8YKW%Si|k0J)Y2u z%cIJ6T37*#VjX|VP~l7UflmIws}se=8RF!4dPgYiRM+!=4{vflu2O@WFz@Ymon8SU z7DE;Y_1EoG0%{)tBp@j?`@E!AuHm+;B9 zr-wUlQeRqQ`;NHRK;^e1K?-vz9 zAVkLJvK=v$zlGLiN+j1W5SwC}4*iD&VOl(O|7(j0Qcu;7jr}WEgX3s|^LT{w!VN58 zl8n2AK17w1h#TWE)F0*~9gKfMG=E2Qs+}bTpmwCKCd2l5pt#jpHL@)@Jl|=(f9u_~ z7h@MLZk* zsx2mU@wgrCuR|W(1(wCX_u{JGMgG$(WtkGNRI{5P%U{a>aozu4cm8F^_{%z`zp5C|Qm_r)Xs2mq}7{Ut4)kuU!hkTE8k2dePi0fN*+{Ic8}-kixp2%MFiA zK#1ewFxxkT@v6!!qD}Xzvva>uc)s+efR!12mV&EGP2L9_8 zX2{mBT>3|ke8tC`wJ*icYA#a7K)&W1pKA5 zO$X003MYy-sUl}ujkz(_*Ea5N`z8&1ByFN@ovtb`un_~B6cf_YM8vPI;Im%=ve?`C z*MBC?Ut#Oal3IzR$z!$1Abn%26;e+?-Kg{kMG-jE= z=yPB*{=;_86?0_%0=Mrh-S>V!7mpeq0l}fq>^pvFmc`KDb*nqJJ2t9SR1ZMN9-(GIpW zLfHh=f^MKc*v!>dxs@?-+b;nBJOA6|o{V=bQ(qUR+YF=L2S@5Mv(hjLG_Qa+ZGGMU z%8Bl1FQoZ1mSqUQYd|S$q@XF%dIvWlc8E@p@iEd`A?BW}*&jtd^ z`ladk{RkH*#+$H^8f_U}8c+ zO}&64@jjbh1tW$+CadD~5vH-cG7bIeR64*p{M(h8%l!T95lhLxY5;LpnF8rg*l-I1~GzC1T!O;P~@G++TbBZ$_JKQn_-I596pZQS>ry+n1BJ*d*OjMSc z^7NPcHus0<13uIO?wG%S4A^9^*PA#CCZ@-0T*c zjqg7aK_GLyPUoU;-c(Y^{O(20e3UAkS|k+ivpjah{XH4DnpRR&geFzt0dw895l>`2 zPr2Z&lvp`wi;ylbcb%Cb8L~8Y^!AS+BpJGOP@YC4h%)8c=4SWzc^7A9*`R;C)}UqW zkN*e-8)s%B5b=Cw!3~ywXN1u zPY{UUA!_h|t~p(4IS{X4-D-HEz1npaO2fmmwObZ%yU^^~m&Xav9fr}??-%jqI-HYV z?@xb!|8`}!(WP_G2N|+FFBw7PEo9}_hr4nd1wtcqT6SWMrS30}g3CZOm2%FJp;ui> z)a=7t{J}b@*Ts&bkk!Lg!JQcVMU8bnJxCVQNXmQcrZ)bw##++@5Q3vMitnIJWq?ZV zdwqIze;3wjmzhZ>{Ls8IT_KjQNSw@jLVV@4Y8>S<4R|jj*$ChfJSIIEm~Pm(xVTtO zEfQfH$5ACZHMb{2J=s!d(8W8c&LD{gVpDnz2A{JD%MDUPFI!NSgL*KP{|u1udT%(r zb_xLp_+oP~)wGnQ3%u`gyf)(4Mh?mKWlIA2y1qZn4ztI^81Wq|D|WM4D!1A80(p@f zD=we+*?~!uppENY@7wS@TkPi4~&}3A8Txvw}*MV|haM zEi&TWQeMsn9BV)GZ&2{>ly`Akyj;k#Xk*g{2M4#u+}1vXjxAodSxyvdIt@R1eI|TcdD+P;&T*1D1(K>w`Yk7h4z(OlCXa!nTwl3qk6PC6B-pv_ z=H~&{h69qojT~RO-Yd~Uco^N>7C!$RYCgK|oBV+$oTv_P#BUOPpfXB4b2{@A31Kw3 z7`1%GlHpHAe$nU&`esUldjJjW=v*70&2&y?9#mbcEWE%QZnzSqqijZ69Q;le4Z$&r zcr{p0W%9}5lr43zaB$;=s@7g>cI14{TlJ!uHj~vAFQO13DP2fIQKaW<{w-c;U+6nJ zL;zPYBz(Tb(rL|p?*0kNQ;>|_F1mzC|KmoA@Vz^K*R(M)39sKuq>v2 z;LU}theIzcxz%j-2-t&t@vUx$?X?Y6OXWijyis7?13KXK^%YcP@j97dj(|H`ugoZa$C?1I6o{Vrs!?p-?+)hcg}C$G zbwqf0xIe^?ywQce(OPqC*y3jl8X$os#Ke$4Oar1LC<+IvE{R+{%~$lPk?y9FclTWF zN(Xn?mjcf3W`4TwPp~8U`gEII7Yt7AFG&gIM z-kJ@irGhXt@K|%O^+RZc?gzoz0%d#Sz-kdFn8MQQ6!u{QS#;XL+%;G_UK5EyC0t3mNCO4fQ5gl( z)AgNFikQIJH)fdJY`Tj&*wA5wE^$<)w73N1dAi?B{Fit|@e@1--3BYcxYI=yK^_`f z+8OUrW-_u0owph9QJ+9k`pT014i=rJM~tZl;+B`6lgrBWJf`(?=r_5Elp3`q$wl`b~q)OCZb0LR)&J z=b0Ors>`cQ(lEEZ{W^1?TW3mZ4zzp>Ya+zNRPio8+vP?kJg300teFL+a1Ul{bW1M| zmbl5tGMzS|Krd_YZ57ZHM{WF zx-DT`T?H*KTWv&fhlc*tDM`);LD&Dx?dNb-uI;NMFf_>m>nmBwY5Qg7YcIBM5C5W8d-6QC;dgzF%+|Bz{EcW{tiY%E~S`D1XABOOp3lo5dfBU z*}E+8E)G~*!@i33Il92rFj;3YjtR;A26-v&yxO%77^5*HE8^e#wONDF(k(#MhE#qx z4uXj8sN}A9pqU8z4;9-F9$ELeO-z*&7#pU z9PYr!l86og!IuN{k3$N~he)iaN(uPSlM)m0o%(>HC1f|BNS#i^WDiQ|?1QS48Xe%@&Zjbx&KiHV#6!dKoQy0w z1R(y%h-?F1aBxWy?i>W+`ar{of7wzq>!cMvzrT2JDp3FfL{lU z;2E&6m&p%UgxL7#>7PuN-b4IlU$C-nlTsHm7I*8S*q z(1kOzQ!|w}kS^@D+)j494kMm3VFhH?M6GQyplB{M;n^>^e-FtA++io^e3JDPQ*cm_ z-NLnXx=;&rAxzlkS{{&U2d{P}tgS)!(9vJ!Hac~W7B4UFI5gcrdhzgtZbH5o={tk{n>K)!3vJs3=#QssCN_u za#NyK?!mLeDF@_+Z2~!@;IaKsS$XQ13yP&WZ8jg>B8|Bn@EX}|tgIN60H4+Vj8}%? z65z8#t!k_eGg_N2NSV}1Q1w4j1 zzjJwe65m6>DcZBspGgr{&(U$VKU1{{Rw4)=`4mE>m{RWJMX`B%vGfAO?*y^p5)h1L zOI`Hz5R0po=zK~~C&d6kmMS1!7r)Dn>siryi|4?XgI~XX1)&gH3ZA1PA&ptp*f$?( zXlVh4H{n#-(k0L~?rzY)QQp(jBm9NielkPEhkR$Q&P`sv9~AA8I{*<8Qb7Ic9~)E6 z9tkJr_XM$mlw3C}!KC@}9v$7?n;?*C;|=)9);m|gL@ypnBsh} zPz|`%*t$TR!(x`H>Xk00Br<8U{gTL_$8YiUYSzhInYP+~iHkhT$IcGA+~Kprtsk~oLoeHMuC0;zb#GnM<(1TSW5cEzjRj}U-< zdwJNId(eDBg`wYKMG)+FK9hiedbP z{rWDm6(UPyGK2D;p2en*Ol5zf3Jb=w%F>1lYJQuZ49vp$BAvD1vYz0dps)-6oC-(y zf6d$ffpPfv*8f$TQ=-8f5g$uhc{Ypw&*P0$5d&p^#Q^UJ<)``_5W!@2&`Zd7b^eBd0PdcMKi_(J)!XeQzlWMCk-PduTkNd>8j5pfbY z^eor9ZV~tX2?--3#VFyBg-vPNF^B_ck{HjDbF7xx*ax8jh#wb7Nz@NOWS|UC_vN*QG{$M$NwkH zm17~mi$k!CDV`uwzJPln`MvLk;_vtw!2r(GcYLgn3Pc8)48(qHbk^UYbBXOd4!>{` z)BoND(M6XAOp0R;5efJ`oSRm$v1(ZAH;Gh#KYmMvllJ11x=mh&(NrSkzi%?)T$xGy z`$GNy&&dCK!~g0$!u<^z{9lG70!RNvz{^Wk=??%9au`M8+n5IsZ3*B9{@auPglYbD zv_WJrx$zrl90o&iywuPWbp~KHZZRNuAN~bNdbtk?-5jBm8!|D4-$L;*Hgl9&xKXSp zzN_Zr;8P)GApVP?sJTeKP^!1Tyv7r|JA4OZWjL#1^Rh^Ma&}aM3lsN)MqvuOwq_0D@`}}Pi7B5&bnWehSFe4UL(Q2)9o#yqPYC9hR9Wsl zyg~WXfA!gt>07TP+gZU$-4YGQ!Dl}cNLa|nN>F$mB;{lH7Mt*#RyLxD=VVEsk^5}^ zw*R^eiZ~)UeM*T{; zAtdh^_}xBcqrUg#-9e)(ify{$B4U@P_+=j!%m(k{F>|)i90A%D@ve<3OF_Qt25SdZ zv9}HZ+5C_P(OeZp54+1rQenQ!%`b*i%$mgjtBXD%n64YUCoZnG`{HOj!O(nvAnZr5 zSYI09sm_#3BpPOha$)-b4UM$jpQlV6+`mW{PUZ9XoO+@AJ#odySEoswgmiZ4*Hzho zuXUxE>%I;4<1(o67ga3$bdKR=Vr&-6#NJz%R}(&!aAtEJrLMNg2M@-k`#NN%mU{WF zTknK5YP{-`6q-zGO*&3F4F~Gxux78edYPIXTFQiz?ljkW7v@@@3FVj{!PmMQk8IDM z+da4%wJAQV%Y5^8+&pi&*v|>4ZmPr`P(#_JC!El%wJIf8cwgp8b{1iG^r-O`VPXZL zXKBcMWiR1j`gZ+ZSc5U~3=^YJy+}D!rs!WTKmt|7bmP(Ld%w~vmj$-m<44UNLZ1xn zyIYeq9e(&JrF@LJw2+v!Quu}J=aA(E<#4&ay`9$Rk|$Srd!KcDPgho*@OX7F4^*T9EwFi~wwQpbETK7W=jsb=2F^xm{*-kXh{n_o2K)KetsD-zuczBw<$%GFHZFzodmn%(Uo*U=a z85)w2vqQR8D!R`_Fz%IMQa8Q`X&#PlJP2^wt!OONj+qj4X@X1(v^Ob*vW05J``jJC z_2Qy4wqHpK(%_$Aq3+GijH%Y02itB)?Rz_$=8QFzTrNj^{(KtMszoiMa(b5d%l3Yi zP%acZV0opKzGdLr9`Bii<(`v$+@;0tPEuH~X~E-h`5%e0MvvK3m7;kxStE3|PU=s^ z?>@*7KDNupZK3si1_yt6S9_FQa56H~B9Q-PNI7f9zGb*t=8ki>jv_zD)O-_jWIEbM zN_ax){xD;#;aP;Kj=w73?ot<`(`j;pu|UBC_PrEXApIY`FB!5t#^8;mPOT@hDik+e zxtWfJ1cRq+Z}okH$Y2`pmx{5>nyXBf-eFB9f8Z{yyYBNK^{KGiN1O9CXyW*);j$|= z#$MTY?p51k&{);~bcd);YL>oPz=>J^xX~$}HMVOv)*Nwmq^O!qN9ICHDoLmfZG7 z5kDQ}Si{qRc4wW}qdf!|QJ0<`YONg!G{@t}omtFw`faKD`FXC#TVMLx!W!HamgwfQ zWd!xl+K*GS-pl43-&JaG>ZiM(E=P~a2^p+zMGCOI<`fG;-D`EZF{BhKQlsmN+-f7Y zO-=PO^;;Jl#mZ+EnzY@6xvJJ=0?EQajx@uSyIQY$i83fL@%>SORpk#uRh1xF+BFn# z93=a^?rd7yn#T`?zi9V=B{=9;BfJ1#SXX$(Unq5S zl)c0pF-3K5Qo*NL(C|y^;qgTB!}x}9YsIvv%TRLz%&7V!f(@!CbX}hx(}u1U>V~qv zaLA@$bu25@9)GxJUqaXn!MTFvGk4Tv=V6oyUQXbCqe}bo3B^=m#JKz-E0Hn|I+%u1UIxdS|LFmvO7(-ga@a zw({B>YB0yXIVcG7UBTSx)RbF@dBE-M@uX&na^Kb-v@dO<;N6_=-Od`4gy&)$sWd+o z%aq`?L^YN?)vq-%Gj-YDKABo~G<|EdJ2TdOx;xyiA_Lpo3KJ^pPwC! zwCjP2A0%ANB@)w>)cj4BH8VsSgQOgn+BMHR;(sQhj5{ql|)_ zdDlvAnTk*H>IU?gCJ!!;jhA0`S~!PWOJRj15}~3%)KENSQu8uJ%BG?-sAU*ex(oW% zo^YgSFsmb{DiU&j9jZybXmX9j=Pr7P+P(K?tZ{R3h4s&h`H08V7;+#i@4#?L@mlMZw$t7?&YWpY*BM9jlWLubPIr~e940=NBNlo4;1TEb^~Op^mCTG% zWBg%&Zb{R21bX>l?e5@O?Q++za*}0M16PIO23)6`mel$v7 zq4~q-UVOx37NH0~D}NnJ3Pt$}YZHugE%rsE#!Y=nEen6L;%2w`9G8WFAoP^z$AWmF zj)Hj5bRC+@@`=UgikBs1y~X2G2b1eK#R7WM3t6xtCHrY}&reOXp3$f&JqB2zGJy!yrn@qYgxa*$_d6mHS8qI=9=*@1>4A;wTLMoP$%FL%v9uW0fToU?}yQ~l_*-0Hm z%I0y44w07bb+f5ofXF`q@8KI+GUb$rY+2t=^u^Ukd`&#r9X!F zmgo@8UyTLi(yx*V!u!|S-n!ad-FFU6&bKD>xQq@z%|`h60TrdF3yHO&%~p7Rx&YoE zHJYqyiFkGFTdKYN)O0}uUV1ZhN|3V#Qw$@hCDO3$cK_S@994;dfZy@2tJp`&;|;s? zlQszcJ%`IzRn)L>m%VGfa+(J=3l1OXdQz6Sy=O+dRm-U55!W53!xodL>om=of>RS? z8a5JAaqEQT@db^xGDw&P-*~0ztGg@&CQcK3DOuF;oqUGcDU$Yk<*TaU2fcP9rMPQ6 zGt&0pblFWF+NiNzJ=h`78?i85S8qZ~;dAP!L<{tgE8*;DtGRebT%5pUh((wXvU+)| zy$XMJK!y9X$a<&tfY0;%7vri;Dv?B=@RxfY3i~^gn$@idXguVMy zO(!YafN-yu!}g;>mDLZ*iwYs)qQBoTR0<<$Dlcjre4`0zPw3kSszG^+jg%-aDe`*e zzSDfbw9~)FN-Or~a=V5cuKd%(3$mwd+-2_f!NSVI#R>VlF>tC`-tJZDF9!{Z7|(p0YWJz4yagB;7jCH_sk>+3od=CN?1aD#1mdTgw8{+RuI<-vFZ zVbJX}=gQos$W97z)DOua=)f__Y-Kd;0D;)Pu!xh#z1ftQcsi5yF4e`fM!W@C)AF#H ztx@CRTXB3u?X)l8Snu(8T_@tEmEy#4=B}&b`r$xZ9954XvUY__B2flkzCq^z2Bs^v3ZJ*v7;w#EYO?GvKo+FZILr)Vq?7yKGvSyaw)WBT#?5+at`c zhnayjq122nUS<^@<=ODU*g|0E>j^m%Jf?2DMc--k{!y?(f5EAK6TLNclyJ0)&qSR} zdlEqLG=K$%7*i`kg0|rcAFWi$gr*!rzaXqY5KNS|Rwjqu>tB@#HQ3H!k?2o}QHfg$ zrgjldx4}dTS;Hhr=v&zm<&3pg5L9$oJ>;(0msoOxLJ|LN-CjN93!$&Ll-ea^&YiWIp>rsq1l^RL@A}D;i&w zBsDBs@2t+^$iY2s6;Zd-shfW{l+1O36z16(WYmX)QX4E?o>i=1#D-r53aPSXG-YdL z*jO;oFf8~itJ&xU?2}YYvQy2zB#&DPow45AZ*#keQlD@k^{YU=e}@>5@EPl*ESbgQ z8#;1l>w{%-1>Vc~yPPUKdsM>lvCf5t>aRP;HTABq-@U@Hg$lV_rC4q6ZZCYZ1shLc z!yXw0MwEO+J)W>17Sf*cnw$UyBHi#S*U<|OAGv=)eYo_dy1m_KbvNkw7=F<9nP;Jz z=h5j9>iJP^-RLI@j-sE{M3iBn37Um+eG@HwND`u5ooX|KFV-j3ho*KI0-MClEV6q@H zEf(EaXoL-7poVTAD|meryY+A;s6DaJ7O`Ai!?0tZGQyuMpo&Z>>vzvubF@HHEskMd zU-Fxl?J*zTPSlY4tX|Z1zaFZVd2wcwkJF-Tx~h-o$SxT~dc21~%* z^)yH_AX3b>CB8fJCk|hLf02%@dP!c9WYvw9cB(kx@F-Np#`aC4_fa^mCov!|ys`{z zED=7qZn;hg2-?nGfYEV`Ht*0ysKjoD``$&F(&UT;*`Z`x-E}~Vp$aGId0kbIN#1M= z5kZf_9D^7gUv|jhDz><6j)bynl7D+Itj`zr^9_kjZJV~J3cScCvW^lOIT0;TJMYO; zE7NjpblB*`n5U45iL-9SALDW+nPm4^-N>U>&TV#rPaq{ao;64)^R2rUE#i~k#$5J# z?)8{1-YaQ5`#4>F_WZ0dJ^tu>^W#mXg)I2h%40h4P`kUi>!I|u1e=Acy5g$U@2oH_ z0(!#N-l;!&^Yb=S8s3g|4p`nMz6%m%Mb>S*js<5PDv^+;CUYvQ>Doqm8$S2`i-;}` zsF<3Gt~vG5yH0aX9esz1u*s8cptVrXQAA>_1RrM%C^CFT_VBwsYFI$Dvk;f*6 z!j`)`@xm-JRnVtDu&?50R8=s_PJ{0RwW=AjUyLC#1hmMo)}*WD@^kEFX@z!wZ;{Rp zJwe3zZd}`|hlF06h*KX=R|Yz!CEshrSdoL7lFkc52JEMoxOWt`YMoi~Y|p|Ytc4iq z%dRc&6scmN&<**P#!!g>VQ#)&`rA>2 zefS)IA-J3|Tfh*Kza}@HbacW-0!J;06C)JZFTj}LRr(lla^Wxm)hW%r#VoXQhklUX z`57?x67vz`p+D|;ZvI%sHhoXZD4r|%v_)7%ex6+Z;lvW7%#e4b?Iwm$I=9J7w=m&> zguX>e}jF*Yhr@!JKw#55L>uJWnobt21V_G1NmJf~bY( znZ-hO&zgvX=!w0F|pVW9o|0GNljYE>zoY=68!1Dwqk*oe)Y}9^`3|{ zhNA9H8-Iv2WGmI7@PUoPwUHNgIrT}nL=$nrY6IKtyV$)FGEHhADL_&iROKl zv1=Aylx2KqeBNvH_iVSAudVHqXNZ(!y zYmXKyXqtq)<6|k8%t@yvl9$WD+w0qi{;vY>{Ds}RrZXUhvZ*?|tb~GNx0&eRknn`# ztxyQ=sRiSXW2TXoXnhfc2F1a_P|+M(^4*%Qc>&$PFofbnj4DJu59RU>O^>xrNUow{eJJQ{xW(JFD1mJ7GYFB%iQX6@C@ zP8Uq}w?r>wVkkuNhG{=vFQx`!klmUVJB~UL3Z<-2;<5={GEvj&L)W{6oND;mwAd~2 z(O`NQZFt;T%)||;;ZsGjA4LS6ZBguv4~k?{D*aH?Z=)`WweZjbZBs1{{lMX&7x3oZ zq^?z8T#rj3pZb7G_JC3r;Q|}&U{g=OSi6CYt+fdoP-O=8*v0{4DNdJ3?siWy&>^Ilf9S~~Bb2fpH=RJqRFa1>^=oq*u(Q##J zMUdrRas1<&ifJv-UwhNp);ER`@;wNs>DhT-df_L)gHnhjCODN4_AWnEhkq5Y#_1WL z=dwFSIHA^@8XwZRMByBvqoLESHIWo||8&($2eFy)O{A+9$xY)oW>B}78eEB9=>Wz_ zcDzjb8jk%HNK!n4fy-Z8S>!s@!45f`iblF(m)&UQ=b~f#P#J@t$mfult;OOoX+ADF zwp&L+G<`I-_EhYC`fgk_{g!9COQ?KcglT_de{sy$C^T9);1miY?1^}rT zM)03pY_Pt#{yI~hY~GzJ?Dg$T_o%N`$6f_RP(*9bJdA?E{MKddF=UV5+3uuq{a}E= zuUFpARt@HBPfl`jc6*2KewThi*&^qlRquDQ#|Vp;+oX87aWMV{O8qH(JDVUW>`v~a zm4$A+>z<$pCBN6vWGEF0kI`VWY4+(E79t(~oTB9>GD6S~k@wz>(sD5`3)DOt zM2jh1m)sZ%I8d0)o1Cqc2E5q2ad6{?v`>j~M$a5P2WN3elcvadhA$*-ZbxESH|iqO zBQs~?Zh5@sy2?PylzxVypqL$pHwP5>E38B*xeIu@8^XUYo!BAo40sY?L%0{b$|Hh~ zu_EHxl2+OEMOl}cb!b>;7a7>KEKWN#2trD9sz&&@1i$=__c$RTA1%fh`QCtGQmZ57 z#c(n?XuQ~LVYfd*<03FM8hSUj(IpPUZA?l4<>N){XmpV<&+hQXi zJ3r?d^?v9eLbcvWS^rEu7>$)smg?1rHb0A%X`@^?f%_OHxS_aeEF@esRL1Dob>%VS zc_vGhG~FwK2Xf+Ibo1?qH~g#L2NlUDt01wz)$2L>@8lxR-Q_I^O0g6k%tGo^s>sCS zo%1S23JF}isCfc|Y}*F03BeKvEvg2u1nqyK>6AdX3h4(Ue*v>yWdlN3$M6Oy@3GNw z>CtB#%7GfiVDSWGYZ>k7Jb@fNwFGvm71lT%FX!CUoES}cHw(k?5;f!MZ%pW1z|LEg zPiu;B61Is~EJs4$HYGz}%jkfFnA=vc{-TV1JdXDu(z24>_B1?{%YqGeySOkz^Ai@~ zG7ppnmm*(hSR(MpI@rDp@Q_e7UuK~isi}gAT{jgW{uVE1dEXRU8^bMZ90DiLrj=E^ zr3b4;iKi5O`cw0($kniglw>G3`D9!XE9DNYskH?(Q<4NR6uBy)qB>NL7H?1kAF<%G z42Ba>eNu6BO zoSd!ke8eZ>f*$m(&y9KLHNR3Am#tR%s7Ao(aVkog8YyGSmkA@5{o-lN_eaLVtV)`$ zDvbi1a<7y}rwgdoWd(D$#&y`*wBR5s9q0G0t~R)e=xeQ5>kJ?Vy$$Jb%r3Ow+3C&X zxHXO}ak`W-8%wnR%E~uKT=p$zc?Ofi`sx6cIC07E^QG%2TPP8*2@z9rE@-NzAK`x; z?>C<6nPz{<7<-vZQ>5}8$z^> zPA+zp408%BRIHR^O@VXyq=RprEHx4Ux_^>j7g&2#hiloKG+>E`+hf8~I$y8klC6_H z&`bLS*+OEWa?_!(u6pXR2zqd&{#v>AYumeILlhV)Hf;?9eSOtB3uunPLiXF+hQ(d{ z87?4^nzMp>=~#JTx+@^U_Jlarkqj$5c^cNoh;oG>GT_8&mqMSv4hD<{zZ-H?gx7fg zSA%}sp`LyOh*X6oBtHS?>0Con_QY#1rdgjRqy*ZMJTezg7t6rBuaa35z5<(-7nn;X zyCNrY+`d+c$ddl)2NTH`Uj(iwSt*@*{GqX3D3w)J*HDY2BrBEo9rU3efh{Wix=P#j zvF@XnCQZw7ns!b+3_DZyOQj0Et3_NAj$-Yz>C8p=~?g}$2lsQy{3SDMs?#R-Y| zn^n?eQDoNcucO)9qjuMS9v!d2UOMcq6O3*#YMLmmvNP-n^7_7oIosTZzHzMcOCRKk zG;9G8d8eLt5pzcvuSJ05Iay?x$}JN>B`Du?c>mbDtXrs^C46U7N@lxNh=c)mtc%gQ zL3^wKx5FV^2cN~Sm4qtCvZnETe-`r6#9diKQN+MkM%*18QXOTHJz{|s!*0;O1K09e zp**<^O~JW_Rj{!Z!QVKaaZk2<#F%-4oms+3ZCE$~3ybE!Gp3rQ!)wFv{B#mnG~{H$ zbXh16_qw+y_WM4x8-WAwqsBn2tG?x>SK=qTv`x5FefA-A3Ykj1Laz@_vKD10x7b`^ z8%Fu`jJ-AGQrpj9Gntv`F^_A`oAUPPMM zs;m$tO^vqFUtg&snj+(Q5iO(1&RvO+t_Zu-!mn*vo*p{ zQwe=Q$`5%yV(L>9g9@t=?xZ$SnrM3GNOJf{n5Rm9+{iFoNWg=bO^Mvb794>ETSyI& z9>&U;isgwMv51#)B0@m$YSi-g;~O#`F!$xxKplfpRd|=uE^gBO&~~Oct0U44$~Ncbeh9L+pn;;fqxBt& z26p&CoCO4|^n{;G94R5w4u%q$NJM+A$i;;v+Sq_=!=dL`F`hq9H z4v!Ip%6^W<$;OG!EH4{*}0b9GbCCMBU%Ewv>?e z1u=|lSHo3NpB`ZCuhCek6+0kj*LufNJR`g)0LKs~p{v9PIKD%VIw-~TU@tzk)Cz!5 z`6L35uLP{Fp0Im8vTc>-pysyL3*E&2TrAWB`3DIn>Vah5`-4@yx&t9I=mN;cOh7oH2US%S%^=HJhd>B0 zWI$d3c24}}n1X34GFq{T0_Zpy2PGpvUFE)}u+8z=(cU~62Y+3LPZL+udAfU`fP#*B zMn=JJt;;W+$((s{XOvbidxR?2?2&(ahe4*=GyAx*(vof#`DLRrWv)*TlkZ$>(=D(} z5Gmh_n6u#%luXXZd`{9gVoj={&Z-0`^L)}ZymuE()Y%PU4Id`Ng2Q6F^Ul;3KQ$7j z{krD0oRH!mvqi?Y7c5)T;TTZ77(c#bsqQEr>?Qr z!(U3dBq^2wqKJ#)%Eecer!i_WB(OfCR+7Xa1$XDf%L;sAQD z-m8c?v^d#`_EvqUu>AfKzfSf9fxRFEXeIZ{5=-(nI~!3 zHkHB0zI%G9B_*O%PkznO zW}Idrr_S-Yn2bxwhK^pUio|0`vcxVv^zFH)f{N!V9+CiYVE=~wHr~@`neUqQrRY~x zO&E3G_R>ut!wVI)v7;cQrZLdG%3Uuo3wN=c3D8wrEEtJ_r-us;M5w7W9(=IYK+{1i+8LCgNd*vx( zifFZ+Mi4_R5kTZlFc4PRs+#;R4uy^@S)#>kriYi3F~8J+V1e~(s{x3HP}NpXj!(i{ zpw-JCn;4fJWlM*=oEqG3y*_Oj+bjd_0>hHYa8mXRxqMe#Coi+2@1Uf)xP-(ct5g{^ z#NRB3G?$IHxgNK}gbfG+7li?$aHNu=(y{H+lX!2Xt@OE2YuiQ?)p9L)ex`13yi##G zZfztNY^0JaGTrLI#JX`;EcsGWKF_1e!eTK5mj|Q8HPSYmL7%qJ?1mrY??B-;E-xTt z3fygg!=q=AmPOBI+@ixMg3XH% z%^MLH1$}Q(9d#G|7V%csPQ}$*+u)#DGC8D8`9vPq!&-f*7}csRj@e2ya{^4gMoex; zi}Ycx``-u~16_V?D3>aO-~ExXRK;?_(s|rxjbrzDLk(v3vN(z8avI}-p9Am>xnCU& zMmzz~9hNQrdwNO#CEwBXP~N-7Mgba&6UectbSfB61`@27K}bLPysX7=7| zuXW$|TFV^~{j%7w$^C?(DONqmcB)BRas9BPE>ns7_oY+U4>%DXq>_=d?!8&=_-soy z1*Y1>$rvYmK;`*hGgbCojDYkb)=0EJ*G-AHHjKujc zZSin08Va|04%!KD0ojPZ+(XmeV`fpzuXvvkWc3Tuyik@l(r7NpVM^B^q? zF&Ja5!FZiU{!P3J*5}0GiiL7p%6N1}HB|_eeB0L$!A+NC}3ab+NbpHAJv3zZEVgT#GeAfr4* zyIvFtiynq4w7YnGsQssuh=WGwjWIDouIMb&zTCDS@m8^P+zv5-6czoC@mOikO?Vs7 z1d@e*QzbkwW00t&QvOVmeYNUt!S)VfeMgvb-d&6;ARQh!Mmgig$f2b45kgx^jrA zKaG6Pyk;~=N-EDA5s+gT+-=oh3Y6B=KW^J5_Mum)*-vm8EDOey8H!OX zw0wf!m`#E#QJfZ0X%q3_$N}}3AEHt3JC}agdG(Jb@+3NLi3?Mb14=+Df|}Q2xhGIV z#5;}4XMOcVz+cL6XOP)}HfN+tk4HRn`X_r{FS=B?)s(i_*w$645RRJyl@|Ua=Gg2A z|LAm5Ehrm1foQ36hPZaUI#I1g05iJo{d?GSQQ)QtF1%4Dc6+0fVt)-PIrudfd2B;) z3Kbmf;loGDN^>eP|E2Elm#2 zS$(6ptOl}*T&;4FE0I%QpB;6t;R}<5NK;VuDu7jU?>vw#!jG{;W_L2U5Z^a|c}c2s zxaAOs@hcVr8)Xi=5P{wwCPGyubQdt;xy-5i`t0}UwVLYmmnXGmF|`Lu9gET_P6fkM z%_yVw{4Wo6^{KkocA(wvQB~Wu-4+5DN9!3`skQ{vE>DEg;}7z*Dkm>|o6@tr6B4y^ z^c)q5H=LREU#5oiT77s2k0*c+02W}oMA3UN{t5qiD(GJq?I8Rp{_K4|y49G$AExaROKW(4iEk*}N-qSg#|rm3NrpA!_43Oc#g9#i7{NZpy67ge*rPWIH# zd*g1yPUWb&jWq@X&Kcm3YjAj5tVw4#v`8+(nRWmkR}%zqATOt%!#g-SFi1!%lq1q zUzD4wgoo;fryn(aK=pR8 zSw8@EJLsG8{o+hcb7D79I|72mM_dgO%PKFqJs6+=xKvWBsM3FK#q5)mOHOFHbP~^! z9hlf^x?%{*W;(skt#h!^5jW`_KQNg(~syK%grrIKA&&%2PM~cEWfJ03y>exNIH0Dp> zO^f%wS`a_DRE!>ec-_RnoA&WW(xo^JioaarWaMJ>KC}yfdBX{_RUA`cY(_N9&Z`bL zi|rdMm5z5tb90r)NbJ4{fyTiK&RTk=7+&F?#mNEtm^*$vJF0A(a`3>{a~0^Me&hW$ z&8|Y0!FkD3`rl$-@~#D0sMeqwjSV&v8rQp&% zKM)QS#Ei}DBIesrp)ITzBo#4t79@A=z0N(qkvw_Ixp9~(;ZR5^X+}bVhneU|MiqGP zFdiN(IC!%F6UW9zDVW)*MoCUCISe#XMp{2OCP0xv(U3ooRbo;WvjB|Ju~%?QN(e?_GXsugy{g9=cPn>fnX#{&Nd+^cTTIwSRov zt^68mQlL;2za)`iy=UuLjK#tkjB?gZe__3GyLUKY zvH0mjxv})CGx;q!wND2A%$sgI#ofG#%2Ca0n~Y!Lwy81j4cEFp=esNpC4s%Eee}?{ zsg6oVr?kN-E%M=LXPh$1{5xrK7Tvq|*3sN-v)?nfSS;pybap920NO1R9+QQ2zM_Iu zxs1}Ue^e_twLBT=BrF)#A12op{PiC1XmcVE<$3{-Z%94t!rY|gFuanIP6pOcr}5zK z%rC=1DodAGx{;xQ@rVZjl^D2u`EBZ$;DSX^pWLf$ADE7PIqIPYN38aDrk~wnRb$iU_Q=s#DJd#rBCzP|Fu#|G7?YM5o?9v0 zNoJ87Ja+S>muYtOa1iyzdne$Jl@xN)8shl|hyCtgg`&D*HqDIKe+HczACy{E=MP*i z)+(d;p?zGAr^n5}`bzh5)!DI~9-<7G+L55$)tqT2#+WY1_#_;bmh7Nm(y|GdyaEl{ z<@L>!gwMt71ezqtmdh8PO9o(GciO{sg;JLa#kYJf(vr9FyzcI!26gl@V!}oeO%QBv% z8ZRs?)6TDWE=G;}I#KZDx2HYg?0Q8JxP%L6Rh4`P`D|XUNf-R!vD%DAKYK6$$V>Mg zi!c)M(MF$$ORu$lc39_id5iv4GwRLFXb{n#v8WzfAxfBPwqk@4O>UtI`5g0#C%E~GA}_w?)04@{MWG@7+reA_az68&%me?b5uE^)^>gLy{n-XX~! z7H$@Kq4Tq(_zZRhuv2XWVh(y2eXz4%d6&;k8jQ)PhiiOJV!#Gl8hAZ=+5~{DuO4?4 zp=-K&rU87jI=()XWzk_&V{p1#Z>yL=ZACB*+&1<0QpRLq$uPJ(^dhLFp zH^<$)Er?dx`g@aO*U`6c|8AKV&lyY3=d7C?i>3avusqhW4FT?;hoygm!IU@Je6(3c ze3qWii5$oz$G8;~5a+1WS!2-waZgF2gFSfG)Z7V-ar{&8itM8?3QV)OC?TNxPW=ps z=Rz&1vz18pR^G;iAd$J#(<4Uy-$<(zx!XE%!v67o627t|6|PR`U`;i){+^&xQ)ur@ zKX;bVR0=SK69@AnOU;*e@#5oBE~Uk{u2Cyv54M14vGI+IzUhk=8x$|f?aPSWN;4Ki z#W0k5JxD;??YP0>eiF|ud7rJ&25?oSB57b+K{z-#(Xf{tR{TZr$B_B$&;$gHPc(x_ zmFkMyXGX`3Jj$nIDLLcoNkL5u2eHfCru_K@b%8_eqinY)t{N|Z5Mv-i6l4Qn8qx_M z;D4XP`i@^pkgp8US855 zHA}&srRV4+@i_Ph9K+2;W?tUn)63~-QPT@2G5niR z@Z&TDY#Km%=Od`|H)~@RkH=rV(&@61F%iD@9CB|kmawt<1~9IY2jj?XY24t9neW~- zK0_IzC4dRAw;w;63Wv1WRF`Fv9kG1NQ~xwq^;jlfu=ChNiD;qN6$A5H{vLhZl1oA> z$sX5Nu8T@j^N?@?{vfc5?e?JE0F2&%1dfhQTmN+)Rkl(%kSXaCFxX@XbZvGi*r}er z6uxiRBh>r9aGO5Dx=7^(KK^jruru`}BkgsK5MdCoWV@9a$8n3@YvjlijFGr?ZaV z{lJ+IZ26-8<9}B0AidHplb~*xtxQ#lU!Y3;O7-9ig=Z(i#U)s~7-OOSwM$ojNAwdzTv(VT?xnl(*U`W&tu4CM+=PtYufV1AR@ z8|iD?3IBS}P;T64w+`H3dtqq_kw``Ylm@P&j7_;Mw?k;C5-)#B)Lt&0SENhf%VO0& zP6k#3OxurtjbWZG@>r$`AqJQBSOk6KmjNy6nN6XdD(*ElJWEU5RMzY_c})(KD&Z)}o5hPgpGvd`obEbj4U{j*QA^y>^Ep>sr zycwT3F^G^$45Kxa1fvUP^J2zCSJ8=vN@OU*ci;5+C{Vw`_owl8FX8pI1y5L+NwE)J zU2KzMHzXOa$!JA4rrw$z(@O`ePIY1%d9iX;E`PcQHNMz;%3&+H_G5Pe{qVYb9SpND zmY>DZtzna9#m8l;20K5uVy23L%%8qBqi4;pGzJPtx2wyTc-zWU?vjVI9KN)R>s^Je z;eCoz?vyozZFBg`#1Dy9SWf)1M{^XJV&=qgt^!V+j3lfF_rcl)?{?)Vf4}dFuaDgQ zikcFRiZK`7EM5_K{MqmGSe#@8r@-UJa}9I}Uai(~!R`)0U2-^jFO-HmYb*Rz?)$x3 zIerJDCCzdngu|j{ImWv2!zsST=3djsbTw_h@!4iGzQlD!BCq9XIkT7JdX64TEE|=2 z*Uf@GSxWvb)7#3Q^S&1gu1;Y$!>)_&n_|81krw=A98y5USTg>r`r!o^jZSy^{#OM? zQ_9vBlaq=#Ozw0%%Z%6o3*4Hv69@Qm41vFn8wHgALvlvGw+MT5y42OaZH4(A z**WNg-mlY}sD`VFq2YkY!)bL9#28&|4ypjV)WcMeenX zxkm}CcZK-bh;kR?Kj&#^3a@j1OwiZvF|lu##ECcw8p0x8FNwE0^){W|%J)zReYMH& zV3)m~);J|q*}A~6SEB>5jF>p1@l*FVHpvY&^9pH-(wFX7$j=$!7qiu~NNdh{#p;$L zcND|;*rc4NLMJja1xpnVgQB3-w@E*U;affOH6punx-*onRyYC%Ip_$*7M;jHLeRlA z`W@|j_T%Tc$kAW%$gy=ZB|*sL(aFt-N2%)U3eB({-P8D&9zQ8;q}H((pztm&vfKz#IE^X;o44*c;fUg7ToN$#K}|i;Xoo%yH=SJo~4^ z-Ll;peR7s0cI!C~cdjJ=>q*2@=@qPD)fQ}qGRp;!>3PgJ(rRiK&eKe~4}Z8|IaS({ z*D{nWYF>dbU{=_st zinSFZxXccD!_fSeF&hULd`UC2!Jo^BbS#mN*56ac>s$O#%i;FXL~Hs4DpYV7cBhL> z{*nYlQw0CsCk2NayJN3{`)?4uJBqLPdk!A^&*A@k4qVR)eZ%m-HyKGkV22(H9FTrU p#yO+-@5R5*`x4juf4MM-y&0M?>Y@EM?%(pH$_g6KDxSO!`5$qt2f+XU literal 0 HcmV?d00001 diff --git a/assets/chart-analytics-light.png b/assets/chart-analytics-light.png new file mode 100644 index 0000000000000000000000000000000000000000..40d76c7e7c4ea080941b5c53e1088918081b7b1f GIT binary patch literal 109235 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%0Asm${oa$*|fi`1KCDJ?!y? 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 sqrts(OZb2P$Nm4>FaAG!iTA+uT_&=L Date: Wed, 10 Mar 2021 22:38:02 +0100 Subject: [PATCH 060/117] Renaming some files. Re-create a new thread and fix: raise RuntimeError("threads can only be started once") --- .../TwitchChannelPointsMiner.py | 4 +- TwitchChannelPointsMiner/classes/Chat.py | 75 +++++++++++++++++++ .../classes/TwitchChat.py | 34 --------- .../classes/entities/Chat.py | 39 ---------- .../classes/entities/Streamer.py | 9 +++ 5 files changed, 86 insertions(+), 75 deletions(-) create mode 100644 TwitchChannelPointsMiner/classes/Chat.py delete mode 100644 TwitchChannelPointsMiner/classes/TwitchChat.py delete mode 100644 TwitchChannelPointsMiner/classes/entities/Chat.py diff --git a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py index 95921f8..e014e23 100644 --- a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py +++ b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py @@ -11,6 +11,7 @@ import uuid from collections import OrderedDict from datetime import datetime +from TwitchChannelPointsMiner.classes.Chat import ThreadChat from TwitchChannelPointsMiner.classes.entities.PubsubTopic import PubsubTopic from TwitchChannelPointsMiner.classes.entities.Streamer import ( Streamer, @@ -19,7 +20,6 @@ from TwitchChannelPointsMiner.classes.entities.Streamer import ( from TwitchChannelPointsMiner.classes.Exceptions import StreamerDoesNotExistException from TwitchChannelPointsMiner.classes.Settings import Priority, Settings from TwitchChannelPointsMiner.classes.Twitch import Twitch -from TwitchChannelPointsMiner.classes.TwitchChat import TwitchChat from TwitchChannelPointsMiner.classes.WebSocketsPool import WebSocketsPool from TwitchChannelPointsMiner.logger import LoggerSettings, configure_loggers from TwitchChannelPointsMiner.utils import ( @@ -168,7 +168,7 @@ class TwitchChannelPointsMiner: streamer.settings.bet, Settings.streamer_settings.bet ) if streamer.settings.join_chat is True: - streamer.irc_chat = TwitchChat( + streamer.irc_chat = ThreadChat( self.username, self.twitch.twitch_login.get_auth_token(), streamer.username, diff --git a/TwitchChannelPointsMiner/classes/Chat.py b/TwitchChannelPointsMiner/classes/Chat.py new file mode 100644 index 0000000..5e0272d --- /dev/null +++ b/TwitchChannelPointsMiner/classes/Chat.py @@ -0,0 +1,75 @@ +import logging +import time +from threading import Thread + +from irc.bot import SingleServerIRCBot + +from TwitchChannelPointsMiner.constants import IRC, IRC_PORT + +logger = logging.getLogger(__name__) + + +class ClientIRC(SingleServerIRCBot): + def __init__(self, username, token, channel): + self.token = token + self.channel = "#" + channel + self.__active = False + + super(ClientIRC, self).__init__( + [(IRC, IRC_PORT, f"oauth:{token}")], username, username + ) + + def on_welcome(self, client, event): + client.join(self.channel) + + def start(self): + self.__active = True + self._connect() + while self.__active: + try: + self.reactor.process_once(timeout=0.2) + time.sleep(0.01) + except Exception as e: + logger.error( + f"Exception raised: {e}. Thread is active: {self.__active}" + ) + + def die(self, msg="Bye, cruel world!"): + self.connection.disconnect(msg) + self.__active = False + + """ + def on_join(self, connection, event): + logger.info(f"Event: {event}", extra={"emoji": ":speech_balloon:"}) + + def on_pubmsg(self, client, message): + logger.info(f"Message: {message}", extra={"emoji": ":speech_balloon:"}) + """ + + +class ThreadChat(Thread): + def __deepcopy__(self, memo): + return None + + def __init__(self, username, token, channel): + super(ThreadChat, self).__init__() + + self.username = username + self.token = token + self.channel = channel + + self.chat_irc = None + + def run(self): + self.chat_irc = ClientIRC(self.username, self.token, self.channel) + logger.info( + f"Join IRC Chat: {self.channel}", extra={"emoji": ":speech_balloon:"} + ) + self.chat_irc.start() + + def stop(self): + if self.chat_irc is not None: + logger.info( + f"Leave IRC Chat: {self.channel}", extra={"emoji": ":speech_balloon:"} + ) + self.chat_irc.die() diff --git a/TwitchChannelPointsMiner/classes/TwitchChat.py b/TwitchChannelPointsMiner/classes/TwitchChat.py deleted file mode 100644 index b438c5d..0000000 --- a/TwitchChannelPointsMiner/classes/TwitchChat.py +++ /dev/null @@ -1,34 +0,0 @@ -import logging -from threading import Thread - -from TwitchChannelPointsMiner.classes.entities.Chat import Chat - -logger = logging.getLogger(__name__) - - -class TwitchChat(Thread): - def __deepcopy__(self, memo): - return None - - def __init__(self, username, token, channel): - super(TwitchChat, self).__init__() - - self.token = token - self.username = username - self.channel = channel - - self.chat_irc = None - - def run(self): - self.chat_irc = Chat(self.username, self.token, self.channel) - logger.info( - f"Join IRC Chat: {self.channel}", extra={"emoji": ":speech_balloon:"} - ) - self.chat_irc.start() - - def stop(self): - if self.chat_irc is not None: - logger.info( - f"Leave IRC Chat: {self.channel}", extra={"emoji": ":speech_balloon:"} - ) - self.chat_irc.die() diff --git a/TwitchChannelPointsMiner/classes/entities/Chat.py b/TwitchChannelPointsMiner/classes/entities/Chat.py deleted file mode 100644 index fb65bba..0000000 --- a/TwitchChannelPointsMiner/classes/entities/Chat.py +++ /dev/null @@ -1,39 +0,0 @@ -import logging - -from irc.bot import SingleServerIRCBot - -from TwitchChannelPointsMiner.constants import IRC, IRC_PORT - -logger = logging.getLogger(__name__) - - -class Chat(SingleServerIRCBot): - def __init__(self, username, token, channel): - self.token = token - self.channel = "#" + channel - self.__active = False - - super(Chat, self).__init__( - [(IRC, IRC_PORT, f"oauth:{token}")], username, username - ) - - def on_welcome(self, client, event): - client.join(self.channel) - - def start(self): - self.__active = True - self._connect() - while self.__active: - self.reactor.process_once(timeout=0.2) - - def die(self, msg="Bye, cruel world!"): - self.connection.disconnect(msg) - self.__active = False - - """ - def on_join(self, connection, event): - logger.info(f"Event: {event}", extra={"emoji": ":speech_balloon:"}) - - def on_pubmsg(self, client, message): - logger.info(f"Message: {message}", extra={"emoji": ":speech_balloon:"}) - """ diff --git a/TwitchChannelPointsMiner/classes/entities/Streamer.py b/TwitchChannelPointsMiner/classes/entities/Streamer.py index c6ac48e..dc1ef53 100644 --- a/TwitchChannelPointsMiner/classes/entities/Streamer.py +++ b/TwitchChannelPointsMiner/classes/entities/Streamer.py @@ -1,6 +1,7 @@ import logging import time +from TwitchChannelPointsMiner.classes.Chat import ThreadChat from TwitchChannelPointsMiner.classes.entities.Bet import BetSettings from TwitchChannelPointsMiner.classes.entities.Stream import Stream from TwitchChannelPointsMiner.classes.Settings import Settings @@ -164,6 +165,14 @@ class Streamer(object): if self.irc_chat is not None: self.irc_chat.stop() + # Recreate a new thread to start again + # raise RuntimeError("threads can only be started once") + self.irc_chat = ThreadChat( + self.irc_chat.username, + self.irc_chat.token, + self.username, + ) + def join_chat(self): if self.irc_chat is not None: self.irc_chat.start() From bd702ad25c48d3ffb979595bc0593e45b3d04dad Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Wed, 10 Mar 2021 22:40:00 +0100 Subject: [PATCH 061/117] Suppress irc.client in logger --- TwitchChannelPointsMiner/TwitchChannelPointsMiner.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py index e014e23..d8b68c8 100644 --- a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py +++ b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py @@ -34,8 +34,12 @@ from TwitchChannelPointsMiner.utils import ( # - chardet.charsetprober - [feed] # - chardet.charsetprober - [get_confidence] # - requests - [Starting new HTTPS connection (1)] +# - irc.client - [process_data] +# - irc.client - [_dispatcher] +# - irc.client - [_handle_message] logging.getLogger("chardet.charsetprober").setLevel(logging.ERROR) logging.getLogger("requests").setLevel(logging.ERROR) +logging.getLogger("irc.client").setLevel(logging.ERROR) logger = logging.getLogger(__name__) From 639b6d7df3b2e162f9a8bfd434e3715638dc07c4 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Thu, 11 Mar 2021 22:44:36 +0100 Subject: [PATCH 062/117] Minimum points for the bot to place a bet #106 --- .../classes/WebSocketsPool.py | 59 ++++++++++++------- .../classes/entities/Bet.py | 6 +- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/TwitchChannelPointsMiner/classes/WebSocketsPool.py b/TwitchChannelPointsMiner/classes/WebSocketsPool.py index 8913bb5..67d7778 100644 --- a/TwitchChannelPointsMiner/classes/WebSocketsPool.py +++ b/TwitchChannelPointsMiner/classes/WebSocketsPool.py @@ -253,29 +253,48 @@ class WebSocketsPool: ): if event.streamer.viewer_is_mod is True: logger.info( - f"Sorry, you are moderator of {event.streamer}, so you can't bet!" - ) - else: - ws.events_predictions[event_id] = event - start_after = event.closing_bet_after( - current_tmsp - ) - - place_bet_thread = threading.Timer( - start_after, - ws.twitch.make_predictions, - (ws.events_predictions[event_id],), - ) - place_bet_thread.daemon = True - place_bet_thread.start() - - logger.info( - f"Place the bet after: {start_after}s for: {ws.events_predictions[event_id]}", + f"Sorry, you are moderator of {event.streamer}, so you can't bet!", extra={ - "emoji": ":alarm_clock:", - "color": Settings.logger.color_palette.BET_START, + "emoji": ":pushpin:", + "color": Settings.logger.color_palette.BET_FILTERS, }, ) + else: + streamer = ws.streamers[streamer_index] + bet_settings = streamer.settings.bet + if ( + bet_settings.minimum_points is None + or streamer.channel_points + > bet_settings.minimum_points + ): + ws.events_predictions[event_id] = event + start_after = event.closing_bet_after( + current_tmsp + ) + + place_bet_thread = threading.Timer( + start_after, + ws.twitch.make_predictions, + (ws.events_predictions[event_id],), + ) + place_bet_thread.daemon = True + place_bet_thread.start() + + logger.info( + f"Place the bet after: {start_after}s for: {ws.events_predictions[event_id]}", + extra={ + "emoji": ":alarm_clock:", + "color": Settings.logger.color_palette.BET_START, + }, + ) + else: + logger.info( + f"{streamer} have only {streamer.channel_points} channel points and the minimum for bet is: {bet_settings.minimum_points}", + extra={ + "emoji": ":pushpin:", + "color": Settings.logger.color_palette.BET_FILTERS, + }, + ) elif ( message.type == "event-updated" diff --git a/TwitchChannelPointsMiner/classes/entities/Bet.py b/TwitchChannelPointsMiner/classes/entities/Bet.py index cffde0d..e030ae6 100644 --- a/TwitchChannelPointsMiner/classes/entities/Bet.py +++ b/TwitchChannelPointsMiner/classes/entities/Bet.py @@ -63,6 +63,7 @@ class BetSettings(object): "percentage", "percentage_gap", "max_points", + "minimum_points", "stealth_mode", "filter_condition", ] @@ -73,6 +74,7 @@ class BetSettings(object): percentage: int = None, percentage_gap: int = None, max_points: int = None, + minimum_points: int = None, stealth_mode: bool = None, filter_condition: FilterCondition = None, ): @@ -80,6 +82,7 @@ class BetSettings(object): self.percentage = percentage self.percentage_gap = percentage_gap self.max_points = max_points + self.minimum_points = minimum_points self.stealth_mode = stealth_mode self.filter_condition = filter_condition @@ -88,10 +91,11 @@ class BetSettings(object): self.percentage = self.percentage if not None else 5 self.percentage_gap = self.percentage_gap if not None else 20 self.max_points = self.max_points if not None else 50000 + self.minimum_points = self.minimum_points if not None else 0 self.stealth_mode = self.stealth_mode if not None else False def __repr__(self): - return f"BetSettings(strategy={self.strategy}, percentage={self.percentage}, percentage_gap={self.percentage_gap}, max_points={self.max_points}, stealth_mode={self.stealth_mode})" + return f"BetSettings(strategy={self.strategy}, percentage={self.percentage}, percentage_gap={self.percentage_gap}, max_points={self.max_points}, minimum_points={self.minimum_points}, stealth_mode={self.stealth_mode})" class Bet(object): From 7465308074efd2833df70396f21f2fa77e79ff0e Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Fri, 12 Mar 2021 23:01:01 +0100 Subject: [PATCH 063/117] Update README.md --- README.md | 20 ++++++++++--------- .../TwitchChannelPointsMiner.py | 2 +- example.py | 2 +- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 649a967..ca64949 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,7 @@ twitch_miner = TwitchChannelPointsMiner( follow_raid=True, # Follow raid to obtain more points claim_drops=True, # We can't filter rewards base on stream. Set to False for skip viewing counter increase and you will never obtain a drop reward from this script. Issue #21 watch_streak=True, # If a streamer go online change the priotiry of streamers array and catch the watch screak. Issue #11 + join_chat=True, # Join irc chat to increase watch-time bet=BetSettings( strategy=Strategy.SMART, # Choose you strategy! percentage=5, # Place the x% of your channel points @@ -334,18 +335,19 @@ ColorPalette( |-------------------- |------------- |-------------------------------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `make_predictions` | bool | True | Choose if you want to make predictions / bet or not | | `follow_raid` | bool | True | Choose if you want to follow raid +250 points | -| `claim_drops` | bool | True | If this value is True, the script will increase the watch-time for the current game. With this, you can claim the drops from Twitch Inventory [#21](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/21) | +| `claim_drops` | bool | True | If this value is True, the script will increase the watch-time for the current game. With this, you can claim the drops from Twitch Inventory [#21](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/21) | | `watch_streak` | bool | True | Choose if you want to change a priority for these streamers and try to catch the Watch Streak event [#11](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/11) | | `bet` | BetSettings | | Rules to follow for the bet | +| `follow_raid` | bool | True | Choose if you want to follow raid +250 points | ### BetSettings -| Key | Type | Default | Description | -|-------------------- |----------------- |--------- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `strategy` | Strategy | SMART | Choose your strategy! See above for more info | -| `percentage` | int | 5 | Place the x% of your channel points | -| `percentage_gap` | int | 20 | Gap difference between outcomesA and outcomesB (for SMART stragegy) | -| `max_points` | int | 50000 | If the x percentage of your channel points is GT bet_max_points set this value | -| `stealth_mode` | bool | False | If the calculated amount of channel points is GT the highest bet, place the highest value minus 1-2 points [#33](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/33) | -| `filter_condition` | FilterCondition | None | Based on this filter the script will skip some bet [#29](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/29) | +| Key | Type | Default | Description | +|-------------------- |----------------- |--------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `strategy` | Strategy | SMART | Choose your strategy! See above for more info | +| `percentage` | int | 5 | Place the x% of your channel points | +| `percentage_gap` | int | 20 | Gap difference between outcomesA and outcomesB (for SMART stragegy) | +| `max_points` | int | 50000 | If the x percentage of your channel points is GT bet_max_points set this value | +| `stealth_mode` | bool | False | If the calculated amount of channel points is GT the highest bet, place the highest value minus 1-2 points [#33](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/33) | +| `join_chat` | bool | True | Join IRC-Chat to appear online in chat and attempt to get StreamElements channel points and increase view-time [#47](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/47) | #### Bet strategy diff --git a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py index aea4f73..c00eb38 100644 --- a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py +++ b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py @@ -12,8 +12,8 @@ from collections import OrderedDict from datetime import datetime from pathlib import Path -from TwitchChannelPointsMiner.classes.Chat import ThreadChat from TwitchChannelPointsMiner.classes.AnalyticsServer import AnalyticsServer +from TwitchChannelPointsMiner.classes.Chat import ThreadChat from TwitchChannelPointsMiner.classes.entities.PubsubTopic import PubsubTopic from TwitchChannelPointsMiner.classes.entities.Streamer import ( Streamer, diff --git a/example.py b/example.py index 9938a1c..a8eb20f 100644 --- a/example.py +++ b/example.py @@ -35,7 +35,7 @@ twitch_miner = TwitchChannelPointsMiner( follow_raid=True, # Follow raid to obtain more points claim_drops=True, # We can't filter rewards base on stream. Set to False for skip viewing counter increase and you will never obtain a drop reward from this script. Issue #21 watch_streak=True, # If a streamer go online change the priotiry of streamers array and catch the watch screak. Issue #11 - join_chat=True, # Join irc chat + join_chat=True, # Join irc chat to increase watch-time bet=BetSettings( strategy=Strategy.SMART, # Choose you strategy! percentage=5, # Place the x% of your channel points From 43a545fe2d4b7c2a03939261fcc8705ff151adb7 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Fri, 12 Mar 2021 23:14:33 +0100 Subject: [PATCH 064/117] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ca64949..588813e 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,8 @@ If you have any issues or you want to contribute, you are welcome! But please be - Auto claim game drops from Twitch inventory [#21](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/21) Read more about game drops [here](https://help.twitch.tv/s/article/mission-based-drops) - Place the bet / make a prediction and win or lose (πŸ€) your channel points! No browser needed. [#41](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/41) ([@lay295](https://github.com/lay295)) -- Analytics chart +- Analytics chart that show the progress of your points with various annotations [#96](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/96) +- Join IRC Chat for increase watch-time and get StreamElements points [#47](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/47) ## Logs feature ### Full logs From 03c07220d1180b1b57e59f8a804592dc5be9208a Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Sat, 13 Mar 2021 13:17:33 +0100 Subject: [PATCH 065/117] Clear annotation on streamer change, prevent porting annotation between different streamers --- assets/charts.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/charts.html b/assets/charts.html index f0e2086..6d287c4 100644 --- a/assets/charts.html +++ b/assets/charts.html @@ -178,8 +178,9 @@ name: streamer.replace(".json", ""), data: response["series"] }], true) + clearAnnotations(); annotations = response["annotations"]; - updateAnnotations() + updateAnnotations(); setTimeout(function(){getStreamerData(streamer);}, 300000); // 5 minutes }); } From cebd48326fd4466226219a8151da724301b377ba Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Sat, 13 Mar 2021 13:33:30 +0100 Subject: [PATCH 066/117] Deleted wrong settings.json file upload from another branch --- settings.json | 86 --------------------------------------------------- 1 file changed, 86 deletions(-) delete mode 100644 settings.json diff --git a/settings.json b/settings.json deleted file mode 100644 index 8c74855..0000000 --- a/settings.json +++ /dev/null @@ -1,86 +0,0 @@ -[ - { - "settings": null, - "username": "r_eqxxx" - }, - { - "settings": null, - "username": "vhauss" - }, - { - "settings": null, - "username": "carriedbyoujua" - }, - { - "settings": null, - "username": "robertcrg" - }, - { - "settings": null, - "username": "blu3berrycupcake" - }, - { - "settings": null, - "username": "brbteabreak" - }, - { - "settings": null, - "username": "nut9tv" - }, - { - "settings": null, - "username": "joaoboavista" - }, - { - "settings": null, - "username": "hyeaga" - }, - { - "settings": null, - "username": "chaosmachinegr" - }, - { - "settings": null, - "username": "malteseknight_" - }, - { - "settings": null, - "username": "daniel_rusev" - }, - { - "settings": null, - "username": "serwinterofficial" - }, - { - "settings": null, - "username": "shadowfrax" - }, - { - "settings": null, - "username": "boxbox" - }, - { - "settings": null, - "username": "ash_on_lol" - }, - { - "settings": null, - "username": "dyannatv" - }, - { - "settings": null, - "username": "mammoth" - }, - { - "settings": null, - "username": "bchillz" - }, - { - "settings": null, - "username": "buddha" - }, - { - "settings": null, - "username": "trausi" - } -] From bea8bb84287a65d166e624364d045bfb66d14e34 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Sun, 14 Mar 2021 20:18:42 +0100 Subject: [PATCH 067/117] init res dict --- TwitchChannelPointsMiner/classes/entities/EventPrediction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TwitchChannelPointsMiner/classes/entities/EventPrediction.py b/TwitchChannelPointsMiner/classes/entities/EventPrediction.py index 7349aa4..b6cde38 100644 --- a/TwitchChannelPointsMiner/classes/entities/EventPrediction.py +++ b/TwitchChannelPointsMiner/classes/entities/EventPrediction.py @@ -36,7 +36,7 @@ class EventPrediction(object): self.created_at = created_at self.prediction_window_seconds = prediction_window_seconds self.status = status - self.result: dict = {} + self.result: dict = {"string": "", "type": None, "gained": 0} self.box_fillable = False self.bet_confirmed = False From 2f373bf101c3bdac4eb985dec6d330db0097f82d Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Sun, 28 Mar 2021 00:01:32 +0100 Subject: [PATCH 068/117] Each 30m refresh context and check for claimable bonus. Reference for #114 --- TwitchChannelPointsMiner/TwitchChannelPointsMiner.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py index c00eb38..86f782e 100644 --- a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py +++ b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py @@ -276,6 +276,7 @@ class TwitchChannelPointsMiner: PubsubTopic("predictions-channel-v1", streamer=streamer) ) + refresh_context = time.time() while self.running: time.sleep(random.uniform(20, 60)) # Do an external control for WebSocket. Check if the thread is running @@ -291,6 +292,14 @@ class TwitchChannelPointsMiner: ) WebSocketsPool.handle_reconnection(self.ws_pool.ws[index]) + if ((time.time() - refresh_context) // 60) >= 30: + refresh_context = time.time() + for index in range(0, len(self.streamers)): + if self.streamers[index].is_online: + self.twitch.load_channel_points_context( + self.streamers[index] + ) + def end(self, signum, frame): logger.info("CTRL+C Detected! Please wait just a moment!") From 5342a1b3f2834413e90693297598f4145c8f4cfe Mon Sep 17 00:00:00 2001 From: Caterpy <50979288+CaterpyOwO@users.noreply.github.com> Date: Wed, 31 Mar 2021 23:57:33 +0200 Subject: [PATCH 069/117] Saving settings into `localStorage` --- assets/charts.html | 76 ++++++++++++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 26 deletions(-) diff --git a/assets/charts.html b/assets/charts.html index 6d287c4..760989b 100644 --- a/assets/charts.html +++ b/assets/charts.html @@ -7,35 +7,44 @@ Twitch-Channel-Points-Miner-v2 - - + + - +
@@ -46,7 +55,8 @@
@@ -66,9 +76,12 @@
-
If you want to help on this project, please leave a star 🌟 and share it with your friends! 😎 - A coffee is always a gesture of LOVE ❀️ +
If you want to help on this project, please leave a star 🌟 and share it with + your friends! 😎 - A coffee is always a gesture of LOVE ❀️ - Buy Me A Coffee + Buy Me A Coffee
@@ -133,7 +146,7 @@ show: true, format: 'HH:mm:ss dd MMM', }, - custom: ({series, seriesIndex, dataPointIndex, w}) => { + custom: ({ series, seriesIndex, dataPointIndex, w }) => { return (`
${w.globals.seriesNames[seriesIndex]}
@@ -159,20 +172,26 @@ var refresh = parseInt("{{ refresh }}"); - $(document).ready(function() { + $(document).ready(function () { chart.render(); $("li").eq(0).click(); + + $('#annotations').prop("checked", localStorage.getItem("annotations") === "true"); + $('#dark-mode').prop("checked", localStorage.getItem("dark-mode") === "true"); + + updateAnnotations(); + toggleDarkMode(); }); function changeStreamer(streamer, index) { - $("li").removeClass( "is-active" ) + $("li").removeClass("is-active") $("li").eq(index - 1).addClass('is-active'); currentStreamer = streamer; getStreamerData(streamer); } function getStreamerData(streamer) { - if(currentStreamer == streamer){ + if (currentStreamer == streamer) { $.getJSON(`./json/${streamer}`, function (response) { chart.updateSeries([{ name: streamer.replace(".json", ""), @@ -181,31 +200,36 @@ clearAnnotations(); annotations = response["annotations"]; updateAnnotations(); - setTimeout(function(){getStreamerData(streamer);}, 300000); // 5 minutes + setTimeout(function () { getStreamerData(streamer); }, 300000); // 5 minutes }); } } function updateAnnotations() { - if($('#annotations').prop("checked") === true){ + if ($('#annotations').prop("checked") === true) { clearAnnotations() - if(annotations && annotations.length > 0) + if (annotations && annotations.length > 0) annotations.forEach((annotation, index) => { annotations[index]['id'] = `id-${index}` chart.addXaxisAnnotation(annotation, true) }) - } else clearAnnotations() + localStorage.setItem('annotations', true); + } else { + clearAnnotations(); + localStorage.setItem('annotations', false); + } } - function clearAnnotations(){ - if(annotations && annotations.length > 0) + function clearAnnotations() { + if (annotations && annotations.length > 0) annotations.forEach((annotation, index) => { chart.removeAnnotation(annotation['id']) }) } - function toggleDarkMode(){ + function toggleDarkMode() { var darkMode = $('#dark-mode').prop("checked") + localStorage.setItem('dark-mode', darkMode); $("link[href='{{url_for('static', filename='dark-theme.css')}}']").prop("disabled", !darkMode); chart.updateOptions({ colors: darkMode === true ? ["#f9826c"] : ['#008ffb'], From f989651934678a69a85e8d0a17280584d4eb5833 Mon Sep 17 00:00:00 2001 From: Caterpy <50979288+CaterpyOwO@users.noreply.github.com> Date: Thu, 1 Apr 2021 00:06:28 +0200 Subject: [PATCH 070/117] Updated default settings --- assets/charts.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/assets/charts.html b/assets/charts.html index 760989b..007c6be 100644 --- a/assets/charts.html +++ b/assets/charts.html @@ -176,6 +176,9 @@ chart.render(); $("li").eq(0).click(); + if (!localStorage.getItem("annotations")) localStorage.setItem("annotations", true); + if (!localStorage.getItem("dark-mode")) localStorage.setItem("dark-mode", true); + $('#annotations').prop("checked", localStorage.getItem("annotations") === "true"); $('#dark-mode').prop("checked", localStorage.getItem("dark-mode") === "true"); From 8987feb20263d72af8126e8c9ba8096765916d11 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Sat, 3 Apr 2021 01:06:09 +0200 Subject: [PATCH 071/117] The gql seems to be have a max limits of args, reduce the campaign to 20. Ref for #128 and #129 --- TwitchChannelPointsMiner/classes/Twitch.py | 28 ++++++++++++++-------- TwitchChannelPointsMiner/constants.py | 2 +- TwitchChannelPointsMiner/utils.py | 4 ++++ 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/TwitchChannelPointsMiner/classes/Twitch.py b/TwitchChannelPointsMiner/classes/Twitch.py index 08ce5ad..f05e327 100644 --- a/TwitchChannelPointsMiner/classes/Twitch.py +++ b/TwitchChannelPointsMiner/classes/Twitch.py @@ -24,7 +24,11 @@ from TwitchChannelPointsMiner.classes.Exceptions import ( from TwitchChannelPointsMiner.classes.Settings import Priority, Settings from TwitchChannelPointsMiner.classes.TwitchLogin import TwitchLogin from TwitchChannelPointsMiner.constants import API, CLIENT_ID, GQLOperations -from TwitchChannelPointsMiner.utils import _millify, internet_connection_available +from TwitchChannelPointsMiner.utils import ( + _millify, + create_chunks, + internet_connection_available, +) logger = logging.getLogger(__name__) @@ -489,16 +493,20 @@ class Twitch(object): return campaigns def __get_campaigns_details(self, campaigns): - json_data = [] - for campaign in campaigns: - json_data.append(copy.deepcopy(GQLOperations.DropCampaignDetails)) - json_data[-1]["variables"] = { - "dropID": campaign["id"], - "channelLogin": f"{self.twitch_login.get_user_id()}", - } + result = [] + chunks = create_chunks(campaigns) + for chunk in chunks: + json_data = [] + for campaign in chunk: + json_data.append(copy.deepcopy(GQLOperations.DropCampaignDetails)) + json_data[-1]["variables"] = { + "dropID": campaign["id"], + "channelLogin": f"{self.twitch_login.get_user_id()}", + } - response = self.post_gql_request(json_data) - return list(map(lambda x: x["data"]["user"]["dropCampaign"], response)) + response = self.post_gql_request(json_data) + result += list(map(lambda x: x["data"]["user"]["dropCampaign"], response)) + return result def __sync_campaigns(self, campaigns): # We need the inventory only for get the real updated value/progress diff --git a/TwitchChannelPointsMiner/constants.py b/TwitchChannelPointsMiner/constants.py index 81c5100..6127c7f 100644 --- a/TwitchChannelPointsMiner/constants.py +++ b/TwitchChannelPointsMiner/constants.py @@ -118,7 +118,7 @@ class GQLOperations: "extensions": { "persistedQuery": { "version": 1, - "sha256Hash": "7da6078b1bfa2f0a4dd061cb47bdcd1ffddf31cccadd966ec192e4cd06666e2b", + "sha256Hash": "14b5e8a50777165cfc3971e1d93b4758613fe1c817d5542c398dce70b7a45c05", # "7da6078b1bfa2f0a4dd061cb47bdcd1ffddf31cccadd966ec192e4cd06666e2b", } }, } diff --git a/TwitchChannelPointsMiner/utils.py b/TwitchChannelPointsMiner/utils.py index 0a4ab14..ceaef57 100644 --- a/TwitchChannelPointsMiner/utils.py +++ b/TwitchChannelPointsMiner/utils.py @@ -150,3 +150,7 @@ def internet_connection_available(host="8.8.8.8", port=53, timeout=3): def percentage(a, b): return 0 if a == 0 else int((a / b) * 100) + + +def create_chunks(lst, n): + return [lst[i : (i + n)] for i in range(0, len(lst), n)] # noqa: E203 From 7f277d8148a6c9dbcef0323bb899063d7d0d7ff1 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Sat, 3 Apr 2021 01:07:41 +0200 Subject: [PATCH 072/117] chart.clearAnnotations(); #122 --- assets/charts.html | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/charts.html b/assets/charts.html index 6d287c4..00ecab8 100644 --- a/assets/charts.html +++ b/assets/charts.html @@ -202,6 +202,7 @@ annotations.forEach((annotation, index) => { chart.removeAnnotation(annotation['id']) }) + chart.clearAnnotations(); } function toggleDarkMode(){ From f7856203fff4e59bca4fe63118fa8dee13984d54 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Sat, 3 Apr 2021 01:41:07 +0200 Subject: [PATCH 073/117] Update Twitch.py --- TwitchChannelPointsMiner/classes/Twitch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TwitchChannelPointsMiner/classes/Twitch.py b/TwitchChannelPointsMiner/classes/Twitch.py index f05e327..788e7cd 100644 --- a/TwitchChannelPointsMiner/classes/Twitch.py +++ b/TwitchChannelPointsMiner/classes/Twitch.py @@ -494,7 +494,7 @@ class Twitch(object): def __get_campaigns_details(self, campaigns): result = [] - chunks = create_chunks(campaigns) + chunks = create_chunks(campaigns, 20) for chunk in chunks: json_data = [] for campaign in chunk: From 68e7ca5d3789b7bb9885f9e274c3e29427641943 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Sat, 10 Apr 2021 11:47:19 +0200 Subject: [PATCH 074/117] Improve error handling for claim drops. Drops already claimed or service error. #136 --- TwitchChannelPointsMiner/classes/Twitch.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/TwitchChannelPointsMiner/classes/Twitch.py b/TwitchChannelPointsMiner/classes/Twitch.py index f05e327..b71fac3 100644 --- a/TwitchChannelPointsMiner/classes/Twitch.py +++ b/TwitchChannelPointsMiner/classes/Twitch.py @@ -538,7 +538,21 @@ class Twitch(object): json_data["variables"] = {"input": {"dropInstanceID": drop.drop_instance_id}} response = self.post_gql_request(json_data) try: - return response["data"]["claimDropRewards"]["status"] == "ELIGIBLE_FOR_ALL" + # response["data"]["claimDropRewards"] can be null and respose["data"]["errors"] != [] + # or response["data"]["claimDropRewards"]["status"] === DROP_INSTANCE_ALREADY_CLAIMED + if ("claimDropRewards" in response["data"]) and ( + response["data"]["claimDropRewards"] is None + ): + return False + elif ("errors" in response["data"]) and (response["data"]["errors"] != []): + return False + elif ("claimDropRewards" in response["data"]) and ( + response["data"]["claimDropRewards"]["status"] + in ["ELIGIBLE_FOR_ALL", "DROP_INSTANCE_ALREADY_CLAIMED"] + ): + return True + else: + return False except (ValueError, KeyError): return False From e508d6aaa8fb9b3bd5428010e0db3fdc13058b1d Mon Sep 17 00:00:00 2001 From: Thomas Couchoud Date: Sat, 10 Apr 2021 11:53:45 +0200 Subject: [PATCH 075/117] Allow mods to place predictions --- .../TwitchChannelPointsMiner.py | 2 - .../classes/WebSocketsPool.py | 41 ++++++++----------- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py index 86f782e..f48d8ff 100644 --- a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py +++ b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py @@ -207,8 +207,6 @@ class TwitchChannelPointsMiner: self.twitch.load_channel_points_context(streamer) self.twitch.check_streamer_online(streamer) self.twitch.viewer_is_mod(streamer) - if streamer.viewer_is_mod is True: - streamer.settings.make_predictions = False self.original_streamers = [ streamer.channel_points for streamer in self.streamers diff --git a/TwitchChannelPointsMiner/classes/WebSocketsPool.py b/TwitchChannelPointsMiner/classes/WebSocketsPool.py index 00bf3e3..1bbf804 100644 --- a/TwitchChannelPointsMiner/classes/WebSocketsPool.py +++ b/TwitchChannelPointsMiner/classes/WebSocketsPool.py @@ -261,31 +261,26 @@ class WebSocketsPool: ws.streamers[streamer_index].is_online and event.closing_bet_after(current_tmsp) > 0 ): - if event.streamer.viewer_is_mod is True: - logger.info( - f"Sorry, you are moderator of {event.streamer}, so you can't bet!" - ) - else: - ws.events_predictions[event_id] = event - start_after = event.closing_bet_after( - current_tmsp - ) + ws.events_predictions[event_id] = event + start_after = event.closing_bet_after( + current_tmsp + ) - place_bet_thread = Timer( - start_after, - ws.twitch.make_predictions, - (ws.events_predictions[event_id],), - ) - place_bet_thread.daemon = True - place_bet_thread.start() + place_bet_thread = Timer( + start_after, + ws.twitch.make_predictions, + (ws.events_predictions[event_id],), + ) + place_bet_thread.daemon = True + place_bet_thread.start() - logger.info( - f"Place the bet after: {start_after}s for: {ws.events_predictions[event_id]}", - extra={ - "emoji": ":alarm_clock:", - "color": Settings.logger.color_palette.BET_START, - }, - ) + logger.info( + f"Place the bet after: {start_after}s for: {ws.events_predictions[event_id]}", + extra={ + "emoji": ":alarm_clock:", + "color": Settings.logger.color_palette.BET_START, + }, + ) elif ( message.type == "event-updated" From 4ef80a1de03b1145961b4735b678cf9e8f1a3842 Mon Sep 17 00:00:00 2001 From: Thomas Couchoud Date: Sat, 10 Apr 2021 11:58:28 +0200 Subject: [PATCH 076/117] Linter --- TwitchChannelPointsMiner/classes/WebSocketsPool.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/TwitchChannelPointsMiner/classes/WebSocketsPool.py b/TwitchChannelPointsMiner/classes/WebSocketsPool.py index 1bbf804..26d1c5f 100644 --- a/TwitchChannelPointsMiner/classes/WebSocketsPool.py +++ b/TwitchChannelPointsMiner/classes/WebSocketsPool.py @@ -262,9 +262,7 @@ class WebSocketsPool: and event.closing_bet_after(current_tmsp) > 0 ): ws.events_predictions[event_id] = event - start_after = event.closing_bet_after( - current_tmsp - ) + start_after = event.closing_bet_after(current_tmsp) place_bet_thread = Timer( start_after, From f3b6c733ba5357b0db4ef6a14e9924b12aecbc75 Mon Sep 17 00:00:00 2001 From: Thomas Couchoud Date: Sat, 10 Apr 2021 13:03:27 +0200 Subject: [PATCH 077/117] Do not update moderator status, we avoid sending a request per streamer --- TwitchChannelPointsMiner/TwitchChannelPointsMiner.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py index f48d8ff..ce1711f 100644 --- a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py +++ b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py @@ -201,12 +201,12 @@ class TwitchChannelPointsMiner: # Populate the streamers with default values. # 1. Load channel points and auto-claim bonus # 2. Check if streamers are online - # 3. Check if the user is a Streamer. In thi case you can't do prediction + # 3. DEACTIVATED: Check if the user is a moderator. (was used before the 5th of April 2021 to deactivate predictions) for streamer in self.streamers: time.sleep(random.uniform(0.3, 0.7)) self.twitch.load_channel_points_context(streamer) self.twitch.check_streamer_online(streamer) - self.twitch.viewer_is_mod(streamer) + # self.twitch.viewer_is_mod(streamer) self.original_streamers = [ streamer.channel_points for streamer in self.streamers From 861f7a9d7fd9175278d93ae07375227e91543b5f Mon Sep 17 00:00:00 2001 From: Artem <1v@users.noreply.github.com> Date: Sat, 1 May 2021 16:09:17 +0300 Subject: [PATCH 078/117] Duplicate line --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 588813e..c3060b5 100644 --- a/README.md +++ b/README.md @@ -339,7 +339,6 @@ ColorPalette( | `claim_drops` | bool | True | If this value is True, the script will increase the watch-time for the current game. With this, you can claim the drops from Twitch Inventory [#21](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/21) | | `watch_streak` | bool | True | Choose if you want to change a priority for these streamers and try to catch the Watch Streak event [#11](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/11) | | `bet` | BetSettings | | Rules to follow for the bet | -| `follow_raid` | bool | True | Choose if you want to follow raid +250 points | ### BetSettings | Key | Type | Default | Description | |-------------------- |----------------- |--------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | From c74f25044a575b37a90c187a7fef45741c176fe5 Mon Sep 17 00:00:00 2001 From: Thomas Couchoud Date: Fri, 7 May 2021 22:44:26 +0200 Subject: [PATCH 079/117] Prioritize streamer you're subscribed to (has points multiplier) --- README.md | 1 + TwitchChannelPointsMiner/classes/Settings.py | 1 + TwitchChannelPointsMiner/classes/Twitch.py | 8 ++++++++ TwitchChannelPointsMiner/classes/entities/Streamer.py | 2 ++ 4 files changed, 12 insertions(+) diff --git a/README.md b/README.md index c3060b5..9b4fb49 100644 --- a/README.md +++ b/README.md @@ -278,6 +278,7 @@ You can watch only two streamers per time. With `priority` settings, you can sel Available values are the following: - `STREAK` - Catch the watch streak from all streamers - `DROPS` - Claim all drops from streamers with drops tags enabled + - `SUBSCRIBED` - Prioritize streamers you're subscribed to - `ORDER` - Following the order of the list - `POINTS_ASCENDING` - On top the streamers with the lowest points - `POINTS_DESCEDING` - On top the streamers with the highest points diff --git a/TwitchChannelPointsMiner/classes/Settings.py b/TwitchChannelPointsMiner/classes/Settings.py index 05c6479..c4a550f 100644 --- a/TwitchChannelPointsMiner/classes/Settings.py +++ b/TwitchChannelPointsMiner/classes/Settings.py @@ -5,6 +5,7 @@ class Priority(Enum): ORDER = auto() STREAK = auto() DROPS = auto() + SUBSCRIBED = auto() POINTS_ASCENDING = auto() POINTS_DESCEDING = auto() diff --git a/TwitchChannelPointsMiner/classes/Twitch.py b/TwitchChannelPointsMiner/classes/Twitch.py index 9a7abd3..d9fdd1c 100644 --- a/TwitchChannelPointsMiner/classes/Twitch.py +++ b/TwitchChannelPointsMiner/classes/Twitch.py @@ -314,6 +314,13 @@ class Twitch(object): if len(streamers_watching) == 2: break + elif prior == Priority.SUBSCRIBED and len(streamers_watching) < 2: + for index in streamers_index: + if streamers[index].viewer_has_points_multiplier is True: + streamers_watching.append(index) + if len(streamers_watching) == 2: + break + """ Twitch has a limit - you can't watch more than 2 channels at one time. We take the first two streamers from the list as they have the highest priority (based on order or WatchStreak). @@ -386,6 +393,7 @@ class Twitch(object): channel = response["data"]["community"]["channel"] community_points = channel["self"]["communityPoints"] streamer.channel_points = community_points["balance"] + streamer.viewer_has_points_multiplier = len(community_points["activeMultipliers"]) > 0 if community_points["availableClaim"] is not None: self.claim_bonus(streamer, community_points["availableClaim"]["id"]) diff --git a/TwitchChannelPointsMiner/classes/entities/Streamer.py b/TwitchChannelPointsMiner/classes/entities/Streamer.py index 771420d..afd8dad 100644 --- a/TwitchChannelPointsMiner/classes/entities/Streamer.py +++ b/TwitchChannelPointsMiner/classes/entities/Streamer.py @@ -70,6 +70,7 @@ class Streamer(object): "channel_points", "minute_watched_requests", "viewer_is_mod", + "viewer_has_points_multiplier", "irc_chat", "stream", "raid", @@ -89,6 +90,7 @@ class Streamer(object): self.channel_points = 0 self.minute_watched_requests = None self.viewer_is_mod = False + self.viewer_has_points_multiplier = False self.irc_chat = None self.stream = Stream() From 9e09b0483a4c9d064f72114ad5abfe676bb33dfb Mon Sep 17 00:00:00 2001 From: Thomas Couchoud Date: Fri, 7 May 2021 23:20:23 +0200 Subject: [PATCH 080/117] Higher sub tiers first --- README.md | 2 +- TwitchChannelPointsMiner/classes/Twitch.py | 17 +++++++++++------ .../classes/entities/Streamer.py | 19 +++++++++++++++++-- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 9b4fb49..793368c 100644 --- a/README.md +++ b/README.md @@ -278,7 +278,7 @@ You can watch only two streamers per time. With `priority` settings, you can sel Available values are the following: - `STREAK` - Catch the watch streak from all streamers - `DROPS` - Claim all drops from streamers with drops tags enabled - - `SUBSCRIBED` - Prioritize streamers you're subscribed to + - `SUBSCRIBED` - Prioritize streamers you're subscribed to (higher subscription tiers are mined first) - `ORDER` - Following the order of the list - `POINTS_ASCENDING` - On top the streamers with the lowest points - `POINTS_DESCEDING` - On top the streamers with the highest points diff --git a/TwitchChannelPointsMiner/classes/Twitch.py b/TwitchChannelPointsMiner/classes/Twitch.py index d9fdd1c..d7ade43 100644 --- a/TwitchChannelPointsMiner/classes/Twitch.py +++ b/TwitchChannelPointsMiner/classes/Twitch.py @@ -315,11 +315,16 @@ class Twitch(object): break elif prior == Priority.SUBSCRIBED and len(streamers_watching) < 2: - for index in streamers_index: - if streamers[index].viewer_has_points_multiplier is True: - streamers_watching.append(index) - if len(streamers_watching) == 2: - break + streamers_with_multiplier = [ + index + for index in streamers_index + if streamers[index].viewer_has_points_multiplier() + ] + streamers_with_multiplier = sorted( + streamers_with_multiplier, + key=lambda x: streamers[x].total_points_multiplier(), + ) + streamers_watching += streamers_with_multiplier[:2] """ Twitch has a limit - you can't watch more than 2 channels at one time. @@ -393,7 +398,7 @@ class Twitch(object): channel = response["data"]["community"]["channel"] community_points = channel["self"]["communityPoints"] streamer.channel_points = community_points["balance"] - streamer.viewer_has_points_multiplier = len(community_points["activeMultipliers"]) > 0 + streamer.activeMultipliers = community_points["activeMultipliers"] if community_points["availableClaim"] is not None: self.claim_bonus(streamer, community_points["availableClaim"]["id"]) diff --git a/TwitchChannelPointsMiner/classes/entities/Streamer.py b/TwitchChannelPointsMiner/classes/entities/Streamer.py index afd8dad..7422e91 100644 --- a/TwitchChannelPointsMiner/classes/entities/Streamer.py +++ b/TwitchChannelPointsMiner/classes/entities/Streamer.py @@ -70,7 +70,7 @@ class Streamer(object): "channel_points", "minute_watched_requests", "viewer_is_mod", - "viewer_has_points_multiplier", + "activeMultipliers", "irc_chat", "stream", "raid", @@ -90,7 +90,7 @@ class Streamer(object): self.channel_points = 0 self.minute_watched_requests = None self.viewer_is_mod = False - self.viewer_has_points_multiplier = False + self.activeMultipliers = None self.irc_chat = None self.stream = Stream() @@ -170,6 +170,21 @@ class Streamer(object): and self.stream.campaigns_ids != [] ) + def viewer_has_points_multiplier(self): + return self.activeMultipliers is not None and len(self.activeMultipliers) > 0 + + def total_points_multiplier(self): + return ( + sum( + map( + lambda x: x["factor"], + self.activeMultipliers, + ), + ) + if self.activeMultipliers is not None + else 0 + ) + # === ANALYTICS === # def persistent_annotations(self, event_type, event_text): event_type = event_type.upper() From 902ca57857388d46cf21064a5fe9ac50ab44a217 Mon Sep 17 00:00:00 2001 From: Thomas Couchoud Date: Fri, 7 May 2021 23:33:29 +0200 Subject: [PATCH 081/117] Actually get higher multipliers first, not the lowest --- TwitchChannelPointsMiner/classes/Twitch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/TwitchChannelPointsMiner/classes/Twitch.py b/TwitchChannelPointsMiner/classes/Twitch.py index d7ade43..945b855 100644 --- a/TwitchChannelPointsMiner/classes/Twitch.py +++ b/TwitchChannelPointsMiner/classes/Twitch.py @@ -323,6 +323,7 @@ class Twitch(object): streamers_with_multiplier = sorted( streamers_with_multiplier, key=lambda x: streamers[x].total_points_multiplier(), + reverse=True, ) streamers_watching += streamers_with_multiplier[:2] From 3c98fee4a4bb04879af1eeb29e95345458ec9538 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Sun, 23 May 2021 15:21:36 +0200 Subject: [PATCH 082/117] Remove blacklist username of streamer from streamers_name and check if username It's also in streamers_name. Fix #176 --- .../TwitchChannelPointsMiner.py | 52 ++++++++++--------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py index ce1711f..066bf4e 100644 --- a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py +++ b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py @@ -165,38 +165,40 @@ class TwitchChannelPointsMiner: streamers_name = list( OrderedDict.fromkeys(streamers_name + followers_array) ) + streamers_name = [name for name in streamers_name if name not in blacklist] logger.info( f"Loading data for {len(streamers_name)} streamers. Please wait...", extra={"emoji": ":nerd_face:"}, ) for username in streamers_name: - time.sleep(random.uniform(0.3, 0.7)) - try: - streamer = ( - streamers_dict[username] - if isinstance(streamers_dict[username], Streamer) is True - else Streamer(username) - ) - streamer.channel_id = self.twitch.get_channel_id(username) - streamer.settings = set_default_settings( - streamer.settings, Settings.streamer_settings - ) - streamer.settings.bet = set_default_settings( - streamer.settings.bet, Settings.streamer_settings.bet - ) - if streamer.settings.join_chat is True: - streamer.irc_chat = ThreadChat( - self.username, - self.twitch.twitch_login.get_auth_token(), - streamer.username, + if username in streamers_dict: + time.sleep(random.uniform(0.3, 0.7)) + try: + streamer = ( + streamers_dict[username] + if isinstance(streamers_dict[username], Streamer) is True + else Streamer(username) + ) + streamer.channel_id = self.twitch.get_channel_id(username) + streamer.settings = set_default_settings( + streamer.settings, Settings.streamer_settings + ) + streamer.settings.bet = set_default_settings( + streamer.settings.bet, Settings.streamer_settings.bet + ) + if streamer.settings.join_chat is True: + streamer.irc_chat = ThreadChat( + self.username, + self.twitch.twitch_login.get_auth_token(), + streamer.username, + ) + self.streamers.append(streamer) + except StreamerDoesNotExistException: + logger.info( + f"Streamer {username} does not exist", + extra={"emoji": ":cry:"}, ) - self.streamers.append(streamer) - except StreamerDoesNotExistException: - logger.info( - f"Streamer {username} does not exist", - extra={"emoji": ":cry:"}, - ) # Populate the streamers with default values. # 1. Load channel points and auto-claim bonus From dad0dc2cb2535ce0d33acbe61e0c79ec03ffdde7 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Sun, 23 May 2021 15:22:06 +0200 Subject: [PATCH 083/117] Remove blacklist username of streamer from streamers_name and check if username It's also in streamers_name. Fix #176 --- TwitchChannelPointsMiner/TwitchChannelPointsMiner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py index 066bf4e..a97048e 100644 --- a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py +++ b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py @@ -172,7 +172,7 @@ class TwitchChannelPointsMiner: extra={"emoji": ":nerd_face:"}, ) for username in streamers_name: - if username in streamers_dict: + if username in streamers_name: time.sleep(random.uniform(0.3, 0.7)) try: streamer = ( From b5d0cd2962436224127c108c940f7eb2b8517fe4 Mon Sep 17 00:00:00 2001 From: Thomas Couchoud Date: Fri, 28 May 2021 20:42:30 +0200 Subject: [PATCH 084/117] Add missing parameters from on_close --- TwitchChannelPointsMiner/classes/WebSocketsPool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TwitchChannelPointsMiner/classes/WebSocketsPool.py b/TwitchChannelPointsMiner/classes/WebSocketsPool.py index 26d1c5f..ea5de39 100644 --- a/TwitchChannelPointsMiner/classes/WebSocketsPool.py +++ b/TwitchChannelPointsMiner/classes/WebSocketsPool.py @@ -110,7 +110,7 @@ class WebSocketsPool: logger.error(f"#{ws.index} - WebSocket error: {error}") @staticmethod - def on_close(ws): + def on_close(ws, close_status_code, close_reason): logger.info(f"#{ws.index} - WebSocket closed") # On close please reconnect automatically WebSocketsPool.handle_reconnection(ws) From f34cd47ceaf7e79f34b23a9f10520c9f16c4abaa Mon Sep 17 00:00:00 2001 From: Thomas Couchoud Date: Fri, 28 May 2021 20:52:10 +0200 Subject: [PATCH 085/117] Fixed requirements --- requirements.txt | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/requirements.txt b/requirements.txt index 6f2cbbb..0b1e260 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,11 @@ -requests -websocket-client -browser_cookie3 -pillow -python-dateutil -emoji -millify -pre-commit -colorama -flask -irc \ No newline at end of file +requests==2.25.1 +websocket-client==1.0.1 +browser_cookie3==0.12.1 +pillow==8.2.0 +python-dateutil==2.8.1 +emoji==1.2.0 +millify==0.1.1 +pre-commit==2.13.0 +colorama==0.4.4 +flask==2.0.1 +irc==19.0.1 \ No newline at end of file From e5ed951d1aae3451aae00ef3963795b04081450b Mon Sep 17 00:00:00 2001 From: Thomas Couchoud Date: Wed, 9 Jun 2021 20:51:51 +0200 Subject: [PATCH 086/117] Do not iterate over all followed streamers as they can be blacklisted --- TwitchChannelPointsMiner/TwitchChannelPointsMiner.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py index a97048e..633820f 100644 --- a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py +++ b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py @@ -158,14 +158,8 @@ class TwitchChannelPointsMiner: ) for username in followers_array: if username not in streamers_dict and username not in blacklist: + streamers_name.append(username) streamers_dict[username] = username.lower().strip() - else: - followers_array = [] - - streamers_name = list( - OrderedDict.fromkeys(streamers_name + followers_array) - ) - streamers_name = [name for name in streamers_name if name not in blacklist] logger.info( f"Loading data for {len(streamers_name)} streamers. Please wait...", From 85362f6adb18cf6e6144480094d6f84d5d379d77 Mon Sep 17 00:00:00 2001 From: Thomas Couchoud Date: Wed, 9 Jun 2021 20:56:30 +0200 Subject: [PATCH 087/117] Remove unused import --- TwitchChannelPointsMiner/TwitchChannelPointsMiner.py | 1 - 1 file changed, 1 deletion(-) diff --git a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py index 633820f..7b44c89 100644 --- a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py +++ b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py @@ -8,7 +8,6 @@ import sys import threading import time import uuid -from collections import OrderedDict from datetime import datetime from pathlib import Path From a036471415cb2708c88c73f7355bf9d35531f09f Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Wed, 23 Jun 2021 11:31:26 +0200 Subject: [PATCH 088/117] Update stale.yml --- .github/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/stale.yml b/.github/stale.yml index 8a3287d..073f80c 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,5 +1,5 @@ # Number of days of inactivity before an issue becomes stale -daysUntilStale: 60 +daysUntilStale: 150 # Number of days of inactivity before a stale issue is closed daysUntilClose: 7 From b07420443bc303bc1454e5631abfd8b23cd4cdd8 Mon Sep 17 00:00:00 2001 From: Thomas Couchoud Date: Sat, 26 Jun 2021 20:49:15 +0200 Subject: [PATCH 089/117] Custom bet delay --- README.md | 20 +++++++++++++++++-- .../classes/WebSocketsPool.py | 2 +- .../classes/entities/Bet.py | 17 ++++++++++++++++ .../classes/entities/Streamer.py | 14 ++++++++++++- 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 793368c..779265f 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ from colorama import Fore from TwitchChannelPointsMiner import TwitchChannelPointsMiner from TwitchChannelPointsMiner.logger import LoggerSettings, ColorPalette from TwitchChannelPointsMiner.classes.Settings import Priority -from TwitchChannelPointsMiner.classes.entities.Bet import Strategy, BetSettings, Condition, OutcomeKeys, FilterCondition +from TwitchChannelPointsMiner.classes.entities.Bet import Strategy, BetSettings, Condition, OutcomeKeys, FilterCondition, DelayMode from TwitchChannelPointsMiner.classes.entities.Streamer import Streamer, StreamerSettings twitch_miner = TwitchChannelPointsMiner( @@ -217,7 +217,9 @@ twitch_miner = TwitchChannelPointsMiner( percentage_gap=20, # Gap difference between outcomesA and outcomesB (for SMART stragegy) max_points=50000, # If the x percentage of your channel points is gt bet_max_points set this value stealth_mode=True, # If the calculated amount of channel points is GT the highest bet, place the highest value minus 1-2 points #33 - filter_condition=FilterCondition( + delay_mode=DelayMode.FROM_END, # When placing a bet, we will wait until `delay` seconds before the end of the timer + delay=6, + filter_condition=FilterCondition( by=OutcomeKeys.TOTAL_USERS, # Where apply the filter. Allowed [PERCENTAGE_USERS, ODDS_PERCENTAGE, ODDS, TOP_POINTS, TOTAL_USERS, TOTAL_POINTS] where=Condition.LTE, # 'by' must be [GT, LT, GTE, LTE] than value value=800 @@ -349,6 +351,8 @@ ColorPalette( | `max_points` | int | 50000 | If the x percentage of your channel points is GT bet_max_points set this value | | `stealth_mode` | bool | False | If the calculated amount of channel points is GT the highest bet, place the highest value minus 1-2 points [#33](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/33) | | `join_chat` | bool | True | Join IRC-Chat to appear online in chat and attempt to get StreamElements channel points and increase view-time [#47](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/47) | +| `delay_mode` | DelayMode | FROM_END | Define how is calculating the waiting time before placing a bet | +| `delay` | float | 6 | Value to be used to calculate bet delay depending on `delay_mode` value | #### Bet strategy @@ -394,6 +398,18 @@ Allowed values for `where` are: `GT, LT, GTE, LTE` - If you want to place the bet ONLY if the highest bet is lower than 2000 `FilterCondition(by=OutcomeKeys.TOP_POINTS, where=Condition.LT, value=2000)` +### DelayMode + +- **FROM_START**: Will wait `delay` seconds from when the bet was opened +- **FROM_END**: Will until there is `delay` seconds left to place the bet +- **PERCENTAGE**: Will place the bet when `delay` percent of the set timer is elapsed + +Here a concrete example. Let's suppose we have a bet that is opened with a timer of 10 minutes: + +- **FROM_START** with `delay=20`: The bet will be placed 20s after the bet is opened +- **FROM_END** with `delay=20`: The bet will be placed 20s before the end of the bet (so 9mins 40s after the bet is opened) +- **PERCENTAGE** with `delay=0.2`: The bet will be placed when the timer went down by 20% (so 2mins after the bet is opened) + ## Analytics We have recently introduced a little frontend where you can show with a chart you points trend. The script will spawn a Flask web-server on your machine where you can select binding address and port. The chart provides some annotation to handle the prediction and watch strike events. Usually annotation are used to notice big increase / decrease of points. If you want to can disable annotations. diff --git a/TwitchChannelPointsMiner/classes/WebSocketsPool.py b/TwitchChannelPointsMiner/classes/WebSocketsPool.py index ea5de39..0641c12 100644 --- a/TwitchChannelPointsMiner/classes/WebSocketsPool.py +++ b/TwitchChannelPointsMiner/classes/WebSocketsPool.py @@ -247,7 +247,7 @@ class WebSocketsPool: event_dict["prediction_window_seconds"] ) # Reduce prediction window by 3/6s - Collect more accurate data for decision - prediction_window_seconds -= random.uniform(3, 6) + prediction_window_seconds = ws.streamers[streamer_index].get_prediction_window(prediction_window_seconds) event = EventPrediction( ws.streamers[streamer_index], event_id, diff --git a/TwitchChannelPointsMiner/classes/entities/Bet.py b/TwitchChannelPointsMiner/classes/entities/Bet.py index cffde0d..b43b716 100644 --- a/TwitchChannelPointsMiner/classes/entities/Bet.py +++ b/TwitchChannelPointsMiner/classes/entities/Bet.py @@ -41,6 +41,15 @@ class OutcomeKeys(object): DECISION_POINTS = "decision_points" +class DelayMode(Enum): + FROM_START = auto() + FROM_END = auto() + PERCENTAGE = auto() + + def __str__(self): + return self.name + + class FilterCondition(object): __slots__ = [ "by", @@ -65,6 +74,8 @@ class BetSettings(object): "max_points", "stealth_mode", "filter_condition", + "delay", + "delay_mode" ] def __init__( @@ -75,6 +86,8 @@ class BetSettings(object): max_points: int = None, stealth_mode: bool = None, filter_condition: FilterCondition = None, + delay: float = None, + delay_mode: DelayMode = None ): self.strategy = strategy self.percentage = percentage @@ -82,6 +95,8 @@ class BetSettings(object): self.max_points = max_points self.stealth_mode = stealth_mode self.filter_condition = filter_condition + self.delay = delay + self.delay_mode = delay_mode def default(self): self.strategy = self.strategy if not None else Strategy.SMART @@ -89,6 +104,8 @@ class BetSettings(object): self.percentage_gap = self.percentage_gap if not None else 20 self.max_points = self.max_points if not None else 50000 self.stealth_mode = self.stealth_mode if not None else False + self.delay = self.delay if not None else 6 + self.delay_mode = self.delay_mode if not None else DelayMode.FROM_END def __repr__(self): return f"BetSettings(strategy={self.strategy}, percentage={self.percentage}, percentage_gap={self.percentage_gap}, max_points={self.max_points}, stealth_mode={self.stealth_mode})" diff --git a/TwitchChannelPointsMiner/classes/entities/Streamer.py b/TwitchChannelPointsMiner/classes/entities/Streamer.py index 7422e91..b47596a 100644 --- a/TwitchChannelPointsMiner/classes/entities/Streamer.py +++ b/TwitchChannelPointsMiner/classes/entities/Streamer.py @@ -6,7 +6,7 @@ from datetime import datetime from threading import Lock from TwitchChannelPointsMiner.classes.Chat import ThreadChat -from TwitchChannelPointsMiner.classes.entities.Bet import BetSettings +from TwitchChannelPointsMiner.classes.entities.Bet import BetSettings, DelayMode from TwitchChannelPointsMiner.classes.entities.Stream import Stream from TwitchChannelPointsMiner.classes.Settings import Settings from TwitchChannelPointsMiner.constants import URL @@ -185,6 +185,18 @@ class Streamer(object): else 0 ) + def get_prediction_window(self, prediction_window_seconds): + delay_mode = self.settings.bet.delay_mode + delay = self.settings.bet.delay + if delay_mode == DelayMode.FROM_START: + return delay + elif delay_mode == DelayMode.FROM_END: + return prediction_window_seconds - delay + elif delay_mode == DelayMode.PERCENTAGE: + return prediction_window_seconds * delay + else: + return prediction_window_seconds + # === ANALYTICS === # def persistent_annotations(self, event_type, event_text): event_type = event_type.upper() From 511671075d793c56029084d6309f63265b641b66 Mon Sep 17 00:00:00 2001 From: Thomas Couchoud Date: Sat, 26 Jun 2021 20:59:08 +0200 Subject: [PATCH 090/117] Make the linter happy --- TwitchChannelPointsMiner/classes/WebSocketsPool.py | 4 +++- TwitchChannelPointsMiner/classes/entities/Bet.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/TwitchChannelPointsMiner/classes/WebSocketsPool.py b/TwitchChannelPointsMiner/classes/WebSocketsPool.py index 0641c12..7047991 100644 --- a/TwitchChannelPointsMiner/classes/WebSocketsPool.py +++ b/TwitchChannelPointsMiner/classes/WebSocketsPool.py @@ -247,7 +247,9 @@ class WebSocketsPool: event_dict["prediction_window_seconds"] ) # Reduce prediction window by 3/6s - Collect more accurate data for decision - prediction_window_seconds = ws.streamers[streamer_index].get_prediction_window(prediction_window_seconds) + prediction_window_seconds = ws.streamers[ + streamer_index + ].get_prediction_window(prediction_window_seconds) event = EventPrediction( ws.streamers[streamer_index], event_id, diff --git a/TwitchChannelPointsMiner/classes/entities/Bet.py b/TwitchChannelPointsMiner/classes/entities/Bet.py index b43b716..a1dc0c9 100644 --- a/TwitchChannelPointsMiner/classes/entities/Bet.py +++ b/TwitchChannelPointsMiner/classes/entities/Bet.py @@ -75,7 +75,7 @@ class BetSettings(object): "stealth_mode", "filter_condition", "delay", - "delay_mode" + "delay_mode", ] def __init__( @@ -87,7 +87,7 @@ class BetSettings(object): stealth_mode: bool = None, filter_condition: FilterCondition = None, delay: float = None, - delay_mode: DelayMode = None + delay_mode: DelayMode = None, ): self.strategy = strategy self.percentage = percentage From 60a108857be89af172a6528fe74f71f4d4eff5fa Mon Sep 17 00:00:00 2001 From: Thomas Couchoud Date: Sat, 26 Jun 2021 20:59:25 +0200 Subject: [PATCH 091/117] Typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 779265f..a914b82 100644 --- a/README.md +++ b/README.md @@ -404,7 +404,7 @@ Allowed values for `where` are: `GT, LT, GTE, LTE` - **FROM_END**: Will until there is `delay` seconds left to place the bet - **PERCENTAGE**: Will place the bet when `delay` percent of the set timer is elapsed -Here a concrete example. Let's suppose we have a bet that is opened with a timer of 10 minutes: +Here's a concrete example. Let's suppose we have a bet that is opened with a timer of 10 minutes: - **FROM_START** with `delay=20`: The bet will be placed 20s after the bet is opened - **FROM_END** with `delay=20`: The bet will be placed 20s before the end of the bet (so 9mins 40s after the bet is opened) From 2a6e81934c182682e52834c495741042173f82d7 Mon Sep 17 00:00:00 2001 From: Thomas Couchoud Date: Mon, 28 Jun 2021 17:56:38 +0200 Subject: [PATCH 092/117] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a914b82..4ba7c93 100644 --- a/README.md +++ b/README.md @@ -350,9 +350,9 @@ ColorPalette( | `percentage_gap` | int | 20 | Gap difference between outcomesA and outcomesB (for SMART stragegy) | | `max_points` | int | 50000 | If the x percentage of your channel points is GT bet_max_points set this value | | `stealth_mode` | bool | False | If the calculated amount of channel points is GT the highest bet, place the highest value minus 1-2 points [#33](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/33) | -| `join_chat` | bool | True | Join IRC-Chat to appear online in chat and attempt to get StreamElements channel points and increase view-time [#47](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/47) | -| `delay_mode` | DelayMode | FROM_END | Define how is calculating the waiting time before placing a bet | -| `delay` | float | 6 | Value to be used to calculate bet delay depending on `delay_mode` value | +| `join_chat` | bool | True | Join IRC-Chat to appear online in chat and attempt to get StreamElements channel points and increase view-time [#47](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/47) | +| `delay_mode` | DelayMode | FROM_END | Define how is calculating the waiting time before placing a bet | +| `delay` | float | 6 | Value to be used to calculate bet delay depending on `delay_mode` value | #### Bet strategy From aea617fce2a8deb4298ed35ce0a814f7edadfa29 Mon Sep 17 00:00:00 2001 From: Thomas Couchoud Date: Mon, 19 Jul 2021 23:06:41 +0200 Subject: [PATCH 093/117] Improve delay FROM_START by not going over prediction_window_seconds Improve delay FROM_END by not going over below 0 --- TwitchChannelPointsMiner/classes/entities/Streamer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TwitchChannelPointsMiner/classes/entities/Streamer.py b/TwitchChannelPointsMiner/classes/entities/Streamer.py index b47596a..c01cd79 100644 --- a/TwitchChannelPointsMiner/classes/entities/Streamer.py +++ b/TwitchChannelPointsMiner/classes/entities/Streamer.py @@ -189,9 +189,9 @@ class Streamer(object): delay_mode = self.settings.bet.delay_mode delay = self.settings.bet.delay if delay_mode == DelayMode.FROM_START: - return delay + return min(delay, prediction_window_seconds) elif delay_mode == DelayMode.FROM_END: - return prediction_window_seconds - delay + return max(prediction_window_seconds - delay, 0) elif delay_mode == DelayMode.PERCENTAGE: return prediction_window_seconds * delay else: From b874d95ec323549abcf6ee3dbad17135b4e75b30 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Fri, 30 Jul 2021 21:52:48 +0200 Subject: [PATCH 094/117] Use TimedRotatingFileHandler instead of FileHandler. Close #215 --- TwitchChannelPointsMiner/logger.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/TwitchChannelPointsMiner/logger.py b/TwitchChannelPointsMiner/logger.py index 619be5d..895e3fe 100644 --- a/TwitchChannelPointsMiner/logger.py +++ b/TwitchChannelPointsMiner/logger.py @@ -1,7 +1,7 @@ import logging import os import platform -from datetime import datetime +from logging.handlers import TimedRotatingFileHandler from pathlib import Path import emoji @@ -144,9 +144,16 @@ def configure_loggers(username, settings): Path(logs_path).mkdir(parents=True, exist_ok=True) logs_file = os.path.join( logs_path, - f"{username}.{datetime.now().strftime('%Y%m%d-%H%M%S')}.log", + f"{username}.log", + ) + file_handler = TimedRotatingFileHandler( + logs_file, + when="D", + interval=1, + backupCount=7, + encoding="utf-8", + delay=False, ) - file_handler = logging.FileHandler(logs_file, "w", "utf-8") file_handler.setFormatter( logging.Formatter( fmt="%(asctime)s - %(levelname)s - %(name)s - [%(funcName)s]: %(message)s", From fcb8de55223983a16915ac66b3dd0def79feb5e6 Mon Sep 17 00:00:00 2001 From: Thomas Couchoud Date: Sat, 14 Aug 2021 11:54:55 +0200 Subject: [PATCH 095/117] Display reason why placing bet failed. For example if TOS were not accepted it'll display: "Failed to place bet, error: MUST_ACCEPT_TOS" --- TwitchChannelPointsMiner/classes/Twitch.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/TwitchChannelPointsMiner/classes/Twitch.py b/TwitchChannelPointsMiner/classes/Twitch.py index 945b855..6cbf1ea 100644 --- a/TwitchChannelPointsMiner/classes/Twitch.py +++ b/TwitchChannelPointsMiner/classes/Twitch.py @@ -451,7 +451,20 @@ class Twitch(object): "transactionID": token_hex(16), } } - return self.post_gql_request(json_data) + response = self.post_gql_request(json_data) + if ( + "data" in response and + "makePrediction" in response["data"] and + "error" in response["data"]["makePrediction"] + ): + error_code = response["data"]["makePrediction"]["code"] + logger.error( + f"Failed to place bet, error: {error_code}", + extra={ + "emoji": ":four_leaf_clover:", + "color": Settings.logger.color_palette.BET_FAILED, + }, + ) else: logger.info( f"Oh no! The event is not active anymore! Current status: {event.status}", From 2ae92bf9a2740accb514e1aa15331aa7c09594d2 Mon Sep 17 00:00:00 2001 From: Thomas Couchoud Date: Sat, 14 Aug 2021 12:00:28 +0200 Subject: [PATCH 096/117] lint --- TwitchChannelPointsMiner/classes/Twitch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TwitchChannelPointsMiner/classes/Twitch.py b/TwitchChannelPointsMiner/classes/Twitch.py index 6cbf1ea..cc610f5 100644 --- a/TwitchChannelPointsMiner/classes/Twitch.py +++ b/TwitchChannelPointsMiner/classes/Twitch.py @@ -453,9 +453,9 @@ class Twitch(object): } response = self.post_gql_request(json_data) if ( - "data" in response and - "makePrediction" in response["data"] and - "error" in response["data"]["makePrediction"] + "data" in response and + "makePrediction" in response["data"] and + "error" in response["data"]["makePrediction"] ): error_code = response["data"]["makePrediction"]["code"] logger.error( From e2628d66b49f4616c06a3c58f98f0bf48838972e Mon Sep 17 00:00:00 2001 From: Thomas Couchoud Date: Sat, 14 Aug 2021 12:01:22 +0200 Subject: [PATCH 097/117] lint --- TwitchChannelPointsMiner/classes/Twitch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TwitchChannelPointsMiner/classes/Twitch.py b/TwitchChannelPointsMiner/classes/Twitch.py index cc610f5..1056bf6 100644 --- a/TwitchChannelPointsMiner/classes/Twitch.py +++ b/TwitchChannelPointsMiner/classes/Twitch.py @@ -453,9 +453,9 @@ class Twitch(object): } response = self.post_gql_request(json_data) if ( - "data" in response and - "makePrediction" in response["data"] and - "error" in response["data"]["makePrediction"] + "data" in response + and "makePrediction" in response["data"] + and "error" in response["data"]["makePrediction"] ): error_code = response["data"]["makePrediction"]["code"] logger.error( From 0f56501aa28b22a912fb3b03dcad29843737df63 Mon Sep 17 00:00:00 2001 From: Thomas Couchoud Date: Sat, 14 Aug 2021 13:30:08 +0200 Subject: [PATCH 098/117] select correct field --- TwitchChannelPointsMiner/classes/Twitch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TwitchChannelPointsMiner/classes/Twitch.py b/TwitchChannelPointsMiner/classes/Twitch.py index 1056bf6..adc879a 100644 --- a/TwitchChannelPointsMiner/classes/Twitch.py +++ b/TwitchChannelPointsMiner/classes/Twitch.py @@ -457,7 +457,7 @@ class Twitch(object): and "makePrediction" in response["data"] and "error" in response["data"]["makePrediction"] ): - error_code = response["data"]["makePrediction"]["code"] + error_code = response["data"]["makePrediction"]["error"]["code"] logger.error( f"Failed to place bet, error: {error_code}", extra={ From 7dc1677591df3d611ab172edca1b4b61e309cba8 Mon Sep 17 00:00:00 2001 From: Thomas Couchoud Date: Sat, 14 Aug 2021 14:09:27 +0200 Subject: [PATCH 099/117] Check error is not none --- TwitchChannelPointsMiner/classes/Twitch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/TwitchChannelPointsMiner/classes/Twitch.py b/TwitchChannelPointsMiner/classes/Twitch.py index adc879a..cb47874 100644 --- a/TwitchChannelPointsMiner/classes/Twitch.py +++ b/TwitchChannelPointsMiner/classes/Twitch.py @@ -456,6 +456,7 @@ class Twitch(object): "data" in response and "makePrediction" in response["data"] and "error" in response["data"]["makePrediction"] + and response["data"]["makePrediction"]["error"] is not None ): error_code = response["data"]["makePrediction"]["error"]["code"] logger.error( From 1c507a0996b8eaf448906c01b8ab9e95560efa98 Mon Sep 17 00:00:00 2001 From: Thomas Couchoud Date: Sat, 14 Aug 2021 15:31:58 +0200 Subject: [PATCH 100/117] Log when bet isn't placed because amount is too less --- TwitchChannelPointsMiner/classes/Twitch.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/TwitchChannelPointsMiner/classes/Twitch.py b/TwitchChannelPointsMiner/classes/Twitch.py index cb47874..23679fb 100644 --- a/TwitchChannelPointsMiner/classes/Twitch.py +++ b/TwitchChannelPointsMiner/classes/Twitch.py @@ -466,6 +466,14 @@ class Twitch(object): "color": Settings.logger.color_palette.BET_FAILED, }, ) + else: + logger.info( + f"Bet won't be placed as the amount {_millify(decision['amount'])} is less than the minimum required 10", + extra={ + "emoji": ":four_leaf_clover:", + "color": Settings.logger.color_palette.BET_GENERAL, + }, + ) else: logger.info( f"Oh no! The event is not active anymore! Current status: {event.status}", From 7db26bc9ebc6c0efa14c3942131f09bff2a3cfd4 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Mon, 16 Aug 2021 22:02:29 +0200 Subject: [PATCH 101/117] Update README and example --- README.md | 13 +++++++------ example.py | 11 +++++++---- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 4ba7c93..1fcec52 100644 --- a/README.md +++ b/README.md @@ -216,12 +216,13 @@ twitch_miner = TwitchChannelPointsMiner( percentage=5, # Place the x% of your channel points percentage_gap=20, # Gap difference between outcomesA and outcomesB (for SMART stragegy) max_points=50000, # If the x percentage of your channel points is gt bet_max_points set this value - stealth_mode=True, # If the calculated amount of channel points is GT the highest bet, place the highest value minus 1-2 points #33 - delay_mode=DelayMode.FROM_END, # When placing a bet, we will wait until `delay` seconds before the end of the timer - delay=6, - filter_condition=FilterCondition( - by=OutcomeKeys.TOTAL_USERS, # Where apply the filter. Allowed [PERCENTAGE_USERS, ODDS_PERCENTAGE, ODDS, TOP_POINTS, TOTAL_USERS, TOTAL_POINTS] - where=Condition.LTE, # 'by' must be [GT, LT, GTE, LTE] than value + stealth_mode=True, # If the calculated amount of channel points is GT the highest bet, place the highest value minus 1-2 points Issue #33 + delay_mode=DelayMode.FROM_END, # When placing a bet, we will wait until `delay` seconds before the end of the timer + delay=6, + minimum_points=2000, # Place the bet only if we have at least 20k points. Issue #113 + filter_condition=FilterCondition( + by=OutcomeKeys.TOTAL_USERS, # Where apply the filter. Allowed [PERCENTAGE_USERS, ODDS_PERCENTAGE, ODDS, TOP_POINTS, TOTAL_USERS, TOTAL_POINTS] + where=Condition.LTE, # 'by' must be [GT, LT, GTE, LTE] than value value=800 ) ) diff --git a/example.py b/example.py index a8eb20f..5395516 100644 --- a/example.py +++ b/example.py @@ -5,7 +5,7 @@ from colorama import Fore from TwitchChannelPointsMiner import TwitchChannelPointsMiner from TwitchChannelPointsMiner.logger import LoggerSettings, ColorPalette from TwitchChannelPointsMiner.classes.Settings import Priority -from TwitchChannelPointsMiner.classes.entities.Bet import Strategy, BetSettings, Condition, OutcomeKeys, FilterCondition +from TwitchChannelPointsMiner.classes.entities.Bet import Strategy, BetSettings, Condition, OutcomeKeys, FilterCondition, DelayMode from TwitchChannelPointsMiner.classes.entities.Streamer import Streamer, StreamerSettings twitch_miner = TwitchChannelPointsMiner( @@ -41,10 +41,13 @@ twitch_miner = TwitchChannelPointsMiner( percentage=5, # Place the x% of your channel points percentage_gap=20, # Gap difference between outcomesA and outcomesB (for SMART stragegy) max_points=50000, # If the x percentage of your channel points is gt bet_max_points set this value - stealth_mode=True, # If the calculated amount of channel points is GT the highest bet, place the highest value minus 1-2 points #33 + stealth_mode=True, # If the calculated amount of channel points is GT the highest bet, place the highest value minus 1-2 points Issue #33 + delay_mode=DelayMode.FROM_END, # When placing a bet, we will wait until `delay` seconds before the end of the timer + delay=6, + minimum_points=2000, # Place the bet only if we have at least 20k points. Issue #113 filter_condition=FilterCondition( - by=OutcomeKeys.TOTAL_USERS, # Where apply the filter. Allowed [PERCENTAGE_USERS, ODDS_PERCENTAGE, ODDS, TOP_POINTS, TOTAL_USERS, TOTAL_POINTS] - where=Condition.LTE, # 'by' must be [GT, LT, GTE, LTE] than value + by=OutcomeKeys.TOTAL_USERS, # Where apply the filter. Allowed [PERCENTAGE_USERS, ODDS_PERCENTAGE, ODDS, TOP_POINTS, TOTAL_USERS, TOTAL_POINTS] + where=Condition.LTE, # 'by' must be [GT, LT, GTE, LTE] than value value=800 ) ) From a6a8934bb6314ebe34c6c992dc59d3dd939c11c4 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Sat, 21 Aug 2021 10:45:27 +0200 Subject: [PATCH 102/117] Update __init__.py --- TwitchChannelPointsMiner/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TwitchChannelPointsMiner/__init__.py b/TwitchChannelPointsMiner/__init__.py index 4c150ca..f581ca0 100644 --- a/TwitchChannelPointsMiner/__init__.py +++ b/TwitchChannelPointsMiner/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -__version__ = "2.7.1" +__version__ = "2.7.2" from .TwitchChannelPointsMiner import TwitchChannelPointsMiner __all__ = [ From 76679e9ed325f34215b5d8ffa658d585a845c06c Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Sat, 21 Aug 2021 10:45:55 +0200 Subject: [PATCH 103/117] Update TwitchLogin.py --- TwitchChannelPointsMiner/classes/TwitchLogin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TwitchChannelPointsMiner/classes/TwitchLogin.py b/TwitchChannelPointsMiner/classes/TwitchLogin.py index 3ad8e09..95f46a6 100644 --- a/TwitchChannelPointsMiner/classes/TwitchLogin.py +++ b/TwitchChannelPointsMiner/classes/TwitchLogin.py @@ -19,7 +19,7 @@ logger = logging.getLogger(__name__) class TwitchLogin(object): - __self__ = [ + __slots__ = [ "client_id", "token", "login_check_result", From 0bcd1ac0a05815e4baf947e67038d07921a964bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Sep 2021 02:53:27 +0000 Subject: [PATCH 104/117] Bump pillow from 8.2.0 to 8.3.2 Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.2.0 to 8.3.2. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/8.2.0...8.3.2) --- updated-dependencies: - dependency-name: pillow dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0b1e260..6423ea2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ requests==2.25.1 websocket-client==1.0.1 browser_cookie3==0.12.1 -pillow==8.2.0 +pillow==8.3.2 python-dateutil==2.8.1 emoji==1.2.0 millify==0.1.1 From b3dcb811ca5ac43e61cbfa0e09b530b74f582b1c Mon Sep 17 00:00:00 2001 From: Aaron Rice Date: Mon, 20 Sep 2021 17:36:04 -0700 Subject: [PATCH 105/117] Fix typos in example.py --- example.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/example.py b/example.py index 5395516..005526e 100644 --- a/example.py +++ b/example.py @@ -15,11 +15,11 @@ twitch_miner = TwitchChannelPointsMiner( priority=[ # Custom priority in this case for example: Priority.STREAK, # - We want first of all to catch all watch streak from all streamers Priority.DROPS, # - When we don't have anymore watch streak to catch, wait until all drops are collected over the streamers - Priority.ORDER # - When we have all of the drops claimed and no watch-streak available, use the order priority (POINTS_ASCENDING, POINTS_DESCEDING) + Priority.ORDER # - When we have all of the drops claimed and no watch-streak available, use the order priority (POINTS_ASCENDING, POINTS_DESCEDING) ], logger_settings=LoggerSettings( save=True, # If you want to save logs in a file (suggested) - console_level=logging.INFO, # Level of logs - use logging.DEBUG for more info) + console_level=logging.INFO, # Level of logs - use logging.DEBUG for more info file_level=logging.DEBUG, # Level of logs - If you think the log file it's too big, use logging.INFO emoji=True, # On Windows, we have a problem printing emoji. Set to false if you have a problem less=False, # If you think that the logs are too verbose, set this to True @@ -34,12 +34,12 @@ twitch_miner = TwitchChannelPointsMiner( make_predictions=True, # If you want to Bet / Make prediction follow_raid=True, # Follow raid to obtain more points claim_drops=True, # We can't filter rewards base on stream. Set to False for skip viewing counter increase and you will never obtain a drop reward from this script. Issue #21 - watch_streak=True, # If a streamer go online change the priotiry of streamers array and catch the watch screak. Issue #11 + watch_streak=True, # If a streamer go online change the priority of streamers array and catch the watch streak. Issue #11 join_chat=True, # Join irc chat to increase watch-time bet=BetSettings( strategy=Strategy.SMART, # Choose you strategy! percentage=5, # Place the x% of your channel points - percentage_gap=20, # Gap difference between outcomesA and outcomesB (for SMART stragegy) + percentage_gap=20, # Gap difference between outcomesA and outcomesB (for SMART strategy) max_points=50000, # If the x percentage of your channel points is gt bet_max_points set this value stealth_mode=True, # If the calculated amount of channel points is GT the highest bet, place the highest value minus 1-2 points Issue #33 delay_mode=DelayMode.FROM_END, # When placing a bet, we will wait until `delay` seconds before the end of the timer From dd8b6bf6e3e91026459fd7ecebfb11951dfb9805 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Wed, 29 Sep 2021 15:56:46 +0200 Subject: [PATCH 106/117] Attempt to use grapql instead of helix request for get followers and streamer_id --- TwitchChannelPointsMiner/classes/Twitch.py | 45 ++++++++++++------- .../classes/TwitchLogin.py | 22 ++++++--- TwitchChannelPointsMiner/constants.py | 30 ++++++++++++- 3 files changed, 72 insertions(+), 25 deletions(-) diff --git a/TwitchChannelPointsMiner/classes/Twitch.py b/TwitchChannelPointsMiner/classes/Twitch.py index 23679fb..cd6a947 100644 --- a/TwitchChannelPointsMiner/classes/Twitch.py +++ b/TwitchChannelPointsMiner/classes/Twitch.py @@ -23,7 +23,7 @@ from TwitchChannelPointsMiner.classes.Exceptions import ( ) from TwitchChannelPointsMiner.classes.Settings import Priority, Settings from TwitchChannelPointsMiner.classes.TwitchLogin import TwitchLogin -from TwitchChannelPointsMiner.constants import API, CLIENT_ID, GQLOperations +from TwitchChannelPointsMiner.constants import CLIENT_ID, GQLOperations from TwitchChannelPointsMiner.utils import ( _millify, create_chunks, @@ -143,16 +143,20 @@ class Twitch(object): streamer.set_offline() def get_channel_id(self, streamer_username): - json_response = self.__do_helix_request(f"/users?login={streamer_username}") - if "data" not in json_response: + # json_response = self.__do_helix_request(f"/users?login={streamer_username}") + json_data = copy.deepcopy(GQLOperations.ReportMenuItem) + json_data["variables"] = {"channelLogin": streamer_username} + json_response = self.post_gql_request(json_data) + if ( + "data" not in json_response + or "user" not in json_response["data"] + or json_response["data"]["user"] is None + ): raise StreamerDoesNotExistException else: - data = json_response["data"] - if len(data) >= 1: - return data[0]["id"] - else: - raise StreamerDoesNotExistException + return json_response["data"]["user"]["id"] + """ def get_followers(self, first=100): followers = [] pagination = {} @@ -170,6 +174,23 @@ class Twitch(object): break return followers + """ + + def get_followers(self): + json_data = copy.deepcopy(GQLOperations.PersonalSections) + json_response = self.post_gql_request(json_data) + try: + if ( + "data" in json_response + and "personalSections" in json_response["data"] + and json_response["data"]["personalSections"] != [] + ): + return [ + fw["user"]["login"] + for fw in json_response["data"]["personalSections"][0]["items"] + ] + except KeyError: + return [] def update_raid(self, streamer, raid): if streamer.raid != raid: @@ -211,14 +232,6 @@ class Twitch(object): ) self.__chuncked_sleep(random_sleep * 60, chunk_size=chunk_size) - def __do_helix_request(self, query, response_as_json=True): - url = f"{API}/helix/{query.strip('/')}" - response = self.twitch_login.session.get(url) - logger.debug( - f"Query: {query}, Status code: {response.status_code}, Content: {response.json()}" - ) - return response.json() if response_as_json is True else response - def post_gql_request(self, json_data): try: response = requests.post( diff --git a/TwitchChannelPointsMiner/classes/TwitchLogin.py b/TwitchChannelPointsMiner/classes/TwitchLogin.py index 95f46a6..3c2464a 100644 --- a/TwitchChannelPointsMiner/classes/TwitchLogin.py +++ b/TwitchChannelPointsMiner/classes/TwitchLogin.py @@ -2,6 +2,7 @@ # Original Copyright (c) 2020 Rodney # The MIT License (MIT) +import copy import getpass import logging import os @@ -14,6 +15,7 @@ from TwitchChannelPointsMiner.classes.Exceptions import ( BadCredentialsException, WrongCookiesException, ) +from TwitchChannelPointsMiner.constants import GQLOperations logger = logging.getLogger(__name__) @@ -177,13 +179,19 @@ class TwitchLogin(object): if self.token is None: return False - response = self.session.get( - f"https://api.twitch.tv/helix/users?login={self.username}" - ) - response = response.json() - if "data" in response: - self.login_check_result = True - self.user_id = response["data"][0]["id"] + json_data = copy.deepcopy(GQLOperations.ReportMenuItem) + json_data["variables"] = {"channelLogin": self.usrername} + response = self.session.post(GQLOperations.url, json=json_data) + + if response.status_code == 200: + json_response = response.json() + if ( + "data" in json_response + and "user" in json_response["data"] + and json_response["data"]["user"]["id"] is not None + ): + self.user_id = json_response["data"]["user"]["id"] + self.login_check_result = True return self.login_check_result diff --git a/TwitchChannelPointsMiner/constants.py b/TwitchChannelPointsMiner/constants.py index 6127c7f..5189f4a 100644 --- a/TwitchChannelPointsMiner/constants.py +++ b/TwitchChannelPointsMiner/constants.py @@ -1,6 +1,5 @@ # Twitch endpoints URL = "https://www.twitch.tv" -API = "https://api.twitch.tv" IRC = "irc.chat.twitch.tv" IRC_PORT = 6667 WEBSOCKET = "wss://pubsub-edge.twitch.tv/v1" @@ -118,7 +117,7 @@ class GQLOperations: "extensions": { "persistedQuery": { "version": 1, - "sha256Hash": "14b5e8a50777165cfc3971e1d93b4758613fe1c817d5542c398dce70b7a45c05", # "7da6078b1bfa2f0a4dd061cb47bdcd1ffddf31cccadd966ec192e4cd06666e2b", + "sha256Hash": "14b5e8a50777165cfc3971e1d93b4758613fe1c817d5542c398dce70b7a45c05", } }, } @@ -131,3 +130,30 @@ class GQLOperations: } }, } + ReportMenuItem = { # Use for replace https://api.twitch.tv/helix/users?login={self.username} + "operationName": "ReportMenuItem", + "extensions": { + "persistedQuery": { + "version": 1, + "sha256Hash": "8f3628981255345ca5e5453dfd844efffb01d6413a9931498836e6268692a30c", + } + }, + } + PersonalSections = { + "operationName": "PersonalSections", + "variables": { + "input": { + "sectionInputs": ["FOLLOWED_SECTION"], + "recommendationContext": {"platform": "web"}, + }, + "channelLogin": None, + "withChannelUser": False, + "creatorAnniversariesExperimentEnabled": False, + }, + "extensions": { + "persistedQuery": { + "version": 1, + "sha256Hash": "9fbdfb00156f754c26bde81eb47436dee146655c92682328457037da1a48ed39", + } + }, + } From b64fb60ccaacb7162c10d0f618d6379ef7d5a8f3 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Wed, 29 Sep 2021 19:25:49 +0200 Subject: [PATCH 107/117] typo on usrername --- TwitchChannelPointsMiner/classes/TwitchLogin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TwitchChannelPointsMiner/classes/TwitchLogin.py b/TwitchChannelPointsMiner/classes/TwitchLogin.py index 3c2464a..180be28 100644 --- a/TwitchChannelPointsMiner/classes/TwitchLogin.py +++ b/TwitchChannelPointsMiner/classes/TwitchLogin.py @@ -180,7 +180,7 @@ class TwitchLogin(object): return False json_data = copy.deepcopy(GQLOperations.ReportMenuItem) - json_data["variables"] = {"channelLogin": self.usrername} + json_data["variables"] = {"channelLogin": self.username} response = self.session.post(GQLOperations.url, json=json_data) if response.status_code == 200: From 5f6c75560e131f17c17f304444c62060e5acf982 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Wed, 29 Sep 2021 22:54:43 +0200 Subject: [PATCH 108/117] Check if the fw['user'] is None #289 --- TwitchChannelPointsMiner/classes/Twitch.py | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/TwitchChannelPointsMiner/classes/Twitch.py b/TwitchChannelPointsMiner/classes/Twitch.py index cd6a947..e082262 100644 --- a/TwitchChannelPointsMiner/classes/Twitch.py +++ b/TwitchChannelPointsMiner/classes/Twitch.py @@ -143,7 +143,6 @@ class Twitch(object): streamer.set_offline() def get_channel_id(self, streamer_username): - # json_response = self.__do_helix_request(f"/users?login={streamer_username}") json_data = copy.deepcopy(GQLOperations.ReportMenuItem) json_data["variables"] = {"channelLogin": streamer_username} json_response = self.post_gql_request(json_data) @@ -156,26 +155,6 @@ class Twitch(object): else: return json_response["data"]["user"]["id"] - """ - def get_followers(self, first=100): - followers = [] - pagination = {} - while 1: - query = f"/users/follows?from_id={self.twitch_login.get_user_id()}&first={first}" - if pagination != {}: - query += f"&after={pagination['cursor']}" - - json_response = self.__do_helix_request(query) - pagination = json_response["pagination"] - followers += [fw["to_login"].lower() for fw in json_response["data"]] - time.sleep(random.uniform(0.3, 0.7)) - - if pagination == {}: - break - - return followers - """ - def get_followers(self): json_data = copy.deepcopy(GQLOperations.PersonalSections) json_response = self.post_gql_request(json_data) @@ -188,6 +167,7 @@ class Twitch(object): return [ fw["user"]["login"] for fw in json_response["data"]["personalSections"][0]["items"] + if fw["user"] is not None ] except KeyError: return [] From 3c3101516ffa29d31a648ec9ae64fe5ff387e2ac Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Thu, 30 Sep 2021 15:53:48 +0200 Subject: [PATCH 109/117] The user_id can't be None, double check before return If It's None please do a gql request --- .../TwitchChannelPointsMiner.py | 5 ++- TwitchChannelPointsMiner/classes/Twitch.py | 6 +-- .../classes/TwitchLogin.py | 41 ++++++++++++------- .../classes/TwitchWebSocket.py | 1 - .../classes/WebSocketsPool.py | 1 - 5 files changed, 32 insertions(+), 22 deletions(-) diff --git a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py index 7b44c89..f32be2b 100644 --- a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py +++ b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py @@ -240,10 +240,11 @@ class TwitchChannelPointsMiner: ) # Subscribe to community-points-user. Get update for points spent or gains + user_id = self.twitch.twitch_login.get_user_id() self.ws_pool.submit( PubsubTopic( "community-points-user-v1", - user_id=self.twitch.twitch_login.get_user_id(), + user_id=user_id, ) ) @@ -252,7 +253,7 @@ class TwitchChannelPointsMiner: self.ws_pool.submit( PubsubTopic( "predictions-user-v1", - user_id=self.twitch.twitch_login.get_user_id(), + user_id=user_id, ) ) diff --git a/TwitchChannelPointsMiner/classes/Twitch.py b/TwitchChannelPointsMiner/classes/Twitch.py index e082262..9dc0ac9 100644 --- a/TwitchChannelPointsMiner/classes/Twitch.py +++ b/TwitchChannelPointsMiner/classes/Twitch.py @@ -355,14 +355,12 @@ class Twitch(object): drop.has_preconditions_met is not False and drop.is_printable is True ): - # print("=" * 125) logger.info( f"{streamers[index]} is streaming {streamers[index].stream}" ) logger.info(f"Campaign: {campaign}") logger.info(f"Drop: {drop}") logger.info(f"{drop.progress_bar()}") - # print("=" * 125) except requests.exceptions.ConnectionError as e: logger.error(f"Error while trying to send minute watched: {e}") @@ -534,7 +532,9 @@ class Twitch(object): } response = self.post_gql_request(json_data) - result += list(map(lambda x: x["data"]["user"]["dropCampaign"], response)) + for r in response: + if r["data"]["user"] is not None: + result.append(r["data"]["user"]["dropCampaign"]) return result def __sync_campaigns(self, campaigns): diff --git a/TwitchChannelPointsMiner/classes/TwitchLogin.py b/TwitchChannelPointsMiner/classes/TwitchLogin.py index 180be28..d257a85 100644 --- a/TwitchChannelPointsMiner/classes/TwitchLogin.py +++ b/TwitchChannelPointsMiner/classes/TwitchLogin.py @@ -179,20 +179,7 @@ class TwitchLogin(object): if self.token is None: return False - json_data = copy.deepcopy(GQLOperations.ReportMenuItem) - json_data["variables"] = {"channelLogin": self.username} - response = self.session.post(GQLOperations.url, json=json_data) - - if response.status_code == 200: - json_response = response.json() - if ( - "data" in json_response - and "user" in json_response["data"] - and json_response["data"]["user"]["id"] is not None - ): - self.user_id = json_response["data"]["user"]["id"] - self.login_check_result = True - + self.login_check_result = self.__set_user_id() return self.login_check_result def save_cookies(self, cookies_file): @@ -211,6 +198,7 @@ class TwitchLogin(object): if cookie["name"] == key: if cookie["value"] is not None: return cookie["value"] + return None def load_cookies(self, cookies_file): if os.path.isfile(cookies_file): @@ -219,7 +207,30 @@ class TwitchLogin(object): raise WrongCookiesException("There must be a cookies file!") def get_user_id(self): - return int(self.get_cookie_value("persistent").split("%")[0]) + persistent = self.get_cookie_value("persistent") + user_id = ( + int(persistent.split("%")[0]) if persistent is not None else self.user_id + ) + if user_id is None: + if self.__set_user_id() is True: + return self.user_id + return user_id + + def __set_user_id(self): + json_data = copy.deepcopy(GQLOperations.ReportMenuItem) + json_data["variables"] = {"channelLogin": self.username} + response = self.session.post(GQLOperations.url, json=json_data) + + if response.status_code == 200: + json_response = response.json() + if ( + "data" in json_response + and "user" in json_response["data"] + and json_response["data"]["user"]["id"] is not None + ): + self.user_id = json_response["data"]["user"]["id"] + return True + return False def get_auth_token(self): return self.get_cookie_value("auth-token") diff --git a/TwitchChannelPointsMiner/classes/TwitchWebSocket.py b/TwitchChannelPointsMiner/classes/TwitchWebSocket.py index 2e85bc5..2b3dc33 100644 --- a/TwitchChannelPointsMiner/classes/TwitchWebSocket.py +++ b/TwitchChannelPointsMiner/classes/TwitchWebSocket.py @@ -43,7 +43,6 @@ class TwitchWebSocket(WebSocketApp): data = {"topics": [str(topic)]} if topic.is_user_topic() and auth_token is not None: data["auth_token"] = auth_token - nonce = create_nonce() self.send({"type": "LISTEN", "nonce": nonce, "data": data}) diff --git a/TwitchChannelPointsMiner/classes/WebSocketsPool.py b/TwitchChannelPointsMiner/classes/WebSocketsPool.py index 71fa0fb..8fecf6b 100644 --- a/TwitchChannelPointsMiner/classes/WebSocketsPool.py +++ b/TwitchChannelPointsMiner/classes/WebSocketsPool.py @@ -142,7 +142,6 @@ class WebSocketsPool: # Why not create a new ws on the same array index? Let's try. self = ws.parent_pool self.ws[ws.index] = self.__new(ws.index) # Create a new connection. - # self.ws[ws.index].topics = ws.topics self.__start(ws.index) # Start a new thread. time.sleep(30) From cdff31c454db0eb0d7a535f0ba2644a316eac926 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Fri, 1 Oct 2021 10:53:37 +0200 Subject: [PATCH 110/117] Prevent the weird error on CTRL+C before ws_pool init #293 --- TwitchChannelPointsMiner/TwitchChannelPointsMiner.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py index f32be2b..0dc8a4c 100644 --- a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py +++ b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py @@ -304,7 +304,8 @@ class TwitchChannelPointsMiner: streamer.irc_chat.join() self.running = self.twitch.running = False - self.ws_pool.end() + if self.ws_pool is not None: + self.ws_pool.end() if self.minute_watcher_thread is not None: self.minute_watcher_thread.join() From 0a5fbbe183003265b3ab4cbfee47fe6ac8447eca Mon Sep 17 00:00:00 2001 From: raymondcc2tm <75225829+raymondcc2tm@users.noreply.github.com> Date: Tue, 5 Oct 2021 10:25:00 +0530 Subject: [PATCH 111/117] Fixed link to "Migrating from an old repository (the original one)" --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1fcec52..9050fac 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Read more about channels point [here](https://help.twitch.tv/s/article/channel-p - [FilterCondition](#filtercondition) - [Example](#example) 6. πŸ“ˆ [Analytics](#analytics) -7. πŸͺ [Migrating from old repository (the original one)](#migrating-from-old-repository-the-original-one) +7. πŸͺ [Migrating from an old repository (the original one)](#migrating-from-an-old-repository-the-original-one) 8. πŸͺŸ [Windows](#windows) 9. πŸ“± [Termux](#termux) 10. ⚠️ [Disclaimer](#disclaimer) From a55548eb1b78e6e81ef5e3b4ccbec3ab32a9f723 Mon Sep 17 00:00:00 2001 From: EgrorBs <44816767+EgrorBs@users.noreply.github.com> Date: Fri, 8 Oct 2021 15:43:11 +0300 Subject: [PATCH 112/117] Update README Fix: `join_chat` should be in the StreamerSettings property table instead of the BetSettings property table. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1fcec52..0056f1e 100644 --- a/README.md +++ b/README.md @@ -342,6 +342,7 @@ ColorPalette( | `follow_raid` | bool | True | Choose if you want to follow raid +250 points | | `claim_drops` | bool | True | If this value is True, the script will increase the watch-time for the current game. With this, you can claim the drops from Twitch Inventory [#21](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/21) | | `watch_streak` | bool | True | Choose if you want to change a priority for these streamers and try to catch the Watch Streak event [#11](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/11) | +| `join_chat` | bool | True | Join IRC-Chat to appear online in chat and attempt to get StreamElements channel points and increase view-time [#47](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/47) | | `bet` | BetSettings | | Rules to follow for the bet | ### BetSettings | Key | Type | Default | Description | @@ -351,7 +352,6 @@ ColorPalette( | `percentage_gap` | int | 20 | Gap difference between outcomesA and outcomesB (for SMART stragegy) | | `max_points` | int | 50000 | If the x percentage of your channel points is GT bet_max_points set this value | | `stealth_mode` | bool | False | If the calculated amount of channel points is GT the highest bet, place the highest value minus 1-2 points [#33](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/33) | -| `join_chat` | bool | True | Join IRC-Chat to appear online in chat and attempt to get StreamElements channel points and increase view-time [#47](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/47) | | `delay_mode` | DelayMode | FROM_END | Define how is calculating the waiting time before placing a bet | | `delay` | float | 6 | Value to be used to calculate bet delay depending on `delay_mode` value | From 181d0368aff5ed6e23f555bc13aa7bc4aded788b Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Mon, 11 Oct 2021 10:49:21 +0200 Subject: [PATCH 113/117] auto_clear logs in settings :) --- README.md | 1 + TwitchChannelPointsMiner/logger.py | 35 ++++++++++++++++++++---------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 1fcec52..73138fb 100644 --- a/README.md +++ b/README.md @@ -297,6 +297,7 @@ You can combine all priority but keep in mind that use `ORDER` and `POINTS_ASCEN | `file_level` | level | logging.DEBUG | Level of logs in file save - If you think the log file it's too big, use logging.INFO | | `emoji` | bool | For Windows is False else True | On Windows, we have a problem printing emoji. Set to false if you have a problem | | `colored` | bool | True | If you want to print colored text [#45](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/45) [#82](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/82) | +| `auto_clear` | bool | True | Create a file rotation handler with interval = 1D and backupCount = 7 [#215](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/215) | | `color_palette` | ColorPalette | All messages are Fore.RESET except WIN and LOSE bet (GREEN and RED) | Create your custom color palette. Read more above. | #### Color Palette diff --git a/TwitchChannelPointsMiner/logger.py b/TwitchChannelPointsMiner/logger.py index 895e3fe..e2912ac 100644 --- a/TwitchChannelPointsMiner/logger.py +++ b/TwitchChannelPointsMiner/logger.py @@ -1,6 +1,7 @@ import logging import os import platform +from datetime import datetime from logging.handlers import TimedRotatingFileHandler from pathlib import Path @@ -104,6 +105,7 @@ class LoggerSettings: file_level: int = logging.DEBUG, emoji: bool = platform.system() != "Windows", colored: bool = False, + auto_clear: bool = True, color_palette: ColorPalette = ColorPalette(), ): self.save = save @@ -112,6 +114,7 @@ class LoggerSettings: self.file_level = file_level self.emoji = emoji self.colored = colored + self.auto_clear = auto_clear self.color_palette = color_palette @@ -142,18 +145,26 @@ def configure_loggers(username, settings): if settings.save is True: logs_path = os.path.join(Path().absolute(), "logs") Path(logs_path).mkdir(parents=True, exist_ok=True) - logs_file = os.path.join( - logs_path, - f"{username}.log", - ) - file_handler = TimedRotatingFileHandler( - logs_file, - when="D", - interval=1, - backupCount=7, - encoding="utf-8", - delay=False, - ) + if settings.auto_clear is True: + logs_file = os.path.join( + logs_path, + f"{username}.log", + ) + file_handler = TimedRotatingFileHandler( + logs_file, + when="D", + interval=1, + backupCount=7, + encoding="utf-8", + delay=False, + ) + else: + logs_file = os.path.join( + logs_path, + f"{username}.{datetime.now().strftime('%Y%m%d-%H%M%S')}.log", + ) + file_handler = logging.FileHandler(logs_file, "w", "utf-8") + file_handler.setFormatter( logging.Formatter( fmt="%(asctime)s - %(levelname)s - %(name)s - [%(funcName)s]: %(message)s", From cf4b6e6f58e3e7b180bdece42f9059cfa0797e5d Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Thu, 28 Oct 2021 15:59:04 +0200 Subject: [PATCH 114/117] README.md update --- README.md | 10 +++++++++- assets/charts.html | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 813554f..33f2608 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,14 @@ Read more about channels point [here](https://help.twitch.tv/s/article/channel-points-guide) +## πŸ“’ Help wanted +Currently, we have a lot of PRs requests opened, but the time to test and improve It's less and less. If you want to help the community and the project, please test the following PRs and give us feedback: +- [Features/improvements to analytics #131](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/pull/131) +- [Add SMART_HIGH_ODDS strategy #172](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/pull/172) +- [Replace join_chat boolean with chat: ChatPresence #253](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/pull/253) +- [Receive updates to Telegram #254](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/pull/254) +- [Add stats #318](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/pull/318) + # README Contents 1. 🀝 [Community](#community) 2. πŸš€ [Main differences from the original repository](#main-differences-from-the-original-repository) @@ -46,7 +54,7 @@ If you have any type of issue, need help, or want to suggest a new feature, plea If you want to help on this project, please leave a star 🌟 and share it with your friends! 😎 -A coffee is always a gesture of LOVE ❀️ +If you want to offer me a coffee, I would be grateful ❀️ Buy Me A Coffee diff --git a/assets/charts.html b/assets/charts.html index 10b2956..86a6a58 100644 --- a/assets/charts.html +++ b/assets/charts.html @@ -77,7 +77,7 @@
If you want to help on this project, please leave a star 🌟 and share it with - your friends! 😎 - A coffee is always a gesture of LOVE ❀️ + your friends! 😎 - If you want to offer me a coffee, I would be grateful ❀️ Buy Me A Coffee Date: Wed, 10 Nov 2021 15:37:25 +0100 Subject: [PATCH 115/117] Deploy on pypi.org --- README.md | 22 +++++++++++++++------- TwitchChannelPointsMiner/__init__.py | 2 +- setup.py | 18 ++++++++++++++---- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 33f2608..d4e39b7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![Twitch Channel Points Miner - v2](./assets/banner.png) +![Twitch Channel Points Miner - v2](https://raw.githubusercontent.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/master/assets/banner.png)

License Python3 @@ -58,7 +58,7 @@ If you want to offer me a coffee, I would be grateful ❀️ Buy Me A Coffee -If you have any issues or you want to contribute, you are welcome! But please before read the [CONTRIBUTING.md](/CONTRIBUTING.md) +If you have any issues or you want to contribute, you are welcome! But please before read the [CONTRIBUTING.md](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/blob/master/CONTRIBUTING.md) file. ## Main differences from the original repository: @@ -171,15 +171,23 @@ No browser needed. [#41](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner ``` ## How to use: +### Download via pip +Via pip you can download the stable version of the project. + +`pip install Twitch-Channel-Points-Miner-v2` + +### Download via Github +Via GitHub you can download the latest version of the project. 1. Clone this repository `git clone https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2` -2. Install all the requirements `pip install -r requirements.txt` . If you have problems with requirements, make sure to have at least Python3.6. You could also try to create a virtualenv and then install all the requirements +2. Install all the requirements `pip install -r requirements.txt` . If you have problems with requirements, make sure to have at least Python3.6. You could also try to create a _virtualenv_ and then install all the requirements ```sh pip install virtualenv virtualenv -p python3 venv source venv/bin/activate pip install -r requirements.txt ``` -3. Create your `run.py` file start from [example.py](/example.py) + +Then create your `run.py` file start from [example.py](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/blob/master/example.py). ```python # -*- coding: utf-8 -*- @@ -276,7 +284,7 @@ from TwitchChannelPointsMiner import TwitchChannelPointsMiner twitch_miner = TwitchChannelPointsMiner("your-twitch-username") twitch_miner.mine(followers=True, blacklist=["user1", "user2"]) # Blacklist example ``` -4. Start mining! `python run.py` +Start mining! `python run.py` πŸ₯³ ### Limits > Twitch has a limit - you can't watch more than two channels at one time. We take the first two streamers from the list as they have the highest priority. @@ -371,7 +379,7 @@ ColorPalette( - **PERCENTAGE**: Select the option with the highest percentage based on odds (It's the same that show Twitch) - Should be the same as select LOWEST_ODDS - **SMART**: If the majority in percent chose an option, then follow the other users, otherwise select the option with the highest odds -![Screenshot](./assets/prediction.png) +![Screenshot](https://raw.githubusercontent.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/master/assets/prediction.png) Here a concrete example: @@ -428,7 +436,7 @@ If you want you can toggle the dark theme with the dedicated checkbox. | Light theme | Dark theme | | ----------- | ---------- | -| ![Light theme](./assets/chart-analytics-light.png) | ![Dark theme](./assets/chart-analytics-dark.png) | +| ![Light theme](https://raw.githubusercontent.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/master/assets/chart-analytics-light.png) | ![Dark theme](https://raw.githubusercontent.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/master/assets/chart-analytics-dark.png) | For use this feature just call the `analytics` method before start mining. Read more at: [#96](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/96) The chart will be autofreshed each `refresh` minutes. If you want to connect from one to second machine that have that webpanel you have to use `0.0.0.0` instead of `127.0.0.1`. diff --git a/TwitchChannelPointsMiner/__init__.py b/TwitchChannelPointsMiner/__init__.py index f581ca0..5ba6a33 100644 --- a/TwitchChannelPointsMiner/__init__.py +++ b/TwitchChannelPointsMiner/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -__version__ = "2.7.2" +__version__ = "2.0.3" from .TwitchChannelPointsMiner import TwitchChannelPointsMiner __all__ = [ diff --git a/setup.py b/setup.py index c9d80da..840cc83 100644 --- a/setup.py +++ b/setup.py @@ -25,12 +25,22 @@ setuptools.setup( url="https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2", packages=setuptools.find_packages(), include_package_data=True, - install_requires=read("requirements.txt").splitlines(), + install_requires=[ + "requests==2.25.1", + "websocket-client==1.0.1", + "browser_cookie3==0.12.1", + "pillow==8.3.2", + "python-dateutil==2.8.1", + "emoji==1.2.0", + "millify==0.1.1", + "pre-commit==2.13.0", + "colorama==0.4.4", + "flask==2.0.1", + "irc==19.0.1", + ], long_description=read("README.md"), long_description_content_type="text/markdown", classifiers=[ - "Development Status :: 4 - Beta", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", @@ -43,5 +53,5 @@ setuptools.setup( "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Natural Language :: English", ], - python_requires=">=3, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", + python_requires=">=3.6", ) From 14ea85c40f37117b3827e8eb2b99631ed528e27d Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Thu, 11 Nov 2021 11:20:12 +0100 Subject: [PATCH 116/117] Check if the current version It's the same on GitHub master branch. Download missing assets for analytics (usefull if the script was downloaded via pip --- .../TwitchChannelPointsMiner.py | 7 +++++ TwitchChannelPointsMiner/__init__.py | 2 +- .../classes/AnalyticsServer.py | 30 +++++++++++++++++++ TwitchChannelPointsMiner/constants.py | 6 ++++ TwitchChannelPointsMiner/utils.py | 28 ++++++++++++++++- 5 files changed, 71 insertions(+), 2 deletions(-) diff --git a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py index 0dc8a4c..6fd3ad4 100644 --- a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py +++ b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py @@ -26,6 +26,7 @@ from TwitchChannelPointsMiner.logger import LoggerSettings, configure_loggers from TwitchChannelPointsMiner.utils import ( _millify, at_least_one_value_in_settings_is, + check_versions, get_user_agent, internet_connection_available, set_default_settings, @@ -109,6 +110,12 @@ class TwitchChannelPointsMiner: self.logs_file = configure_loggers(self.username, logger_settings) + # Check for the latest version of the script + current_version, github_version = check_versions() + if current_version != github_version: + logger.info(f"You are running the version {current_version} of this script") + logger.info(f"The latest version on GitHub is: {github_version}") + for sign in [signal.SIGINT, signal.SIGSEGV, signal.SIGTERM]: signal.signal(sign, self.end) diff --git a/TwitchChannelPointsMiner/__init__.py b/TwitchChannelPointsMiner/__init__.py index 5ba6a33..5be7dc1 100644 --- a/TwitchChannelPointsMiner/__init__.py +++ b/TwitchChannelPointsMiner/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -__version__ = "2.0.3" +__version__ = "2.0.4" from .TwitchChannelPointsMiner import TwitchChannelPointsMiner __all__ = [ diff --git a/TwitchChannelPointsMiner/classes/AnalyticsServer.py b/TwitchChannelPointsMiner/classes/AnalyticsServer.py index 0a5c0a1..3daa843 100644 --- a/TwitchChannelPointsMiner/classes/AnalyticsServer.py +++ b/TwitchChannelPointsMiner/classes/AnalyticsServer.py @@ -6,6 +6,7 @@ from threading import Thread from flask import Flask, Response, cli, render_template from TwitchChannelPointsMiner.classes.Settings import Settings +from TwitchChannelPointsMiner.utils import download_file cli.show_server_banner = lambda *_: None logger = logging.getLogger(__name__) @@ -38,10 +39,39 @@ def index(refresh=5): ) +def download_assets(assets_folder, required_files): + Path(assets_folder).mkdir(parents=True, exist_ok=True) + logger.info(f"Downloading assets to {assets_folder}") + + for f in required_files: + if os.path.isfile(os.path.join(assets_folder, f)) is False: + if ( + download_file(os.path.join("assets", f), os.path.join(assets_folder, f)) + is True + ): + logger.info(f"Downloaded {f}") + + +def check_assets(): + required_files = ["banner.png", "charts.html", "dark-theme.css"] + assets_folder = os.path.join(Path().absolute(), "assets") + if os.path.isdir(assets_folder) is False: + logger.info(f"Assets folder not found at {assets_folder}") + download_assets(assets_folder, required_files) + else: + for f in required_files: + if os.path.isfile(os.path.join(assets_folder, f)) is False: + logger.info(f"Missing file {f} in {assets_folder}") + download_assets(assets_folder, required_files) + break + + class AnalyticsServer(Thread): def __init__(self, host: str = "127.0.0.1", port: int = 5000, refresh: int = 5): super(AnalyticsServer, self).__init__() + check_assets() + self.host = host self.port = port self.refresh = refresh diff --git a/TwitchChannelPointsMiner/constants.py b/TwitchChannelPointsMiner/constants.py index 5189f4a..96a8d99 100644 --- a/TwitchChannelPointsMiner/constants.py +++ b/TwitchChannelPointsMiner/constants.py @@ -17,6 +17,12 @@ USER_AGENTS = { }, } +BRANCH = "master" +GITHUB_url = ( + "https://raw.githubusercontent.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/" + + BRANCH +) + class GQLOperations: url = "https://gql.twitch.tv/gql" diff --git a/TwitchChannelPointsMiner/utils.py b/TwitchChannelPointsMiner/utils.py index ceaef57..ef6d3d4 100644 --- a/TwitchChannelPointsMiner/utils.py +++ b/TwitchChannelPointsMiner/utils.py @@ -4,11 +4,13 @@ import socket import time from copy import deepcopy from datetime import datetime, timezone +from os import path from random import randrange +import requests from millify import millify -from TwitchChannelPointsMiner.constants import USER_AGENTS +from TwitchChannelPointsMiner.constants import USER_AGENTS, GITHUB_url def _millify(input, precision=2): @@ -154,3 +156,27 @@ def percentage(a, b): def create_chunks(lst, n): return [lst[i : (i + n)] for i in range(0, len(lst), n)] # noqa: E203 + + +def download_file(name, fpath): + r = requests.get(path.join(GITHUB_url, name), stream=True) + with open(fpath, "wb") as f: + for chunk in r.iter_content(chunk_size=1024): + if chunk: + f.write(chunk) + return True + + +def read(fname): + return open(path.join(path.dirname(__file__), fname), encoding="utf-8").read() + + +def init2dict(content): + return dict(re.findall(r"""__([a-z]+)__ = "([^"]+)""", content)) + + +def check_versions(): + current_version = init2dict(read("__init__.py"))["version"] + r = requests.get(path.join(GITHUB_url, "TwitchChannelPointsMiner", "__init__.py")) + github_version = init2dict(r.text)["version"] + return current_version, github_version From 5efbed68dc94fb7da9c2a0fab6c2b61f4d6e0970 Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Fri, 12 Nov 2021 09:35:11 +0100 Subject: [PATCH 117/117] Prevent exception during check_versions. Close #337 --- .../TwitchChannelPointsMiner.py | 6 +++- TwitchChannelPointsMiner/__init__.py | 2 +- TwitchChannelPointsMiner/utils.py | 35 ++++++++++++++----- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py index 6fd3ad4..fb32852 100644 --- a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py +++ b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py @@ -112,7 +112,11 @@ class TwitchChannelPointsMiner: # Check for the latest version of the script current_version, github_version = check_versions() - if current_version != github_version: + if github_version == "0.0.0": + logger.error( + "Unable to detect if you have the latest version of this script" + ) + elif current_version != github_version: logger.info(f"You are running the version {current_version} of this script") logger.info(f"The latest version on GitHub is: {github_version}") diff --git a/TwitchChannelPointsMiner/__init__.py b/TwitchChannelPointsMiner/__init__.py index 5be7dc1..cf775fa 100644 --- a/TwitchChannelPointsMiner/__init__.py +++ b/TwitchChannelPointsMiner/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -__version__ = "2.0.4" +__version__ = "2.0.5" from .TwitchChannelPointsMiner import TwitchChannelPointsMiner __all__ = [ diff --git a/TwitchChannelPointsMiner/utils.py b/TwitchChannelPointsMiner/utils.py index ef6d3d4..7f6dbda 100644 --- a/TwitchChannelPointsMiner/utils.py +++ b/TwitchChannelPointsMiner/utils.py @@ -159,11 +159,16 @@ def create_chunks(lst, n): def download_file(name, fpath): - r = requests.get(path.join(GITHUB_url, name), stream=True) - with open(fpath, "wb") as f: - for chunk in r.iter_content(chunk_size=1024): - if chunk: - f.write(chunk) + r = requests.get( + path.join(GITHUB_url, name), + headers={"User-Anget": get_user_agent("FIREFOX")}, + stream=True, + ) + if r.status_code == 200: + with open(fpath, "wb") as f: + for chunk in r.iter_content(chunk_size=1024): + if chunk: + f.write(chunk) return True @@ -176,7 +181,21 @@ def init2dict(content): def check_versions(): - current_version = init2dict(read("__init__.py"))["version"] - r = requests.get(path.join(GITHUB_url, "TwitchChannelPointsMiner", "__init__.py")) - github_version = init2dict(r.text)["version"] + try: + current_version = init2dict(read("__init__.py")) + current_version = ( + current_version["version"] if "version" in current_version else "0.0.0" + ) + except Exception: + current_version = "0.0.0" + try: + r = requests.get( + path.join(GITHUB_url, "TwitchChannelPointsMiner", "__init__.py") + ) + github_version = init2dict(r.text) + github_version = ( + github_version["version"] if "version" in github_version else "0.0.0" + ) + except Exception: + github_version = "0.0.0" return current_version, github_version