diff --git a/app/api/taxonomies.py b/app/api/taxonomies.py index 1d334a1..c444d74 100644 --- a/app/api/taxonomies.py +++ b/app/api/taxonomies.py @@ -9,6 +9,7 @@ Zwraca też scene_count żeby UI pokazywał "(123)" przy każdym tagu/performerz """ from __future__ import annotations +import logging import uuid from typing import Annotated @@ -28,6 +29,8 @@ from app.models.studio import Studio from app.models.tag import Tag from app.verticals import VERTICALS +log = logging.getLogger(__name__) + router = APIRouter(tags=["taxonomies"], dependencies=[Depends(require_api_key)]) @@ -440,6 +443,10 @@ class PerformerRefreshOut(BaseModel): counters: dict[str, dict[str, int]] new_scenes: int last_searched_at: str | None + # "started" = wyszukiwanie ruszyło w tle i sceny dopiszą się za chwilę. Pole doszło + # przy przejściu na async (zgłoszenie 23c64a28); klient sprzed tej zmiany go nie + # czyta i pokaże "0 new scenes", co jest znośne przez okno do publikacji OTA. + status: str = "done" class PerformerRescrapeOut(BaseModel): @@ -704,11 +711,18 @@ def refresh_performer( performer_id: uuid.UUID, session: Annotated[Session, Depends(get_session)], ) -> PerformerRefreshOut: - """On-demand search across all tubes dla pojedynczego performera. Synchronous, - blokujemy aż search skończy. Mobile pokazuje spinner. + """On-demand search across all tubes dla pojedynczego performera. Startuje w TLE + i odpowiada od razu (`status="started"`), a sceny dopisują się w miarę mielenia. - Rate-guard: jeśli refresh był < 60s temu, zwraca cached result (HTTP 429-style - detail). Continuous worker w tle też robi swoje, więc cache jest częsty. + Dlaczego nie synchronicznie, jak do 2026-08-26: search po ~25 tubach trwa dłużej, + niż klient jest gotów czekać, i przy rozbudowanych performerach telefon zrywał + połączenie. W logu zostawały starty `performer_driven` bez ANI JEDNEJ domkniętej + odpowiedzi (Cassie Del Isla, 871 scen, 4 zerwane requesty), użytkownik widział + "Refresh failed", klikał znowu i mieliliśmy to samo kilka razy równolegle + (zgłoszenie 23c64a28 "Search more scenes button not working"). + + Rate-guard bez zmian: refresh < 60s temu → 429. `last_searched_at` stawiamy PRZED + startem wątku, więc guard chroni też przed dublowaniem samego startu. """ from datetime import UTC as _UTC, datetime as _dt, timedelta as _td @@ -724,27 +738,37 @@ def refresh_performer( detail=f"recently searched {int(elapsed.total_seconds())}s ago, try in a bit", ) - # Lazy import, performer_driven ma ciężki connector tree - from app.scheduler.performer_driven import run_performer_driven - - # NOTE: ten request blokuje request thread API na 30-90s (search across ~25 tubes). - # Akceptowalne dla self-hosted single-user. W razie potrzeby dorobić task queue. - counters_obj = run_performer_driven( - performer_ids=[performer_id], - top_n=0, - per_performer_limit=200, - ) - - # Update last_searched_at + counter (tak samo jak continuous worker) perf.last_searched_at = _dt.now(_UTC) perf.search_run_count = (perf.search_run_count or 0) + 1 session.commit() + name = perf.canonical_name + searched_at = perf.last_searched_at.isoformat() + + def _run_in_background() -> None: + # Własny import i własna obsługa błędów: wątek nie ma requestu, któremu mógłby + # oddać wyjątek, a `run_performer_driven` otwiera sesje DB samodzielnie. + try: + from app.scheduler.performer_driven import run_performer_driven + + counters_obj = run_performer_driven( + performer_ids=[performer_id], + top_n=0, + per_performer_limit=200, + ) + new_total = sum(s.get("new", 0) for s in counters_obj.per_source.values()) + log.info("refresh-performer %s (%s): tlo skonczone, new=%d", performer_id, name, new_total) + except Exception: + log.exception("refresh-performer %s (%s): tlo padlo", performer_id, name) + + import threading + + threading.Thread(target=_run_in_background, name=f"perf-refresh-{performer_id}", daemon=True).start() - new_total = sum(s.get("new", 0) for s in counters_obj.per_source.values()) return PerformerRefreshOut( performer_id=performer_id, - canonical_name=perf.canonical_name, - counters=counters_obj.per_source, - new_scenes=new_total, - last_searched_at=perf.last_searched_at.isoformat() if perf.last_searched_at else None, + canonical_name=name, + counters={}, + new_scenes=0, + last_searched_at=searched_at, + status="started", ) diff --git a/mobile/src/api.ts b/mobile/src/api.ts index 1c3b75f..1340d17 100644 --- a/mobile/src/api.ts +++ b/mobile/src/api.ts @@ -268,6 +268,8 @@ export class GoonClient { counters: Record>; new_scenes: number; last_searched_at: string | null; + /** "started" = backend mieli w tle i sceny dopiszą się za chwilę (od 2026-08-26). */ + status?: string; }> { return this.request(`/performers/${performerId}/refresh?pages=${pages}`, { method: 'POST', diff --git a/mobile/src/changelog.ts b/mobile/src/changelog.ts index 0f0e051..2af7524 100644 --- a/mobile/src/changelog.ts +++ b/mobile/src/changelog.ts @@ -29,6 +29,14 @@ export type ChangelogEntry = { }; export const CHANGELOG: ChangelogEntry[] = [ + { + id: '2026-08-26a', + date: 'August 2026', + items: [ + '"Search more scenes" on a performer now answers right away and keeps working in the background. It used to look broken on performers with lots of scenes: the search took longer than the phone keeps a connection open, so you saw "Refresh failed" even though the search actually ran. New finds show up within a minute or two.', + 'Movie hosts that quietly moved to a new address (myvidplay, doply) play again. The app was being handed the old address and its pop-up blocker stopped the jump to the new one, which looked like a black screen.', + ], + }, { id: '2026-08-24b', date: 'August 2026', diff --git a/mobile/src/screens/PerformerScenesScreen.tsx b/mobile/src/screens/PerformerScenesScreen.tsx index d933898..7606453 100644 --- a/mobile/src/screens/PerformerScenesScreen.tsx +++ b/mobile/src/screens/PerformerScenesScreen.tsx @@ -72,6 +72,21 @@ export function PerformerScenesScreen() { const refreshMutation = useMutation({ mutationFn: () => client.refreshPerformer(id), onSuccess: (data) => { + // Od 2026-08-26 backend startuje wyszukiwanie w tle i odpowiada od razu. + // Poprzedni tryb (czekanie na wynik) trwał dłużej, niż telefon utrzymuje + // połączenie: przy rozbudowanych performerach każdy tap kończył się "Refresh + // failed", mimo że sceny i tak dopisywały się w tle (zgłoszenie 23c64a28). + // Listę odświeżamy z opóźnieniem, żeby pierwsze znaleziska były już zapisane. + if (data.status === 'started') { + Alert.alert( + 'Searching…', + 'Checking all sites for new scenes in the background. New finds appear within a minute or two; pull down to refresh.', + ); + setTimeout(() => { + queryClient.invalidateQueries({ queryKey: ['performer-scenes', id] }); + }, 60_000); + return; + } queryClient.invalidateQueries({ queryKey: ['performer-scenes', id] }); Alert.alert( 'Refresh complete',