import logging import sentry_sdk from fastapi import FastAPI from sentry_sdk.integrations.fastapi import FastApiIntegration from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration from sentry_sdk.integrations.starlette import StarletteIntegration from sqlalchemy import text from app.api.admin import router as admin_router from app.api.admin_html import mount_static from app.api.admin_html import router as admin_html_router from app.api.blacklist import router as blacklist_router from app.api.bug_reports import router as bug_reports_router from app.api.expo_updates import router as expo_updates_router from app.api.favorites import router as favorites_router from app.api.me import router as me_router from app.api.movies import router as movies_router from app.api.playback import movies_router as movies_playback_router from app.api.playback import router as playback_router from app.api.saved_searches import router as saved_searches_router from app.api.scene_favorites import router as scene_favorites_router from app.api.scenes import router as scenes_router from app.api.seo import router as seo_router from app.api.sources import router as sources_router from app.api.stream_proxy import router as stream_proxy_router from app.api.taxonomies import router as taxonomies_router from app.api.watch import router as watch_router from app.config import get_settings from app.db import session_scope _settings = get_settings() logging.basicConfig(level=_settings.log_level) # Sentry: init przed FastAPI() żeby SDK przechwycił request handlery od pierwszego. # Pusty DSN → SDK no-op (dev/local). Integracje: FastApi (request context, transactions), # Starlette (middleware-level errors), SQLAlchemy (slow queries jako spans). if _settings.sentry_dsn: def _sentry_before_send(event, hint): """Filter expected/transient HTTPException z proxy + playback resolve. Sentry FastAPI integration loguje WSZYSTKIE HTTPException jako error. Dla nas 502/503/504 to expected behavior gdy upstream CDN unreachable albo extractor zwraca None (legitnie). Spam-flooduje issue list (GOON-3 16ev/5h, GOON-G 8ev, GOON-4 6ev). """ exc_info = hint.get("exc_info") if hint else None if not exc_info: return event exc = exc_info[1] from fastapi import HTTPException as _HTTPExc if isinstance(exc, _HTTPExc) and exc.status_code in {502, 503, 504}: return None # drop return event sentry_sdk.init( dsn=_settings.sentry_dsn, environment=_settings.sentry_environment, traces_sample_rate=_settings.sentry_traces_sample_rate, # send_default_pii=False — bezpieczne, headers/cookies/IP nie idą do Sentry. # Zostawiamy default (False) bo niektóre tube'y mają potencjalnie wrażliwe URL'e. integrations=[ StarletteIntegration(transaction_style="endpoint"), FastApiIntegration(transaction_style="endpoint"), SqlalchemyIntegration(), ], before_send=_sentry_before_send, release="goon@0.1.8", ) app = FastAPI(title="goon", version="0.1.8") app.include_router(scenes_router) app.include_router(sources_router) app.include_router(movies_router) app.include_router(playback_router) app.include_router(movies_playback_router) app.include_router(scene_favorites_router) app.include_router(stream_proxy_router) app.include_router(taxonomies_router) app.include_router(favorites_router) app.include_router(blacklist_router) app.include_router(bug_reports_router) app.include_router(saved_searches_router) app.include_router(expo_updates_router) app.include_router(watch_router) app.include_router(me_router) app.include_router(admin_router) app.include_router(admin_html_router) app.include_router(seo_router) mount_static(app) @app.get("/healthz") def healthz() -> dict[str, str]: return {"status": "ok"} @app.get("/version") def version() -> dict[str, str | None]: """Mobile sprawdza po starcie żeby wykryć dostępność nowszej wersji APK. Zwraca: - `version`: kanoniczna wersja serwera (źródło prawdy o latest APK) - `apk_url`: bezpośredni link do najnowszego APK (self-hosted) lub None gdy tylko external (GitHub Releases). Mobile otwiera w browser przy update. APK jest serwowany ze statycznego endpointu `/static/app-debug.apk` jeśli istnieje (gradle build → scp na VPS → tu serwujemy). Brak pliku → `apk_url=None`, mobile pokazuje tylko "newer version available" bez direct link. """ import os from pathlib import Path apk_path = Path(__file__).resolve().parent / "static" / "app-release.apk" apk_url: str | None = None if apk_path.exists(): # `BACKEND_PUBLIC_URL` z env to URL pod którym mobile może hit'nąć backend # (production: https://goon.example.com lub IP:port). Default — relatywny URL, # mobile sklei z baseUrl. public_url = os.environ.get("BACKEND_PUBLIC_URL", "").rstrip("/") apk_url = f"{public_url}/static/app-release.apk" if public_url else "/static/app-release.apk" return {"version": "0.2.1", "apk_url": apk_url} @app.get("/readyz") def readyz() -> dict[str, object]: try: with session_scope() as session: session.execute(text("SELECT 1")) return {"status": "ready", "db": "ok"} except Exception as exc: return {"status": "degraded", "db": "error", "error": str(exc)}