pimpbunny fix
This commit is contained in:
@@ -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/"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user