update
This commit is contained in:
315
TwitchChannelPointsMiner/classes/entities/Bet.py
Normal file
315
TwitchChannelPointsMiner/classes/entities/Bet.py
Normal file
@ -0,0 +1,315 @@
|
||||
import copy
|
||||
from enum import Enum, auto
|
||||
from random import uniform
|
||||
|
||||
from millify import millify
|
||||
|
||||
#from TwitchChannelPointsMiner.utils import char_decision_as_index, float_round
|
||||
from TwitchChannelPointsMiner.utils import float_round
|
||||
|
||||
|
||||
class Strategy(Enum):
|
||||
MOST_VOTED = auto()
|
||||
HIGH_ODDS = auto()
|
||||
PERCENTAGE = auto()
|
||||
SMART_MONEY = auto()
|
||||
SMART = auto()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Condition(Enum):
|
||||
GT = auto()
|
||||
LT = auto()
|
||||
GTE = auto()
|
||||
LTE = auto()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class OutcomeKeys(object):
|
||||
# Real key on Bet dict ['']
|
||||
PERCENTAGE_USERS = "percentage_users"
|
||||
ODDS_PERCENTAGE = "odds_percentage"
|
||||
ODDS = "odds"
|
||||
TOP_POINTS = "top_points"
|
||||
# Real key on Bet dict [''] - Sum()
|
||||
TOTAL_USERS = "total_users"
|
||||
TOTAL_POINTS = "total_points"
|
||||
# This key does not exist
|
||||
DECISION_USERS = "decision_users"
|
||||
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",
|
||||
"where",
|
||||
"value",
|
||||
]
|
||||
|
||||
def __init__(self, by=None, where=None, value=None, decision=None):
|
||||
self.by = by
|
||||
self.where = where
|
||||
self.value = value
|
||||
|
||||
def __repr__(self):
|
||||
return f"FilterCondition(by={self.by.upper()}, where={self.where}, value={self.value})"
|
||||
|
||||
|
||||
class BetSettings(object):
|
||||
__slots__ = [
|
||||
"strategy",
|
||||
"percentage",
|
||||
"percentage_gap",
|
||||
"max_points",
|
||||
"minimum_points",
|
||||
"stealth_mode",
|
||||
"filter_condition",
|
||||
"delay",
|
||||
"delay_mode",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
strategy: Strategy = None,
|
||||
percentage: int = None,
|
||||
percentage_gap: int = None,
|
||||
max_points: int = None,
|
||||
minimum_points: int = None,
|
||||
stealth_mode: bool = None,
|
||||
filter_condition: FilterCondition = None,
|
||||
delay: float = None,
|
||||
delay_mode: DelayMode = None,
|
||||
):
|
||||
self.strategy = strategy
|
||||
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
|
||||
self.delay = delay
|
||||
self.delay_mode = delay_mode
|
||||
|
||||
def default(self):
|
||||
self.strategy = self.strategy if self.strategy is not None else Strategy.SMART
|
||||
self.percentage = self.percentage if self.percentage is not None else 5
|
||||
self.percentage_gap = (
|
||||
self.percentage_gap if self.percentage_gap is not None else 20
|
||||
)
|
||||
self.max_points = self.max_points if self.max_points is not None else 50000
|
||||
self.minimum_points = (
|
||||
self.minimum_points if self.minimum_points is not None else 0
|
||||
)
|
||||
self.stealth_mode = (
|
||||
self.stealth_mode if self.stealth_mode is not None else False
|
||||
)
|
||||
self.delay = self.delay if self.delay is not None else 6
|
||||
self.delay_mode = (
|
||||
self.delay_mode if self.delay_mode is 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}, minimum_points={self.minimum_points}, stealth_mode={self.stealth_mode})"
|
||||
|
||||
|
||||
class Bet(object):
|
||||
__slots__ = ["outcomes", "decision", "total_users", "total_points", "settings"]
|
||||
|
||||
def __init__(self, outcomes: list, settings: BetSettings):
|
||||
self.outcomes = outcomes
|
||||
self.__clear_outcomes()
|
||||
self.decision: dict = {}
|
||||
self.total_users = 0
|
||||
self.total_points = 0
|
||||
self.settings = settings
|
||||
|
||||
def update_outcomes(self, outcomes):
|
||||
for index in range(0, len(self.outcomes)):
|
||||
self.outcomes[index][OutcomeKeys.TOTAL_USERS] = int(
|
||||
outcomes[index][OutcomeKeys.TOTAL_USERS]
|
||||
)
|
||||
self.outcomes[index][OutcomeKeys.TOTAL_POINTS] = int(
|
||||
outcomes[index][OutcomeKeys.TOTAL_POINTS]
|
||||
)
|
||||
if outcomes[index]["top_predictors"] != []:
|
||||
# Sort by points placed by other users
|
||||
outcomes[index]["top_predictors"] = sorted(
|
||||
outcomes[index]["top_predictors"],
|
||||
key=lambda x: x["points"],
|
||||
reverse=True,
|
||||
)
|
||||
# Get the first elements (most placed)
|
||||
top_points = outcomes[index]["top_predictors"][0]["points"]
|
||||
self.outcomes[index][OutcomeKeys.TOP_POINTS] = top_points
|
||||
|
||||
# Inefficient, but otherwise outcomekeys are represented wrong
|
||||
self.total_points = 0
|
||||
self.total_users = 0
|
||||
for index in range(0, len(self.outcomes)):
|
||||
self.total_users += self.outcomes[index][OutcomeKeys.TOTAL_USERS]
|
||||
self.total_points += self.outcomes[index][OutcomeKeys.TOTAL_POINTS]
|
||||
|
||||
if (
|
||||
self.total_users > 0
|
||||
and self.total_points > 0
|
||||
):
|
||||
for index in range(0, len(self.outcomes)):
|
||||
self.outcomes[index][OutcomeKeys.PERCENTAGE_USERS] = float_round(
|
||||
(100 * self.outcomes[index][OutcomeKeys.TOTAL_USERS]) / self.total_users
|
||||
)
|
||||
self.outcomes[index][OutcomeKeys.ODDS] = float_round(
|
||||
#self.total_points / max(self.outcomes[index][OutcomeKeys.TOTAL_POINTS], 1)
|
||||
0
|
||||
if self.outcomes[index][OutcomeKeys.TOTAL_POINTS] == 0
|
||||
else self.total_points / self.outcomes[index][OutcomeKeys.TOTAL_POINTS]
|
||||
)
|
||||
self.outcomes[index][OutcomeKeys.ODDS_PERCENTAGE] = float_round(
|
||||
#100 / max(self.outcomes[index][OutcomeKeys.ODDS], 1)
|
||||
0
|
||||
if self.outcomes[index][OutcomeKeys.ODDS] == 0
|
||||
else 100 / self.outcomes[index][OutcomeKeys.ODDS]
|
||||
)
|
||||
|
||||
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\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]
|
||||
decision = self.outcomes[self.decision["choice"]]
|
||||
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):
|
||||
return Bet.__parse_outcome(self.outcomes[index])
|
||||
|
||||
def __clear_outcomes(self):
|
||||
for index in range(0, len(self.outcomes)):
|
||||
keys = copy.deepcopy(list(self.outcomes[index].keys()))
|
||||
for key in keys:
|
||||
if key not in [
|
||||
OutcomeKeys.TOTAL_USERS,
|
||||
OutcomeKeys.TOTAL_POINTS,
|
||||
OutcomeKeys.TOP_POINTS,
|
||||
OutcomeKeys.PERCENTAGE_USERS,
|
||||
OutcomeKeys.ODDS,
|
||||
OutcomeKeys.ODDS_PERCENTAGE,
|
||||
"title",
|
||||
"color",
|
||||
"id",
|
||||
]:
|
||||
del self.outcomes[index][key]
|
||||
for key in [
|
||||
OutcomeKeys.PERCENTAGE_USERS,
|
||||
OutcomeKeys.ODDS,
|
||||
OutcomeKeys.ODDS_PERCENTAGE,
|
||||
OutcomeKeys.TOP_POINTS,
|
||||
]:
|
||||
if key not in self.outcomes[index]:
|
||||
self.outcomes[index][key] = 0
|
||||
|
||||
'''def __return_choice(self, key) -> str:
|
||||
return "A" if self.outcomes[0][key] > self.outcomes[1][key] else "B"'''
|
||||
|
||||
def __return_choice(self, key) -> int:
|
||||
largest=0
|
||||
for index in range(0, len(self.outcomes)):
|
||||
if self.outcomes[index][key] > self.outcomes[largest][key]:
|
||||
largest = index
|
||||
return largest
|
||||
|
||||
def skip(self) -> bool:
|
||||
if self.settings.filter_condition is not None:
|
||||
# key == by , condition == where
|
||||
key = self.settings.filter_condition.by
|
||||
condition = self.settings.filter_condition.where
|
||||
value = self.settings.filter_condition.value
|
||||
|
||||
fixed_key = (
|
||||
key
|
||||
if key not in [OutcomeKeys.DECISION_USERS, OutcomeKeys.DECISION_POINTS]
|
||||
else key.replace("decision", "total")
|
||||
)
|
||||
if key in [OutcomeKeys.TOTAL_USERS, OutcomeKeys.TOTAL_POINTS]:
|
||||
compared_value = (
|
||||
self.outcomes[0][fixed_key] + self.outcomes[1][fixed_key]
|
||||
)
|
||||
else:
|
||||
#outcome_index = char_decision_as_index(self.decision["choice"])
|
||||
outcome_index = self.decision["choice"]
|
||||
compared_value = self.outcomes[outcome_index][fixed_key]
|
||||
|
||||
# Check if condition is satisfied
|
||||
if condition == Condition.GT:
|
||||
if compared_value > value:
|
||||
return False, compared_value
|
||||
elif condition == Condition.LT:
|
||||
if compared_value < value:
|
||||
return False, compared_value
|
||||
elif condition == Condition.GTE:
|
||||
if compared_value >= value:
|
||||
return False, compared_value
|
||||
elif condition == Condition.LTE:
|
||||
if compared_value <= value:
|
||||
return False, compared_value
|
||||
return True, compared_value # Else skip the bet
|
||||
else:
|
||||
return False, 0 # Default don't skip the bet
|
||||
|
||||
def calculate(self, balance: int) -> dict:
|
||||
self.decision = {"choice": None, "amount": 0, "id": None}
|
||||
if self.settings.strategy == Strategy.MOST_VOTED:
|
||||
self.decision["choice"] = self.__return_choice(OutcomeKeys.TOTAL_USERS)
|
||||
elif self.settings.strategy == Strategy.HIGH_ODDS:
|
||||
self.decision["choice"] = self.__return_choice(OutcomeKeys.ODDS)
|
||||
elif self.settings.strategy == Strategy.PERCENTAGE:
|
||||
self.decision["choice"] = self.__return_choice(OutcomeKeys.ODDS_PERCENTAGE)
|
||||
elif self.settings.strategy == Strategy.SMART_MONEY:
|
||||
self.decision["choice"] = self.__return_choice(OutcomeKeys.TOP_POINTS)
|
||||
elif self.settings.strategy == Strategy.SMART:
|
||||
difference = abs(
|
||||
self.outcomes[0][OutcomeKeys.PERCENTAGE_USERS]
|
||||
- self.outcomes[1][OutcomeKeys.PERCENTAGE_USERS]
|
||||
)
|
||||
self.decision["choice"] = (
|
||||
self.__return_choice(OutcomeKeys.ODDS)
|
||||
if difference < self.settings.percentage_gap
|
||||
else self.__return_choice(OutcomeKeys.TOTAL_USERS)
|
||||
)
|
||||
|
||||
if self.decision["choice"] is not None:
|
||||
#index = char_decision_as_index(self.decision["choice"])
|
||||
index = self.decision["choice"]
|
||||
self.decision["id"] = self.outcomes[index]["id"]
|
||||
self.decision["amount"] = min(
|
||||
int(balance * (self.settings.percentage / 100)),
|
||||
self.settings.max_points,
|
||||
)
|
||||
if (
|
||||
self.settings.stealth_mode is True
|
||||
and self.decision["amount"]
|
||||
>= self.outcomes[index][OutcomeKeys.TOP_POINTS]
|
||||
):
|
||||
reduce_amount = uniform(1, 5)
|
||||
self.decision["amount"] = (
|
||||
self.outcomes[index][OutcomeKeys.TOP_POINTS] - reduce_amount
|
||||
)
|
||||
self.decision["amount"] = int(self.decision["amount"])
|
||||
return self.decision
|
||||
74
TwitchChannelPointsMiner/classes/entities/Campaign.py
Normal file
74
TwitchChannelPointsMiner/classes/entities/Campaign.py
Normal file
@ -0,0 +1,74 @@
|
||||
from datetime import datetime
|
||||
|
||||
from TwitchChannelPointsMiner.classes.entities.Drop import Drop
|
||||
from TwitchChannelPointsMiner.classes.Settings import Settings
|
||||
|
||||
|
||||
class Campaign(object):
|
||||
__slots__ = [
|
||||
"id",
|
||||
"game",
|
||||
"name",
|
||||
"status",
|
||||
"in_inventory",
|
||||
"end_at",
|
||||
"start_at",
|
||||
"dt_match",
|
||||
"drops",
|
||||
"channels",
|
||||
]
|
||||
|
||||
def __init__(self, dict):
|
||||
self.id = dict["id"]
|
||||
self.game = dict["game"]
|
||||
self.name = dict["name"]
|
||||
self.status = dict["status"]
|
||||
self.channels = (
|
||||
[]
|
||||
if dict["allow"]["channels"] is None
|
||||
else list(map(lambda x: x["id"], dict["allow"]["channels"]))
|
||||
)
|
||||
self.in_inventory = False
|
||||
|
||||
self.end_at = datetime.strptime(dict["endAt"], "%Y-%m-%dT%H:%M:%SZ")
|
||||
self.start_at = datetime.strptime(dict["startAt"], "%Y-%m-%dT%H:%M:%SZ")
|
||||
self.dt_match = self.start_at < datetime.now() < self.end_at
|
||||
|
||||
self.drops = list(map(lambda x: Drop(x), dict["timeBasedDrops"]))
|
||||
|
||||
def __repr__(self):
|
||||
return f"Campaign(id={self.id}, name={self.name}, game={self.game}, in_inventory={self.in_inventory})"
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"{self.name}, Game: {self.game['displayName']} - Drops: {len(self.drops)} pcs. - In inventory: {self.in_inventory}"
|
||||
if Settings.logger.less
|
||||
else self.__repr__()
|
||||
)
|
||||
|
||||
def clear_drops(self):
|
||||
self.drops = list(
|
||||
filter(lambda x: x.dt_match is True and x.is_claimed is False, self.drops)
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return self.id == other.id
|
||||
else:
|
||||
return False
|
||||
|
||||
def sync_drops(self, drops, callback):
|
||||
# Iterate all the drops from inventory
|
||||
for drop in drops:
|
||||
# Iterate all the drops from out campaigns array
|
||||
# After id match update with:
|
||||
# [currentMinutesWatched, hasPreconditionsMet, dropInstanceID, isClaimed]
|
||||
for i in range(len(self.drops)):
|
||||
current_id = self.drops[i].id
|
||||
if drop["id"] == current_id:
|
||||
self.drops[i].update(drop["self"])
|
||||
# If after update we all conditions are meet we can claim the drop
|
||||
if self.drops[i].is_claimable is True:
|
||||
claimed = callback(self.drops[i])
|
||||
self.drops[i].is_claimed = claimed
|
||||
break
|
||||
103
TwitchChannelPointsMiner/classes/entities/Drop.py
Normal file
103
TwitchChannelPointsMiner/classes/entities/Drop.py
Normal file
@ -0,0 +1,103 @@
|
||||
from datetime import datetime
|
||||
|
||||
from TwitchChannelPointsMiner.classes.Settings import Settings
|
||||
from TwitchChannelPointsMiner.utils import percentage
|
||||
|
||||
|
||||
class Drop(object):
|
||||
__slots__ = [
|
||||
"id",
|
||||
"name",
|
||||
"benefit",
|
||||
"minutes_required",
|
||||
"has_preconditions_met",
|
||||
"current_minutes_watched",
|
||||
"drop_instance_id",
|
||||
"is_claimed",
|
||||
"is_claimable",
|
||||
"percentage_progress",
|
||||
"end_at",
|
||||
"start_at",
|
||||
"dt_match",
|
||||
"is_printable",
|
||||
]
|
||||
|
||||
def __init__(self, dict):
|
||||
self.id = dict["id"]
|
||||
self.name = dict["name"]
|
||||
self.benefit = ", ".join(
|
||||
list(set([bf["benefit"]["name"] for bf in dict["benefitEdges"]]))
|
||||
)
|
||||
self.minutes_required = dict["requiredMinutesWatched"]
|
||||
|
||||
self.has_preconditions_met = None # [True, False], None we don't know
|
||||
self.current_minutes_watched = 0
|
||||
self.drop_instance_id = None
|
||||
self.is_claimed = False
|
||||
self.is_claimable = False
|
||||
self.is_printable = False
|
||||
self.percentage_progress = 0
|
||||
|
||||
self.end_at = datetime.strptime(dict["endAt"], "%Y-%m-%dT%H:%M:%SZ")
|
||||
self.start_at = datetime.strptime(dict["startAt"], "%Y-%m-%dT%H:%M:%SZ")
|
||||
self.dt_match = self.start_at < datetime.now() < self.end_at
|
||||
|
||||
def update(
|
||||
self,
|
||||
progress,
|
||||
):
|
||||
self.has_preconditions_met = progress["hasPreconditionsMet"]
|
||||
|
||||
updated_percentage = percentage(
|
||||
progress["currentMinutesWatched"], self.minutes_required
|
||||
)
|
||||
quarter = round((updated_percentage / 25), 4).is_integer()
|
||||
self.is_printable = (
|
||||
# The new currentMinutesWatched are GT than previous
|
||||
progress["currentMinutesWatched"] > self.current_minutes_watched
|
||||
and (
|
||||
# The drop is printable when we have a new updated values and:
|
||||
# - also the percentage It's different and quarter is True (self.current_minutes_watched != 0 for skip boostrap phase)
|
||||
# - or we have watched 1 and the previous value is 0 - We are collecting a new drop :)
|
||||
(
|
||||
updated_percentage > self.percentage_progress
|
||||
and quarter is True
|
||||
and self.current_minutes_watched != 0
|
||||
)
|
||||
or (
|
||||
progress["currentMinutesWatched"] == 1
|
||||
and self.current_minutes_watched == 0
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
self.current_minutes_watched = progress["currentMinutesWatched"]
|
||||
self.drop_instance_id = progress["dropInstanceID"]
|
||||
self.is_claimed = progress["isClaimed"]
|
||||
self.is_claimable = (
|
||||
self.is_claimed is False and self.drop_instance_id is not None
|
||||
)
|
||||
self.percentage_progress = updated_percentage
|
||||
|
||||
def __repr__(self):
|
||||
return f"Drop(id={self.id}, name={self.name}, benefit={self.benefit}, minutes_required={self.minutes_required}, has_preconditions_met={self.has_preconditions_met}, current_minutes_watched={self.current_minutes_watched}, percentage_progress={self.percentage_progress}%, drop_instance_id={self.drop_instance_id}, is_claimed={self.is_claimed})"
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"{self.name} ({self.benefit}) {self.current_minutes_watched}/{self.minutes_required} ({self.percentage_progress}%)"
|
||||
if Settings.logger.less
|
||||
else self.__repr__()
|
||||
)
|
||||
|
||||
def progress_bar(self):
|
||||
progress = self.percentage_progress // 2
|
||||
remaining = (100 - self.percentage_progress) // 2
|
||||
if remaining + progress < 50:
|
||||
remaining += 50 - (remaining + progress)
|
||||
return f"|{('█' * progress)}{(' ' * remaining)}|\t{self.percentage_progress}% [{self.current_minutes_watched}/{self.minutes_required}]"
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return self.id == other.id
|
||||
else:
|
||||
return False
|
||||
94
TwitchChannelPointsMiner/classes/entities/EventPrediction.py
Normal file
94
TwitchChannelPointsMiner/classes/entities/EventPrediction.py
Normal file
@ -0,0 +1,94 @@
|
||||
from TwitchChannelPointsMiner.classes.entities.Bet import Bet
|
||||
from TwitchChannelPointsMiner.classes.entities.Streamer import Streamer
|
||||
from TwitchChannelPointsMiner.classes.Settings import Settings
|
||||
from TwitchChannelPointsMiner.utils import _millify, float_round
|
||||
|
||||
|
||||
class EventPrediction(object):
|
||||
__slots__ = [
|
||||
"streamer",
|
||||
"event_id",
|
||||
"title",
|
||||
"created_at",
|
||||
"prediction_window_seconds",
|
||||
"status",
|
||||
"result",
|
||||
"box_fillable",
|
||||
"bet_confirmed",
|
||||
"bet_placed",
|
||||
"bet",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
streamer: Streamer,
|
||||
event_id,
|
||||
title,
|
||||
created_at,
|
||||
prediction_window_seconds,
|
||||
status,
|
||||
outcomes,
|
||||
):
|
||||
self.streamer = streamer
|
||||
|
||||
self.event_id = event_id
|
||||
self.title = title.strip()
|
||||
self.created_at = created_at
|
||||
self.prediction_window_seconds = prediction_window_seconds
|
||||
self.status = status
|
||||
self.result: dict = {"string": "", "type": None, "gained": 0}
|
||||
|
||||
self.box_fillable = False
|
||||
self.bet_confirmed = False
|
||||
self.bet_placed = False
|
||||
self.bet = Bet(outcomes, streamer.settings.bet)
|
||||
|
||||
def __repr__(self):
|
||||
return f"EventPrediction(event_id={self.event_id}, streamer={self.streamer}, title={self.title})"
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"EventPrediction: {self.streamer} - {self.title}"
|
||||
if Settings.logger.less
|
||||
else self.__repr__()
|
||||
)
|
||||
|
||||
def elapsed(self, timestamp):
|
||||
return float_round((timestamp - self.created_at).total_seconds())
|
||||
|
||||
def closing_bet_after(self, timestamp):
|
||||
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['string']}"
|
||||
|
||||
def parse_result(self, result) -> dict:
|
||||
result_type = result["type"]
|
||||
|
||||
points = {}
|
||||
points["placed"] = (
|
||||
self.bet.decision["amount"] if result_type != "REFUND" else 0
|
||||
)
|
||||
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
|
||||
69
TwitchChannelPointsMiner/classes/entities/Message.py
Normal file
69
TwitchChannelPointsMiner/classes/entities/Message.py
Normal file
@ -0,0 +1,69 @@
|
||||
import json
|
||||
|
||||
from TwitchChannelPointsMiner.utils import server_time
|
||||
|
||||
|
||||
class Message(object):
|
||||
__slots__ = [
|
||||
"topic",
|
||||
"topic_user",
|
||||
"message",
|
||||
"type",
|
||||
"data",
|
||||
"timestamp",
|
||||
"channel_id",
|
||||
"identifier",
|
||||
]
|
||||
|
||||
def __init__(self, data):
|
||||
self.topic, self.topic_user = data["topic"].split(".")
|
||||
|
||||
self.message = json.loads(data["message"])
|
||||
self.type = self.message["type"]
|
||||
|
||||
self.data = self.message["data"] if "data" in self.message else None
|
||||
|
||||
self.timestamp = self.__get_timestamp()
|
||||
self.channel_id = self.__get_channel_id()
|
||||
|
||||
self.identifier = f"{self.type}.{self.topic}.{self.channel_id}"
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.message}"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.message}"
|
||||
|
||||
def __get_timestamp(self):
|
||||
return (
|
||||
server_time(self.message)
|
||||
if self.data is None
|
||||
else (
|
||||
self.data["timestamp"]
|
||||
if "timestamp" in self.data
|
||||
else server_time(self.data)
|
||||
)
|
||||
)
|
||||
|
||||
def __get_channel_id(self):
|
||||
return (
|
||||
self.topic_user
|
||||
if self.data is None
|
||||
else (
|
||||
self.data["prediction"]["channel_id"]
|
||||
if "prediction" in self.data
|
||||
else (
|
||||
self.data["claim"]["channel_id"]
|
||||
if "claim" in self.data
|
||||
else (
|
||||
self.data["channel_id"]
|
||||
if "channel_id" in self.data
|
||||
else (
|
||||
self.data["balance"]["channel_id"]
|
||||
if "balance" in self.data
|
||||
else self.topic_user
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
16
TwitchChannelPointsMiner/classes/entities/PubsubTopic.py
Normal file
16
TwitchChannelPointsMiner/classes/entities/PubsubTopic.py
Normal file
@ -0,0 +1,16 @@
|
||||
class PubsubTopic(object):
|
||||
__slots__ = ["topic", "user_id", "streamer"]
|
||||
|
||||
def __init__(self, topic, user_id=None, streamer=None):
|
||||
self.topic = topic
|
||||
self.user_id = user_id
|
||||
self.streamer = streamer
|
||||
|
||||
def is_user_topic(self):
|
||||
return self.streamer is None
|
||||
|
||||
def __str__(self):
|
||||
if self.is_user_topic():
|
||||
return f"{self.topic}.{self.user_id}"
|
||||
else:
|
||||
return f"{self.topic}.{self.streamer.channel_id}"
|
||||
12
TwitchChannelPointsMiner/classes/entities/Raid.py
Normal file
12
TwitchChannelPointsMiner/classes/entities/Raid.py
Normal file
@ -0,0 +1,12 @@
|
||||
class Raid(object):
|
||||
__slots__ = ["raid_id", "target_login"]
|
||||
|
||||
def __init__(self, raid_id, target_login):
|
||||
self.raid_id = raid_id
|
||||
self.target_login = target_login
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return self.raid_id == other.raid_id
|
||||
else:
|
||||
return False
|
||||
107
TwitchChannelPointsMiner/classes/entities/Stream.py
Normal file
107
TwitchChannelPointsMiner/classes/entities/Stream.py
Normal file
@ -0,0 +1,107 @@
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from base64 import b64encode
|
||||
|
||||
from TwitchChannelPointsMiner.classes.Settings import Settings
|
||||
from TwitchChannelPointsMiner.constants import DROP_ID
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Stream(object):
|
||||
__slots__ = [
|
||||
"broadcast_id",
|
||||
"title",
|
||||
"game",
|
||||
"tags",
|
||||
"drops_tags",
|
||||
"campaigns",
|
||||
"campaigns_ids",
|
||||
"viewers_count",
|
||||
"spade_url",
|
||||
"payload",
|
||||
"watch_streak_missing",
|
||||
"minute_watched",
|
||||
"__last_update",
|
||||
"__minute_watched_timestamp",
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self.broadcast_id = None
|
||||
|
||||
self.title = None
|
||||
self.game = {}
|
||||
self.tags = []
|
||||
|
||||
self.drops_tags = False
|
||||
self.campaigns = []
|
||||
self.campaigns_ids = []
|
||||
|
||||
self.viewers_count = 0
|
||||
self.__last_update = 0
|
||||
|
||||
self.spade_url = None
|
||||
self.payload = None
|
||||
|
||||
self.init_watch_streak()
|
||||
|
||||
def encode_payload(self) -> dict:
|
||||
json_event = json.dumps(self.payload, separators=(",", ":"))
|
||||
return {"data": (b64encode(json_event.encode("utf-8"))).decode("utf-8")}
|
||||
|
||||
def update(self, broadcast_id, title, game, tags, viewers_count):
|
||||
self.broadcast_id = broadcast_id
|
||||
self.title = title.strip()
|
||||
self.game = game
|
||||
# #343 temporary workaround
|
||||
self.tags = tags or []
|
||||
# ------------------------
|
||||
self.viewers_count = viewers_count
|
||||
|
||||
self.drops_tags = (
|
||||
DROP_ID in [tag["id"] for tag in self.tags] and self.game != {}
|
||||
)
|
||||
self.__last_update = time.time()
|
||||
|
||||
logger.debug(f"Update: {self}")
|
||||
|
||||
def __repr__(self):
|
||||
return f"Stream(title={self.title}, game={self.__str_game()}, tags={self.__str_tags()})"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title}" if Settings.logger.less else self.__repr__()
|
||||
|
||||
def __str_tags(self):
|
||||
return (
|
||||
None
|
||||
if self.tags == []
|
||||
else ", ".join([tag["localizedName"] for tag in self.tags])
|
||||
)
|
||||
|
||||
def __str_game(self):
|
||||
return None if self.game in [{}, None] else self.game["displayName"]
|
||||
|
||||
def game_name(self):
|
||||
return None if self.game in [{}, None] else self.game["name"]
|
||||
|
||||
def game_id(self):
|
||||
return None if self.game in [{}, None] else self.game["id"]
|
||||
|
||||
def update_required(self):
|
||||
return self.__last_update == 0 or self.update_elapsed() >= 120
|
||||
|
||||
def update_elapsed(self):
|
||||
return 0 if self.__last_update == 0 else (time.time() - self.__last_update)
|
||||
|
||||
def init_watch_streak(self):
|
||||
self.watch_streak_missing = True
|
||||
self.minute_watched = 0
|
||||
self.__minute_watched_timestamp = 0
|
||||
|
||||
def update_minute_watched(self):
|
||||
if self.__minute_watched_timestamp != 0:
|
||||
self.minute_watched += round(
|
||||
(time.time() - self.__minute_watched_timestamp) / 60, 5
|
||||
)
|
||||
self.__minute_watched_timestamp = time.time()
|
||||
284
TwitchChannelPointsMiner/classes/entities/Streamer.py
Normal file
284
TwitchChannelPointsMiner/classes/entities/Streamer.py
Normal file
@ -0,0 +1,284 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user