Sasha commited on
Commit
54a28a5
·
1 Parent(s): a295ebf

feat: role_backfiller auto mode - fetches all VODs from server

Browse files
Files changed (1) hide show
  1. 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 = {} # Store latest role per user
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
- comments_edge = video.get("comments") or {}
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} сообщений. Найдено уникальных пользователей: {len(roles_dict)}...", end="\r")
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[Roles] Ошибка при загрузке: {e}")
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
- vod_id = input("Введите ID Twitch VOD (например, 2154382910): ").strip()
136
- if not vod_id:
137
- print("ID VOD не введен. Выход.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  sys.exit(0)
139
-
140
- roles = download_vod_roles(vod_id)
141
- if roles:
142
- print("\n[Sync] Отправка ролей на сервер (пакетами по 500 пользователей)...")
143
- batch_size = 500
144
- for i in range(0, len(roles), batch_size):
145
- batch = roles[i:i+batch_size]
146
- try:
147
- url = f"{API_URL}/api/log/roles"
148
- res = requests.post(url, json={"roles": batch}, headers=headers, timeout=10)
149
- if res.status_code == 200:
150
- print(f"-> Обновлено пользователей: {i + len(batch)} / {len(roles)}", end="\r")
151
- else:
152
- print(f"\n-> Ошибка отправки пакета: {res.status_code}")
153
- except Exception as e:
154
- print(f"\n-> Ошибка сети при отправке пакета: {e}")
155
- print("\n-> Восстановление ролей успешно завершено!")
156
- else:
157
- print("\n-> Нет ролей для обновления.")
 
 
 
 
 
 
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("=========================================================")