diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..008af35 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__ +*.pyc +*.pyo diff --git a/README.md b/README.md index e790db1..571b699 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,28 @@ WantedBy=multi-user.target ``` +## Localisation + +The UI strings can be localised by setting the `LOCALE` environment variable before starting the app. Locale files live in `app/locales/.json`. + +Built-in locales: + +- `en` (default) +- `de` +- `fr` + +Examples: + +```sh +# from app +LOCALE=de python portable.py + +# or with gunicorn +LOCALE=fr python -m gunicorn 'portable:app' +``` + +To add a new locale, copy `app/locales/en.json` to a new file such as `app/locales/es.json` and translate the values. + ## Screenshots ### Step 1 diff --git a/app/__pycache__/portable.cpython-312.pyc b/app/__pycache__/portable.cpython-312.pyc new file mode 100644 index 0000000..513563d Binary files /dev/null and b/app/__pycache__/portable.cpython-312.pyc differ diff --git a/app/locales/de.json b/app/locales/de.json new file mode 100644 index 0000000..cfe7120 --- /dev/null +++ b/app/locales/de.json @@ -0,0 +1,27 @@ +{ + "heading": "Website-Link eingeben", + "label": "Link der Website, deren Paywall Sie entfernen möchten:", + "submit": "Absenden", + "toggle_dark_mode": "Dunkelmodus umschalten", + "status_heading": "Artikel wird geladen", + "error_title": "Etwas ist schiefgelaufen", + "retry_button": "Andere URL versuchen", + "elapsed_template": "{seconds}s vergangen", + "connection_lost_error": "Verbindung zum Server verloren. Das Laden der Seite dauert möglicherweise zu lange.", + "step_connect": "Verbindung zur Website wird hergestellt...", + "step_fetch": "Seiteninhalt wird heruntergeladen...", + "step_detect": "Es wird nach Anti-Bot-Schutz gesucht...", + "step_fallback_freedium": "Freedium wird versucht (Medium-Umgehung)...", + "step_fallback_org": "Es wird ein archive.org-Schnappschuss versucht...", + "step_fallback_ph": "Es wird ein archive.today-/archive.ph-Schnappschuss versucht...", + "step_process": "Artikel wird verarbeitet...", + "step_cleanup": "Ansicht wird bereinigt und vorbereitet...", + "step_done": "Fertig!", + "invalid_url": "Ungültige URL", + "medium_challenge_error": "Medium hat eine Anti-Bot-Prüfung ausgeliefert und Freedium / archive.org / archive.today lieferten kein Ergebnis. Der Artikel ist möglicherweise noch zu neu, um archiviert worden zu sein.", + "challenge_error": "Die Website hat eine Anti-Bot-Prüfung (Cloudflare oder ähnlich) ausgeliefert und es wurde kein brauchbarer Schnappschuss auf archive.org oder archive.today gefunden. Diese Website blockiert aktiv automatisierte Anfragen.", + "timeout_error": "Die Website hat zu lange zum Antworten gebraucht (30s-Timeout). Bitte versuchen Sie es später erneut.", + "connection_error": "Die Verbindung zur Website konnte nicht hergestellt werden. Prüfen Sie die URL und versuchen Sie es erneut.", + "request_error": "Die Seite konnte nicht geladen werden.", + "unexpected_error": "Unerwarteter Fehler beim Laden der Seite." +} diff --git a/app/locales/en.json b/app/locales/en.json new file mode 100644 index 0000000..bd5c5b6 --- /dev/null +++ b/app/locales/en.json @@ -0,0 +1,27 @@ +{ + "heading": "Enter Website Link", + "label": "Link of the website you want to remove paywall for:", + "submit": "Submit", + "toggle_dark_mode": "Toggle Dark Mode", + "status_heading": "Fetching Article", + "error_title": "Something went wrong", + "retry_button": "Try Another URL", + "elapsed_template": "{seconds}s elapsed", + "connection_lost_error": "Connection to server lost. The page may be taking too long to load.", + "step_connect": "Connecting to website...", + "step_fetch": "Downloading page content...", + "step_detect": "Checking for anti-bot challenges...", + "step_fallback_freedium": "Trying Freedium (Medium bypass)...", + "step_fallback_org": "Trying archive.org snapshot...", + "step_fallback_ph": "Trying archive.today / archive.ph snapshot...", + "step_process": "Processing article...", + "step_cleanup": "Cleaning up & preparing view...", + "step_done": "Done!", + "invalid_url": "Invalid URL", + "medium_challenge_error": "Medium served an anti-bot challenge and Freedium / archive.org / archive.today all came back empty. The article may be too new to have been archived yet.", + "challenge_error": "The site served an anti-bot challenge (Cloudflare or similar) and no usable snapshot was found on archive.org or archive.today. This site is actively blocking automated requests.", + "timeout_error": "The website took too long to respond (30s timeout). Try again later.", + "connection_error": "Could not connect to the website. Check the URL and try again.", + "request_error": "Failed to fetch the page.", + "unexpected_error": "Unexpected error while fetching the page." +} diff --git a/app/locales/fr.json b/app/locales/fr.json new file mode 100644 index 0000000..4078415 --- /dev/null +++ b/app/locales/fr.json @@ -0,0 +1,27 @@ +{ + "heading": "Entrez le lien du site web", + "label": "Lien du site web dont vous souhaitez supprimer le paywall :", + "submit": "Soumettre", + "toggle_dark_mode": "Basculer en mode sombre", + "status_heading": "Récupération de l'article", + "error_title": "Une erreur s'est produite", + "retry_button": "Essayer une autre URL", + "elapsed_template": "{seconds}s écoulées", + "connection_lost_error": "Connexion au serveur perdue. Le chargement de la page prend peut-être trop de temps.", + "step_connect": "Connexion au site web...", + "step_fetch": "Téléchargement du contenu de la page...", + "step_detect": "Vérification des protections anti-bot...", + "step_fallback_freedium": "Essai de Freedium (contournement de Medium)...", + "step_fallback_org": "Essai d'un instantané archive.org...", + "step_fallback_ph": "Essai d'un instantané archive.today / archive.ph...", + "step_process": "Traitement de l'article...", + "step_cleanup": "Nettoyage et préparation de l'affichage...", + "step_done": "Terminé !", + "invalid_url": "URL invalide", + "medium_challenge_error": "Medium a présenté une protection anti-bot et Freedium / archive.org / archive.today n'ont rien renvoyé. L'article est peut-être trop récent pour avoir déjà été archivé.", + "challenge_error": "Le site a présenté une protection anti-bot (Cloudflare ou similaire) et aucun instantané exploitable n'a été trouvé sur archive.org ou archive.today. Ce site bloque activement les requêtes automatisées.", + "timeout_error": "Le site web a mis trop de temps à répondre (délai de 30 s). Veuillez réessayer plus tard.", + "connection_error": "Impossible de se connecter au site web. Vérifiez l'URL et réessayez.", + "request_error": "Impossible de récupérer la page.", + "unexpected_error": "Erreur inattendue lors du chargement de la page." +} diff --git a/app/portable.py b/app/portable.py index 48452d5..6924f5a 100644 --- a/app/portable.py +++ b/app/portable.py @@ -1,3 +1,4 @@ +import json import os import flask import re @@ -5,11 +6,79 @@ import requests import uuid import threading import time +from functools import lru_cache from urllib.parse import urljoin, urlparse, quote from flask import request, Response from bs4 import BeautifulSoup app = flask.Flask(__name__) + +_DEFAULT_STRINGS = { + "heading": "Enter Website Link", + "label": "Link of the website you want to remove paywall for:", + "submit": "Submit", + "toggle_dark_mode": "Toggle Dark Mode", + "status_heading": "Fetching Article", + "error_title": "Something went wrong", + "retry_button": "Try Another URL", + "elapsed_template": "{seconds}s elapsed", + "connection_lost_error": "Connection to server lost. The page may be taking too long to load.", + "step_connect": "Connecting to website...", + "step_fetch": "Downloading page content...", + "step_detect": "Checking for anti-bot challenges...", + "step_fallback_freedium": "Trying Freedium (Medium bypass)...", + "step_fallback_org": "Trying archive.org snapshot...", + "step_fallback_ph": "Trying archive.today / archive.ph snapshot...", + "step_process": "Processing article...", + "step_cleanup": "Cleaning up & preparing view...", + "step_done": "Done!", + "invalid_url": "Invalid URL", + "medium_challenge_error": ( + "Medium served an anti-bot challenge and Freedium / archive.org / " + "archive.today all came back empty. The article may be too new to " + "have been archived yet." + ), + "challenge_error": ( + "The site served an anti-bot challenge (Cloudflare or similar) " + "and no usable snapshot was found on archive.org or archive.today. " + "This site is actively blocking automated requests." + ), + "timeout_error": "The website took too long to respond (30s timeout). Try again later.", + "connection_error": "Could not connect to the website. Check the URL and try again.", + "request_error": "Failed to fetch the page.", + "unexpected_error": "Unexpected error while fetching the page.", +} + + +def get_locale(): + locale = os.environ.get("LOCALE", "en") + if not re.match(r"^[a-zA-Z0-9_-]+$", locale): + locale = "en" + return locale + + +@lru_cache(maxsize=None) +def load_strings_for_locale(locale): + strings = dict(_DEFAULT_STRINGS) + locale_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "locales") + locale_file = os.path.join(locale_dir, f"{locale}.json") + + if locale != "en": + try: + with open(locale_file, "r", encoding="utf-8") as file: + strings.update(json.load(file)) + except (FileNotFoundError, json.JSONDecodeError, OSError): + strings = dict(_DEFAULT_STRINGS) + locale = "en" + + strings["lang"] = locale + return strings + + +def load_strings(): + return dict(load_strings_for_locale(get_locale())) + + googlebot_headers = { "User-Agent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.119 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" } @@ -17,9 +86,15 @@ googlebot_headers = { jobs = {} jobs_lock = threading.Lock() + +class UserFacingError(Exception): + def __init__(self, user_message): + super().__init__(user_message) + self.user_message = user_message + html = """ - + @@ -249,21 +324,21 @@ html = """
- +
-

Enter Website Link

- +

{{ heading }}

+ - +