Origin/hoster filter w /scenes + Filter modal

Dotąd nie dało się docelować sceny konkretnego hostera — search faworyzuje
xnxx/xvideos (dominują bazę), brak filtra po źródle. Diagnostyka per-hoster
(test cookie-fix, luluvid, porntrex) wymagała trafienia sceny danego tube'a.

- /scenes?origin=<substr> — exists() na PlaybackSource.origin ilike, np.
  'hqporner' łapie tube:hqpornercom
- ScenesFilterModal: sekcja "Source / hoster" (TextInput) w FilterState.origin
- ScenesScreen: filter.origin → listScenes; liczone do activeCount

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
https://github.com/goon-foss/goon 2026-05-22 12:12:50 +02:00
parent 28273eda02
commit b6e3b1cbb5
5 changed files with 38 additions and 1 deletions

View file

@ -83,6 +83,13 @@ def list_scenes(
"po PlaybackSource.quality (string typu '720p' / '1080p Full HD')." "po PlaybackSource.quality (string typu '720p' / '1080p Full HD')."
), ),
), ),
origin: str | None = Query(
default=None,
description=(
"Filtruj po playback origin (np. 'tube:hqpornercom'). Substring match — "
"'hqporner' złapie tube:hqpornercom. Diagnostyka per-hoster."
),
),
include_stubs: bool = Query( include_stubs: bool = Query(
default=False, default=False,
description=( description=(
@ -164,6 +171,18 @@ def list_scenes(
) )
) )
if origin:
# Substring match na origin — 'hqporner' złapie 'tube:hqpornercom'.
base = base.where(
exists(
select(1).where(
PlaybackSource.scene_id == Scene.id,
PlaybackSource.dead_at.is_(None),
PlaybackSource.origin.ilike(f"%{origin}%"),
)
)
)
# Blacklisty — globalne wykluczenia. Jeśli scena ma JAKIEGOKOLWIEK blacklisted # Blacklisty — globalne wykluczenia. Jeśli scena ma JAKIEGOKOLWIEK blacklisted
# performera, jest na blacklisted studio, lub ma JAKIKOLWIEK blacklisted tag → out. # performera, jest na blacklisted studio, lub ma JAKIKOLWIEK blacklisted tag → out.
from app.models.blacklist import ( from app.models.blacklist import (

View file

@ -116,6 +116,7 @@ export class GoonClient {
qs.set('released_within_days', String(params.released_within_days)); qs.set('released_within_days', String(params.released_within_days));
if (params.min_quality_p !== undefined) qs.set('min_quality_p', String(params.min_quality_p)); if (params.min_quality_p !== undefined) qs.set('min_quality_p', String(params.min_quality_p));
if (params.include_stubs !== undefined) qs.set('include_stubs', String(params.include_stubs)); if (params.include_stubs !== undefined) qs.set('include_stubs', String(params.include_stubs));
if (params.origin) qs.set('origin', params.origin);
if (params.sort) qs.set('sort', params.sort); if (params.sort) qs.set('sort', params.sort);
qs.set('page', String(params.page ?? 1)); qs.set('page', String(params.page ?? 1));
qs.set('per_page', String(params.per_page ?? 50)); qs.set('per_page', String(params.per_page ?? 50));

View file

@ -24,6 +24,7 @@ export interface FilterState {
sort: ScenesSort; sort: ScenesSort;
hasPlayback: boolean; hasPlayback: boolean;
includeStubs: boolean; includeStubs: boolean;
origin: string;
} }
export const DEFAULT_FILTER: FilterState = { export const DEFAULT_FILTER: FilterState = {
@ -33,6 +34,7 @@ export const DEFAULT_FILTER: FilterState = {
sort: 'created_at', sort: 'created_at',
hasPlayback: true, // default: ukryj sceny bez playback linka (kompletność > świeżość) hasPlayback: true, // default: ukryj sceny bez playback linka (kompletność > świeżość)
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'
}; };
const SORT_OPTIONS: { value: ScenesSort; label: string }[] = [ const SORT_OPTIONS: { value: ScenesSort; label: string }[] = [
@ -161,6 +163,18 @@ export function ScenesFilterModal({
</View> </View>
</Section> </Section>
<Section title="Source / hoster">
<TextInput
style={styles.search}
value={draft.origin}
onChangeText={(v) => setDraft({ ...draft, origin: v })}
placeholder="np. hqporner, porntrex, xnxx — puste = wszystkie"
placeholderTextColor={theme.muted}
autoCapitalize="none"
autoCorrect={false}
/>
</Section>
<Section title={`Tags (${draft.tagSlugs.length} selected)`}> <Section title={`Tags (${draft.tagSlugs.length} selected)`}>
<TextInput <TextInput
style={styles.search} style={styles.search}

View file

@ -55,6 +55,7 @@ export function ScenesScreen() {
has_playback: filter.hasPlayback || undefined, has_playback: filter.hasPlayback || undefined,
sort: filter.sort, sort: filter.sort,
include_stubs: filter.includeStubs || undefined, include_stubs: filter.includeStubs || undefined,
origin: filter.origin.trim() || undefined,
page: pageParam, page: pageParam,
per_page: PER_PAGE, per_page: PER_PAGE,
}), }),
@ -71,7 +72,8 @@ export function ScenesScreen() {
filter.tagSlugs.length + filter.tagSlugs.length +
filter.studioSlugs.length + filter.studioSlugs.length +
filter.performerIds.length + filter.performerIds.length +
(filter.hasPlayback ? 1 : 0); (filter.hasPlayback ? 1 : 0) +
(filter.origin.trim() ? 1 : 0);
return ( return (
<View style={styles.container}> <View style={styles.container}>

View file

@ -118,6 +118,7 @@ export interface ScenesListParams {
released_within_days?: number; released_within_days?: number;
min_quality_p?: number; min_quality_p?: number;
include_stubs?: boolean; include_stubs?: boolean;
origin?: string;
sort?: ScenesSort; sort?: ScenesSort;
page?: number; page?: number;
per_page?: number; per_page?: number;