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 <noreply@anthropic.com>
This commit is contained in:
parent
d9177febf7
commit
a1f604a7a4
10 changed files with 68 additions and 12 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export function PerformerScenesScreen() {
|
|||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<RootStackParamList, 'PerformerScenes'>>();
|
||||
const route = useRoute<RouteProp<RootStackParamList, 'PerformerScenes'>>();
|
||||
const { id, name, seenSince } = route.params;
|
||||
const { id, name, seenSince, vertical } = route.params;
|
||||
|
||||
const [tab, setTab] = React.useState<Tab>('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,
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<Pressable
|
||||
key={t.id}
|
||||
style={styles.pillSecondary}
|
||||
onPress={() => 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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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<Tab>('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,
|
||||
|
|
|
|||
|
|
@ -24,17 +24,20 @@ export function TagScenesScreen() {
|
|||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<RootStackParamList, 'TagScenes'>>();
|
||||
const route = useRoute<RouteProp<RootStackParamList, 'TagScenes'>>();
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue