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:
Simon
2026-10-07 21:33:45 +00:00
parent 2c456dff59
commit ea567a791e

View File

@@ -3,7 +3,7 @@ use crate::api::ClientVersion;
use crate::providers::{Provider, report_provider_error, report_provider_error_background};
use crate::util::cache::VideoCache;
use crate::util::parse_abbreviated_number;
use crate::videos::{ServerOptions, VideoItem};
use crate::videos::{ServerOptions, VideoFormat, VideoItem};
use crate::{status::*, util};
use async_trait::async_trait;
use error_chain::error_chain;
@@ -96,6 +96,7 @@ impl BeegProvider {
async fn fetch_tags() -> Result<Value> {
let mut requester = util::requester::Requester::new();
// Try the old tags endpoint first
let endpoints = [
"https://store.externulls.com/tag/facts/tags?get_original=true&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())
}
@@ -268,7 +271,7 @@ impl BeegProvider {
}
let video_url = format!(
"https://store.externulls.com/facts/tag?limit=100&offset={}{}",
page - 1,
(page as u32 - 1) * 100,
match slug {
"" => "&id=27173".to_string(),
_ => format!("&slug={}", slug.replace(" ", "")),
@@ -302,7 +305,7 @@ impl BeegProvider {
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() {
cache.remove(&video_url);
cache.insert(video_url.clone(), video_items.clone());
@@ -321,10 +324,10 @@ impl BeegProvider {
) -> Result<Vec<VideoItem>> {
let video_url = format!(
"https://store.externulls.com/facts/tag?get_original=true&limit=100&offset={}&slug={}",
page - 1,
(page as u32 - 1) * 100,
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) {
Some((time, items)) => {
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
@@ -356,7 +359,7 @@ impl BeegProvider {
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() {
cache.remove(&video_url);
cache.insert(video_url.clone(), video_items.clone());
@@ -366,7 +369,11 @@ impl BeegProvider {
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 array = match json.as_array() {
Some(a) => a,
@@ -378,20 +385,47 @@ impl BeegProvider {
Some(v) => v,
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,
};
let key = match hls.get("fl_cdn_multi").and_then(|v| v.as_str()) {
Some(v) => v,
None => continue,
let file_id = file_id_str.as_str();
// 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
.get("id")
.and_then(|v| v.as_i64())
.unwrap_or(0)
.to_string();
// Parse the play_url response: key=xxx,end=xxx,limit=xxx/path
let (key, end, limit, path) = match Self::parse_play_url_response(&play_response) {
Some(parsed) => parsed,
None => {
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
.get("data")
.and_then(|v| v.get(0))
@@ -421,7 +455,7 @@ impl BeegProvider {
let mut item = VideoItem::new(
id,
title,
format!("https://video.externulls.com/{}", key),
hls_url.clone(),
"beeg".into(),
thumb,
duration as u32,
@@ -431,10 +465,49 @@ impl BeegProvider {
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
}
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]
@@ -464,4 +537,4 @@ impl Provider for BeegProvider {
fn get_channel(&self, clientversion: ClientVersion) -> Option<Channel> {
Some(self.build_channel(clientversion))
}
}
}