Spaces:
Sleeping
Sleeping
File size: 10,937 Bytes
513eb9c 54a28a5 f26efec 54a28a5 f26efec 54a28a5 f26efec 54a28a5 513eb9c f26efec 54a28a5 513eb9c 54a28a5 513eb9c f26efec 513eb9c e98099c 513eb9c 54a28a5 e98099c f26efec e98099c 54a28a5 513eb9c f26efec 54a28a5 513eb9c 54a28a5 513eb9c 54a28a5 f26efec 54a28a5 f26efec 54a28a5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 | import os
import sys
import time
import requests
from dotenv import load_dotenv
# Load local environment settings
load_dotenv()
API_URL = os.getenv("API_URL", "http://localhost:3000")
API_KEY = os.getenv("API_KEY", "")
if not API_KEY:
print("[Error] API_KEY is missing in .env! Cannot push data to backend.")
sys.exit(1)
headers = {
"x-api-key": API_KEY,
"Content-Type": "application/json"
}
def get_all_streams():
"""Fetch all streams from the backend"""
try:
res = requests.get(f"{API_URL}/api/streams", headers=headers, timeout=10)
if res.status_code == 200:
data = res.json()
# API returns { streams: [...] } or just [...]
if isinstance(data, list):
return data
return data.get("streams", [])
except Exception as e:
print(f"[Error] Не удалось получить список стримов: {e}")
return []
def find_vod_id_by_stream_id(stream_id):
"""Try to find a VOD ID for a given Twitch stream ID using GQL"""
url = "https://gql.twitch.tv/gql"
gql_headers = {
"Client-Id": "kimne78kx3ncx6brgo4mv6wki5h1ko",
"User-Agent": "Mozilla/5.0"
}
payload = {
"query": f"""
query {{
channel(name: "{os.getenv('TWITCH_CHANNEL', 'winx_prinx')}") {{
videos(first: 30, type: ARCHIVE) {{
edges {{
node {{
id
broadcastType
lengthSeconds
publishedAt
stream {{
id
}}
}}
}}
}}
}}
}}
"""
}
try:
res = requests.post(url, json=payload, headers=gql_headers, timeout=10)
if res.status_code == 200:
data = res.json()
edges = data.get("data", {}).get("channel", {}).get("videos", {}).get("edges", [])
for edge in edges:
node = edge.get("node", {})
stream_info = node.get("stream") or {}
if stream_info.get("id") == str(stream_id):
return node.get("id")
except Exception as e:
pass
return None
def extract_vod_id(stream):
"""Extract VOD ID from stream record"""
twitch_id = str(stream.get("twitch_stream_id", ""))
if twitch_id.startswith("vod-"):
return twitch_id[4:]
# Live stream ID — try to find corresponding VOD
print(f" -> Поиск VOD для live stream ID {twitch_id}...")
vod_id = find_vod_id_by_stream_id(twitch_id)
if vod_id:
print(f" -> Найден VOD ID: {vod_id}")
else:
print(f" -> VOD не найден (стрим мог быть удалён или VOD недоступен)")
return vod_id
def download_vod_roles(vod_id):
"""Download chat log of a past VOD and extract only roles, with retry on failure"""
roles_dict = {}
url = "https://gql.twitch.tv/gql"
gql_headers = {
"Client-Id": "kimne78kx3ncx6brgo4mv6wki5h1ko",
"User-Agent": "Mozilla/5.0"
}
current_offset = 0
total_messages = 0
MAX_RETRIES = 5
with requests.Session() as session:
while True:
payload = {
"operationName": "VideoCommentsByOffsetOrCursor",
"variables": {
"videoID": str(vod_id),
"contentOffsetSeconds": current_offset
},
"extensions": {
"persistedQuery": {
"version": 1,
"sha256Hash": "b70a3591ff0f4e0313d126c6a1502d79a1c02baebb288227c582044aa76adf6a"
}
}
}
success = False
for attempt in range(MAX_RETRIES):
try:
res = session.post(url, json=payload, headers=gql_headers, timeout=15)
if res.status_code == 429:
wait = 2 ** attempt
print(f"\n [!] Rate limited (429). Повтор через {wait}с...")
time.sleep(wait)
continue
if res.status_code != 200:
print(f"\n [!] Ошибка GQL статус {res.status_code}")
break
data = res.json()
if isinstance(data, list):
data = data[0]
video = data.get("data", {}).get("video", {})
if not video:
return list(roles_dict.values()) # VOD ended or not found
edges = (video.get("comments") or {}).get("edges") or []
if not edges:
return list(roles_dict.values()) # Reached end
for edge in edges:
if not edge:
continue
node = edge.get("node")
if not node:
continue
commenter = node.get("commenter")
if not commenter:
continue
user = commenter.get("login")
if not user:
continue
display_name = commenter.get("displayName", user)
message = node.get("message")
if not message:
continue
timestamp = node.get("createdAt")
user_badges = message.get("userBadges") or []
badges = [b.get("setID") for b in user_badges if b]
# Debug: print first badge encounter to verify GQL structure
if total_messages < 3 and user_badges:
print(f"\n [DEBUG] userBadges raw: {user_badges}")
print(f" [DEBUG] parsed badges: {badges}")
is_mod = "moderator" in badges or "broadcaster" in badges
is_sub = "subscriber" in badges or "founder" in badges
is_vip = "vip" in badges
# Only store users who have at least one badge
if is_mod or is_sub or is_vip:
roles_dict[user.lower()] = {
"username": user,
"displayName": display_name,
"isMod": is_mod,
"isSub": is_sub,
"isVip": is_vip,
"timestamp": timestamp
}
total_messages += 1
print(f" -> Обработано {total_messages} сообщений, найдено {len(roles_dict)} уникальных пользователей...", end="\r")
last_offset = edges[-1].get("node", {}).get("contentOffsetSeconds")
if last_offset is not None:
next_offset = last_offset + 1
if next_offset <= current_offset:
next_offset = current_offset + 30
current_offset = next_offset
else:
return list(roles_dict.values())
# Tiny sleep to avoid aggressive spamming
time.sleep(0.1)
success = True
break # Success — go to next page
except Exception as e:
wait = 2 ** attempt # 1, 2, 4, 8, 16 seconds
print(f"\n [!] Ошибка (попытка {attempt+1}/{MAX_RETRIES}): {e}")
if attempt < MAX_RETRIES - 1:
print(f" [!] Повтор через {wait}с...")
time.sleep(wait)
if not success:
print(f"\n [!] Не удалось получить данные после {MAX_RETRIES} попыток. Останавливаемся на offset={current_offset}.")
break
return list(roles_dict.values())
def upload_roles(roles, vod_id):
"""Send roles to backend in batches"""
batch_size = 500
for i in range(0, len(roles), batch_size):
batch = roles[i:i+batch_size]
try:
res = requests.post(f"{API_URL}/api/log/roles", json={"roles": batch}, headers=headers, timeout=10)
if res.status_code == 200:
print(f" -> Обновлено пользователей: {i + len(batch)} / {len(roles)}", end="\r")
else:
print(f"\n [!] Ошибка отправки: {res.status_code}")
except Exception as e:
print(f"\n [!] Ошибка сети: {e}")
print()
if __name__ == "__main__":
print("=========================================================")
print(" Ultra-Fast Role Backfiller (AUTO MODE) ")
print("=========================================================")
print(f"Backend: {API_URL}")
print()
# Fetch all streams
print("[1] Получение списка стримов с сервера...")
streams = get_all_streams()
if not streams:
print("[Error] Список стримов пустой или не удалось получить!")
sys.exit(1)
# Filter only VODs (twitch_stream_id starts with "vod-")
vod_streams = [s for s in streams if str(s.get("twitch_stream_id", "")).startswith("vod-")]
print(f"[+] Найдено стримов всего: {len(streams)}")
print()
# Process each stream
for idx, stream in enumerate(streams, 1):
vod_id = extract_vod_id(stream)
title = stream.get("title", "Без названия")
print(f"[{idx}/{len(streams)}] Stream — «{title[:50]}»")
if not vod_id:
print(" -> Пропуск: нет VOD ID")
continue
roles = download_vod_roles(vod_id)
print(f"\n -> Найдено пользователей с бейджами: {len(roles)}")
if roles:
upload_roles(roles, vod_id)
print(f" -> ✅ Готово!")
else:
print(f" -> Чат пустой или VOD недоступен, пропуск.")
print()
print("=========================================================")
print(" Восстановление ролей завершено! ")
print("=========================================================")
|