parent
0d1bb2c019
commit
fcc37b18ad
@ -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" |
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…
Reference in new issue