all repos — rastro @ main

BitTorrent tracker!

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()