diff --git a/README.md b/README.md index 281d177..571b699 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ 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 `/home/runner/work/13ft/13ft/app/locales/.json`. +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: @@ -179,14 +179,14 @@ Built-in locales: Examples: ```sh -# from /home/runner/work/13ft/13ft/app +# from app LOCALE=de python portable.py # or with gunicorn LOCALE=fr python -m gunicorn 'portable:app' ``` -To add a new locale, copy `/home/runner/work/13ft/13ft/app/locales/en.json` to a new file such as `/home/runner/work/13ft/13ft/app/locales/es.json` and translate the values. +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 diff --git a/app/__pycache__/portable.cpython-312.pyc b/app/__pycache__/portable.cpython-312.pyc index 8ee6010..513563d 100644 Binary files a/app/__pycache__/portable.cpython-312.pyc and b/app/__pycache__/portable.cpython-312.pyc differ diff --git a/app/locales/de.json b/app/locales/de.json index b184ca0..cfe7120 100644 --- a/app/locales/de.json +++ b/app/locales/de.json @@ -22,6 +22,6 @@ "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": "Seite konnte nicht geladen werden: {error}", - "unexpected_error": "Unerwarteter Fehler: {error}" + "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 index 0907a34..bd5c5b6 100644 --- a/app/locales/en.json +++ b/app/locales/en.json @@ -22,6 +22,6 @@ "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: {error}", - "unexpected_error": "Unexpected error: {error}" + "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 index 3aba146..4078415 100644 --- a/app/locales/fr.json +++ b/app/locales/fr.json @@ -22,6 +22,6 @@ "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": "Échec de récupération de la page : {error}", - "unexpected_error": "Erreur inattendue : {error}" + "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 3208896..6924f5a 100644 --- a/app/portable.py +++ b/app/portable.py @@ -6,6 +6,7 @@ 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 @@ -44,16 +45,20 @@ _DEFAULT_STRINGS = { ), "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: {error}", - "unexpected_error": "Unexpected error: {error}", + "request_error": "Failed to fetch the page.", + "unexpected_error": "Unexpected error while fetching the page.", } -def load_strings(): +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") @@ -63,10 +68,17 @@ def load_strings(): 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)" } @@ -74,6 +86,12 @@ 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 = """ @@ -692,7 +710,7 @@ def bypass_paywall(url, strings, job_id=None): msg = strings["medium_challenge_error"] else: msg = strings["challenge_error"] - raise RuntimeError(msg) + raise UserFacingError(msg) set_step(job_id, 'process') result = add_base_tag(html_text, final_url) @@ -717,15 +735,15 @@ def fetch_worker(job_id, url, strings): jobs[job_id]['step'] = 'error' except requests.exceptions.RequestException as e: with jobs_lock: - jobs[job_id]['error'] = strings["request_error"].format(error=e) + jobs[job_id]['error'] = strings["request_error"] jobs[job_id]['step'] = 'error' - except RuntimeError as e: + except UserFacingError as error: with jobs_lock: - jobs[job_id]['error'] = str(e) + jobs[job_id]['error'] = error.user_message jobs[job_id]['step'] = 'error' except Exception as e: with jobs_lock: - jobs[job_id]['error'] = strings["unexpected_error"].format(error=e) + jobs[job_id]['error'] = strings["unexpected_error"] jobs[job_id]['step'] = 'error' @@ -782,10 +800,16 @@ def show_article(): strings = load_strings() try: return bypass_paywall(link, strings) - except requests.exceptions.RequestException as e: - return strings["request_error"].format(error=e), 400 - except Exception as e: - raise e + except requests.exceptions.Timeout: + return strings["timeout_error"], 400 + except requests.exceptions.ConnectionError: + return strings["connection_error"], 400 + except requests.exceptions.RequestException: + return strings["request_error"], 400 + except UserFacingError as error: + return error.user_message, 400 + except Exception: + return strings["unexpected_error"], 500 @app.route("/", defaults={"path": ""}) @@ -798,10 +822,16 @@ def get_article(path): actual_url = "https://" + parts[4].lstrip("/") try: return bypass_paywall(actual_url, strings) - except requests.exceptions.RequestException as e: - return strings["request_error"].format(error=e), 400 - except Exception as e: - raise e + except requests.exceptions.Timeout: + return strings["timeout_error"], 400 + except requests.exceptions.ConnectionError: + return strings["connection_error"], 400 + except requests.exceptions.RequestException: + return strings["request_error"], 400 + except UserFacingError as error: + return error.user_message, 400 + except Exception: + return strings["unexpected_error"], 500 else: return strings["invalid_url"], 400