File size: 4,102 Bytes
5be97e9 638e4b7 7e4c8eb 638e4b7 87c1bc6 638e4b7 41829e6 638e4b7 5be97e9 87c1bc6 638e4b7 7e4c8eb 5be97e9 638e4b7 40d72d0 87c1bc6 638e4b7 87c1bc6 638e4b7 87c1bc6 5be97e9 87c1bc6 638e4b7 87c1bc6 41829e6 40d72d0 87c1bc6 40d72d0 638e4b7 87c1bc6 41829e6 87c1bc6 89e8c7a 638e4b7 7e4c8eb 5be97e9 87c1bc6 5be97e9 eae8220 33f6f2c |
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File: main.py
from fastapi import FastAPI, Query
from fastapi.responses import Response
import requests
from bs4 import BeautifulSoup
from urllib.parse import quote
import xml.etree.ElementTree as ET
from datetime import datetime
app = FastAPI()
# ========== FB2 Generator ==========
def html_to_fb2(title: str, body: str) -> str:
clean_text = BeautifulSoup(body, "html.parser").get_text(separator="\n")
fb2 = f"""<?xml version='1.0' encoding='utf-8'?>
<FictionBook xmlns:xlink='http://www.w3.org/1999/xlink'>
<description>
<title-info>
<genre>nonfiction</genre>
<author><first-name>OPDS</first-name><last-name>DuckScraper</last-name></author>
<book-title>{title}</book-title>
<lang>en</lang>
</title-info>
</description>
<body>
<section>
<title><p>{title}</p></title>
<p>{clean_text}</p>
</section>
</body>
</FictionBook>"""
return fb2
# ========== DuckDuckGo Search ==========
def duckduckgo_search(query: str):
res = requests.post(
"https://html.duckduckgo.com/html/",
data={"q": query},
headers={"User-Agent": "Mozilla/5.0"},
timeout=10
)
res.raise_for_status()
soup = BeautifulSoup(res.text, "html.parser")
results = []
for a in soup.select("a.result__a"):
href = a.get("href")
title = a.get_text()
if href and title:
results.append((title.strip(), href))
if len(results) >= 10:
break
return results
# ========== OPDS Feed Generators ==========
def generate_root_feed():
ns = "http://www.w3.org/2005/Atom"
ET.register_namespace("", ns)
feed = ET.Element("feed", xmlns=ns)
ET.SubElement(feed, "title").text = "DuckDuckGo OPDS Catalog"
ET.SubElement(feed, "updated").text = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
# Relative search link (OpenSearch template)
feed.append(ET.Element("link", {
"rel": "search",
"type": "application/atom+xml",
"href": "/opds/search?q={searchTerms}",
"templated": "true"
}))
return ET.tostring(feed, encoding="utf-8", xml_declaration=True)
def generate_search_feed(query: str, results):
ns = "http://www.w3.org/2005/Atom"
ET.register_namespace("", ns)
feed = ET.Element("feed", xmlns=ns)
ET.SubElement(feed, "title").text = f"Search results for '{query}'"
ET.SubElement(feed, "updated").text = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
for title, url in results:
entry = ET.SubElement(feed, "entry")
ET.SubElement(entry, "title").text = title
ET.SubElement(entry, "id").text = url
ET.SubElement(entry, "updated").text = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
ET.SubElement(entry, "link", {
"rel": "http://opds-spec.org/acquisition",
"href": f"/download?url={quote(url, safe='')}",
"type": "application/fb2+xml"
})
return ET.tostring(feed, encoding="utf-8", xml_declaration=True)
# ========== Routes ==========
@app.get("/opds", include_in_schema=False)
def opds_root() -> Response:
xml_data = generate_root_feed()
return Response(content=xml_data, media_type="application/atom+xml")
@app.get("/opds/search")
def opds_search(q: str = Query(..., description="Search query")) -> Response:
results = duckduckgo_search(q)
xml_data = generate_search_feed(q, results)
return Response(content=xml_data, media_type="application/atom+xml")
@app.get("/download")
def download_fb2(url: str) -> Response:
res = requests.get(url, headers={"User-Agent": "Mozilla/5.0"}, timeout=10)
res.raise_for_status()
soup = BeautifulSoup(res.text, "html.parser")
title = soup.title.string.strip() if soup.title and soup.title.string else "article"
body = str(soup.body)
fb2 = html_to_fb2(title, body)
filename = f"{quote(title, safe='').replace('%20','_')[:30]}.fb2"
return Response(
content=fb2,
media_type="application/fb2+xml",
headers={"Content-Disposition": f"attachment; filename={filename}"}
)
|