Goon — self-hosted aggregator for adult-content scene metadata. Indexes scenes from TPDB, StashDB, and 30+ public adult tube sites. Cross-source deduplication via perceptual hash + Levenshtein distance. FastAPI backend + APScheduler worker + React Native (Expo) mobile client. FOSS, ad-free, donation-funded. See README for details.
116 lines
5.1 KiB
Python
116 lines
5.1 KiB
Python
from functools import lru_cache
|
|
|
|
from pydantic import Field
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
model_config = SettingsConfigDict(env_file=".env", extra="ignore", case_sensitive=False)
|
|
|
|
database_url: str = Field(
|
|
default="postgresql+psycopg://goon:goon@localhost:5432/goon",
|
|
validation_alias="DATABASE_URL",
|
|
)
|
|
|
|
tpdb_api_token: str | None = Field(default=None, validation_alias="TPDB_API_TOKEN")
|
|
tpdb_base_url: str = Field(
|
|
default="https://api.theporndb.net", validation_alias="TPDB_BASE_URL"
|
|
)
|
|
|
|
stashdb_api_key: str | None = Field(default=None, validation_alias="STASHDB_API_KEY")
|
|
stashdb_graphql_url: str = Field(
|
|
default="https://stashdb.org/graphql", validation_alias="STASHDB_GRAPHQL_URL"
|
|
)
|
|
|
|
log_level: str = Field(default="INFO", validation_alias="LOG_LEVEL")
|
|
|
|
|
|
# Sentry observability — pusty DSN = init no-op (devel/local). Cloud free tier
|
|
# 5k errors/mies wystarczy dla 1-user app.
|
|
sentry_dsn: str | None = Field(default=None, validation_alias="SENTRY_DSN")
|
|
sentry_environment: str = Field(default="dev", validation_alias="SENTRY_ENVIRONMENT")
|
|
sentry_traces_sample_rate: float = Field(
|
|
default=0.1, validation_alias="SENTRY_TRACES_SAMPLE_RATE"
|
|
)
|
|
|
|
api_keys_raw: str = Field(default="", validation_alias="API_KEYS")
|
|
"""Lista API keys oddzielona przecinkami. Pusta = auth wyłączony (tylko dev/local)."""
|
|
|
|
allowed_app_sig_hashes_raw: str = Field(default="", validation_alias="ALLOWED_APP_SIG_HASH")
|
|
"""Whitelist SHA256 (hex) podpisów APK akceptowane przez backend. Każdy request mobile
|
|
wysyła `X-App-Signature` z hashem signing certu (PackageManager.GET_SIGNING_CERTIFICATES).
|
|
Pusta = check wyłączony (dev/wstępny rollout). Lista = comma-separated lowercase hex.
|
|
Re-packaging APK innym keystorem zmienia hash → 403."""
|
|
|
|
auto_merge_threshold: float = 0.92
|
|
review_threshold: float = 0.75
|
|
fingerprint_hamming_max: int = 5
|
|
title_token_set_min: int = 88
|
|
date_window_days: int = 7
|
|
|
|
# APScheduler (M5). Każdy 0/None = job wyłączony.
|
|
sched_tpdb_hours: int = Field(default=6, validation_alias="GOON_SCHED_TPDB_HOURS")
|
|
sched_stashdb_hours: int = Field(default=6, validation_alias="GOON_SCHED_STASHDB_HOURS")
|
|
sched_performer_driven_hours: int = Field(
|
|
default=12, validation_alias="GOON_SCHED_PERFORMER_DRIVEN_HOURS"
|
|
)
|
|
sched_performer_driven_top_n: int = Field(
|
|
default=20, validation_alias="GOON_SCHED_PERFORMER_DRIVEN_TOP_N"
|
|
)
|
|
# Continuous worker. interval=15s + max_instances=1 + coalesce=True ⇒ effective rate
|
|
# = max(15, real_tick_duration). Real tick ~50-80s przy full coverage. Set to 0 to disable.
|
|
sched_performer_continuous_seconds: int = Field(
|
|
default=15, validation_alias="GOON_SCHED_PERFORMER_CONTINUOUS_SECONDS"
|
|
)
|
|
sched_performer_continuous_refresh_days: int = Field(
|
|
default=30, validation_alias="GOON_SCHED_PERFORMER_CONTINUOUS_REFRESH_DAYS"
|
|
)
|
|
# Movie ingest — paradisehill (primary) + dooplay mirrory (mangoporn/streamporn/
|
|
# pandamovies). Każdy connector zapisuje swój `Source` i robi delta od ostatniego
|
|
# successful run. Set to 0 to disable. Domyślnie 24h: movie sites rosną wolniej
|
|
# niż tube'y (~5-30 nowych dziennie), nie ma sensu wymiatać częściej.
|
|
sched_movie_ingest_hours: int = Field(
|
|
default=24, validation_alias="GOON_SCHED_MOVIE_INGEST_HOURS"
|
|
)
|
|
# Browse-latest scheduler: freshporno/porn00/pornxp newest scenes raz dziennie.
|
|
sched_browse_latest_hours: int = Field(
|
|
default=24, validation_alias="GOON_SCHED_BROWSE_LATEST_HOURS"
|
|
)
|
|
sched_browse_latest_max_pages: int = Field(
|
|
default=5, validation_alias="GOON_SCHED_BROWSE_LATEST_MAX_PAGES"
|
|
)
|
|
|
|
# Hetzner Cloud bandwidth monitor — read-only API token (Security → API Tokens
|
|
# w panelu Hetzner Cloud). Bez tokenu monitor wyłączony (warning w log).
|
|
# Free traffic per server: CX22=20TB, CPX21=20TB itd. Overage = €1/TB.
|
|
hetzner_api_token: str | None = Field(default=None, validation_alias="HETZNER_API_TOKEN")
|
|
hetzner_server_id: int | None = Field(default=None, validation_alias="HETZNER_SERVER_ID")
|
|
# Alert thresholds (% of included_traffic) — Sentry severity levels.
|
|
hetzner_alert_info_pct: int = Field(default=50, validation_alias="HETZNER_ALERT_INFO_PCT")
|
|
hetzner_alert_warning_pct: int = Field(default=80, validation_alias="HETZNER_ALERT_WARNING_PCT")
|
|
hetzner_alert_error_pct: int = Field(default=95, validation_alias="HETZNER_ALERT_ERROR_PCT")
|
|
|
|
@property
|
|
def api_keys(self) -> set[str]:
|
|
return {k.strip() for k in self.api_keys_raw.split(",") if k.strip()}
|
|
|
|
@property
|
|
def auth_enabled(self) -> bool:
|
|
return bool(self.api_keys)
|
|
|
|
@property
|
|
def allowed_app_sig_hashes(self) -> set[str]:
|
|
return {
|
|
h.strip().lower().replace(":", "")
|
|
for h in self.allowed_app_sig_hashes_raw.split(",")
|
|
if h.strip()
|
|
}
|
|
|
|
@property
|
|
def app_sig_check_enabled(self) -> bool:
|
|
return bool(self.allowed_app_sig_hashes)
|
|
|
|
|
|
@lru_cache
|
|
def get_settings() -> Settings:
|
|
return Settings()
|