diff --git a/.github/stale.yml b/.github/stale.yml index 8a3287d00..073f80cfb 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 diff --git a/README.md b/README.md index b17d8d5d7..09f17ad59 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( @@ -218,7 +218,9 @@ twitch_miner = TwitchChannelPointsMiner( target_odd=3, # Target odd for SMART_HIGH_ODDS 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 #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 @@ -279,6 +281,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 (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 @@ -340,7 +343,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 | |-------------------- |----------------- |--------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -350,7 +352,9 @@ ColorPalette( | `target_odd` | float | 3 | Target odd for SMART_HIGH_ODDS strategy | | `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) | +| `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 @@ -398,6 +402,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'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) +- **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/TwitchChannelPointsMiner.py b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py index ce1711fcf..7b44c8920 100644 --- a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py +++ b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py @@ -8,7 +8,6 @@ import threading import time import uuid -from collections import OrderedDict from datetime import datetime from pathlib import Path @@ -158,45 +157,41 @@ def run(self, streamers: list = [], blacklist: list = [], followers=False): ) 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) - ) 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_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, + ) + 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 diff --git a/TwitchChannelPointsMiner/__init__.py b/TwitchChannelPointsMiner/__init__.py index 4c150cafe..f581ca025 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__ = [ diff --git a/TwitchChannelPointsMiner/classes/Settings.py b/TwitchChannelPointsMiner/classes/Settings.py index 05c6479e2..c4a550fe0 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 9a7abd302..23679fb3f 100644 --- a/TwitchChannelPointsMiner/classes/Twitch.py +++ b/TwitchChannelPointsMiner/classes/Twitch.py @@ -314,6 +314,19 @@ def send_minute_watched_events(self, streamers, priority, chunk_size=3): if len(streamers_watching) == 2: break + elif prior == Priority.SUBSCRIBED and len(streamers_watching) < 2: + 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(), + reverse=True, + ) + streamers_watching += streamers_with_multiplier[:2] + """ 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 +399,7 @@ def load_channel_points_context(self, streamer): channel = response["data"]["community"]["channel"] community_points = channel["self"]["communityPoints"] streamer.channel_points = community_points["balance"] + streamer.activeMultipliers = community_points["activeMultipliers"] if community_points["availableClaim"] is not None: self.claim_bonus(streamer, community_points["availableClaim"]["id"]) @@ -437,7 +451,29 @@ def make_predictions(self, event): "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"] + and response["data"]["makePrediction"]["error"] is not None + ): + error_code = response["data"]["makePrediction"]["error"]["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"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}", diff --git a/TwitchChannelPointsMiner/classes/TwitchLogin.py b/TwitchChannelPointsMiner/classes/TwitchLogin.py index 3ad8e096b..95f46a6d4 100644 --- a/TwitchChannelPointsMiner/classes/TwitchLogin.py +++ b/TwitchChannelPointsMiner/classes/TwitchLogin.py @@ -19,7 +19,7 @@ class TwitchLogin(object): - __self__ = [ + __slots__ = [ "client_id", "token", "login_check_result", diff --git a/TwitchChannelPointsMiner/classes/WebSocketsPool.py b/TwitchChannelPointsMiner/classes/WebSocketsPool.py index 26d1c5fb2..70479915b 100644 --- a/TwitchChannelPointsMiner/classes/WebSocketsPool.py +++ b/TwitchChannelPointsMiner/classes/WebSocketsPool.py @@ -110,7 +110,7 @@ def on_error(ws, error): 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) @@ -247,7 +247,9 @@ def on_message(ws, message): 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 ae0eaec80..f11c34903 100644 --- a/TwitchChannelPointsMiner/classes/entities/Bet.py +++ b/TwitchChannelPointsMiner/classes/entities/Bet.py @@ -42,6 +42,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", @@ -68,6 +77,8 @@ class BetSettings(object): "only_doubt", "stealth_mode", "filter_condition", + "delay", + "delay_mode", ] def __init__( @@ -80,6 +91,8 @@ def __init__( only_doubt: bool = None, stealth_mode: bool = None, filter_condition: FilterCondition = None, + delay: float = None, + delay_mode: DelayMode = None, ): self.strategy = strategy self.percentage = percentage @@ -89,6 +102,8 @@ def __init__( self.only_doubt = only_doubt 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 @@ -98,6 +113,8 @@ def default(self): self.target_odd = self.target_odd if not None else 3 self.only_doubt = self.only_doubt if not None else False 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 771420d3d..c01cd7917 100644 --- a/TwitchChannelPointsMiner/classes/entities/Streamer.py +++ b/TwitchChannelPointsMiner/classes/entities/Streamer.py @@ -6,7 +6,7 @@ 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 @@ -70,6 +70,7 @@ class Streamer(object): "channel_points", "minute_watched_requests", "viewer_is_mod", + "activeMultipliers", "irc_chat", "stream", "raid", @@ -89,6 +90,7 @@ def __init__(self, username, settings=None): self.channel_points = 0 self.minute_watched_requests = None self.viewer_is_mod = False + self.activeMultipliers = None self.irc_chat = None self.stream = Stream() @@ -168,6 +170,33 @@ def drops_condition(self): 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 + ) + + 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 min(delay, prediction_window_seconds) + elif delay_mode == DelayMode.FROM_END: + return max(prediction_window_seconds - delay, 0) + 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() diff --git a/TwitchChannelPointsMiner/logger.py b/TwitchChannelPointsMiner/logger.py index 619be5d74..895e3fe8c 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", diff --git a/requirements.txt b/requirements.txt index 6f2cbbbfb..0b1e26079 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