Merge pull request #96 from Tkd-Alex/analytics
Show analytics of your points mining
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -148,4 +148,5 @@ chromedriver*
|
|||||||
cookies/*
|
cookies/*
|
||||||
logs/*
|
logs/*
|
||||||
screenshots/*
|
screenshots/*
|
||||||
htmls/*
|
htmls/*
|
||||||
|
analytics/*
|
||||||
|
|||||||
31
README.md
31
README.md
@@ -34,10 +34,11 @@ Read more about channels point [here](https://help.twitch.tv/s/article/channel-p
|
|||||||
- [Bet strategy](#bet-strategy)
|
- [Bet strategy](#bet-strategy)
|
||||||
- [FilterCondition](#filtercondition)
|
- [FilterCondition](#filtercondition)
|
||||||
- [Example](#example)
|
- [Example](#example)
|
||||||
6. 🍪 [Migrating from old repository (the original one)](#migrating-from-old-repository-the-original-one)
|
6. 📈 [Analytics](#analytics)
|
||||||
7. 🪟 [Windows](#windows)
|
7. 🍪 [Migrating from old repository (the original one)](#migrating-from-old-repository-the-original-one)
|
||||||
8. 📱 [Termux](#termux)
|
8. 🪟 [Windows](#windows)
|
||||||
9. ⚠️ [Disclaimer](#disclaimer)
|
9. 📱 [Termux](#termux)
|
||||||
|
10. ⚠️ [Disclaimer](#disclaimer)
|
||||||
|
|
||||||
|
|
||||||
## Community
|
## Community
|
||||||
@@ -61,6 +62,7 @@ If you have any issues or you want to contribute, you are welcome! But please be
|
|||||||
- Auto claim game drops from Twitch inventory [#21](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/21) Read more about game drops [here](https://help.twitch.tv/s/article/mission-based-drops)
|
- Auto claim game drops from Twitch inventory [#21](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/21) Read more about game drops [here](https://help.twitch.tv/s/article/mission-based-drops)
|
||||||
- Place the bet / make a prediction and win or lose (🍀) your channel points!
|
- Place the bet / make a prediction and win or lose (🍀) your channel points!
|
||||||
No browser needed. [#41](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/41) ([@lay295](https://github.com/lay295))
|
No browser needed. [#41](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/41) ([@lay295](https://github.com/lay295))
|
||||||
|
- Analytics chart
|
||||||
|
|
||||||
## Logs feature
|
## Logs feature
|
||||||
### Full logs
|
### Full logs
|
||||||
@@ -259,7 +261,7 @@ If you follow so many streamers on Twitch, but you don't want to mine points for
|
|||||||
```python
|
```python
|
||||||
from TwitchChannelPointsMiner import TwitchChannelPointsMiner
|
from TwitchChannelPointsMiner import TwitchChannelPointsMiner
|
||||||
twitch_miner = TwitchChannelPointsMiner("your-twitch-username")
|
twitch_miner = TwitchChannelPointsMiner("your-twitch-username")
|
||||||
twitch_miner.mine(followers=True, blacklist=["user1", "user2"]) # Automatic use the followers list OR
|
twitch_miner.mine(followers=True, blacklist=["user1", "user2"]) # Blacklist example
|
||||||
```
|
```
|
||||||
4. Start mining! `python run.py`
|
4. Start mining! `python run.py`
|
||||||
|
|
||||||
@@ -389,6 +391,25 @@ Allowed values for `where` are: `GT, LT, GTE, LTE`
|
|||||||
- If you want to place the bet ONLY if the highest bet is lower than 2000
|
- If you want to place the bet ONLY if the highest bet is lower than 2000
|
||||||
`FilterCondition(by=OutcomeKeys.TOP_POINTS, where=Condition.LT, value=2000)`
|
`FilterCondition(by=OutcomeKeys.TOP_POINTS, where=Condition.LT, value=2000)`
|
||||||
|
|
||||||
|
## Analytics
|
||||||
|
We have recently introduced a little frontend where you can show with a chart you points trend. The script will spawn a Flask web-server on your machine where you can select binding address and port.
|
||||||
|
The chart provides some annotation to handle the prediction and watch strike events. Usually annotation are used to notice big increase / decrease of points. If you want to can disable annotations.
|
||||||
|
On each (x, y) points Its present a tooltip that show points, date time and reason of points gained / lost. This web page was just a funny idea, and it is not intended to use for a professional usage.
|
||||||
|
If you want you can toggle the dark theme with the dedicated checkbox.
|
||||||
|
|
||||||
|
| Light theme | Dark theme |
|
||||||
|
| ----------- | ---------- |
|
||||||
|
|  |  |
|
||||||
|
|
||||||
|
For use this feature just call the `analytics` method before start mining. Read more at: [#96](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/96)
|
||||||
|
The chart will be autofreshed each `refresh` minutes. If you want to connect from one to second machine that have that webpanel you have to use `0.0.0.0` instead of `127.0.0.1`.
|
||||||
|
```python
|
||||||
|
from TwitchChannelPointsMiner import TwitchChannelPointsMiner
|
||||||
|
twitch_miner = TwitchChannelPointsMiner("your-twitch-username")
|
||||||
|
twitch_miner.analytics(host="127.0.0.1", port=5000, refresh=5) # Analytics web-server
|
||||||
|
twitch_miner.mine(followers=True, blacklist=["user1", "user2"])
|
||||||
|
```
|
||||||
|
|
||||||
## Migrating from an old repository (the original one):
|
## Migrating from an old repository (the original one):
|
||||||
If you already have a `twitch-cookies.pkl` and you don't want to log in again, please create a `cookies/` folder in the current directory and then copy the .pkl file with a new name `your-twitch-username.pkl`
|
If you already have a `twitch-cookies.pkl` and you don't want to log in again, please create a `cookies/` folder in the current directory and then copy the .pkl file with a new name `your-twitch-username.pkl`
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import copy
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import random
|
import random
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
@@ -10,7 +10,9 @@ import time
|
|||||||
import uuid
|
import uuid
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from TwitchChannelPointsMiner.classes.AnalyticsServer import AnalyticsServer
|
||||||
from TwitchChannelPointsMiner.classes.entities.PubsubTopic import PubsubTopic
|
from TwitchChannelPointsMiner.classes.entities.PubsubTopic import PubsubTopic
|
||||||
from TwitchChannelPointsMiner.classes.entities.Streamer import (
|
from TwitchChannelPointsMiner.classes.entities.Streamer import (
|
||||||
Streamer,
|
Streamer,
|
||||||
@@ -33,8 +35,10 @@ from TwitchChannelPointsMiner.utils import (
|
|||||||
# - chardet.charsetprober - [feed]
|
# - chardet.charsetprober - [feed]
|
||||||
# - chardet.charsetprober - [get_confidence]
|
# - chardet.charsetprober - [get_confidence]
|
||||||
# - requests - [Starting new HTTPS connection (1)]
|
# - requests - [Starting new HTTPS connection (1)]
|
||||||
|
# - Flask (werkzeug) logs
|
||||||
logging.getLogger("chardet.charsetprober").setLevel(logging.ERROR)
|
logging.getLogger("chardet.charsetprober").setLevel(logging.ERROR)
|
||||||
logging.getLogger("requests").setLevel(logging.ERROR)
|
logging.getLogger("requests").setLevel(logging.ERROR)
|
||||||
|
logging.getLogger("werkzeug").setLevel(logging.ERROR)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -62,12 +66,16 @@ class TwitchChannelPointsMiner:
|
|||||||
username: str,
|
username: str,
|
||||||
password: str = None,
|
password: str = None,
|
||||||
claim_drops_startup: bool = False,
|
claim_drops_startup: bool = False,
|
||||||
|
# Settings for logging and selenium as you can see.
|
||||||
priority: list = [Priority.STREAK, Priority.DROPS, Priority.ORDER],
|
priority: list = [Priority.STREAK, Priority.DROPS, Priority.ORDER],
|
||||||
# This settings will be global shared trought Settings class
|
# This settings will be global shared trought Settings class
|
||||||
logger_settings: LoggerSettings = LoggerSettings(),
|
logger_settings: LoggerSettings = LoggerSettings(),
|
||||||
# Default values for all streamers
|
# Default values for all streamers
|
||||||
streamer_settings: StreamerSettings = StreamerSettings(),
|
streamer_settings: StreamerSettings = StreamerSettings(),
|
||||||
):
|
):
|
||||||
|
Settings.analytics_path = os.path.join(Path().absolute(), "analytics", username)
|
||||||
|
Path(Settings.analytics_path).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
self.username = username
|
self.username = username
|
||||||
|
|
||||||
# Set as global config
|
# Set as global config
|
||||||
@@ -100,6 +108,12 @@ class TwitchChannelPointsMiner:
|
|||||||
for sign in [signal.SIGINT, signal.SIGSEGV, signal.SIGTERM]:
|
for sign in [signal.SIGINT, signal.SIGSEGV, signal.SIGTERM]:
|
||||||
signal.signal(sign, self.end)
|
signal.signal(sign, self.end)
|
||||||
|
|
||||||
|
def analytics(self, host: str = "127.0.0.1", port: int = 5000, refresh: int = 5):
|
||||||
|
http_server = AnalyticsServer(host=host, port=port, refresh=refresh)
|
||||||
|
http_server.daemon = True
|
||||||
|
http_server.name = "Analytics Thread"
|
||||||
|
http_server.start()
|
||||||
|
|
||||||
def mine(self, streamers: list = [], blacklist: list = [], followers=False):
|
def mine(self, streamers: list = [], blacklist: list = [], followers=False):
|
||||||
self.run(streamers=streamers, blacklist=blacklist, followers=followers)
|
self.run(streamers=streamers, blacklist=blacklist, followers=followers)
|
||||||
|
|
||||||
@@ -185,7 +199,9 @@ class TwitchChannelPointsMiner:
|
|||||||
if streamer.viewer_is_mod is True:
|
if streamer.viewer_is_mod is True:
|
||||||
streamer.settings.make_predictions = False
|
streamer.settings.make_predictions = False
|
||||||
|
|
||||||
self.original_streamers = copy.deepcopy(self.streamers)
|
self.original_streamers = [
|
||||||
|
streamer.channel_points for streamer in self.streamers
|
||||||
|
]
|
||||||
|
|
||||||
# If we have at least one streamer with settings = make_predictions True
|
# If we have at least one streamer with settings = make_predictions True
|
||||||
make_predictions = at_least_one_value_in_settings_is(
|
make_predictions = at_least_one_value_in_settings_is(
|
||||||
@@ -276,7 +292,13 @@ class TwitchChannelPointsMiner:
|
|||||||
if self.sync_campaigns_thread is not None:
|
if self.sync_campaigns_thread is not None:
|
||||||
self.sync_campaigns_thread.join()
|
self.sync_campaigns_thread.join()
|
||||||
|
|
||||||
time.sleep(1)
|
# Check if all the mutex are unlocked.
|
||||||
|
# Prevent breaks of .json file
|
||||||
|
for streamer in self.streamers:
|
||||||
|
if streamer.mutex.locked():
|
||||||
|
streamer.mutex.acquire()
|
||||||
|
streamer.mutex.release()
|
||||||
|
|
||||||
self.__print_report()
|
self.__print_report()
|
||||||
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
@@ -322,7 +344,7 @@ class TwitchChannelPointsMiner:
|
|||||||
if self.streamers[streamer_index].history != {}:
|
if self.streamers[streamer_index].history != {}:
|
||||||
gained = (
|
gained = (
|
||||||
self.streamers[streamer_index].channel_points
|
self.streamers[streamer_index].channel_points
|
||||||
- self.original_streamers[streamer_index].channel_points
|
- self.original_streamers[streamer_index]
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"{repr(self.streamers[streamer_index])}, Total Points Gained (after farming - before farming): {_millify(gained)}",
|
f"{repr(self.streamers[streamer_index])}, Total Points Gained (after farming - before farming): {_millify(gained)}",
|
||||||
|
|||||||
62
TwitchChannelPointsMiner/classes/AnalyticsServer.py
Normal file
62
TwitchChannelPointsMiner/classes/AnalyticsServer.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from flask import Flask, Response, cli, render_template
|
||||||
|
|
||||||
|
from TwitchChannelPointsMiner.classes.Settings import Settings
|
||||||
|
|
||||||
|
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 read_json(streamer):
|
||||||
|
path = Settings.analytics_path
|
||||||
|
streamer = streamer if streamer.endswith(".json") else f"{streamer}.json"
|
||||||
|
return Response(
|
||||||
|
open(os.path.join(path, streamer)) if streamer in streamers_available() else [],
|
||||||
|
status=200,
|
||||||
|
mimetype="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def index(refresh=5):
|
||||||
|
return render_template(
|
||||||
|
"charts.html",
|
||||||
|
refresh=(refresh * 60 * 1000),
|
||||||
|
streamers=",".join(streamers_available()),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AnalyticsServer(Thread):
|
||||||
|
def __init__(self, host: str = "127.0.0.1", port: int = 5000, refresh: int = 5):
|
||||||
|
super(AnalyticsServer, self).__init__()
|
||||||
|
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.refresh = refresh
|
||||||
|
|
||||||
|
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})
|
||||||
|
self.app.add_url_rule("/json/<string:streamer>", "json", read_json)
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -176,11 +176,18 @@ class WebSocketsPool:
|
|||||||
if streamer_index != -1:
|
if streamer_index != -1:
|
||||||
try:
|
try:
|
||||||
if message.topic == "community-points-user-v1":
|
if message.topic == "community-points-user-v1":
|
||||||
|
if message.type in ["points-earned", "points-spent"]:
|
||||||
|
balance = message.data["balance"]["balance"]
|
||||||
|
ws.streamers[streamer_index].channel_points = balance
|
||||||
|
ws.streamers[streamer_index].persistent_series(
|
||||||
|
event_type=message.data["point_gain"]["reason_code"]
|
||||||
|
if message.type == "points-earned"
|
||||||
|
else "Spent"
|
||||||
|
)
|
||||||
|
|
||||||
if message.type == "points-earned":
|
if message.type == "points-earned":
|
||||||
earned = message.data["point_gain"]["total_points"]
|
earned = message.data["point_gain"]["total_points"]
|
||||||
reason_code = message.data["point_gain"]["reason_code"]
|
reason_code = message.data["point_gain"]["reason_code"]
|
||||||
balance = message.data["balance"]["balance"]
|
|
||||||
ws.streamers[streamer_index].channel_points = balance
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"+{earned} → {ws.streamers[streamer_index]} - Reason: {reason_code}.",
|
f"+{earned} → {ws.streamers[streamer_index]} - Reason: {reason_code}.",
|
||||||
extra={
|
extra={
|
||||||
@@ -193,6 +200,9 @@ class WebSocketsPool:
|
|||||||
ws.streamers[streamer_index].update_history(
|
ws.streamers[streamer_index].update_history(
|
||||||
reason_code, earned
|
reason_code, earned
|
||||||
)
|
)
|
||||||
|
ws.streamers[streamer_index].persistent_annotations(
|
||||||
|
reason_code, f"+{earned} - {reason_code}"
|
||||||
|
)
|
||||||
elif message.type == "claim-available":
|
elif message.type == "claim-available":
|
||||||
ws.twitch.claim_bonus(
|
ws.twitch.claim_bonus(
|
||||||
ws.streamers[streamer_index],
|
ws.streamers[streamer_index],
|
||||||
@@ -333,8 +343,18 @@ class WebSocketsPool:
|
|||||||
-points["won"],
|
-points["won"],
|
||||||
counter=-1,
|
counter=-1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if event_prediction.result["type"] != "LOSE":
|
||||||
|
ws.streamers[streamer_index].persistent_annotations(
|
||||||
|
event_prediction.result["type"],
|
||||||
|
f"{ws.events_predictions[event_id].title}",
|
||||||
|
)
|
||||||
elif message.type == "prediction-made":
|
elif message.type == "prediction-made":
|
||||||
event_prediction.bet_confirmed = True
|
event_prediction.bet_confirmed = True
|
||||||
|
ws.streamers[streamer_index].persistent_annotations(
|
||||||
|
"PREDICTION_MADE",
|
||||||
|
f"Decision: {event_prediction.bet.decision['choice']} - {event_prediction.title}",
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Exception raised for topic: {message.topic} and message: {message}",
|
f"Exception raised for topic: {message.topic} and message: {message}",
|
||||||
|
|||||||
@@ -58,7 +58,11 @@ class Message(object):
|
|||||||
else (
|
else (
|
||||||
self.data["channel_id"]
|
self.data["channel_id"]
|
||||||
if "channel_id" in self.data
|
if "channel_id" in self.data
|
||||||
else self.topic_user
|
else (
|
||||||
|
self.data["balance"]["channel_id"]
|
||||||
|
if "balance" in self.data
|
||||||
|
else self.topic_user
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
from TwitchChannelPointsMiner.classes.entities.Bet import BetSettings
|
from TwitchChannelPointsMiner.classes.entities.Bet import BetSettings
|
||||||
from TwitchChannelPointsMiner.classes.entities.Stream import Stream
|
from TwitchChannelPointsMiner.classes.entities.Stream import Stream
|
||||||
@@ -60,6 +64,7 @@ class Streamer(object):
|
|||||||
"raid",
|
"raid",
|
||||||
"history",
|
"history",
|
||||||
"streamer_url",
|
"streamer_url",
|
||||||
|
"mutex",
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, username, settings=None):
|
def __init__(self, username, settings=None):
|
||||||
@@ -81,6 +86,8 @@ class Streamer(object):
|
|||||||
|
|
||||||
self.streamer_url = f"{URL}/{self.username}"
|
self.streamer_url = f"{URL}/{self.username}"
|
||||||
|
|
||||||
|
self.mutex = Lock()
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"Streamer(username={self.username}, channel_id={self.channel_id}, channel_points={_millify(self.channel_points)})"
|
return f"Streamer(username={self.username}, channel_id={self.channel_id}, channel_points={_millify(self.channel_points)})"
|
||||||
|
|
||||||
@@ -146,3 +153,44 @@ class Streamer(object):
|
|||||||
and self.stream.drops_tags is True
|
and self.stream.drops_tags is True
|
||||||
and self.stream.campaigns_ids != []
|
and self.stream.campaigns_ids != []
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# === 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
|
||||||
|
# data.update({"x": round(time.time() * 1000)})
|
||||||
|
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)
|
||||||
|
|||||||
BIN
assets/chart-analytics-dark.png
Normal file
BIN
assets/chart-analytics-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
BIN
assets/chart-analytics-light.png
Normal file
BIN
assets/chart-analytics-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
227
assets/charts.html
Normal file
227
assets/charts.html
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Twitch-Channel-Points-Miner-v2</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/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="{{url_for('static', filename='dark-theme.css')}}" rel="stylesheet"/>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body style="height: 100vh;">
|
||||||
|
<div class="container">
|
||||||
|
<div style="text-align: center">
|
||||||
|
<img style="margin-top: -20px;" width="600px" src="{{url_for('static', filename='banner.png')}}" alt="banner">
|
||||||
|
<br>
|
||||||
|
<div style="margin-top: -15px;">
|
||||||
|
<a style="text-decoration: none;" 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>
|
||||||
|
<a style="text-decoration: none;" 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>
|
||||||
|
<a style="text-decoration: none;" 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>
|
||||||
|
<a style="text-decoration: none;" 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>
|
||||||
|
<a style="text-decoration: none;" 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>
|
||||||
|
<a style="text-decoration: none;" 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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-10">
|
||||||
|
<div class="tabs">
|
||||||
|
<ul>
|
||||||
|
{% for streamer in streamers.split(',')|sort %}
|
||||||
|
<li onClick="changeStreamer('{{ streamer }}', {{ loop.index }})"><a>{{ streamer.replace(".json", "") }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column" style="text-align: right">
|
||||||
|
<label class="checkbox">
|
||||||
|
Annotations
|
||||||
|
<input type="checkbox" checked="true" id="annotations">
|
||||||
|
</label>
|
||||||
|
<label class="checkbox">
|
||||||
|
Dark mode
|
||||||
|
<input type="checkbox" checked="true" id="dark-mode">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box" id="chart" style="padding: 0.30rem;"></div>
|
||||||
|
|
||||||
|
<!-- <hr style="margin: 1.2rem 0;"> -->
|
||||||
|
<div style="text-align: center">If you want to help on this project, please leave a star 🌟 and share it with your friends! 😎 - A coffee is always a gesture of LOVE ❤️
|
||||||
|
<a href="https://www.buymeacoffee.com/tkdalex" target="_blank">
|
||||||
|
<img style="margin-bottom: -8px; margin-left: 10px;" src="https://cdn.buymeacoffee.com/buttons/lato-yellow.png" alt="Buy Me A Coffee" height="5" width="138">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
<script type="text/javascript">
|
||||||
|
// https://apexcharts.com/javascript-chart-demos/line-charts/zoomable-timeseries/
|
||||||
|
var options = {
|
||||||
|
series: [],
|
||||||
|
chart: {
|
||||||
|
type: 'area',
|
||||||
|
stacked: false,
|
||||||
|
height: 400,
|
||||||
|
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 refresh = parseInt("{{ refresh }}");
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
chart.render();
|
||||||
|
$("li").eq(0).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
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}`, function (response) {
|
||||||
|
chart.updateSeries([{
|
||||||
|
name: streamer.replace(".json", ""),
|
||||||
|
data: response["series"]
|
||||||
|
}], true)
|
||||||
|
annotations = response["annotations"];
|
||||||
|
updateAnnotations()
|
||||||
|
setTimeout(function(){getStreamerData(streamer);}, 300000); // 5 minutes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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'])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// if (darkMode === true) $("#chart").addClass("box")
|
||||||
|
// else $("#chart").removeClass("box")
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#annotations').click(() => { updateAnnotations(); });
|
||||||
|
$('#dark-mode').click(() => { toggleDarkMode(); });
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</html>
|
||||||
29
assets/dark-theme.css
Normal file
29
assets/dark-theme.css
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -6,4 +6,5 @@ python-dateutil
|
|||||||
emoji
|
emoji
|
||||||
millify
|
millify
|
||||||
pre-commit
|
pre-commit
|
||||||
|
flask
|
||||||
colorama
|
colorama
|
||||||
|
|||||||
Reference in New Issue
Block a user