Update main.py
Browse files
main.py
CHANGED
@@ -3,8 +3,6 @@
|
|
3 |
from fastapi import FastAPI, Query
|
4 |
from fastapi.responses import Response
|
5 |
import requests
|
6 |
-
from bs4 import BeautifulSoup
|
7 |
-
from urllib.parse import quote
|
8 |
import xml.etree.ElementTree as ET
|
9 |
from datetime import datetime
|
10 |
from typing import Optional
|
@@ -12,6 +10,9 @@ from typing import Optional
|
|
12 |
app = FastAPI()
|
13 |
|
14 |
# ========== FB2 Generator ==========
|
|
|
|
|
|
|
15 |
def html_to_fb2(title: str, body: str) -> str:
|
16 |
clean_text = BeautifulSoup(body, "html.parser").get_text(separator="\n")
|
17 |
return f"""<?xml version='1.0' encoding='utf-8'?>
|
@@ -32,28 +33,30 @@ def html_to_fb2(title: str, body: str) -> str:
|
|
32 |
</body>
|
33 |
</FictionBook>"""
|
34 |
|
35 |
-
# ========== DuckDuckGo Search ==========
|
36 |
def duckduckgo_search(query: str):
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
|
|
|
|
43 |
res.raise_for_status()
|
44 |
-
|
45 |
results = []
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
return results
|
54 |
|
55 |
-
# ========== OPDS Feed
|
56 |
-
def create_feed(entries: list,
|
57 |
ns = "http://www.w3.org/2005/Atom"
|
58 |
ET.register_namespace("", ns)
|
59 |
feed = ET.Element("feed", xmlns=ns)
|
@@ -62,15 +65,14 @@ def create_feed(entries: list, templated: bool, q: Optional[str]) -> bytes:
|
|
62 |
ET.SubElement(feed, "updated").text = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
|
63 |
|
64 |
# OpenSearch templated search link
|
65 |
-
|
66 |
"rel": "search",
|
67 |
"type": "application/atom+xml;profile=opds-catalog;kind=search",
|
68 |
"href": "/opds?q={searchTerms}",
|
69 |
"templated": "true"
|
70 |
-
}
|
71 |
-
ET.SubElement(feed, "link", link_attrs)
|
72 |
|
73 |
-
# Add entries
|
74 |
for entry_info in entries:
|
75 |
entry = ET.SubElement(feed, "entry")
|
76 |
ET.SubElement(entry, "id").text = entry_info['id']
|
@@ -81,8 +83,8 @@ def create_feed(entries: list, templated: bool, q: Optional[str]) -> bytes:
|
|
81 |
return ET.tostring(feed, encoding="utf-8", xml_declaration=True)
|
82 |
|
83 |
# ========== Routes ==========
|
84 |
-
@app.get("/opds"
|
85 |
-
def opds(q: Optional[str] = Query(None,
|
86 |
entries = []
|
87 |
kind = "search"
|
88 |
if q:
|
@@ -99,7 +101,7 @@ def opds(q: Optional[str] = Query(None, alias="q", description="Search query"))
|
|
99 |
}
|
100 |
})
|
101 |
kind = "acquisition"
|
102 |
-
xml_data = create_feed(entries,
|
103 |
return Response(content=xml_data,
|
104 |
media_type=f"application/atom+xml;profile=opds-catalog;kind={kind}")
|
105 |
|
@@ -107,6 +109,7 @@ def opds(q: Optional[str] = Query(None, alias="q", description="Search query"))
|
|
107 |
def download_fb2(url: str) -> Response:
|
108 |
res = requests.get(url, headers={"User-Agent": "Mozilla/5.0"}, timeout=10)
|
109 |
res.raise_for_status()
|
|
|
110 |
soup = BeautifulSoup(res.text, "html.parser")
|
111 |
title = soup.title.string.strip() if soup.title and soup.title.string else "article"
|
112 |
fb2 = html_to_fb2(title, str(soup.body))
|
@@ -117,3 +120,16 @@ def download_fb2(url: str) -> Response:
|
|
117 |
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
118 |
)
|
119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
from fastapi import FastAPI, Query
|
4 |
from fastapi.responses import Response
|
5 |
import requests
|
|
|
|
|
6 |
import xml.etree.ElementTree as ET
|
7 |
from datetime import datetime
|
8 |
from typing import Optional
|
|
|
10 |
app = FastAPI()
|
11 |
|
12 |
# ========== FB2 Generator ==========
|
13 |
+
from bs4 import BeautifulSoup
|
14 |
+
from urllib.parse import quote
|
15 |
+
|
16 |
def html_to_fb2(title: str, body: str) -> str:
|
17 |
clean_text = BeautifulSoup(body, "html.parser").get_text(separator="\n")
|
18 |
return f"""<?xml version='1.0' encoding='utf-8'?>
|
|
|
33 |
</body>
|
34 |
</FictionBook>"""
|
35 |
|
36 |
+
# ========== DuckDuckGo JSON Search ==========
|
37 |
def duckduckgo_search(query: str):
|
38 |
+
api_url = "https://api.duckduckgo.com/"
|
39 |
+
params = {
|
40 |
+
"q": query,
|
41 |
+
"format": "json",
|
42 |
+
"no_html": 1,
|
43 |
+
"skip_disambig": 1
|
44 |
+
}
|
45 |
+
res = requests.get(api_url, params=params, headers={"User-Agent": "Mozilla/5.0"}, timeout=10)
|
46 |
res.raise_for_status()
|
47 |
+
data = res.json()
|
48 |
results = []
|
49 |
+
def extract_topics(topics):
|
50 |
+
for item in topics:
|
51 |
+
if "FirstURL" in item and "Text" in item:
|
52 |
+
results.append((item["Text"], item["FirstURL"]))
|
53 |
+
elif "Topics" in item:
|
54 |
+
extract_topics(item["Topics"])
|
55 |
+
extract_topics(data.get("RelatedTopics", []))
|
56 |
+
return results[:10]
|
57 |
|
58 |
+
# ========== OPDS Feed Generator ==========
|
59 |
+
def create_feed(entries: list, q: Optional[str]) -> bytes:
|
60 |
ns = "http://www.w3.org/2005/Atom"
|
61 |
ET.register_namespace("", ns)
|
62 |
feed = ET.Element("feed", xmlns=ns)
|
|
|
65 |
ET.SubElement(feed, "updated").text = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
|
66 |
|
67 |
# OpenSearch templated search link
|
68 |
+
ET.SubElement(feed, "link", {
|
69 |
"rel": "search",
|
70 |
"type": "application/atom+xml;profile=opds-catalog;kind=search",
|
71 |
"href": "/opds?q={searchTerms}",
|
72 |
"templated": "true"
|
73 |
+
})
|
|
|
74 |
|
75 |
+
# Add entries
|
76 |
for entry_info in entries:
|
77 |
entry = ET.SubElement(feed, "entry")
|
78 |
ET.SubElement(entry, "id").text = entry_info['id']
|
|
|
83 |
return ET.tostring(feed, encoding="utf-8", xml_declaration=True)
|
84 |
|
85 |
# ========== Routes ==========
|
86 |
+
@app.get("/opds")
|
87 |
+
def opds(q: Optional[str] = Query(None, description="Search query")) -> Response:
|
88 |
entries = []
|
89 |
kind = "search"
|
90 |
if q:
|
|
|
101 |
}
|
102 |
})
|
103 |
kind = "acquisition"
|
104 |
+
xml_data = create_feed(entries, q)
|
105 |
return Response(content=xml_data,
|
106 |
media_type=f"application/atom+xml;profile=opds-catalog;kind={kind}")
|
107 |
|
|
|
109 |
def download_fb2(url: str) -> Response:
|
110 |
res = requests.get(url, headers={"User-Agent": "Mozilla/5.0"}, timeout=10)
|
111 |
res.raise_for_status()
|
112 |
+
from bs4 import BeautifulSoup
|
113 |
soup = BeautifulSoup(res.text, "html.parser")
|
114 |
title = soup.title.string.strip() if soup.title and soup.title.string else "article"
|
115 |
fb2 = html_to_fb2(title, str(soup.body))
|
|
|
120 |
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
121 |
)
|
122 |
|
123 |
+
# File: Dockerfile
|
124 |
+
FROM python:3.11-slim
|
125 |
+
WORKDIR /app
|
126 |
+
COPY requirements.txt .
|
127 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
128 |
+
COPY main.py .
|
129 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
130 |
+
|
131 |
+
# File: requirements.txt
|
132 |
+
fastapi
|
133 |
+
uvicorn
|
134 |
+
requests
|
135 |
+
beautifulsoup4
|