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:
commit
c436885cbc
11 changed files with 337 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
26
.vscode/launch.json
vendored
Normal file
26
.vscode/launch.json
vendored
Normal 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
5
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"files.associations": {
|
||||||
|
"*.html": "jinja-html"
|
||||||
|
}
|
||||||
|
}
|
19
LICENSE
Normal file
19
LICENSE
Normal 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
27
README.md
Normal 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
119
app.py
Normal 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
2
requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
flask
|
||||||
|
bs4
|
6
templates/article.html
Normal file
6
templates/article.html
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
<p>{{ content|safe }}</p>
|
||||||
|
{% endblock %}
|
110
templates/base.html
Normal file
110
templates/base.html
Normal 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 %} ‐ {% 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
6
templates/home.html
Normal 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 %}
|
13
templates/search_results.html
Normal file
13
templates/search_results.html
Normal 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 %}
|
Loading…
Reference in a new issue