9.2 KiB
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:
/api/statusshows the channel with sensible options and grouping./api/videosreturns real items for the default feed.- Search works.
- Pagination works.
- Thumbnails load.
video.urlor at least oneformats[*].urlresolves to playable media.- If the site needs proxying, the
/proxy/...route works. HOT_TUB_PROVIDER=<id> cargo check -qpasses.
Files To Touch
Always:
build.rssrc/providers/<channel_id>.rs
Sometimes:
src/proxy.rssrc/proxies/<channel_id>.rssrc/proxies/<channel_id>thumb.rsprompts/new-channel.mdif you are improving the handoff promptdocs/provider-catalog.mdif 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, orporndish.rs - Very simple archive/search site: copy a small provider from
mainstream-tube
Before writing code, confirm the site shape:
- home or latest feed URL
- search URL and page 2 URL
- detail page URL shape
- player request or manifest request
- thumbnail host and whether it needs referer/cookies
- tag/category/uploader/studio routes if they exist
- 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/videosmodule: Rust file namety: provider struct name
If this is missing, the server will not discover the provider.
Step 3: Define Channel Metadata
Every provider should export:
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-searchmainstream-tubetiktokstudio-networkamateur-homemadeonlyfanschinesejavfetish-kinkhentai-animationaigay-malelive-camspmv-compilation
Step 4: Build The Channel Surface
Implement build_channel or equivalent and return it from get_channel.
Required:
idnamedescriptionfaviconstatusnsfw
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:
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<RwLock<Vec<FilterOption>>> - initialize them with an
Alloption - spawn a background thread in
new() - create a tiny Tokio runtime inside that thread
- fill the lists without blocking server startup
Patterns:
hsex.rsomgxxx.rspornhd3x.rsvjav.rs
If tags or uploaders need stable IDs, keep a lookup map such as:
HashMap<String, String>from title to site IDHashMap<String, String>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:
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:
- Fetch the archive or search page.
- Parse a lightweight list of stubs.
- Return list data directly if enough metadata is already present.
- 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 VideoItems
Always fill:
idtitleurlchannelthumbduration
Fill when available:
viewsratinguploaderuploaderUrluploaderIdtagsuploadedAtpreviewaspectRatioformats
Rules:
- Keep
tagsas a list of displayable titles. - Keep uploader data as structured fields, not mashed into the title.
- If you support uploader profiles, set
uploaderIdto a namespaced value like<channel>:<site-local-id>. - Do not include
embedunless the provider truly needs it. - If direct media exists, prefer
formatsand keepurlstable.
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:
- add
src/proxies/<id>.rs - wire the route in
src/proxy.rs - generate provider URLs with
build_proxy_url(&options, "<id>", target)
Step 11: Implement Search Correctly
Check for three search modes:
- native site search endpoint
- tag/uploader shortcut search from preloaded filter catalogs
- literal client-side substring search after fetch, triggered by quoted queries
Important server behavior:
#tagbecomestag"teacher"becomes a literal post-fetch filter- raw URL queries may bypass the provider through the
yt-dlpfast 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 namechannelvideoCounttotalViews
Nice to have:
avatardescriptionvideoslayout- per-channel stats
Validation Checklist
Run all of these:
cargo check -q
HOT_TUB_PROVIDER=<channel_id> cargo check -q
HOT_TUB_PROVIDER=<channel_id> cargo run --features debug
Then hit:
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
curl -s http://127.0.0.1:18080/api/videos \
-H 'Content-Type: application/json' \
-d '{"channel":"<channel_id>","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 '<video.url or first format url>'resolves media- thumbnail URL returns an image
- proxy route returns a
302or working media body, whichever is expected - if uploaders are implemented,
/api/uploadersworks with bothuploaderIdanduploaderName
Common Failure Modes
- Forgot
build.rsentry. - Returned page URLs but no playable media/formats.
- Used a local requester instead of the shared one and lost cookies.
- Built
/proxy/...URLs withoutpublic_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.rsplussrc/proxies/spankbang.rs - Manifest or image proxy:
noodlemagazine.rsplussrc/proxies/noodlemagazine.rs - Complex detail enrichment:
pornhd3x.rs