File size: 1,391 Bytes
0c11338
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
"""Firecrawl: renders JavaScript sites, so posts on sites without a feed (Next.js, Framer, Webflow) still come in."""

import httpx

from app.config import settings


class FirecrawlError(RuntimeError):
    pass


def enabled() -> bool:
    return bool(settings.firecrawl_api_key)


def _post(path: str, body: dict, timeout: float = 90) -> dict:
    resp = httpx.post(
        f"{settings.firecrawl_url.rstrip('/')}{path}",
        json=body,
        headers={"Authorization": f"Bearer {settings.firecrawl_api_key}"},
        timeout=timeout,
    )
    if resp.status_code >= 400:
        raise FirecrawlError(f"Firecrawl returned {resp.status_code}: {resp.text[:200]}")
    data = resp.json()
    if data.get("success") is False:
        raise FirecrawlError(str(data.get("error") or "Firecrawl request failed")[:200])
    return data


def map_site(url: str, limit: int) -> list[str]:
    """Every URL Firecrawl can find on the site (sitemap plus links on rendered pages)."""
    data = _post("/v2/map", {"url": url, "limit": limit, "sitemap": "include"})
    return [link if isinstance(link, str) else link.get("url", "") for link in data.get("links") or []]


def scrape(url: str) -> dict:
    """Main content of one page as markdown, plus page metadata."""
    data = _post("/v2/scrape", {"url": url, "formats": ["markdown"], "onlyMainContent": True})
    return data.get("data") or {}