This commit is contained in:
Win 2024-03-10 19:54:16 +07:00
parent 6fde26affd
commit 52e8b92f1f
16 changed files with 6436 additions and 2 deletions

10
Dockerfile Normal file
View File

@ -0,0 +1,10 @@
FROM python:3-alpine
WORKDIR /usr/src/rural-dict
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD [ "python", "./main.py" ]

View File

@ -1,3 +1,46 @@
# country-dictionary
# Country Dictionary
A privacy-respecting Urban Dictionary client in Flask - Mostly based on https://git.vern.cc/cobra/rural-dict
## Instances
| **URL** | **Country** | **Ownername** | **Owner Website** | |
| ---------------------------------- | ----------- | ------------- | ------------------------ | --- |
| https://country-dict.winscloud.net | TH | WinsDominoes | https://winsdominoes.net | |
## About
Country Dictionary scrapes _Urban Dictionary_ for data and then displays it in HTML & CSS.
### Supports
- Define a word with multiple entries
- Pagination
- Random list of words
- User pages
- Urban Dictionary home with words of the day
- Matches urban dictionary's endpoints for features listed above
### Dependencies
- bs4
- requests
- waitress
- Relatively new version of python
### Redirection
Simply replace an _Urban Dictionary_ URL with a Country Dictionary URL from the instance list above.
```
https://urbandictionary.com/define.php?term=eevee
https://country-dict.winscloud.net/define.php?term=eevee
```
NOTE: More endpoints are supported
## Contributors
- https://git.vern.cc/cobra/rural-dict - the original Rural Dictionary project
- https://codeberg.org/zortazert - created the initial Urban Dictionary frontend using JavaScript and helped develop Rural Dictionary

9
docker-compose.yml Normal file
View File

@ -0,0 +1,9 @@
version: "3"
services:
rural-dict:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"

8
instances.json Normal file
View File

@ -0,0 +1,8 @@
[
{
"clearnet": "https://country-dict.winscloud.net",
"country": "TH",
"owner_name": "WinsDominoes",
"owner_website": "https://winsdominoes.net"
}
]

59
main.py Normal file
View File

@ -0,0 +1,59 @@
#!/usr/bin/env python
from flask import Flask, render_template, request, redirect
import requests
import html
import re
from bs4 import BeautifulSoup
from urllib.parse import quote, unquote
def scrape(url):
data = requests.get(url)
our_path = re.sub(r".*://.*/", "/", request.url)
path = re.sub(r".*://.*/", "/", data.url)
if our_path != path and \
quote(unquote(re.sub("[?&=]", "", our_path))) != re.sub("[?&=]", "", path):
# this is bad ^
return f"REDIRECT {path}"
ret = []
soup = BeautifulSoup(data.text, "html.parser")
defs = [(div, div.get('data-defid')) for div in soup.find_all("div") if div.get('data-defid')]
try:
thumbs_data = {
str(entry['defid']): entry
for entry
in requests.get(
'https://api.urbandictionary.com/v0/uncacheable?ids=' + ','.join(defid for (_, defid) in defs)
).json()['thumbs']
}
except:
thumbs_data = {}
for (definition, defid) in defs:
word = definition.select("div div h1 a, div div h2 a")[0].text
meaning = definition.find(attrs={"class" : ["break-words meaning mb-4"]}).decode_contents()
example = definition.find(attrs={"class" : ["break-words example italic mb-4"]}).decode_contents()
contributor = definition.find(attrs={"class" : ["contributor font-bold"]})
thumbs_up = thumbs_data.get(defid, {}).get('up')
thumbs_down = thumbs_data.get(defid, {}).get('down')
ret.append([defid, word, meaning, example, contributor, thumbs_up, thumbs_down])
pages = soup.find(attrs={"class" : ["pagination text-xl text-center"]})
if pages == None:
pages = ""
return (ret, pages)
app = Flask(__name__, template_folder="templates", static_folder="static")
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def catch_all(path):
scraped = scrape(f"https://urbandictionary.com/{re.sub(r'.*://.*/', '/', request.url)}")
if type(scraped) == str and scraped.startswith("REDIRECT"):
return redirect(scraped.replace("REDIRECT ", ""), 302)
return render_template('index.html', data=scraped)
if __name__ == '__main__':
from waitress import serve
serve(app, host="0.0.0.0", port=8089)

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
beautifulsoup4
requests
flask
waitress

6177
static/css/bootstrap-icons.min.css vendored Normal file

File diff suppressed because it is too large Load Diff

6
static/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

28
static/css/main.css Normal file
View File

@ -0,0 +1,28 @@
@font-face {
font-family: "Roboto";
src: url("../fonts/Roboto-Regular.ttf") format("truetype");
} /* Safari, Android, iOS */
html,
body {
font-family: "Roboto" !important;
height: 100%;
margin: 0;
padding: 0;
}
a {
text-decoration: none;
}
.pagination {
margin-right: 1ch;
text-align: center;
}
.pagination ul > li {
list-style: none;
display: inline-block;
padding-left: 1ch;
}

Binary file not shown.

BIN
static/img/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

19
static/js/theme.js Normal file
View File

@ -0,0 +1,19 @@
(function () {
const htmlElement = document.querySelector("html");
if (htmlElement.getAttribute("data-bs-theme") === "auto") {
function updateTheme() {
document
.querySelector("html")
.setAttribute(
"data-bs-theme",
window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light"
);
}
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", updateTheme);
updateTheme();
}
})();

71
templates/index.html Normal file
View File

@ -0,0 +1,71 @@
<!DOCTYPE html>
<html class="mdui-theme-auto" data-bs-theme="auto">
<head>
<title>Country Dictionary</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<script src="{{ url_for('static', filename='js/theme.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap-icons.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}" />
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/favicon.png') }}">
</head>
<body>
<div class="container">
<div id="logoContainer" class="mt-5">
<a href="/" class="text-light-emphasis">
<h1 class="display-1 fw-bold">Country Dictionary</h1>
</a>
<p>A fork of <a href="https://git.vern.cc/cobra/rural-dict">Rural Dictionary</a></p>
</div>
<div class="field-text">
<form id="search" role="search" method="get" action="/define.php">
<div class="input-group mb-3">
<input type="search" id="term" name="term" placeholder="Search" class="form-control" autofocus>
<button class="btn btn-outline-secondary">Go</button>
</div>
</form>
</div>
<div class="row row-cols-auto gap-2 mb-3 justify-content-center">
<a href=/random.php>
<button class="btn btn-danger">
Random
</button>
</a>
<a href="https://git.winscloud.net/winsdominoes/country-dict">
<button class="btn btn-warning">
Source Code
</button>
</a>
</div>
{% for defid, word, definition, example, author, thumbs_up, thumbs_down in data[0] %}
<div class="card mb-3">
<div class="card-body">
<div class="{{ defid }}">
<a href="/define.php?term={{ word }}">
<h2>{{ word }}</h2>
</a>
<p>{{ definition|safe }}</p>
<p><i>{{ example|safe }}</i></p>
<p>{{ author|safe }}</p>
{% if thumbs_up and thumbs_down %}
<div class="d-grid gap-2 d-md-block">
<button class="btn btn-outline-secondary disabled">
<i class="bi bi-hand-thumbs-up-fill"></i>
{{ thumbs_up|safe }}
</button>
<button class="btn btn-outline-secondary disabled">
<i class="bi bi-hand-thumbs-down-fill"></i>
{{ thumbs_down|safe }}
</button>
</div>
{% endif %}
</div>
</div>
</div>
{% endfor %}
<div class="d-flex justify-content-center">
{{ data[1]|safe }}
</div>
</body>
</html>