fix(performers): wlasna obsada dla JAV, brak przycisku dla animacji
Domkniecie tej samej rodziny bledu co przy tagach i studiach. Pomiar rozstrzygnal, ze oba wertykaly wymagaja INNEGO rozwiazania, wiec nie ma tu jednego filtra: animacja 12 698 scen -> 0 performerow JAV 54 888 scen -> 1 392 performerow JAV: /performers dostaje parametr `vertical`, liczony na zywo, bo zdenormalizowany `performers.scene_count` z zalozenia opisuje wylacznie glowny feed (job taxonomy_counts wyklucza originy wertykalowe). Bez tego zakladka pokazywala zachodnia obsade, a wlasne nazwiska mialy licznik 0 i ladowaly na koncu listy. Po zmianie: Satomi Suzuki (137), Kei Mizushima (128), yui hatano (68). Animacja: przycisk Performers jest chowany. Zero obsady to nie brak danych, tylko wlasciwosc tresci: dla postaci rysowanych nie istnieje kanon (TPDB/StashDB ich nie maja), wiec zaden scraper nie ma czego przypisac i zaden nigdy nie bedzie mial. Filtrowanie dawaloby gwarantowanie pusta liste, czyli dokladnie ten sam objaw, ktory naprawiamy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
3818cd6141
commit
f48fbbd4ad
6 changed files with 97 additions and 11 deletions
|
|
@ -238,10 +238,72 @@ def list_performers(
|
|||
order: str = Query(default="scene_count", description="scene_count|name"),
|
||||
page: int = Query(default=1, ge=1),
|
||||
per_page: int = Query(default=50, ge=1, le=500),
|
||||
vertical: str | None = Query(
|
||||
default=None,
|
||||
description=(
|
||||
"Wertykał ('jav'): licz i pokazuj TYLKO obsadę jego scen. Brak → główny feed. "
|
||||
"Dla 'animation' zwraca pusto, bo animacja z definicji nie ma obsady."
|
||||
),
|
||||
),
|
||||
) -> PerformerListOut:
|
||||
if order not in ("scene_count", "popular", "name"):
|
||||
raise HTTPException(status_code=400, detail="order must be 'scene_count' or 'name'")
|
||||
|
||||
if vertical in VERTICALS:
|
||||
# Jak przy tagach: wertykał liczymy NA ŻYWO, bo `Performer.scene_count` z założenia
|
||||
# opisuje wyłącznie główny feed (job taxonomy_counts wyklucza originy wertykałowe).
|
||||
# Bez tego zakładka JAV pokazywała zachodnią obsadę, a jej własne 1392 nazwiska
|
||||
# miały licznik 0 i lądowały na końcu listy.
|
||||
#
|
||||
# Animacja NIE MA obsady i to nie jest brak danych, tylko wlasciwosc: dla postaci
|
||||
# rysowanych nie istnieje kanon (TPDB/StashDB ich nie maja), patrz app/verticals.py.
|
||||
# Zmierzone 2026-08-14: 12 698 scen animowanych, 0 performerów. Zapytanie odda
|
||||
# wiec pusto i tak ma byc; apka chowa tam ten przycisk.
|
||||
v_scenes = (
|
||||
select(PlaybackSource.scene_id)
|
||||
.where(
|
||||
PlaybackSource.dead_at.is_(None),
|
||||
PlaybackSource.origin.in_(VERTICALS[vertical]),
|
||||
)
|
||||
.distinct()
|
||||
.subquery()
|
||||
)
|
||||
v_count = (
|
||||
select(ScenePerformer.performer_id, func.count().label("c"))
|
||||
.join(v_scenes, v_scenes.c.scene_id == ScenePerformer.scene_id)
|
||||
.group_by(ScenePerformer.performer_id)
|
||||
.subquery()
|
||||
)
|
||||
base = (
|
||||
select(Performer, func.coalesce(v_count.c.c, 0).label("scene_count"))
|
||||
.join(v_count, v_count.c.performer_id == Performer.id)
|
||||
)
|
||||
if q:
|
||||
base = base.where(Performer.name_normalized.ilike(f"%{q.lower()}%"))
|
||||
total = session.execute(
|
||||
select(func.count()).select_from(base.subquery())
|
||||
).scalar_one()
|
||||
if order in ("scene_count", "popular"):
|
||||
ordered = base.order_by(v_count.c.c.desc(), Performer.canonical_name.asc())
|
||||
else:
|
||||
ordered = base.order_by(Performer.canonical_name.asc())
|
||||
rows = session.execute(
|
||||
ordered.offset((page - 1) * per_page).limit(per_page)
|
||||
).all()
|
||||
return PerformerListOut(
|
||||
items=[
|
||||
PerformerCount(
|
||||
id=p.id,
|
||||
canonical_name=p.canonical_name,
|
||||
slug=p.slug,
|
||||
gender=p.gender.value if p.gender else None,
|
||||
scene_count=int(c),
|
||||
)
|
||||
for p, c in rows
|
||||
],
|
||||
total=total, page=page, per_page=per_page,
|
||||
)
|
||||
|
||||
# Czyta zdenormalizowany Performer.scene_count (refresh w tle). Wcześniej agregat
|
||||
# 3M scene_performers + EXISTS playback per-request. Patrz migracja 0019.
|
||||
base = select(Performer)
|
||||
|
|
|
|||
|
|
@ -242,12 +242,15 @@ export class GoonClient {
|
|||
return this.request(`/performers/${performerId}/tags?limit=${limit}`);
|
||||
}
|
||||
|
||||
async listPerformers(params: { q?: string; order?: 'scene_count' | 'name'; page?: number; per_page?: number } = {}): Promise<PerformerListOut> {
|
||||
async listPerformers(params: { q?: string; order?: 'scene_count' | 'name'; page?: number; per_page?: number; vertical?: string } = {}): Promise<PerformerListOut> {
|
||||
const qs = new URLSearchParams();
|
||||
if (params.q) qs.set('q', params.q);
|
||||
qs.set('order', params.order ?? 'scene_count');
|
||||
qs.set('page', String(params.page ?? 1));
|
||||
qs.set('per_page', String(params.per_page ?? 100));
|
||||
// Bez tego zakladka JAV pokazywala zachodnia obsade: `performers.scene_count` liczy
|
||||
// wylacznie glowny feed, wiec 1392 nazwiska JAV mialy tam 0 i szly na koniec listy.
|
||||
if (params.vertical) qs.set('vertical', params.vertical);
|
||||
return this.request(`/performers?${qs.toString()}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,14 @@ export type ChangelogEntry = {
|
|||
};
|
||||
|
||||
export const CHANGELOG: ChangelogEntry[] = [
|
||||
{
|
||||
id: '2026-08-14e',
|
||||
date: 'August 2026',
|
||||
items: [
|
||||
'JAV now has its own performer list. Opening Performers from that tab used to show the western cast; it now shows the 1392 names that actually appear in JAV scenes.',
|
||||
'The Anime tab no longer offers a Performers button. Animation has no cast to list, so the button only ever led to an empty screen.',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2026-08-14d',
|
||||
date: 'August 2026',
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export type RootStackParamList = {
|
|||
MovieDetail: { id: string };
|
||||
// autoplay: wejście z ▶ na kafelku (quick-play), SceneDetail sam odpala najlepsze źródło.
|
||||
SceneDetail: { id: string; autoplay?: boolean };
|
||||
Performers: undefined;
|
||||
Performers: { vertical?: string } | undefined;
|
||||
// `seenSince` (ISO timestamp): jeśli ten performer jest favorited, FavoritesScreen
|
||||
// przekazuje `last_seen_at` SPRZED markFavoriteSeen. Sceny z `created_at > seenSince`
|
||||
// dostają NEW badge.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Lista performerów + tap → PerformerScenes.
|
||||
// Domyślny order: scene_count desc (najpierw popularni); search po name_normalized.
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||
import type { RouteProp } from '@react-navigation/native';
|
||||
import { formatCount } from '../lib/formatCount';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
|
@ -29,6 +30,8 @@ export function PerformersScreen() {
|
|||
const [debouncedQ, setDebouncedQ] = useState('');
|
||||
const [order, setOrder] = useState<Order>('scene_count');
|
||||
const [searchFocused, setSearchFocused] = useState(false);
|
||||
const route = useRoute<RouteProp<RootStackParamList, 'Performers'>>();
|
||||
const vertical = route.params?.vertical;
|
||||
|
||||
React.useEffect(() => {
|
||||
const t = setTimeout(() => setDebouncedQ(q), 350);
|
||||
|
|
@ -36,8 +39,9 @@ export function PerformersScreen() {
|
|||
}, [q]);
|
||||
|
||||
const { data, isLoading, error, refetch, isRefetching } = useQuery({
|
||||
queryKey: ['performers', debouncedQ, order],
|
||||
queryFn: () => client.listPerformers({ q: debouncedQ || undefined, order, per_page: 200 }),
|
||||
queryKey: ['performers', debouncedQ, order, vertical ?? null],
|
||||
queryFn: () =>
|
||||
client.listPerformers({ q: debouncedQ || undefined, order, per_page: 200, vertical }),
|
||||
});
|
||||
|
||||
const total = data?.items.length ?? 0;
|
||||
|
|
@ -89,6 +93,7 @@ export function PerformersScreen() {
|
|||
navigation.navigate('PerformerScenes', {
|
||||
id: item.id,
|
||||
name: item.canonical_name,
|
||||
vertical,
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ export function ScenesScreen() {
|
|||
const vertical = params?.vertical;
|
||||
// Wspólna nazwa wertykału do zapytań o tagi (stary `jav` też ma swój słownik).
|
||||
const verticalKey = vertical ?? (jav ? 'jav' : undefined);
|
||||
// Animacja NIE MA obsady i to nie brak danych, tylko wlasciwosc tresci: dla postaci
|
||||
// rysowanych nie istnieje kanon, wiec zaden scraper nie ma czego przypisac (0 performerow
|
||||
// na 12 698 scen, pomiar 2026-08-14). Przycisk prowadzilby do gwarantowanie pustej listy.
|
||||
const hasCast = verticalKey !== 'animation';
|
||||
const [q, setQ] = useState('');
|
||||
const [debouncedQ, setDebouncedQ] = useState('');
|
||||
const [filter, setFilter] = useState<FilterState>(DEFAULT_FILTER);
|
||||
|
|
@ -211,12 +215,16 @@ export function ScenesScreen() {
|
|||
) : null}
|
||||
|
||||
<View style={styles.subToolbar}>
|
||||
<Pressable
|
||||
style={styles.subButton}
|
||||
onPress={() => navigation.navigate('Performers')}
|
||||
>
|
||||
<Text style={styles.subButtonText}>Performers</Text>
|
||||
</Pressable>
|
||||
{hasCast ? (
|
||||
<Pressable
|
||||
style={styles.subButton}
|
||||
onPress={() =>
|
||||
navigation.navigate('Performers', verticalKey ? { vertical: verticalKey } : undefined)
|
||||
}
|
||||
>
|
||||
<Text style={styles.subButtonText}>Performers</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
<Pressable
|
||||
style={styles.subButton}
|
||||
onPress={() => navigation.navigate('Tags', verticalKey ? { vertical: verticalKey } : undefined)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue