From a1f604a7a4d15e2e2c9dda3ce3f478b3dff975b0 Mon Sep 17 00:00:00 2001 From: goon-foss Date: Fri, 14 Aug 2026 11:41:40 +0200 Subject: [PATCH] fix(verticals): przenos wertykal przy przejsciu do tagu/studia/performera Zgloszenia 195ae42e i e9de414c: klikniecie studia w anime nic nie robilo, a tag kliniety w anime pokazywal sceny spoza anime. Jedna przyczyna: TagScenes/StudioScenes/PerformerScenes wolaly listScenes BEZ parametru vertical, a backend bez niego wyklucza wszystkie wertykaly. Studio animacyjne mialo wiec zero scen (pusty ekran = wyglada na martwy klik), a tag zwracal wylacznie sceny glownego katalogu. SceneOut dostaje pole "vertical" liczone z originow zrodel, czyli jedno zrodlo prawdy w app/verticals.py zamiast dublowania listy originow w apce. W trybie light dociagamy origin z tego samego wiersza co miniaturka, wiec zero dodatkowych zapytan. SceneDetail przekazuje wartosc dalej w paramach nawigacji. Dotyczy tak samo sekcji JAV, ktora miala ten sam blad. Co-Authored-By: Claude Opus 5 --- app/api/scenes.py | 12 ++++++++++- app/api/schemas.py | 6 ++++++ app/verticals.py | 21 ++++++++++++++++++++ mobile/src/changelog.ts | 7 +++++++ mobile/src/navigation.tsx | 6 +++--- mobile/src/screens/PerformerScenesScreen.tsx | 5 +++-- mobile/src/screens/SceneDetailScreen.tsx | 7 +++++-- mobile/src/screens/StudioScenesScreen.tsx | 5 +++-- mobile/src/screens/TagScenesScreen.tsx | 7 +++++-- mobile/src/types.ts | 4 ++++ 10 files changed, 68 insertions(+), 12 deletions(-) diff --git a/app/api/scenes.py b/app/api/scenes.py index 5bfb892..9ffe92e 100644 --- a/app/api/scenes.py +++ b/app/api/scenes.py @@ -856,6 +856,8 @@ def _build_scenes_out_batch( # 5) Playback sources. Light mode: tylko miniaturka (jedna na scenę), kafelek # czyta wyłącznie playback_sources[].thumbnail_url / animated_thumbnail_url. pb_by_scene: dict = defaultdict(list) + # origin per scena, wyłącznie do policzenia `vertical` (obie ścieżki go wypełniają). + origins_by_scene: dict = {} if light: pb_light = session.execute( select( @@ -863,6 +865,10 @@ def _build_scenes_out_batch( PlaybackSource.thumbnail_url, PlaybackSource.animated_thumbnail_url, PlaybackSource.page_url, + # origin dociągamy wyłącznie po to, żeby policzyć `vertical` (tania kolumna + # z tego samego wiersza, zero dodatkowych zapytań). Do samego kafelka + # nieużywany, slim wpis niżej dostaje origin="". + PlaybackSource.origin, ) .where( PlaybackSource.scene_id.in_(scene_ids), @@ -878,7 +884,8 @@ def _build_scenes_out_batch( thumb_by_scene: dict = {} thumb_fallback: dict = {} anim_by_scene: dict = {} - for sid, thumb, anim, page_url in pb_light: + for sid, thumb, anim, page_url, origin in pb_light: + origins_by_scene.setdefault(sid, []).append(origin) sxy = _sxyprn_thumb_url(page_url) yp = _youporn_thumb_url(page_url, thumb) if sxy: @@ -936,6 +943,7 @@ def _build_scenes_out_batch( .order_by(PlaybackSource.origin.asc()) ).scalars().all() for p in pb_rows: + origins_by_scene.setdefault(p.scene_id, []).append(p.origin) out = PlaybackSourceOut.model_validate(p) # sxyprn/trafficdeposit stored thumb rotuje (404 po ~tygodniach). SceneDetail # bierze PIERWSZE źródło z thumbem (origin ASC → sxyprncom często pierwsze), więc @@ -997,6 +1005,7 @@ def _build_scenes_out_batch( tags=tags_by_scene.get(scene.id, []), external_refs=refs_by_scene.get(scene.id, []), playback_sources=pb_by_scene.get(scene.id, []), + vertical=verticals.vertical_for_origins(origins_by_scene.get(scene.id, ())), created_at=scene.created_at, backfill=scene.backfill, last_played_at=progress.last_played_at if progress else None, @@ -1145,6 +1154,7 @@ def _build_scene_out(session: Session, scene: Scene, *, device_id: str = LEGACY_ tags=tags_out, external_refs=refs_out, playback_sources=playback_out, + vertical=verticals.vertical_for_origins(p.origin for p in playback_rows), created_at=scene.created_at, backfill=scene.backfill, last_played_at=progress.last_played_at if progress else None, diff --git a/app/api/schemas.py b/app/api/schemas.py index 73aa31b..9a383fd 100644 --- a/app/api/schemas.py +++ b/app/api/schemas.py @@ -66,6 +66,12 @@ class SceneOut(BaseModel): tags: list[TagOut] = [] external_refs: list[ExternalRefOut] = [] playback_sources: list[PlaybackSourceOut] = [] + # Wertykał sceny ('jav' / 'animation') albo None dla głównego katalogu. Liczony + # z originów źródeł, patrz `app/verticals.py`. Mobile MUSI go przekazywać dalej przy + # nawigacji do tagu/studia/performera: bez `?vertical=` backend wyklucza wszystkie + # wertykały, więc tag z anime pokazywał sceny spoza anime, a studio animacyjne + # wyglądało na martwe (zgłoszenia 195ae42e / e9de414c, 2026-08-14). + vertical: str | None = None # Kiedy scena trafiła do bazy (ingest). Używane przez mobile do oznaczenia # "NEW" na karcie scen w PerformerScenesScreen / StudioScenesScreen, gdy # `created_at > last_seen_at` (favorite) → badge. diff --git a/app/verticals.py b/app/verticals.py index 9943c68..0a81c49 100644 --- a/app/verticals.py +++ b/app/verticals.py @@ -55,3 +55,24 @@ def origins_for(vertical: str | None) -> tuple[str, ...]: if vertical is None: return ALL_VERTICAL_ORIGINS return VERTICALS.get(vertical, ()) + + +#: Odwrotna mapa origin → wertykał. Budowana raz, bo `vertical_for_origins` leci +#: dla każdej sceny w odpowiedzi. +_ORIGIN_TO_VERTICAL: dict[str, str] = { + origin: name for name, origins in VERTICALS.items() for origin in origins +} + + +def vertical_for_origins(origins) -> str | None: + """Wertykał sceny na podstawie originów jej źródeł, albo None dla głównego katalogu. + + Scena wertykałowa ma WYŁĄCZNIE originy swojego wertykału (patrz docstring modułu), + więc wystarczy pierwszy trafiony. Kolejność wyniku jest deterministyczna, bo pytamy + o konkretną scenę, a nie o zbiór. + """ + for origin in origins: + vertical = _ORIGIN_TO_VERTICAL.get(origin) + if vertical is not None: + return vertical + return None diff --git a/mobile/src/changelog.ts b/mobile/src/changelog.ts index 369c8d0..1026dab 100644 --- a/mobile/src/changelog.ts +++ b/mobile/src/changelog.ts @@ -16,6 +16,13 @@ export type ChangelogEntry = { }; export const CHANGELOG: ChangelogEntry[] = [ + { + id: '2026-08-14c', + date: 'August 2026', + items: [ + 'Anime: tapping a tag or a studio now stays inside the Anime section. Before, a tag opened from an anime scene showed everything except anime, and an anime studio looked like it did nothing because its page came up empty. The JAV section had the same bug and is fixed too.', + ], + }, { id: '2026-08-14b', date: 'August 2026', diff --git a/mobile/src/navigation.tsx b/mobile/src/navigation.tsx index a6134e4..8d984fd 100644 --- a/mobile/src/navigation.tsx +++ b/mobile/src/navigation.tsx @@ -51,16 +51,16 @@ export type RootStackParamList = { // `seenSince` (ISO timestamp): jeśli ten performer jest favorited, FavoritesScreen // przekazuje `last_seen_at` SPRZED markFavoriteSeen. Sceny z `created_at > seenSince` // dostają NEW badge. - PerformerScenes: { id: string; name: string; seenSince?: string }; + PerformerScenes: { id: string; name: string; seenSince?: string; vertical?: string | null }; // StudioScenes: `id` to studio.slug (do filter scen `studio_slugs`), // `studioId` to UUID (do favorite/blacklist API). Backend filtruje scen po slug, // ale favorites + blacklist używają UUID. - StudioScenes: { id: string; name: string; studioId: string; seenSince?: string }; + StudioScenes: { id: string; name: string; studioId: string; seenSince?: string; vertical?: string | null }; // StudioMovies: `slug` do filter movies `studio_slugs`, `studioId` (UUID) do favorite. StudioMovies: { slug: string; name: string; studioId: string }; Favorites: undefined; Tags: undefined; - TagScenes: { slug: string; name: string }; + TagScenes: { slug: string; name: string; vertical?: string | null }; AppLockSettings: undefined; Blacklist: undefined; Donate: undefined; diff --git a/mobile/src/screens/PerformerScenesScreen.tsx b/mobile/src/screens/PerformerScenesScreen.tsx index b21b22d..06db505 100644 --- a/mobile/src/screens/PerformerScenesScreen.tsx +++ b/mobile/src/screens/PerformerScenesScreen.tsx @@ -39,7 +39,7 @@ export function PerformerScenesScreen() { const navigation = useNavigation>(); const route = useRoute>(); - const { id, name, seenSince } = route.params; + const { id, name, seenSince, vertical } = route.params; const [tab, setTab] = React.useState('scenes'); @@ -143,10 +143,11 @@ export function PerformerScenesScreen() { const [selectedTag, setSelectedTag] = React.useState<{ slug: string; name: string } | null>(null); const scenesQuery = useQuery({ - queryKey: ['performer-scenes', id, selectedTag?.slug ?? null], + queryKey: ['performer-scenes', id, selectedTag?.slug ?? null, vertical ?? null], queryFn: () => client.listScenes({ performer_ids: [id], + vertical: vertical ?? undefined, tags: selectedTag ? [selectedTag.slug] : undefined, sort: 'release_date', per_page: 200, diff --git a/mobile/src/screens/SceneDetailScreen.tsx b/mobile/src/screens/SceneDetailScreen.tsx index 3975298..cb9d27e 100644 --- a/mobile/src/screens/SceneDetailScreen.tsx +++ b/mobile/src/screens/SceneDetailScreen.tsx @@ -280,7 +280,7 @@ export function SceneDetailScreen() { key={p.id} style={styles.pillPrimary} onPress={() => - nav.push('PerformerScenes', { id: p.id, name: p.canonical_name }) + nav.push('PerformerScenes', { id: p.id, name: p.canonical_name, vertical: data.vertical }) } onLongPress={() => onPerformerLongPress(p.id, p.canonical_name)} delayLongPress={400} @@ -306,6 +306,9 @@ export function SceneDetailScreen() { id: data.studio!.slug, studioId: data.studio!.id, name: data.studio!.name, + // Bez tego backend odcina wertykał i studio animacyjne wychodzi + // puste, co wyglądało jakby kliknięcie nic nie robiło (195ae42e). + vertical: data.vertical, }) } > @@ -325,7 +328,7 @@ export function SceneDetailScreen() { nav.push('TagScenes', { slug: t.slug, name: t.name })} + onPress={() => nav.push('TagScenes', { slug: t.slug, name: t.name, vertical: data.vertical })} onLongPress={() => onTagLongPress(t.id, t.name)} delayLongPress={500} > diff --git a/mobile/src/screens/StudioScenesScreen.tsx b/mobile/src/screens/StudioScenesScreen.tsx index e14e0db..866857c 100644 --- a/mobile/src/screens/StudioScenesScreen.tsx +++ b/mobile/src/screens/StudioScenesScreen.tsx @@ -47,7 +47,7 @@ export function StudioScenesScreen() { // backend ScenesListParams.studio_slugs. Trzymamy tę nazwę dla spójności z // PerformerScenes, ale w handlerach favorite/blacklist używamy studioId (UUID) // który dodajemy jako kolejny param. - const { id: slug, name, seenSince, studioId } = route.params; + const { id: slug, name, seenSince, studioId, vertical } = route.params; const [tab, setTab] = React.useState('scenes'); const favoritesQuery = useQuery({ @@ -120,10 +120,11 @@ export function StudioScenesScreen() { }, [navigation, name, isFavorite, addMutation.isPending, removeMutation.isPending]); const { data, isLoading, error, refetch, isRefetching } = useQuery({ - queryKey: ['studio-scenes', slug], + queryKey: ['studio-scenes', slug, vertical ?? null], queryFn: () => client.listScenes({ studio_slugs: [slug], + vertical: vertical ?? undefined, sort: 'release_date', per_page: 200, has_playback: true, diff --git a/mobile/src/screens/TagScenesScreen.tsx b/mobile/src/screens/TagScenesScreen.tsx index 21caf96..20a842b 100644 --- a/mobile/src/screens/TagScenesScreen.tsx +++ b/mobile/src/screens/TagScenesScreen.tsx @@ -24,17 +24,20 @@ export function TagScenesScreen() { const navigation = useNavigation>(); const route = useRoute>(); - const { slug, name } = route.params; + const { slug, name, vertical } = route.params; React.useLayoutEffect(() => { navigation.setOptions({ title: name }); }, [navigation, name]); const { data, isLoading, error, refetch, isRefetching } = useQuery({ - queryKey: ['tag-scenes', slug], + queryKey: ['tag-scenes', slug, vertical ?? null], queryFn: () => client.listScenes({ tags: [slug], + // Wertykał przyniesiony ze sceny. Bez niego backend wycina wszystkie sekcje + // poboczne, więc tag kliknięty w anime pokazywał sceny spoza anime (e9de414c). + vertical: vertical ?? undefined, sort: 'release_date', has_playback: true, per_page: 200, diff --git a/mobile/src/types.ts b/mobile/src/types.ts index 8f367e1..aadd598 100644 --- a/mobile/src/types.ts +++ b/mobile/src/types.ts @@ -173,6 +173,10 @@ export interface SceneOut { tags: TagOut[]; external_refs: ExternalRef[]; playback_sources: PlaybackSource[]; + /** Sekcja, do której należy scena ('animation' | 'jav'), albo brak dla głównego + * katalogu. Przekazuj dalej przy przejściu do tagu/studia/performera, inaczej + * backend odetnie wertykał i lista wyjdzie pusta albo z obcymi scenami. */ + vertical?: string | null; // Kiedy scena trafiła do bazy (ingest). Używane do oznaczenia "NEW", gdy // `created_at > favoriteSeenSince` (param przekazany z FavoritesScreen). created_at?: string | null;