|
@@ -0,0 +1,125 @@
|
|
|
+import re
|
|
|
+from contextlib import asynccontextmanager
|
|
|
+from datetime import datetime
|
|
|
+from json import JSONDecodeError
|
|
|
+
|
|
|
+import aiohttp
|
|
|
+from fastapi import FastAPI, Request
|
|
|
+from fastapi.responses import HTMLResponse, RedirectResponse
|
|
|
+from fastapi.staticfiles import StaticFiles
|
|
|
+from fastapi.templating import Jinja2Templates
|
|
|
+from selectolax.parser import HTMLParser, Node
|
|
|
+
|
|
|
+
|
|
|
+@asynccontextmanager
|
|
|
+async def lifespan(app: FastAPI):
|
|
|
+ """Establishing an aiohttp ClientSession for the duration of the app's lifecycle."""
|
|
|
+ global session
|
|
|
+ session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(10))
|
|
|
+ yield
|
|
|
+ await session.close()
|
|
|
+
|
|
|
+
|
|
|
+app = FastAPI(lifespan=lifespan, docs_url=None, redoc_url=None)
|
|
|
+app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
|
+templates = Jinja2Templates(directory="templates")
|
|
|
+session: aiohttp.ClientSession = None
|
|
|
+
|
|
|
+
|
|
|
+def remove_classes(node: Node) -> Node:
|
|
|
+ """Recursively remove all classes from all nodes."""
|
|
|
+ if "class" in node.attributes:
|
|
|
+ del node.attrs["class"]
|
|
|
+ for child in node.iter():
|
|
|
+ remove_classes(child)
|
|
|
+ return node
|
|
|
+
|
|
|
+
|
|
|
+@app.get("/{path:path}", response_class=HTMLResponse)
|
|
|
+async def catch_all(response: Request):
|
|
|
+ """Handle all routes on Urban Dictionary and perform redirection if necessary."""
|
|
|
+ path_without_host = (
|
|
|
+ f"{response.url.path}{f'?{response.url.query}' if response.url.query else ''}"
|
|
|
+ )
|
|
|
+ url = f"https://www.urbandictionary.com{path_without_host}"
|
|
|
+ term = response.query_params.get("term")
|
|
|
+
|
|
|
+ async with session.get(url) as dict_response:
|
|
|
+ if dict_response.history:
|
|
|
+ return RedirectResponse(str(dict_response.url.relative()), status_code=301)
|
|
|
+ html = await dict_response.text()
|
|
|
+ parser = HTMLParser(html)
|
|
|
+ if dict_response.status != 200:
|
|
|
+ similar_words = None
|
|
|
+ if (try_this := parser.css_first("div.try-these")) is not None:
|
|
|
+ similar_words = [remove_classes(word).html for word in try_this.css("li a")]
|
|
|
+ return templates.TemplateResponse(
|
|
|
+ "404.html",
|
|
|
+ {
|
|
|
+ "request": response,
|
|
|
+ "similar_words": similar_words,
|
|
|
+ "term": term,
|
|
|
+ "site_title": f"Rural Dictionary: {term}",
|
|
|
+ "site_description": (
|
|
|
+ "View on Rural Dictionary, an alternative private "
|
|
|
+ "frontend to Urban Dictionary."
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ status_code=404,
|
|
|
+ )
|
|
|
+
|
|
|
+ results = []
|
|
|
+ definitions = parser.css("div[data-defid]")
|
|
|
+ try:
|
|
|
+ thumbs_api_url = (
|
|
|
+ f'https://api.urbandictionary.com/v0/uncacheable?ids='
|
|
|
+ f'{",".join(d.attributes["data-defid"] or "-1" for d in definitions)}'
|
|
|
+ )
|
|
|
+ async with session.get(thumbs_api_url) as thumbs_response:
|
|
|
+ thumbs_json = await thumbs_response.json()
|
|
|
+ thumbs_data = {el["defid"]: el for el in thumbs_json["thumbs"]}
|
|
|
+ except (KeyError, JSONDecodeError, TimeoutError):
|
|
|
+ thumbs_data = {}
|
|
|
+
|
|
|
+ site_description = None
|
|
|
+ for definition in definitions:
|
|
|
+ word = definition.css_first("a.word").text()
|
|
|
+ meaning_node = remove_classes(definition.css_first("div.meaning"))
|
|
|
+ if site_description is None:
|
|
|
+ site_description = re.sub(r"\s+", " ", meaning_node.text(strip=True, separator=" "))
|
|
|
+ meaning = meaning_node.html
|
|
|
+ example = remove_classes(definition.css_first("div.example")).html
|
|
|
+ contributor = remove_classes(definition.css_first("div.contributor")).html
|
|
|
+ definition_id = int(definition.attributes["data-defid"] or "-1")
|
|
|
+ definition_thumbs = thumbs_data.get(definition_id, {})
|
|
|
+ thumbs_up = definition_thumbs.get("up")
|
|
|
+ thumbs_down = definition_thumbs.get("down")
|
|
|
+ results.append(
|
|
|
+ [definition_id, word, meaning, example, contributor, thumbs_up, thumbs_down]
|
|
|
+ )
|
|
|
+ if (pagination := parser.css_first("div.pagination")) is not None:
|
|
|
+ pagination = remove_classes(pagination)
|
|
|
+ pagination.attrs["class"] = "pagination"
|
|
|
+ pagination = pagination.html
|
|
|
+
|
|
|
+ term = term or results[0][1]
|
|
|
+ site_title = "Rural Dictionary"
|
|
|
+ match response.url.path:
|
|
|
+ case "/":
|
|
|
+
|
|
|
+ site_title += f', {datetime.now().strftime("%d %B")}'
|
|
|
+ case "/random.php":
|
|
|
+ term = "Random words"
|
|
|
+ site_title += f": {term}"
|
|
|
+
|
|
|
+ return templates.TemplateResponse(
|
|
|
+ "index.html",
|
|
|
+ {
|
|
|
+ "request": response,
|
|
|
+ "results": results,
|
|
|
+ "pagination": pagination,
|
|
|
+ "term": term,
|
|
|
+ "site_title": site_title,
|
|
|
+ "site_description": site_description,
|
|
|
+ },
|
|
|
+ )
|