remove browser eye (operator request); relay-v2.42-guestwatch-pro
Browse files- Dockerfile +2 -4
- app.py +1 -28
- browser_eye.py +0 -302
- guest_probe.py +1 -68
- ingest.py +1 -1
- test_browser_eye.py +0 -86
- verify.sh +0 -1
Dockerfile
CHANGED
|
@@ -5,10 +5,8 @@ RUN ffmpeg -hide_banner -h protocol=rtmps 2>&1 | grep -q rtmp_enhanced_codecs
|
|
| 5 |
WORKDIR /app
|
| 6 |
COPY requirements.txt .
|
| 7 |
RUN pip install --no-cache-dir -U -r requirements.txt
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
COPY app.py relay.py youtube_api.py youtube_manager.py flv_bridge.py stream_monitor.py telemetry.py tiktok_auth.py ingest.py guest_probe.py tt_sign.py browser_eye.py index.html ./
|
| 11 |
-
COPY verify.sh verify_tokens.py probe_live.py test_watchdog.py test_lifecycle.py test_relay.py test_source_reconnect.py test_ingest_routes.py test_transition_ingest.py test_concurrency.py test_flv_bridge.py test_adaptive_format.py test_regressions.py test_completion.py test_channel_switch.py test_dashboard.js test_telemetry.py test_smart_mode.py test_guest_probe.py test_tt_sign.py test_browser_eye.py ./
|
| 12 |
|
| 13 |
# TikTok Live Monitor → Direct Relay → YouTube. No local video files.
|
| 14 |
ENV TZ=Asia/Baghdad \
|
|
|
|
| 5 |
WORKDIR /app
|
| 6 |
COPY requirements.txt .
|
| 7 |
RUN pip install --no-cache-dir -U -r requirements.txt
|
| 8 |
+
COPY app.py relay.py youtube_api.py youtube_manager.py flv_bridge.py stream_monitor.py telemetry.py tiktok_auth.py ingest.py guest_probe.py tt_sign.py index.html ./
|
| 9 |
+
COPY verify.sh verify_tokens.py probe_live.py test_watchdog.py test_lifecycle.py test_relay.py test_source_reconnect.py test_ingest_routes.py test_transition_ingest.py test_concurrency.py test_flv_bridge.py test_adaptive_format.py test_regressions.py test_completion.py test_channel_switch.py test_dashboard.js test_telemetry.py test_smart_mode.py test_guest_probe.py test_tt_sign.py ./
|
|
|
|
|
|
|
| 10 |
|
| 11 |
# TikTok Live Monitor → Direct Relay → YouTube. No local video files.
|
| 12 |
ENV TZ=Asia/Baghdad \
|
app.py
CHANGED
|
@@ -490,33 +490,6 @@ def _guest_pro_eye():
|
|
| 490 |
except Exception:
|
| 491 |
_guest_pro=None
|
| 492 |
return _guest_pro
|
| 493 |
-
_guest_browser=False
|
| 494 |
-
def _guest_browser_eye():
|
| 495 |
-
global _guest_browser
|
| 496 |
-
if _guest_browser not in (False,'building'):return _guest_browser
|
| 497 |
-
if os.getenv('GUEST_BROWSER','').strip().lower() not in ('1','true','yes','on'):
|
| 498 |
-
_guest_browser=None;return None
|
| 499 |
-
if _guest_browser=='building':return None
|
| 500 |
-
_guest_browser='building'
|
| 501 |
-
def _build():
|
| 502 |
-
global _guest_browser
|
| 503 |
-
try:
|
| 504 |
-
import browser_eye
|
| 505 |
-
if not browser_eye.ensure_installed(lambda m:log('system',m)):
|
| 506 |
-
log('system','[GUEST] browser eye unavailable (playwright install failed)')
|
| 507 |
-
_guest_browser=None;return
|
| 508 |
-
cookie=''
|
| 509 |
-
try:
|
| 510 |
-
us=_guest_users()
|
| 511 |
-
if us:cookie=tiktok_auth.headers(us[0]).get('Cookie','') or ''
|
| 512 |
-
except Exception:cookie=''
|
| 513 |
-
_guest_browser=browser_eye.BrowserEye(cookie_header=cookie)
|
| 514 |
-
log('system','[GUEST] browser eye ready (cookies=%s)'%('yes' if cookie else 'no'))
|
| 515 |
-
except Exception as e:
|
| 516 |
-
log('system','[GUEST] browser eye build failed: %s %s'%(type(e).__name__,str(e)[:120]))
|
| 517 |
-
_guest_browser=None
|
| 518 |
-
import threading;threading.Thread(target=_build,name='browser-eye-build',daemon=True).start()
|
| 519 |
-
return None
|
| 520 |
_guest_diag={'n':0}
|
| 521 |
def _guest_cycle():
|
| 522 |
global _guest_watcher
|
|
@@ -528,7 +501,7 @@ def _guest_cycle():
|
|
| 528 |
import guest_probe as _gp
|
| 529 |
_hosts=_guest_hosts()
|
| 530 |
_scan=(lambda uids:_gp.scan_host_rooms(uids,_hosts)) if _hosts else None
|
| 531 |
-
written=_guest_watcher.cycle(users,probe=lambda u:_gp.probe_user(u,
|
| 532 |
except Exception as e:
|
| 533 |
_guest_diag['n']+=1
|
| 534 |
if _guest_diag['n']<=5 or _guest_diag['n']%30==1:
|
|
|
|
| 490 |
except Exception:
|
| 491 |
_guest_pro=None
|
| 492 |
return _guest_pro
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
_guest_diag={'n':0}
|
| 494 |
def _guest_cycle():
|
| 495 |
global _guest_watcher
|
|
|
|
| 501 |
import guest_probe as _gp
|
| 502 |
_hosts=_guest_hosts()
|
| 503 |
_scan=(lambda uids:_gp.scan_host_rooms(uids,_hosts)) if _hosts else None
|
| 504 |
+
written=_guest_watcher.cycle(users,probe=lambda u:_gp.probe_user(u,pro=_guest_pro_eye(),**_guest_fetchers(u)),host_scan=_scan)
|
| 505 |
except Exception as e:
|
| 506 |
_guest_diag['n']+=1
|
| 507 |
if _guest_diag['n']<=5 or _guest_diag['n']%30==1:
|
browser_eye.py
DELETED
|
@@ -1,302 +0,0 @@
|
|
| 1 |
-
"""العين المتصفحية: متصفح Chromium حقيقي (Playwright) ينفّذ طلبات تيك توك من
|
| 2 |
-
داخل صفحة tiktok.com الأصلية، فيوقّعها webmssdk تلقائيًا (X-Bogus/X-Gnarly/
|
| 3 |
-
msToken حقيقية) وتُنفَّذ باتصال متصفح كامل — نفس نمط خوادم التوقيع المجانية
|
| 4 |
-
المنشورة (carcabot/tiktok-signature وأمثالها).
|
| 5 |
-
|
| 6 |
-
لماذا؟ قياس 2026-10-17: طلبات /api/* تُفرَّغ (200 بطول 0) لكل عميل غير
|
| 7 |
-
متصفحي مهما كان التوقيع سليمًا، و webcast/room/info_by_user المنفّذة داخل
|
| 8 |
-
متصفح حقيقي هي أقوى سياق مجاني متاح. مع كوكيز جلسة المستخدم نفسه (اختياري)
|
| 9 |
-
تصبح العين «مُصادقة + موقّعة + متصفحية» معًا.
|
| 10 |
-
|
| 11 |
-
البنية: خيط خلفي واحد يملك حلقة asyncio ومتصفحًا كسول الإقلاع يُعاد تدويره
|
| 12 |
-
كل recycle_seconds. كل resolve ≤ ~40 ثانية؛ أي فشل يرفع استثناء والمستدعي
|
| 13 |
-
(guest_probe) يسجّله ويكمل بالقنوات الأخرى. بلا أي حذف/تدوير لأي سجل.
|
| 14 |
-
"""
|
| 15 |
-
from __future__ import annotations
|
| 16 |
-
|
| 17 |
-
import asyncio
|
| 18 |
-
import json
|
| 19 |
-
import re
|
| 20 |
-
import threading
|
| 21 |
-
import time
|
| 22 |
-
|
| 23 |
-
UA = ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
|
| 24 |
-
'(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36')
|
| 25 |
-
HOME_URL = 'https://www.tiktok.com/'
|
| 26 |
-
LIVE_URL = 'https://www.tiktok.com/@%s/live'
|
| 27 |
-
BY_USER_URL = ('https://webcast.tiktok.com/webcast/room/info_by_user/'
|
| 28 |
-
'?aid=1988&unique_id=%s')
|
| 29 |
-
DETAIL_URL = 'https://www.tiktok.com/api/user/detail/?uniqueId=%s'
|
| 30 |
-
_ROOM_RE = re.compile(r'"roomId"\s*:\s*"(\d{10,})"')
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
def parse_cookie_header(header):
|
| 34 |
-
"""'a=1; b=2' -> قائمة كوكيز Playwright لنطاق تيك توك."""
|
| 35 |
-
out = []
|
| 36 |
-
for part in str(header or '').split(';'):
|
| 37 |
-
part = part.strip()
|
| 38 |
-
if not part or '=' not in part:
|
| 39 |
-
continue
|
| 40 |
-
name, value = part.split('=', 1)
|
| 41 |
-
name = name.strip()
|
| 42 |
-
if not name:
|
| 43 |
-
continue
|
| 44 |
-
out.append({'name': name, 'value': value.strip(),
|
| 45 |
-
'domain': '.tiktok.com', 'path': '/'})
|
| 46 |
-
return out
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
def _json_obj_at(text, start):
|
| 50 |
-
depth = 0
|
| 51 |
-
in_str = False
|
| 52 |
-
esc = False
|
| 53 |
-
for i in range(start, len(text)):
|
| 54 |
-
ch = text[i]
|
| 55 |
-
if in_str:
|
| 56 |
-
if esc:
|
| 57 |
-
esc = False
|
| 58 |
-
elif ch == '\\':
|
| 59 |
-
esc = True
|
| 60 |
-
elif ch == '"':
|
| 61 |
-
in_str = False
|
| 62 |
-
elif ch == '"':
|
| 63 |
-
in_str = True
|
| 64 |
-
elif ch == '{':
|
| 65 |
-
depth += 1
|
| 66 |
-
elif ch == '}':
|
| 67 |
-
depth -= 1
|
| 68 |
-
if depth == 0:
|
| 69 |
-
try:
|
| 70 |
-
return json.loads(text[start:i + 1])
|
| 71 |
-
except ValueError:
|
| 72 |
-
return None
|
| 73 |
-
return None
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
def parse_by_user_body(text):
|
| 77 |
-
"""رد info_by_user -> {owner, room, status, message}."""
|
| 78 |
-
out = {}
|
| 79 |
-
try:
|
| 80 |
-
j = json.loads(text or '')
|
| 81 |
-
except ValueError:
|
| 82 |
-
return out
|
| 83 |
-
data = (j or {}).get('data') or {}
|
| 84 |
-
if not isinstance(data, dict):
|
| 85 |
-
return out
|
| 86 |
-
if data.get('message'):
|
| 87 |
-
out['message'] = str(data.get('message'))[:80]
|
| 88 |
-
owner = data.get('owner') or {}
|
| 89 |
-
handle = str(owner.get('display_id') or owner.get('uniqueId') or owner.get('unique_id') or '')
|
| 90 |
-
if handle:
|
| 91 |
-
out['owner'] = handle
|
| 92 |
-
for k in ('id_str', 'room_id', 'roomId', 'id'):
|
| 93 |
-
v = data.get(k)
|
| 94 |
-
if isinstance(v, int) and v > 10 ** 15:
|
| 95 |
-
out['room'] = str(v)
|
| 96 |
-
break
|
| 97 |
-
if isinstance(v, str) and v.isdigit() and len(v) >= 15:
|
| 98 |
-
out['room'] = v
|
| 99 |
-
break
|
| 100 |
-
if data.get('status') is not None:
|
| 101 |
-
out['status'] = data.get('status')
|
| 102 |
-
return out
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
def parse_detail_body(text):
|
| 106 |
-
"""رد user/detail -> أدلة الحقول الحيّة فقط (بلا روابط)."""
|
| 107 |
-
out = {'bytes': len(text or '')}
|
| 108 |
-
if not text:
|
| 109 |
-
return out
|
| 110 |
-
try:
|
| 111 |
-
j = json.loads(text)
|
| 112 |
-
except ValueError:
|
| 113 |
-
out['unparsed'] = True
|
| 114 |
-
return out
|
| 115 |
-
if isinstance(j, dict):
|
| 116 |
-
usr = ((j.get('userInfo') or {}).get('user')) or {}
|
| 117 |
-
fields = {}
|
| 118 |
-
for k, v in usr.items():
|
| 119 |
-
if 'live' in str(k).lower() or 'room' in str(k).lower():
|
| 120 |
-
fields[k] = v if not isinstance(v, (dict, list)) else '<obj>'
|
| 121 |
-
if fields:
|
| 122 |
-
out['live_fields'] = fields
|
| 123 |
-
ids = sorted(set(_ROOM_RE.findall(text)))
|
| 124 |
-
if ids:
|
| 125 |
-
out['room_ids'] = ids
|
| 126 |
-
m = re.search(r'"liveRoom"\s*:\s*\{', text)
|
| 127 |
-
if m:
|
| 128 |
-
node = _json_obj_at(text, m.end() - 1)
|
| 129 |
-
if isinstance(node, dict) and str(node.get('status')) == '2':
|
| 130 |
-
for k in ('id_str', 'room_id', 'roomId', 'id'):
|
| 131 |
-
v = node.get(k)
|
| 132 |
-
if isinstance(v, int) and v > 10 ** 15:
|
| 133 |
-
out['guest_room'] = str(v)
|
| 134 |
-
break
|
| 135 |
-
if isinstance(v, str) and v.isdigit() and len(v) >= 15:
|
| 136 |
-
out['guest_room'] = v
|
| 137 |
-
break
|
| 138 |
-
ow = node.get('owner') or {}
|
| 139 |
-
if isinstance(ow, dict):
|
| 140 |
-
handle = str(ow.get('display_id') or ow.get('uniqueId') or '')
|
| 141 |
-
if handle:
|
| 142 |
-
out['guest_owner'] = handle
|
| 143 |
-
return out
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
def ensure_installed(log=lambda *a: None):
|
| 147 |
-
"""ثبّت Playwright + Chromium عند غيابهما (يُستدعى من خيط الإقلاع فقط)."""
|
| 148 |
-
try:
|
| 149 |
-
import playwright # noqa: F401
|
| 150 |
-
from playwright.sync_api import sync_playwright
|
| 151 |
-
with sync_playwright() as pw:
|
| 152 |
-
exe = pw.chromium.executable_path
|
| 153 |
-
import os
|
| 154 |
-
if exe and os.path.exists(exe):
|
| 155 |
-
return True
|
| 156 |
-
except Exception:
|
| 157 |
-
pass
|
| 158 |
-
import subprocess
|
| 159 |
-
import sys
|
| 160 |
-
log('[BROWSER] installing playwright chromium (one-time per container)')
|
| 161 |
-
subprocess.run([sys.executable, '-m', 'pip', 'install', '-q', 'playwright'],
|
| 162 |
-
check=False, timeout=600)
|
| 163 |
-
subprocess.run([sys.executable, '-m', 'playwright', 'install', 'chromium-headless-shell'],
|
| 164 |
-
check=False, timeout=900)
|
| 165 |
-
try:
|
| 166 |
-
import os
|
| 167 |
-
from playwright.sync_api import sync_playwright
|
| 168 |
-
with sync_playwright() as pw:
|
| 169 |
-
return bool(pw.chromium.executable_path and os.path.exists(pw.chromium.executable_path))
|
| 170 |
-
except Exception:
|
| 171 |
-
return False
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
class BrowserEye:
|
| 175 |
-
"""واجهة متزامنة فوق متصفح خلفي. resolve(user) يعيد dict بالأدلة."""
|
| 176 |
-
|
| 177 |
-
def __init__(self, cookie_header='', recycle_seconds=1500.0):
|
| 178 |
-
self._cookies = parse_cookie_header(cookie_header)
|
| 179 |
-
self._recycle = float(recycle_seconds)
|
| 180 |
-
self._loop = None
|
| 181 |
-
self._thread = None
|
| 182 |
-
self._browser = None
|
| 183 |
-
self._context = None
|
| 184 |
-
self._born = 0.0
|
| 185 |
-
self._started = False
|
| 186 |
-
self._lock = threading.Lock()
|
| 187 |
-
|
| 188 |
-
# ---------- البنية الخلفية ----------
|
| 189 |
-
def _ensure_thread(self):
|
| 190 |
-
if self._thread and self._thread.is_alive():
|
| 191 |
-
return
|
| 192 |
-
ready = threading.Event()
|
| 193 |
-
|
| 194 |
-
def run():
|
| 195 |
-
self._loop = asyncio.new_event_loop()
|
| 196 |
-
asyncio.set_event_loop(self._loop)
|
| 197 |
-
ready.set()
|
| 198 |
-
self._loop.run_forever()
|
| 199 |
-
|
| 200 |
-
self._thread = threading.Thread(target=run, name='browser-eye', daemon=True)
|
| 201 |
-
self._thread.start()
|
| 202 |
-
ready.wait(10)
|
| 203 |
-
|
| 204 |
-
def _run(self, coro, timeout):
|
| 205 |
-
self._ensure_thread()
|
| 206 |
-
fut = asyncio.run_coroutine_threadsafe(coro, self._loop)
|
| 207 |
-
return fut.result(timeout=timeout)
|
| 208 |
-
|
| 209 |
-
async def _get_context(self):
|
| 210 |
-
from playwright.async_api import async_playwright
|
| 211 |
-
now = time.time()
|
| 212 |
-
if (self._context is not None and self._browser is not None
|
| 213 |
-
and now - self._born < self._recycle):
|
| 214 |
-
return self._context
|
| 215 |
-
await self._close_async()
|
| 216 |
-
self._pw = await async_playwright().start()
|
| 217 |
-
self._browser = await self._pw.chromium.launch(args=['--no-sandbox'])
|
| 218 |
-
self._context = await self._browser.new_context(user_agent=UA, locale='en-US')
|
| 219 |
-
if self._cookies:
|
| 220 |
-
try:
|
| 221 |
-
await self._context.add_cookies(self._cookies)
|
| 222 |
-
except Exception:
|
| 223 |
-
pass
|
| 224 |
-
page = await self._context.new_page()
|
| 225 |
-
try:
|
| 226 |
-
await page.goto(HOME_URL, wait_until='domcontentloaded', timeout=60000)
|
| 227 |
-
await page.wait_for_timeout(5000)
|
| 228 |
-
finally:
|
| 229 |
-
await page.close()
|
| 230 |
-
self._born = now
|
| 231 |
-
return self._context
|
| 232 |
-
|
| 233 |
-
async def _close_async(self):
|
| 234 |
-
for attr in ('_context', '_browser'):
|
| 235 |
-
obj = getattr(self, attr, None)
|
| 236 |
-
if obj is not None:
|
| 237 |
-
try:
|
| 238 |
-
await obj.close()
|
| 239 |
-
except Exception:
|
| 240 |
-
pass
|
| 241 |
-
setattr(self, attr, None)
|
| 242 |
-
pw = getattr(self, '_pw', None)
|
| 243 |
-
if pw is not None:
|
| 244 |
-
try:
|
| 245 |
-
await pw.stop()
|
| 246 |
-
except Exception:
|
| 247 |
-
pass
|
| 248 |
-
self._pw = None
|
| 249 |
-
|
| 250 |
-
# ---------- القياس ----------
|
| 251 |
-
async def _resolve_async(self, user):
|
| 252 |
-
import guest_probe
|
| 253 |
-
ctx = await self._get_context()
|
| 254 |
-
page = await ctx.new_page()
|
| 255 |
-
captured = {}
|
| 256 |
-
|
| 257 |
-
def on_req(req):
|
| 258 |
-
u = req.url
|
| 259 |
-
if 'user/detail' in u:
|
| 260 |
-
captured['detail'] = u
|
| 261 |
-
elif 'info_by_user' in u:
|
| 262 |
-
captured['by_user'] = u
|
| 263 |
-
|
| 264 |
-
page.on('request', on_req)
|
| 265 |
-
out = {}
|
| 266 |
-
try:
|
| 267 |
-
await page.goto(LIVE_URL % user, wait_until='domcontentloaded', timeout=60000)
|
| 268 |
-
await page.wait_for_timeout(7000)
|
| 269 |
-
html = await page.content()
|
| 270 |
-
out['ssr'] = guest_probe.parse_live_page(html)
|
| 271 |
-
out['final_url'] = page.url[:120]
|
| 272 |
-
await page.evaluate(
|
| 273 |
-
"""(u) => {
|
| 274 |
-
fetch('/api/user/detail/?uniqueId=' + u).catch(() => {});
|
| 275 |
-
fetch('https://webcast.tiktok.com/webcast/room/info_by_user/?aid=1988&unique_id=' + u,
|
| 276 |
-
{credentials: 'include'}).catch(() => {});
|
| 277 |
-
return 1;
|
| 278 |
-
}""", user)
|
| 279 |
-
await page.wait_for_timeout(4000)
|
| 280 |
-
finally:
|
| 281 |
-
page.remove_listener('request', on_req)
|
| 282 |
-
await page.close()
|
| 283 |
-
for name, parser in (('by_user', parse_by_user_body), ('detail', parse_detail_body)):
|
| 284 |
-
url = captured.get(name)
|
| 285 |
-
if not url:
|
| 286 |
-
continue
|
| 287 |
-
p2 = await ctx.new_page()
|
| 288 |
-
try:
|
| 289 |
-
resp = await p2.goto(url, wait_until='domcontentloaded', timeout=45000)
|
| 290 |
-
body = await p2.inner_text('body')
|
| 291 |
-
parsed = parser(body)
|
| 292 |
-
parsed['http'] = resp.status if resp else 0
|
| 293 |
-
out[name] = parsed
|
| 294 |
-
except Exception as e:
|
| 295 |
-
out[name] = {'err': type(e).__name__}
|
| 296 |
-
finally:
|
| 297 |
-
await p2.close()
|
| 298 |
-
return out
|
| 299 |
-
|
| 300 |
-
def resolve(self, user, timeout=120.0):
|
| 301 |
-
with self._lock:
|
| 302 |
-
return self._run(self._resolve_async(user), timeout)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
guest_probe.py
CHANGED
|
@@ -546,70 +546,8 @@ def _euler_view(user, rec, pro, get_json, req):
|
|
| 546 |
return None
|
| 547 |
|
| 548 |
|
| 549 |
-
def _browser_view(user, rec, browser, get_json, req):
|
| 550 |
-
"""قرار العين المتصفحية: Chromium حقيقي + (اختياريًا) جلسة المستخدم نفسه.
|
| 551 |
-
نفس قواعد _pro_view لكن بمصادر browser_*، والأدلة تحت rec['browser']."""
|
| 552 |
-
ev = browser.resolve(user) or {}
|
| 553 |
-
bv = {}
|
| 554 |
-
promoted = None
|
| 555 |
-
|
| 556 |
-
def _decide(owner, owner_id, room, status, source):
|
| 557 |
-
nonlocal promoted
|
| 558 |
-
if promoted is not None or not owner or not room or str(status) != '2':
|
| 559 |
-
return
|
| 560 |
-
same_person = owner.lower() == str(user).lower() or (
|
| 561 |
-
owner_id and rec.get('uid') and str(owner_id) == str(rec.get('uid')))
|
| 562 |
-
if same_person:
|
| 563 |
-
promoted = ('hosting', user, room, source)
|
| 564 |
-
else:
|
| 565 |
-
promoted = ('guest', owner, room, source)
|
| 566 |
-
|
| 567 |
-
ssr = ev.get('ssr') or {}
|
| 568 |
-
keep = {k: ssr.get(k) for k in ('owner', 'owner_id', 'room', 'status')
|
| 569 |
-
if ssr.get(k) not in (None, '')}
|
| 570 |
-
if keep:
|
| 571 |
-
bv['ssr'] = keep
|
| 572 |
-
_decide(str(ssr.get('owner') or ''), str(ssr.get('owner_id') or ''),
|
| 573 |
-
str(ssr.get('room') or ''), ssr.get('status'), 'browser_ssr')
|
| 574 |
-
bu = ev.get('by_user') or {}
|
| 575 |
-
keep = {k: bu.get(k) for k in ('owner', 'room', 'status', 'message', 'http')
|
| 576 |
-
if bu.get(k) not in (None, '')}
|
| 577 |
-
if keep:
|
| 578 |
-
bv['by_user'] = keep
|
| 579 |
-
_decide(str(bu.get('owner') or ''), '', str(bu.get('room') or ''),
|
| 580 |
-
bu.get('status'), 'browser_by_user')
|
| 581 |
-
dt = ev.get('detail') or {}
|
| 582 |
-
if dt:
|
| 583 |
-
bv['detail'] = dt
|
| 584 |
-
if promoted is None:
|
| 585 |
-
groom = str(dt.get('guest_room') or '')
|
| 586 |
-
if groom and groom != str(rec.get('own_room') or ''):
|
| 587 |
-
promoted = ('guest', str(dt.get('guest_owner') or ''), groom, 'browser_detail')
|
| 588 |
-
rec['browser'] = bv
|
| 589 |
-
if promoted:
|
| 590 |
-
kind, host_user, host_room, src = promoted
|
| 591 |
-
rec.update(state=kind, host_user=host_user, host_room=host_room,
|
| 592 |
-
host_status=2, evidence_source=src)
|
| 593 |
-
if kind == 'guest':
|
| 594 |
-
try:
|
| 595 |
-
if get_json is not None:
|
| 596 |
-
info = (get_json(INFO_API, {'aid': 1988, 'room_id': host_room}) or {}).get('data') or {}
|
| 597 |
-
else:
|
| 598 |
-
rr = req().get(INFO_API, params={'aid': 1988, 'room_id': host_room},
|
| 599 |
-
impersonate='chrome',
|
| 600 |
-
headers={'Referer': 'https://www.tiktok.com/'}, timeout=20)
|
| 601 |
-
rr.raise_for_status()
|
| 602 |
-
info = (rr.json() or {}).get('data') or {}
|
| 603 |
-
rec['watched_present'] = _watched_present(info, rec.get('uid', ''), user)
|
| 604 |
-
rec['evidence'] = _participants_view(info)
|
| 605 |
-
rec['host_title'] = str(info.get('title') or '')[:80]
|
| 606 |
-
except Exception as e:
|
| 607 |
-
rec['err'] = (rec['err'] + ' ' if rec['err'] else '') + 'browser_confirm:' + type(e).__name__
|
| 608 |
-
return promoted
|
| 609 |
-
|
| 610 |
-
|
| 611 |
def probe_user(user, client=None, get_json=None, get_page=None,
|
| 612 |
-
get_json_authed=None, get_page_authed=None, pro=None
|
| 613 |
"""قياس كامل لمستخدم واحد الآن. يعيد سجلًّا جاهزًا للإلحاق (لا يكتب بنفسه)."""
|
| 614 |
req = client
|
| 615 |
def _req():
|
|
@@ -687,11 +625,6 @@ def probe_user(user, client=None, get_json=None, get_page=None,
|
|
| 687 |
rec['state'] = classify(watched=user, own_status=rec['own_status'],
|
| 688 |
page_user=rec['page_user'], page_room=rec['page_room'],
|
| 689 |
host_status=rec['host_status'])
|
| 690 |
-
if rec['state'] in ('offline', 'unknown') and browser is not None:
|
| 691 |
-
try:
|
| 692 |
-
_browser_view(user, rec, browser, get_json, _req)
|
| 693 |
-
except Exception as e:
|
| 694 |
-
rec['browser'] = {'err': type(e).__name__}
|
| 695 |
if rec['state'] in ('offline', 'unknown') and pro is not None:
|
| 696 |
try:
|
| 697 |
_pro_view(user, rec, pro, get_json, _req)
|
|
|
|
| 546 |
return None
|
| 547 |
|
| 548 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
def probe_user(user, client=None, get_json=None, get_page=None,
|
| 550 |
+
get_json_authed=None, get_page_authed=None, pro=None):
|
| 551 |
"""قياس كامل لمستخدم واحد الآن. يعيد سجلًّا جاهزًا للإلحاق (لا يكتب بنفسه)."""
|
| 552 |
req = client
|
| 553 |
def _req():
|
|
|
|
| 625 |
rec['state'] = classify(watched=user, own_status=rec['own_status'],
|
| 626 |
page_user=rec['page_user'], page_room=rec['page_room'],
|
| 627 |
host_status=rec['host_status'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 628 |
if rec['state'] in ('offline', 'unknown') and pro is not None:
|
| 629 |
try:
|
| 630 |
_pro_view(user, rec, pro, get_json, _req)
|
ingest.py
CHANGED
|
@@ -6,7 +6,7 @@ import os, threading, time
|
|
| 6 |
from datetime import datetime
|
| 7 |
from zoneinfo import ZoneInfo
|
| 8 |
|
| 9 |
-
APP_VERSION = 'relay-v2.
|
| 10 |
|
| 11 |
|
| 12 |
def env_rtmp_destination():
|
|
|
|
| 6 |
from datetime import datetime
|
| 7 |
from zoneinfo import ZoneInfo
|
| 8 |
|
| 9 |
+
APP_VERSION = 'relay-v2.42-guestwatch-pro'
|
| 10 |
|
| 11 |
|
| 12 |
def env_rtmp_destination():
|
test_browser_eye.py
DELETED
|
@@ -1,86 +0,0 @@
|
|
| 1 |
-
"""اختبارات نقية للعين المتصفحية: تحليل الكوكيز والردود دون متصفح فعلي."""
|
| 2 |
-
import json
|
| 3 |
-
import unittest
|
| 4 |
-
|
| 5 |
-
import browser_eye as be
|
| 6 |
-
import guest_probe as gp
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
class CookieParseTests(unittest.TestCase):
|
| 10 |
-
def test_pairs(self):
|
| 11 |
-
out = be.parse_cookie_header('sessionid=abc; ttwid=xyz; ; msToken = tok ')
|
| 12 |
-
names = [c['name'] for c in out]
|
| 13 |
-
self.assertEqual(names, ['sessionid', 'ttwid', 'msToken'])
|
| 14 |
-
self.assertTrue(all(c['domain'] == '.tiktok.com' for c in out))
|
| 15 |
-
|
| 16 |
-
def test_empty(self):
|
| 17 |
-
self.assertEqual(be.parse_cookie_header(''), [])
|
| 18 |
-
self.assertEqual(be.parse_cookie_header(None), [])
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
class BodyParseTests(unittest.TestCase):
|
| 22 |
-
def test_by_user_finished(self):
|
| 23 |
-
out = be.parse_by_user_body('{"data":{"message":"room has finished"},"status_code":30003}')
|
| 24 |
-
self.assertEqual(out, {'message': 'room has finished'})
|
| 25 |
-
|
| 26 |
-
def test_by_user_live_host(self):
|
| 27 |
-
body = json.dumps({'data': {'id_str': '777666555444333222', 'status': 2,
|
| 28 |
-
'owner': {'display_id': 'bob'}}})
|
| 29 |
-
out = be.parse_by_user_body(body)
|
| 30 |
-
self.assertEqual(out['owner'], 'bob')
|
| 31 |
-
self.assertEqual(out['room'], '777666555444333222')
|
| 32 |
-
self.assertEqual(out['status'], 2)
|
| 33 |
-
|
| 34 |
-
def test_detail_empty(self):
|
| 35 |
-
self.assertEqual(be.parse_detail_body(''), {'bytes': 0})
|
| 36 |
-
|
| 37 |
-
def test_detail_guest_room(self):
|
| 38 |
-
body = json.dumps({'userInfo': {'user': {'uniqueId': 'alice', 'roomId': ''}},
|
| 39 |
-
'liveRoom': {'status': 2, 'id_str': '777666555444333222',
|
| 40 |
-
'owner': {'display_id': 'bob'}}})
|
| 41 |
-
out = be.parse_detail_body(body)
|
| 42 |
-
self.assertEqual(out['guest_room'], '777666555444333222')
|
| 43 |
-
self.assertEqual(out['guest_owner'], 'bob')
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
class _FakeBrowser:
|
| 47 |
-
def __init__(self, result):
|
| 48 |
-
self._result = result
|
| 49 |
-
|
| 50 |
-
def resolve(self, user):
|
| 51 |
-
return dict(self._result)
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
class BrowserViewTests(unittest.TestCase):
|
| 55 |
-
def _offline_json(self, url, params):
|
| 56 |
-
if 'user/room' in url:
|
| 57 |
-
return {'data': {'liveRoom': {'status': 4, 'roomId': '555'},
|
| 58 |
-
'user': {'id': '4242', 'roomId': '555', 'nickname': 'Alice'}}}
|
| 59 |
-
return {'data': {'status': 2, 'title': 'Host LIVE', 'owner': {'display_id': 'bob'},
|
| 60 |
-
'link_mic': {'linked_user_list': []},
|
| 61 |
-
'linker_map': {'4242': {'user_id': '4242'}}}}
|
| 62 |
-
|
| 63 |
-
def _page_self(self, url):
|
| 64 |
-
return 'https://www.tiktok.com/@alice/live', '"roomId":"555"'
|
| 65 |
-
|
| 66 |
-
def test_browser_by_user_guest(self):
|
| 67 |
-
br = _FakeBrowser({'by_user': {'owner': 'bob', 'room': '777666555444333222', 'status': 2, 'http': 200}})
|
| 68 |
-
rec = gp.probe_user('alice', get_json=self._offline_json,
|
| 69 |
-
get_page=self._page_self, browser=br)
|
| 70 |
-
self.assertEqual(rec['state'], 'guest')
|
| 71 |
-
self.assertEqual(rec['host_user'], 'bob')
|
| 72 |
-
self.assertEqual(rec['evidence_source'], 'browser_by_user')
|
| 73 |
-
self.assertIs(rec['watched_present'], True)
|
| 74 |
-
|
| 75 |
-
def test_browser_ssr_offline_stays_offline(self):
|
| 76 |
-
br = _FakeBrowser({'ssr': {'owner': 'alice', 'owner_id': '4242', 'room': '555', 'status': 4},
|
| 77 |
-
'by_user': {'message': 'room has finished', 'http': 200}})
|
| 78 |
-
rec = gp.probe_user('alice', get_json=self._offline_json,
|
| 79 |
-
get_page=self._page_self, browser=br)
|
| 80 |
-
self.assertEqual(rec['state'], 'offline')
|
| 81 |
-
self.assertEqual(rec['browser']['by_user']['message'], 'room has finished')
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
if __name__ == '__main__':
|
| 85 |
-
import sys
|
| 86 |
-
unittest.main(argv=[sys.argv[0]])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
verify.sh
CHANGED
|
@@ -36,7 +36,6 @@ python3 test_telemetry.py "$ROOT"
|
|
| 36 |
python3 test_smart_mode.py "$ROOT"
|
| 37 |
python3 test_guest_probe.py "$ROOT"
|
| 38 |
python3 test_tt_sign.py "$ROOT"
|
| 39 |
-
python3 test_browser_eye.py "$ROOT"
|
| 40 |
# Optional heavy tests when ffmpeg exists
|
| 41 |
if command -v ffmpeg >/dev/null 2>&1; then
|
| 42 |
python3 test_watchdog.py "$ROOT"
|
|
|
|
| 36 |
python3 test_smart_mode.py "$ROOT"
|
| 37 |
python3 test_guest_probe.py "$ROOT"
|
| 38 |
python3 test_tt_sign.py "$ROOT"
|
|
|
|
| 39 |
# Optional heavy tests when ffmpeg exists
|
| 40 |
if command -v ffmpeg >/dev/null 2>&1; then
|
| 41 |
python3 test_watchdog.py "$ROOT"
|