Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
177501c269 |
@@ -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
|
||||
},
|
||||
)
|
||||
@@ -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()
|
||||
|
||||
@@ -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,4 +1,3 @@
|
||||
import platform
|
||||
import re
|
||||
import socket
|
||||
import time
|
||||
|
||||
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 |
@@ -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>
|
||||
@@ -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 |
@@ -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> " + displayname;
|
||||
else if (sortField == 'last_activity') displayname = "<font size='-2'>" + formatDate(streamer['last_activity']) + "</font> "+ 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);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user