Upload files to "backend"
This commit is contained in:
11
backend/Dockerfile
Normal file
11
backend/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM python:3.12-slim
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libnss3 libatk-bridge2.0-0 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libxss1 libasound2 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /app
|
||||
RUN pip install playwright && playwright install --with-deps chromium
|
||||
COPY requirements.txt .
|
||||
RUN pip install -r requirements.txt
|
||||
COPY app.py .
|
||||
EXPOSE 8000
|
||||
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
96
backend/app.py
Normal file
96
backend/app.py
Normal file
@@ -0,0 +1,96 @@
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from playwright.sync_api import sync_playwright
|
||||
from bs4 import BeautifulSoup
|
||||
import requests
|
||||
from openai import OpenAI
|
||||
import random
|
||||
from datetime import datetime
|
||||
import pandas as pd
|
||||
import io
|
||||
import os
|
||||
import time
|
||||
|
||||
app = FastAPI(title="Deal Hunter Backend")
|
||||
client = OpenAI(api_key=os.getenv("GROK_API_KEY"), base_url="https://api.x.ai/v1")
|
||||
TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN")
|
||||
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID")
|
||||
|
||||
class SearchRequest(BaseModel):
|
||||
item: str = "iPhone 14 Pro Max"
|
||||
location_radius: int = 50
|
||||
min_price: float = 0
|
||||
max_price: float = 1000
|
||||
condition: str = "Any"
|
||||
min_score: int = 7
|
||||
|
||||
@app.post("/hunt")
|
||||
def hunt_deals(req: SearchRequest):
|
||||
# Scrape FB
|
||||
ua = random.choice([
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
])
|
||||
url = f"https://www.facebook.com/marketplace/search/?query={req.item.replace(' ', '+')}&radius={req.location_radius}"
|
||||
listings = []
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
page = browser.new_page(user_agent=ua)
|
||||
page.goto(url, wait_until="networkidle")
|
||||
time.sleep(5)
|
||||
for _ in range(3):
|
||||
page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
|
||||
time.sleep(3)
|
||||
soup = BeautifulSoup(page.content(), 'html.parser')
|
||||
articles = soup.find_all('div', {'role': 'article'})[:20]
|
||||
for article in articles:
|
||||
try:
|
||||
title = article.find('span', class_='x1lliihq').text.strip() or 'N/A'
|
||||
price_elem = article.find('span', attrs={'aria-label': lambda v: v and '$' in v})
|
||||
price = float(price_elem['aria-label'].replace('$', '').replace(',', '')) if price_elem else 0
|
||||
if req.min_price <= price <= req.max_price:
|
||||
link = 'https://fb.com' + article.find('a')['href'] if article.find('a') else 'N/A'
|
||||
img = article.find('img')['src'] if article.find('img') else 'N/A'
|
||||
cond = 'New' if 'new' in title.lower() else 'Used'
|
||||
if req.condition == "Any" or cond == req.condition:
|
||||
listings.append({'title': title, 'price': price, 'link': link, 'image': img, 'condition': cond})
|
||||
except:
|
||||
continue
|
||||
browser.close()
|
||||
|
||||
# Value & Score
|
||||
deals = []
|
||||
for listing in listings:
|
||||
query = listing['title'].replace(' ', '+')
|
||||
ebay_url = f"https://www.ebay.com/sch/i.html?_nkw={query}&LH_Sold=1&LH_Complete=1&rt=nc"
|
||||
response = requests.get(ebay_url, headers={'User-Agent': ua}, timeout=10)
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
prices = [float(span.text.replace('$', '').replace(',', '')) for span in soup.find_all('span', class_='s-item__price')[:5] if span.text.replace('$', '').replace(',', '').replace('.', '').isdigit()]
|
||||
resale = sum(prices) / len(prices) if prices else 0
|
||||
offer = resale * (1 - 0.3)
|
||||
score = min(10, (resale - listing['price']) / resale * 10) + (2 if listing['condition'] == 'New' else 0) if resale > 0 else 0
|
||||
status = 'Hot Deal' if score >= req.min_score else 'Pass'
|
||||
|
||||
# AI Offer
|
||||
prompt = f"Polite lowball offer for {listing['title']} at ${listing['price']}, condition {listing['condition']}, resale ${resale}. Offer ${offer}."
|
||||
response = client.chat.completions.create(model="grok-beta", messages=[{"role": "user", "content": prompt}])
|
||||
ai_offer = response.choices[0].message.content
|
||||
|
||||
deal = {**listing, 'resale_avg': resale, 'offer': offer, 'score': score, 'status': status, 'ai_offer': ai_offer}
|
||||
deals.append(deal)
|
||||
|
||||
if status == 'Hot Deal':
|
||||
caption = f"*Hot Deal!*\n*{listing['title']}*\nPrice: ${listing['price']}\nOffer: ${offer}\nScore: {score}/10\n{ai_offer}\n[Link]({listing['link']})"
|
||||
requests.post(f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendPhoto", data={'chat_id': TELEGRAM_CHAT_ID, 'caption': caption, 'parse_mode': 'Markdown'}, files={'photo': requests.get(listing['image']).content} if listing['image'].startswith('http') else {})
|
||||
|
||||
# CSV
|
||||
df = pd.DataFrame(deals)
|
||||
csv_buffer = io.StringIO()
|
||||
df.to_csv(csv_buffer, index=False)
|
||||
csv_data = csv_buffer.getvalue()
|
||||
|
||||
return {"deals": deals, "csv": csv_data, "hot_count": len([d for d in deals if d['status'] == 'Hot Deal'])}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
7
backend/requirements.txt
Normal file
7
backend/requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
fastapi
|
||||
uvicorn
|
||||
playwright
|
||||
beautifulsoup4
|
||||
requests
|
||||
openai
|
||||
pandas
|
||||
Reference in New Issue
Block a user