Compare commits

...
1 Commits
Author SHA1 Message Date
Roman Davydov 177501c269 minimal version 2023-03-24 16:49:20 +03:00
24 changed files with 12 additions and 2195 deletions
@@ -53,14 +53,11 @@ class TwitchChannelPointsMiner:
__slots__ = [
"username",
"twitch",
"claim_drops_startup",
"enable_analytics",
"disable_ssl_cert_verification",
"priority",
"streamers",
"events_predictions",
"minute_watcher_thread",
"sync_campaigns_thread",
"ws_pool",
"session_id",
"running",
@@ -74,7 +71,6 @@ class TwitchChannelPointsMiner:
self,
username: str,
password: str = None,
claim_drops_startup: bool = False,
enable_analytics: bool = False,
disable_ssl_cert_verification: bool = False,
# Settings for logging and selenium as you can see.
@@ -128,20 +124,18 @@ class TwitchChannelPointsMiner:
# Init as default all the missing values
streamer_settings.default()
streamer_settings.bet.default()
Settings.streamer_settings = streamer_settings
# user_agent = get_user_agent("FIREFOX")
user_agent = get_user_agent("CHROME")
self.twitch = Twitch(self.username, user_agent, password)
self.claim_drops_startup = claim_drops_startup
self.priority = priority if isinstance(priority, list) else [priority]
self.streamers = []
self.events_predictions = {}
self.minute_watcher_thread = None
self.sync_campaigns_thread = None
self.ws_pool = None
self.session_id = str(uuid.uuid4())
@@ -221,9 +215,6 @@ class TwitchChannelPointsMiner:
self.twitch.login()
if self.claim_drops_startup is True:
self.twitch.claim_all_drops_from_inventory()
streamers_name: list = []
streamers_dict: dict = {}
@@ -267,9 +258,7 @@ class TwitchChannelPointsMiner:
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.chat != ChatPresence.NEVER:
streamer.irc_chat = ThreadChat(
self.username,
@@ -298,24 +287,9 @@ class TwitchChannelPointsMiner:
]
# If we have at least one streamer with settings = make_predictions True
make_predictions = at_least_one_value_in_settings_is(
self.streamers, "make_predictions", True
)
# If we have at least one streamer with settings = claim_drops True
# Spawn a thread for sync inventory and dashboard
if (
at_least_one_value_in_settings_is(
self.streamers, "claim_drops", True)
is True
):
self.sync_campaigns_thread = threading.Thread(
target=self.twitch.sync_campaigns,
args=(self.streamers,),
)
self.sync_campaigns_thread.name = "Sync campaigns/inventory"
self.sync_campaigns_thread.start()
time.sleep(30)
self.minute_watcher_thread = threading.Thread(
target=self.twitch.send_minute_watched_events,
@@ -327,7 +301,7 @@ class TwitchChannelPointsMiner:
self.ws_pool = WebSocketsPool(
twitch=self.twitch,
streamers=self.streamers,
events_predictions=self.events_predictions,
)
# Subscribe to community-points-user. Get update for points spent or gains
@@ -347,13 +321,6 @@ class TwitchChannelPointsMiner:
)
# Going to subscribe to predictions-user-v1. Get update when we place a new prediction (confirm)
if make_predictions is True:
self.ws_pool.submit(
PubsubTopic(
"predictions-user-v1",
user_id=user_id,
)
)
for streamer in self.streamers:
self.ws_pool.submit(
@@ -363,18 +330,6 @@ class TwitchChannelPointsMiner:
if streamer.settings.follow_raid is True:
self.ws_pool.submit(PubsubTopic("raid", streamer=streamer))
if streamer.settings.make_predictions is True:
self.ws_pool.submit(
PubsubTopic("predictions-channel-v1",
streamer=streamer)
)
if streamer.settings.claim_moments is True:
self.ws_pool.submit(
PubsubTopic("community-moments-channel-v1",
streamer=streamer)
)
refresh_context = time.time()
while self.running:
time.sleep(random.uniform(20, 60))
@@ -419,9 +374,6 @@ class TwitchChannelPointsMiner:
if self.minute_watcher_thread is not None:
self.minute_watcher_thread.join()
if self.sync_campaigns_thread is not None:
self.sync_campaigns_thread.join()
# Check if all the mutex are unlocked.
# Prevent breaks of .json file
for streamer in self.streamers:
@@ -450,28 +402,6 @@ class TwitchChannelPointsMiner:
extra={"emoji": ":hourglass:"},
)
if self.events_predictions != {}:
print("")
for event_id in self.events_predictions:
event = self.events_predictions[event_id]
if (
event.bet_confirmed is True
and event.streamer.settings.make_predictions is True
):
logger.info(
f"{event.streamer.settings.bet}",
extra={"emoji": ":wrench:"},
)
if event.streamer.settings.bet.filter_condition is not None:
logger.info(
f"{event.streamer.settings.bet.filter_condition}",
extra={"emoji": ":pushpin:"},
)
logger.info(
f"{event.print_recap()}",
extra={"emoji": ":bar_chart:"},
)
print("")
for streamer_index in range(0, len(self.streamers)):
if self.streamers[streamer_index].history != {}:
@@ -1,235 +0,0 @@
import json
import logging
import os
from datetime import datetime
from pathlib import Path
from threading import Thread
import pandas as pd
from flask import Flask, Response, cli, render_template, request
from TwitchChannelPointsMiner.classes.Settings import Settings
from TwitchChannelPointsMiner.utils import download_file
cli.show_server_banner = lambda *_: None
logger = logging.getLogger(__name__)
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 aggregate(df, freq="30Min"):
df_base_events = df[(df.z == "Watch") | (df.z == "Claim")]
df_other_events = df[(df.z != "Watch") & (df.z != "Claim")]
be = df_base_events.groupby([pd.Grouper(freq=freq, key="datetime"), "z"]).max()
be = be.reset_index()
oe = df_other_events.groupby([pd.Grouper(freq=freq, key="datetime"), "z"]).max()
oe = oe.reset_index()
result = pd.concat([be, oe])
return result
def filter_datas(start_date, end_date, datas):
# Note: https://stackoverflow.com/questions/4676195/why-do-i-need-to-multiply-unix-timestamps-by-1000-in-javascript
start_date = (
datetime.strptime(start_date, "%Y-%m-%d").timestamp() * 1000
if start_date is not None
else 0
)
end_date = (
datetime.strptime(end_date, "%Y-%m-%d")
if end_date is not None
else datetime.now()
).replace(hour=23, minute=59, second=59).timestamp() * 1000
if "series" in datas:
df = pd.DataFrame(datas["series"])
df["datetime"] = pd.to_datetime(df.x // 1000, unit="s")
df = df[(df.x >= start_date) & (df.x <= end_date)]
datas["series"] = (
df.drop(columns="datetime")
.sort_values(by=["x", "y"], ascending=True)
.to_dict("records")
)
else:
datas["series"] = []
if "annotations" in datas:
df = pd.DataFrame(datas["annotations"])
df["datetime"] = pd.to_datetime(df.x // 1000, unit="s")
df = df[(df.x >= start_date) & (df.x <= end_date)]
datas["annotations"] = (
df.drop(columns="datetime")
.sort_values(by="x", ascending=True)
.to_dict("records")
)
else:
datas["annotations"] = []
return datas
def read_json(streamer, return_response=True):
start_date = request.args.get("startDate", type=str)
end_date = request.args.get("endDate", type=str)
path = Settings.analytics_path
streamer = streamer if streamer.endswith(".json") else f"{streamer}.json"
if return_response is True:
return Response(
json.dumps(
filter_datas(
start_date, end_date, json.load(open(os.path.join(path, streamer)))
)
)
if streamer in streamers_available()
else [],
status=200,
mimetype="application/json",
)
return (
json.load(open(os.path.join(path, streamer)))
if streamer in streamers_available()
else []
)
def get_challenge_points(streamer):
datas = read_json(streamer, return_response=False)
if datas != {}:
return datas["series"][-1]["y"]
return 0
def get_last_activity(streamer):
datas = read_json(streamer, return_response=False)
if datas != {}:
return datas["series"][-1]["x"]
return 0
def json_all():
return Response(
json.dumps(
[
{
"name": streamer.strip(".json"),
"data": read_json(streamer, return_response=False),
}
for streamer in streamers_available()
]
),
status=200,
mimetype="application/json",
)
def index(refresh=5, days_ago=7):
return render_template(
"charts.html",
refresh=(refresh * 60 * 1000),
daysAgo=days_ago,
)
def streamers():
return Response(
json.dumps(
[
{"name": s, "points": get_challenge_points(s), "last_activity": get_last_activity(s)}
for s in sorted(streamers_available())
]
),
status=200,
mimetype="application/json",
)
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",
"script.js",
"style.css",
"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,
days_ago: int = 7,
):
super(AnalyticsServer, self).__init__()
check_assets()
self.host = host
self.port = port
self.refresh = refresh
self.days_ago = days_ago
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,
defaults={"refresh": refresh, "days_ago": days_ago},
methods=["GET"],
)
self.app.add_url_rule("/streamers", "streamers", streamers, methods=["GET"])
self.app.add_url_rule(
"/json/<string:streamer>", "json", read_json, methods=["GET"]
)
self.app.add_url_rule("/json_all", "json_all", json_all, methods=["GET"])
def run(self):
logger.info(
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, debug=False)
@@ -1,24 +0,0 @@
from textwrap import dedent
import requests
from TwitchChannelPointsMiner.classes.Settings import Events
class Discord(object):
__slots__ = ["webhook_api", "events"]
def __init__(self, webhook_api: str, events: list):
self.webhook_api = webhook_api
self.events = [str(e) for e in events]
def send(self, message: str, event: Events) -> None:
if str(event) in self.events:
requests.post(
url=self.webhook_api,
data={
"content": dedent(message),
"username": "Twitch Channel Points Miner",
"avatar_url": "https://i.imgur.com/X9fEkhT.png",
},
)
@@ -41,8 +41,6 @@ class Events(Enum):
BONUS_CLAIM = auto()
MOMENT_CLAIM = auto()
JOIN_RAID = auto()
DROP_CLAIM = auto()
DROP_STATUS = auto()
def __str__(self):
return self.name
@@ -1,29 +0,0 @@
from textwrap import dedent
import requests
from TwitchChannelPointsMiner.classes.Settings import Events
class Telegram(object):
__slots__ = ["chat_id", "telegram_api", "events", "disable_notification"]
def __init__(
self, chat_id: int, token: str, events: list, disable_notification: bool = False
):
self.chat_id = chat_id
self.telegram_api = f"https://api.telegram.org/bot{token}/sendMessage"
self.events = [str(e) for e in events]
self.disable_notification = disable_notification
def send(self, message: str, event: Events) -> None:
if str(event) in self.events:
requests.post(
url=self.telegram_api,
data={
"chat_id": self.chat_id,
"text": dedent(message),
"disable_web_page_preview": True, # include link to twitch streamer?
"disable_notification": self.disable_notification, # no sound, notif just in tray
},
)
-317
View File
@@ -11,17 +11,11 @@ import random
import re
import string
import time
from datetime import datetime
from pathlib import Path
from secrets import choice, token_hex
import json
from base64 import urlsafe_b64decode
import requests
from TwitchChannelPointsMiner.classes.entities.Campaign import Campaign
from TwitchChannelPointsMiner.classes.entities.Drop import Drop
from TwitchChannelPointsMiner.classes.Exceptions import (
StreamerDoesNotExistException,
StreamerIsOfflineException,
@@ -110,16 +104,6 @@ class Twitch(object):
"user_id": self.twitch_login.get_user_id(),
}
if (
streamer.stream.game_name() is not None
and streamer.settings.claim_drops is True
):
event_properties["game"] = streamer.stream.game_name()
# Update also the campaigns_ids so we are sure to tracking the correct campaign
streamer.stream.campaigns_ids = (
self.__get_campaign_ids_from_streamer(streamer)
)
streamer.stream.payload = [
{"event": "minute-watched", "properties": event_properties}
]
@@ -435,13 +419,6 @@ class Twitch(object):
if len(streamers_watching) == 2:
break
elif prior == Priority.DROPS and len(streamers_watching) < 2:
for index in streamers_index:
if streamers[index].drops_condition() is True:
streamers_watching.append(index)
if len(streamers_watching) == 2:
break
elif prior == Priority.SUBSCRIBED and len(streamers_watching) < 2:
streamers_with_multiplier = [
index
@@ -484,41 +461,6 @@ class Twitch(object):
For time-based Drops, if you are unable to claim the Drop in time, you will be able to claim it from the inventory page until the Drops campaign ends.
"""
for campaign in streamers[index].stream.campaigns:
for drop in campaign.drops:
# We could add .has_preconditions_met condition inside is_printable
if (
drop.has_preconditions_met is not False
and drop.is_printable is True
):
drop_messages = [
f"{streamers[index]} is streaming {streamers[index].stream}",
f"Campaign: {campaign}",
f"Drop: {drop}",
f"{drop.progress_bar()}",
]
for single_line in drop_messages:
logger.info(
single_line,
extra={
"event": Events.DROP_STATUS,
"skip_telegram": True,
"skip_discord": True,
},
)
if Settings.logger.telegram is not None:
Settings.logger.telegram.send(
"\n".join(drop_messages),
Events.DROP_STATUS,
)
if Settings.logger.discord is not None:
Settings.logger.discord.send(
"\n".join(drop_messages),
Events.DROP_STATUS,
)
except requests.exceptions.ConnectionError as e:
logger.error(
f"Error while trying to send minute watched: {e}")
@@ -556,86 +498,6 @@ class Twitch(object):
self.claim_bonus(
streamer, community_points["availableClaim"]["id"])
def make_predictions(self, event):
decision = event.bet.calculate(event.streamer.channel_points)
# selector_index = 0 if decision["choice"] == "A" else 1
logger.info(
f"Going to complete bet for {event}",
extra={
"emoji": ":four_leaf_clover:",
"event": Events.BET_GENERAL,
},
)
if event.status == "ACTIVE":
skip, compared_value = event.bet.skip()
if skip is True:
logger.info(
f"Skip betting for the event {event}",
extra={
"emoji": ":pushpin:",
"event": Events.BET_FILTERS,
},
)
logger.info(
f"Skip settings {event.bet.settings.filter_condition}, current value is: {compared_value}",
extra={
"emoji": ":pushpin:",
"event": Events.BET_FILTERS,
},
)
else:
if decision["amount"] >= 10:
logger.info(
# f"Place {_millify(decision['amount'])} channel points on: {event.bet.get_outcome(selector_index)}",
f"Place {_millify(decision['amount'])} channel points on: {event.bet.get_outcome(decision['choice'])}",
extra={
"emoji": ":four_leaf_clover:",
"event": Events.BET_GENERAL,
},
)
json_data = copy.deepcopy(GQLOperations.MakePrediction)
json_data["variables"] = {
"input": {
"eventID": event.event_id,
"outcomeID": decision["id"],
"points": decision["amount"],
"transactionID": token_hex(16),
}
}
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:",
"event": Events.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:",
"event": Events.BET_GENERAL,
},
)
else:
logger.info(
f"Oh no! The event is not active anymore! Current status: {event.status}",
extra={
"emoji": ":disappointed_relieved:",
"event": Events.BET_FAILED,
},
)
def claim_bonus(self, streamer, claim_id):
if Settings.logger.less is False:
logger.info(
@@ -650,184 +512,5 @@ class Twitch(object):
self.post_gql_request(json_data)
# === MOMENTS === #
def claim_moment(self, streamer, moment_id):
if Settings.logger.less is False:
logger.info(
f"Claiming the moment for {streamer}!",
extra={"emoji": ":video_camera:",
"event": Events.MOMENT_CLAIM},
)
json_data = copy.deepcopy(GQLOperations.CommunityMomentCallout_Claim)
json_data["variables"] = {
"input": {"momentID": moment_id}
}
self.post_gql_request(json_data)
# === CAMPAIGNS / DROPS / INVENTORY === #
def __get_campaign_ids_from_streamer(self, streamer):
json_data = copy.deepcopy(
GQLOperations.DropsHighlightService_AvailableDrops)
json_data["variables"] = {"channelID": streamer.channel_id}
response = self.post_gql_request(json_data)
try:
return (
[]
if response["data"]["channel"]["viewerDropCampaigns"] is None
else [
item["id"]
for item in response["data"]["channel"]["viewerDropCampaigns"]
]
)
except (ValueError, KeyError):
return []
def __get_inventory(self):
response = self.post_gql_request(GQLOperations.Inventory)
try:
return (
response["data"]["currentUser"]["inventory"] if response != {} else {}
)
except (ValueError, KeyError, TypeError):
return {}
def __get_drops_dashboard(self, status=None):
response = self.post_gql_request(GQLOperations.ViewerDropsDashboard)
campaigns = response["data"]["currentUser"]["dropCampaigns"]
if status is not None:
campaigns = list(
filter(lambda x: x["status"] == status.upper(), campaigns))
return campaigns
def __get_campaigns_details(self, campaigns):
result = []
chunks = create_chunks(campaigns, 20)
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)
for r in response:
if r["data"]["user"] is not None:
result.append(r["data"]["user"]["dropCampaign"])
return result
def __sync_campaigns(self, campaigns):
# We need the inventory only for get the real updated value/progress
# Get data from inventory and sync current status with streamers.campaigns
inventory = self.__get_inventory()
if inventory not in [None, {}] and inventory["dropCampaignsInProgress"] not in [
None,
{},
]:
# Iterate all campaigns from dashboard (only active, with working drops)
# In this array we have also the campaigns never started from us (not in nventory)
for i in range(len(campaigns)):
campaigns[i].clear_drops() # Remove all the claimed drops
# Iterate all campaigns currently in progress from out inventory
for progress in inventory["dropCampaignsInProgress"]:
if progress["id"] == campaigns[i].id:
campaigns[i].in_inventory = True
campaigns[i].sync_drops(
progress["timeBasedDrops"], self.claim_drop
)
# Remove all the claimed drops
campaigns[i].clear_drops()
break
return campaigns
def claim_drop(self, drop):
logger.info(
f"Claim {drop}", extra={"emoji": ":package:", "event": Events.DROP_CLAIM}
)
json_data = copy.deepcopy(GQLOperations.DropsPage_ClaimDropRewards)
json_data["variables"] = {
"input": {"dropInstanceID": drop.drop_instance_id}}
response = self.post_gql_request(json_data)
try:
# 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
def claim_all_drops_from_inventory(self):
inventory = self.__get_inventory()
if inventory not in [None, {}]:
if inventory["dropCampaignsInProgress"] not in [None, {}]:
for campaign in inventory["dropCampaignsInProgress"]:
for drop_dict in campaign["timeBasedDrops"]:
drop = Drop(drop_dict)
drop.update(drop_dict["self"])
if drop.is_claimable is True:
drop.is_claimed = self.claim_drop(drop)
time.sleep(random.uniform(5, 10))
def sync_campaigns(self, streamers, chunk_size=3):
campaigns_update = 0
while self.running:
try:
# Get update from dashboard each 60minutes
if (
campaigns_update == 0
or ((time.time() - campaigns_update) / 60) > 60
):
campaigns_update = time.time()
# Get full details from current ACTIVE campaigns
# Use dashboard so we can explore new drops not currently active in our Inventory
campaigns_details = self.__get_campaigns_details(
self.__get_drops_dashboard(status="ACTIVE")
)
campaigns = []
# Going to clear array and structure. Remove all the timeBasedDrops expired or not started yet
for index in range(0, len(campaigns_details)):
campaign = Campaign(campaigns_details[index])
if campaign.dt_match is True:
# Remove all the drops already claimed or with dt not matching
campaign.clear_drops()
if campaign.drops != []:
campaigns.append(campaign)
# Divide et impera :)
campaigns = self.__sync_campaigns(campaigns)
# Check if user It's currently streaming the same game present in campaigns_details
for i in range(0, len(streamers)):
if streamers[i].drops_condition() is True:
# yes! The streamer[i] have the drops_tags enabled and we It's currently stream a game with campaign active!
# With 'campaigns_ids' we are also sure that this streamer have the campaign active.
# yes! The streamer[index] have the drops_tags enabled and we It's currently stream a game with campaign active!
streamers[i].stream.campaigns = list(
filter(
lambda x: x.drops != []
and x.game == streamers[i].stream.game
and x.id in streamers[i].stream.campaigns_ids,
campaigns,
)
)
except (ValueError, KeyError, requests.exceptions.ConnectionError) as e:
logger.error(f"Error while syncing inventory: {e}")
self.__check_connection_handler(chunk_size)
self.__chuncked_sleep(60, chunk_size=chunk_size)
@@ -27,7 +27,6 @@ class TwitchWebSocket(WebSocketApp):
self.twitch = parent_pool.twitch
self.streamers = parent_pool.streamers
self.events_predictions = parent_pool.events_predictions
self.last_message_timestamp = None
self.last_message_type_channel = None
@@ -2,11 +2,8 @@ import json
import logging
import random
import time
from threading import Thread, Timer
from threading import Thread # , Timer
from dateutil import parser
from TwitchChannelPointsMiner.classes.entities.EventPrediction import EventPrediction
from TwitchChannelPointsMiner.classes.entities.Message import Message
from TwitchChannelPointsMiner.classes.entities.Raid import Raid
from TwitchChannelPointsMiner.classes.Settings import Events, Settings
@@ -21,13 +18,12 @@ logger = logging.getLogger(__name__)
class WebSocketsPool:
__slots__ = ["ws", "twitch", "streamers", "events_predictions"]
__slots__ = ["ws", "twitch", "streamers"]
def __init__(self, twitch, streamers, events_predictions):
def __init__(self, twitch, streamers):
self.ws = []
self.twitch = twitch
self.streamers = streamers
self.events_predictions = events_predictions
"""
API Limits
@@ -241,158 +237,6 @@ class WebSocketsPool:
)
ws.twitch.update_raid(
ws.streamers[streamer_index], raid)
elif message.topic == "community-moments-channel-v1":
if message.type == "active":
ws.twitch.claim_moment(
ws.streamers[streamer_index],
message.data["moment_id"]
)
elif message.topic == "predictions-channel-v1":
event_dict = message.data["event"]
event_id = event_dict["id"]
event_status = event_dict["status"]
current_tmsp = parser.parse(message.timestamp)
if (
message.type == "event-created"
and event_id not in ws.events_predictions
):
if event_status == "ACTIVE":
prediction_window_seconds = float(
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)
event = EventPrediction(
ws.streamers[streamer_index],
event_id,
event_dict["title"],
parser.parse(event_dict["created_at"]),
prediction_window_seconds,
event_status,
event_dict["outcomes"],
)
if (
ws.streamers[streamer_index].is_online
and event.closing_bet_after(current_tmsp) > 0
):
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 = 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:",
"event": Events.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:",
"event": Events.BET_FILTERS,
},
)
elif (
message.type == "event-updated"
and event_id in ws.events_predictions
):
ws.events_predictions[event_id].status = event_status
# Game over we can't update anymore the values... The bet was placed!
if (
ws.events_predictions[event_id].bet_placed is False
and ws.events_predictions[event_id].bet.decision == {}
):
ws.events_predictions[event_id].bet.update_outcomes(
event_dict["outcomes"]
)
elif message.topic == "predictions-user-v1":
event_id = message.data["prediction"]["event_id"]
if event_id in ws.events_predictions:
event_prediction = ws.events_predictions[event_id]
if (
message.type == "prediction-result"
and event_prediction.bet_confirmed
):
points = event_prediction.parse_result(
message.data["prediction"]["result"]
)
decision = event_prediction.bet.get_decision()
choice = event_prediction.bet.decision["choice"]
logger.info(
(
f"{event_prediction} - Decision: {choice}: {decision['title']} "
f"({decision['color']}) - Result: {event_prediction.result['string']}"
),
extra={
"emoji": ":bar_chart:",
"event": Events.get(
f"BET_{event_prediction.result['type']}"
),
},
)
ws.streamers[streamer_index].update_history(
"PREDICTION", points["gained"]
)
# Remove duplicate history records from previous message sent in community-points-user-v1
if event_prediction.result["type"] == "REFUND":
ws.streamers[streamer_index].update_history(
"REFUND",
-points["placed"],
counter=-1,
)
elif event_prediction.result["type"] == "WIN":
ws.streamers[streamer_index].update_history(
"PREDICTION",
-points["won"],
counter=-1,
)
if event_prediction.result["type"] != "LOSE":
# Analytics switch
if Settings.enable_analytics is True:
ws.streamers[streamer_index].persistent_annotations(
event_prediction.result["type"],
f"{ws.events_predictions[event_id].title}",
)
elif message.type == "prediction-made":
event_prediction.bet_confirmed = True
# Analytics switch
if Settings.enable_analytics is True:
ws.streamers[streamer_index].persistent_annotations(
"PREDICTION_MADE",
f"Decision: {event_prediction.bet.decision['choice']} - {event_prediction.title}",
)
except Exception:
logger.error(
f"Exception raised for topic: {message.topic} and message: {message}",
@@ -1,315 +0,0 @@
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
@@ -1,74 +0,0 @@
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
@@ -1,103 +0,0 @@
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
@@ -1,94 +0,0 @@
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
@@ -4,7 +4,6 @@ import time
from base64 import b64encode
from TwitchChannelPointsMiner.classes.Settings import Settings
from TwitchChannelPointsMiner.constants import DROP_ID
logger = logging.getLogger(__name__)
@@ -15,9 +14,6 @@ class Stream(object):
"title",
"game",
"tags",
"drops_tags",
"campaigns",
"campaigns_ids",
"viewers_count",
"spade_url",
"payload",
@@ -34,10 +30,6 @@ class Stream(object):
self.game = {}
self.tags = []
self.drops_tags = False
self.campaigns = []
self.campaigns_ids = []
self.viewers_count = 0
self.__last_update = 0
@@ -57,9 +49,6 @@ class Stream(object):
self.tags = tags
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}")
@@ -1,12 +1,8 @@
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
@@ -17,50 +13,33 @@ 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})"
return f"follow_raid={self.follow_raid}, watch_streak={self.watch_streak}, chat={self.chat})"
class Streamer(object):
@@ -169,14 +148,6 @@ class Streamer(object):
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
@@ -192,59 +163,6 @@ 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 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"]:
primary_color = (
"#45c1ff"
if event_type == "WATCH_STREAK"
else ("#ffe045" if event_type == "PREDICTION_MADE" else "#54ff45")
)
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")
with self.mutex:
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, open(fname, "w"), indent=4)
def leave_chat(self):
if self.irc_chat is not None:
self.irc_chat.stop()
+4 -129
View File
@@ -2,76 +2,27 @@ import logging
import os
import platform
import queue
import pytz
from datetime import datetime
from logging.handlers import QueueHandler, QueueListener, TimedRotatingFileHandler
from pathlib import Path
import emoji
from colorama import Fore, init
# from colorama import Fore, init
from TwitchChannelPointsMiner.classes.Discord import Discord
from TwitchChannelPointsMiner.classes.Settings import Events
from TwitchChannelPointsMiner.classes.Telegram import Telegram
from TwitchChannelPointsMiner.utils import remove_emoji
# Fore: BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, RESET.
class ColorPalette(object):
def __init__(self, **kwargs):
# Init with default values RESET for all and GREEN and RED only for WIN and LOSE bet
# Then set args from kwargs
for k in Events:
setattr(self, str(k), Fore.RESET)
setattr(self, "BET_WIN", Fore.GREEN)
setattr(self, "BET_LOSE", Fore.RED)
for k in kwargs:
if k.upper() in dir(self) and getattr(self, k.upper()) is not None:
if kwargs[k] in [
Fore.BLACK,
Fore.RED,
Fore.GREEN,
Fore.YELLOW,
Fore.BLUE,
Fore.MAGENTA,
Fore.CYAN,
Fore.WHITE,
Fore.RESET,
]:
setattr(self, k.upper(), kwargs[k])
elif kwargs[k].upper() in [
"BLACK",
"RED",
"GREEN",
"YELLOW",
"BLUE",
"MAGENTA",
"CYAN",
"WHITE",
"RESET",
]:
setattr(self, k.upper(), getattr(Fore, kwargs[k].upper()))
def get(self, key):
color = getattr(self, str(key)) if str(key) in dir(self) else None
return Fore.RESET if color is None else color
class LoggerSettings:
__slots__ = [
"save",
"less",
"console_level",
"console_username",
"time_zone",
"file_level",
"emoji",
"colored",
"color_palette",
"auto_clear",
"telegram",
"discord"
]
def __init__(
@@ -80,71 +31,27 @@ class LoggerSettings:
less: bool = False,
console_level: int = logging.INFO,
console_username: bool = False,
time_zone: str or None = None,
file_level: int = logging.DEBUG,
emoji: bool = platform.system() != "Windows",
colored: bool = False,
color_palette: ColorPalette = ColorPalette(),
auto_clear: bool = True,
telegram: Telegram or None = None,
discord: Discord or None = None,
):
self.save = save
self.less = less
self.console_level = console_level
self.console_username = console_username
self.time_zone = time_zone
self.file_level = file_level
self.emoji = emoji
self.colored = colored
self.color_palette = color_palette
# self.color_palette = color_palette
self.auto_clear = auto_clear
self.telegram = telegram
self.discord = discord
class FileFormatter(logging.Formatter):
def __init__(self, *, fmt, settings: LoggerSettings, datefmt=None):
self.settings = settings
self.timezone = None
if settings.time_zone:
try:
self.timezone = pytz.timezone(settings.time_zone)
logging.info(f"File logger time zone set to: {self.timezone}")
except pytz.UnknownTimeZoneError:
logging.error(
f"File logger: invalid time zone: {settings.time_zone}")
logging.Formatter.__init__(self, fmt=fmt, datefmt=datefmt)
def formatTime(self, record, datefmt=None):
if self.timezone:
dt = datetime.fromtimestamp(record.created, self.timezone)
else:
dt = datetime.fromtimestamp(record.created)
return dt.strftime(datefmt or self.default_time_format)
class GlobalFormatter(logging.Formatter):
def __init__(self, *, fmt, settings: LoggerSettings, datefmt=None):
self.settings = settings
self.timezone = None
if settings.time_zone:
try:
self.timezone = pytz.timezone(settings.time_zone)
logging.info(
f"Console logger time zone set to: {self.timezone}")
except pytz.UnknownTimeZoneError:
logging.error(
f"Console logger: invalid time zone: {settings.time_zone}")
logging.Formatter.__init__(self, fmt=fmt, datefmt=datefmt)
def formatTime(self, record, datefmt=None):
if self.timezone:
dt = datetime.fromtimestamp(record.created, self.timezone)
else:
dt = datetime.fromtimestamp(record.created)
return dt.strftime(datefmt or self.default_time_format)
def format(self, record):
record.emoji_is_present = (
record.emoji_is_present if hasattr(
@@ -169,9 +76,6 @@ class GlobalFormatter(logging.Formatter):
record.msg = remove_emoji(record.msg)
if hasattr(record, "event"):
self.telegram(record)
self.discord(record)
if self.settings.colored is True:
record.msg = (
f"{self.settings.color_palette.get(record.event)}{record.msg}"
@@ -179,34 +83,8 @@ class GlobalFormatter(logging.Formatter):
return super().format(record)
def telegram(self, record):
skip_telegram = False if hasattr(
record, "skip_telegram") is False else True
if (
self.settings.telegram is not None
and skip_telegram is False
and self.settings.telegram.chat_id != 123456789
):
self.settings.telegram.send(record.msg, record.event)
def discord(self, record):
skip_discord = False if hasattr(
record, "skip_discord") is False else True
if (
self.settings.discord is not None
and skip_discord is False
and self.settings.discord.webhook_api
!= "https://discord.com/api/webhooks/0123456789/0a1B2c3D4e5F6g7H8i9J"
):
self.settings.discord.send(record.msg, record.event)
def configure_loggers(username, settings):
if settings.colored is True:
init(autoreset=True)
# Queue handler that will handle the logger queue
logger_queue = queue.Queue(-1)
queue_handler = QueueHandler(logger_queue)
@@ -253,19 +131,16 @@ def configure_loggers(username, settings):
delay=False,
)
else:
# Getting time zone from the console_handler's formatter since they are the same
tz = "" if console_handler.formatter.timezone is False else console_handler.formatter.timezone
logs_file = os.path.join(
logs_path,
f"{username}.{datetime.now(tz).strftime('%Y%m%d-%H%M%S')}.log",
f"{username}.{datetime.now().strftime('%Y%m%d-%H%M%S')}.log",
)
file_handler = logging.FileHandler(logs_file, "w", "utf-8")
file_handler.setFormatter(
FileFormatter(
logging.Formatter(
fmt="%(asctime)s - %(levelname)s - %(name)s - [%(funcName)s]: %(message)s",
datefmt="%d/%m/%y %H:%M:%S",
settings=settings
)
)
file_handler.setLevel(settings.file_level)
-1
View File
@@ -1,4 +1,3 @@
import platform
import re
import socket
import time
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

-212
View File
@@ -1,212 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Twitch-Channel-Points-Miner-v2</title>
<link rel="icon" type="image/png" sizes="32x32"
href="https://static.twitchcdn.net/assets/favicon-32-e29e246c157142c94346.png">
<link rel="icon" type="image/png" sizes="16x16"
href="https://static.twitchcdn.net/assets/favicon-16-52e571ffea063af7a7f4.png">
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.1/css/bulma.css" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" rel="stylesheet"
integrity="sha512-1ycn6IcaQQ40/MKBW2W4Rhis/DbILU74C1vSrLJxCq57o941Ym01SwNsOMqvEBFlcgUa6xLiPY/NS5R+E6ztJQ=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
<link href="{{url_for('static', filename='dark-theme.css')}}" rel="stylesheet" />
<link href="{{url_for('static', filename='style.css')}}" rel="stylesheet" />
</head>
<body style="height: 100vh;">
<div class="container has-text-centered" style="height: 100vh;">
<div class="columns" id="header">
<div class="column has-text-right">
<img style="margin-top: -20px;" width="600px" src="{{url_for('static', filename='banner.png')}}"
alt="banner">
</div>
<div class="column is-4 has-text-left" style="margin-top: 35px;">
<table>
<tr>
<td>Tkd-Alex</td>
<td>rdavydov</td>
</tr>
<tr>
<td><a href="https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/blob/master/LICENSE">
<img alt="License"
src="https://img.shields.io/github/license/Tkd-Alex/Twitch-Channel-Points-Miner-v2" /></a>
</td>
<td><a href="https://github.com/rdavydov/Twitch-Channel-Points-Miner-v2/blob/master/LICENSE">
<img alt="License"
src="https://img.shields.io/github/license/rdavydov/Twitch-Channel-Points-Miner-v2" /></a>
</td>
</tr>
<tr>
<td><a href="https://www.python.org/downloads/release/python-360/">
<img alt="Python3"
src="https://img.shields.io/badge/built%20for-Python≥3.6-red.svg?style=flat"></a>
</td>
<td><a href="https://www.python.org/downloads/release/python-360/">
<img alt="Python3"
src="https://img.shields.io/badge/built%20for-Python≥3.6-red.svg?style=flat"></a>
</td>
</tr>
<tr>
<td><a href="https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/pulls">
<img alt="PRsWelcome"
src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" /></a>
</td>
<td><a href="https://github.com/rdavydov/Twitch-Channel-Points-Miner-v2/pulls">
<img alt="PRsWelcome"
src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" /></a>
</td>
</tr>
<tr>
<td><a href="https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/stargazers">
<img alt="GitHub Repo stars"
src="https://img.shields.io/github/stars/Tkd-Alex/Twitch-Channel-Points-Miner-v2" /></a>
</td>
<td><a href="https://github.com/rdavydov/Twitch-Channel-Points-Miner-v2/stargazers">
<img alt="GitHub Repo stars"
src="https://img.shields.io/github/stars/rdavydov/Twitch-Channel-Points-Miner-v2" /></a>
</td>
</tr>
<tr>
<td><a
href="https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues?q=is%3Aissue+is%3Aclosed">
<img alt="GitHub closed issues"
src="https://img.shields.io/github/issues-closed/Tkd-Alex/Twitch-Channel-Points-Miner-v2"></a>
</td>
<td><a
href="https://github.com/rdavydov/Twitch-Channel-Points-Miner-v2/issues?q=is%3Aissue+is%3Aclosed">
<img alt="GitHub closed issues"
src="https://img.shields.io/github/issues-closed/rdavydov/Twitch-Channel-Points-Miner-v2"></a>
</td>
</tr>
<tr>
<td><a href="https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2">
<img alt="GitHub last commit"
src="https://img.shields.io/github/last-commit/Tkd-Alex/Twitch-Channel-Points-Miner-v2" /></a>
</td>
<td><a href="https://github.com/rdavydov/Twitch-Channel-Points-Miner-v2">
<img alt="GitHub last commit"
src="https://img.shields.io/github/last-commit/rdavydov/Twitch-Channel-Points-Miner-v2" /></a>
</td>
</tr>
<tr>
<td><a href="https://www.buymeacoffee.com/tkdalex" target="_blank">
<img src="https://cdn.buymeacoffee.com/buttons/lato-yellow.png"
alt="Buy Tkd-Alex A Coffee" height="5" width="138"></a></td>
<td><a href="https://www.buymeacoffee.com/davydov" target="_blank">
<img src="https://cdn.buymeacoffee.com/buttons/lato-blue.png"
alt="Buy rdavydov A Coffee" height="5" width="138"></td>
</tr>
</table>
</div>
</div>
<div class="columns">
<div class="column is-narrow has-text-center">
<div class="dropdown">
<div class="dropdown-trigger">
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu">
<span id="sorting-by"></span>
<span class="icon is-small">
<i class="fas fa-angle-down" aria-hidden="true"></i>
</span>
</button>
</div>
<div class="dropdown-menu" id="dropdown-menu" role="menu">
<div class="dropdown-content">
<a href="#" class="dropdown-item" onClick="changeSortBy(this)">
Name ascending
</a>
<a href="#" class="dropdown-item" onClick="changeSortBy(this)">
Name descending
</a>
<a href="#" class="dropdown-item" onClick="changeSortBy(this)">
Points ascending
</a>
<a href="#" class="dropdown-item" onClick="changeSortBy(this)">
Points descending
</a>
<a href="#" class="dropdown-item" onClick="changeSortBy(this)">
Last activity ascending
</a>
<a href="#" class="dropdown-item" onClick="changeSortBy(this)">
Last activity descending
</a>
</div>
</div>
</div>
</div>
<div class="column is-2 has-text-left">
<input class="input" type="date" id="startDate">
</div>
<div class="column is-2 has-text-left">
<input class="input" type="date" id="endDate">
</div>
<div class="column has-text-right" style="margin: auto">
<label class="checkbox checkbox-label">
Annotations
<input type="checkbox" checked="true" id="annotations">
</label>
<label class="checkbox checkbox-label">
Dark mode
<input type="checkbox" checked="true" id="dark-mode">
</label>
<label class="checkbox checkbox-label">
Header
<input type="checkbox" checked="true" id="toggle-header">
</label>
</div>
</div>
<div class="columns">
<div class="column is-narrow">
<div class="tabs">
<ul id="streamers-list"></ul>
</div>
</div>
<div class="column">
<div class="box" id="chart" style="padding: 0.30rem;"></div>
</div>
</div>
</div>
</body>
<script type="text/javascript">
// n.b. Seems that we are unable to call Flask variable in JS code imported from JS file.
// That's why "link[href='{{url_for('static', filename='dark-theme.css')}}']", {{ refresh }}, {{ daysAgo }} are here.
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"
}
})
}
var refresh = parseInt("{{ refresh }}");
var daysAgo = parseInt("{{ daysAgo }}");
$('#toggle-header').change(function () {
if (this.checked) {
$('#header').show();
} else {
$('#header').hide();
}
});
</script>
<script type="text/javascript" src="{{url_for('static', filename='script.js')}}"></script>
</html>
-29
View File
@@ -1,29 +0,0 @@
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;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

-248
View File
@@ -1,248 +0,0 @@
// https://apexcharts.com/javascript-chart-demos/line-charts/zoomable-timeseries/
var options = {
series: [],
chart: {
type: 'area',
stacked: false,
height: 490,
zoom: {
type: 'x',
enabled: true,
autoScaleYaxis: true
},
// background: '#2B2D3E',
foreColor: '#fff'
},
dataLabels: {
enabled: false
},
stroke: {
curve: 'smooth',
},
markers: {
size: 0,
},
title: {
text: 'Channel points (dates are displayed in UTC)',
align: 'left'
},
colors: ["#f9826c"],
fill: {
type: 'gradient',
gradient: {
shadeIntensity: 1,
inverseColors: false,
opacityFrom: 0.5,
opacityTo: 0,
stops: [0, 90, 100]
},
},
yaxis: {
title: {
text: 'Channel points'
},
},
xaxis: {
type: 'datetime',
labels: {
datetimeUTC: false
}
},
tooltip: {
theme: 'dark',
shared: false,
x: {
show: true,
format: 'HH:mm:ss dd MMM',
},
custom: ({
series,
seriesIndex,
dataPointIndex,
w
}) => {
return (`<div class="apexcharts-active">
<div class="apexcharts-tooltip-title">${w.globals.seriesNames[seriesIndex]}</div>
<div class="apexcharts-tooltip-series-group apexcharts-active" style="order: 1; display: flex; padding-bottom: 0px !important;">
<div class="apexcharts-tooltip-text">
<div class="apexcharts-tooltip-y-group">
<span class="apexcharts-tooltip-text-label"><b>Points</b>: ${series[seriesIndex][dataPointIndex]}</span><br>
<span class="apexcharts-tooltip-text-label"><b>Reason</b>: ${w.globals.seriesZ[seriesIndex][dataPointIndex] ? w.globals.seriesZ[seriesIndex][dataPointIndex] : ''}</span>
</div>
</div>
</div>
</div>`)
}
},
noData: {
text: 'Loading...'
}
};
var chart = new ApexCharts(document.querySelector("#chart"), options);
var currentStreamer = null;
var annotations = [];
var streamersList = [];
var sortBy = "Name ascending";
var sortField = 'name';
var startDate = new Date();
startDate.setDate(startDate.getDate() - daysAgo);
var endDate = new Date();
$(document).ready(function () {
chart.render();
if (!localStorage.getItem("annotations")) localStorage.setItem("annotations", true);
if (!localStorage.getItem("dark-mode")) localStorage.setItem("dark-mode", true);
if (!localStorage.getItem("sort-by")) localStorage.setItem("sort-by", "Name ascending");
$('#annotations').prop("checked", localStorage.getItem("annotations") === "true");
$('#dark-mode').prop("checked", localStorage.getItem("dark-mode") === "true");
$('#startDate').val(formatDate(startDate));
$('#endDate').val(formatDate(endDate));
sortBy = localStorage.getItem("sort-by");
if (sortBy.includes("Points")) sortField = 'points';
else if (sortBy.includes("Last activity")) sortField = 'last_activity';
else sortField = 'name';
$('#sorting-by').text(sortBy);
getStreamers();
updateAnnotations();
toggleDarkMode();
});
function formatDate(date) {
var d = new Date(date),
month = '' + (d.getMonth() + 1),
day = '' + d.getDate(),
year = d.getFullYear();
if (month.length < 2) month = '0' + month;
if (day.length < 2) day = '0' + day;
return [year, month, day].join('-');
}
function changeStreamer(streamer, index) {
$("li").removeClass("is-active")
$("li").eq(index - 1).addClass('is-active');
currentStreamer = streamer;
getStreamerData(streamer);
}
function getStreamerData(streamer) {
if (currentStreamer == streamer) {
$.getJSON(`./json/${streamer}`, {
startDate: formatDate(startDate),
endDate: formatDate(endDate)
}, function (response) {
chart.updateSeries([{
name: streamer.replace(".json", ""),
data: response["series"]
}], true)
clearAnnotations();
annotations = response["annotations"];
updateAnnotations();
setTimeout(function () {
getStreamerData(streamer);
}, 300000); // 5 minutes
});
}
}
function getAllStreamersData() {
$.getJSON(`./json_all`, function (response) {
for (var i in response) {
chart.appendSeries({
name: response[i]["name"].replace(".json", ""),
data: response[i]["data"]["series"]
}, true)
}
});
}
function getStreamers() {
$.getJSON('streamers', function (response) {
streamersList = response;
sortStreamers();
renderStreamers();
});
}
function renderStreamers() {
$("#streamers-list").empty();
var promised = new Promise((resolve, reject) => {
streamersList.forEach((streamer, index, array) => {
displayname = streamer.name.replace(".json", "");
if (sortField == 'points') displayname = "<font size='-2'>" + streamer['points'] + "</font>&nbsp;" + displayname;
else if (sortField == 'last_activity') displayname = "<font size='-2'>" + formatDate(streamer['last_activity']) + "</font>&nbsp;"+ displayname;
$("#streamers-list").append(`<li><a onClick="changeStreamer('${streamer.name}', ${index + 1}); return false;">${displayname}</a></li>`);
if (index === array.length - 1) resolve();
});
});
promised.then(() => {
changeStreamer(streamersList[0].name, 1);
});
}
function sortStreamers() {
streamersList = streamersList.sort((a, b) => {
return (a[sortField] > b[sortField] ? 1 : -1) * (sortBy.includes("ascending") ? 1 : -1);
});
}
function changeSortBy(option) {
sortBy = option.innerText.trim();
if (sortBy.includes("Points")) sortField = 'points'
else if (sortBy.includes("Last activity")) sortField = 'last_activity'
else sortField = 'name';
sortStreamers();
renderStreamers();
$('#sorting-by').text(sortBy);
localStorage.setItem("sort-by", sortBy);
}
function updateAnnotations() {
if ($('#annotations').prop("checked") === true) {
clearAnnotations()
if (annotations && annotations.length > 0)
annotations.forEach((annotation, index) => {
annotations[index]['id'] = `id-${index}`
chart.addXaxisAnnotation(annotation, true)
})
} else clearAnnotations()
}
function clearAnnotations() {
if (annotations && annotations.length > 0)
annotations.forEach((annotation, index) => {
chart.removeAnnotation(annotation['id'])
})
chart.clearAnnotations();
}
// Toggle
$('#annotations').click(() => {
updateAnnotations();
});
$('#dark-mode').click(() => {
toggleDarkMode();
});
$('.dropdown').click(() => {
$('.dropdown').toggleClass('is-active');
});
// Input date
$('#startDate').change(() => {
startDate = new Date($('#startDate').val());
getStreamerData(currentStreamer);
});
$('#endDate').change(() => {
endDate = new Date($('#endDate').val());
getStreamerData(currentStreamer);
});
-55
View File
@@ -1,55 +0,0 @@
html,
body {
overflow-x: hidden;
overflow-y: auto;
}
a {
text-decoration: none;
}
.tabs ul {
-webkit-flex-direction: column;
flex-direction: column;
flex-grow: 0px;
}
.tabs {
max-height: 500px;
overflow-y: auto;
direction: rtl;
}
.checkbox-label {
font-size: 20px;
padding-left: 15px;
font-weight: bold
}
::-webkit-scrollbar {
width: 20px;
}
::-webkit-scrollbar-track {
background-color: transparent;
}
::-webkit-scrollbar-thumb {
background-color: #d6dee1;
}
::-webkit-scrollbar-thumb {
background-color: #d6dee1;
border-radius: 20px;
}
::-webkit-scrollbar-thumb {
background-color: #d6dee1;
border-radius: 20px;
border: 6px solid transparent;
background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover {
background-color: #a8bbbf;
}