xfree frontpage

This commit is contained in:
Simon
2026-06-23 10:47:30 +00:00
parent 0402e5ac76
commit 4dcdf5e8d1
2 changed files with 102 additions and 15 deletions

View File

@@ -179,6 +179,89 @@ impl XfreeProvider {
Ok(video_items)
}
/// Front-page feed used when there is no search query. This mirrors the
/// site's homepage, which dispatches `getAutoPop` against
/// `/api/post/?t=popular&nsfhp=true&limit=30&offset=N&lgbt=X` instead of the
/// `/api/2/search` endpoint. (`popular` is the real feed type the homepage
/// loads first; `posts` is only a Vuex store key and 404s as a `t=` value.)
/// The response body is the post array directly, not `body.posts`.
async fn front_page(
&self,
cache: VideoCache,
page: u8,
options: ServerOptions,
pool: DbPool,
) -> Result<Vec<VideoItem>> {
let sexuality = match options.clone().sexuality {
Some(s) if !s.is_empty() => s,
_ => "1".to_string(),
};
let offset = (page as u32 - 1) * 30;
let video_url = format!(
"{}/api/post/?t=popular&nsfhp=true&limit=30{}&lgbt={}",
self.url,
if page > 1 {
format!("&offset={offset}")
} else {
String::new()
},
sexuality,
);
// Check our Video Cache. If the result is younger than 1 hour, we return it.
let old_items = match cache.get(&video_url) {
Some((time, items)) => {
if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 * 24 {
return Ok(items.clone());
} else {
let _ = cache.check().await;
return Ok(items.clone());
}
}
None => {
vec![]
}
};
let mut requester =
crate::providers::requester_or_default(&options, module_path!(), "missing_requester");
let text = match requester
.get_with_headers(
&video_url,
vec![
("Apiversion".to_string(), "1.0".to_string()),
(
"Accept".to_string(),
"application/json text/plain */*".to_string(),
),
("Referer".to_string(), "https://www.xfree.com/".to_string()),
],
Some(Version::HTTP_2),
)
.await
{
Ok(text) => text,
Err(e) => {
crate::providers::report_provider_error(
"xfree",
"front_page.request",
&format!("url={video_url}; error={e}"),
)
.await;
return Ok(old_items);
}
};
let video_items: Vec<VideoItem> = self
.get_video_items_from_json(text.clone(), &mut requester, pool)
.await;
if !video_items.is_empty() {
cache.remove(&video_url);
cache.insert(video_url.clone(), video_items.clone());
} else {
return Ok(old_items);
}
Ok(video_items)
}
async fn get_video_items_from_json(
&self,
html: String,
@@ -201,12 +284,17 @@ impl XfreeProvider {
}
};
for post in json
.get("body")
// The search endpoint returns `{ body: { posts: [...] } }`, while the
// front-page feed (`/api/post/`) returns `{ body: [...] }` directly.
// Mirror the site's own logic (`body.posts ? body.posts : body`).
let empty: Vec<serde_json::Value> = vec![];
let body = json.get("body");
let posts = body
.and_then(|v| v.get("posts"))
.and_then(|p| p.as_array())
.unwrap_or(&vec![])
{
.or_else(|| body.and_then(|v| v.as_array()))
.unwrap_or(&empty);
for post in posts {
let id = post
.get("media")
.and_then(|v| v.get("name"))
@@ -319,16 +407,15 @@ impl Provider for XfreeProvider {
) -> Vec<VideoItem> {
let page = page.parse::<u8>().unwrap_or(1);
let res = self
.to_owned()
.query(
cache,
page,
&query.unwrap_or("null".to_string()),
options,
pool,
)
.await;
let query = query.unwrap_or_default();
let res = if query.trim().is_empty() {
// Empty query => front page feed, not the search endpoint.
self.to_owned().front_page(cache, page, options, pool).await
} else {
self.to_owned()
.query(cache, page, &query, options, pool)
.await
};
res.unwrap_or_else(|e| {
eprintln!("xfree error: {e}");