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