import json import logging import os import time from datetime import datetime from threading import Lock from TwitchChannelPointsMiner.classes.Chat import ChatPresence, ThreadChat from TwitchChannelPointsMiner.classes.entities.Bet import BetSettings, DelayMode from TwitchChannelPointsMiner.classes.entities.Stream import Stream from TwitchChannelPointsMiner.classes.Settings import Events, Settings from TwitchChannelPointsMiner.constants import URL from TwitchChannelPointsMiner.utils import _millify logger = logging.getLogger(__name__) class StreamerSettings(object): __slots__ = [ "make_predictions", "follow_raid", "claim_drops", "claim_moments", "watch_streak", "bet", "chat", ] def __init__( self, make_predictions: bool = None, follow_raid: bool = None, claim_drops: bool = None, claim_moments: bool = None, watch_streak: bool = None, bet: BetSettings = None, chat: ChatPresence = None, ): self.make_predictions = make_predictions self.follow_raid = follow_raid self.claim_drops = claim_drops self.claim_moments = claim_moments self.watch_streak = watch_streak self.bet = bet self.chat = chat def default(self): for name in [ "make_predictions", "follow_raid", "claim_drops", "claim_moments", "watch_streak", ]: if getattr(self, name) is None: setattr(self, name, True) if self.bet is None: self.bet = BetSettings() if self.chat is None: self.chat = ChatPresence.ONLINE def __repr__(self): return f"BetSettings(make_predictions={self.make_predictions}, follow_raid={self.follow_raid}, claim_drops={self.claim_drops}, claim_moments={self.claim_moments}, watch_streak={self.watch_streak}, bet={self.bet}, chat={self.chat})" class Streamer(object): __slots__ = [ "username", "channel_id", "settings", "is_online", "stream_up", "online_at", "offline_at", "channel_points", "minute_watched_requests", "viewer_is_mod", "activeMultipliers", "irc_chat", "stream", "raid", "history", "streamer_url", "mutex", ] def __init__(self, username, settings=None): self.username: str = username.lower().strip() self.channel_id: str = "" self.settings = settings self.is_online = False self.stream_up = 0 self.online_at = 0 self.offline_at = 0 self.channel_points = 0 self.minute_watched_requests = None self.viewer_is_mod = False self.activeMultipliers = None self.irc_chat = None self.stream = Stream() self.raid = None self.history = {} self.streamer_url = f"{URL}/{self.username}" self.mutex = Lock() def __repr__(self): return f"Streamer(username={self.username}, channel_id={self.channel_id}, channel_points={_millify(self.channel_points)})" def __str__(self): return ( f"{self.username} ({_millify(self.channel_points)} points)" if Settings.logger.less else self.__repr__() ) def set_offline(self): if self.is_online is True: self.offline_at = time.time() self.is_online = False self.toggle_chat() logger.info( f"{self} is Offline!", extra={ "emoji": ":sleeping:", "event": Events.STREAMER_OFFLINE, }, ) def set_online(self): if self.is_online is False: self.online_at = time.time() self.is_online = True self.stream.init_watch_streak() self.toggle_chat() logger.info( f"{self} is Online!", extra={ "emoji": ":partying_face:", "event": Events.STREAMER_ONLINE, }, ) def print_history(self): return ", ".join( [ f"{key}({self.history[key]['counter']} times, {_millify(self.history[key]['amount'])} gained)" for key in sorted(self.history) if self.history[key]["counter"] != 0 ] ) def update_history(self, reason_code, earned, counter=1): if reason_code not in self.history: self.history[reason_code] = {"counter": 0, "amount": 0} self.history[reason_code]["counter"] += counter self.history[reason_code]["amount"] += earned if reason_code == "WATCH_STREAK": self.stream.watch_streak_missing = False def stream_up_elapsed(self): return self.stream_up == 0 or ((time.time() - self.stream_up) > 120) def drops_condition(self): return ( self.settings.claim_drops is True and self.is_online is True # and self.stream.drops_tags is True 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() if event_type in ["WATCH_STREAK", "WIN", "PREDICTION_MADE", "LOSE"]: primary_color = ( "#45c1ff" # blue #45c1ff yellow #ffe045 green #36b535 red #ff4545 if event_type == "WATCH_STREAK" else ("#ffe045" if event_type == "PREDICTION_MADE" else ("#36b535" if event_type == "WIN" else "#ff4545")) ) data = { "borderColor": primary_color, "label": { "style": {"color": "#000", "background": primary_color}, "text": event_text, }, } self.__save_json("annotations", data) def persistent_series(self, event_type="Watch"): self.__save_json("series", event_type=event_type) 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 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") temp_fname = fname + '.temp' # Temporary file name with self.mutex: # Create and write to the temporary file with open(temp_fname, "w") as temp_file: json_data = json.load( open(fname, "r")) if os.path.isfile(fname) else {} if key not in json_data: json_data[key] = [] json_data[key].append(data) json.dump(json_data, temp_file, indent=4) # Replace the original file with the temporary file os.replace(temp_fname, fname) def leave_chat(self): 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: if self.irc_chat.is_alive() is False: self.irc_chat.start() def toggle_chat(self): if self.settings.chat == ChatPresence.ALWAYS: self.__join_chat() elif self.settings.chat != ChatPresence.NEVER: if self.is_online is True: if self.settings.chat == ChatPresence.ONLINE: self.__join_chat() elif self.settings.chat == ChatPresence.OFFLINE: self.leave_chat() else: if self.settings.chat == ChatPresence.ONLINE: self.leave_chat() elif self.settings.chat == ChatPresence.OFFLINE: self.__join_chat()