feat(filtr): minimalna jakosc w filtrze scen (720p/1080p/4K)
Prosba uzytkownika (zgloszenie 17376139). Backend mial juz parametr `min_quality_p`, ale w praktyce byl NIEUZYWALNY i to zajelo wiekszosc pracy. Wydajnosc, mierzona na produkcji przez EXPLAIN ANALYZE, prog 1080p: EXISTS na playback_sources (stan zastany) 32 s 3 556 612 odwiedzonych scen + indeks funkcyjny per-scena 24 s JOIN z DISTINCT po jakosci 1,1 s W IZOLACJI ten sam JOIN + reszta filtrow feedu 52 s planer wraca do skanu po dacie kolumna scenes.max_quality_p (0029) 2,0-2,6 s + indeksy czesciowe pod progi (0030) 0,09-0,11 s Wniosek, ktory kosztowal najwiecej: ksztaltem zapytania sie tego NIE dalo zalatwic. Koszt to LICZBA odwiedzonych scen, nie pojedynczy test, a przy `ORDER BY created_at DESC LIMIT 24` planer i tak wracal do przechodzenia scen po dacie, gdy tylko doszly pozostale filtry (bramka wertykalow, heurystyka szkieletow, prog dlugosci). Stad denormalizacja + indeksy czesciowe z predykatem progu, ktore daja gotowe sortowanie. Semantyka SUROWA, swiadomie: `quality` zapisujemy oportunistycznie, zna ja 6,5% scen (210 302 z 3 236 368), najlepsze zrodlo ma 26% pokrycia. Wariant lagodny, przepuszczajacy sceny o nieznanej jakosci, odrzucony po zmierzeniu: odsiewalby 88 647 z 3,2 mln, czyli nie realizowalby intencji "nie chce ogladac ziemniaka". Filtr pokazuje wiec tylko jakosc POTWIERDZONA, a UI mowi to wprost podpisem pod przelacznikiem, bo inaczej gwaltowny spadek liczby wynikow wyglada na zepsuta aplikacje. Warianty tekstowe 4K/UHD usuniete z warunku: 0 wystapien na 238 452 zywe zrodla, a `OR ilike` blokowal uzycie indeksu. Odswiezanie w taxonomy_counts (co 3h), z czyszczeniem do NULL gdy scena traci zrodlo z jakoscia. Backfill: 210 302 sceny w 2m56s. Zweryfikowane na emulatorze: sekcja MIN QUALITY renderuje sie z podpisem ostrzegawczym. Lista wyszla pusta, ale to build debugowy dostajacy 403 (podpis debug spoza allowlisty), nie blad filtra: ten sam URL z poprawnym podpisem oddaje 50 wynikow w 0,12 s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
085c0c1dd6
commit
0fed952c5c
8 changed files with 222 additions and 23 deletions
50
alembic/versions/20260818_0029_scenes_max_quality.py
Normal file
50
alembic/versions/20260818_0029_scenes_max_quality.py
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
"""scenes.max_quality_p: zdenormalizowana najwyzsza znana jakosc zrodel sceny
|
||||||
|
|
||||||
|
Po co, skoro filtr dalo sie napisac zapytaniem: NIE dalo sie go napisac szybko.
|
||||||
|
Zmierzone na produkcji 2026-08-18 (EXPLAIN ANALYZE, prog 1080p):
|
||||||
|
|
||||||
|
EXISTS na playback_sources 32 s (3 556 612 odwiedzonych scen)
|
||||||
|
+ indeks funkcyjny per-scena 24 s
|
||||||
|
JOIN z DISTINCT po jakosci 1,1 s W IZOLACJI, ale
|
||||||
|
ten sam JOIN + reszta filtrow 52 s (planer wraca do przechodzenia scen)
|
||||||
|
|
||||||
|
Ostatni wiersz jest sednem: w polaczeniu z bramka wertykalow, heurystyka szkieletow
|
||||||
|
i progiem dlugosci planer i tak wybiera przejscie `scenes` po `created_at` i testowanie
|
||||||
|
warunku scena po scenie. Kaztaltem zapytania sie tego nie ominie, bo koszt to LICZBA
|
||||||
|
odwiedzonych scen, a nie pojedynczy test. Skalarna kolumna zdejmuje problem: staje sie
|
||||||
|
zwyklym filtrem, takim jak `duration_sec`, ktory jest szybki.
|
||||||
|
|
||||||
|
NULL = nie znamy jakosci zadnego zywego zrodla (93,5% katalogu: `quality` zapisujemy
|
||||||
|
oportunistycznie, tylko gdy listing zrodla ja podaje). Filtr celowo takie sceny ODSIEWA,
|
||||||
|
patrz komentarz przy QUALITY_OPTIONS w mobile/src/screens/ScenesFilterModal.tsx.
|
||||||
|
|
||||||
|
Revision ID: 0029_scenes_max_quality
|
||||||
|
Revises: 0028_phash_backfill_queue
|
||||||
|
Create Date: 2026-08-18
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision: str = "0029_scenes_max_quality"
|
||||||
|
down_revision: str | None = "0028_phash_backfill_queue"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column("scenes", sa.Column("max_quality_p", sa.SmallInteger(), nullable=True))
|
||||||
|
# (max_quality_p, created_at DESC): filtr to zakres po pierwszej kolumnie, a sort
|
||||||
|
# feedu idzie po drugiej. Partial, bo 93,5% scen ma NULL i nie ma po co ich indeksowac.
|
||||||
|
op.create_index(
|
||||||
|
"ix_scenes_max_quality_created",
|
||||||
|
"scenes",
|
||||||
|
["max_quality_p", sa.text("created_at DESC")],
|
||||||
|
postgresql_where=sa.text("max_quality_p IS NOT NULL"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index("ix_scenes_max_quality_created", table_name="scenes")
|
||||||
|
op.drop_column("scenes", "max_quality_p")
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
"""Indeksy czesciowe pod progi filtra jakosci (720/1080/2160)
|
||||||
|
|
||||||
|
Dlaczego osobne indeksy na prog, a nie jeden na (max_quality_p, created_at):
|
||||||
|
tamten nie da sie uzyc do `ORDER BY created_at DESC LIMIT n`, bo wynik nie jest
|
||||||
|
posortowany po created_at w obrebie ZAKRESU wartosci max_quality_p. Planer go
|
||||||
|
ignorowal i wracal do przejscia `scenes` po dacie z filtrem.
|
||||||
|
|
||||||
|
Indeks czesciowy z predykatem `max_quality_p >= N`, posortowany po created_at DESC,
|
||||||
|
jest dla progu N dokladnie tym, czego zapytanie potrzebuje: Postgres uzywa go, gdy
|
||||||
|
warunek zapytania implikuje predykat indeksu. Zmierzone na produkcji 2026-08-18:
|
||||||
|
|
||||||
|
0029 (sama kolumna) 2,0-2,6 s planer szedl ix_scenes_created_at_desc
|
||||||
|
0030 (te indeksy) 0,09-0,11 s Index Scan using ix_scenes_created_q1080
|
||||||
|
|
||||||
|
Progi sa ZWIAZANE Z UI: `QUALITY_OPTIONS` w mobile/src/screens/ScenesFilterModal.tsx
|
||||||
|
oferuje dokladnie 720/1080/2160. Inna wartosc w `?min_quality_p=` nadal dziala, tylko
|
||||||
|
wolniej (fallback do skanu po dacie). Dodajac opcje w UI, dodaj tu indeks.
|
||||||
|
|
||||||
|
Rozmiary sa pomijalne (4,3 MB / 2,7 MB / 0,3 MB), bo jakosc zna tylko 6,5% katalogu.
|
||||||
|
|
||||||
|
Revision ID: 0030_scenes_quality_idx
|
||||||
|
Revises: 0029_scenes_max_quality
|
||||||
|
Create Date: 2026-08-18
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision: str = "0030_scenes_quality_idx"
|
||||||
|
down_revision: str | None = "0029_scenes_max_quality"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
_THRESHOLDS = (720, 1080, 2160)
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
for q in _THRESHOLDS:
|
||||||
|
op.create_index(
|
||||||
|
f"ix_scenes_created_q{q}",
|
||||||
|
"scenes",
|
||||||
|
[sa.text("created_at DESC")],
|
||||||
|
postgresql_where=sa.text(f"max_quality_p >= {q}"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
for q in _THRESHOLDS:
|
||||||
|
op.drop_index(f"ix_scenes_created_q{q}", table_name="scenes")
|
||||||
|
|
@ -390,28 +390,14 @@ def list_scenes(
|
||||||
base = base.where(Scene.release_date >= cutoff)
|
base = base.where(Scene.release_date >= cutoff)
|
||||||
|
|
||||||
if min_quality_p is not None:
|
if min_quality_p is not None:
|
||||||
# PlaybackSource.quality to wolny string, szukamy liczb w prefixie ('1080p',
|
# Skalarny filtr po zdenormalizowanej kolumnie. Wcześniejsze podejścia (EXISTS
|
||||||
# '1080p Full HD', '2160p'). Heurystyka: wystarczy że scena ma JEDEN żywy
|
# na playback_sources, potem JOIN z DISTINCT) dawały 32-52 s na produkcji, bo
|
||||||
# playback z quality liczbą >= min. '4K'/'UHD' aliasujemy na 2160.
|
# planer i tak przechodził `scenes` po `created_at` scena po scenie. Pomiary
|
||||||
from sqlalchemy import Integer, cast, or_
|
# i uzasadnienie: migracja 0029_scenes_max_quality.
|
||||||
numeric_q = cast(
|
#
|
||||||
func.coalesce(func.substring(PlaybackSource.quality, r"\d+"), "0"),
|
# NULL (93,5% katalogu) NIE przechodzi, i tak ma być: filtr obiecuje jakość
|
||||||
Integer,
|
# potwierdzoną, a nie zgadywaną. UI mówi o tym wprost pod przełącznikiem.
|
||||||
)
|
base = base.where(Scene.max_quality_p >= min_quality_p)
|
||||||
conds = [numeric_q >= min_quality_p]
|
|
||||||
if min_quality_p <= 2160:
|
|
||||||
conds.append(PlaybackSource.quality.ilike("%4k%"))
|
|
||||||
conds.append(PlaybackSource.quality.ilike("%uhd%"))
|
|
||||||
base = base.where(
|
|
||||||
exists(
|
|
||||||
select(1).where(
|
|
||||||
PlaybackSource.scene_id == Scene.id,
|
|
||||||
PlaybackSource.dead_at.is_(None),
|
|
||||||
PlaybackSource.quality.isnot(None),
|
|
||||||
or_(*conds),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not include_stubs:
|
if not include_stubs:
|
||||||
# Stub scene heuristic (współdzielona z licznikiem +N ulubionych): tube-only scena
|
# Stub scene heuristic (współdzielona z licznikiem +N ulubionych): tube-only scena
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ from sqlalchemy import (
|
||||||
Float,
|
Float,
|
||||||
ForeignKey,
|
ForeignKey,
|
||||||
Integer,
|
Integer,
|
||||||
|
SmallInteger,
|
||||||
String,
|
String,
|
||||||
Text,
|
Text,
|
||||||
UniqueConstraint,
|
UniqueConstraint,
|
||||||
|
|
@ -50,6 +51,13 @@ class Scene(UUIDPKMixin, TimestampMixin, Base):
|
||||||
Boolean, nullable=False, server_default="false", index=True
|
Boolean, nullable=False, server_default="false", index=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Najwyzsza ZNANA jakosc sposrod zywych zrodel sceny (720, 1080, 2160...).
|
||||||
|
# NULL = zadne zywe zrodlo nie podalo jakosci, czyli 93,5% katalogu; filtr jakosci
|
||||||
|
# takie sceny odsiewa. Odswiezane w `app/scheduler/taxonomy_counts.py`.
|
||||||
|
# Denormalizacja jest tu KONIECZNA, nie wygodna: uzasadnienie i pomiary w migracji
|
||||||
|
# 0029_scenes_max_quality.
|
||||||
|
max_quality_p: Mapped[int | None] = mapped_column(SmallInteger)
|
||||||
|
|
||||||
|
|
||||||
class SceneExternalRef(Base):
|
class SceneExternalRef(Base):
|
||||||
__tablename__ = "scene_external_refs"
|
__tablename__ = "scene_external_refs"
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,37 @@ _PERFORMERS_SQL = text(
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# scenes.max_quality_p: najwyzsza ZNANA jakosc sposrod zywych zrodel sceny. Odswiezamy
|
||||||
|
# tu, a nie przy ingescie, bo `quality` moze dojsc pozniej (re-scrape, nowe zrodlo po
|
||||||
|
# merge), a zapytanie jest tanie: dotyka tylko scen, ktore maja zrodlo z jakoscia.
|
||||||
|
# Sceny, ktore jakosc STRACILY (zrodlo umarlo), musza wrocic do NULL, stad druga czesc.
|
||||||
|
_MAX_QUALITY_SQL = text(
|
||||||
|
r"""
|
||||||
|
WITH q AS (
|
||||||
|
SELECT ps.scene_id,
|
||||||
|
max(CAST(COALESCE(SUBSTRING(ps.quality FROM '\d+'), '0') AS INTEGER)) AS maxq
|
||||||
|
FROM playback_sources ps
|
||||||
|
WHERE ps.dead_at IS NULL AND ps.quality IS NOT NULL
|
||||||
|
GROUP BY ps.scene_id
|
||||||
|
)
|
||||||
|
UPDATE scenes s SET max_quality_p = q.maxq
|
||||||
|
FROM q
|
||||||
|
WHERE q.scene_id = s.id
|
||||||
|
AND s.max_quality_p IS DISTINCT FROM q.maxq
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
_MAX_QUALITY_CLEAR_SQL = text(
|
||||||
|
"""
|
||||||
|
UPDATE scenes s SET max_quality_p = NULL
|
||||||
|
WHERE s.max_quality_p IS NOT NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM playback_sources ps
|
||||||
|
WHERE ps.scene_id = s.id AND ps.dead_at IS NULL AND ps.quality IS NOT NULL
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
_STUDIOS_SQL = text(
|
_STUDIOS_SQL = text(
|
||||||
f"""
|
f"""
|
||||||
UPDATE studios s SET scene_count = COALESCE(a.c, 0)
|
UPDATE studios s SET scene_count = COALESCE(a.c, 0)
|
||||||
|
|
@ -112,4 +143,12 @@ def refresh_taxonomy_counts() -> dict[str, int]:
|
||||||
out[name] = res.rowcount or 0
|
out[name] = res.rowcount or 0
|
||||||
# Commit per-tabela, długie transakcje trzymałyby locki na hot tables.
|
# Commit per-tabela, długie transakcje trzymałyby locki na hot tables.
|
||||||
session.commit()
|
session.commit()
|
||||||
|
# max_quality_p, bez parametru wertykałów: jakość opisuje samo źródło, więc
|
||||||
|
# dotyczy tak samo głównego feedu jak wertykałów.
|
||||||
|
for name, stmt in (
|
||||||
|
("max_quality", _MAX_QUALITY_SQL),
|
||||||
|
("max_quality_cleared", _MAX_QUALITY_CLEAR_SQL),
|
||||||
|
):
|
||||||
|
out[name] = session.execute(stmt).rowcount or 0
|
||||||
|
session.commit()
|
||||||
return out
|
return out
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,13 @@ export type ChangelogEntry = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CHANGELOG: ChangelogEntry[] = [
|
export const CHANGELOG: ChangelogEntry[] = [
|
||||||
|
{
|
||||||
|
id: '2026-08-18c',
|
||||||
|
date: 'August 2026',
|
||||||
|
items: [
|
||||||
|
'Filter by minimum quality: 720p, 1080p or 4K, next to the length filter. It only shows scenes whose quality we actually know, and most sites never state it, so expect the list to get a lot shorter when you switch it on. The filter says so underneath.',
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: '2026-08-18b',
|
id: '2026-08-18b',
|
||||||
date: 'August 2026',
|
date: 'August 2026',
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,9 @@ export interface FilterState {
|
||||||
includeStubs: boolean;
|
includeStubs: boolean;
|
||||||
origin: string;
|
origin: string;
|
||||||
minDurationSec: number; // 0 = bazowy próg (60s); >0 podnosi minimum (filtr na klipy-reklamy)
|
minDurationSec: number; // 0 = bazowy próg (60s); >0 podnosi minimum (filtr na klipy-reklamy)
|
||||||
|
/** 0 = bez filtra. >0 = pokaż TYLKO sceny, o których WIEMY, że mają źródło w tej
|
||||||
|
* rozdzielczości lub wyższej. Patrz komentarz przy QUALITY_OPTIONS. */
|
||||||
|
minQualityP: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_FILTER: FilterState = {
|
export const DEFAULT_FILTER: FilterState = {
|
||||||
|
|
@ -37,6 +40,7 @@ export const DEFAULT_FILTER: FilterState = {
|
||||||
includeStubs: false, // default: ukryj 5-10min hqporner trailery bez release_date
|
includeStubs: false, // default: ukryj 5-10min hqporner trailery bez release_date
|
||||||
origin: '', // pusty = wszystkie źródła; substring match np. 'hqporner'
|
origin: '', // pusty = wszystkie źródła; substring match np. 'hqporner'
|
||||||
minDurationSec: 0,
|
minDurationSec: 0,
|
||||||
|
minQualityP: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SORT_OPTIONS: { value: ScenesSort; label: string }[] = [
|
const SORT_OPTIONS: { value: ScenesSort; label: string }[] = [
|
||||||
|
|
@ -56,6 +60,26 @@ const DURATION_OPTIONS: { value: number; label: string }[] = [
|
||||||
{ value: 1800, label: '30+ min' },
|
{ value: 1800, label: '30+ min' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Filtr jakości (prośba użytkownika 17376139: "filter by video quality, like 720P, 1080p").
|
||||||
|
//
|
||||||
|
// UWAGA co do zasięgu, i to jest istotne przy czytaniu wyników: `quality` zapisujemy
|
||||||
|
// oportunistycznie, tylko gdy listing źródła ją podaje. Pomiar 2026-08-18: zna ją
|
||||||
|
// **6,5% scen** (210 302 z 3 236 368), a najlepsze źródło ma 26% pokrycia.
|
||||||
|
//
|
||||||
|
// Dlatego filtr jest SUROWY: pokazuje wyłącznie sceny z potwierdzoną jakością >= progu
|
||||||
|
// (dla 1080p to 121 655 scen). Wariant łagodny, przepuszczający sceny o nieznanej
|
||||||
|
// jakości, został odrzucony po zmierzeniu: odsiewałby tylko 88 647 scen z 3,2 mln,
|
||||||
|
// czyli nie realizowałby intencji "nie chcę oglądać ziemniaka".
|
||||||
|
//
|
||||||
|
// Konsekwencja dla UI: po włączeniu filtra lista gwałtownie się kurczy i to jest
|
||||||
|
// POPRAWNE. Podpis pod przełącznikiem musi to mówić wprost, inaczej wygląda na zepsute.
|
||||||
|
const QUALITY_OPTIONS: { value: number; label: string }[] = [
|
||||||
|
{ value: 0, label: 'Any' },
|
||||||
|
{ value: 720, label: '720p+' },
|
||||||
|
{ value: 1080, label: '1080p+' },
|
||||||
|
{ value: 2160, label: '4K' },
|
||||||
|
];
|
||||||
|
|
||||||
export function ScenesFilterModal({
|
export function ScenesFilterModal({
|
||||||
visible,
|
visible,
|
||||||
initial,
|
initial,
|
||||||
|
|
@ -133,7 +157,8 @@ export function ScenesFilterModal({
|
||||||
draft.studioSlugs.length +
|
draft.studioSlugs.length +
|
||||||
draft.performerIds.length +
|
draft.performerIds.length +
|
||||||
(draft.hasPlayback ? 1 : 0) +
|
(draft.hasPlayback ? 1 : 0) +
|
||||||
(draft.minDurationSec > 0 ? 1 : 0);
|
(draft.minDurationSec > 0 ? 1 : 0) +
|
||||||
|
(draft.minQualityP > 0 ? 1 : 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal visible={visible} animationType="slide" onRequestClose={onClose}>
|
<Modal visible={visible} animationType="slide" onRequestClose={onClose}>
|
||||||
|
|
@ -214,6 +239,36 @@ export function ScenesFilterModal({
|
||||||
</View>
|
</View>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Min quality">
|
||||||
|
<View style={styles.sortRow}>
|
||||||
|
{QUALITY_OPTIONS.map((opt) => (
|
||||||
|
<Pressable
|
||||||
|
key={opt.value}
|
||||||
|
style={[
|
||||||
|
styles.sortPill,
|
||||||
|
draft.minQualityP === opt.value && styles.sortPillActive,
|
||||||
|
]}
|
||||||
|
onPress={() => setDraft({ ...draft, minQualityP: opt.value })}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.sortPillText,
|
||||||
|
draft.minQualityP === opt.value && styles.sortPillTextActive,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
{draft.minQualityP > 0 ? (
|
||||||
|
<Text style={styles.qualityNote}>
|
||||||
|
Only scenes whose quality we actually know. Most sites do not state it,
|
||||||
|
so this hides a lot, including videos that may well be {draft.minQualityP}p.
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</Section>
|
||||||
|
|
||||||
<Section title="Source / hoster">
|
<Section title="Source / hoster">
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.search}
|
style={styles.search}
|
||||||
|
|
@ -422,6 +477,9 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
sortPillActive: { borderColor: theme.accentText, backgroundColor: theme.bg },
|
sortPillActive: { borderColor: theme.accentText, backgroundColor: theme.bg },
|
||||||
sortPillText: { color: theme.fg, fontSize: 13 },
|
sortPillText: { color: theme.fg, fontSize: 13 },
|
||||||
|
// Podpis pojawia się TYLKO gdy filtr jest włączony: bez niego gwałtowny spadek
|
||||||
|
// liczby wyników wygląda na błąd aplikacji, a jest poprawnym skutkiem 6,5% pokrycia.
|
||||||
|
qualityNote: { color: theme.muted, fontSize: 12, lineHeight: 17, marginTop: 8 },
|
||||||
sortPillTextActive: { color: theme.accentText, fontWeight: '600' },
|
sortPillTextActive: { color: theme.accentText, fontWeight: '600' },
|
||||||
switchRow: {
|
switchRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ export function ScenesScreen({
|
||||||
origin: filter.origin.trim() || undefined,
|
origin: filter.origin.trim() || undefined,
|
||||||
jav: jav || undefined,
|
jav: jav || undefined,
|
||||||
vertical,
|
vertical,
|
||||||
|
min_quality_p: filter.minQualityP > 0 ? filter.minQualityP : undefined,
|
||||||
// 0 = "Any" → undefined → backend stosuje bazowy próg 60s. >0 podnosi minimum.
|
// 0 = "Any" → undefined → backend stosuje bazowy próg 60s. >0 podnosi minimum.
|
||||||
// JAV nie ma duration (javflix nie podaje) → w sekcji JAV wyłączamy próg 60s
|
// JAV nie ma duration (javflix nie podaje) → w sekcji JAV wyłączamy próg 60s
|
||||||
// (explicit 0), inaczej NULL >= 60 = false i cała sekcja byłaby pusta.
|
// (explicit 0), inaczej NULL >= 60 = false i cała sekcja byłaby pusta.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue