# New Provider Playbook This is the implementation checklist for adding a working channel with the least guessing. ## Definition Of Done A provider is not done when it compiles. It is done when: 1. `/api/status` shows the channel with sensible options and grouping. 2. `/api/videos` returns real items for the default feed. 3. Search works. 4. Pagination works. 5. Thumbnails load. 6. `video.url` or at least one `formats[*].url` resolves to playable media. 7. If the site needs proxying, the `/proxy/...` route works. 8. `HOT_TUB_PROVIDER= cargo check -q` passes. ## Files To Touch Always: - `build.rs` - `src/providers/.rs` Sometimes: - `src/proxy.rs` - `src/proxies/.rs` - `src/proxies/thumb.rs` - `prompts/new-channel.md` if you are improving the handoff prompt - `docs/provider-catalog.md` if you add a new provider or proxy ## Step 1: Pick The Closest Template Do not start from an empty file. Choose the nearest match: - API-first site with tags/uploader metadata: copy `vjav.rs` - HTML site with background-loaded tags/uploaders: copy `hsex.rs` - Site with multiple large catalogs like sites/networks/stars: copy `omgxxx.rs` - Site whose media or thumbs need local proxying: copy `noodlemagazine.rs`, `pornhd3x.rs`, `spankbang.rs`, or `porndish.rs` - Very simple archive/search site: copy a small provider from `mainstream-tube` Before writing code, confirm the site shape: 1. home or latest feed URL 2. search URL and page 2 URL 3. detail page URL shape 4. player request or manifest request 5. thumbnail host and whether it needs referer/cookies 6. tag/category/uploader/studio routes if they exist 7. whether the site exposes JSON endpoints that are easier than HTML scraping Use browser/network tooling for this if needed. Do not guess URL patterns from one page. ## Step 2: Register The Provider Add the provider to `build.rs`: - `id`: channel id used by `/api/videos` - `module`: Rust file name - `ty`: provider struct name If this is missing, the server will not discover the provider. ## Step 3: Define Channel Metadata Every provider should export: ```rust pub const CHANNEL_METADATA: crate::providers::ProviderChannelMetadata = crate::providers::ProviderChannelMetadata { group_id: "...", tags: &["...", "...", "..."], }; ``` Pick `group_id` from the existing set in `src/providers/mod.rs`: - `meta-search` - `mainstream-tube` - `tiktok` - `studio-network` - `amateur-homemade` - `onlyfans` - `chinese` - `jav` - `fetish-kink` - `hentai-animation` - `ai` - `gay-male` - `live-cams` - `pmv-compilation` ## Step 4: Build The Channel Surface Implement `build_channel` or equivalent and return it from `get_channel`. Required: - `id` - `name` - `description` - `favicon` - `status` - `nsfw` Recommended: - `cacheDuration: Some(1800)` unless the site is unusually stable - use standard option IDs like `sort`, `filter`, `sites`, `category`, `stars`, `categories` - keep options minimal at first; only expose filters that actually work in `get_videos` The option `id` values matter more than the display `title`. ## Step 5: Model Provider Routing Explicitly Create a local enum like: ```rust enum Target { Latest, Search { query: String }, Tag { slug: String }, Uploader { id: String }, } ``` Then write one function that resolves `sort`, `query`, `filter`, `sites`, and related options into a `Target`. This is easier to debug than scattering URL decisions across the provider. ## Step 6: Load Filter Catalogs In The Background If Needed If the site exposes tags, uploaders, studios, networks, or stars: - store them in `Arc>>` - initialize them with an `All` option - spawn a background thread in `new()` - create a tiny Tokio runtime inside that thread - fill the lists without blocking server startup Patterns: - `hsex.rs` - `omgxxx.rs` - `pornhd3x.rs` - `vjav.rs` If tags or uploaders need stable IDs, keep a lookup map such as: - `HashMap` from title to site ID - `HashMap` from site ID to URL target Normalize lookup keys to lowercase trimmed strings. ## Step 7: Fetch Pages Through The Shared Requester In `get_videos`, start with: ```rust let mut requester = requester_or_default(&options, CHANNEL_ID, "get_videos"); ``` Use it for HTML, JSON, and raw media requests. Why: - it preserves cookies - it carries debug trace IDs - it respects Burp proxying - it can fall back to Jina or FlareSolverr ## Step 8: Parse Listing Cards First, Then Enrich Only If Needed Preferred flow: 1. Fetch the archive or search page. 2. Parse a lightweight list of stubs. 3. Return list data directly if enough metadata is already present. 4. Fetch detail pages or JSON endpoints only for fields the card does not expose. Use bounded concurrency for detail enrichment. Existing providers usually use `futures::stream` with `buffer_unordered`. ## Step 9: Build High-Quality `VideoItem`s Always fill: - `id` - `title` - `url` - `channel` - `thumb` - `duration` Fill when available: - `views` - `rating` - `uploader` - `uploaderUrl` - `uploaderId` - `tags` - `uploadedAt` - `preview` - `aspectRatio` - `formats` Rules: - Keep `tags` as a list of displayable titles. - Keep uploader data as structured fields, not mashed into the title. - If you support uploader profiles, set `uploaderId` to a namespaced value like `:`. - Do not include `embed` unless the provider truly needs it. - If direct media exists, prefer `formats` and keep `url` stable. ## Step 10: Decide Whether A Proxy Is Required Use no proxy when: - page URLs are enough and the client can resolve media itself - or direct media URLs already work cleanly Use a redirect proxy when: - the provider must turn a detail URL into a resolved media URL - headers/cookies do not need full response rewriting Use a media/image proxy when: - the site requires a referer for every fetch - thumbnails need cookie-backed access - manifests contain relative URIs that must be rewritten - the server must stream binary content itself If a proxy is needed: 1. add `src/proxies/.rs` 2. wire the route in `src/proxy.rs` 3. generate provider URLs with `build_proxy_url(&options, "", target)` ## Step 11: Implement Search Correctly Check for three search modes: 1. native site search endpoint 2. tag/uploader shortcut search from preloaded filter catalogs 3. literal client-side substring search after fetch, triggered by quoted queries Important server behavior: - `#tag` becomes `tag` - `"teacher"` becomes a literal post-fetch filter - raw URL queries may bypass the provider through the `yt-dlp` fast path Provider guidance: - if the query matches a known tag/uploader shortcut, prefer the site’s direct archive URL instead of generic search - otherwise fall back to the site’s keyword search ## Step 12: Support Pagination Explicitly Do not assume pagination is `?page=N`. Confirm: - archive page 2 URL shape - search page 2 URL shape - tag page 2 URL shape - uploader page 2 URL shape If the site uses infinite scroll or an XHR endpoint, document that in code comments and hit the underlying endpoint directly. ## Step 13: Only Add `/api/uploaders` When The Site Has Real Uploader Identity Uploader support is optional. Only implement it when the site exposes stable uploader pages or IDs. Use `hsex.rs`, `omgxxx.rs`, or `vjav.rs` as the template. Minimum expectations for `UploaderProfile`: - stable `id` - `name` - `channel` - `videoCount` - `totalViews` Nice to have: - `avatar` - `description` - `videos` - `layout` - per-channel stats ## Validation Checklist Run all of these: ```bash cargo check -q HOT_TUB_PROVIDER= cargo check -q HOT_TUB_PROVIDER= cargo run --features debug ``` Then hit: ```bash curl -s http://127.0.0.1:18080/api/status \ -H 'User-Agent: Hot%20Tub/22c CFNetwork/1494.0.7 Darwin/23.4.0' | jq ``` ```bash curl -s http://127.0.0.1:18080/api/videos \ -H 'Content-Type: application/json' \ -d '{"channel":"","sort":"new","page":1,"perPage":10}' | jq ``` Also verify: - search query works - page 2 works - tag shortcut works if implemented - uploader shortcut works if implemented - `yt-dlp ''` resolves media - thumbnail URL returns an image - proxy route returns a `302` or working media body, whichever is expected - if uploaders are implemented, `/api/uploaders` works with both `uploaderId` and `uploaderName` ## Common Failure Modes - Forgot `build.rs` entry. - Returned page URLs but no playable media/formats. - Used a local requester instead of the shared one and lost cookies. - Built `/proxy/...` URLs without `public_url_base`. - Put human-readable titles into filter IDs, making routing brittle. - Added huge option lists to the status response without background loading. - Implemented search but not search pagination. - Implemented proxies but forgot to test them independently with `curl -I`. ## Best Reference Matrix - Rich uploader support: `vjav.rs`, `hsex.rs`, `omgxxx.rs` - Tag and uploader lookup maps: `vjav.rs`, `hsex.rs` - Background catalog loading: `hsex.rs`, `omgxxx.rs`, `pornhd3x.rs` - Redirect proxy: `spankbang.rs` plus `src/proxies/spankbang.rs` - Manifest or image proxy: `noodlemagazine.rs` plus `src/proxies/noodlemagazine.rs` - Complex detail enrichment: `pornhd3x.rs`