Initial commit

pull/3/head
beucismis 1 year ago
parent 0d1bb2c019
commit fcc37b18ad
  1. 13
      LICENSE
  2. 8
      README.md
  3. 13
      ozgursozluk/__init__.py
  4. 138
      ozgursozluk/api.py
  5. 7
      ozgursozluk/config.py
  6. BIN
      ozgursozluk/static/favicon.png
  7. 174
      ozgursozluk/static/style.css
  8. 8
      ozgursozluk/templates/404.html
  9. 27
      ozgursozluk/templates/base.html
  10. 27
      ozgursozluk/templates/index.html
  11. 4
      ozgursozluk/templates/navigation.html
  12. 36
      ozgursozluk/templates/settings.html
  13. 30
      ozgursozluk/templates/topic.html
  14. 82
      ozgursozluk/views.py
  15. 4
      requirements.txt
  16. 8
      serve.py
  17. 22
      test.py

@ -0,0 +1,13 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2023 beucismis <beucismis@tutamail.com>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

@ -1,2 +1,10 @@
# ozgursozluk
Free alternative simple ekşi sözlük front-end
# Installing
```
git clone https://github.com/beucismis/ozgursozluk
cd ozgursozluk/
pip3 install -r requirements.txt
python3 serve.py
```

@ -0,0 +1,13 @@
import flask
__version__ = "0.1"
__author__ = "beucismis"
__source__ = "https://github.com/beucismis/ozgursozluk"
__description__ = "free alternative simple ekşi sözlük front-end"
app = flask.Flask(__name__)
app.config.from_object("ozgursozluk.config")
import ozgursozluk.views

@ -0,0 +1,138 @@
from typing import Iterator
from dataclasses import dataclass
import flask
import requests
from bs4 import BeautifulSoup
from fake_useragent import UserAgent
from ozgursozluk.config import DEFAULT_EKSI_BASE_URL
CHARMAP = {
"ç": "c",
"ı": "i",
"ö": "o",
"ş": "s",
"ü": "u",
}
@dataclass
class Agenda:
title: str
views: str
pinned: bool
permalink: str
@dataclass
class Entry:
id: str
content: str
author: str
datetime: str
permalink: str
@dataclass
class Topic:
id: str
title: str
pagecount: int
permalink: str
entrys: Iterator[Entry]
def title_id(self) -> str:
return _unicode_tr(f"{self.title}--{self.id}".replace(" ", "-"))
class Eksi:
def __init__(self, base_url: str = DEFAULT_EKSI_BASE_URL) -> None:
self.base_url = base_url
self.headers = {"User-Agent": UserAgent().random}
def _get(self, endpoint: str = "/", params: dict = {}) -> dict:
response = requests.get(
f"{self.base_url}{endpoint}", params=params, headers=self.headers
)
if response.status_code != 200:
flask.abort(response.status_code)
return response
def _get_entrys(self, soup: BeautifulSoup) -> Iterator[Entry]:
entry_items = soup.find_all("li", id="entry-item")
for entry in entry_items:
a = entry.find("a", class_="entry-date permalink", href=True)
yield Entry(
entry.attrs["data-id"],
entry.find("div", class_="content"),
entry.find("a", class_="entry-author").text,
a.text,
self.base_url + a["href"],
)
def search_topic(self, q: str) -> Topic:
response = self._get("/", {"q": q})
soup = BeautifulSoup(response.content, "html.parser")
h1 = soup.find("h1", id="title")
pager = soup.find("div", class_="pager")
return Topic(
h1.attrs["data-id"],
h1.attrs["data-title"],
int(pager.attrs["data-pagecount"]) if pager is not None else 0,
self.base_url + h1.find("a", href=True)["href"],
self._get_entrys(soup),
)
def get_topic(self, title: str, page: int = 1) -> Topic:
response = self._get(f"/{title}", {"p": page})
soup = BeautifulSoup(response.content, "html.parser")
h1 = soup.find("h1", id="title")
pager = soup.find("div", class_="pager")
return Topic(
h1.attrs["data-id"],
h1.attrs["data-title"],
int(pager.attrs["data-pagecount"]) if pager is not None else 0,
self.base_url + h1.find("a", href=True)["href"],
self._get_entrys(soup),
)
def get_entry(self, id: int) -> Topic:
response = self._get(f"/entry/{id}")
soup = BeautifulSoup(response.content, "html.parser")
h1 = soup.find("h1", id="title")
return Topic(
h1.attrs["data-id"],
h1.attrs["data-title"],
0,
self.base_url + h1.find("a", href=True)["href"],
self._get_entrys(soup),
)
def get_agenda(self) -> Iterator[Agenda]:
response = self._get()
soup = BeautifulSoup(response.content, "html.parser")
topic_list = soup.find("ul", class_="topic-list").find_all("a", href=True)
for topic in topic_list:
yield Agenda(
topic.contents[0],
"" if len(topic.contents) < 2 else topic.contents[1],
topic.has_attr("class"),
topic["href"],
)
def _unicode_tr(text: str) -> str:
for key, value in CHARMAP.items():
text = text.replace(key, value)
return text

@ -0,0 +1,7 @@
HOST = "0.0.0.0"
PORT = 3131
DEBUG = False
SECRET_KEY = "Some secret string here"
DEFAULT_THEME = "light"
DEFAULT_EKSI_BASE_URL = "https://eksisozluk.com"

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 B

@ -0,0 +1,174 @@
:root {
--text-color: #000000;
--title-color: #81C14B;
--background-color: #FFFFFF;
--entry-link-color: #81C14B;
--entry-background-color: #EEEEEE;
--form-text-color: #000000;
--form-border-color: #8F8F9D;
--form-input-background-color: #FFFFFF;
--form-button-background-color: #E9E9ED;
}
.dark {
--text-color: #E8E6E3;
--title-color: #81C14B;
--background-color: #181A1B;
--entry-link-color: #81C14B;
--entry-background-color: #222426;
--form-text-color: #D8D4CF;
--form-border-color: #373C3E;
--form-input-background-color: #232627;
--form-button-background-color: #232627;
}
body {
font-family: sans-serif;
color: var(--text-color);
background-color: var(--background-color);
}
a {
color: var(--text-color);
}
header div {
top: 0;
right: 0;
position: absolute;
}
header a {
display: inline-block;
padding: 0.5rem 0.2rem;
}
main {
width: 80%;
margin: 0 auto;
}
nav {
width: 50%;
margin: 0 auto;
text-align: center;
}
nav a {
padding: 0.5rem;
display: inline-block;
}
.title {
text-align: center;
font-weight: normal;
}
h1 a {
color: black;
text-decoration: none;
}
small {
opacity: 0.5;
color: var(--text-color);
}
.ozgur {
color: var(--text-color);
}
.sozluk {
color: var(--title-color);
}
.description {
text-align: center;
}
.topic {
width: 70%;
margin: 0 auto;
}
.topic > .entry {
margin: 1rem;
padding: 0.5rem;
background-color: var(--entry-background-color);
}
.content {
margin: 0;
padding: 0;
}
.content .b, .content .url {
text-decoration: none;
color: var(--entry-link-color);
}
.entry-datetime {
opacity: 0.5;
text-decoration: none;
}
.info {
margin: 1rem;
display: flex;
padding-top: 1rem;
align-items: center;
justify-content: space-between;
}
.info > div p {
margin: 0;
padding: 0;
}
.settings {
width: 35%;
margin: 0 auto;
padding-top: 3rem;
}
form {
padding-top: 0.5rem;
}
form .settings-group {
display: flex;
align-items: center;
padding-bottom: 0.5rem;
}
form .settings-group > :last-child {
margin-left: auto;
}
form input[type=text], form select, form button {
padding: 0.5rem;
color: var(--form-text-color);
border: 1px solid var(--form-border-color);
}
form input[type=text] {
background-color: var(--form-input-background-color);
}
form select, form button {
background-color: var(--form-button-background-color);
}
@media only screen and (max-width: 768px) {
main, nav, .topic, .settings {
width: 100%;
}
nav, .title, .description {
text-align: left;
}
form input[type=text], form select {
width: 50%;
}
}

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block title %}özgürsözlük - 404{% endblock %}
{% block main %}
<nav>{% include "navigation.html" %}</nav>
<div class="topic" style="text-align: center; padding-top: 3rem;">
<p>Page not found!</p>
</div>
{% endblock %}

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html class="{{ 'dark' if request.cookies.get('theme') == 'dark' }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="{{ url_for('static', filename='favicon.png') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<title>{% block title %}{% endblock %}</title>
</head>
<body>
<header>
<div>
<a href="{{ url_for('settings') }}">settings</a>
<a href="{{ source }}" target="_blank">source code</a>
</div>
</header>
<main>
<h1 class="title">
<a href="/">
<span class="ozgur">özgür</span><span class="sozluk">sözlük</span>
</a>
</h1>
<p class="description">{{ description }}</p>
{% block main %}{% endblock %}
</main>
</body>
</html>

@ -0,0 +1,27 @@
{% extends "base.html" %}
{% block title %}özgürsözlük - {{ description }}{% endblock %}
{% block main %}
<nav>{% include "navigation.html" %}</nav>
<div class="topic">
<div class="info">
<div><p>gündem</p></div>
</div>
{% for topic in agenda %}
<div class="entry">
<div style="display: flex; justify-content: space-between;">
<div>
<a style="text-decoration: none;" href="{{ topic.permalink }}">
{{ topic.title }}
</a>
</div>
<div>
{% if topic.pinned %}
<small style="opacity: 0.5;">pinned</small>
{% endif %}
{{ topic.views|safe }}
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}

@ -0,0 +1,4 @@
<form method="POST" action="/">
<input type="text" placeholder="search topic... e.g: ataturk" name="q">
<button type="submit">go</button>
</form>

@ -0,0 +1,36 @@
{% extends "base.html" %}
{% block title %}özgürsözlük - settings{% endblock %}
{% block main %}
<div class="settings">
<form method="POST" action="/settings">
<div class="settings-group">
theme:
<select name="theme">
{% if theme == 'light' %}
<option value="light" selected>light</option>
<option value="dark">dark</option>
{% else %}
<option value="light">light</option>
<option value="dark" selected>dark</option>
{% endif %}
</select>
</div>
<div class="settings-group">
ekşi base url:
<select name="eksi_base_url">
{% if eksi_base_url == 'https://eksisozluk.com' %}
<option value="https://eksisozluk.com" selected>eksisozluk.com</option>
<option value="https://eksisozluk2023.com">eksisozluk2023.com</option>
{% else %}
<option value="https://eksisozluk.com">eksisozluk.com</option>
<option value="https://eksisozluk2023.com" selected>eksisozluk2023.com</option>
{% endif %}
</select>
</div>
<div style="text-align: right;">
<button type="submit">save</button>
</div>
</form>
<div style="text-align: center; padding-top: 3rem;">v{{ version }}</div>
</div>
{% endblock %}

@ -0,0 +1,30 @@
{% extends "base.html" %}
{% block title %}özgürsözlük - {{ topic.title }}{% endblock %}
{% block main %}
<nav>{% include "navigation.html" %}</nav>
<div class="topic">
<div class="info">
<div><p>{{ topic.title }}</p></div>
<div>
{% if p > 1 %}
<a href="/{{ topic.title_id() }}?p={{ p-1 }}">previous</a>
{% if p < topic.pagecount %}/{% endif %}
{% endif %}
{% if p < topic.pagecount %}
<a href="/{{ topic.title_id() }}?p={{ p+1 }}">next</a>
{% endif %}
{% if topic.pagecount > 1 %}
-
<a href="/{{ topic.title_id() }}?p={{ topic.pagecount }}">last page</a>
{% endif %}
</div>
</div>
{% for entry in topic.entrys %}
<div class="entry">
{{ entry.content|safe }}
</br>
<div style="text-align: right;"><small>{{ entry.datetime }}</small></div>
</div>
{% endfor %}
</div>
{% endblock %}

@ -0,0 +1,82 @@
import flask
import ozgursozluk
from ozgursozluk.api import Eksi
from ozgursozluk.config import DEFAULT_THEME, DEFAULT_EKSI_BASE_URL
eksi = Eksi()
@ozgursozluk.app.context_processor
def context_processor():
return dict(
version=ozgursozluk.__version__,
source=ozgursozluk.__source__,
description=ozgursozluk.__description__,
)
@ozgursozluk.app.route("/", methods=["GET", "POST"])
def index():
q = flask.request.args.get("q", default=None, type=str)
if q is not None:
return flask.redirect(flask.url_for("search", q=q))
if flask.request.method == "POST":
return flask.redirect(flask.url_for("search", q=flask.request.form["q"]))
eksi.base_url = flask.request.cookies.get("eksi_base_url", DEFAULT_EKSI_BASE_URL)
agenda = eksi.get_agenda()
return flask.render_template("index.html", agenda=agenda)
@ozgursozluk.app.route("/search/<q>")
def search(q: str):
eksi.base_url = flask.request.cookies.get("eksi_base_url", DEFAULT_EKSI_BASE_URL)
return flask.render_template("topic.html", topic=eksi.search_topic(q), p=1)
@ozgursozluk.app.route("/<title>")
def topic(title: str):
p = flask.request.args.get("p", default=1, type=int)
eksi.base_url = flask.request.cookies.get("eksi_base_url", DEFAULT_EKSI_BASE_URL)
result = eksi.get_topic(title, p)
return flask.render_template("topic.html", topic=result, p=p)
@ozgursozluk.app.route("/entry/<int:id>")
def entry(id: int):
eksi.base_url = flask.request.cookies.get("eksi_base_url", DEFAULT_EKSI_BASE_URL)
return flask.render_template("topic.html", topic=eksi.get_entry(id), p=1)
@ozgursozluk.app.route("/settings", methods=["GET", "POST"])
def settings():
theme = flask.request.cookies.get("theme", DEFAULT_THEME)
eksi_base_url = flask.request.cookies.get("eksi_base_url", DEFAULT_EKSI_BASE_URL)
if flask.request.method == "POST":
response = flask.make_response(
flask.render_template(
"settings.html", theme=theme, eksi_base_url=eksi_base_url
)
)
response.set_cookie("theme", flask.request.form["theme"])
response.set_cookie("eksi_base_url", flask.request.form["eksi_base_url"])
return response
return flask.render_template(
"settings.html", theme=theme, eksi_base_url=eksi_base_url
)
@ozgursozluk.app.errorhandler(404)
def page_not_found(e):
return flask.render_template("404.html"), 404

@ -0,0 +1,4 @@
flask==2.2.3
requests==2.28.2
fake-useragent==1.1.3
beautifulsoup4==4.12.2

@ -0,0 +1,8 @@
#!/usr/bin/python3
from ozgursozluk import app
from ozgursozluk.config import HOST, PORT, DEBUG
if __name__ == "__main__":
app.run(host=HOST, port=PORT, debug=DEBUG)

@ -0,0 +1,22 @@
import unittest
from ozgursozluk.api import Eksi
eksi = Eksi()
topic = eksi.search_topic("linux")
class TestTopic(unittest.TestCase):
def test_id(self):
self.assertEqual(topic.id, "32084")
def test_title(self):
self.assertEqual(topic.title, "linux")
def test_permalink(self):
self.assertEqual(topic.permalink, "https://eksisozluk2023.com/linux--32084")
if __name__ == "__main__":
unittest.main()
Loading…
Cancel
Save