Merge branch 'master' into chat-client
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -149,3 +149,4 @@ cookies/*
|
||||
logs/*
|
||||
screenshots/*
|
||||
htmls/*
|
||||
analytics/*
|
||||
34
README.md
34
README.md
@@ -34,10 +34,11 @@ Read more about channels point [here](https://help.twitch.tv/s/article/channel-p
|
||||
- [Bet strategy](#bet-strategy)
|
||||
- [FilterCondition](#filtercondition)
|
||||
- [Example](#example)
|
||||
6. 🍪 [Migrating from old repository (the original one)](#migrating-from-old-repository-the-original-one)
|
||||
7. 🪟 [Windows](#windows)
|
||||
8. 📱 [Termux](#termux)
|
||||
9. ⚠️ [Disclaimer](#disclaimer)
|
||||
6. 📈 [Analytics](#analytics)
|
||||
7. 🍪 [Migrating from old repository (the original one)](#migrating-from-old-repository-the-original-one)
|
||||
8. 🪟 [Windows](#windows)
|
||||
9. 📱 [Termux](#termux)
|
||||
10. ⚠️ [Disclaimer](#disclaimer)
|
||||
|
||||
|
||||
## 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)
|
||||
- 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))
|
||||
- Analytics chart
|
||||
|
||||
## Logs feature
|
||||
### Full logs
|
||||
@@ -259,7 +261,7 @@ If you follow so many streamers on Twitch, but you don't want to mine points for
|
||||
```python
|
||||
from TwitchChannelPointsMiner import TwitchChannelPointsMiner
|
||||
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`
|
||||
|
||||
@@ -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
|
||||
`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):
|
||||
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`
|
||||
```
|
||||
@@ -422,7 +443,7 @@ Clone this repository
|
||||
(2 way):
|
||||
Download sources from GitHub and put it into your Termux storage
|
||||
|
||||
Now you can enter the directory with our miner, do this by typing this command:
|
||||
Now you can enter the directory with our miner, type this command:
|
||||
`cd Twitch-Channel-Points-Miner-v2`
|
||||
|
||||
Configure your miner on your preferences by typing
|
||||
@@ -438,5 +459,6 @@ Now when we did everything we can run miner:
|
||||
`python run.py`
|
||||
|
||||
Read more at [#92](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/92) [#76](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/76)
|
||||
|
||||
## Disclaimer
|
||||
This project comes with no guarantee or warranty. You are responsible for whatever happens from using this project. It is possible to get soft or hard banned by using this project if you are not careful. This is a personal project and is in no way affiliated with Twitch.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import signal
|
||||
import sys
|
||||
@@ -10,8 +10,10 @@ import time
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from TwitchChannelPointsMiner.classes.Chat import ThreadChat
|
||||
from TwitchChannelPointsMiner.classes.AnalyticsServer import AnalyticsServer
|
||||
from TwitchChannelPointsMiner.classes.entities.PubsubTopic import PubsubTopic
|
||||
from TwitchChannelPointsMiner.classes.entities.Streamer import (
|
||||
Streamer,
|
||||
@@ -34,11 +36,13 @@ from TwitchChannelPointsMiner.utils import (
|
||||
# - chardet.charsetprober - [feed]
|
||||
# - chardet.charsetprober - [get_confidence]
|
||||
# - requests - [Starting new HTTPS connection (1)]
|
||||
# - Flask (werkzeug) logs
|
||||
# - irc.client - [process_data]
|
||||
# - irc.client - [_dispatcher]
|
||||
# - irc.client - [_handle_message]
|
||||
logging.getLogger("chardet.charsetprober").setLevel(logging.ERROR)
|
||||
logging.getLogger("requests").setLevel(logging.ERROR)
|
||||
logging.getLogger("werkzeug").setLevel(logging.ERROR)
|
||||
logging.getLogger("irc.client").setLevel(logging.ERROR)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -67,12 +71,16 @@ class TwitchChannelPointsMiner:
|
||||
username: str,
|
||||
password: str = None,
|
||||
claim_drops_startup: bool = False,
|
||||
# Settings for logging and selenium as you can see.
|
||||
priority: list = [Priority.STREAK, Priority.DROPS, Priority.ORDER],
|
||||
# This settings will be global shared trought Settings class
|
||||
logger_settings: LoggerSettings = LoggerSettings(),
|
||||
# Default values for all streamers
|
||||
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
|
||||
|
||||
# Set as global config
|
||||
@@ -105,6 +113,12 @@ class TwitchChannelPointsMiner:
|
||||
for sign in [signal.SIGINT, signal.SIGSEGV, signal.SIGTERM]:
|
||||
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):
|
||||
self.run(streamers=streamers, blacklist=blacklist, followers=followers)
|
||||
|
||||
@@ -196,7 +210,9 @@ class TwitchChannelPointsMiner:
|
||||
if streamer.viewer_is_mod is True:
|
||||
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
|
||||
make_predictions = at_least_one_value_in_settings_is(
|
||||
@@ -293,7 +309,13 @@ class TwitchChannelPointsMiner:
|
||||
if self.sync_campaigns_thread is not None:
|
||||
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()
|
||||
|
||||
sys.exit(0)
|
||||
@@ -339,7 +361,7 @@ class TwitchChannelPointsMiner:
|
||||
if self.streamers[streamer_index].history != {}:
|
||||
gained = (
|
||||
self.streamers[streamer_index].channel_points
|
||||
- self.original_streamers[streamer_index].channel_points
|
||||
- self.original_streamers[streamer_index]
|
||||
)
|
||||
logger.info(
|
||||
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)
|
||||
@@ -58,12 +58,10 @@ class TwitchLogin(object):
|
||||
|
||||
use_backup_flow = False
|
||||
|
||||
# while True:
|
||||
for attempt in range(0, 25):
|
||||
# self.username = input('Enter Twitch username: ')
|
||||
password = (
|
||||
getpass.getpass(f"Enter Twitch password for {self.username}: ")
|
||||
if self.password is None
|
||||
if self.password in [None, ""]
|
||||
else self.password
|
||||
)
|
||||
|
||||
@@ -79,7 +77,7 @@ class TwitchLogin(object):
|
||||
|
||||
if "error_code" in login_response:
|
||||
err_code = login_response["error_code"]
|
||||
if err_code == 3011 or err_code == 3012: # missing 2fa token
|
||||
if err_code in [3011, 3012]: # missing 2fa token
|
||||
if err_code == 3011:
|
||||
logger.info(
|
||||
"Two factor authentication enabled, please enter token below."
|
||||
@@ -91,7 +89,7 @@ class TwitchLogin(object):
|
||||
post_data["authy_token"] = twofa.strip()
|
||||
continue
|
||||
|
||||
elif err_code == 3022 or err_code == 3023: # missing 2fa token
|
||||
elif err_code in [3022, 3023]: # missing 2fa token
|
||||
if err_code == 3022:
|
||||
logger.info("Login Verification code required.")
|
||||
self.email = login_response["obscured_email"]
|
||||
@@ -106,11 +104,12 @@ class TwitchLogin(object):
|
||||
post_data["twitchguard_code"] = twofa.strip()
|
||||
continue
|
||||
|
||||
elif err_code == 3001: # invalid password
|
||||
# invalid password, or password not provided
|
||||
elif err_code in [3001, 3003]:
|
||||
logger.info("Invalid username or password, please try again.")
|
||||
|
||||
# If the password is loaded from run.py, require the user to fix it there.
|
||||
if self.password is not None:
|
||||
if self.password not in [None, ""]:
|
||||
raise BadCredentialsException(
|
||||
"Username or password is incorrect."
|
||||
)
|
||||
|
||||
@@ -176,11 +176,18 @@ class WebSocketsPool:
|
||||
if streamer_index != -1:
|
||||
try:
|
||||
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":
|
||||
earned = message.data["point_gain"]["total_points"]
|
||||
reason_code = message.data["point_gain"]["reason_code"]
|
||||
balance = message.data["balance"]["balance"]
|
||||
ws.streamers[streamer_index].channel_points = balance
|
||||
logger.info(
|
||||
f"+{earned} → {ws.streamers[streamer_index]} - Reason: {reason_code}.",
|
||||
extra={
|
||||
@@ -193,6 +200,9 @@ class WebSocketsPool:
|
||||
ws.streamers[streamer_index].update_history(
|
||||
reason_code, earned
|
||||
)
|
||||
ws.streamers[streamer_index].persistent_annotations(
|
||||
reason_code, f"+{earned} - {reason_code}"
|
||||
)
|
||||
elif message.type == "claim-available":
|
||||
ws.twitch.claim_bonus(
|
||||
ws.streamers[streamer_index],
|
||||
@@ -333,8 +343,18 @@ class WebSocketsPool:
|
||||
-points["won"],
|
||||
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":
|
||||
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:
|
||||
logger.error(
|
||||
f"Exception raised for topic: {message.topic} and message: {message}",
|
||||
|
||||
@@ -58,8 +58,12 @@ class Message(object):
|
||||
else (
|
||||
self.data["channel_id"]
|
||||
if "channel_id" in self.data
|
||||
else (
|
||||
self.data["balance"]["channel_id"]
|
||||
if "balance" in self.data
|
||||
else self.topic_user
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
from threading import Lock
|
||||
|
||||
from TwitchChannelPointsMiner.classes.Chat import ThreadChat
|
||||
from TwitchChannelPointsMiner.classes.entities.Bet import BetSettings
|
||||
@@ -71,6 +75,7 @@ class Streamer(object):
|
||||
"raid",
|
||||
"history",
|
||||
"streamer_url",
|
||||
"mutex",
|
||||
]
|
||||
|
||||
def __init__(self, username, settings=None):
|
||||
@@ -93,6 +98,8 @@ class Streamer(object):
|
||||
|
||||
self.streamer_url = f"{URL}/{self.username}"
|
||||
|
||||
self.mutex = Lock()
|
||||
|
||||
def __repr__(self):
|
||||
return f"Streamer(username={self.username}, channel_id={self.channel_id}, channel_points={_millify(self.channel_points)})"
|
||||
|
||||
@@ -161,6 +168,47 @@ class Streamer(object):
|
||||
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)
|
||||
|
||||
def leave_chat(self):
|
||||
if self.irc_chat is not None:
|
||||
self.irc_chat.stop()
|
||||
|
||||
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,5 +6,6 @@ python-dateutil
|
||||
emoji
|
||||
millify
|
||||
pre-commit
|
||||
irc
|
||||
colorama
|
||||
flask
|
||||
irc
|
||||
Reference in New Issue
Block a user