diff --git a/src/providers/freeuseporn.rs b/src/providers/freeuseporn.rs index 84e7c5f..e77b14f 100644 --- a/src/providers/freeuseporn.rs +++ b/src/providers/freeuseporn.rs @@ -9,10 +9,10 @@ use crate::videos::{ServerOptions, VideoFormat, VideoItem}; use async_trait::async_trait; use error_chain::error_chain; use htmlentity::entity::{ICodedDataTrait, decode}; -use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode}; use scraper::{Html, Selector}; use std::collections::HashSet; use std::vec; +use url::form_urlencoded::Serializer; pub const CHANNEL_METADATA: crate::providers::ProviderChannelMetadata = crate::providers::ProviderChannelMetadata { @@ -151,19 +151,22 @@ impl FreeusepornProvider { } } - fn build_list_url( - &self, - sort: &str, - page: u8, - query: Option<&str>, - category: Option<&str>, - ) -> String { - let path = if let Some(query) = query.map(str::trim).filter(|value| !value.is_empty()) { - format!( - "/search/videos/{}", - utf8_percent_encode(query, NON_ALPHANUMERIC) - ) - } else if let Some(category) = category + fn append_sort_and_page(&self, base_url: &str, sort: &str, page: u8) -> String { + let mut params = vec![format!("o={}", Self::sort_param(sort))]; + if page > 1 { + params.push(format!("page={page}")); + } + + if params.is_empty() { + return base_url.to_string(); + } + + let separator = if base_url.contains('?') { "&" } else { "?" }; + format!("{base_url}{separator}{}", params.join("&")) + } + + fn build_list_url(&self, sort: &str, page: u8, category: Option<&str>) -> String { + let path = if let Some(category) = category .map(str::trim) .filter(|value| !value.is_empty() && *value != "all") { @@ -172,12 +175,34 @@ impl FreeusepornProvider { "/videos".to_string() }; - let mut params = vec![format!("o={}", Self::sort_param(sort))]; - if page > 1 { - params.push(format!("page={page}")); - } + let base_url = format!("{}{}", self.url, path); + self.append_sort_and_page(&base_url, sort, page) + } - format!("{}{}?{}", self.url, path, params.join("&")) + fn build_search_request_body(query: &str) -> String { + let mut serializer = Serializer::new(String::new()); + serializer.append_pair("search_query", query); + serializer.finish() + } + + async fn resolve_search_url(&self, query: &str, options: &ServerOptions) -> Result { + let search_url = format!("{}/search/videos", self.url); + let search_body = Self::build_search_request_body(query); + let referer = format!("{}/videos", self.url); + let mut requester = requester_or_default(options, module_path!(), "missing_requester"); + let response = requester + .post( + &search_url, + &search_body, + vec![ + ("Content-Type", "application/x-www-form-urlencoded"), + ("Referer", referer.as_str()), + ], + ) + .await + .map_err(|error| format!("search submit failed url={search_url}; error={error}"))?; + + Ok(response.uri().to_string().trim_end_matches('/').to_string()) } fn build_formats(&self, id: &str) -> Vec { @@ -380,7 +405,7 @@ impl FreeusepornProvider { sort: &str, options: ServerOptions, ) -> Result> { - let url = self.build_list_url(sort, page, None, options.category.as_deref()); + let url = self.build_list_url(sort, page, options.category.as_deref()); self.fetch_listing(cache, url, options, "get.request").await } @@ -392,7 +417,19 @@ impl FreeusepornProvider { sort: &str, options: ServerOptions, ) -> Result> { - let url = self.build_list_url(sort, page, Some(query), None); + let search_base = match self.resolve_search_url(query, &options).await { + Ok(url) => url, + Err(error) => { + report_provider_error( + "freeuseporn", + "query.search_submit", + &error.to_string(), + ) + .await; + return Ok(vec![]); + } + }; + let url = self.append_sort_and_page(&search_base, sort, page); self.fetch_listing(cache, url, options, "query.request").await } } @@ -464,16 +501,28 @@ mod tests { let provider = provider(); assert_eq!( - provider.build_list_url("recent", 1, None, None), + provider.build_list_url("recent", 1, None), "https://www.freeuseporn.com/videos?o=mr" ); assert_eq!( - provider.build_list_url("viewed", 2, None, Some("mind-control")), + provider.build_list_url("viewed", 2, Some("mind-control")), "https://www.freeuseporn.com/videos/mind-control?o=mv&page=2" ); assert_eq!( - provider.build_list_url("favorites", 3, Some("mind control"), None), - "https://www.freeuseporn.com/search/videos/mind%20control?o=tf&page=3" + provider.append_sort_and_page( + "https://www.freeuseporn.com/search/videos/Nicole-Kitt", + "favorites", + 3 + ), + "https://www.freeuseporn.com/search/videos/Nicole-Kitt?o=tf&page=3" + ); + } + + #[test] + fn builds_search_request_body_with_form_encoding() { + assert_eq!( + FreeusepornProvider::build_search_request_body("Nicole Kitt & Cory Chase"), + "search_query=Nicole+Kitt+%26+Cory+Chase" ); } diff --git a/src/util/requester.rs b/src/util/requester.rs index 11b521b..e6bc359 100644 --- a/src/util/requester.rs +++ b/src/util/requester.rs @@ -375,7 +375,11 @@ impl Requester { Ok(response) } - #[cfg(any(not(hottub_single_provider), hottub_provider = "hypnotube"))] + #[cfg(any( + not(hottub_single_provider), + hottub_provider = "hypnotube", + hottub_provider = "freeuseporn", + ))] pub async fn post( &mut self, url: &str,