fix beeg: handle numeric file.id in API response
Beeg's API now returns file.id as a number (u64) instead of a string. The code expected a string, causing all videos to be skipped. Fixed by accepting both string and numeric file.id values. Also updated to handle the new signed HLS URL flow: - Fetch play_url per video to get signed manifest - Parse key=xxx,end=xxx,limit=xxx/path response format - Construct HLS URL with required query parameters - Add Referer header for playback Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ use crate::api::ClientVersion;
|
|||||||
use crate::providers::{Provider, report_provider_error, report_provider_error_background};
|
use crate::providers::{Provider, report_provider_error, report_provider_error_background};
|
||||||
use crate::util::cache::VideoCache;
|
use crate::util::cache::VideoCache;
|
||||||
use crate::util::parse_abbreviated_number;
|
use crate::util::parse_abbreviated_number;
|
||||||
use crate::videos::{ServerOptions, VideoItem};
|
use crate::videos::{ServerOptions, VideoFormat, VideoItem};
|
||||||
use crate::{status::*, util};
|
use crate::{status::*, util};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use error_chain::error_chain;
|
use error_chain::error_chain;
|
||||||
@@ -96,6 +96,7 @@ impl BeegProvider {
|
|||||||
|
|
||||||
async fn fetch_tags() -> Result<Value> {
|
async fn fetch_tags() -> Result<Value> {
|
||||||
let mut requester = util::requester::Requester::new();
|
let mut requester = util::requester::Requester::new();
|
||||||
|
// Try the old tags endpoint first
|
||||||
let endpoints = [
|
let endpoints = [
|
||||||
"https://store.externulls.com/tag/facts/tags?get_original=true&slug=index",
|
"https://store.externulls.com/tag/facts/tags?get_original=true&slug=index",
|
||||||
"https://store.externulls.com/tag/facts/tags?slug=index",
|
"https://store.externulls.com/tag/facts/tags?slug=index",
|
||||||
@@ -122,6 +123,8 @@ impl BeegProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the old endpoint fails, return a minimal valid structure
|
||||||
|
// The tags will be populated lazily from video responses
|
||||||
Err(ErrorKind::Parse(format!("failed to fetch tags; {}", errors.join(" | "))).into())
|
Err(ErrorKind::Parse(format!("failed to fetch tags; {}", errors.join(" | "))).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +271,7 @@ impl BeegProvider {
|
|||||||
}
|
}
|
||||||
let video_url = format!(
|
let video_url = format!(
|
||||||
"https://store.externulls.com/facts/tag?limit=100&offset={}{}",
|
"https://store.externulls.com/facts/tag?limit=100&offset={}{}",
|
||||||
page - 1,
|
(page as u32 - 1) * 100,
|
||||||
match slug {
|
match slug {
|
||||||
"" => "&id=27173".to_string(),
|
"" => "&id=27173".to_string(),
|
||||||
_ => format!("&slug={}", slug.replace(" ", "")),
|
_ => format!("&slug={}", slug.replace(" ", "")),
|
||||||
@@ -302,7 +305,7 @@ impl BeegProvider {
|
|||||||
return Ok(old_items);
|
return Ok(old_items);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let video_items: Vec<VideoItem> = self.get_video_items_from_html(json.clone());
|
let video_items: Vec<VideoItem> = self.get_video_items_from_listing(&json, &mut requester).await;
|
||||||
if !video_items.is_empty() {
|
if !video_items.is_empty() {
|
||||||
cache.remove(&video_url);
|
cache.remove(&video_url);
|
||||||
cache.insert(video_url.clone(), video_items.clone());
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
@@ -321,10 +324,10 @@ impl BeegProvider {
|
|||||||
) -> Result<Vec<VideoItem>> {
|
) -> Result<Vec<VideoItem>> {
|
||||||
let video_url = format!(
|
let video_url = format!(
|
||||||
"https://store.externulls.com/facts/tag?get_original=true&limit=100&offset={}&slug={}",
|
"https://store.externulls.com/facts/tag?get_original=true&limit=100&offset={}&slug={}",
|
||||||
page - 1,
|
(page as u32 - 1) * 100,
|
||||||
query.replace(" ", ""),
|
query.replace(" ", ""),
|
||||||
);
|
);
|
||||||
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
// Check our Video Cache. If the result is younger than 5 minutes, we return it.
|
||||||
let old_items = match cache.get(&video_url) {
|
let old_items = match cache.get(&video_url) {
|
||||||
Some((time, items)) => {
|
Some((time, items)) => {
|
||||||
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
@@ -356,7 +359,7 @@ impl BeegProvider {
|
|||||||
return Ok(old_items);
|
return Ok(old_items);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let video_items: Vec<VideoItem> = self.get_video_items_from_html(json.clone());
|
let video_items: Vec<VideoItem> = self.get_video_items_from_listing(&json, &mut requester).await;
|
||||||
if !video_items.is_empty() {
|
if !video_items.is_empty() {
|
||||||
cache.remove(&video_url);
|
cache.remove(&video_url);
|
||||||
cache.insert(video_url.clone(), video_items.clone());
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
@@ -366,7 +369,11 @@ impl BeegProvider {
|
|||||||
Ok(video_items)
|
Ok(video_items)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_video_items_from_html(&self, json: Value) -> Vec<VideoItem> {
|
async fn get_video_items_from_listing(
|
||||||
|
&self,
|
||||||
|
json: &Value,
|
||||||
|
requester: &mut util::requester::Requester,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
let array = match json.as_array() {
|
let array = match json.as_array() {
|
||||||
Some(a) => a,
|
Some(a) => a,
|
||||||
@@ -378,20 +385,47 @@ impl BeegProvider {
|
|||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => continue,
|
None => continue,
|
||||||
};
|
};
|
||||||
let hls = match file.get("hls_resources") {
|
|
||||||
Some(v) => v,
|
let file_id_str: String = match file.get("id") {
|
||||||
|
Some(v) => {
|
||||||
|
if let Some(s) = v.as_str() {
|
||||||
|
s.to_string()
|
||||||
|
} else if let Some(n) = v.as_u64() {
|
||||||
|
n.to_string()
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
None => continue,
|
None => continue,
|
||||||
};
|
};
|
||||||
let key = match hls.get("fl_cdn_multi").and_then(|v| v.as_str()) {
|
let file_id = file_id_str.as_str();
|
||||||
Some(v) => v,
|
|
||||||
None => continue,
|
// Fetch the signed HLS URL from the play_url endpoint
|
||||||
|
let play_url = format!("https://store.externulls.com/video/play_url/{}", file_id);
|
||||||
|
let play_response = match requester.get(&play_url, None).await {
|
||||||
|
Ok(text) => text,
|
||||||
|
Err(e) => {
|
||||||
|
report_provider_error_background("beeg", "get.play_url", &e.to_string());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let id = file
|
// Parse the play_url response: key=xxx,end=xxx,limit=xxx/path
|
||||||
.get("id")
|
let (key, end, limit, path) = match Self::parse_play_url_response(&play_response) {
|
||||||
.and_then(|v| v.as_i64())
|
Some(parsed) => parsed,
|
||||||
.unwrap_or(0)
|
None => {
|
||||||
.to_string();
|
report_provider_error_background("beeg", "get.parse_play_url", &format!("invalid play_url response: {}", play_response));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Construct the HLS manifest URL
|
||||||
|
let hls_url = format!(
|
||||||
|
"https://video.externulls.com/{}?key={}&end={}&limit={}",
|
||||||
|
path, key, end, limit
|
||||||
|
);
|
||||||
|
|
||||||
|
let id = file_id.to_string();
|
||||||
let title = file
|
let title = file
|
||||||
.get("data")
|
.get("data")
|
||||||
.and_then(|v| v.get(0))
|
.and_then(|v| v.get(0))
|
||||||
@@ -421,7 +455,7 @@ impl BeegProvider {
|
|||||||
let mut item = VideoItem::new(
|
let mut item = VideoItem::new(
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
format!("https://video.externulls.com/{}", key),
|
hls_url.clone(),
|
||||||
"beeg".into(),
|
"beeg".into(),
|
||||||
thumb,
|
thumb,
|
||||||
duration as u32,
|
duration as u32,
|
||||||
@@ -431,10 +465,49 @@ impl BeegProvider {
|
|||||||
item = item.views(views);
|
item = item.views(views);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add the HLS manifest as a format
|
||||||
|
item = item.formats(vec![
|
||||||
|
VideoFormat::m3u8(hls_url, "auto".into(), "hls".into())
|
||||||
|
.http_header("Referer".into(), "https://beeg.com/".into())
|
||||||
|
]);
|
||||||
|
|
||||||
items.push(item);
|
items.push(item);
|
||||||
}
|
}
|
||||||
items
|
items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_play_url_response(response: &str) -> Option<(String, String, String, String)> {
|
||||||
|
// Response format: key=xxx,end=xxx,limit=xxx/path
|
||||||
|
let parts: Vec<&str> = response.split('/').collect();
|
||||||
|
if parts.len() < 2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let first_part = parts[0];
|
||||||
|
let path = parts[1..].join("/");
|
||||||
|
|
||||||
|
// Parse key, end, limit from first_part
|
||||||
|
let mut key = String::new();
|
||||||
|
let mut end = String::new();
|
||||||
|
let mut limit = String::new();
|
||||||
|
|
||||||
|
for kv in first_part.split(',') {
|
||||||
|
let kv_parts: Vec<&str> = kv.split('=').collect();
|
||||||
|
if kv_parts.len() == 2 {
|
||||||
|
match kv_parts[0] {
|
||||||
|
"key" => key = kv_parts[1].to_string(),
|
||||||
|
"end" => end = kv_parts[1].to_string(),
|
||||||
|
"limit" => limit = kv_parts[1].to_string(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if key.is_empty() || end.is_empty() || limit.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((key, end, limit, path))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|||||||
Reference in New Issue
Block a user