feat: replace analytics-only UI with full miner config dashboard
add JSON-backed miner configuration make new config UI the root web interface move analytics view to /analytics add follower import with selectable and sortable streamer priority list add notifier, logger, streamer default, and prediction settings controls add prediction presets and per-streamer overrides preserve mounted cookies/logs/analytics for Docker deployment mask secrets in config API responses
This commit is contained in:
+1
-1
@@ -141,7 +141,7 @@ cython_debug/
|
||||
.idea/
|
||||
|
||||
# Custom files
|
||||
run.py
|
||||
config/miner.json
|
||||
chromedriver*
|
||||
|
||||
# Folders
|
||||
|
||||
@@ -44,4 +44,6 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get install -qq -y --fix-missing --no-ins
|
||||
&& rm -rf /usr/share/doc/*
|
||||
|
||||
ADD ./TwitchChannelPointsMiner ./TwitchChannelPointsMiner
|
||||
ADD ./assets ./assets
|
||||
ADD ./run.py ./run.py
|
||||
ENTRYPOINT [ "python", "run.py" ]
|
||||
|
||||
@@ -194,6 +194,7 @@ class TwitchChannelPointsMiner:
|
||||
refresh=refresh,
|
||||
days_ago=days_ago,
|
||||
username=self.username,
|
||||
miner=self,
|
||||
)
|
||||
http_server.daemon = True
|
||||
http_server.name = "Analytics Thread"
|
||||
@@ -510,4 +511,4 @@ class TwitchChannelPointsMiner:
|
||||
logger.info(
|
||||
f"{streamer_gain}\n{streamer_history}",
|
||||
extra={"emoji": ":moneybag:"},
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
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 flask import Flask, Response, cli, jsonify, render_template, request
|
||||
|
||||
from TwitchChannelPointsMiner.classes import ConfigManager
|
||||
from TwitchChannelPointsMiner.classes.Settings import FollowersOrder
|
||||
from TwitchChannelPointsMiner.classes.Settings import Settings
|
||||
from TwitchChannelPointsMiner.utils import download_file
|
||||
|
||||
@@ -207,6 +210,9 @@ def check_assets():
|
||||
required_files = [
|
||||
"banner.png",
|
||||
"charts.html",
|
||||
"config.css",
|
||||
"config.html",
|
||||
"config.js",
|
||||
"script.js",
|
||||
"style.css",
|
||||
"dark-theme.css",
|
||||
@@ -231,7 +237,8 @@ class AnalyticsServer(Thread):
|
||||
port: int = 5000,
|
||||
refresh: int = 5,
|
||||
days_ago: int = 7,
|
||||
username: str = None
|
||||
username: str = None,
|
||||
miner=None
|
||||
):
|
||||
super(AnalyticsServer, self).__init__()
|
||||
|
||||
@@ -242,6 +249,7 @@ class AnalyticsServer(Thread):
|
||||
self.refresh = refresh
|
||||
self.days_ago = days_ago
|
||||
self.username = username
|
||||
self.miner = miner
|
||||
|
||||
def generate_log():
|
||||
global last_sent_log_index # Use the global variable
|
||||
@@ -269,8 +277,9 @@ class AnalyticsServer(Thread):
|
||||
template_folder=os.path.join(Path().absolute(), "assets"),
|
||||
static_folder=os.path.join(Path().absolute(), "assets"),
|
||||
)
|
||||
self.app.add_url_rule("/", "config", self.config_page, methods=["GET"])
|
||||
self.app.add_url_rule(
|
||||
"/",
|
||||
"/analytics",
|
||||
"index",
|
||||
index,
|
||||
defaults={"refresh": refresh, "days_ago": days_ago},
|
||||
@@ -285,6 +294,50 @@ class AnalyticsServer(Thread):
|
||||
json_all, methods=["GET"])
|
||||
self.app.add_url_rule(
|
||||
"/log", "log", generate_log, methods=["GET"])
|
||||
self.app.add_url_rule("/config", "config_alias", self.config_page, methods=["GET"])
|
||||
self.app.add_url_rule("/api/config", "api_config_get", self.get_config, methods=["GET"])
|
||||
self.app.add_url_rule("/api/config", "api_config_put", self.put_config, methods=["PUT"])
|
||||
self.app.add_url_rule("/api/meta", "api_meta", self.get_meta, methods=["GET"])
|
||||
self.app.add_url_rule(
|
||||
"/api/followers/import",
|
||||
"api_followers_import",
|
||||
self.import_followers,
|
||||
methods=["POST"],
|
||||
)
|
||||
self.app.add_url_rule("/api/restart", "api_restart", self.restart, methods=["POST"])
|
||||
|
||||
def config_page(self):
|
||||
return render_template("config.html")
|
||||
|
||||
def get_meta(self):
|
||||
return jsonify(ConfigManager.metadata())
|
||||
|
||||
def get_config(self):
|
||||
return jsonify(ConfigManager.client_config(ConfigManager.load_config()))
|
||||
|
||||
def put_config(self):
|
||||
config = request.get_json(force=True)
|
||||
saved = ConfigManager.save_config(config)
|
||||
return jsonify(ConfigManager.client_config(saved))
|
||||
|
||||
def import_followers(self):
|
||||
if self.miner is None or self.miner.twitch is None:
|
||||
return jsonify({"error": "Miner is not ready yet."}), 503
|
||||
payload = request.get_json(silent=True) or {}
|
||||
order = ConfigManager.enum_member(
|
||||
FollowersOrder,
|
||||
payload.get("followers_order", "ASC"),
|
||||
FollowersOrder.ASC,
|
||||
)
|
||||
limit = int(payload.get("limit", 100))
|
||||
return jsonify({"followers": self.miner.twitch.get_followers(limit=limit, order=order)})
|
||||
|
||||
def restart(self):
|
||||
def exit_process():
|
||||
os._exit(0)
|
||||
|
||||
threading.Timer(0.5, exit_process).start()
|
||||
return jsonify({"status": "restarting"})
|
||||
|
||||
def run(self):
|
||||
logger.info(
|
||||
|
||||
@@ -0,0 +1,462 @@
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from TwitchChannelPointsMiner import TwitchChannelPointsMiner
|
||||
from TwitchChannelPointsMiner.classes.Chat import ChatPresence
|
||||
from TwitchChannelPointsMiner.classes.Discord import Discord
|
||||
from TwitchChannelPointsMiner.classes.Gotify import Gotify
|
||||
from TwitchChannelPointsMiner.classes.Matrix import Matrix
|
||||
from TwitchChannelPointsMiner.classes.Pushover import Pushover
|
||||
from TwitchChannelPointsMiner.classes.Settings import Events, FollowersOrder, Priority
|
||||
from TwitchChannelPointsMiner.classes.Telegram import Telegram
|
||||
from TwitchChannelPointsMiner.classes.Webhook import Webhook
|
||||
from TwitchChannelPointsMiner.classes.entities.Bet import (
|
||||
BetSettings,
|
||||
Condition,
|
||||
DelayMode,
|
||||
FilterCondition,
|
||||
OutcomeKeys,
|
||||
Strategy,
|
||||
)
|
||||
from TwitchChannelPointsMiner.classes.entities.Streamer import (
|
||||
Streamer,
|
||||
StreamerSettings,
|
||||
)
|
||||
from TwitchChannelPointsMiner.logger import ColorPalette, LoggerSettings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_PATH = Path(os.environ.get("MINER_CONFIG_PATH", "config/miner.json"))
|
||||
MASKED_SECRET = "********"
|
||||
SECRET_PATHS = [
|
||||
"account.password",
|
||||
"notifications.telegram.token",
|
||||
"notifications.discord.webhook_api",
|
||||
"notifications.webhook.endpoint",
|
||||
"notifications.matrix.password",
|
||||
"notifications.pushover.userkey",
|
||||
"notifications.pushover.token",
|
||||
"notifications.gotify.endpoint",
|
||||
]
|
||||
|
||||
OUTCOME_KEYS = [
|
||||
OutcomeKeys.PERCENTAGE_USERS,
|
||||
OutcomeKeys.ODDS_PERCENTAGE,
|
||||
OutcomeKeys.ODDS,
|
||||
OutcomeKeys.TOP_POINTS,
|
||||
OutcomeKeys.TOTAL_USERS,
|
||||
OutcomeKeys.TOTAL_POINTS,
|
||||
OutcomeKeys.DECISION_USERS,
|
||||
OutcomeKeys.DECISION_POINTS,
|
||||
]
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"account": {
|
||||
"username": "",
|
||||
"password": "",
|
||||
},
|
||||
"miner": {
|
||||
"claim_drops_startup": False,
|
||||
"enable_analytics": True,
|
||||
"disable_ssl_cert_verification": False,
|
||||
"disable_at_in_nickname": False,
|
||||
"priority": ["STREAK", "DROPS", "ORDER"],
|
||||
"followers": False,
|
||||
"followers_order": "ASC",
|
||||
"blacklist": [],
|
||||
},
|
||||
"analytics": {
|
||||
"host": "0.0.0.0",
|
||||
"port": 5000,
|
||||
"refresh": 5,
|
||||
"days_ago": 7,
|
||||
},
|
||||
"logger": {
|
||||
"save": True,
|
||||
"console_level": "INFO",
|
||||
"console_username": False,
|
||||
"auto_clear": True,
|
||||
"time_zone": "",
|
||||
"file_level": "INFO",
|
||||
"emoji": True,
|
||||
"less": True,
|
||||
"colored": True,
|
||||
"color_palette": {
|
||||
"STREAMER_ONLINE": "GREEN",
|
||||
"STREAMER_OFFLINE": "RED",
|
||||
"BET_WIN": "MAGENTA",
|
||||
},
|
||||
},
|
||||
"notifications": {
|
||||
"telegram": {
|
||||
"enabled": False,
|
||||
"chat_id": "",
|
||||
"token": "",
|
||||
"events": [],
|
||||
"disable_notification": True,
|
||||
},
|
||||
"discord": {"enabled": False, "webhook_api": "", "events": []},
|
||||
"webhook": {
|
||||
"enabled": False,
|
||||
"endpoint": "",
|
||||
"method": "GET",
|
||||
"events": [],
|
||||
},
|
||||
"matrix": {
|
||||
"enabled": False,
|
||||
"username": "",
|
||||
"password": "",
|
||||
"homeserver": "matrix.org",
|
||||
"room_id": "",
|
||||
"events": [],
|
||||
},
|
||||
"pushover": {
|
||||
"enabled": False,
|
||||
"userkey": "",
|
||||
"token": "",
|
||||
"priority": 0,
|
||||
"sound": "pushover",
|
||||
"events": [],
|
||||
},
|
||||
"gotify": {"enabled": False, "endpoint": "", "priority": 8, "events": []},
|
||||
},
|
||||
"streamer_defaults": {
|
||||
"make_predictions": True,
|
||||
"follow_raid": True,
|
||||
"claim_drops": True,
|
||||
"claim_moments": True,
|
||||
"watch_streak": True,
|
||||
"community_goals": False,
|
||||
"chat": "ONLINE",
|
||||
"bet": {
|
||||
"strategy": "SMART",
|
||||
"percentage": 5,
|
||||
"percentage_gap": 20,
|
||||
"max_points": 50000,
|
||||
"minimum_points": 0,
|
||||
"stealth_mode": False,
|
||||
"delay_mode": "FROM_END",
|
||||
"delay": 6,
|
||||
"filter_condition": {
|
||||
"enabled": False,
|
||||
"by": "total_users",
|
||||
"where": "GTE",
|
||||
"value": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
"streamers": [],
|
||||
}
|
||||
|
||||
|
||||
def metadata():
|
||||
return {
|
||||
"events": enum_names(Events),
|
||||
"priorities": enum_names(Priority),
|
||||
"followers_orders": enum_names(FollowersOrder),
|
||||
"chat_presences": enum_names(ChatPresence),
|
||||
"bet_strategies": enum_names(Strategy),
|
||||
"delay_modes": enum_names(DelayMode),
|
||||
"filter_conditions": enum_names(Condition),
|
||||
"outcome_keys": OUTCOME_KEYS,
|
||||
"log_levels": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
||||
"colors": [
|
||||
"BLACK",
|
||||
"RED",
|
||||
"GREEN",
|
||||
"YELLOW",
|
||||
"BLUE",
|
||||
"MAGENTA",
|
||||
"CYAN",
|
||||
"WHITE",
|
||||
"RESET",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def enum_names(enum_class):
|
||||
return [item.name for item in enum_class]
|
||||
|
||||
|
||||
def load_config(path=CONFIG_PATH):
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
if Path(path).is_file():
|
||||
with open(path, "r", encoding="utf-8") as file:
|
||||
config = deep_merge(config, json.load(file))
|
||||
return config
|
||||
|
||||
|
||||
def save_config(config, path=CONFIG_PATH, preserve_secrets=True):
|
||||
if preserve_secrets and Path(path).is_file():
|
||||
config = preserve_existing_secrets(config, load_config(path))
|
||||
normalized = deep_merge(copy.deepcopy(DEFAULT_CONFIG), config)
|
||||
target = Path(path)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
temp = target.with_suffix(target.suffix + ".tmp")
|
||||
with open(temp, "w", encoding="utf-8") as file:
|
||||
json.dump(normalized, file, indent=2)
|
||||
file.write("\n")
|
||||
os.replace(temp, target)
|
||||
return normalized
|
||||
|
||||
|
||||
def client_config(config):
|
||||
sanitized = copy.deepcopy(config)
|
||||
for path in SECRET_PATHS:
|
||||
value = get_path(sanitized, path)
|
||||
if value not in (None, ""):
|
||||
set_path(sanitized, path, MASKED_SECRET)
|
||||
return sanitized
|
||||
|
||||
|
||||
def preserve_existing_secrets(incoming, existing):
|
||||
merged = copy.deepcopy(incoming)
|
||||
for path in SECRET_PATHS:
|
||||
value = get_path(merged, path)
|
||||
if value in ("", None, MASKED_SECRET):
|
||||
set_path(merged, path, get_path(existing, path))
|
||||
return merged
|
||||
|
||||
|
||||
def deep_merge(base, override):
|
||||
for key, value in override.items():
|
||||
if isinstance(value, dict) and isinstance(base.get(key), dict):
|
||||
base[key] = deep_merge(base[key], value)
|
||||
else:
|
||||
base[key] = value
|
||||
return base
|
||||
|
||||
|
||||
def get_path(config, path):
|
||||
current = config
|
||||
for part in path.split("."):
|
||||
if not isinstance(current, dict):
|
||||
return None
|
||||
current = current.get(part)
|
||||
return current
|
||||
|
||||
|
||||
def set_path(config, path, value):
|
||||
parts = path.split(".")
|
||||
current = config
|
||||
for part in parts[:-1]:
|
||||
current = current.setdefault(part, {})
|
||||
current[parts[-1]] = value
|
||||
|
||||
|
||||
def build_runtime(config):
|
||||
config = deep_merge(copy.deepcopy(DEFAULT_CONFIG), config)
|
||||
logger_settings = build_logger_settings(config)
|
||||
streamer_defaults = build_streamer_settings(config["streamer_defaults"])
|
||||
|
||||
miner = TwitchChannelPointsMiner(
|
||||
username=config["account"]["username"],
|
||||
password=config["account"].get("password") or None,
|
||||
claim_drops_startup=as_bool(config["miner"]["claim_drops_startup"]),
|
||||
enable_analytics=as_bool(config["miner"]["enable_analytics"]),
|
||||
disable_ssl_cert_verification=as_bool(
|
||||
config["miner"]["disable_ssl_cert_verification"]
|
||||
),
|
||||
disable_at_in_nickname=as_bool(config["miner"]["disable_at_in_nickname"]),
|
||||
priority=[
|
||||
enum_member(Priority, item, Priority.ORDER)
|
||||
for item in config["miner"].get("priority", [])
|
||||
],
|
||||
logger_settings=logger_settings,
|
||||
streamer_settings=streamer_defaults,
|
||||
)
|
||||
|
||||
streamers = [
|
||||
build_streamer(item)
|
||||
for item in config.get("streamers", [])
|
||||
if item.get("enabled", True) and item.get("username", "").strip()
|
||||
]
|
||||
|
||||
return {
|
||||
"miner": miner,
|
||||
"analytics": config["analytics"],
|
||||
"mine": {
|
||||
"streamers": streamers,
|
||||
"blacklist": config["miner"].get("blacklist", []),
|
||||
"followers": as_bool(config["miner"].get("followers", False)),
|
||||
"followers_order": enum_member(
|
||||
FollowersOrder,
|
||||
config["miner"].get("followers_order"),
|
||||
FollowersOrder.ASC,
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def build_logger_settings(config):
|
||||
logger_config = config["logger"]
|
||||
notifications = config["notifications"]
|
||||
return LoggerSettings(
|
||||
save=as_bool(logger_config["save"]),
|
||||
console_level=log_level(logger_config["console_level"]),
|
||||
console_username=as_bool(logger_config["console_username"]),
|
||||
auto_clear=as_bool(logger_config["auto_clear"]),
|
||||
time_zone=logger_config.get("time_zone") or "",
|
||||
file_level=log_level(logger_config["file_level"]),
|
||||
emoji=as_bool(logger_config["emoji"]),
|
||||
less=as_bool(logger_config["less"]),
|
||||
colored=as_bool(logger_config["colored"]),
|
||||
color_palette=ColorPalette(**logger_config.get("color_palette", {})),
|
||||
telegram=build_telegram(notifications["telegram"]),
|
||||
discord=build_discord(notifications["discord"]),
|
||||
webhook=build_webhook(notifications["webhook"]),
|
||||
matrix=build_matrix(notifications["matrix"]),
|
||||
pushover=build_pushover(notifications["pushover"]),
|
||||
gotify=build_gotify(notifications["gotify"]),
|
||||
)
|
||||
|
||||
|
||||
def build_telegram(config):
|
||||
if not as_bool(config.get("enabled")):
|
||||
return None
|
||||
chat_id = config.get("chat_id")
|
||||
return Telegram(
|
||||
chat_id=int(chat_id) if str(chat_id).strip() else 123456789,
|
||||
token=config.get("token", ""),
|
||||
events=event_list(config.get("events", [])),
|
||||
disable_notification=as_bool(config.get("disable_notification", False)),
|
||||
)
|
||||
|
||||
|
||||
def build_discord(config):
|
||||
if not as_bool(config.get("enabled")):
|
||||
return None
|
||||
return Discord(config.get("webhook_api", ""), event_list(config.get("events", [])))
|
||||
|
||||
|
||||
def build_webhook(config):
|
||||
if not as_bool(config.get("enabled")):
|
||||
return None
|
||||
return Webhook(
|
||||
config.get("endpoint", ""),
|
||||
config.get("method", "GET"),
|
||||
event_list(config.get("events", [])),
|
||||
)
|
||||
|
||||
|
||||
def build_matrix(config):
|
||||
if not as_bool(config.get("enabled")):
|
||||
return None
|
||||
return Matrix(
|
||||
config.get("username", ""),
|
||||
config.get("password", ""),
|
||||
config.get("homeserver", "matrix.org"),
|
||||
config.get("room_id", ""),
|
||||
event_list(config.get("events", [])),
|
||||
)
|
||||
|
||||
|
||||
def build_pushover(config):
|
||||
if not as_bool(config.get("enabled")):
|
||||
return None
|
||||
return Pushover(
|
||||
config.get("userkey", ""),
|
||||
config.get("token", ""),
|
||||
int(config.get("priority", 0)),
|
||||
config.get("sound", "pushover"),
|
||||
event_list(config.get("events", [])),
|
||||
)
|
||||
|
||||
|
||||
def build_gotify(config):
|
||||
if not as_bool(config.get("enabled")):
|
||||
return None
|
||||
return Gotify(
|
||||
config.get("endpoint", ""),
|
||||
int(config.get("priority", 8)),
|
||||
event_list(config.get("events", [])),
|
||||
)
|
||||
|
||||
|
||||
def build_streamer(config):
|
||||
settings_config = config.get("settings")
|
||||
settings = build_streamer_settings(settings_config) if settings_config else None
|
||||
return Streamer(config["username"], settings=settings)
|
||||
|
||||
|
||||
def build_streamer_settings(config):
|
||||
return StreamerSettings(
|
||||
make_predictions=nullable_bool(config.get("make_predictions")),
|
||||
follow_raid=nullable_bool(config.get("follow_raid")),
|
||||
claim_drops=nullable_bool(config.get("claim_drops")),
|
||||
claim_moments=nullable_bool(config.get("claim_moments")),
|
||||
watch_streak=nullable_bool(config.get("watch_streak")),
|
||||
community_goals=nullable_bool(config.get("community_goals")),
|
||||
chat=enum_member(ChatPresence, config.get("chat"), None),
|
||||
bet=build_bet_settings(config.get("bet", {})),
|
||||
)
|
||||
|
||||
|
||||
def build_bet_settings(config):
|
||||
return BetSettings(
|
||||
strategy=enum_member(Strategy, config.get("strategy"), None),
|
||||
percentage=nullable_int(config.get("percentage")),
|
||||
percentage_gap=nullable_int(config.get("percentage_gap")),
|
||||
max_points=nullable_int(config.get("max_points")),
|
||||
minimum_points=nullable_int(config.get("minimum_points")),
|
||||
stealth_mode=nullable_bool(config.get("stealth_mode")),
|
||||
delay=nullable_float(config.get("delay")),
|
||||
delay_mode=enum_member(DelayMode, config.get("delay_mode"), None),
|
||||
filter_condition=build_filter_condition(config.get("filter_condition", {})),
|
||||
)
|
||||
|
||||
|
||||
def build_filter_condition(config):
|
||||
if not as_bool(config.get("enabled")):
|
||||
return None
|
||||
return FilterCondition(
|
||||
by=config.get("by", OutcomeKeys.TOTAL_USERS),
|
||||
where=enum_member(Condition, config.get("where"), Condition.GTE),
|
||||
value=nullable_float(config.get("value")) or 0,
|
||||
)
|
||||
|
||||
|
||||
def event_list(values):
|
||||
return [enum_member(Events, value, None) for value in values if enum_member(Events, value, None)]
|
||||
|
||||
|
||||
def enum_member(enum_class, value, default):
|
||||
if value is None:
|
||||
return default
|
||||
if isinstance(value, enum_class):
|
||||
return value
|
||||
name = str(value).upper()
|
||||
return getattr(enum_class, name, default)
|
||||
|
||||
|
||||
def log_level(value):
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
return getattr(logging, str(value).upper(), logging.INFO)
|
||||
|
||||
|
||||
def nullable_int(value):
|
||||
if value in (None, ""):
|
||||
return None
|
||||
return int(value)
|
||||
|
||||
|
||||
def nullable_float(value):
|
||||
if value in (None, ""):
|
||||
return None
|
||||
return float(value)
|
||||
|
||||
|
||||
def nullable_bool(value):
|
||||
if value is None:
|
||||
return None
|
||||
return as_bool(value)
|
||||
|
||||
|
||||
def as_bool(value):
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
return str(value).lower() in ["1", "true", "yes", "on"]
|
||||
+2
-1
@@ -145,6 +145,7 @@
|
||||
<input class="input" type="date" id="endDate">
|
||||
</div>
|
||||
<div class="column has-text-right" style="margin: auto">
|
||||
<a class="button" href="/">Config</a>
|
||||
<button id="auto-update-log">⏸️</button>
|
||||
<label class="checkbox checkbox-label">
|
||||
Log
|
||||
@@ -206,4 +207,4 @@
|
||||
</script>
|
||||
<script type="text/javascript" src="{{url_for('static', filename='script.js')}}"></script>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,445 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #101114;
|
||||
--panel: #181a20;
|
||||
--panel-2: #20232b;
|
||||
--line: #343844;
|
||||
--text: #f0f2f5;
|
||||
--muted: #9ca3af;
|
||||
--accent: #00c7a7;
|
||||
--danger: #ff5c77;
|
||||
--focus: #8b5cf6;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font: 14px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
inset: 0 auto 0 0;
|
||||
width: 238px;
|
||||
border-right: 1px solid var(--line);
|
||||
background: #15171d;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 8px;
|
||||
background: var(--accent);
|
||||
color: #07110f;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.brand span:last-child {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: var(--muted);
|
||||
text-decoration: none;
|
||||
padding: 9px 10px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
background: var(--panel-2);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 238px;
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.topbar,
|
||||
.section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 4px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 18px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
section {
|
||||
border-top: 1px solid var(--line);
|
||||
padding: 26px 0;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.two {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.three {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.four {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
label {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
background: var(--panel);
|
||||
color: var(--text);
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
min-height: 38px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 420px;
|
||||
resize: vertical;
|
||||
font-family: "Cascadia Code", "Consolas", monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
border-color: var(--focus);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.checks {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
|
||||
gap: 10px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.checks label,
|
||||
.checkline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
min-height: 38px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 17px;
|
||||
min-height: 17px;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 38px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
background: var(--accent);
|
||||
color: #06110f;
|
||||
font-weight: 750;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: var(--panel-2);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
button.danger {
|
||||
background: var(--danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.actions,
|
||||
.inline-actions,
|
||||
.tabs,
|
||||
.pill-list {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.inline-actions input {
|
||||
width: 210px;
|
||||
}
|
||||
|
||||
.notice {
|
||||
margin: 16px 0;
|
||||
padding: 11px 12px;
|
||||
border-radius: 6px;
|
||||
background: #123229;
|
||||
color: #c6fff3;
|
||||
border: 1px solid #1d6b59;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pill,
|
||||
.streamer-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--panel);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.pill button,
|
||||
.streamer-row button {
|
||||
min-height: 30px;
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.streamer-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.streamer-board {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 0.9fr) minmax(340px, 1.1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.streamer-panel {
|
||||
min-height: 420px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--panel);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.panel-head h3 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.panel-head span,
|
||||
.summary-line {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.inline-actions.full {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.inline-actions.full input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.available-list,
|
||||
.selected-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
max-height: 540px;
|
||||
overflow: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.available-row,
|
||||
.selected-row {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 7px;
|
||||
background: var(--panel-2);
|
||||
color: var(--text);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.available-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.selected-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.selected-row.dragging {
|
||||
opacity: 0.6;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
width: 42px;
|
||||
color: var(--muted);
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.streamer-settings {
|
||||
flex-basis: 100%;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.compact {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.prediction-presets {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.summary-line {
|
||||
margin-bottom: 14px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.streamer-row input[type="text"] {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.streamer-row .spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.tabs button {
|
||||
background: var(--panel-2);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.tabs button.active {
|
||||
background: var(--accent);
|
||||
color: #06110f;
|
||||
}
|
||||
|
||||
.notifier-box {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.events {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.sidebar {
|
||||
position: static;
|
||||
width: auto;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.topbar,
|
||||
.section-head {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.two,
|
||||
.three,
|
||||
.four,
|
||||
.streamer-board {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Twitch Miner Konfiguration</title>
|
||||
<link rel="icon" type="image/png" sizes="32x32"
|
||||
href="https://static.twitchcdn.net/assets/favicon-32-e29e246c157142c94346.png">
|
||||
<link href="{{url_for('static', filename='config.css')}}" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<aside class="sidebar">
|
||||
<div class="brand">
|
||||
<span class="brand-mark">CP</span>
|
||||
<div>
|
||||
<strong>Twitch Miner</strong>
|
||||
<span>Konfiguration</span>
|
||||
</div>
|
||||
</div>
|
||||
<nav>
|
||||
<a href="#account">Account</a>
|
||||
<a href="#streamers">Kanale</a>
|
||||
<a href="#defaults">Defaults</a>
|
||||
<a href="#bets">Predictions</a>
|
||||
<a href="#notifications">Notifier</a>
|
||||
<a href="#logger">Logging</a>
|
||||
<a href="#advanced">JSON</a>
|
||||
<a href="/analytics">Analytics</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main>
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<h1>Bot steuern</h1>
|
||||
<p>Speichern schreibt die JSON-Konfiguration. Ein Neustart aktiviert die neue Laufzeit.</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button id="save">Speichern</button>
|
||||
<button id="restart" class="danger">Neustart</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="notice" class="notice hidden"></div>
|
||||
|
||||
<section id="account">
|
||||
<h2>Account und Miner</h2>
|
||||
<div class="grid two">
|
||||
<label>Twitch Username<input data-path="account.username" autocomplete="username"></label>
|
||||
<label>Twitch Passwort<input data-path="account.password" type="password" autocomplete="current-password"></label>
|
||||
<label>Analytics Host<input data-path="analytics.host"></label>
|
||||
<label>Analytics Port<input data-path="analytics.port" type="number" min="1" max="65535"></label>
|
||||
<label>Refresh Minuten<input data-path="analytics.refresh" type="number" min="1"></label>
|
||||
<label>Zeitraum Tage<input data-path="analytics.days_ago" type="number" min="1"></label>
|
||||
</div>
|
||||
<div class="checks">
|
||||
<label><input data-path="miner.claim_drops_startup" type="checkbox"> Drops beim Start claimen</label>
|
||||
<label><input data-path="miner.enable_analytics" type="checkbox"> Analytics aktivieren</label>
|
||||
<label><input data-path="miner.disable_ssl_cert_verification" type="checkbox"> SSL-Prufung deaktivieren</label>
|
||||
<label><input data-path="miner.disable_at_in_nickname" type="checkbox"> Chat-Mention ohne @ erkennen</label>
|
||||
<label><input data-path="miner.followers" type="checkbox"> Twitch-Follows automatisch minen</label>
|
||||
</div>
|
||||
<div class="grid two">
|
||||
<label>Follower-Reihenfolge<select data-path="miner.followers_order" data-options="followers_orders"></select></label>
|
||||
<label>Blacklist, komma-getrennt<input id="blacklist"></label>
|
||||
</div>
|
||||
<div class="priority">
|
||||
<h3>Globale Watch-Prioritat</h3>
|
||||
<div id="priority-list" class="pill-list"></div>
|
||||
<select id="priority-add" data-options="priorities"></select>
|
||||
<button id="add-priority" type="button">Hinzufugen</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="streamers">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2>Kanale</h2>
|
||||
<p>Links stehen importierte oder manuelle Kanale. Rechts steht die aktive Watch-Reihenfolge.</p>
|
||||
</div>
|
||||
<div class="inline-actions">
|
||||
<button id="import-followers" type="button">Follows importieren</button>
|
||||
<button id="select-all-visible" type="button" class="secondary">Sichtbare auswahlen</button>
|
||||
<button id="clear-selected" type="button" class="secondary">Auswahl leeren</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="streamer-board">
|
||||
<div class="streamer-panel">
|
||||
<div class="panel-head">
|
||||
<h3>Verfugbare Kanale</h3>
|
||||
<span id="available-count"></span>
|
||||
</div>
|
||||
<div class="inline-actions full">
|
||||
<input id="streamer-name" placeholder="channelname">
|
||||
<button id="add-streamer" type="button">Hinzufugen</button>
|
||||
</div>
|
||||
<input id="streamer-search" placeholder="Suchen">
|
||||
<div id="available-streamers" class="available-list"></div>
|
||||
</div>
|
||||
<div class="streamer-panel">
|
||||
<div class="panel-head">
|
||||
<h3>Aktive Watch-Prioritat</h3>
|
||||
<span id="selected-count"></span>
|
||||
</div>
|
||||
<div id="selected-streamers" class="selected-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="defaults">
|
||||
<h2>Streamer Defaults</h2>
|
||||
<div class="checks">
|
||||
<label><input data-path="streamer_defaults.make_predictions" type="checkbox"> Predictions setzen</label>
|
||||
<label><input data-path="streamer_defaults.follow_raid" type="checkbox"> Raids folgen</label>
|
||||
<label><input data-path="streamer_defaults.claim_drops" type="checkbox"> Drops claimen</label>
|
||||
<label><input data-path="streamer_defaults.claim_moments" type="checkbox"> Moments claimen</label>
|
||||
<label><input data-path="streamer_defaults.watch_streak" type="checkbox"> Watch Streak priorisieren</label>
|
||||
<label><input data-path="streamer_defaults.community_goals" type="checkbox"> Community Goals beitragen</label>
|
||||
</div>
|
||||
<label>Chat Presence<select data-path="streamer_defaults.chat" data-options="chat_presences"></select></label>
|
||||
</section>
|
||||
|
||||
<section id="bets">
|
||||
<h2>Prediction Settings</h2>
|
||||
<div class="prediction-presets">
|
||||
<button type="button" data-preset="off" class="secondary">Aus</button>
|
||||
<button type="button" data-preset="conservative" class="secondary">Konservativ</button>
|
||||
<button type="button" data-preset="balanced" class="secondary">Balanced</button>
|
||||
<button type="button" data-preset="aggressive" class="secondary">Aggressiv</button>
|
||||
</div>
|
||||
<div id="prediction-summary" class="summary-line"></div>
|
||||
<div class="grid four">
|
||||
<label>Strategie<select data-path="streamer_defaults.bet.strategy" data-options="bet_strategies"></select></label>
|
||||
<label>Prozent<input data-path="streamer_defaults.bet.percentage" type="number" min="0"></label>
|
||||
<label>Smart Gap<input data-path="streamer_defaults.bet.percentage_gap" type="number" min="0"></label>
|
||||
<label>Max Points<input data-path="streamer_defaults.bet.max_points" type="number" min="0"></label>
|
||||
<label>Minimum Points<input data-path="streamer_defaults.bet.minimum_points" type="number" min="0"></label>
|
||||
<label>Delay Mode<select data-path="streamer_defaults.bet.delay_mode" data-options="delay_modes"></select></label>
|
||||
<label>Delay<input data-path="streamer_defaults.bet.delay" type="number" min="0" step="0.1"></label>
|
||||
<label class="checkline"><input data-path="streamer_defaults.bet.stealth_mode" type="checkbox"> Stealth Mode</label>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label class="checkline"><input data-path="streamer_defaults.bet.filter_condition.enabled" type="checkbox"> Filterbedingung aktiv</label>
|
||||
<div class="grid three">
|
||||
<label>Feld<select data-path="streamer_defaults.bet.filter_condition.by" data-options="outcome_keys"></select></label>
|
||||
<label>Bedingung<select data-path="streamer_defaults.bet.filter_condition.where" data-options="filter_conditions"></select></label>
|
||||
<label>Wert<input data-path="streamer_defaults.bet.filter_condition.value" type="number" step="0.1"></label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="notifications">
|
||||
<h2>Notifier</h2>
|
||||
<div id="notifier-tabs" class="tabs"></div>
|
||||
<div id="notifier-panel"></div>
|
||||
</section>
|
||||
|
||||
<section id="logger">
|
||||
<h2>Logging</h2>
|
||||
<div class="grid four">
|
||||
<label>Console Level<select data-path="logger.console_level" data-options="log_levels"></select></label>
|
||||
<label>File Level<select data-path="logger.file_level" data-options="log_levels"></select></label>
|
||||
<label>Timezone<input data-path="logger.time_zone" placeholder="Europe/Berlin"></label>
|
||||
<label>Online Farbe<select data-path="logger.color_palette.STREAMER_ONLINE" data-options="colors"></select></label>
|
||||
<label>Offline Farbe<select data-path="logger.color_palette.STREAMER_OFFLINE" data-options="colors"></select></label>
|
||||
<label>Bet-Win Farbe<select data-path="logger.color_palette.BET_WIN" data-options="colors"></select></label>
|
||||
</div>
|
||||
<div class="checks">
|
||||
<label><input data-path="logger.save" type="checkbox"> Logs speichern</label>
|
||||
<label><input data-path="logger.console_username" type="checkbox"> Username in Console Logs</label>
|
||||
<label><input data-path="logger.auto_clear" type="checkbox"> Rotation 7 Tage</label>
|
||||
<label><input data-path="logger.emoji" type="checkbox"> Emoji</label>
|
||||
<label><input data-path="logger.less" type="checkbox"> Kurze Logs</label>
|
||||
<label><input data-path="logger.colored" type="checkbox"> Farben</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="advanced">
|
||||
<h2>Advanced JSON</h2>
|
||||
<p>Vollstandige Konfiguration fur seltene Optionen und pro-Kanal Overrides.</p>
|
||||
<textarea id="raw-config" spellcheck="false"></textarea>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="{{url_for('static', filename='config.js')}}"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,615 @@
|
||||
let config = null;
|
||||
let meta = null;
|
||||
let activeNotifier = "telegram";
|
||||
let availableStreamers = [];
|
||||
let draggedStreamerIndex = null;
|
||||
|
||||
const notifierFields = {
|
||||
telegram: [
|
||||
["chat_id", "Chat ID", "number"],
|
||||
["token", "Bot Token", "password"],
|
||||
["disable_notification", "Silent", "checkbox"],
|
||||
],
|
||||
discord: [["webhook_api", "Webhook URL", "text"]],
|
||||
webhook: [
|
||||
["endpoint", "Endpoint", "text"],
|
||||
["method", "Methode", "select:GET,POST"],
|
||||
],
|
||||
matrix: [
|
||||
["username", "Username", "text"],
|
||||
["password", "Passwort", "password"],
|
||||
["homeserver", "Homeserver", "text"],
|
||||
["room_id", "Room ID", "text"],
|
||||
],
|
||||
pushover: [
|
||||
["userkey", "User Key", "text"],
|
||||
["token", "App Token", "password"],
|
||||
["priority", "Priority", "number"],
|
||||
["sound", "Sound", "text"],
|
||||
],
|
||||
gotify: [
|
||||
["endpoint", "Endpoint", "text"],
|
||||
["priority", "Priority", "number"],
|
||||
],
|
||||
};
|
||||
|
||||
const predictionPresets = {
|
||||
off: { make_predictions: false },
|
||||
conservative: {
|
||||
make_predictions: true,
|
||||
bet: {
|
||||
strategy: "SMART",
|
||||
percentage: 3,
|
||||
percentage_gap: 25,
|
||||
max_points: 2500,
|
||||
minimum_points: 25000,
|
||||
stealth_mode: true,
|
||||
delay_mode: "FROM_END",
|
||||
delay: 4,
|
||||
filter_condition: { enabled: true, by: "total_users", where: "GTE", value: 25 },
|
||||
},
|
||||
},
|
||||
balanced: {
|
||||
make_predictions: true,
|
||||
bet: {
|
||||
strategy: "SMART_MONEY",
|
||||
percentage: 7,
|
||||
percentage_gap: 18,
|
||||
max_points: 10000,
|
||||
minimum_points: 20000,
|
||||
stealth_mode: true,
|
||||
delay_mode: "FROM_END",
|
||||
delay: 3,
|
||||
filter_condition: { enabled: true, by: "total_users", where: "GTE", value: 10 },
|
||||
},
|
||||
},
|
||||
aggressive: {
|
||||
make_predictions: true,
|
||||
bet: {
|
||||
strategy: "HIGH_ODDS",
|
||||
percentage: 12,
|
||||
percentage_gap: 10,
|
||||
max_points: 50000,
|
||||
minimum_points: 5000,
|
||||
stealth_mode: false,
|
||||
delay_mode: "FROM_END",
|
||||
delay: 1,
|
||||
filter_condition: { enabled: false, by: "total_users", where: "GTE", value: 1 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
const [metaResponse, configResponse] = await Promise.all([
|
||||
fetch("/api/meta"),
|
||||
fetch("/api/config"),
|
||||
]);
|
||||
meta = await metaResponse.json();
|
||||
config = await configResponse.json();
|
||||
normalizeConfig();
|
||||
availableStreamers = uniqueNames(config.streamers.map((streamer) => streamer.username));
|
||||
|
||||
hydrateOptions();
|
||||
bindStaticInputs();
|
||||
renderPriority();
|
||||
renderStreamerBoard();
|
||||
renderPredictionSummary();
|
||||
renderNotifierTabs();
|
||||
renderNotifierPanel();
|
||||
syncRawConfig();
|
||||
bindActions();
|
||||
});
|
||||
|
||||
function normalizeConfig() {
|
||||
config.streamers = (config.streamers || [])
|
||||
.map((streamer) => typeof streamer === "string" ? { username: cleanName(streamer), enabled: true } : streamer)
|
||||
.filter((streamer) => cleanName(streamer.username))
|
||||
.map((streamer) => ({ ...streamer, username: cleanName(streamer.username), enabled: streamer.enabled !== false }));
|
||||
}
|
||||
|
||||
function hydrateOptions() {
|
||||
document.querySelectorAll("select[data-options]").forEach((select) => {
|
||||
const values = meta[select.dataset.options] || [];
|
||||
select.replaceChildren(...values.map((value) => option(value, value)));
|
||||
});
|
||||
}
|
||||
|
||||
function bindStaticInputs() {
|
||||
document.querySelectorAll("[data-path]").forEach((inputElement) => {
|
||||
setInputValue(inputElement, getPath(config, inputElement.dataset.path));
|
||||
inputElement.addEventListener("input", () => {
|
||||
setPath(config, inputElement.dataset.path, readInputValue(inputElement));
|
||||
renderPredictionSummary();
|
||||
syncRawConfig();
|
||||
});
|
||||
inputElement.addEventListener("change", () => {
|
||||
setPath(config, inputElement.dataset.path, readInputValue(inputElement));
|
||||
renderPredictionSummary();
|
||||
syncRawConfig();
|
||||
});
|
||||
});
|
||||
document.querySelector("#blacklist").value = (config.miner.blacklist || []).join(", ");
|
||||
document.querySelector("#blacklist").addEventListener("input", (event) => {
|
||||
config.miner.blacklist = splitNames(event.target.value);
|
||||
syncRawConfig();
|
||||
});
|
||||
}
|
||||
|
||||
function bindActions() {
|
||||
document.querySelector("#save").addEventListener("click", saveConfig);
|
||||
document.querySelector("#restart").addEventListener("click", restartMiner);
|
||||
document.querySelector("#add-streamer").addEventListener("click", () => {
|
||||
addAvailableStreamer(document.querySelector("#streamer-name").value, true);
|
||||
document.querySelector("#streamer-name").value = "";
|
||||
});
|
||||
document.querySelector("#streamer-name").addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
addAvailableStreamer(event.target.value, true);
|
||||
event.target.value = "";
|
||||
}
|
||||
});
|
||||
document.querySelector("#streamer-search").addEventListener("input", renderAvailableStreamers);
|
||||
document.querySelector("#import-followers").addEventListener("click", importFollowers);
|
||||
document.querySelector("#select-all-visible").addEventListener("click", selectAllVisible);
|
||||
document.querySelector("#clear-selected").addEventListener("click", () => {
|
||||
config.streamers = [];
|
||||
renderStreamerBoard();
|
||||
syncRawConfig();
|
||||
});
|
||||
document.querySelector("#add-priority").addEventListener("click", () => {
|
||||
config.miner.priority.push(document.querySelector("#priority-add").value);
|
||||
renderPriority();
|
||||
syncRawConfig();
|
||||
});
|
||||
document.querySelectorAll("[data-preset]").forEach((button) => {
|
||||
button.addEventListener("click", () => applyPredictionPreset(button.dataset.preset));
|
||||
});
|
||||
}
|
||||
|
||||
function renderPriority() {
|
||||
const target = document.querySelector("#priority-list");
|
||||
target.replaceChildren(...config.miner.priority.map((name, index) => {
|
||||
const row = element("div", "pill");
|
||||
row.append(
|
||||
text("strong", name),
|
||||
smallButton("Up", () => move(config.miner.priority, index, index - 1, renderPriority)),
|
||||
smallButton("Down", () => move(config.miner.priority, index, index + 1, renderPriority)),
|
||||
smallButton("Remove", () => {
|
||||
config.miner.priority.splice(index, 1);
|
||||
renderPriority();
|
||||
syncRawConfig();
|
||||
}, "danger")
|
||||
);
|
||||
return row;
|
||||
}));
|
||||
}
|
||||
|
||||
function renderStreamerBoard() {
|
||||
renderAvailableStreamers();
|
||||
renderSelectedStreamers();
|
||||
}
|
||||
|
||||
function renderAvailableStreamers() {
|
||||
const target = document.querySelector("#available-streamers");
|
||||
const selected = new Set(config.streamers.map((streamer) => streamer.username));
|
||||
const search = cleanName(document.querySelector("#streamer-search").value);
|
||||
const names = availableStreamers
|
||||
.filter((name) => !search || name.includes(search))
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
document.querySelector("#available-count").textContent = `${names.length} sichtbar`;
|
||||
target.replaceChildren(...names.map((name) => {
|
||||
const checkbox = input("checkbox", selected.has(name));
|
||||
checkbox.addEventListener("change", () => {
|
||||
if (checkbox.checked) selectStreamer(name);
|
||||
else removeStreamer(name);
|
||||
});
|
||||
const row = element("label", "available-row");
|
||||
row.append(checkbox, text("span", name));
|
||||
return row;
|
||||
}));
|
||||
}
|
||||
|
||||
function renderSelectedStreamers() {
|
||||
const target = document.querySelector("#selected-streamers");
|
||||
document.querySelector("#selected-count").textContent = `${config.streamers.length} aktiv`;
|
||||
target.replaceChildren(...config.streamers.map((streamer, index) => selectedStreamerRow(streamer, index)));
|
||||
}
|
||||
|
||||
function selectedStreamerRow(streamer, index) {
|
||||
const row = element("div", "selected-row");
|
||||
row.draggable = true;
|
||||
row.dataset.index = index;
|
||||
row.addEventListener("dragstart", () => {
|
||||
draggedStreamerIndex = index;
|
||||
row.classList.add("dragging");
|
||||
});
|
||||
row.addEventListener("dragend", () => {
|
||||
draggedStreamerIndex = null;
|
||||
row.classList.remove("dragging");
|
||||
});
|
||||
row.addEventListener("dragover", (event) => event.preventDefault());
|
||||
row.addEventListener("drop", () => {
|
||||
move(config.streamers, draggedStreamerIndex, index, renderStreamerBoard);
|
||||
});
|
||||
|
||||
const handle = text("span", "Drag");
|
||||
handle.className = "drag-handle";
|
||||
const enabled = input("checkbox", streamer.enabled !== false);
|
||||
enabled.addEventListener("change", () => {
|
||||
streamer.enabled = enabled.checked;
|
||||
syncRawConfig();
|
||||
});
|
||||
const title = text("strong", streamer.username);
|
||||
const override = input("checkbox", Boolean(streamer.settings));
|
||||
override.addEventListener("change", () => {
|
||||
streamer.settings = override.checked ? deepClone(config.streamer_defaults) : undefined;
|
||||
renderSelectedStreamers();
|
||||
syncRawConfig();
|
||||
});
|
||||
row.append(
|
||||
handle,
|
||||
enabled,
|
||||
title,
|
||||
element("span", "spacer"),
|
||||
checkLabel(override, "eigene Settings"),
|
||||
smallButton("Up", () => move(config.streamers, index, index - 1, renderStreamerBoard)),
|
||||
smallButton("Down", () => move(config.streamers, index, index + 1, renderStreamerBoard)),
|
||||
smallButton("Remove", () => removeStreamer(streamer.username), "danger")
|
||||
);
|
||||
if (streamer.settings) row.append(streamerSettingsPanel(streamer));
|
||||
return row;
|
||||
}
|
||||
|
||||
function streamerSettingsPanel(streamer) {
|
||||
const panel = element("div", "streamer-settings");
|
||||
const flags = element("div", "checks compact");
|
||||
[
|
||||
["make_predictions", "Predictions"],
|
||||
["follow_raid", "Raids"],
|
||||
["claim_drops", "Drops"],
|
||||
["claim_moments", "Moments"],
|
||||
["watch_streak", "Watch Streak"],
|
||||
["community_goals", "Community Goals"],
|
||||
].forEach(([key, label]) => flags.append(objectCheckbox(streamer.settings, key, label)));
|
||||
const bet = streamer.settings.bet || {};
|
||||
streamer.settings.bet = bet;
|
||||
const betGrid = element("div", "grid four");
|
||||
betGrid.append(
|
||||
objectSelect(bet, "strategy", "Strategie", meta.bet_strategies),
|
||||
objectNumber(bet, "percentage", "Prozent"),
|
||||
objectNumber(bet, "max_points", "Max Points"),
|
||||
objectNumber(bet, "minimum_points", "Min Points"),
|
||||
objectCheckbox(bet, "stealth_mode", "Stealth"),
|
||||
objectSelect(bet, "delay_mode", "Delay Mode", meta.delay_modes),
|
||||
objectNumber(bet, "delay", "Delay"),
|
||||
objectNumber(bet, "percentage_gap", "Smart Gap")
|
||||
);
|
||||
panel.append(flags, betGrid);
|
||||
return panel;
|
||||
}
|
||||
|
||||
function renderPredictionSummary() {
|
||||
const defaults = config.streamer_defaults;
|
||||
const bet = defaults.bet;
|
||||
const summary = defaults.make_predictions
|
||||
? `Aktiv: ${bet.strategy}, ${bet.percentage}% bis max ${bet.max_points}, Minimum ${bet.minimum_points}, Delay ${bet.delay_mode}/${bet.delay}s`
|
||||
: "Predictions sind global deaktiviert.";
|
||||
document.querySelector("#prediction-summary").textContent = summary;
|
||||
}
|
||||
|
||||
function applyPredictionPreset(name) {
|
||||
const preset = predictionPresets[name];
|
||||
config.streamer_defaults = deepMerge(config.streamer_defaults, deepClone(preset));
|
||||
document.querySelectorAll("[data-path]").forEach((inputElement) => {
|
||||
setInputValue(inputElement, getPath(config, inputElement.dataset.path));
|
||||
});
|
||||
renderPredictionSummary();
|
||||
syncRawConfig();
|
||||
}
|
||||
|
||||
function renderNotifierTabs() {
|
||||
const target = document.querySelector("#notifier-tabs");
|
||||
target.replaceChildren(...Object.keys(notifierFields).map((name) => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.textContent = labelize(name);
|
||||
button.className = name === activeNotifier ? "active" : "";
|
||||
button.addEventListener("click", () => {
|
||||
activeNotifier = name;
|
||||
renderNotifierTabs();
|
||||
renderNotifierPanel();
|
||||
});
|
||||
return button;
|
||||
}));
|
||||
}
|
||||
|
||||
function renderNotifierPanel() {
|
||||
const notifier = config.notifications[activeNotifier];
|
||||
const panel = document.querySelector("#notifier-panel");
|
||||
const box = element("div", "notifier-box");
|
||||
const enabled = input("checkbox", notifier.enabled);
|
||||
enabled.addEventListener("change", () => {
|
||||
notifier.enabled = enabled.checked;
|
||||
syncRawConfig();
|
||||
});
|
||||
box.append(checkLabel(enabled, `${labelize(activeNotifier)} aktiv`));
|
||||
|
||||
const fields = element("div", "grid two");
|
||||
notifierFields[activeNotifier].forEach(([key, label, type]) => fields.append(fieldFor(notifier, key, label, type)));
|
||||
box.append(fields, eventPicker(notifier));
|
||||
panel.replaceChildren(box);
|
||||
}
|
||||
|
||||
function fieldFor(object, key, label, type) {
|
||||
const wrapper = document.createElement("label");
|
||||
wrapper.append(document.createTextNode(label));
|
||||
if (type.startsWith("select:")) {
|
||||
const select = document.createElement("select");
|
||||
type.slice(7).split(",").forEach((value) => select.append(option(value, value)));
|
||||
select.value = object[key] || "";
|
||||
select.addEventListener("change", () => {
|
||||
object[key] = select.value;
|
||||
syncRawConfig();
|
||||
});
|
||||
wrapper.append(select);
|
||||
return wrapper;
|
||||
}
|
||||
const control = input(type, object[key]);
|
||||
control.addEventListener("input", () => {
|
||||
object[key] = readInputValue(control);
|
||||
syncRawConfig();
|
||||
});
|
||||
control.addEventListener("change", () => {
|
||||
object[key] = readInputValue(control);
|
||||
syncRawConfig();
|
||||
});
|
||||
wrapper.append(control);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function eventPicker(notifier) {
|
||||
const wrap = element("div", "events");
|
||||
const selected = new Set(notifier.events || []);
|
||||
meta.events.forEach((eventName) => {
|
||||
const checkbox = input("checkbox", selected.has(eventName));
|
||||
checkbox.addEventListener("change", () => {
|
||||
if (checkbox.checked) selected.add(eventName);
|
||||
else selected.delete(eventName);
|
||||
notifier.events = [...selected];
|
||||
syncRawConfig();
|
||||
});
|
||||
wrap.append(checkLabel(checkbox, eventName));
|
||||
});
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function addAvailableStreamer(value, selectAfterAdd = false) {
|
||||
const username = cleanName(value);
|
||||
if (!username) return;
|
||||
if (!availableStreamers.includes(username)) availableStreamers.push(username);
|
||||
if (selectAfterAdd) selectStreamer(username);
|
||||
renderStreamerBoard();
|
||||
syncRawConfig();
|
||||
}
|
||||
|
||||
function selectStreamer(name) {
|
||||
const username = cleanName(name);
|
||||
if (!config.streamers.some((streamer) => streamer.username === username)) {
|
||||
config.streamers.push({ username, enabled: true });
|
||||
}
|
||||
renderStreamerBoard();
|
||||
syncRawConfig();
|
||||
}
|
||||
|
||||
function removeStreamer(name) {
|
||||
const username = cleanName(name);
|
||||
config.streamers = config.streamers.filter((streamer) => streamer.username !== username);
|
||||
renderStreamerBoard();
|
||||
syncRawConfig();
|
||||
}
|
||||
|
||||
function selectAllVisible() {
|
||||
document.querySelectorAll("#available-streamers input[type='checkbox']").forEach((checkbox) => {
|
||||
if (!checkbox.checked) checkbox.click();
|
||||
});
|
||||
}
|
||||
|
||||
async function importFollowers() {
|
||||
showNotice("Import lauft...");
|
||||
const response = await fetch("/api/followers/import", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ followers_order: config.miner.followers_order, limit: 100 }),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
showNotice(data.error || "Follower konnten nicht importiert werden.");
|
||||
return;
|
||||
}
|
||||
const before = availableStreamers.length;
|
||||
availableStreamers = uniqueNames([...availableStreamers, ...data.followers]);
|
||||
renderStreamerBoard();
|
||||
syncRawConfig();
|
||||
showNotice(`${availableStreamers.length - before} neue Kanale importiert. Hak sie links an und sortiere sie rechts.`);
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
try {
|
||||
config = JSON.parse(document.querySelector("#raw-config").value);
|
||||
normalizeConfig();
|
||||
} catch (error) {
|
||||
showNotice(`JSON ist ungultig: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
const response = await fetch("/api/config", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
config = await response.json();
|
||||
normalizeConfig();
|
||||
availableStreamers = uniqueNames([...availableStreamers, ...config.streamers.map((streamer) => streamer.username)]);
|
||||
renderStreamerBoard();
|
||||
renderPredictionSummary();
|
||||
syncRawConfig();
|
||||
showNotice("Gespeichert. Neustart aktiviert die Anderungen.");
|
||||
}
|
||||
|
||||
async function restartMiner() {
|
||||
await fetch("/api/restart", { method: "POST" });
|
||||
showNotice("Neustart angefordert. Die Seite ist gleich wieder erreichbar.");
|
||||
}
|
||||
|
||||
function objectCheckbox(object, key, label) {
|
||||
const checkbox = input("checkbox", object[key]);
|
||||
checkbox.addEventListener("change", () => {
|
||||
object[key] = checkbox.checked;
|
||||
syncRawConfig();
|
||||
});
|
||||
return checkLabel(checkbox, label);
|
||||
}
|
||||
|
||||
function objectNumber(object, key, label) {
|
||||
return objectField(object, key, label, "number");
|
||||
}
|
||||
|
||||
function objectSelect(object, key, label, values) {
|
||||
const wrapper = document.createElement("label");
|
||||
wrapper.append(document.createTextNode(label));
|
||||
const select = document.createElement("select");
|
||||
values.forEach((value) => select.append(option(value, value)));
|
||||
select.value = object[key] || values[0];
|
||||
select.addEventListener("change", () => {
|
||||
object[key] = select.value;
|
||||
syncRawConfig();
|
||||
});
|
||||
wrapper.append(select);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function objectField(object, key, label, type) {
|
||||
const wrapper = document.createElement("label");
|
||||
wrapper.append(document.createTextNode(label));
|
||||
const control = input(type, object[key]);
|
||||
control.addEventListener("input", () => {
|
||||
object[key] = readInputValue(control);
|
||||
syncRawConfig();
|
||||
});
|
||||
wrapper.append(control);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function getPath(object, path) {
|
||||
return path.split(".").reduce((current, key) => current?.[key], object);
|
||||
}
|
||||
|
||||
function setPath(object, path, value) {
|
||||
const keys = path.split(".");
|
||||
const last = keys.pop();
|
||||
const target = keys.reduce((current, key) => current[key], object);
|
||||
target[last] = value;
|
||||
}
|
||||
|
||||
function setInputValue(inputElement, value) {
|
||||
if (inputElement.type === "checkbox") inputElement.checked = Boolean(value);
|
||||
else inputElement.value = value ?? "";
|
||||
}
|
||||
|
||||
function readInputValue(inputElement) {
|
||||
if (inputElement.type === "checkbox") return inputElement.checked;
|
||||
if (inputElement.type === "number") return inputElement.value === "" ? "" : Number(inputElement.value);
|
||||
return inputElement.value;
|
||||
}
|
||||
|
||||
function splitNames(value) {
|
||||
return value.split(",").map(cleanName).filter(Boolean);
|
||||
}
|
||||
|
||||
function cleanName(value) {
|
||||
return String(value || "").trim().toLowerCase().replace(/^@/, "");
|
||||
}
|
||||
|
||||
function uniqueNames(values) {
|
||||
return [...new Set(values.map(cleanName).filter(Boolean))];
|
||||
}
|
||||
|
||||
function move(array, from, to, callback) {
|
||||
if (from === null || from === undefined || to < 0 || to >= array.length) return;
|
||||
const [item] = array.splice(from, 1);
|
||||
array.splice(to, 0, item);
|
||||
callback();
|
||||
syncRawConfig();
|
||||
}
|
||||
|
||||
function option(value, label) {
|
||||
const item = document.createElement("option");
|
||||
item.value = value;
|
||||
item.textContent = label;
|
||||
return item;
|
||||
}
|
||||
|
||||
function input(type, value) {
|
||||
const control = document.createElement("input");
|
||||
control.type = type;
|
||||
setInputValue(control, value);
|
||||
return control;
|
||||
}
|
||||
|
||||
function smallButton(label, onClick, className = "secondary") {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = className;
|
||||
button.textContent = label;
|
||||
button.addEventListener("click", onClick);
|
||||
return button;
|
||||
}
|
||||
|
||||
function element(tag, className) {
|
||||
const node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
return node;
|
||||
}
|
||||
|
||||
function text(tag, content) {
|
||||
const node = document.createElement(tag);
|
||||
node.textContent = content;
|
||||
return node;
|
||||
}
|
||||
|
||||
function checkLabel(control, label) {
|
||||
const wrapper = document.createElement("label");
|
||||
wrapper.className = "checkline";
|
||||
wrapper.append(control, document.createTextNode(label));
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function labelize(value) {
|
||||
return value.charAt(0).toUpperCase() + value.slice(1);
|
||||
}
|
||||
|
||||
function showNotice(message) {
|
||||
const notice = document.querySelector("#notice");
|
||||
notice.textContent = message;
|
||||
notice.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function syncRawConfig() {
|
||||
const raw = document.querySelector("#raw-config");
|
||||
if (raw) raw.value = JSON.stringify(config, null, 2);
|
||||
}
|
||||
|
||||
function deepClone(value) {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
function deepMerge(base, override) {
|
||||
const result = deepClone(base);
|
||||
Object.entries(override).forEach(([key, value]) => {
|
||||
if (value && typeof value === "object" && !Array.isArray(value) && result[key]) {
|
||||
result[key] = deepMerge(result[key], value);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"account": {
|
||||
"username": "your-twitch-username",
|
||||
"password": ""
|
||||
},
|
||||
"miner": {
|
||||
"claim_drops_startup": false,
|
||||
"enable_analytics": true,
|
||||
"disable_ssl_cert_verification": false,
|
||||
"disable_at_in_nickname": false,
|
||||
"priority": ["STREAK", "DROPS", "ORDER"],
|
||||
"followers": false,
|
||||
"followers_order": "ASC",
|
||||
"blacklist": []
|
||||
},
|
||||
"analytics": {
|
||||
"host": "0.0.0.0",
|
||||
"port": 5000,
|
||||
"refresh": 5,
|
||||
"days_ago": 7
|
||||
},
|
||||
"logger": {
|
||||
"save": true,
|
||||
"console_level": "INFO",
|
||||
"console_username": false,
|
||||
"auto_clear": true,
|
||||
"time_zone": "",
|
||||
"file_level": "INFO",
|
||||
"emoji": true,
|
||||
"less": true,
|
||||
"colored": true,
|
||||
"color_palette": {
|
||||
"STREAMER_ONLINE": "GREEN",
|
||||
"STREAMER_OFFLINE": "RED",
|
||||
"BET_WIN": "MAGENTA"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"chat_id": "",
|
||||
"token": "",
|
||||
"events": ["STREAMER_ONLINE", "STREAMER_OFFLINE", "CHAT_MENTION"],
|
||||
"disable_notification": true
|
||||
},
|
||||
"discord": {
|
||||
"enabled": false,
|
||||
"webhook_api": "",
|
||||
"events": ["STREAMER_ONLINE", "STREAMER_OFFLINE", "CHAT_MENTION"]
|
||||
},
|
||||
"webhook": {
|
||||
"enabled": false,
|
||||
"endpoint": "",
|
||||
"method": "GET",
|
||||
"events": []
|
||||
},
|
||||
"matrix": {
|
||||
"enabled": false,
|
||||
"username": "",
|
||||
"password": "",
|
||||
"homeserver": "matrix.org",
|
||||
"room_id": "",
|
||||
"events": []
|
||||
},
|
||||
"pushover": {
|
||||
"enabled": false,
|
||||
"userkey": "",
|
||||
"token": "",
|
||||
"priority": 0,
|
||||
"sound": "pushover",
|
||||
"events": []
|
||||
},
|
||||
"gotify": {
|
||||
"enabled": false,
|
||||
"endpoint": "",
|
||||
"priority": 8,
|
||||
"events": []
|
||||
}
|
||||
},
|
||||
"streamer_defaults": {
|
||||
"make_predictions": true,
|
||||
"follow_raid": true,
|
||||
"claim_drops": true,
|
||||
"claim_moments": true,
|
||||
"watch_streak": true,
|
||||
"community_goals": false,
|
||||
"chat": "ONLINE",
|
||||
"bet": {
|
||||
"strategy": "SMART",
|
||||
"percentage": 5,
|
||||
"percentage_gap": 20,
|
||||
"max_points": 50000,
|
||||
"minimum_points": 0,
|
||||
"stealth_mode": false,
|
||||
"delay_mode": "FROM_END",
|
||||
"delay": 6,
|
||||
"filter_condition": {
|
||||
"enabled": false,
|
||||
"by": "total_users",
|
||||
"where": "GTE",
|
||||
"value": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"streamers": [
|
||||
{
|
||||
"username": "streamer-username",
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from TwitchChannelPointsMiner.classes.ConfigManager import build_runtime, load_config
|
||||
|
||||
|
||||
config = load_config()
|
||||
runtime = build_runtime(config)
|
||||
twitch_miner = runtime["miner"]
|
||||
|
||||
if config["miner"]["enable_analytics"]:
|
||||
analytics = runtime["analytics"]
|
||||
twitch_miner.analytics(
|
||||
host=analytics["host"],
|
||||
port=int(analytics["port"]),
|
||||
refresh=int(analytics["refresh"]),
|
||||
days_ago=int(analytics["days_ago"]),
|
||||
)
|
||||
|
||||
twitch_miner.mine(**runtime["mine"])
|
||||
Reference in New Issue
Block a user