Spaces:
Sleeping
Sleeping
| from fastapi import FastAPI | |
| from fastapi.responses import HTMLResponse, StreamingResponse | |
| import asyncio | |
| from playwright.async_api import async_playwright | |
| app = FastAPI() | |
| async def index(): | |
| return """ | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <title>FlashScore Monitor</title> | |
| <style> | |
| body { background:#0d0d0d; color:#eee; font-family:monospace; padding:10px; margin:0; } | |
| h2 { color:#ed1c24; } | |
| .btn { padding:10px 25px; font-size:15px; border:none; cursor:pointer; border-radius:4px; margin-right:8px; } | |
| .btn-start { background:#ed1c24; color:#fff; } | |
| .btn-stop { background:#333; color:#fff; } | |
| #status { margin:10px 0; color:#888; font-size:12px; } | |
| table { width:100%; border-collapse:collapse; margin-top:10px; } | |
| th { background:#1a1a1a; color:#ed1c24; padding:8px; text-align:left; font-size:11px; text-transform:uppercase; } | |
| </style> | |
| </head> | |
| <body> | |
| <h2>🎾 FlashScore Tennis Monitor</h2> | |
| <button class="btn btn-start" onclick="startStream()">▶ Старт</button> | |
| <button class="btn btn-stop" onclick="stopStream()">⏹ Стоп</button> | |
| <div id="status">Нажмите Старт</div> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th style="width:65px">Время</th> | |
| <th style="width:85px">Статус</th> | |
| <th style="width:210px">Игроки</th> | |
| <th>Счёт</th> | |
| </tr> | |
| </thead> | |
| <tbody id="log-body"></tbody> | |
| </table> | |
| <script> | |
| let controller = null; | |
| async function startStream() { | |
| if (controller) controller.abort(); | |
| controller = new AbortController(); | |
| document.getElementById('status').textContent = '⏳ Подключаюсь...'; | |
| document.getElementById('log-body').innerHTML = ''; | |
| try { | |
| const response = await fetch('/stream', { signal: controller.signal }); | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| document.getElementById('status').textContent = '🟢 Мониторинг идёт...'; | |
| let buffer = ''; | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, { stream: true }); | |
| const lines = buffer.split('\n'); | |
| buffer = lines.pop(); | |
| for (const line of lines) { | |
| if (line.startsWith('data: ')) { | |
| const html = line.slice(6); | |
| if (html) { | |
| const tbody = document.getElementById('log-body'); | |
| tbody.insertAdjacentHTML('afterbegin', html); | |
| if (tbody.children.length > 60) tbody.removeChild(tbody.lastChild); | |
| } | |
| } | |
| } | |
| } | |
| } catch(e) { | |
| if (e.name === 'AbortError') { | |
| document.getElementById('status').textContent = '⏹ Остановлено'; | |
| } | |
| } | |
| } | |
| function stopStream() { | |
| if (controller) { controller.abort(); controller = null; } | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| async def stream(): | |
| async def generate(): | |
| async with async_playwright() as p: | |
| browser = await p.chromium.launch( | |
| headless=True, | |
| args=["--no-sandbox", "--disable-dev-shm-usage"] | |
| ) | |
| page = await browser.new_page() | |
| # Вся логика парсинга — прямо как в Tampermonkey, на JS | |
| await page.add_init_script(""" | |
| window.__parsed_rows = []; | |
| const matchStorage = {}; | |
| const statusMap = { | |
| '46': '1-й сет', '47': '2-й сет', '48': '3-й сет', | |
| '49': '4-й сет', '50': '5-й сет', '38': 'Тай-брейк', | |
| '13': 'Перерыв', '1': 'Завершен' | |
| }; | |
| const dict = { | |
| 'WA': 'p1_pts', 'WB': 'p2_pts', 'WC': 'srv', | |
| 'BA': 'p1_s1', 'BB': 'p2_s1', 'CA': 'p1_s2', 'CB': 'p2_s2', | |
| 'DA': 'p1_s3', 'DB': 'p2_s3', 'AC': 'status_id', | |
| 'BD': 'val_d', 'BF': 'val_f' | |
| }; | |
| function parseToRow(raw) { | |
| let parts = raw.split('\\u00ac'); | |
| let update = {}; | |
| let matchId = ""; | |
| parts.forEach(part => { | |
| let pair = part.split('\\u00f7'); | |
| if (pair.length === 2) { | |
| let key = pair[0].replace(/[^A-Z]/g, ''); | |
| if (key === 'AA') matchId = pair[1]; | |
| if (dict[key]) update[dict[key]] = pair[1]; | |
| } | |
| }); | |
| if (!matchId || Object.keys(update).length === 0) return null; | |
| if (!matchStorage[matchId]) { | |
| matchStorage[matchId] = { | |
| p1_pts: '0', p2_pts: '0', srv: '0', | |
| status: null, | |
| p1_s1: '0', p2_s1: '0', p1_s2: '0', p2_s2: '0', | |
| p1_s3: '0', p2_s3: '0', | |
| }; | |
| } | |
| const ms = matchStorage[matchId]; | |
| if (update.status_id && statusMap[update.status_id]) { | |
| ms.status = statusMap[update.status_id]; | |
| } else if (update.val_f === '38' || update.val_d === '38') { | |
| ms.status = 'Тай-брейк'; | |
| } else { | |
| if (update.p1_s3 || update.p2_s3) ms.status = '3-й сет'; | |
| else if (update.p1_s2 || update.p2_s2) ms.status = '2-й сет'; | |
| else if (update.p1_s1 || update.p2_s1) ms.status = '1-й сет'; | |
| } | |
| Object.assign(ms, update); | |
| const displayStatus = ms.status || 'НЕ ЗНАЮ'; | |
| const t = new Date().toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'}); | |
| const srv1 = ms.srv === '1' ? '🎾 ' : ''; | |
| const srv2 = ms.srv === '2' ? '🎾 ' : ''; | |
| return `<tr style="border-bottom:1px solid #333;height:42px;background:${update.p1_pts||update.p2_pts?'#1a1a1a':'transparent'}"> | |
| <td style="color:#666;width:65px;font-size:10px;text-align:center">${t}</td> | |
| <td style="width:85px;color:${displayStatus==='НЕ ЗНАЮ'?'#555':'#ff4e4e'};font-size:10px;font-weight:bold;text-align:center;text-transform:uppercase">${displayStatus}</td> | |
| <td style="width:210px;padding-left:10px"> | |
| <div style="color:${ms.srv==='1'?'#fff':'#999'}">${srv1}${matchId}</div> | |
| </td> | |
| <td style="font-family:monospace;font-size:15px"> | |
| <div style="color:#ccc">${ms.p1_s1}|${ms.p1_s2}|${ms.p1_s3} <b style="color:#fff">${ms.p1_pts}</b></div> | |
| <div style="color:#ccc">${ms.p2_s1}|${ms.p2_s2}|${ms.p2_s3} <b style="color:#fff">${ms.p2_pts}</b></div> | |
| </td> | |
| </tr>`; | |
| } | |
| const OriginalWebSocket = window.WebSocket; | |
| window.WebSocket = function(url, protocols) { | |
| const socket = new OriginalWebSocket(url, protocols); | |
| if (url.includes('fsdatacentre.com')) { | |
| socket.binaryType = 'arraybuffer'; | |
| socket.addEventListener('message', async function(event) { | |
| let text; | |
| if (event.data instanceof ArrayBuffer) { | |
| text = new TextDecoder('utf-8').decode(new Uint8Array(event.data)); | |
| } else if (event.data instanceof Blob) { | |
| text = await event.data.text(); | |
| } else { | |
| text = event.data; | |
| } | |
| const row = parseToRow(text); | |
| if (row) window.__parsed_rows.push(row); | |
| }); | |
| } | |
| return socket; | |
| }; | |
| window.WebSocket.prototype = OriginalWebSocket.prototype; | |
| """) | |
| await page.goto("https://www.flashscore.com/tennis/", wait_until="networkidle") | |
| while True: | |
| await asyncio.sleep(1) | |
| rows = await page.evaluate(""" | |
| () => { | |
| const r = window.__parsed_rows || []; | |
| window.__parsed_rows = []; | |
| return r; | |
| } | |
| """) | |
| for row in rows: | |
| yield f"data: {row}\n\n" | |
| return StreamingResponse(generate(), media_type="text/event-stream") |