selfserv/shellfish
0
1import re2import time3import json4import asyncio5import urllib36from contextlib import asynccontextmanager7from fastapi import FastAPI, Request8from fastapi.responses import HTMLResponse, StreamingResponse9from playwright.async_api import async_playwright10 11# Standard requests for KB12import requests13# curl_cffi for TQ14from curl_cffi import requests as cffi_requests15from bs4 import BeautifulSoup16 17# Suppress insecure request warnings for KB18urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)19 20# --- GLOBAL VARIABLES ---21playwright_instance = None22browser = None23 24@asynccontextmanager25async def lifespan(app: FastAPI):26 global playwright_instance, browser27 playwright_instance = await async_playwright().start()28 29 # Launch browser for KL in background30 browser = await playwright_instance.chromium.launch(31 headless=True,32 args=[33 "--no-sandbox",34 "--disable-setuid-sandbox",35 "--disable-dev-shm-usage",36 "--disable-blink-features=AutomationControlled"37 ]38 )39 yield40 41 # Clean up on shutdown42 await browser.close()43 await playwright_instance.stop()44 45app = FastAPI(lifespan=lifespan)46 47# --- FORMATTING HELPER ---48def format_inr(number):49 """Formats a number into the Indian numbering system pattern (e.g., 00,00,000)"""50 try:51 if number is None or number == "N/A":52 return "N/A"53 54 # Round the float and convert to int to drop decimals safely55 num_str = str(int(round(float(number))))56 57 if len(num_str) <= 3:58 return num_str59 60 last_three = num_str[-3:]61 rest = num_str[:-3]62 chunks = []63 64 while rest:65 chunks.append(rest[-2:])66 rest = rest[:-2]67 68 return ",".join(reversed(chunks)) + "," + last_three69 except Exception:70 return str(number)71 72# --- SCRAPING LOGIC ---73def fetch_kb():74 start_time = time.time()75 TARGET_URL = "https://bcast.kartikeybullion.com:7768/VOTSBroadcastStreaming/Services/xml/GetLiveRateByTemplateID/kartikeyallcity"76 77 try:78 current_timestamp = int(time.time() * 1000)79 url = f"{TARGET_URL}?_={current_timestamp}"80 headers = {'User-Agent': 'Mozilla/5.0'}81 82 response = requests.get(url, headers=headers, timeout=10, verify=False)83 response.raise_for_status()84 85 kb1 = "N/A"86 kb2 = "N/A"87 88 for line in response.text.splitlines():89 if "5391" in line and "GOLD 995" in line:90 numbers = re.findall(r'\b\d{5,7}\b', line)91 if numbers:92 kb1 = format_inr(int(numbers[0]))93 elif "4998" in line and "GOLD 995" in line:94 numbers = re.findall(r'\b\d{5,7}\b', line)95 if numbers:96 kb2 = format_inr(int(numbers[0]))97 98 if kb1 != "N/A" and kb2 != "N/A":99 break100 101 elapsed = round(time.time() - start_time, 2)102 103 return {104 "source": "KB",105 "kb1": kb1,106 "kb2": kb2,107 "elapsed_sec": elapsed,108 "status": "success"109 }110 except Exception as e:111 elapsed = round(time.time() - start_time, 2)112 return {113 "source": "KB",114 "error": str(e),115 "elapsed_sec": elapsed,116 "status": "error"117 }118 119async def fetch_kl():120 start_time = time.time()121 URL = "https://store.kalyanjewellers.net/gold-rate/Samastipur"122 global browser123 124 context = await browser.new_context(125 user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"126 )127 page = await context.new_page()128 129 async def route_intercept(route):130 if route.request.resource_type in ["image", "stylesheet", "font", "media"]:131 await route.abort()132 else:133 await route.continue_()134 135 await page.route("**/*", route_intercept)136 137 try:138 await page.goto(URL, wait_until="domcontentloaded", timeout=45000)139 await page.wait_for_selector("text=gold", timeout=15000)140 body_text = await page.inner_text("body")141 142 match_24k = re.search(r'₹\s*([\d,]+)[^₹]*?24k gold', body_text, re.IGNORECASE | re.DOTALL)143 kl24 = format_inr(match_24k.group(1).replace(',', '').strip()) if match_24k else "N/A"144 145 match_22k = re.search(r'₹\s*([\d,]+)[^₹]*?22k gold', body_text, re.IGNORECASE | re.DOTALL)146 kl22 = format_inr(match_22k.group(1).replace(',', '').strip()) if match_22k else "N/A"147 148 elapsed = round(time.time() - start_time, 2)149 return {150 "source": "KL",151 "kl24": kl24,152 "kl22": kl22,153 "elapsed_sec": elapsed,154 "status": "success"155 }156 except Exception as e:157 elapsed = round(time.time() - start_time, 2)158 return {159 "source": "KL",160 "error": str(e),161 "elapsed_sec": elapsed,162 "status": "error"163 }164 finally:165 await context.close()166 167def fetch_tq():168 start_time = time.time()169 url = "https://www.tanishq.co.in/gold-rate.html?lang=en_IN"170 171 try:172 response = cffi_requests.get(url, impersonate="chrome", timeout=10)173 if response.status_code != 200:174 return {175 "source": "TQ",176 "error": f"HTTP {response.status_code}",177 "elapsed_sec": round(time.time() - start_time, 2),178 "status": "error"179 }180 181 soup = BeautifulSoup(response.text, "lxml")182 gram_cells = soup.find_all(lambda tag: tag.name == "td" and "1 G" in tag.text.upper())183 prices = []184 185 for gram_cell in gram_cells:186 sibling = gram_cell.find_next_sibling("td")187 if sibling:188 match = re.search(r'[\d,]+', sibling.text.strip())189 if match:190 prices.append(int(match.group(0).replace(',', '')))191 192 elapsed = round(time.time() - start_time, 2)193 194 if prices:195 tq22_raw = min(prices) * 10196 tq22 = format_inr(tq22_raw)197 return {198 "source": "TQ",199 "tq22": tq22,200 "elapsed_sec": elapsed,201 "status": "success"202 }203 204 return {205 "source": "TQ",206 "error": "HTML parse error",207 "elapsed_sec": elapsed,208 "status": "error"209 }210 except Exception as e:211 elapsed = round(time.time() - start_time, 2)212 return {213 "source": "TQ",214 "error": str(e),215 "elapsed_sec": elapsed,216 "status": "error"217 }218 219# --- API ENDPOINTS ---220@app.get("/api/kb")221def api_kb():222 return fetch_kb()223 224@app.get("/api/kl")225async def api_kl():226 return await fetch_kl()227 228@app.get("/api/tq")229def api_tq():230 return fetch_tq()231 232# --- FRONTEND & STREAMING ROUTE ---233def get_html_dashboard():234 html_content = """<!DOCTYPE html>235<html>236<head>237 <title>API Status</title>238 <style>239 body { background-color: #0e0e0e; color: #cccccc; font-family: Consolas, "Courier New", monospace; font-size: 14px; margin: 16px; }240 .line { margin: 0; padding: 0; line-height: 1.4; white-space: pre; }241 </style>242</head>243<body>244 <div class="line">KB : <span id="kb-val">loading . . .</span></div>245 <div class="line">KL : <span id="kl-val">loading . . .</span></div>246 <div class="line">TQ : <span id="tq-val">loading . . .</span></div>247 248 <script>249 function fetchKB() {250 fetch('/api/kb')251 .then(response => response.json())252 .then(data => {253 if (data.status === "error") {254 document.getElementById('kb-val').innerText = 'Error (' + data.elapsed_sec + ' sec)';255 } else {256 document.getElementById('kb-val').innerText = '"' + data.kb1 + '" / "' + data.kb2 + '" (' + data.elapsed_sec + ' sec)';257 }258 })259 .catch(err => {260 document.getElementById('kb-val').innerText = 'error loading data';261 });262 }263 264 function fetchKL() {265 fetch('/api/kl')266 .then(response => response.json())267 .then(data => {268 if (data.status === "error") {269 document.getElementById('kl-val').innerText = 'Error (' + data.elapsed_sec + ' sec)';270 } else {271 document.getElementById('kl-val').innerText = '24K "' + data.kl24 + '" / 22K "' + data.kl22 + '" (' + data.elapsed_sec + ' sec)';272 }273 })274 .catch(err => {275 document.getElementById('kl-val').innerText = 'error loading data';276 });277 }278 279 function fetchTQ() {280 fetch('/api/tq')281 .then(response => response.json())282 .then(data => {283 if (data.status === "error") {284 document.getElementById('tq-val').innerText = 'Error (' + data.elapsed_sec + ' sec)';285 } else {286 document.getElementById('tq-val').innerText = '22K "' + data.tq22 + '" (' + data.elapsed_sec + ' sec)';287 }288 })289 .catch(err => {290 document.getElementById('tq-val').innerText = 'error loading data';291 });292 }293 294 // Start all background fetches295 fetchKB();296 fetchKL();297 fetchTQ();298 </script>299</body>300</html>"""301 return HTMLResponse(content=html_content)302 303@app.get("/api/all")304async def api_all():305 """Fetches all data concurrently and returns a single JSON object. Ideal for mobile apps."""306 loop = asyncio.get_running_loop()307 308 # Run the synchronous fetchers in threads, and await the async one309 kb_task = loop.run_in_executor(None, fetch_kb)310 kl_task = fetch_kl()311 tq_task = loop.run_in_executor(None, fetch_tq)312 313 # Wait for all of them to finish (takes about 3-5 seconds)314 kb_res, kl_res, tq_res = await asyncio.gather(kb_task, kl_task, tq_task)315 316 return {317 "kb": kb_res,318 "kl": kl_res,319 "tq": tq_res320 }321 322@app.get("/")323async def index(request: Request):324 accept_header = request.headers.get("accept", "")325 326 if "text/html" in accept_header:327 return get_html_dashboard()328 329 async def generate_stream():330 loop = asyncio.get_running_loop()331 tasks = [332 asyncio.ensure_future(loop.run_in_executor(None, fetch_kb)),333 asyncio.ensure_future(fetch_kl()),334 asyncio.ensure_future(loop.run_in_executor(None, fetch_tq))335 ]336 337 for coro in asyncio.as_completed(tasks):338 try:339 result = await coro340 yield json.dumps(result) + "\n"341 except Exception as e:342 yield json.dumps({"error": str(e), "status": "error"}) + "\n"343 344 return StreamingResponse(generate_stream(), media_type="application/x-ndjson")