Build 0202261541: allow FQDN service URLs
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
BUILD_NUMBER = "3001262148"
|
||||
BUILD_NUMBER = "0202261541"
|
||||
CHANGELOG = '2026-01-22\\n- Initial commit\\n- Ignore build artifacts\\n- Update README\\n- Update README with Docker-first guide\\n\\n2026-01-23\\n- Fix cache titles via Jellyseerr media lookup\\n- Split search actions and improve download options\\n- Fallback manual grab to qBittorrent\\n- Hide header actions when signed out\\n- Add feedback form and webhook\\n- Fix cache titles and move feedback link\\n- Show available status on landing when in Jellyfin\\n- Add default branding assets when missing\\n- Use bundled branding assets\\n- Remove password fields from users page\\n- Add Docker Hub compose override\\n- Fix backend Dockerfile paths for root context\\n- Copy public assets into frontend image\\n- Use backend branding assets for logo and favicon\\n\\n2026-01-24\\n- Route grabs through Sonarr/Radarr only\\n- Document fix buttons in how-it-works\\n- Clarify how-it-works steps and fixes\\n- Map Prowlarr releases to Arr indexers for manual grab\\n- Improve request handling and qBittorrent categories\\n\\n2026-01-25\\n- Add site banner, build number, and changelog\\n- Automate build number tagging and sync\\n- Improve mobile header layout\\n- Move account actions into avatar menu\\n- Add user stats and activity tracking\\n- Add Jellyfin login cache and admin-only stats\\n- Tidy request sync controls\\n- Seed branding logo from bundled assets\\n- Serve bundled branding assets by default\\n- Harden request cache titles and cache-only reads\\n- Build 2501262041\\n\\n2026-01-26\\n- Fix cache title hydration\\n- Fix sync progress bar animation\\n\\n2026-01-27\\n- Add cache control artwork stats\\n- Improve cache stats performance (build 271261145)\\n- Fix backend cache stats import (build 271261149)\\n- Clarify request sync settings (build 271261159)\\n- Bump build number to 271261202\\n- Fix request titles in snapshots (build 271261219)\\n- Fix snapshot title fallback (build 271261228)\\n- Add cache load spinner (build 271261238)\\n- Bump build number (process 2) 271261322\\n- Add service test buttons (build 271261335)\\n- Fallback to TMDB when artwork cache fails (build 271261524)\\n- Hydrate missing artwork from Jellyseerr (build 271261539)\\n\\n2026-01-29\\n- release: 2901262036\\n- release: 2901262044\\n- release: 2901262102\\n- Hardcode build number in backend\\n- Bake build number and changelog\\n- Update full changelog\\n- Tidy full changelog\\n- Build 2901262240: cache users\n\n2026-01-30\n- Merge backend and frontend into one container'
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from typing import Any, Dict, List, Optional
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import ipaddress
|
||||
import os
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File
|
||||
|
||||
@@ -64,6 +66,16 @@ SENSITIVE_KEYS = {
|
||||
"qbittorrent_password",
|
||||
}
|
||||
|
||||
URL_SETTING_KEYS = {
|
||||
"jellyseerr_base_url",
|
||||
"jellyfin_base_url",
|
||||
"jellyfin_public_url",
|
||||
"sonarr_base_url",
|
||||
"radarr_base_url",
|
||||
"prowlarr_base_url",
|
||||
"qbittorrent_base_url",
|
||||
}
|
||||
|
||||
SETTING_KEYS: List[str] = [
|
||||
"jellyseerr_base_url",
|
||||
"jellyseerr_api_key",
|
||||
@@ -107,6 +119,49 @@ def _normalize_username(value: str) -> str:
|
||||
normalized = normalized.split("@", 1)[0]
|
||||
return normalized
|
||||
|
||||
|
||||
def _is_ip_host(host: str) -> bool:
|
||||
try:
|
||||
ipaddress.ip_address(host)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def _normalize_service_url(value: str) -> str:
|
||||
raw = value.strip()
|
||||
if not raw:
|
||||
raise ValueError("URL cannot be empty.")
|
||||
|
||||
candidate = raw
|
||||
if "://" not in candidate:
|
||||
authority = candidate.split("/", 1)[0].strip()
|
||||
if authority.startswith("["):
|
||||
closing = authority.find("]")
|
||||
host = authority[1:closing] if closing > 0 else authority.strip("[]")
|
||||
else:
|
||||
host = authority.split(":", 1)[0]
|
||||
host = host.strip().lower()
|
||||
default_scheme = "http" if host in {"localhost"} or _is_ip_host(host) or "." not in host else "https"
|
||||
candidate = f"{default_scheme}://{candidate}"
|
||||
|
||||
parsed = urlparse(candidate)
|
||||
if parsed.scheme not in {"http", "https"}:
|
||||
raise ValueError("URL must use http:// or https://.")
|
||||
if not parsed.netloc:
|
||||
raise ValueError("URL must include a host.")
|
||||
if parsed.query or parsed.fragment:
|
||||
raise ValueError("URL must not include query params or fragments.")
|
||||
if not parsed.hostname:
|
||||
raise ValueError("URL must include a valid host.")
|
||||
|
||||
normalized_path = parsed.path.rstrip("/")
|
||||
normalized = parsed._replace(path=normalized_path, params="", query="", fragment="")
|
||||
result = urlunparse(normalized).rstrip("/")
|
||||
if not result:
|
||||
raise ValueError("URL is invalid.")
|
||||
return result
|
||||
|
||||
def _normalize_root_folders(folders: Any) -> List[Dict[str, Any]]:
|
||||
if not isinstance(folders, list):
|
||||
return []
|
||||
@@ -203,7 +258,14 @@ async def update_settings(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
delete_setting(key)
|
||||
updates += 1
|
||||
continue
|
||||
set_setting(key, str(value))
|
||||
value_to_store = str(value).strip() if isinstance(value, str) else str(value)
|
||||
if key in URL_SETTING_KEYS and value_to_store:
|
||||
try:
|
||||
value_to_store = _normalize_service_url(value_to_store)
|
||||
except ValueError as exc:
|
||||
friendly_key = key.replace("_", " ")
|
||||
raise HTTPException(status_code=400, detail=f"{friendly_key}: {exc}") from exc
|
||||
set_setting(key, value_to_store)
|
||||
updates += 1
|
||||
if key in {"log_level", "log_file"}:
|
||||
touched_logging = True
|
||||
|
||||
Reference in New Issue
Block a user