Zgloszone przez uzytkownika: po wydaniu 0.3.0 nie dostal propozycji aktualizacji.
Przyczyna jest moja. `APP_VERSION` czyta `Constants.expoConfig.version`, czyli app.json,
a app.json przyjezdza do aplikacji RAZEM Z PACZKA OTA. Podbilem wersje na 0.3.0 i
opublikowalem te paczke rowniez na stary kanal 1.2, wiec kazdy uzytkownik APK 0.2.2
zaczal raportowac 0.3.0 i sam siebie uznal za aktualnego. Skutek podwojny: nie dostal ani
monitu o nowym APK, ani wpisu w changelogu, ktory byl kierowany dokladnie do niego.
Poprzednia poprawka (dad4bbf, porownanie "nowsza" zamiast "rozna") tego NIE naprawiala:
przy dwoch identycznych numerach oba warunki daja falsz.
Jedyny numer odporny na OTA to runtime z AndroidManifest, bo expo-updates uzywa go do
sprawdzania zgodnosci paczek i nie moze brac go z paczki. Stad `NATIVE_RUNTIME`
w `src/lib/appVersion.ts`, a serwer oddaje teraz `runtime_version` obok `version`.
Porownanie leci po runtime, z zapasowym porownaniem wersji, gdy serwer go nie poda.
Bramka wpisow w changelogu przeszla z `onlyBelowVersion` na `onlyBelowRuntime`.
`expo-application` bylby czystszym zrodlem natywnej wersji, ale to modul natywny, wiec
nie wszedlby przez OTA do ludzi na starym APK, czyli dokladnie do tych, ktorych to
dotyczy. Runtime dziala na tym, co juz maja zainstalowane.
Przy okazji tresc dialogu: pokazywala "Server: 0.3.0, App: 0.3.0", czyli dwie identyczne
liczby, bo drugi numer byl z paczki, a nie z APK. Teraz nie pokazujemy numeru
zainstalowanej wersji, bo po OTA nie mamy wiarygodnego.
Zweryfikowane na emulatorze w obie strony, na podpisanych APK:
* APK 0.2.2 (runtime 1.2) po pobraniu paczki: monit jest, tresc czytelna.
* APK 0.3.0 (runtime 1.3): monitu nie ma.
Instalacja 0.3.0 na wierzch 0.2.2 przeszla ponownie.
139 lines
5.9 KiB
Python
139 lines
5.9 KiB
Python
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.playback_events import router as playback_events_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(playback_events_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"
|
|
# `runtime_version` to jedyny numer, po którym aplikacja pozna, JAKIE APK ma
|
|
# zainstalowane. `version` z app.json przyjeżdża do niej razem z paczką OTA, więc po
|
|
# aktualizacji stary APK raportuje nową wersję i sam siebie uznaje za aktualny.
|
|
# Runtime siedzi w AndroidManifest, OTA go nie rusza. Podbijaj RAZEM z `version`,
|
|
# ale tylko przy wydaniu APK (zmiana natywna).
|
|
return {"version": "0.3.0", "runtime_version": "1.3", "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)}
|