pimpbunny fix

This commit is contained in:
Simon
2026-03-22 12:27:46 +00:00
parent a2d31d90a1
commit 50ea0e73b7
13 changed files with 646 additions and 140 deletions

View File

@@ -49,8 +49,7 @@ impl PimpbunnyProvider {
const FIREFOX_USER_AGENT: &'static str =
"Mozilla/5.0 (X11; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0";
const HTML_ACCEPT: &'static str =
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8";
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
pub fn new() -> Self {
let provider = Self {
url: "https://pimpbunny.com".to_string(),
@@ -235,39 +234,218 @@ impl PimpbunnyProvider {
format!("{}/", self.url.trim_end_matches('/'))
}
fn root_headers(&self) -> Vec<(String, String)> {
Self::html_headers_with_referer(&self.root_referer())
fn sort_by(sort: &str) -> &'static str {
match sort {
"best rated" => "rating",
"most viewed" => "video_viewed",
_ => "post_date",
}
}
fn html_headers_with_referer(referer: &str) -> Vec<(String, String)> {
vec![
("Referer".to_string(), referer.to_string()),
fn build_search_path_query(query: &str, separator: &str) -> String {
query.split_whitespace().collect::<Vec<_>>().join(separator)
}
fn append_archive_query(url: String, sort: &str) -> String {
let separator = if url.contains('?') { '&' } else { '?' };
format!("{url}{separator}sort_by={}", Self::sort_by(sort))
}
fn page_family_referer(&self, request_url: &str) -> String {
let Some(url) = Url::parse(request_url).ok() else {
return self.root_referer();
};
let path = url.path();
let referer_path = if path.starts_with("/videos/") {
"/videos/".to_string()
} else if path.starts_with("/search/") {
let parts: Vec<_> = path.trim_matches('/').split('/').collect();
if parts.len() >= 2 {
format!("/search/{}/", parts[1])
} else {
"/search/".to_string()
}
} else if path.starts_with("/categories/") {
let parts: Vec<_> = path.trim_matches('/').split('/').collect();
if parts.len() >= 2 {
format!("/categories/{}/", parts[1])
} else {
"/categories/".to_string()
}
} else if path.starts_with("/onlyfans-models/") {
let parts: Vec<_> = path.trim_matches('/').split('/').collect();
if parts.len() >= 2 {
format!("/onlyfans-models/{}/", parts[1])
} else {
"/onlyfans-models/".to_string()
}
} else {
"/".to_string()
};
format!("{}{}", self.url.trim_end_matches('/'), referer_path)
}
fn build_browse_url(&self, page: u8, sort: &str) -> String {
let base = if page <= 1 {
format!("{}/videos/", self.url)
} else {
format!("{}/videos/{page}/", self.url)
};
Self::append_archive_query(base, sort)
}
fn build_search_url(&self, query: &str, page: u8, sort: &str) -> String {
let path_query = Self::build_search_path_query(query, "-");
let base = if page <= 1 {
format!("{}/search/{path_query}/", self.url)
} else {
format!("{}/search/{path_query}/{page}/", self.url)
};
Self::append_archive_query(base, sort)
}
fn build_common_archive_url(&self, archive_path: &str, page: u8, sort: &str) -> String {
let canonical = format!(
"{}/{}",
self.url.trim_end_matches('/'),
archive_path.trim_start_matches('/')
);
let base = if page <= 1 {
canonical
} else {
format!("{}/{}", canonical.trim_end_matches('/'), page)
};
let base = if base.ends_with('/') {
base
} else {
format!("{base}/")
};
Self::append_archive_query(base, sort)
}
fn navigation_headers(
referer: Option<&str>,
sec_fetch_site: &'static str,
) -> Vec<(String, String)> {
let mut headers = vec![
(
"User-Agent".to_string(),
Self::FIREFOX_USER_AGENT.to_string(),
),
("Accept".to_string(), Self::HTML_ACCEPT.to_string()),
("Accept-Language".to_string(), "en-US,en;q=0.9".to_string()),
]
("Cache-Control".to_string(), "no-cache".to_string()),
("Pragma".to_string(), "no-cache".to_string()),
("Priority".to_string(), "u=0, i".to_string()),
("Connection".to_string(), "keep-alive".to_string()),
("TE".to_string(), "trailers".to_string()),
("Sec-Fetch-Dest".to_string(), "document".to_string()),
("Sec-Fetch-Mode".to_string(), "navigate".to_string()),
("Sec-Fetch-Site".to_string(), sec_fetch_site.to_string()),
("Sec-Fetch-User".to_string(), "?1".to_string()),
("Upgrade-Insecure-Requests".to_string(), "1".to_string()),
];
if let Some(referer) = referer {
headers.push(("Referer".to_string(), referer.to_string()));
}
headers
}
fn headers_with_cookies(
&self,
requester: &Requester,
request_url: &str,
referer: &str,
referer: Option<&str>,
sec_fetch_site: &'static str,
) -> Vec<(String, String)> {
let mut headers = Self::html_headers_with_referer(referer);
let mut headers = Self::navigation_headers(referer, sec_fetch_site);
if let Some(cookie) = requester.cookie_header_for_url(request_url) {
headers.push(("Cookie".to_string(), cookie));
}
headers
}
fn is_cloudflare_challenge(html: &str) -> bool {
html.contains("cf-turnstile-response")
|| html.contains("Performing security verification")
|| html.contains("__cf_chl_rt_tk")
|| html.contains("cUPMDTk:\"")
|| html.contains("Just a moment...")
}
fn extract_challenge_path(html: &str) -> Option<String> {
html.split("cUPMDTk:\"")
.nth(1)
.and_then(|s| s.split('"').next())
.map(str::to_string)
.or_else(|| {
html.split("__cf_chl_rt_tk=")
.nth(1)
.and_then(|s| s.split('"').next())
.map(|token| format!("/?__cf_chl_rt_tk={token}"))
})
}
fn absolute_site_url(&self, path_or_url: &str) -> String {
if path_or_url.starts_with("http://") || path_or_url.starts_with("https://") {
path_or_url.to_string()
} else {
format!(
"{}/{}",
self.url.trim_end_matches('/'),
path_or_url.trim_start_matches('/')
)
}
}
async fn fetch_html(
&self,
requester: &mut Requester,
request_url: &str,
referer: Option<&str>,
sec_fetch_site: &'static str,
) -> Result<String> {
let headers = self.headers_with_cookies(requester, request_url, referer, sec_fetch_site);
let response = requester
.get_raw_with_headers(request_url, headers.clone())
.await
.map_err(Error::from)?;
let status = response.status();
let body = response.text().await.map_err(Error::from)?;
if status.is_success() || status.as_u16() == 404 {
return Ok(body);
}
if status.as_u16() == 403 && Self::is_cloudflare_challenge(&body) {
if let Some(challenge_path) = Self::extract_challenge_path(&body) {
let challenge_url = self.absolute_site_url(&challenge_path);
let challenge_headers = self.headers_with_cookies(
requester,
&challenge_url,
Some(request_url),
"same-origin",
);
let _ = requester
.get_raw_with_headers(&challenge_url, challenge_headers)
.await;
}
}
let retry_headers =
self.headers_with_cookies(requester, request_url, referer, sec_fetch_site);
requester
.get_with_headers(request_url, retry_headers, Some(Version::HTTP_11))
.await
.map_err(|e| Error::from(format!("{e}")))
}
async fn warm_root_session(&self, requester: &mut Requester) {
let root_url = self.root_referer();
let _ = requester
.get_with_headers(&root_url, self.root_headers(), Some(Version::HTTP_11))
let _ = self
.fetch_html(requester, &root_url, None, "none")
.await;
}
@@ -276,7 +454,7 @@ impl PimpbunnyProvider {
let _ = requester
.get_with_headers(
&root_url,
Self::html_headers_with_referer(&root_url),
Self::navigation_headers(None, "none"),
Some(Version::HTTP_11),
)
.await;
@@ -288,7 +466,7 @@ impl PimpbunnyProvider {
let request_url = format!("{base}/onlyfans-models/?models_per_page=20");
let headers = {
let root_url = format!("{}/", base.trim_end_matches('/'));
let mut headers = Self::html_headers_with_referer(&root_url);
let mut headers = Self::navigation_headers(Some(&root_url), "same-origin");
if let Some(cookie) = requester.cookie_header_for_url(&request_url) {
headers.push(("Cookie".to_string(), cookie));
}
@@ -343,7 +521,7 @@ impl PimpbunnyProvider {
let request_url = format!("{base}/categories/?items_per_page=120");
let headers = {
let root_url = format!("{}/", base.trim_end_matches('/'));
let mut headers = Self::html_headers_with_referer(&root_url);
let mut headers = Self::navigation_headers(Some(&root_url), "same-origin");
if let Some(cookie) = requester.cookie_header_for_url(&request_url) {
headers.push(("Cookie".to_string(), cookie));
}
@@ -393,15 +571,7 @@ impl PimpbunnyProvider {
sort: &str,
options: ServerOptions,
) -> Result<Vec<VideoItem>> {
let sort_string = match sort {
"best rated" => "&sort_by=rating",
"most viewed" => "&sort_by=video_viewed",
_ => "&sort_by=post_date",
};
let video_url = format!(
"{}/videos/{}/?videos_per_page=32{}",
self.url, page, sort_string
);
let video_url = self.build_browse_url(page, sort);
let old_items = match cache.get(&video_url) {
Some((time, items)) => {
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
@@ -417,9 +587,14 @@ impl PimpbunnyProvider {
let mut requester =
crate::providers::requester_or_default(&options, module_path!(), "missing_requester");
self.warm_root_session(&mut requester).await;
let headers = self.headers_with_cookies(&requester, &video_url, &self.root_referer());
let text = match requester
.get_with_headers(&video_url, headers, Some(Version::HTTP_11))
let referer = self.page_family_referer(&video_url);
let text = match self
.fetch_html(
&mut requester,
&video_url,
Some(&referer),
"same-origin",
)
.await
{
Ok(text) => text,
@@ -451,27 +626,17 @@ impl PimpbunnyProvider {
options: ServerOptions,
) -> Result<Vec<VideoItem>> {
let search_string = query.trim().to_string();
let mut video_url = format!(
"{}/search/{}/?mode=async&function=get_block&block_id=list_videos_videos_list_search_result&videos_per_page=32&from_videos={}",
self.url,
search_string.replace(" ", "-"),
page
);
let sort_string = match options.sort.as_deref().unwrap_or("") {
"best rated" => "&sort_by=rating",
"most viewed" => "&sort_by=video_viewed",
_ => "&sort_by=post_date",
};
let sort = options.sort.as_deref().unwrap_or("");
let mut video_url = self.build_search_url(&search_string, page, sort);
if let Ok(stars) = self.stars.read() {
if let Some(star) = stars
.iter()
.find(|s| s.title.to_ascii_lowercase() == search_string.to_ascii_lowercase())
{
video_url = format!(
"{}/onlyfans-models/{}/{}/?videos_per_page=20{}",
self.url, star.id, page, sort_string
video_url = self.build_common_archive_url(
&format!("/onlyfans-models/{}/", star.id),
page,
sort,
);
}
} else {
@@ -486,10 +651,8 @@ impl PimpbunnyProvider {
.iter()
.find(|c| c.title.to_ascii_lowercase() == search_string.to_ascii_lowercase())
{
video_url = format!(
"{}/categories/{}/{}/?videos_per_page=20{}",
self.url, cat.id, page, sort_string
);
video_url =
self.build_common_archive_url(&format!("/categories/{}/", cat.id), page, sort);
}
} else {
crate::providers::report_provider_error_background(
@@ -516,10 +679,14 @@ impl PimpbunnyProvider {
let mut requester =
crate::providers::requester_or_default(&options, module_path!(), "missing_requester");
self.warm_root_session(&mut requester).await;
println!("Fetching URL: {}", video_url);
let headers = self.headers_with_cookies(&requester, &video_url, &self.root_referer());
let text = match requester
.get_with_headers(&video_url, headers, Some(Version::HTTP_2))
let referer = self.page_family_referer(&video_url);
let text = match self
.fetch_html(
&mut requester,
&video_url,
Some(&referer),
"same-origin",
)
.await
{
Ok(text) => text,
@@ -710,9 +877,17 @@ mod tests {
use crate::videos::ServerOptions;
use std::sync::{Arc, RwLock};
fn test_provider() -> PimpbunnyProvider {
PimpbunnyProvider {
url: "https://pimpbunny.com".to_string(),
stars: Arc::new(RwLock::new(vec![])),
categories: Arc::new(RwLock::new(vec![])),
}
}
#[test]
fn rewrites_allowed_thumbs_to_proxy_urls() {
let provider = PimpbunnyProvider::new();
let provider = test_provider();
let options = ServerOptions {
featured: None,
category: None,
@@ -742,7 +917,7 @@ mod tests {
#[test]
fn rewrites_video_pages_to_redirect_proxy() {
let provider = PimpbunnyProvider::new();
let provider = test_provider();
let options = ServerOptions {
featured: None,
category: None,
@@ -772,11 +947,7 @@ mod tests {
#[test]
fn parses_listing_without_detail_requests() {
let provider = PimpbunnyProvider {
url: "https://pimpbunny.com".to_string(),
stars: Arc::new(RwLock::new(vec![])),
categories: Arc::new(RwLock::new(vec![])),
};
let provider = test_provider();
let options = ServerOptions {
featured: None,
category: None,
@@ -820,4 +991,91 @@ mod tests {
assert_eq!(items[0].views, Some(1200));
assert_eq!(items[0].formats.as_ref().map(|f| f.len()), Some(1));
}
#[test]
fn extracts_cloudflare_challenge_path() {
let html = r#"
<script type="text/javascript">
(function(){
window._cf_chl_opt = {
cUPMDTk:"/?mode=async&function=get_block&block_id=videos_videos_list&videos_per_page=8&sort_by=post_date&from=1&__cf_chl_tk=test-token"
};
}());
</script>
"#;
assert!(PimpbunnyProvider::is_cloudflare_challenge(html));
assert_eq!(
PimpbunnyProvider::extract_challenge_path(html).as_deref(),
Some(
"/?mode=async&function=get_block&block_id=videos_videos_list&videos_per_page=8&sort_by=post_date&from=1&__cf_chl_tk=test-token"
)
);
}
#[test]
fn builds_async_browse_url_instead_of_numbered_videos_path() {
let provider = test_provider();
assert_eq!(
provider.build_browse_url(1, "most recent"),
"https://pimpbunny.com/videos/?sort_by=post_date"
);
assert_eq!(
provider.build_browse_url(2, "most recent"),
"https://pimpbunny.com/videos/2/?sort_by=post_date"
);
}
#[test]
fn builds_search_url_with_query_and_pagination() {
let provider = test_provider();
assert_eq!(
provider.build_search_url("adriana chechik", 1, "most viewed"),
"https://pimpbunny.com/search/adriana-chechik/?sort_by=video_viewed"
);
assert_eq!(
provider.build_search_url("adriana chechik", 3, "most viewed"),
"https://pimpbunny.com/search/adriana-chechik/3/?sort_by=video_viewed"
);
}
#[test]
fn builds_common_archive_url_with_async_block() {
let provider = test_provider();
assert_eq!(
provider.build_common_archive_url("/categories/amateur/", 1, "best rated"),
"https://pimpbunny.com/categories/amateur/?sort_by=rating"
);
assert_eq!(
provider.build_common_archive_url("/categories/amateur/", 4, "best rated"),
"https://pimpbunny.com/categories/amateur/4/?sort_by=rating"
);
}
#[test]
fn derives_page_family_referer() {
let provider = test_provider();
assert_eq!(
provider.page_family_referer("https://pimpbunny.com/videos/2/?sort_by=post_date"),
"https://pimpbunny.com/videos/"
);
assert_eq!(
provider.page_family_referer(
"https://pimpbunny.com/categories/blowjob/2/?sort_by=post_date"
),
"https://pimpbunny.com/categories/blowjob/"
);
assert_eq!(
provider.page_family_referer(
"https://pimpbunny.com/search/adriana-chechik/3/?sort_by=video_viewed"
),
"https://pimpbunny.com/search/adriana-chechik/"
);
assert_eq!(
provider.page_family_referer(
"https://pimpbunny.com/onlyfans-models/momoitenshi/3/?sort_by=post_date"
),
"https://pimpbunny.com/onlyfans-models/momoitenshi/"
);
}
}