new fonts

This commit is contained in:
Mateusz Gruszczyński
2026-05-20 13:47:07 +02:00
parent 07c23a8d25
commit 6ab330f583
8 changed files with 423 additions and 7 deletions

View File

@@ -1 +0,0 @@
scripts/INSTALL.md

198
INSTALL.md Normal file
View File

@@ -0,0 +1,198 @@
# pyTorrent stack installer
This document describes the one-command installer for installing **rTorrent + pyTorrent** from a clean server.
The installer is split into two layers:
- `scripts/install_stack.sh` - public bootstrap script intended to be downloaded directly from Git.
- `scripts/stack_installers/` - OS-specific installers and helper scripts used by the bootstrap script.
## Quick install
Run as root or through `sudo`:
```bash
curl -fsSL https://git.linuxiarz.pl/gru/pyTorrent/raw/branch/master/scripts/install_stack.sh | sudo bash
```
The bootstrap script downloads the current pyTorrent repository, detects the operating system family, and runs the matching installer:
- Debian / Ubuntu: `scripts/stack_installers/install_stack_debian_ubuntu.sh`
- RHEL-compatible systems: `scripts/stack_installers/install_stack_rhel.sh`
Supported RHEL-compatible systems include RHEL, Rocky Linux, AlmaLinux, CentOS Stream, and Fedora-like systems where `dnf` or `yum` is available.
## What gets installed
Default installation includes:
- rTorrent `v0.16.11`
- libtorrent `v0.16.11`
- minimal rTorrent build without c-ares/custom curl
- rTorrent system user: `rtorrent`
- rTorrent SCGI endpoint: `scgi://127.0.0.1:5000`
- rTorrent incoming BitTorrent port: `51300`
- pyTorrent application directory: `/opt/pytorrent`
- pyTorrent HTTP port: `8090`
- pyTorrent profile configured through the HTTP API
The installer creates or updates a pyTorrent rTorrent profile through API after both services are installed.
## Recommended usage with overrides
Environment variables must be passed to the `sudo bash` process.
Example:
```bash
curl -fsSL https://git.linuxiarz.pl/gru/pyTorrent/raw/branch/master/scripts/install_stack.sh \
| sudo PYTORRENT_PORT=8091 RTORRENT_SCGI_PORT=5001 bash
```
Another example with a custom profile name:
```bash
curl -fsSL https://git.linuxiarz.pl/gru/pyTorrent/raw/branch/master/scripts/install_stack.sh \
| sudo PYTORRENT_PROFILE_NAME="Local rTorrent" PYTORRENT_PORT=8090 bash
```
## Bootstrap parameters
These variables are used by `scripts/install_stack.sh`.
| Variable | Default | Description |
| --- | --- | --- |
| `PYTORRENT_REPO_URL` | `https://git.linuxiarz.pl/gru/pyTorrent` | Git repository base URL. |
| `PYTORRENT_REPO_BRANCH` | `master` | Branch used to download the repository archive. |
| `PYTORRENT_ARCHIVE_URL` | derived from repo URL and branch | Custom repository archive URL. |
| `PYTORRENT_BOOTSTRAP_DIR` | `/tmp/pytorrent-stack-installer` | Temporary directory used by the bootstrap script. |
| `PYTORRENT_KEEP_BOOTSTRAP_DIR` | `0` | Set to `1` to keep the temporary directory after installation. |
Example using a different branch:
```bash
curl -fsSL https://git.linuxiarz.pl/gru/pyTorrent/raw/branch/master/scripts/install_stack.sh \
| sudo PYTORRENT_REPO_BRANCH=develop bash
```
## rTorrent parameters
These variables are used by both stack installers.
| Variable | Default | Description |
| --- | --- | --- |
| `RTORRENT_USER` | `rtorrent` | System user used to run rTorrent. |
| `RTORRENT_HOME` | `/home/${RTORRENT_USER}` | Home directory for the rTorrent user. |
| `RTORRENT_BASE_DIR` | `/opt/rtorrent_build` | Build and install directory for xmlrpc-c, libtorrent and rTorrent. |
| `RTORRENT_SCGI_PORT` | `5000` | Local SCGI port for rTorrent XMLRPC/SCGI. |
| `RTORRENT_TORRENT_PORT` | `51300` | Incoming BitTorrent listen port. |
| `RTORRENT_REF` | `v0.16.11` | rTorrent Git tag, branch, or commit. |
| `LIBTORRENT_REF` | `v0.16.11` | libtorrent Git tag, branch, or commit. |
Example:
```bash
curl -fsSL https://git.linuxiarz.pl/gru/pyTorrent/raw/branch/master/scripts/install_stack.sh \
| sudo RTORRENT_USER=rtorrent RTORRENT_SCGI_PORT=5001 RTORRENT_TORRENT_PORT=51400 bash
```
## pyTorrent parameters
| Variable | Default | Description |
| --- | --- | --- |
| `PYTORRENT_APP_DIR` | `/opt/pytorrent` | pyTorrent installation directory. |
| `PYTORRENT_PORT` | `8090` | HTTP port used by the pyTorrent service. |
| `PYTORRENT_BASE_URL` | `http://127.0.0.1:${PYTORRENT_PORT}` | Base URL used by the API configurator. |
| `PYTORRENT_PROFILE_NAME` | `Local rTorrent` | Name of the rTorrent profile created in pyTorrent. |
| `PYTORRENT_API_TOKEN` | empty | Bearer token used when pyTorrent API authentication is enabled. |
| `PYTORRENT_SERVICE_NAME` | `pytorrent` | systemd service name for pyTorrent. |
| `PYTORRENT_RTORRENT_SCGI_URL` | `scgi://127.0.0.1:${RTORRENT_SCGI_PORT}` | SCGI URL saved in the pyTorrent rTorrent profile. |
Example with API token:
```bash
curl -fsSL https://git.linuxiarz.pl/gru/pyTorrent/raw/branch/master/scripts/install_stack.sh \
| sudo PYTORRENT_API_TOKEN="pt_xxx" bash
```
## API configurator parameters
The API configurator can be run manually:
```bash
/opt/pytorrent/venv/bin/python /opt/pytorrent/scripts/stack_installers/configure_pytorrent_api.py \
--base-url http://127.0.0.1:8090 \
--profile-name "Local rTorrent" \
--scgi-url scgi://127.0.0.1:5000
```
CLI options:
| Option | Environment variable | Default | Description |
| --- | --- | --- | --- |
| `--base-url` | `PYTORRENT_BASE_URL` | `http://127.0.0.1:8090` | pyTorrent API base URL. |
| `--api-token` | `PYTORRENT_API_TOKEN` | empty | Bearer token for authenticated API calls. |
| `--profile-name` | `PYTORRENT_RTORRENT_PROFILE_NAME` | `Local rTorrent` | Profile name to create or update. |
| `--scgi-url` | `PYTORRENT_RTORRENT_SCGI_URL` | `scgi://127.0.0.1:5000` | rTorrent SCGI URL. |
| `--timeout` | `PYTORRENT_RTORRENT_TIMEOUT` | `10` | rTorrent request timeout in seconds. |
| `--wait` | `PYTORRENT_API_WAIT_SECONDS` | `90` | Time to wait for the pyTorrent API to become available. |
| `--remote` | `PYTORRENT_RTORRENT_REMOTE` | `0` | Mark profile as remote. Accepts `1`, `true`, `yes`, `on`. |
## Local installation without bootstrap
If the repository is already cloned:
Debian / Ubuntu:
```bash
sudo bash scripts/stack_installers/install_stack_debian_ubuntu.sh
```
RHEL-compatible systems:
```bash
sudo bash scripts/stack_installers/install_stack_rhel.sh
```
## Installed service hints
Check services:
```bash
systemctl status pytorrent
systemctl status rtorrent@rtorrent.service
```
Check logs:
```bash
tail -f /data/logs/app.log /data/logs/error.log
journalctl -u pytorrent -f
journalctl -u rtorrent@rtorrent.service -f
```
## Notes
- The default rTorrent build is intentionally minimal.
- c-ares and custom curl are not enabled by the stack installer defaults.
- The rTorrent installer overwrites the generated `.rtorrent.rc` because the stack installer passes `--force-config`.
- pyTorrent is configured through the HTTP API after the service starts.
- If API authentication is enabled before profile configuration, pass `PYTORRENT_API_TOKEN`.
## Build logs and troubleshooting
The stack installer writes quiet build output to `/var/log/pytorrent-installer` by default.
Override it with:
```bash
PYTORRENT_STACK_LOG_DIR=/tmp/pytorrent-build-logs
```
For full command output during rTorrent/libtorrent compilation, run with:
```bash
PYTORRENT_DEBUG_INSTALL=1
```
On RHEL-compatible systems the installer also tries to enable CRB/PowerTools and installs `libcurl-devel`, `redhat-rpm-config`, `patch`, `diffutils`, `findutils`, `file`, and `libstdc++-devel`, because minimal Alma/Rocky images often do not include enough build tooling.

View File

@@ -13,6 +13,33 @@ 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",
@@ -39,6 +66,10 @@ STATIC_ASSETS = {
"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(),
},
"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",
@@ -87,6 +118,7 @@ def missing_offline_paths() -> list[Path]:
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",
LIBS_DIR / "fonts/files",
]
for directory in required_dirs:
if not directory.is_dir() or not any(directory.iterdir()):

View File

@@ -20,11 +20,22 @@ BOOTSTRAP_THEMES = {
FONT_FAMILIES = {
"default": "Theme default",
"adwaita-mono": "Adwaita Mono",
"system-ui": "System UI / Apple-like",
"figtree": "Figtree",
"inter": "Inter",
"system-ui": "System UI",
"geist": "Geist",
"manrope": "Manrope",
"dm-sans": "DM Sans",
"source-sans-3": "Source Sans 3",
"open-sans": "Open Sans",
"roboto": "Roboto",
"lato": "Lato",
"nunito-sans": "Nunito Sans",
"poppins": "Poppins",
"montserrat": "Montserrat",
"ibm-plex-sans": "IBM Plex Sans",
"jetbrains-mono": "JetBrains Mono",
"adwaita-mono": "Adwaita Mono",
}
# Note: Backend owns the recommended torrent table layout so frontend builds do not duplicate presets.

View File

@@ -33,13 +33,66 @@ html[data-app-font="inter"] {
}
html[data-app-font="system-ui"] {
--app-font-family:
system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI",
Roboto, Arial, sans-serif;
}
html[data-app-font="figtree"] {
--app-font-family:
Figtree, Inter, system-ui, -apple-system, "Segoe UI", Roboto, Arial,
sans-serif;
}
html[data-app-font="geist"] {
--app-font-family:
Geist, Inter, system-ui, -apple-system, "Segoe UI", Roboto, Arial,
sans-serif;
}
html[data-app-font="manrope"] {
--app-font-family:
Manrope, Inter, system-ui, -apple-system, "Segoe UI", Roboto, Arial,
sans-serif;
}
html[data-app-font="dm-sans"] {
--app-font-family:
"DM Sans", Inter, system-ui, -apple-system, "Segoe UI", Roboto, Arial,
sans-serif;
}
html[data-app-font="source-sans-3"] {
--app-font-family:
"Source Sans 3", "Source Sans Pro", system-ui, -apple-system, Segoe UI,
Roboto, Arial, sans-serif;
}
html[data-app-font="open-sans"] {
--app-font-family:
"Open Sans", system-ui, -apple-system, "Segoe UI", Roboto, Arial,
sans-serif;
}
html[data-app-font="roboto"] {
--app-font-family:
Roboto, system-ui, -apple-system, "Segoe UI", Arial, sans-serif;
}
html[data-app-font="lato"] {
--app-font-family:
Lato, system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
}
html[data-app-font="nunito-sans"] {
--app-font-family:
"Nunito Sans", system-ui, -apple-system, "Segoe UI", Roboto, Arial,
sans-serif;
}
html[data-app-font="poppins"] {
--app-font-family:
Poppins, system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
}
html[data-app-font="montserrat"] {
--app-font-family:
Montserrat, system-ui, -apple-system, "Segoe UI", Roboto, Arial,
sans-serif;
}
html[data-app-font="ibm-plex-sans"] {
--app-font-family:
"IBM Plex Sans", system-ui, -apple-system, "Segoe UI", Roboto, Arial,
sans-serif;
}
html[data-app-font="jetbrains-mono"] {
--app-font-family:
"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas,

View File

@@ -33,13 +33,66 @@ html[data-app-font="inter"] {
}
html[data-app-font="system-ui"] {
--app-font-family:
system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI",
Roboto, Arial, sans-serif;
}
html[data-app-font="figtree"] {
--app-font-family:
Figtree, Inter, system-ui, -apple-system, "Segoe UI", Roboto, Arial,
sans-serif;
}
html[data-app-font="geist"] {
--app-font-family:
Geist, Inter, system-ui, -apple-system, "Segoe UI", Roboto, Arial,
sans-serif;
}
html[data-app-font="manrope"] {
--app-font-family:
Manrope, Inter, system-ui, -apple-system, "Segoe UI", Roboto, Arial,
sans-serif;
}
html[data-app-font="dm-sans"] {
--app-font-family:
"DM Sans", Inter, system-ui, -apple-system, "Segoe UI", Roboto, Arial,
sans-serif;
}
html[data-app-font="source-sans-3"] {
--app-font-family:
"Source Sans 3", "Source Sans Pro", system-ui, -apple-system, Segoe UI,
Roboto, Arial, sans-serif;
}
html[data-app-font="open-sans"] {
--app-font-family:
"Open Sans", system-ui, -apple-system, "Segoe UI", Roboto, Arial,
sans-serif;
}
html[data-app-font="roboto"] {
--app-font-family:
Roboto, system-ui, -apple-system, "Segoe UI", Arial, sans-serif;
}
html[data-app-font="lato"] {
--app-font-family:
Lato, system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
}
html[data-app-font="nunito-sans"] {
--app-font-family:
"Nunito Sans", system-ui, -apple-system, "Segoe UI", Roboto, Arial,
sans-serif;
}
html[data-app-font="poppins"] {
--app-font-family:
Poppins, system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
}
html[data-app-font="montserrat"] {
--app-font-family:
Montserrat, system-ui, -apple-system, "Segoe UI", Roboto, Arial,
sans-serif;
}
html[data-app-font="ibm-plex-sans"] {
--app-font-family:
"IBM Plex Sans", system-ui, -apple-system, "Segoe UI", Roboto, Arial,
sans-serif;
}
html[data-app-font="jetbrains-mono"] {
--app-font-family:
"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas,

View File

@@ -8,6 +8,7 @@
<link rel="shortcut icon" href="{{ static_url('favicon.svg') }}" type="image/svg+xml">
<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('font_css') }}" rel="stylesheet">
<link href="{{ frontend_asset_url('flag_icons_css') }}" rel="stylesheet">
<link href="{{ static_url('styles.css') }}" rel="stylesheet">
</head>

View File

@@ -8,6 +8,7 @@
<link rel="shortcut icon" href="{{ static_url('favicon.svg') }}" type="image/svg+xml">
<link href="{{ bootstrap_theme_url('default') }}" rel="stylesheet">
<link href="{{ frontend_asset_url('fontawesome_css') }}" rel="stylesheet">
<link href="{{ frontend_asset_url('font_css') }}" rel="stylesheet">
<link href="{{ static_url('styles.css') }}" rel="stylesheet">
</head>
<body class="auth-page">

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import re
from pathlib import Path
from urllib.parse import urljoin
from urllib.parse import urljoin, urlparse
from urllib.request import Request, urlopen
ROOT = Path(__file__).resolve().parents[1]
@@ -14,6 +14,33 @@ 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",
@@ -43,6 +70,10 @@ STATIC_ASSETS = {
"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",
@@ -53,6 +84,7 @@ STATIC_ASSETS = {
},
}
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]:
@@ -97,13 +129,49 @@ def download_css_with_assets(url: str, dest: Path) -> None:
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 dest.suffix == ".css":
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)