Spaces:
Sleeping
Sleeping
Sasha commited on
Commit ·
54a28a5
1
Parent(s): a295ebf
feat: role_backfiller auto mode - fetches all VODs from server
Browse files- local_worker/role_backfiller.py +97 -53
local_worker/role_backfiller.py
CHANGED
|
@@ -19,18 +19,37 @@ headers = {
|
|
| 19 |
"Content-Type": "application/json"
|
| 20 |
}
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
def download_vod_roles(vod_id):
|
| 23 |
"""Download chat log of a past VOD and extract only roles"""
|
| 24 |
-
roles_dict = {}
|
| 25 |
url = "https://gql.twitch.tv/gql"
|
| 26 |
gql_headers = {
|
| 27 |
"Client-Id": "kimne78kx3ncx6brgo4mv6wki5h1ko",
|
| 28 |
"User-Agent": "Mozilla/5.0"
|
| 29 |
}
|
| 30 |
-
|
| 31 |
current_offset = 0
|
| 32 |
-
print(f"\n[Roles] Запуск сверхбыстрого парсера ролей для VOD {vod_id}...")
|
| 33 |
-
|
| 34 |
total_messages = 0
|
| 35 |
|
| 36 |
while True:
|
|
@@ -47,58 +66,49 @@ def download_vod_roles(vod_id):
|
|
| 47 |
}
|
| 48 |
}
|
| 49 |
}
|
| 50 |
-
|
| 51 |
try:
|
| 52 |
res = requests.post(url, json=payload, headers=gql_headers, timeout=10)
|
| 53 |
if res.status_code != 200:
|
| 54 |
-
print(f"[Roles] Ошибка GQL: {res.status_code}")
|
| 55 |
break
|
| 56 |
-
|
| 57 |
data = res.json()
|
| 58 |
if isinstance(data, list):
|
| 59 |
data = data[0]
|
| 60 |
-
|
| 61 |
video = data.get("data", {}).get("video", {})
|
| 62 |
if not video:
|
| 63 |
break
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
edges = comments_edge.get("edges") or []
|
| 67 |
if not edges:
|
| 68 |
break
|
| 69 |
-
|
| 70 |
for edge in edges:
|
| 71 |
if not edge:
|
| 72 |
continue
|
| 73 |
node = edge.get("node")
|
| 74 |
if not node:
|
| 75 |
continue
|
| 76 |
-
|
| 77 |
commenter = node.get("commenter")
|
| 78 |
if not commenter:
|
| 79 |
continue
|
| 80 |
-
|
| 81 |
user = commenter.get("login")
|
| 82 |
if not user:
|
| 83 |
continue
|
| 84 |
-
|
| 85 |
display_name = commenter.get("displayName", user)
|
| 86 |
-
|
| 87 |
message = node.get("message")
|
| 88 |
if not message:
|
| 89 |
continue
|
| 90 |
-
|
| 91 |
timestamp = node.get("createdAt")
|
| 92 |
-
|
| 93 |
-
# Badges parse
|
| 94 |
user_badges = message.get("userBadges") or []
|
| 95 |
badges = [b.get("setID") for b in user_badges if b]
|
| 96 |
is_mod = "moderator" in badges or "broadcaster" in badges
|
| 97 |
is_sub = "subscriber" in badges or "founder" in badges
|
| 98 |
is_vip = "vip" in badges
|
| 99 |
-
|
| 100 |
-
# We always overwrite with the latest role from the VOD
|
| 101 |
-
# because the chat goes forward in time
|
| 102 |
roles_dict[user.lower()] = {
|
| 103 |
"username": user,
|
| 104 |
"displayName": display_name,
|
|
@@ -108,10 +118,9 @@ def download_vod_roles(vod_id):
|
|
| 108 |
"timestamp": timestamp
|
| 109 |
}
|
| 110 |
total_messages += 1
|
| 111 |
-
|
| 112 |
-
print(f"-> Обработано {total_messages} сообщений
|
| 113 |
-
|
| 114 |
-
# Progress offset
|
| 115 |
last_offset = edges[-1].get("node", {}).get("contentOffsetSeconds")
|
| 116 |
if last_offset is not None:
|
| 117 |
next_offset = last_offset + 1
|
|
@@ -121,37 +130,72 @@ def download_vod_roles(vod_id):
|
|
| 121 |
else:
|
| 122 |
break
|
| 123 |
except Exception as e:
|
| 124 |
-
print(f"\n[
|
| 125 |
break
|
| 126 |
-
|
| 127 |
-
print(f"\n[Roles] Парсинг завершен. Всего уникальных пользователей: {len(roles_dict)}")
|
| 128 |
return list(roles_dict.values())
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
if __name__ == "__main__":
|
| 131 |
print("=========================================================")
|
| 132 |
-
print(" Ultra-Fast Role Backfiller
|
| 133 |
print("=========================================================")
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
sys.exit(0)
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
"Content-Type": "application/json"
|
| 20 |
}
|
| 21 |
|
| 22 |
+
def get_all_streams():
|
| 23 |
+
"""Fetch all streams from the backend"""
|
| 24 |
+
try:
|
| 25 |
+
res = requests.get(f"{API_URL}/api/streams", headers=headers, timeout=10)
|
| 26 |
+
if res.status_code == 200:
|
| 27 |
+
data = res.json()
|
| 28 |
+
# API returns { streams: [...] } or just [...]
|
| 29 |
+
if isinstance(data, list):
|
| 30 |
+
return data
|
| 31 |
+
return data.get("streams", [])
|
| 32 |
+
except Exception as e:
|
| 33 |
+
print(f"[Error] Не удалось получить список стримов: {e}")
|
| 34 |
+
return []
|
| 35 |
+
|
| 36 |
+
def extract_vod_id(stream):
|
| 37 |
+
"""Extract VOD ID from stream's twitch_stream_id field (e.g. 'vod-2154382910' -> '2154382910')"""
|
| 38 |
+
twitch_id = stream.get("twitch_stream_id", "")
|
| 39 |
+
if twitch_id.startswith("vod-"):
|
| 40 |
+
return twitch_id[4:]
|
| 41 |
+
return None
|
| 42 |
+
|
| 43 |
def download_vod_roles(vod_id):
|
| 44 |
"""Download chat log of a past VOD and extract only roles"""
|
| 45 |
+
roles_dict = {}
|
| 46 |
url = "https://gql.twitch.tv/gql"
|
| 47 |
gql_headers = {
|
| 48 |
"Client-Id": "kimne78kx3ncx6brgo4mv6wki5h1ko",
|
| 49 |
"User-Agent": "Mozilla/5.0"
|
| 50 |
}
|
| 51 |
+
|
| 52 |
current_offset = 0
|
|
|
|
|
|
|
| 53 |
total_messages = 0
|
| 54 |
|
| 55 |
while True:
|
|
|
|
| 66 |
}
|
| 67 |
}
|
| 68 |
}
|
| 69 |
+
|
| 70 |
try:
|
| 71 |
res = requests.post(url, json=payload, headers=gql_headers, timeout=10)
|
| 72 |
if res.status_code != 200:
|
|
|
|
| 73 |
break
|
| 74 |
+
|
| 75 |
data = res.json()
|
| 76 |
if isinstance(data, list):
|
| 77 |
data = data[0]
|
| 78 |
+
|
| 79 |
video = data.get("data", {}).get("video", {})
|
| 80 |
if not video:
|
| 81 |
break
|
| 82 |
+
|
| 83 |
+
edges = (video.get("comments") or {}).get("edges") or []
|
|
|
|
| 84 |
if not edges:
|
| 85 |
break
|
| 86 |
+
|
| 87 |
for edge in edges:
|
| 88 |
if not edge:
|
| 89 |
continue
|
| 90 |
node = edge.get("node")
|
| 91 |
if not node:
|
| 92 |
continue
|
|
|
|
| 93 |
commenter = node.get("commenter")
|
| 94 |
if not commenter:
|
| 95 |
continue
|
|
|
|
| 96 |
user = commenter.get("login")
|
| 97 |
if not user:
|
| 98 |
continue
|
| 99 |
+
|
| 100 |
display_name = commenter.get("displayName", user)
|
|
|
|
| 101 |
message = node.get("message")
|
| 102 |
if not message:
|
| 103 |
continue
|
| 104 |
+
|
| 105 |
timestamp = node.get("createdAt")
|
|
|
|
|
|
|
| 106 |
user_badges = message.get("userBadges") or []
|
| 107 |
badges = [b.get("setID") for b in user_badges if b]
|
| 108 |
is_mod = "moderator" in badges or "broadcaster" in badges
|
| 109 |
is_sub = "subscriber" in badges or "founder" in badges
|
| 110 |
is_vip = "vip" in badges
|
| 111 |
+
|
|
|
|
|
|
|
| 112 |
roles_dict[user.lower()] = {
|
| 113 |
"username": user,
|
| 114 |
"displayName": display_name,
|
|
|
|
| 118 |
"timestamp": timestamp
|
| 119 |
}
|
| 120 |
total_messages += 1
|
| 121 |
+
|
| 122 |
+
print(f" -> Обработано {total_messages} сообщений, найдено {len(roles_dict)} уникальных пользователей...", end="\r")
|
| 123 |
+
|
|
|
|
| 124 |
last_offset = edges[-1].get("node", {}).get("contentOffsetSeconds")
|
| 125 |
if last_offset is not None:
|
| 126 |
next_offset = last_offset + 1
|
|
|
|
| 130 |
else:
|
| 131 |
break
|
| 132 |
except Exception as e:
|
| 133 |
+
print(f"\n [!] Ошибка при загрузке: {e}")
|
| 134 |
break
|
| 135 |
+
|
|
|
|
| 136 |
return list(roles_dict.values())
|
| 137 |
|
| 138 |
+
def upload_roles(roles, vod_id):
|
| 139 |
+
"""Send roles to backend in batches"""
|
| 140 |
+
batch_size = 500
|
| 141 |
+
for i in range(0, len(roles), batch_size):
|
| 142 |
+
batch = roles[i:i+batch_size]
|
| 143 |
+
try:
|
| 144 |
+
res = requests.post(f"{API_URL}/api/log/roles", json={"roles": batch}, headers=headers, timeout=10)
|
| 145 |
+
if res.status_code == 200:
|
| 146 |
+
print(f" -> Обновлено пользователей: {i + len(batch)} / {len(roles)}", end="\r")
|
| 147 |
+
else:
|
| 148 |
+
print(f"\n [!] Ошибка отправки: {res.status_code}")
|
| 149 |
+
except Exception as e:
|
| 150 |
+
print(f"\n [!] Ошибка сети: {e}")
|
| 151 |
+
print()
|
| 152 |
+
|
| 153 |
if __name__ == "__main__":
|
| 154 |
print("=========================================================")
|
| 155 |
+
print(" Ultra-Fast Role Backfiller (AUTO MODE) ")
|
| 156 |
print("=========================================================")
|
| 157 |
+
print(f"Backend: {API_URL}")
|
| 158 |
+
print()
|
| 159 |
+
|
| 160 |
+
# Fetch all streams
|
| 161 |
+
print("[1] Получение списка стримов с сервера...")
|
| 162 |
+
streams = get_all_streams()
|
| 163 |
+
|
| 164 |
+
if not streams:
|
| 165 |
+
print("[Error] Список стримов пустой или не удалось получить!")
|
| 166 |
+
sys.exit(1)
|
| 167 |
+
|
| 168 |
+
# Filter only VODs (twitch_stream_id starts with "vod-")
|
| 169 |
+
vod_streams = [s for s in streams if str(s.get("twitch_stream_id", "")).startswith("vod-")]
|
| 170 |
+
|
| 171 |
+
print(f"[+] Найдено стримов всего: {len(streams)}")
|
| 172 |
+
print(f"[+] Из них архивных VOD: {len(vod_streams)}")
|
| 173 |
+
print()
|
| 174 |
+
|
| 175 |
+
if not vod_streams:
|
| 176 |
+
print("[!] Нет VOD-стримов для обработки. Выход.")
|
| 177 |
sys.exit(0)
|
| 178 |
+
|
| 179 |
+
# Process each VOD
|
| 180 |
+
for idx, stream in enumerate(vod_streams, 1):
|
| 181 |
+
vod_id = extract_vod_id(stream)
|
| 182 |
+
title = stream.get("title", "Без названия")
|
| 183 |
+
print(f"[{idx}/{len(vod_streams)}] VOD {vod_id} — «{title}»")
|
| 184 |
+
|
| 185 |
+
if not vod_id:
|
| 186 |
+
print(" -> Пропуск: нет VOD ID")
|
| 187 |
+
continue
|
| 188 |
+
|
| 189 |
+
roles = download_vod_roles(vod_id)
|
| 190 |
+
print(f"\n -> Найдено пользователей с бейджами: {len(roles)}")
|
| 191 |
+
|
| 192 |
+
if roles:
|
| 193 |
+
upload_roles(roles, vod_id)
|
| 194 |
+
print(f" -> ✅ Готово!")
|
| 195 |
+
else:
|
| 196 |
+
print(f" -> Чат пустой или VOD недоступен, пропуск.")
|
| 197 |
+
print()
|
| 198 |
+
|
| 199 |
+
print("=========================================================")
|
| 200 |
+
print(" Восстановление ролей завершено! ")
|
| 201 |
+
print("=========================================================")
|