feat: initial setup of Wikimore Flask app with basic features

Added initial setup for "Wikimore", a simple frontend for Wikimedia projects using Flask. The app includes the following features:

- Multi-language and multi-project support
- Search functionality with results displayed
- Proxy support for Wikimedia images
- Basic structure and templates (home, article, search results)

Configured appropriate .gitignore and .vscode settings for development. Licensed under MIT License.
This commit is contained in:
Kumi 2024-07-11 12:25:19 +02:00
commit c436885cbc
Signed by: kumi
GPG key ID: ECBCC9082395383F
11 changed files with 337 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
venv/
.venv/
__pycache__/
*.pyc

26
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,26 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Flask",
"type": "debugpy",
"request": "launch",
"module": "flask",
"env": {
"FLASK_APP": "app.py",
"FLASK_DEBUG": "1",
},
"args": [
"run",
"--no-debugger",
"--no-reload",
"--port=8109"
],
"jinja": true,
"autoStartBrowser": false
}
]
}

5
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,5 @@
{
"files.associations": {
"*.html": "jinja-html"
}
}

19
LICENSE Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) 2024 Private.coffee Team <support@private.coffee>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

27
README.md Normal file
View file

@ -0,0 +1,27 @@
# Wikimore - A simple frontend for Wikimedia projects
Wikimore is a simple frontend for Wikimedia projects. It uses the MediaWiki API to fetch data from Wikimedia projects and display it in a user-friendly way. It is built using Flask.
This project is still in development and more features will be added in the future. It is useful for anyone who wants to access Wikimedia projects with a more basic frontend, or to provide access to Wikimedia projects to users who cannot access them directly, for example due to state censorship.
## Features
- Multi-language support (currently English and German, more can and will be added)
- Multi-project support (currently Wikipedia and Wiktionary, more can and will be added)
- Search functionality
- Proxy support for Wikimedia images
## Installation
1. Clone the repository
2. Install the required packages using `pip install -r requirements.txt`
3. Run the app using `python app.py`
## Usage
1. Open your browser and navigate to `http://localhost:5000`
2. Use the search bar to search for articles on a given Wikimedia project, in a given language
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

119
app.py Normal file
View file

@ -0,0 +1,119 @@
from flask import Flask, render_template, request, redirect, url_for
import urllib.request
from urllib.parse import urlencode
from html import escape
import json
from bs4 import BeautifulSoup
app = Flask(__name__)
WIKIMEDIA_PROJECTS = {
"wikipedia": "wikipedia.org",
"wiktionary": "wiktionary.org",
# TODO: Add more Wikimedia projects
}
def get_proxy_url(url):
if url.startswith("//"):
url = "https:" + url
if not url.startswith("https://upload.wikimedia.org/"):
return url
return f"/proxy?{urlencode({'url': url})}"
@app.route("/proxy")
def proxy():
url = request.args.get("url")
with urllib.request.urlopen(url) as response:
data = response.read()
return data
@app.route("/")
def home():
return render_template("home.html")
@app.route("/search", methods=["GET", "POST"])
def search():
if request.method == "POST":
query = request.form["query"]
lang = request.form["lang"]
project = request.form["project"]
return redirect(
url_for("search_results", project=project, lang=lang, query=query)
)
return render_template("search.html")
@app.route("/<project>/<lang>/wiki/<title>")
def wiki_article(project, lang, title):
base_url = WIKIMEDIA_PROJECTS.get(project, "wikipedia.org")
url = f"https://{lang}.{base_url}/w/api.php?action=query&format=json&titles={escape(title.replace(" ", "_"), True)}&prop=revisions&rvprop=content&rvparse=1"
with urllib.request.urlopen(url) as response:
data = json.loads(response.read().decode())
pages = data["query"]["pages"]
article_html = next(iter(pages.values()))["revisions"][0]["*"]
soup = BeautifulSoup(article_html, "html.parser")
for a in soup.find_all("a", href=True):
href = a["href"]
if href.startswith("/wiki/"):
a["href"] = f"/{project}/{lang}{href}"
elif href.startswith("//") or href.startswith("https://"):
parts = href.split("/")
if len(parts) > 4:
target_project = ".".join(parts[2].split(".")[1:])
target_lang = parts[2].split(".")[0]
target_title = "/".join(parts[4:])
if target_project in WIKIMEDIA_PROJECTS.values():
target_project = list(WIKIMEDIA_PROJECTS.keys())[
list(WIKIMEDIA_PROJECTS.values()).index(target_project)
]
a["href"] = f"/{target_project}/{target_lang}/wiki/{target_title}"
for span in soup.find_all("span", class_="mw-editsection"):
span.decompose()
for style in soup.find_all("style"):
style.decompose()
for img in soup.find_all("img"):
img["src"] = get_proxy_url(img["src"])
for li in soup.find_all("li"):
# If "nv-view", "nv-talk", "nv-edit" classes are on the li element, remove it
if any(cls in li.get("class", []) for cls in ["nv-view", "nv-talk", "nv-edit"]):
li.decompose()
processed_html = str(soup)
return render_template("article.html", title=title, content=processed_html)
@app.route("/<project>/<lang>/search/<query>")
def search_results(project, lang, query):
base_url = WIKIMEDIA_PROJECTS.get(project, "wikipedia.org")
url = f"https://{lang}.{base_url}/w/api.php?action=query&format=json&list=search&srsearch={query}"
with urllib.request.urlopen(url) as response:
data = json.loads(response.read().decode())
search_results = data["query"]["search"]
return render_template(
"search_results.html",
query=query,
search_results=search_results,
project=project,
lang=lang,
)
@app.route("/<project>/<lang>/wiki/Special:Search/<query>")
def search_redirect(project, lang, query):
return redirect(url_for("search_results", project=project, lang=lang, query=query))
if __name__ == "__main__":
app.run(debug=True)

2
requirements.txt Normal file
View file

@ -0,0 +1,2 @@
flask
bs4

6
templates/article.html Normal file
View file

@ -0,0 +1,6 @@
{% extends "base.html" %}
{% block content %}
<h1>{{ title }}</h1>
<p>{{ content|safe }}</p>
{% endblock %}

110
templates/base.html Normal file
View file

@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}{% if title %} &dash; {% endif %}Wikimore</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f8f9fa;
}
#header {
background-color: #ffffff;
border-bottom: 1px solid #a2a9b1;
padding: 10px;
display: flex;
align-items: center;
justify-content: space-between;
}
#header h1 {
margin: 0;
font-size: 1.5em;
}
#header h1 a {
text-decoration: none;
color: #333;
}
#search-form {
margin: 0;
padding: 0;
display: flex;
align-items: center;
}
#search-form input[type="text"] {
width: 300px;
padding: 5px;
}
#search-form select {
padding: 5px;
}
#search-form button {
padding: 5px 10px;
}
#content {
padding: 20px;
}
h1 {
font-size: 2em;
padding-bottom: 10px;
margin-bottom: 20px;
}
.sidebar {
float: right;
width: 250px;
margin-left: 20px;
background-color: #ffffff;
border: 1px solid #a2a9b1;
padding: 10px;
box-sizing: border-box;
}
.toc {
list-style-type: none;
padding: 0;
}
.toc li {
margin-bottom: 5px;
}
.toc a {
text-decoration: none;
color: #333;
}
.toc a:hover {
text-decoration: underline;
}
#footer {
background-color: #ffffff;
border-top: 1px solid #a2a9b1;
padding: 10px;
text-align: center;
}
</style>
</head>
<body>
<div id="header">
<h1><a href="/">Wikimore</a></h1>
<form id="search-form" action="{{ url_for('search') }}" method="post">
<select name="project" id="project">
<option value="wikipedia">Wikipedia</option>
<option value="wiktionary">Wiktionary</option>
<!-- TODO: Add more projects -->
</select>
<select name="lang" id="lang">
<option value="en">English</option>
<option value="de">German</option>
<!-- TODO: Add more languages -->
</select>
<input type="text" name="query" id="query" placeholder="Search Wikipedia" required>
<button type="submit">Search</button>
</form>
</div>
<div id="content">
{% block content %}{% endblock %}
</div>
<div id="footer">
<p>Brought to you by <a href="https://git.private.coffee/privatecoffee/wikimore">Wikimore</a></p>
</div>
</body>
</html>

6
templates/home.html Normal file
View file

@ -0,0 +1,6 @@
{% extends "base.html" %}
{% block content %}
<h1>Welcome to Wikimore</h1>
<p>Use the search form above to find articles on Wikipedia.</p>
{% endblock %}

View file

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% block content %}
<h1>Search Results for "{{ query }}"</h1>
<ul>
{% for result in search_results %}
<li>
<a href="{{ url_for('wiki_article', project=project, lang=lang, title=result['title']) }}">{{ result['title'] }}</a>
<p>{{ result['snippet']|safe }}</p>
</li>
{% endfor %}
</ul>
{% endblock %}