first commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.DS_Store
|
||||||
75
app/docs/index.html
Normal file
75
app/docs/index.html
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Screenshot API Tester</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>📸 Screenshot API Tester</h1>
|
||||||
|
|
||||||
|
<label>URL:</label>
|
||||||
|
<input type="text" id="url" value="https://example.com">
|
||||||
|
|
||||||
|
<label>Width:</label>
|
||||||
|
<input type="number" id="width" value="1280">
|
||||||
|
|
||||||
|
<label>Height:</label>
|
||||||
|
<input type="number" id="height" value="720">
|
||||||
|
|
||||||
|
<label>Device:</label>
|
||||||
|
<select id="device">
|
||||||
|
<option value="desktop">Desktop</option>
|
||||||
|
<option value="mobile">Mobile</option>
|
||||||
|
<option value="tablet">Tablet</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button onclick="startScreenshot()">Take Screenshot</button>
|
||||||
|
|
||||||
|
<h3>Real-Time Updates:</h3>
|
||||||
|
<div id="log"></div>
|
||||||
|
|
||||||
|
<h3>Screenshot:</h3>
|
||||||
|
<img id="screenshot" src="" style="display:none; max-width: 100%;">
|
||||||
|
<a id="download" style="display:none;" download>Download Screenshot</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let socket = new WebSocket("ws://yourserver.com/ws");
|
||||||
|
|
||||||
|
socket.onmessage = function(event) {
|
||||||
|
let log = document.getElementById("log");
|
||||||
|
let message = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (message.screenshot_url) {
|
||||||
|
let img = document.getElementById("screenshot");
|
||||||
|
img.src = message.screenshot_url;
|
||||||
|
img.style.display = "block";
|
||||||
|
|
||||||
|
let download = document.getElementById("download");
|
||||||
|
download.href = message.screenshot_url;
|
||||||
|
download.style.display = "block";
|
||||||
|
} else {
|
||||||
|
log.innerHTML += `<p>${event.data}</p>`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function startScreenshot() {
|
||||||
|
let params = {
|
||||||
|
url: document.getElementById("url").value,
|
||||||
|
width: parseInt(document.getElementById("width").value),
|
||||||
|
height: parseInt(document.getElementById("height").value),
|
||||||
|
full_height: false,
|
||||||
|
format: "png",
|
||||||
|
click_selectors: [],
|
||||||
|
delay: 1000,
|
||||||
|
device: document.getElementById("device").value
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.send(JSON.stringify(params));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
44
app/docs/style.css
Normal file
44
app/docs/style.css
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 80%;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 20px auto;
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h3 {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #0073e6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #005bb5;
|
||||||
|
}
|
||||||
102
app/main.py
Normal file
102
app/main.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
import redis
|
||||||
|
import json
|
||||||
|
from fastapi import FastAPI, Query, HTTPException, WebSocket, WebSocketDisconnect, Request, Depends
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi_limiter import FastAPILimiter
|
||||||
|
from fastapi_limiter.depends import RateLimiter
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
# Secret API Key
|
||||||
|
SECRET_PHRASE = os.getenv("API_SECRET", "my_secret_key")
|
||||||
|
|
||||||
|
# Redis for Rate Limiting
|
||||||
|
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379")
|
||||||
|
redis_client = redis.from_url(REDIS_URL)
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup():
|
||||||
|
"""Initialize FastAPI Limiter."""
|
||||||
|
await FastAPILimiter.init(redis_client)
|
||||||
|
|
||||||
|
# Setup Static Files for Documentation
|
||||||
|
app.mount("/", StaticFiles(directory="app/docs", html=True), name="docs")
|
||||||
|
|
||||||
|
# WebSocket Connections Dictionary
|
||||||
|
active_connections = {}
|
||||||
|
|
||||||
|
# Device Profiles for Emulation
|
||||||
|
DEVICE_PROFILES = {
|
||||||
|
"mobile": {"width": 375, "height": 667, "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)"},
|
||||||
|
"tablet": {"width": 768, "height": 1024, "user_agent": "Mozilla/5.0 (iPad; CPU OS 14_0 like Mac OS X)"},
|
||||||
|
"desktop": {"width": 1280, "height": 720, "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},
|
||||||
|
}
|
||||||
|
|
||||||
|
def generate_md5_hash(url: str, secret: str) -> str:
|
||||||
|
"""Generate MD5 hash for authentication."""
|
||||||
|
return hashlib.md5(f"{url}{secret}".encode()).hexdigest()
|
||||||
|
|
||||||
|
async def take_screenshot(websocket: WebSocket, url: str, width: int, height: int, full_height: bool, format: str, click_selectors: list, delay: int, device: str):
|
||||||
|
"""Capture a screenshot using Playwright and return the file path."""
|
||||||
|
file_ext = format.lower()
|
||||||
|
screenshot_path = f"app/docs/screenshots/{uuid.uuid4()}.{file_ext}"
|
||||||
|
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser = await p.chromium.launch(headless=True)
|
||||||
|
await websocket.send_text("Browser launched...")
|
||||||
|
|
||||||
|
viewport = {"width": width, "height": height}
|
||||||
|
user_agent = None
|
||||||
|
if device in DEVICE_PROFILES:
|
||||||
|
viewport = {"width": DEVICE_PROFILES[device]["width"], "height": DEVICE_PROFILES[device]["height"]}
|
||||||
|
user_agent = DEVICE_PROFILES[device]["user_agent"]
|
||||||
|
|
||||||
|
context = await browser.new_context(viewport=viewport, user_agent=user_agent)
|
||||||
|
page = await context.new_page()
|
||||||
|
await page.goto(url, wait_until="load")
|
||||||
|
await websocket.send_text("Page loaded...")
|
||||||
|
|
||||||
|
for selector in click_selectors:
|
||||||
|
try:
|
||||||
|
await page.click(selector, timeout=2000)
|
||||||
|
await websocket.send_text(f"Clicked element: {selector}")
|
||||||
|
except Exception:
|
||||||
|
pass # Ignore if element is not found
|
||||||
|
|
||||||
|
if delay > 0:
|
||||||
|
await websocket.send_text(f"Waiting for {delay}ms...")
|
||||||
|
await page.wait_for_timeout(delay)
|
||||||
|
|
||||||
|
if full_height:
|
||||||
|
height = await page.evaluate("() => document.body.scrollHeight")
|
||||||
|
await page.set_viewport_size({"width": viewport["width"], "height": height})
|
||||||
|
|
||||||
|
await page.screenshot(path=screenshot_path, full_page=full_height, type=file_ext)
|
||||||
|
await websocket.send_text("Screenshot captured.")
|
||||||
|
|
||||||
|
await browser.close()
|
||||||
|
|
||||||
|
return screenshot_path
|
||||||
|
|
||||||
|
@app.websocket("/ws")
|
||||||
|
async def websocket_endpoint(websocket: WebSocket):
|
||||||
|
"""WebSocket Endpoint for Real-Time Updates."""
|
||||||
|
await websocket.accept()
|
||||||
|
active_connections[websocket] = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data = await websocket.receive_text()
|
||||||
|
params = json.loads(data)
|
||||||
|
|
||||||
|
screenshot_path = await take_screenshot(websocket, **params)
|
||||||
|
await websocket.send_text(json.dumps({"screenshot_url": f"/screenshots/{os.path.basename(screenshot_path)}"}))
|
||||||
|
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
active_connections.pop(websocket, None)
|
||||||
28
docker-compose.yaml
Normal file
28
docker-compose.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
screenshot-api:
|
||||||
|
build: .
|
||||||
|
container_name: screenshot-api
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
environment:
|
||||||
|
- API_SECRET=my_secret_key
|
||||||
|
- REDIS_URL=redis://redis:6379
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
networks:
|
||||||
|
- screenshot-network
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
container_name: screenshot-redis
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
networks:
|
||||||
|
- screenshot-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
screenshot-network:
|
||||||
|
driver: bridge
|
||||||
24
dockerfile
Normal file
24
dockerfile
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Use official Python image
|
||||||
|
FROM python:3.10
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy dependency files first for caching
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Install Playwright and required browsers
|
||||||
|
RUN pip install playwright
|
||||||
|
RUN playwright install --with-deps chromium
|
||||||
|
|
||||||
|
# Copy application files
|
||||||
|
COPY app ./app
|
||||||
|
|
||||||
|
# Expose ports
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Run FastAPI with Uvicorn
|
||||||
|
CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port 80 --reload"]
|
||||||
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
playwright
|
||||||
|
fastapi-limiter
|
||||||
|
redis
|
||||||
Reference in New Issue
Block a user