Files
pyTorrent/scripts/download_frontend_libs.py
Mateusz Gruszczyński 6ab330f583 new fonts
2026-05-20 13:47:07 +02:00

182 lines
5.9 KiB
Python
Executable File

#!/usr/bin/env python3
from __future__ import annotations
import re
from pathlib import Path
from urllib.parse import urljoin, urlparse
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"
GOOGLE_FONT_FAMILIES = (
"DM Sans",
"Figtree",
"Geist",
"IBM Plex Sans",
"Inter",
"JetBrains Mono",
"Lato",
"Manrope",
"Montserrat",
"Nunito Sans",
"Open Sans",
"Poppins",
"Roboto",
"Source Sans 3",
)
GOOGLE_FONT_WEIGHTS = "400;500;600;700;800"
def google_fonts_css_url() -> str:
families = "&".join(
f"family={name.replace(' ', '+')}:wght@{GOOGLE_FONT_WEIGHTS}"
for name in GOOGLE_FONT_FAMILIES
)
return f"https://fonts.googleapis.com/css2?{families}&display=swap"
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",
},
"font_css": {
"local": f"{LIBS_STATIC_DIR}/fonts/google-fonts.css",
"cdn": google_fonts_css_url(),
},
"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\)")
ANY_URL_RE = re.compile(r"url\((['\"]?)(?!data:)([^)'\"]+)\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 download_google_fonts_css(url: str, dest: Path) -> None:
dest.parent.mkdir(parents=True, exist_ok=True)
req = Request(
url,
headers={
"User-Agent": "Mozilla/5.0 pyTorrent installer",
"Accept": "text/css,*/*;q=0.1",
},
)
with urlopen(req, timeout=60) as response:
css = response.read().decode("utf-8", errors="ignore")
if not css.strip():
raise RuntimeError(f"Empty response for {url}")
def replace_url(match: re.Match[str]) -> str:
quote = match.group(1) or ""
asset_url = match.group(2)
parsed = urlparse(asset_url)
if parsed.scheme not in {"http", "https"}:
return match.group(0)
filename = Path(parsed.path).name
if not filename:
return match.group(0)
asset_dest = dest.parent / "files" / filename
if not asset_dest.exists():
download(asset_url, asset_dest)
return f"url({quote}files/{filename}{quote})"
rewritten = ANY_URL_RE.sub(replace_url, css)
tmp = dest.with_suffix(dest.suffix + ".tmp")
tmp.write_text(rewritten, encoding="utf-8")
tmp.replace(dest)
print(f"OK {dest.relative_to(ROOT)}")
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 item.get("local") == STATIC_ASSETS["font_css"]["local"]:
download_google_fonts_css(url, dest)
elif dest.suffix == ".css":
download_css_with_assets(url, dest)
else:
download(url, dest)
if __name__ == "__main__":
main()