offline libs
This commit is contained in:
@@ -9,6 +9,9 @@ PYTORRENT_GEOIP_DB=data/GeoLite2-City.mmdb
|
||||
PYTORRENT_ALLOW_UNSAFE_WERKZEUG=0
|
||||
PYTORRENT_SCGI_RETRIES=8
|
||||
|
||||
# css/js libs
|
||||
PYTORRENT_USE_OFFLINE_LIBS=false
|
||||
|
||||
# python -m pytorrent.cli reset-password admin new_Pass
|
||||
PYTORRENT_AUTH_ENABLE=false
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -37,3 +37,4 @@ data/*
|
||||
logs/*
|
||||
|
||||
todo.txt
|
||||
pytorrent/static/libs/*
|
||||
@@ -5,6 +5,8 @@ python3 -m venv venv
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
cp -n .env.example .env || true
|
||||
grep -q '^PYTORRENT_USE_OFFLINE_LIBS=' .env || echo 'PYTORRENT_USE_OFFLINE_LIBS=true' >> .env
|
||||
./scripts/download_frontend_libs.py
|
||||
mkdir -p data
|
||||
chmod 755 data
|
||||
./scripts/download_geoip.sh data/GeoLite2-City.mmdb
|
||||
|
||||
@@ -16,6 +16,7 @@ from .config import (
|
||||
SOCKETIO_CORS_ALLOWED_ORIGINS,
|
||||
)
|
||||
from .db import init_db
|
||||
from .services.frontend_assets import asset_path, bootstrap_css_path, validate_offline_assets
|
||||
from .utils import file_md5
|
||||
|
||||
socketio = SocketIO(cors_allowed_origins=SOCKETIO_CORS_ALLOWED_ORIGINS, ping_timeout=30, async_mode="threading")
|
||||
@@ -56,6 +57,7 @@ def register_error_pages(app: Flask) -> None:
|
||||
|
||||
|
||||
def create_app() -> Flask:
|
||||
validate_offline_assets()
|
||||
app = Flask(__name__)
|
||||
if PROXY_FIX_ENABLE:
|
||||
app.wsgi_app = ProxyFix(
|
||||
@@ -88,7 +90,21 @@ def create_app() -> Flask:
|
||||
return url_for("static", filename=filename, v=version)
|
||||
except OSError:
|
||||
return url_for("static", filename=filename)
|
||||
return {"static_url": static_url}
|
||||
|
||||
def frontend_asset_url(key: str) -> str:
|
||||
# Notatka: helper przełącza szablony między CDN i lokalnymi plikami bez duplikowania logiki.
|
||||
path = asset_path(key)
|
||||
return path if path.startswith("http") else static_url(path)
|
||||
|
||||
def bootstrap_theme_url(theme: str | None = None) -> str:
|
||||
path = bootstrap_css_path(theme)
|
||||
return path if path.startswith("http") else static_url(path)
|
||||
|
||||
return {
|
||||
"static_url": static_url,
|
||||
"frontend_asset_url": frontend_asset_url,
|
||||
"bootstrap_theme_url": bootstrap_theme_url,
|
||||
}
|
||||
|
||||
@app.after_request
|
||||
def cache_headers(response):
|
||||
|
||||
@@ -25,6 +25,8 @@ if not DB_PATH.is_absolute():
|
||||
HOST = os.getenv("PYTORRENT_HOST", "0.0.0.0")
|
||||
PORT = int(os.getenv("PYTORRENT_PORT", "8090"))
|
||||
DEBUG = _env_bool("PYTORRENT_DEBUG", False)
|
||||
# Notatka: tryb offline wymusza lokalne JS/CSS i wyłącza zależność od CDN.
|
||||
USE_OFFLINE_LIBS = _env_bool("PYTORRENT_USE_OFFLINE_LIBS", False)
|
||||
# Note: Optional authentication remains disabled unless explicitly enabled in .env.
|
||||
AUTH_ENABLE = _env_bool("PYTORRENT_AUTH_ENABLE", False)
|
||||
if AUTH_ENABLE and (not _SECRET_KEY_ENV or SECRET_KEY == "dev-change-me"):
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from flask import Blueprint, render_template, jsonify, Response, request, redirect, url_for, abort
|
||||
from ..services.preferences import get_preferences, list_profiles, active_profile, BOOTSTRAP_THEMES, FONT_FAMILIES, bootstrap_css_url
|
||||
from ..services.preferences import get_preferences, list_profiles, active_profile, BOOTSTRAP_THEMES, FONT_FAMILIES
|
||||
from ..services import auth
|
||||
from ..services.frontend_assets import asset_path
|
||||
|
||||
bp = Blueprint("main", __name__)
|
||||
|
||||
|
||||
def _asset_url(key: str) -> str:
|
||||
# Notatka: API docs korzysta z tego samego przełącznika CDN/offline co reszta aplikacji.
|
||||
path = asset_path(key)
|
||||
return path if path.startswith("http") else url_for("static", filename=path)
|
||||
|
||||
|
||||
|
||||
@bp.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
# Note: When optional authentication is disabled, /login is intentionally unavailable.
|
||||
@@ -39,7 +47,6 @@ def index():
|
||||
active_profile=active_profile(),
|
||||
bootstrap_themes=BOOTSTRAP_THEMES,
|
||||
font_families=FONT_FAMILIES,
|
||||
bootstrap_css_url=bootstrap_css_url((prefs or {}).get("bootstrap_theme")),
|
||||
auth_enabled=auth.enabled(),
|
||||
current_user=auth.current_user(),
|
||||
)
|
||||
@@ -47,7 +54,7 @@ def index():
|
||||
|
||||
@bp.get("/docs")
|
||||
def docs():
|
||||
html = """<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>pyTorrent API Docs</title><link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css\"></head><body><div id=\"swagger-ui\"></div><script src=\"https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js\"></script><script>window.onload=()=>SwaggerUIBundle({url:'/api/openapi.json',dom_id:'#swagger-ui',deepLinking:true,persistAuthorization:true});</script></body></html>"""
|
||||
html = f"""<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>pyTorrent API Docs</title><link rel="stylesheet" href="{_asset_url('swagger_css')}"></head><body><div id="swagger-ui"></div><script src="{_asset_url('swagger_js')}"></script><script>window.onload=()=>SwaggerUIBundle({{url:'/api/openapi.json',dom_id:'#swagger-ui',deepLinking:true,persistAuthorization:true}});</script></body></html>"""
|
||||
return Response(html, mimetype="text/html")
|
||||
|
||||
|
||||
|
||||
111
pytorrent/services/frontend_assets.py
Normal file
111
pytorrent/services/frontend_assets.py
Normal file
@@ -0,0 +1,111 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from ..config import BASE_DIR, USE_OFFLINE_LIBS
|
||||
|
||||
# Notatka: jeden manifest utrzymuje spójne adresy CDN i ścieżki lokalne dla trybu offline.
|
||||
LIBS_STATIC_DIR = "libs"
|
||||
LIBS_DIR = BASE_DIR / "pytorrent" / "static" / LIBS_STATIC_DIR
|
||||
BOOTSTRAP_VERSION = "5.3.3"
|
||||
BOOTSWATCH_VERSION = "5.3.3"
|
||||
FONTAWESOME_VERSION = "6.5.2"
|
||||
FLAG_ICONS_VERSION = "7.2.3"
|
||||
SWAGGER_UI_VERSION = "5"
|
||||
SOCKET_IO_VERSION = "4.7.5"
|
||||
|
||||
BOOTSTRAP_THEMES = (
|
||||
"default",
|
||||
"flatly",
|
||||
"litera",
|
||||
"lumen",
|
||||
"minty",
|
||||
"sketchy",
|
||||
"solar",
|
||||
"spacelab",
|
||||
"united",
|
||||
"zephyr",
|
||||
)
|
||||
|
||||
STATIC_ASSETS = {
|
||||
"bootstrap_js": {
|
||||
"local": f"{LIBS_STATIC_DIR}/bootstrap/{BOOTSTRAP_VERSION}/js/bootstrap.bundle.min.js",
|
||||
"cdn": f"https://cdn.jsdelivr.net/npm/bootstrap@{BOOTSTRAP_VERSION}/dist/js/bootstrap.bundle.min.js",
|
||||
},
|
||||
"fontawesome_css": {
|
||||
"local": f"{LIBS_STATIC_DIR}/fontawesome/{FONTAWESOME_VERSION}/css/all.min.css",
|
||||
"cdn": f"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/{FONTAWESOME_VERSION}/css/all.min.css",
|
||||
},
|
||||
"flag_icons_css": {
|
||||
"local": f"{LIBS_STATIC_DIR}/flag-icons/{FLAG_ICONS_VERSION}/css/flag-icons.min.css",
|
||||
"cdn": f"https://cdn.jsdelivr.net/gh/lipis/flag-icons@{FLAG_ICONS_VERSION}/css/flag-icons.min.css",
|
||||
},
|
||||
"socket_io_js": {
|
||||
"local": f"{LIBS_STATIC_DIR}/socket.io/{SOCKET_IO_VERSION}/socket.io.min.js",
|
||||
"cdn": f"https://cdn.socket.io/{SOCKET_IO_VERSION}/socket.io.min.js",
|
||||
},
|
||||
"swagger_css": {
|
||||
"local": f"{LIBS_STATIC_DIR}/swagger-ui/{SWAGGER_UI_VERSION}/swagger-ui.css",
|
||||
"cdn": f"https://cdn.jsdelivr.net/npm/swagger-ui-dist@{SWAGGER_UI_VERSION}/swagger-ui.css",
|
||||
},
|
||||
"swagger_js": {
|
||||
"local": f"{LIBS_STATIC_DIR}/swagger-ui/{SWAGGER_UI_VERSION}/swagger-ui-bundle.js",
|
||||
"cdn": f"https://cdn.jsdelivr.net/npm/swagger-ui-dist@{SWAGGER_UI_VERSION}/swagger-ui-bundle.js",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def bootstrap_css_asset(theme: str | None = None) -> dict[str, str]:
|
||||
theme = theme if theme in BOOTSTRAP_THEMES else "default"
|
||||
if theme == "default":
|
||||
return {
|
||||
"local": f"{LIBS_STATIC_DIR}/bootstrap/{BOOTSTRAP_VERSION}/css/bootstrap.min.css",
|
||||
"cdn": f"https://cdn.jsdelivr.net/npm/bootstrap@{BOOTSTRAP_VERSION}/dist/css/bootstrap.min.css",
|
||||
}
|
||||
return {
|
||||
"local": f"{LIBS_STATIC_DIR}/bootswatch/{BOOTSWATCH_VERSION}/{theme}/bootstrap.min.css",
|
||||
"cdn": f"https://cdn.jsdelivr.net/npm/bootswatch@{BOOTSWATCH_VERSION}/dist/{theme}/bootstrap.min.css",
|
||||
}
|
||||
|
||||
|
||||
def asset_path(key: str) -> str:
|
||||
return STATIC_ASSETS[key]["local" if USE_OFFLINE_LIBS else "cdn"]
|
||||
|
||||
|
||||
def bootstrap_css_path(theme: str | None = None) -> str:
|
||||
return bootstrap_css_asset(theme)["local" if USE_OFFLINE_LIBS else "cdn"]
|
||||
|
||||
|
||||
def required_offline_paths() -> list[Path]:
|
||||
paths = [LIBS_DIR.parent / item["local"] for item in STATIC_ASSETS.values()]
|
||||
paths.extend(LIBS_DIR.parent / bootstrap_css_asset(theme)["local"] for theme in BOOTSTRAP_THEMES)
|
||||
return paths
|
||||
|
||||
|
||||
def missing_offline_paths() -> list[Path]:
|
||||
missing = [path for path in required_offline_paths() if not path.is_file() or path.stat().st_size <= 0]
|
||||
# Notatka: sprawdzane są też zasoby referencjonowane przez CSS, np. fonty ikon i pliki flag.
|
||||
required_dirs = [
|
||||
LIBS_DIR / f"fontawesome/{FONTAWESOME_VERSION}/webfonts",
|
||||
LIBS_DIR / f"flag-icons/{FLAG_ICONS_VERSION}/flags/4x3",
|
||||
LIBS_DIR / f"flag-icons/{FLAG_ICONS_VERSION}/flags/1x1",
|
||||
]
|
||||
for directory in required_dirs:
|
||||
if not directory.is_dir() or not any(directory.iterdir()):
|
||||
missing.append(directory)
|
||||
return missing
|
||||
|
||||
|
||||
def validate_offline_assets() -> None:
|
||||
# Notatka: aplikacja zatrzymuje start, gdy tryb offline jest aktywny, a pliki nie są zainstalowane.
|
||||
if not USE_OFFLINE_LIBS:
|
||||
return
|
||||
missing = missing_offline_paths()
|
||||
if missing:
|
||||
preview = "\n".join(f"- {path.relative_to(BASE_DIR)}" for path in missing[:20])
|
||||
extra = "" if len(missing) <= 20 else f"\n- ... and {len(missing) - 20} more"
|
||||
raise RuntimeError(
|
||||
"PYTORRENT_USE_OFFLINE_LIBS=true, but frontend libraries are missing. "
|
||||
"Run: ./scripts/download_frontend_libs.py or ./install.sh\n"
|
||||
f"Missing files:\n{preview}{extra}"
|
||||
)
|
||||
@@ -28,11 +28,10 @@ FONT_FAMILIES = {
|
||||
}
|
||||
|
||||
def bootstrap_css_url(theme: str | None) -> str:
|
||||
theme = theme if theme in BOOTSTRAP_THEMES else "default"
|
||||
if theme == "default":
|
||||
return "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
||||
return f"https://cdn.jsdelivr.net/npm/bootswatch@5.3.3/dist/{theme}/bootstrap.min.css"
|
||||
# Notatka: zachowana funkcja zwraca aktualny adres motywu, ale źródło wybiera konfiguracja offline.
|
||||
from .frontend_assets import bootstrap_css_path
|
||||
|
||||
return bootstrap_css_path(theme)
|
||||
|
||||
def list_profiles(user_id: int | None = None):
|
||||
user_id = user_id or auth.current_user_id() or default_user_id()
|
||||
|
||||
@@ -526,7 +526,7 @@
|
||||
}
|
||||
async function generateRtConfig(){ const values=collectRtConfigChanges(); try{ const res=await fetch('/api/rtorrent-config/generate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({values})}); const j=await res.json(); if(!j.ok) throw new Error(j.error||'Generate failed'); if($('rtConfigOutput')) $('rtConfigOutput').value=j.config_text||''; toast('Config generated','success'); }catch(e){ toast(e.message,'danger'); } }
|
||||
|
||||
function bootstrapThemeUrl(theme){ return theme && theme !== "default" ? `https://cdn.jsdelivr.net/npm/bootswatch@5.3.3/dist/${encodeURIComponent(theme)}/bootstrap.min.css` : "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"; }
|
||||
function bootstrapThemeUrl(theme){ /* Notatka: motywy korzystają z mapy URL wygenerowanej przez backend, więc działają także offline. */ const key=theme||"default"; return window.PYTORRENT?.bootstrapThemeUrls?.[key] || window.PYTORRENT?.bootstrapThemeUrls?.default || ""; }
|
||||
function applyBootstrapTheme(theme){ bootstrapTheme = theme || "default"; const link=$("bootstrapThemeStylesheet"); if(link) link.href = bootstrapThemeUrl(bootstrapTheme); if($("bootstrapThemeSelect")) $("bootstrapThemeSelect").value = bootstrapTheme; }
|
||||
function applyFontFamily(font){ fontFamily = font || "default"; document.documentElement.dataset.appFont = fontFamily; if($("fontFamilySelect")) $("fontFamilySelect").value = fontFamily; }
|
||||
async function saveAppearancePreferences(){ applyBootstrapTheme($("bootstrapThemeSelect")?.value || "default"); applyFontFamily($("fontFamilySelect")?.value || "default"); try{ await post("/api/preferences",{bootstrap_theme:bootstrapTheme,font_family:fontFamily}); toast("Appearance preferences saved","success"); }catch(e){ toast(e.message,"danger"); } }
|
||||
|
||||
@@ -750,6 +750,7 @@ body.mobile-mode .table-wrap,
|
||||
body.mobile-mode .details {
|
||||
display: none !important;
|
||||
}
|
||||
/* Notatka: scalone reguły listy mobilnej eliminują powtórzone selektory CSS. */
|
||||
body.mobile-mode #mobileList {
|
||||
display: block !important;
|
||||
min-height: 0;
|
||||
@@ -757,6 +758,7 @@ body.mobile-mode #mobileList {
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding-top: 5.2rem !important;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
body.mobile-mode .content {
|
||||
@@ -1374,9 +1376,6 @@ body.mobile-mode .mobile-card {
|
||||
body.mobile-mode .mobile-filter-bar {
|
||||
display: block !important;
|
||||
}
|
||||
body.mobile-mode #mobileList {
|
||||
padding-top: 5.2rem !important;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
#mobileFilterBar {
|
||||
display: block !important;
|
||||
|
||||
@@ -4,12 +4,11 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>pyTorrent {{ code }}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" rel="stylesheet">
|
||||
<link href="{{ bootstrap_theme_url('default') }}" rel="stylesheet">
|
||||
<link href="{{ frontend_asset_url('fontawesome_css') }}" rel="stylesheet">
|
||||
<link href="{{ static_url('styles.css') }}" rel="stylesheet">
|
||||
</head>
|
||||
<body class="error-page">
|
||||
<!-- Notatka: dedykowany widok błędu utrzymuje wygląd aplikacji zamiast domyślnej strony Flask/Werkzeug. -->
|
||||
<main class="error-card" role="alert">
|
||||
<div class="error-brand"><i class="fa-solid fa-robot"></i> pyTorrent</div>
|
||||
<div class="error-icon" aria-hidden="true"><i class="fa-solid {{ icon }}"></i></div>
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>pyTorrent</title>
|
||||
<link id="bootstrapThemeStylesheet" href="{{ bootstrap_css_url }}" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.2.3/css/flag-icons.min.css" rel="stylesheet">
|
||||
<link id="bootstrapThemeStylesheet" href="{{ bootstrap_theme_url(prefs.bootstrap_theme if prefs else 'default') }}" rel="stylesheet">
|
||||
<link href="{{ frontend_asset_url('fontawesome_css') }}" rel="stylesheet">
|
||||
<link href="{{ frontend_asset_url('flag_icons_css') }}" rel="stylesheet">
|
||||
<link href="{{ static_url('styles.css') }}" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
@@ -42,7 +42,6 @@
|
||||
<span id="connBadge" class="badge text-bg-secondary">offline</span>
|
||||
<button class="btn btn-xs btn-outline-info nav-btn" id="mobileToggle" title="Mobile/simple mode"><i class="fa-solid fa-mobile-screen"></i></button>
|
||||
<button id="themeToggle" class="btn btn-xs btn-outline-secondary nav-btn" title="Change theme"><i class="fa-solid fa-moon"></i></button>
|
||||
<!-- Notatka: subtelny przycisk otwiera okno About bez zmiany istniejącej nawigacji. -->
|
||||
<button class="btn btn-xs btn-outline-secondary nav-btn about-nav-btn" data-bs-toggle="modal" data-bs-target="#aboutModal" title="About pyTorrent"><i class="fa-solid fa-circle-info"></i></button>
|
||||
{% if auth_enabled %}<a class="btn btn-xs btn-outline-danger nav-btn" href="/logout" title="Log out"><i class="fa-solid fa-right-from-bracket"></i><span> {{ current_user.username if current_user else 'logout' }}</span></a>{% endif %}
|
||||
</div>
|
||||
@@ -162,7 +161,6 @@
|
||||
<button class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Notatka: krótkie informacje o licencji, autorze i stacku bez technicznych szczegółów wdrożenia. -->
|
||||
<div class="about-hero">
|
||||
<div class="about-logo"><i class="fa-solid fa-robot"></i></div>
|
||||
<div>
|
||||
@@ -183,8 +181,8 @@
|
||||
</div>
|
||||
|
||||
<div id="toastHost" class="toast-host"></div>
|
||||
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>window.PYTORRENT = {authEnabled: {{ 1 if auth_enabled else 0 }}, currentUser: {% if current_user %}{{ current_user | tojson }}{% else %}null{% endif %}, activeProfile: {{ active_profile.id if active_profile else 'null' }}, tableColumns: {{ (prefs.table_columns_json or '{}') | safe }}, peersRefreshSeconds: {{ prefs.peers_refresh_seconds if prefs else 0 }}, portCheckEnabled: {{ 1 if prefs and prefs.port_check_enabled else 0 }}, bootstrapTheme: {{ (prefs.bootstrap_theme if prefs and prefs.bootstrap_theme else 'default') | tojson }}, fontFamily: {{ (prefs.font_family if prefs and prefs.font_family else 'default') | tojson }}, footerItems: {{ (prefs.footer_items_json or '{}') | safe }}, bootstrapThemes: {{ bootstrap_themes | tojson }}, fontFamilies: {{ font_families | tojson }}};</script>
|
||||
<script src="{{ frontend_asset_url('socket_io_js') }}"></script>
|
||||
<script src="{{ frontend_asset_url('bootstrap_js') }}"></script>
|
||||
<script>window.PYTORRENT = {authEnabled: {{ 1 if auth_enabled else 0 }}, currentUser: {% if current_user %}{{ current_user | tojson }}{% else %}null{% endif %}, activeProfile: {{ active_profile.id if active_profile else 'null' }}, tableColumns: {{ (prefs.table_columns_json or '{}') | safe }}, peersRefreshSeconds: {{ prefs.peers_refresh_seconds if prefs else 0 }}, portCheckEnabled: {{ 1 if prefs and prefs.port_check_enabled else 0 }}, bootstrapTheme: {{ (prefs.bootstrap_theme if prefs and prefs.bootstrap_theme else 'default') | tojson }}, fontFamily: {{ (prefs.font_family if prefs and prefs.font_family else 'default') | tojson }}, footerItems: {{ (prefs.footer_items_json or '{}') | safe }}, bootstrapThemes: {{ bootstrap_themes | tojson }}, bootstrapThemeUrls: { {% for key in bootstrap_themes.keys() %}{{ key | tojson }}: {{ bootstrap_theme_url(key) | tojson }}{% if not loop.last %}, {% endif %}{% endfor %} }, fontFamilies: {{ font_families | tojson }}};</script>
|
||||
<script src="{{ static_url('app.js') }}"></script>
|
||||
</body></html>
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>pyTorrent login</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" rel="stylesheet">
|
||||
<link href="{{ bootstrap_theme_url('default') }}" rel="stylesheet">
|
||||
<link href="{{ frontend_asset_url('fontawesome_css') }}" rel="stylesheet">
|
||||
<link href="{{ static_url('styles.css') }}" rel="stylesheet">
|
||||
</head>
|
||||
<body class="auth-page">
|
||||
|
||||
113
scripts/download_frontend_libs.py
Executable file
113
scripts/download_frontend_libs.py
Executable file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from urllib.parse import urljoin
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
LIBS_STATIC_DIR = "libs"
|
||||
BOOTSTRAP_VERSION = "5.3.3"
|
||||
BOOTSWATCH_VERSION = "5.3.3"
|
||||
FONTAWESOME_VERSION = "6.5.2"
|
||||
FLAG_ICONS_VERSION = "7.2.3"
|
||||
SWAGGER_UI_VERSION = "5"
|
||||
SOCKET_IO_VERSION = "4.7.5"
|
||||
BOOTSTRAP_THEMES = (
|
||||
"default",
|
||||
"flatly",
|
||||
"litera",
|
||||
"lumen",
|
||||
"minty",
|
||||
"sketchy",
|
||||
"solar",
|
||||
"spacelab",
|
||||
"united",
|
||||
"zephyr",
|
||||
)
|
||||
STATIC_ASSETS = {
|
||||
"bootstrap_js": {
|
||||
"local": f"{LIBS_STATIC_DIR}/bootstrap/{BOOTSTRAP_VERSION}/js/bootstrap.bundle.min.js",
|
||||
"cdn": f"https://cdn.jsdelivr.net/npm/bootstrap@{BOOTSTRAP_VERSION}/dist/js/bootstrap.bundle.min.js",
|
||||
},
|
||||
"socket_io_js": {
|
||||
"local": f"{LIBS_STATIC_DIR}/socket.io/{SOCKET_IO_VERSION}/socket.io.min.js",
|
||||
"cdn": f"https://cdn.socket.io/{SOCKET_IO_VERSION}/socket.io.min.js",
|
||||
},
|
||||
"fontawesome_css": {
|
||||
"local": f"{LIBS_STATIC_DIR}/fontawesome/{FONTAWESOME_VERSION}/css/all.min.css",
|
||||
"cdn": f"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/{FONTAWESOME_VERSION}/css/all.min.css",
|
||||
},
|
||||
"flag_icons_css": {
|
||||
"local": f"{LIBS_STATIC_DIR}/flag-icons/{FLAG_ICONS_VERSION}/css/flag-icons.min.css",
|
||||
"cdn": f"https://cdn.jsdelivr.net/gh/lipis/flag-icons@{FLAG_ICONS_VERSION}/css/flag-icons.min.css",
|
||||
},
|
||||
"swagger_css": {
|
||||
"local": f"{LIBS_STATIC_DIR}/swagger-ui/{SWAGGER_UI_VERSION}/swagger-ui.css",
|
||||
"cdn": f"https://cdn.jsdelivr.net/npm/swagger-ui-dist@{SWAGGER_UI_VERSION}/swagger-ui.css",
|
||||
},
|
||||
"swagger_js": {
|
||||
"local": f"{LIBS_STATIC_DIR}/swagger-ui/{SWAGGER_UI_VERSION}/swagger-ui-bundle.js",
|
||||
"cdn": f"https://cdn.jsdelivr.net/npm/swagger-ui-dist@{SWAGGER_UI_VERSION}/swagger-ui-bundle.js",
|
||||
},
|
||||
}
|
||||
URL_RE = re.compile(r"url\((['\"]?)(?!data:)(?!https?:)([^)'\"]+)\1\)")
|
||||
|
||||
|
||||
def bootstrap_css_asset(theme: str) -> dict[str, str]:
|
||||
if theme == "default":
|
||||
return {
|
||||
"local": f"{LIBS_STATIC_DIR}/bootstrap/{BOOTSTRAP_VERSION}/css/bootstrap.min.css",
|
||||
"cdn": f"https://cdn.jsdelivr.net/npm/bootstrap@{BOOTSTRAP_VERSION}/dist/css/bootstrap.min.css",
|
||||
}
|
||||
return {
|
||||
"local": f"{LIBS_STATIC_DIR}/bootswatch/{BOOTSWATCH_VERSION}/{theme}/bootstrap.min.css",
|
||||
"cdn": f"https://cdn.jsdelivr.net/npm/bootswatch@{BOOTSWATCH_VERSION}/dist/{theme}/bootstrap.min.css",
|
||||
}
|
||||
|
||||
|
||||
def download(url: str, dest: Path) -> None:
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
req = Request(url, headers={"User-Agent": "pyTorrent installer"})
|
||||
with urlopen(req, timeout=60) as response:
|
||||
data = response.read()
|
||||
if not data:
|
||||
raise RuntimeError(f"Empty response for {url}")
|
||||
tmp = dest.with_suffix(dest.suffix + ".tmp")
|
||||
tmp.write_bytes(data)
|
||||
tmp.replace(dest)
|
||||
print(f"OK {dest.relative_to(ROOT)}")
|
||||
|
||||
|
||||
def download_css_with_assets(url: str, dest: Path) -> None:
|
||||
download(url, dest)
|
||||
text = dest.read_text(encoding="utf-8", errors="ignore")
|
||||
for match in URL_RE.finditer(text):
|
||||
rel = match.group(2).split("#", 1)[0].split("?", 1)[0]
|
||||
if not rel:
|
||||
continue
|
||||
asset_url = urljoin(url, rel)
|
||||
asset_dest = (dest.parent / rel).resolve()
|
||||
try:
|
||||
asset_dest.relative_to(ROOT)
|
||||
except ValueError:
|
||||
continue
|
||||
if not asset_dest.exists():
|
||||
download(asset_url, asset_dest)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
items = list(STATIC_ASSETS.values())
|
||||
items.extend(bootstrap_css_asset(theme) for theme in BOOTSTRAP_THEMES)
|
||||
for item in items:
|
||||
url = item["cdn"]
|
||||
dest = ROOT / "pytorrent" / "static" / item["local"]
|
||||
if dest.suffix == ".css":
|
||||
download_css_with_assets(url, dest)
|
||||
else:
|
||||
download(url, dest)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user