11 KiB
Hottub Architecture
Purpose
Hottub is a Rust server that exposes Hot Tub compatible endpoints for channel discovery, video search, uploader lookups, and site-specific proxying. Most work in this repo is adding or repairing a provider module under src/providers/.
Top-Level Structure
src/main.rs: server bootstrap, env loading, database pool, shared requester/cache, route mounting.src/api.rs:/api/status,/api/videos,/api/uploaders,/api/test,/api/proxies.src/providers/mod.rs: provider trait, provider registry, build-time provider selection, status decoration, runtime validation, panic/error guards.src/providers/*.rs: one module per channel/provider.src/proxy.rs: route table for/proxy/....src/proxies/*.rs: redirect/media/thumb proxy implementations.src/videos.rs: request/response payloads,VideoItem,VideoFormat,ServerOptions.src/status.rs: status/channel/group payloads.src/uploaders.rs: uploader request/profile payloads.src/util/requester.rs: outbound HTTP with cookies, optional Burp proxying, Jina fallback, and FlareSolverr fallback.build.rs: compile-time provider registry generation and single-provider build support.
Startup Flow
mainloads.envand ensuresRUST_LOGis set.- It creates the Diesel SQLite pool from
DATABASE_URL. - It creates a shared
Requester, enables Burp proxying whenPROXY != 0, and builds the LRU video cache. - It configures provider runtime validation in
providers::configure_runtime_validation. - It spawns a background thread that forces provider initialization via
providers::init_providers_now(). - It starts an
ntexHTTP server on0.0.0.0:18080.
Runtime Environment
Important environment variables:
DATABASE_URL: required SQLite path.RUST_LOG: defaults towarnif unset.PROXY: enables Burp proxying when not equal to0.BURP_URL: outbound proxy URL used whenPROXYis enabled.FLARE_URL: FlareSolverr endpoint used as the last HTML-fetch fallback.DOMAIN: used by the/redirect target.DISCORD_WEBHOOK: enables/api/testand provider error reporting.
Bundled reference material:
docs/hottubapp/📡 Status - Hot Tub Docs.htmldocs/hottubapp/🎬 Videos - Hot Tub Docs.htmldocs/hottubapp/👤 Uploaders - Hot Tub Docs.html
Those HTML files are useful when a provider author needs to confirm the expected client payload shape without reading Rust structs first.
Build-Time Provider Selection
build.rs reads HOT_TUB_PROVIDER or HOTTUB_PROVIDER.
- If unset, every provider in
build.rsis compiled and registered. - If set, only that provider is compiled into the binary.
- In a single-provider build,
/api/videosremaps"channel": "all"to the compiled provider.
Generated files in OUT_DIR are included by src/providers/mod.rs:
provider_modules.rsprovider_registry.rsprovider_metadata_fn.rsprovider_selection.rs
This means adding a new provider always requires updating build.rs.
HTTP Surface
/
Returns a 302 redirect to hottub://source?url=<DOMAIN-or-request-host>.
/api/status
Builds the channel list by iterating ALL_PROVIDERS and calling Provider::get_channel.
Important behavior:
- The
User-Agentis parsed intoClientVersion. - A provider can hide itself by returning
None. providers::build_status_responsedecorates channels withgroupKey, top tags, runtime status, and sort order.- Some heavy status filters are intentionally removed from the client-facing response. The server still accepts them in
/api/videos.
/api/videos
This is the main provider execution path.
Flow:
- Parse
VideosRequest. - Normalize
channel,sort,query,page, andperPage. - Build
ServerOptions. - If
queryis a fullhttp://orhttps://URL, try theyt-dlp -Jfast path first. - Otherwise call
provider.get_videos(...)throughrun_provider_guarded. - For quoted queries like
"teacher", apply a literal substring filter after provider fetch. - Spawn a background prefetch for the next page.
- For short videos (
duration <= 120), populatepreviewfrom the main URL or first format.
Important behavior:
- Leading
#is stripped from queries before provider dispatch. "all"usesAllProviderin a normal build, but resolves to the single compiled provider in a single-provider build.- Older
Hot Tub/38clients are patched by replacingvideo.urlwith the last format URL when formats exist.
/api/uploaders
Uploader lookup is optional and provider-specific.
Important behavior:
- At least one of
uploaderIdoruploaderNameis required. - If
uploaderIdlooks likechannel:id, the server directly targets that provider. - Otherwise it scans all providers and returns the best exact-name match.
- Only
hsex,omgxxx, andvjavcurrently implementget_uploader. - In practice, provider-owned uploader IDs should be namespaced, for example
vjav:12345orhsex:author_slug.
/api/test
Sends a Discord error test if DISCORD_WEBHOOK is configured.
/api/proxies
Returns the background-fetched outbound proxy snapshot from src/util/proxy.rs.
Core Data Structures
VideosRequest
Defined in src/videos.rs. Common fields used by providers:
channelsortquerypageperPagefeaturedcategorysitesall_provider_sitesfilterlanguagenetworksstarscategoriesdurationsexuality
ServerOptions
The server’s normalized option bag. Providers should read from this instead of reparsing the raw API request.
Important fields:
public_url_base: needed when generating/proxy/...URLs.requester: the shared request client with cookies/debug trace/proxy state.sort,sites,filter,category,language,network,stars,categories,duration,sexuality.
VideoItem
Minimum useful fields for a provider:
idtitleurlchannelthumbduration
High-value optional fields:
viewsratinguploaderuploaderUrluploaderIdtagsuploadedAtformatspreviewaspectRatio
Avoid setting embed for new providers unless the site truly needs it.
VideoFormat
Use formats when:
- the site returns a better direct media URL than the page URL
- HLS or multiple qualities exist
- extra HTTP headers such as
Refererare required
Use http_header or add_http_header when the player endpoint needs request headers.
Channel and ChannelOption
Each provider’s get_channel returns the status metadata exposed by /api/status.
Typical option IDs used across the repo:
sortfiltersitescategorylanguagenetworksstarscategories
Use the same IDs when possible so the server and client behavior stay consistent.
UploaderProfile
If a provider supports /api/uploaders, keep the ID routable:
- preferred format:
<channel>:<site-local-id> - examples in the repo:
vjav:<user_id>,hsex:<author>,omgxxx:<kind>:<id>
This lets src/api.rs derive the owning provider immediately.
Provider Contract
Defined in src/providers/mod.rs:
async fn get_videos(...) -> Vec<VideoItem>fn get_channel(clientversion: ClientVersion) -> Option<Channel>async fn get_uploader(...) -> Result<Option<UploaderProfile>, String>optional
The server wraps provider execution in:
run_provider_guardedfor video pathsrun_uploader_provider_guardedfor uploader paths
Panics and reported errors trigger runtime validation and optional Discord reporting.
Runtime Validation and Error Handling
src/providers/mod.rs includes a validation subsystem that:
- runs a small sample request against a provider after failures
- checks that enough video items exist
- tries media URLs or format URLs with a
Rangeheader - marks repeated failures over time
This means a provider that returns page URLs but no real media/formats may pass visually but still fail operationally.
Requester Behavior
src/util/requester.rs is the standard outbound HTTP layer.
Capabilities:
- shared cookie jar across clones
- optional Burp proxying via
PROXYandBURP_URL - direct request retries for
429 - Jina mirror fallback for blocked HTML fetches
- FlareSolverr fallback via
FLARE_URL - raw response helpers for media validation and custom headers
Use the shared requester from ServerOptions through requester_or_default. Do not instantiate a brand-new requester in normal provider fetch paths unless you have a very specific reason.
FlareSolverr note:
src/util/flaresolverr.rskeeps a reusable session pool pattern by rotating a ready session per solve.- If a provider only works after anti-bot negotiation, the shared requester is the path that benefits from that solved session and cookie state.
Proxy Subsystem
There are two proxy styles.
Redirect proxies
These take a provider-specific endpoint and return 302 Location: <resolved-media-url>.
Examples:
/proxy/spankbang/.../proxy/sxyprn/.../proxy/pornhd3x/.../proxy/vjav/...
Media or image proxies
These actively fetch media or thumbnails and stream or rewrite the response.
Examples:
/proxy/noodlemagazine/.../proxy/noodlemagazine-thumb/.../proxy/shooshtime-media/.../proxy/hanime-cdn/...
If a site only needs a referer-preserving redirect, use a redirect proxy. If manifests, relative playlist entries, cookies, or binary thumbs need rewriting, use a media/image proxy.
Best Existing Templates
Use the closest existing provider instead of inventing a new style.
src/providers/vjav.rs: rich API-backed provider with tags, uploader support, and detail enrichment.src/providers/hsex.rs: HTML scraping with background-loaded filters, uploader support, and direct HLS formats.src/providers/omgxxx.rs: large filter catalogs and uploader lookup by site/network identity.src/providers/noodlemagazine.rs: proxied media/thumbs, Jina fallback, and mirrored listing parsing.src/providers/pornhd3x.rs: complex filter catalogs, detail enrichment, and proxy-generated playback URLs.src/providers/spankbang.rs: anti-bot handling and a redirect-proxy-based media strategy.
Important Gotchas
- New providers must export
CHANNEL_METADATA. - New providers must be listed in
build.rsor they will never compile into the registry. - If a provider returns proxied URLs, it usually also needs
options.public_url_base. - Keep filter IDs stable. The
titleis for display; theidis what the provider matches on. categoriesinChannelare not the same asChannelOption { id: "categories" }./api/statussanitizes some options away from the client-facing payload. That does not mean the provider option is useless in/api/videos.- If a site needs per-request cookies or a solved user agent, rely on the shared requester.