Tapniecie nazwy tworcy otwiera jego strone: siatka wszystkich klipow, a tapniecie kafelka gra je na pelnym ekranie z tym samym przewijaniem w pionie co Edge. Zgloszenie z apki 2026-09-15: "cannot tap username as it's hidden behind icon tray". - ClipPager: pionowy pager z dwoma playerami wyjety ze SwipeScreen, wspolny dla Edge i strony tworcy; feed, telemetria i autoSwipe zostaly w ekranach - /clips?creator= oddaje `total` (liczba klipow tworcy) do naglowka - ze strony tworcy leca tylko like/unlike i "nie interesuje": ogladanie stad nie moze zabetonowac feedu na jednym tworcy Sprawdzone na emulatorze: link z Edge otwiera strone (@lilylovesyou2, 2 clips, verified), kafelki trzymaja trzecia szerokosci takze w niepelnym rzedzie, pelny ekran gra i zamyka sie przyciskiem pod paskiem stanu, a Edge dalej odklada zdarzenia w clip_feed_events. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
525 lines
12 KiB
TypeScript
525 lines
12 KiB
TypeScript
// Lustro modeli z FastAPI app/api/schemas.py + admin endpoints.
|
|
|
|
export interface ExternalRef {
|
|
source: string;
|
|
external_id: string;
|
|
url?: string | null;
|
|
last_seen?: string | null;
|
|
}
|
|
|
|
export interface StudioOut {
|
|
id: string;
|
|
name: string;
|
|
slug: string;
|
|
network?: string | null;
|
|
}
|
|
|
|
export interface PerformerOut {
|
|
id: string;
|
|
canonical_name: string;
|
|
slug: string;
|
|
gender?: string | null;
|
|
as_alias?: string | null;
|
|
}
|
|
|
|
export interface TagOut {
|
|
id: string;
|
|
name: string;
|
|
slug: string;
|
|
}
|
|
|
|
export interface PlaybackSource {
|
|
id: string;
|
|
origin: string;
|
|
page_url: string;
|
|
embed_url?: string | null;
|
|
stream_url?: string | null;
|
|
quality?: string | null;
|
|
duration_sec?: number | null;
|
|
thumbnail_url?: string | null;
|
|
animated_thumbnail_url?: string | null;
|
|
}
|
|
|
|
export interface StreamLink {
|
|
// proxy URL (fallback gdy direct fails). Backend re-fetchuje content z VPS IP +
|
|
// streamuje do mobile.
|
|
stream_url?: string | null;
|
|
// hoster embed URL, mobile otwiera w WebView gdy `type=hoster`.
|
|
embed_url?: string | null;
|
|
// raw CDN URL + headers do bezpośredniego fetchu z urządzenia (preferred, 0
|
|
// bandwidth przez VPS). Mobile próbuje to PIERWSZE; fallback na `stream_url` na error.
|
|
direct_url?: string | null;
|
|
headers?: Record<string, string> | null;
|
|
quality?: string | null;
|
|
type?: string | null;
|
|
// Backend metadata (origin, host, part_label dla paradisehill multipart, mobile_direct_ok...).
|
|
// Untyped bag, mobile czyta selektywnie (np. raw.part_label w movie part-pickerze).
|
|
raw?: Record<string, unknown> | null;
|
|
}
|
|
|
|
export interface ResolveOut {
|
|
source: PlaybackSource;
|
|
best?: StreamLink | null;
|
|
links: StreamLink[];
|
|
}
|
|
|
|
export interface TagCount {
|
|
id: string;
|
|
name: string;
|
|
slug: string;
|
|
scene_count: number;
|
|
}
|
|
|
|
export interface TagListOut {
|
|
items: TagCount[];
|
|
total: number;
|
|
page: number;
|
|
per_page: number;
|
|
}
|
|
|
|
export interface PerformerCount {
|
|
id: string;
|
|
canonical_name: string;
|
|
slug: string;
|
|
gender?: string | null;
|
|
scene_count: number;
|
|
}
|
|
|
|
export interface PerformerListOut {
|
|
items: PerformerCount[];
|
|
total: number;
|
|
page: number;
|
|
per_page: number;
|
|
}
|
|
|
|
export interface StudioCount {
|
|
id: string;
|
|
name: string;
|
|
slug: string;
|
|
network?: string | null;
|
|
scene_count: number;
|
|
}
|
|
|
|
export interface StudioListOut {
|
|
items: StudioCount[];
|
|
total: number;
|
|
page: number;
|
|
per_page: number;
|
|
}
|
|
|
|
export interface SourceRating {
|
|
stars: number; // 0-5 ogólna (0 = offline)
|
|
freshness: number; // 0-5
|
|
richness: number; // 0-5
|
|
health: number | null; // 0-5 (0=offline), null gdy brak danych
|
|
health_basis?: string | null; // 'telemetry' | 'proxy'
|
|
components?: {
|
|
pct?: Record<string, number>; // thumb/tag/perf/desc/studio/dur (%)
|
|
health_basis?: string;
|
|
mechanism?: string | null;
|
|
pb_attempts_7d?: number;
|
|
pb_success_rate?: number | null;
|
|
p50_ttff_ms?: number | null;
|
|
} | null;
|
|
}
|
|
|
|
export interface SourceOut {
|
|
origin: string;
|
|
sitetag: string;
|
|
display_name: string;
|
|
scene_count: number;
|
|
last_scraped_at: string | null;
|
|
rating?: SourceRating | null;
|
|
/** Logo strony (favicon jako PNG z backendu). Wzgledne '/static/...' dokleja apka. */
|
|
icon_url?: string | null;
|
|
}
|
|
|
|
export interface SourceListOut {
|
|
items: SourceOut[];
|
|
total: number;
|
|
}
|
|
|
|
export type ScenesSort =
|
|
| 'created_at'
|
|
| 'release_date'
|
|
| 'title'
|
|
| 'studio'
|
|
| 'random'
|
|
| 'duration_desc'
|
|
| 'duration_asc';
|
|
|
|
export interface ScenesListParams {
|
|
q?: string;
|
|
studio_slugs?: string[];
|
|
tags?: string[];
|
|
performer_ids?: string[];
|
|
has_playback?: boolean;
|
|
min_duration_sec?: number;
|
|
max_duration_sec?: number;
|
|
released_within_days?: number;
|
|
min_quality_p?: number;
|
|
include_stubs?: boolean;
|
|
origin?: string;
|
|
jav?: boolean;
|
|
/** Wertykał do pokazania: 'animation' | 'jav'. Brak → główny feed (wertykały odcięte). */
|
|
vertical?: string;
|
|
sort?: ScenesSort;
|
|
/** Dla sort=random: stały w obrębie sesji listy, nowy przy pull-to-refresh. */
|
|
seed?: string;
|
|
page?: number;
|
|
per_page?: number;
|
|
}
|
|
|
|
export interface SceneOut {
|
|
id: string;
|
|
title: string;
|
|
slug?: string | null;
|
|
release_date?: string | null;
|
|
duration_sec?: number | null;
|
|
description?: string | null;
|
|
code?: string | null;
|
|
director?: string | null;
|
|
studio?: StudioOut | null;
|
|
performers: PerformerOut[];
|
|
tags: TagOut[];
|
|
external_refs: ExternalRef[];
|
|
playback_sources: PlaybackSource[];
|
|
/** Sekcja, do której należy scena ('animation' | 'jav'), albo brak dla głównego
|
|
* katalogu. Przekazuj dalej przy przejściu do tagu/studia/performera, inaczej
|
|
* backend odetnie wertykał i lista wyjdzie pusta albo z obcymi scenami. */
|
|
vertical?: string | null;
|
|
/** Najwyższa ZNANA jakość źródeł (720/1080/2160). Brak = żadne źródło jej nie podało
|
|
* (93,5% katalogu), wtedy kafelek nie pokazuje badge. */
|
|
max_quality_p?: number | null;
|
|
// Kiedy scena trafiła do bazy (ingest). Używane do oznaczenia "NEW", gdy
|
|
// `created_at > favoriteSeenSince` (param przekazany z FavoritesScreen).
|
|
created_at?: string | null;
|
|
// True = scena z masowego backfillu katalogu (deep-crawl), stara treść, nie świeży
|
|
// release. NIE pokazujemy na niej NEW badge nawet gdy created_at > seenSince.
|
|
backfill?: boolean;
|
|
// Watched indicator + favorite state. Backend dolicza z scene_play_progress + favorite_scenes.
|
|
last_played_at?: string | null;
|
|
finished?: boolean;
|
|
position_sec?: number;
|
|
is_favorite?: boolean;
|
|
}
|
|
|
|
export interface SceneListOut {
|
|
items: SceneOut[];
|
|
total: number;
|
|
page: number;
|
|
per_page: number;
|
|
// has_more: czy jest kolejna strona (z fetcha per_page+1). Paginuj po TYM, nie po
|
|
// `total`, bo dla list filtrowanych `total` jest bounded ("1000+"). Optional dla
|
|
// wstecznej zgodności ze starym backendem (undefined → fallback na total w UI).
|
|
has_more?: boolean;
|
|
// total_capped: true gdy `total` to cap (jest >total wyników) → pokaż "{total}+".
|
|
total_capped?: boolean;
|
|
}
|
|
|
|
export interface MovieChapterOut {
|
|
chapter_index: number;
|
|
title?: string | null;
|
|
start_sec?: number | null;
|
|
end_sec?: number | null;
|
|
scene_id?: string | null;
|
|
}
|
|
|
|
export interface MovieOut {
|
|
id: string;
|
|
title: string;
|
|
slug?: string | null;
|
|
release_year?: number | null;
|
|
release_date?: string | null;
|
|
duration_sec?: number | null;
|
|
description?: string | null;
|
|
director?: string | null;
|
|
country?: string | null;
|
|
rating?: number | null;
|
|
poster_url?: string | null;
|
|
backdrop_url?: string | null;
|
|
studio?: StudioOut | null;
|
|
performers: PerformerOut[];
|
|
tags: TagOut[];
|
|
chapters: MovieChapterOut[];
|
|
external_refs: ExternalRef[];
|
|
playback_sources: PlaybackSource[];
|
|
created_at?: string | null;
|
|
is_favorite?: boolean;
|
|
// Watched / continue-watching state (mirror SceneOut). Backend zaczął
|
|
// zwracać 2026-05-28, pre-0.2.0 buildy zostawią defaults (false/0/null).
|
|
last_played_at?: string | null;
|
|
finished?: boolean;
|
|
position_sec?: number;
|
|
}
|
|
|
|
export interface MovieListOut {
|
|
items: MovieOut[];
|
|
total: number;
|
|
page: number;
|
|
per_page: number;
|
|
}
|
|
|
|
export interface FavoriteMovieOut {
|
|
movie_id: string;
|
|
title: string;
|
|
slug: string | null;
|
|
poster_url: string | null;
|
|
release_year: number | null;
|
|
studio_name: string | null;
|
|
last_seen_at: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface FavoriteMovieListOut {
|
|
items: FavoriteMovieOut[];
|
|
total: number;
|
|
}
|
|
|
|
export interface FavoriteMovieAddOut {
|
|
movie_id: string;
|
|
created: boolean;
|
|
}
|
|
|
|
export type MoviesSort = 'created_at' | 'release_year' | 'release_date' | 'title' | 'rating';
|
|
|
|
export interface MoviesListParams {
|
|
q?: string;
|
|
studio_slugs?: string[];
|
|
tags?: string[];
|
|
performer_ids?: string[];
|
|
year_from?: number;
|
|
year_to?: number;
|
|
has_playback?: boolean;
|
|
sort?: MoviesSort;
|
|
page?: number;
|
|
per_page?: number;
|
|
}
|
|
|
|
export interface MergeCandidateSummary {
|
|
id: string;
|
|
kind: string;
|
|
left_id: string;
|
|
right_id: string;
|
|
score: number;
|
|
status: 'pending' | 'auto_merged' | 'merged' | 'rejected';
|
|
left_title?: string | null;
|
|
right_title?: string | null;
|
|
left_thumbnail_url?: string | null;
|
|
left_animated_thumbnail_url?: string | null;
|
|
right_thumbnail_url?: string | null;
|
|
right_animated_thumbnail_url?: string | null;
|
|
}
|
|
|
|
export interface MergeCandidateListOut {
|
|
items: MergeCandidateSummary[];
|
|
total: number;
|
|
page: number;
|
|
per_page: number;
|
|
}
|
|
|
|
export interface MergeCandidateDetail {
|
|
id: string;
|
|
kind: string;
|
|
score: number;
|
|
status: string;
|
|
reasons: Record<string, unknown>;
|
|
left: SceneOut | null;
|
|
right: SceneOut | null;
|
|
}
|
|
|
|
export interface ResolveResult {
|
|
id: string;
|
|
status: string;
|
|
keep_id?: string | null;
|
|
drop_id?: string | null;
|
|
}
|
|
|
|
export type ResolveAction = 'merge' | 'reject';
|
|
export type KeepSide = 'left' | 'right';
|
|
|
|
export interface FavoriteOut {
|
|
performer_id: string;
|
|
canonical_name: string;
|
|
slug: string | null;
|
|
scene_count: number;
|
|
new_count: number;
|
|
last_seen_at: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface FavoriteListOut {
|
|
items: FavoriteOut[];
|
|
total: number;
|
|
new_total: number;
|
|
}
|
|
|
|
export interface FavoriteAddOut {
|
|
performer_id: string;
|
|
created: boolean;
|
|
}
|
|
|
|
export interface FavoriteStudioOut {
|
|
studio_id: string;
|
|
name: string;
|
|
slug: string;
|
|
network: string | null;
|
|
scene_count: number;
|
|
/** Filmów z żywym playbackiem. Studio bywa WYŁĄCZNIE filmowe (0 scen,
|
|
* setki filmów), a lista pokazywała wtedy samo „0 scenes". */
|
|
movie_count?: number;
|
|
new_count: number;
|
|
last_seen_at: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface FavoriteStudioListOut {
|
|
items: FavoriteStudioOut[];
|
|
total: number;
|
|
new_total: number;
|
|
}
|
|
|
|
export interface FavoriteStudioAddOut {
|
|
studio_id: string;
|
|
created: boolean;
|
|
}
|
|
|
|
/** Przypiete strony w zakladce Sites: origins w kolejnosci dodania. */
|
|
export interface FavoriteSiteListOut {
|
|
items: string[];
|
|
total: number;
|
|
}
|
|
|
|
export interface FavoriteSiteAddOut {
|
|
origin: string;
|
|
created: boolean;
|
|
}
|
|
|
|
export interface BlacklistEntry {
|
|
id: string;
|
|
name: string;
|
|
slug?: string | null;
|
|
}
|
|
|
|
export interface BlacklistOut {
|
|
performers: BlacklistEntry[];
|
|
studios: BlacklistEntry[];
|
|
tags: BlacklistEntry[];
|
|
}
|
|
|
|
export type BlacklistKind = 'performer' | 'studio' | 'tag';
|
|
|
|
export interface SavedSearchOut {
|
|
id: string;
|
|
query: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface ProgressOut {
|
|
scene_id: string;
|
|
position_sec: number;
|
|
duration_sec: number | null;
|
|
finished: boolean;
|
|
last_played_at: string;
|
|
}
|
|
|
|
export interface WatchEntry {
|
|
scene: SceneOut;
|
|
position_sec: number;
|
|
duration_sec: number | null;
|
|
finished: boolean;
|
|
last_played_at: string;
|
|
}
|
|
|
|
export interface WatchListOut {
|
|
items: WatchEntry[];
|
|
}
|
|
|
|
// ---- Swipe (klipy) --------------------------------------------------------------------
|
|
// Klip to krotkie pionowe wideo tworcy (RedGifs), osobny wertykal od scen. Adresy plikow
|
|
// sa publiczne (media.redgifs.com), graja direct bez proxy i bez Referera.
|
|
export interface ClipOut {
|
|
id: string;
|
|
source: string;
|
|
source_id: string;
|
|
title: string | null;
|
|
creator: string | null;
|
|
creator_verified: boolean;
|
|
duration_sec: number | null;
|
|
width: number | null;
|
|
height: number | null;
|
|
has_audio: boolean;
|
|
content_type: string | null;
|
|
niches: string[];
|
|
tags: string[];
|
|
hd_url: string | null;
|
|
sd_url: string | null;
|
|
poster_url: string | null;
|
|
thumb_url: string | null;
|
|
likes: number;
|
|
views: number;
|
|
published_at: string | null;
|
|
is_favorite?: boolean;
|
|
}
|
|
|
|
export interface ClipFeedItem {
|
|
clip: ClipOut;
|
|
/** 'affinity' | 'explore': z ktorego koszyka przyszedl klip (do telemetrii). */
|
|
slot: string;
|
|
is_fresh: boolean;
|
|
}
|
|
|
|
export interface ClipFeedPage {
|
|
items: ClipFeedItem[];
|
|
next_cursor: string;
|
|
seed: string;
|
|
wrapped: boolean;
|
|
served_total: number;
|
|
personalized: boolean;
|
|
}
|
|
|
|
export type ClipEventKind =
|
|
| 'impression'
|
|
| 'skip'
|
|
| 'complete'
|
|
| 'like'
|
|
| 'unlike'
|
|
| 'not_interested'
|
|
| 'autoswipe'
|
|
| 'open';
|
|
|
|
export interface ClipEventIn {
|
|
clip_id: string;
|
|
kind: ClipEventKind;
|
|
dwell_ms?: number;
|
|
/** Powod „nie interesuje": `tag:<slug>` | `niche:<id>` | `creator:<name>` | `seen_it`. */
|
|
reason?: string;
|
|
slot?: string;
|
|
position?: number;
|
|
}
|
|
|
|
export interface ClipOnboardingOption {
|
|
value: string;
|
|
count: number;
|
|
}
|
|
|
|
export interface ClipOnboardingOptions {
|
|
tags: ClipOnboardingOption[];
|
|
niches: ClipOnboardingOption[];
|
|
}
|
|
|
|
export interface ClipAffinityOut {
|
|
tags: Record<string, number>;
|
|
niches: Record<string, number>;
|
|
creators: Record<string, number>;
|
|
events_seen: number;
|
|
}
|
|
|
|
export interface ClipListOut {
|
|
items: ClipOut[];
|
|
limit: number;
|
|
offset: number;
|
|
has_more: boolean;
|
|
/** Liczba wszystkich klipow przy filtrze po tworcy; null przy innych listach. */
|
|
total?: number | null;
|
|
}
|