app/config.py (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
from __future__ import annotations
from functools import lru_cache
from typing import Literal
from pydantic import Field, field_validator, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
populate_by_name=True,
)
app_name: str = Field(default="Rastro", alias="APP_NAME")
app_description: str = Field(
default="Observatorio publico de trackers BitTorrent",
alias="APP_DESCRIPTION",
)
app_version: str = Field(default="0.1.0", alias="APP_VERSION")
public_url: str = Field(default="http://localhost:8090", alias="PUBLIC_URL")
repository_url: str = Field(default="", alias="REPOSITORY_URL")
database_url: str = Field(
default="sqlite+aiosqlite:///data/rastro.db",
alias="DATABASE_URL",
)
host: str = Field(default="0.0.0.0", alias="HOST")
port: int = Field(default=8090, alias="PORT", ge=1, le=65535)
log_level: str = Field(default="INFO", alias="LOG_LEVEL")
run_scheduler: bool = Field(default=True, alias="RUN_SCHEDULER")
# Source lists refresh on a weekly cron (UTC). A startup import still runs
# when the inventory is empty or the last successful import is stale.
import_day_of_week: str = Field(default="sun", alias="IMPORT_DAY_OF_WEEK")
import_hour_utc: int = Field(default=4, alias="IMPORT_HOUR_UTC", ge=0, le=23)
import_max_age_hours: int = Field(default=144, alias="IMPORT_MAX_AGE_HOURS", ge=1)
default_probe_interval_seconds: int = Field(
default=10800, alias="DEFAULT_PROBE_INTERVAL_SECONDS", ge=60
)
min_probe_interval_seconds: int = Field(default=1800, alias="MIN_PROBE_INTERVAL_SECONDS", ge=60)
probe_concurrency: int = Field(default=10, alias="PROBE_CONCURRENCY", ge=1, le=100)
probe_timeout_seconds: float = Field(default=10.0, alias="PROBE_TIMEOUT_SECONDS", gt=0, le=120)
max_response_bytes: int = Field(default=1_048_576, alias="MAX_RESPONSE_BYTES", ge=1024)
raw_result_retention_days: int = Field(default=90, alias="RAW_RESULT_RETENTION_DAYS", ge=1)
enable_ipv4: bool = Field(default=True, alias="ENABLE_IPV4")
enable_ipv6: Literal["true", "false", "auto"] | bool = Field(
default="auto", alias="ENABLE_IPV6"
)
geoip_asn_db: str = Field(default="", alias="GEOIP_ASN_DB")
# Source URLs (overridable for tests)
trackerslist_all_url: str = Field(
default=("https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt"),
alias="TRACKERSLIST_ALL_URL",
)
trackerslist_best_url: str = Field(
default=("https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt"),
alias="TRACKERSLIST_BEST_URL",
)
newtrackon_all_url: str = Field(
default="https://newtrackon.com/api/all",
alias="NEWTRACKON_ALL_URL",
)
newtrackon_stable_url: str = Field(
default="https://newtrackon.com/api/stable",
alias="NEWTRACKON_STABLE_URL",
)
newtrackon_live_url: str = Field(
default="https://newtrackon.com/api/live",
alias="NEWTRACKON_LIVE_URL",
)
jokepool710_all_url: str = Field(
default=(
"https://raw.githubusercontent.com/jokepool710/Torrent-_Trackers/main/trackers_all.txt"
),
alias="JOKEPOOL710_ALL_URL",
)
xiu2_all_url: str = Field(
default="https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/all.txt",
alias="XIU2_ALL_URL",
)
xiu2_best_url: str = Field(
default="https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/best.txt",
alias="XIU2_BEST_URL",
)
@field_validator("enable_ipv6", mode="before")
@classmethod
def normalize_ipv6(cls, value: object) -> object:
if isinstance(value, str):
lowered = value.strip().lower()
if lowered in {"1", "yes", "true"}:
return True
if lowered in {"0", "no", "false"}:
return False
if lowered == "auto":
return "auto"
return value
@model_validator(mode="after")
def validate_intervals(self) -> Settings:
if self.min_probe_interval_seconds > self.default_probe_interval_seconds:
raise ValueError(
"MIN_PROBE_INTERVAL_SECONDS cannot exceed DEFAULT_PROBE_INTERVAL_SECONDS"
)
return self
def user_agent(self) -> str:
base = f"{self.app_name}/{self.app_version}"
url = (self.public_url or "").strip()
if url:
return f"{base} (+{url})"
return base
def peer_id_prefix(self) -> bytes:
"""Return a 8-byte BitTorrent peer_id style prefix derived from app name."""
slug = "".join(c for c in self.app_name.upper() if c.isalnum())[:6] or "RASTRO"
slug = slug.ljust(6, "X")[:6]
return f"-{slug}-".encode("ascii")
def ipv6_mode(self) -> Literal["true", "false", "auto"]:
if self.enable_ipv6 is True:
return "true"
if self.enable_ipv6 is False:
return "false"
return "auto"
@lru_cache
def get_settings() -> Settings:
return Settings()
|