fix: losowanie w obrebie wynikow filtra, widoczne pobieranie aktualizacji
Zgloszenia z apki: - „random filter on and searching titles shows few results": przy sort=random braliśmy okno 2000 kolejnych id (0,05% katalogu) i dopiero na nim nakladalismy filtry, wiec szukanie w losowej kolejnosci trafialo na garstke scen. Teraz przy aktywnym filtrze (szukanie, tag, obsada, studio, strona, jakosc, data, dlugosc) losujemy WYNIKI: md5(id + seed) nad pula do 5000 wynikow filtra. Okno po PK zostaje tylko dla listy bez filtra, gdzie chronilo przed skanem 2,3 mln wierszy. - „clicking install does nothing": pobieranie 87 MB szlo bez zadnego sladu w UI. Teraz nakladka z kreceleem na czas pobierania, „Download" obok „Install" w monicie i link do pliku w komunikacie bledu (na repo git APK nie ma, user szukal go tam). Sprawdzone: szukanie „massage" z sort=random daje pelne strony (50 + kolejna), ten sam seed powtarza kolejnosc, strony sie nie nakladaja, inny seed inna kolejnosc; czasy 120-800 ms (tag anal 1,1 s), lista bez filtra bez zmian. Na emulatorze pelna sciezka aktualizacji z 0.4.0: monit z trzema przyciskami, Install, pobieranie, systemowy dialog, versionCode 15. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
45754afc69
commit
c8035c68fa
3 changed files with 100 additions and 16 deletions
|
|
@ -52,6 +52,9 @@ log = logging.getLogger(__name__)
|
|||
router = APIRouter(prefix="/scenes", tags=["scenes"], dependencies=[Depends(require_api_key)])
|
||||
|
||||
|
||||
#: Ile wynikow filtra bierze udzial w losowaniu (patrz sort=random z filtrem).
|
||||
_RANDOM_POOL = 5000
|
||||
|
||||
_VALID_SORTS = {
|
||||
"created_at",
|
||||
"release_date",
|
||||
|
|
@ -606,6 +609,19 @@ def list_scenes(
|
|||
total_capped = False
|
||||
total: int | None = _default_scene_count(session) if _is_pure_default else None
|
||||
|
||||
# Czy lista jest juz zawezona filtrem. Rozstrzyga o sposobie losowania (patrz nizej):
|
||||
# wertykalu i has_playback tu nie liczymy, bo to sa polowy katalogu, nie zawezenie.
|
||||
_random_narrow = bool(
|
||||
q
|
||||
or tag_slug_list
|
||||
or perf_id_strings
|
||||
or studio_slug_list
|
||||
or origin
|
||||
or min_quality_p is not None
|
||||
or released_within_days is not None
|
||||
or max_duration_sec is not None
|
||||
)
|
||||
|
||||
# Offset i rozmiar pobrania sa potrzebne juz przy budowie sortu: `sort=studio` musi
|
||||
# wiedziec, ile wierszy moze potrzebowac z jednego studia (patrz _studio_sorted_select).
|
||||
_off = (page - 1) * per_page
|
||||
|
|
@ -676,21 +692,37 @@ def list_scenes(
|
|||
# od losowego punktu startu JEST losowy i idzie po indeksie PK. Punkt startu
|
||||
# wynika z seeda (md5 -> uuid), wiec paginacja jest stabilna, a nowy seed
|
||||
# (pull-to-refresh w apce) daje nowe rozdanie. Za koncem zakresu lista sie konczy.
|
||||
start = uuid.UUID(hashlib.md5((seed or uuid.uuid4().hex).encode("utf-8")).hexdigest())
|
||||
# Okno kandydatow po PK z LIMIT, a filtry (EXISTS/NOT EXISTS) dopiero na nim.
|
||||
# Bez okna planer robil Merge Anti Join z indeksem playback_sources po scene_id
|
||||
# i przechodzil 2,3 mln wierszy (7,6 s), bo ORDER BY id pasowal do obu indeksow.
|
||||
# Z oknem 2000 id to nested loop po kilkudziesieciu wierszach (pomiar 2026-09-14).
|
||||
# Okno rosnie ze strona, zeby glebsze strony mialy z czego wybierac. Sekcje
|
||||
# wertykalne (JAV, Anime) to ~2% katalogu, wiec dostaja 4x szersze okno; z 2000 id
|
||||
# JAV wyciagal 27 scen i druga strona bylaby pusta. Szersze okno dla wszystkich
|
||||
# nie wchodzi w gre: z filtrem tagu 6000 id to 2,7 s (probe scene_tags per scena).
|
||||
base_window = 8000 if (vertical or jav) else 2000
|
||||
window = max(base_window, page * per_page * 4)
|
||||
cand = (
|
||||
select(Scene.id).where(Scene.id > start).order_by(Scene.id.asc()).limit(window).subquery()
|
||||
)
|
||||
ordered = base.where(Scene.id.in_(select(cand.c.id))).order_by(Scene.id.asc())
|
||||
seed_value = seed or uuid.uuid4().hex
|
||||
start = uuid.UUID(hashlib.md5(seed_value.encode("utf-8")).hexdigest())
|
||||
if _random_narrow:
|
||||
# Z filtrem (szukanie, tag, obsada, studio, strona, jakosc) okno po PK nie ma
|
||||
# sensu: 2000 kolejnych id to 0,05% katalogu, wiec wyszukiwanie w losowej
|
||||
# kolejnosci pokazywalo garstke wynikow zamiast wszystkich (zgloszenie z apki
|
||||
# 2026-09-16). Filtr sam zawezą zbior, wiec bierzemy pule jego wynikow i
|
||||
# tasujemy JA: md5(id + seed) daje ten sam porzadek na kolejnych stronach,
|
||||
# a nowy seed nowe rozdanie. Pula jest ograniczona, bo szeroki tag to setki
|
||||
# tysiecy scen, a pelne sortowanie takiego zbioru trwaloby sekundy; przy
|
||||
# typowym wyszukiwaniu (setki wynikow) pula obejmuje wszystko.
|
||||
pool = base.limit(literal_column(str(_RANDOM_POOL))).subquery()
|
||||
ordered = (
|
||||
select(Scene)
|
||||
.join(pool, Scene.id == pool.c.id)
|
||||
.order_by(func.md5(func.concat(Scene.id, seed_value)))
|
||||
)
|
||||
else:
|
||||
# Okno kandydatow po PK z LIMIT, a filtry (EXISTS/NOT EXISTS) dopiero na nim.
|
||||
# Bez okna planer robil Merge Anti Join z indeksem playback_sources po scene_id
|
||||
# i przechodzil 2,3 mln wierszy (7,6 s), bo ORDER BY id pasowal do obu indeksow.
|
||||
# Z oknem 2000 id to nested loop po kilkudziesieciu wierszach (pomiar 2026-09-14).
|
||||
# Okno rosnie ze strona, zeby glebsze strony mialy z czego wybierac. Sekcje
|
||||
# wertykalne (JAV, Anime) to ~2% katalogu, wiec dostaja 4x szersze okno; z 2000
|
||||
# id JAV wyciagal 27 scen i druga strona bylaby pusta.
|
||||
base_window = 8000 if (vertical or jav) else 2000
|
||||
window = max(base_window, page * per_page * 4)
|
||||
cand = (
|
||||
select(Scene.id).where(Scene.id > start).order_by(Scene.id.asc()).limit(window).subquery()
|
||||
)
|
||||
ordered = base.where(Scene.id.in_(select(cand.c.id))).order_by(Scene.id.asc())
|
||||
else: # created_at
|
||||
ordered = base.order_by(
|
||||
Scene.created_at.desc(), Scene.release_date.desc().nullslast(), Scene.id.desc()
|
||||
|
|
|
|||
|
|
@ -157,6 +157,10 @@ export default function App() {
|
|||
// OTA-check settled, gate'uje ekran PIN, żeby ewentualny reloadAsync (silent OTA)
|
||||
// wydarzył się PRZED pytaniem o PIN, nie po (inaczej "PIN ×2", user-report mobilism).
|
||||
const [updateSettled, setUpdateSettled] = useState(false);
|
||||
// Pobieranie APK trwa i nic nie bylo tego widac: user klikal Install, a przez minute
|
||||
// (87 MB) nie dzialo sie NIC, wiec zglaszal, ze przycisk nie dziala (zgloszenie z apki
|
||||
// 2026-09-16). Ten stan rysuje nakladke z kreceleem na czas pobierania.
|
||||
const [installing, setInstalling] = useState(false);
|
||||
const backgroundedAt = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -276,11 +280,23 @@ export default function App() {
|
|||
if (apkInstallerAvailable) {
|
||||
const pendingApkUrl = out.apk_url!;
|
||||
const runInstall = async () => {
|
||||
setInstalling(true);
|
||||
try {
|
||||
await installApk(pendingApkUrl);
|
||||
// PackageInstaller pokazuje natywny dialog, JS już nie ma co robić.
|
||||
} catch (e) {
|
||||
Alert.alert('Install failed', e instanceof Error ? e.message : String(e));
|
||||
// Gdy pobieranie albo instalacja padnie, zostaje przegladarka: plik jest
|
||||
// pod tym samym adresem (na repo git APK nie ma, pyta o to user).
|
||||
Alert.alert(
|
||||
'Install failed',
|
||||
`${e instanceof Error ? e.message : String(e)}\n\nYou can download the APK in your browser instead.`,
|
||||
[
|
||||
{ text: 'Later', style: 'cancel' },
|
||||
{ text: 'Download', onPress: () => Linking.openURL(pendingApkUrl).catch(() => {}) },
|
||||
],
|
||||
);
|
||||
} finally {
|
||||
setInstalling(false);
|
||||
}
|
||||
};
|
||||
const tryInstall = async () => {
|
||||
|
|
@ -320,8 +336,12 @@ export default function App() {
|
|||
// wyświetlały się dwie identyczne liczby („Server: 0.3.0, App: 0.3.0") i
|
||||
// monit wyglądał na pomyłkę.
|
||||
`Goon ${out.version} is ready. It adds parts that silent updates cannot deliver, so it installs like a normal app.\n\nInstall now?`,
|
||||
// Trzy przyciski to maksimum androidowego dialogu. „Download" zostaje,
|
||||
// bo instalator w apce potrafi nie wystartowac (brak zgody, padniete
|
||||
// pobieranie), a wtedy user nie ma jak wziac pliku recznie.
|
||||
[
|
||||
{ text: 'Later', style: 'cancel' },
|
||||
{ text: 'Download', onPress: () => Linking.openURL(out.apk_url!).catch(() => {}) },
|
||||
{ text: 'Install', onPress: tryInstall },
|
||||
],
|
||||
);
|
||||
|
|
@ -450,6 +470,30 @@ export default function App() {
|
|||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
{installing ? (
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
zIndex: 100,
|
||||
backgroundColor: 'rgba(0,0,0,0.75)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 32,
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator color={theme.fg} size="large" />
|
||||
<RNText style={{ color: theme.fg, marginTop: 16, fontSize: 15, textAlign: 'center' }}>
|
||||
Downloading the update
|
||||
</RNText>
|
||||
<RNText style={{ color: theme.muted, marginTop: 8, fontSize: 13, textAlign: 'center', lineHeight: 19 }}>
|
||||
About 87 MB. Android will ask you to confirm the install when it finishes.
|
||||
</RNText>
|
||||
</View>
|
||||
) : null}
|
||||
{client ? (
|
||||
<ClientProvider client={client}>
|
||||
<PreferencesProvider>
|
||||
|
|
|
|||
|
|
@ -35,6 +35,14 @@ export type ChangelogEntry = {
|
|||
};
|
||||
|
||||
export const CHANGELOG: ChangelogEntry[] = [
|
||||
{
|
||||
id: '2026-09-17a',
|
||||
date: 'September 2026',
|
||||
items: [
|
||||
'Random order now shuffles what your filter or search actually found. Searching with random on used to return a handful of results, because the shuffle was picked before the search ran.',
|
||||
'The update prompt shows that the download is running (it is 87 MB and nothing was visible for a minute), and it offers a plain browser download next to the in-app install.',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2026-09-16d',
|
||||
date: 'September 2026',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue