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)])
|
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 = {
|
_VALID_SORTS = {
|
||||||
"created_at",
|
"created_at",
|
||||||
"release_date",
|
"release_date",
|
||||||
|
|
@ -606,6 +609,19 @@ def list_scenes(
|
||||||
total_capped = False
|
total_capped = False
|
||||||
total: int | None = _default_scene_count(session) if _is_pure_default else None
|
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
|
# 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).
|
# wiedziec, ile wierszy moze potrzebowac z jednego studia (patrz _studio_sorted_select).
|
||||||
_off = (page - 1) * per_page
|
_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
|
# 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
|
# 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.
|
# (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())
|
seed_value = seed or uuid.uuid4().hex
|
||||||
# Okno kandydatow po PK z LIMIT, a filtry (EXISTS/NOT EXISTS) dopiero na nim.
|
start = uuid.UUID(hashlib.md5(seed_value.encode("utf-8")).hexdigest())
|
||||||
# Bez okna planer robil Merge Anti Join z indeksem playback_sources po scene_id
|
if _random_narrow:
|
||||||
# i przechodzil 2,3 mln wierszy (7,6 s), bo ORDER BY id pasowal do obu indeksow.
|
# Z filtrem (szukanie, tag, obsada, studio, strona, jakosc) okno po PK nie ma
|
||||||
# Z oknem 2000 id to nested loop po kilkudziesieciu wierszach (pomiar 2026-09-14).
|
# sensu: 2000 kolejnych id to 0,05% katalogu, wiec wyszukiwanie w losowej
|
||||||
# Okno rosnie ze strona, zeby glebsze strony mialy z czego wybierac. Sekcje
|
# kolejnosci pokazywalo garstke wynikow zamiast wszystkich (zgloszenie z apki
|
||||||
# wertykalne (JAV, Anime) to ~2% katalogu, wiec dostaja 4x szersze okno; z 2000 id
|
# 2026-09-16). Filtr sam zawezą zbior, wiec bierzemy pule jego wynikow i
|
||||||
# JAV wyciagal 27 scen i druga strona bylaby pusta. Szersze okno dla wszystkich
|
# tasujemy JA: md5(id + seed) daje ten sam porzadek na kolejnych stronach,
|
||||||
# nie wchodzi w gre: z filtrem tagu 6000 id to 2,7 s (probe scene_tags per scena).
|
# a nowy seed nowe rozdanie. Pula jest ograniczona, bo szeroki tag to setki
|
||||||
base_window = 8000 if (vertical or jav) else 2000
|
# tysiecy scen, a pelne sortowanie takiego zbioru trwaloby sekundy; przy
|
||||||
window = max(base_window, page * per_page * 4)
|
# typowym wyszukiwaniu (setki wynikow) pula obejmuje wszystko.
|
||||||
cand = (
|
pool = base.limit(literal_column(str(_RANDOM_POOL))).subquery()
|
||||||
select(Scene.id).where(Scene.id > start).order_by(Scene.id.asc()).limit(window).subquery()
|
ordered = (
|
||||||
)
|
select(Scene)
|
||||||
ordered = base.where(Scene.id.in_(select(cand.c.id))).order_by(Scene.id.asc())
|
.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
|
else: # created_at
|
||||||
ordered = base.order_by(
|
ordered = base.order_by(
|
||||||
Scene.created_at.desc(), Scene.release_date.desc().nullslast(), Scene.id.desc()
|
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)
|
// 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).
|
// wydarzył się PRZED pytaniem o PIN, nie po (inaczej "PIN ×2", user-report mobilism).
|
||||||
const [updateSettled, setUpdateSettled] = useState(false);
|
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);
|
const backgroundedAt = useRef<number | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -276,11 +280,23 @@ export default function App() {
|
||||||
if (apkInstallerAvailable) {
|
if (apkInstallerAvailable) {
|
||||||
const pendingApkUrl = out.apk_url!;
|
const pendingApkUrl = out.apk_url!;
|
||||||
const runInstall = async () => {
|
const runInstall = async () => {
|
||||||
|
setInstalling(true);
|
||||||
try {
|
try {
|
||||||
await installApk(pendingApkUrl);
|
await installApk(pendingApkUrl);
|
||||||
// PackageInstaller pokazuje natywny dialog, JS już nie ma co robić.
|
// PackageInstaller pokazuje natywny dialog, JS już nie ma co robić.
|
||||||
} catch (e) {
|
} 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 () => {
|
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
|
// wyświetlały się dwie identyczne liczby („Server: 0.3.0, App: 0.3.0") i
|
||||||
// monit wyglądał na pomyłkę.
|
// 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?`,
|
`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: 'Later', style: 'cancel' },
|
||||||
|
{ text: 'Download', onPress: () => Linking.openURL(out.apk_url!).catch(() => {}) },
|
||||||
{ text: 'Install', onPress: tryInstall },
|
{ text: 'Install', onPress: tryInstall },
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
@ -450,6 +470,30 @@ export default function App() {
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : 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 ? (
|
{client ? (
|
||||||
<ClientProvider client={client}>
|
<ClientProvider client={client}>
|
||||||
<PreferencesProvider>
|
<PreferencesProvider>
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,14 @@ export type ChangelogEntry = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CHANGELOG: 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',
|
id: '2026-09-16d',
|
||||||
date: 'September 2026',
|
date: 'September 2026',
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue