modified: src/providers/sources/bludclart.ts

This commit is contained in:
Chris 2025-12-18 18:06:09 +00:00
parent f8fef6f957
commit 6b4af83229

View file

@ -65,18 +65,23 @@ function proxyUrl(url: string): string {
}
async function customFetcher(ctx: ShowScrapeContext | MovieScrapeContext, url: string, options?: any): Promise<any> {
// Try up to 3 different proxies before falling back to direct access
for (let attempt = 0; attempt < 3; attempt++) {
try {
const proxiedUrl = proxyUrl(url);
return await ctx.fetcher(proxiedUrl, options);
const response = await ctx.fetcher(proxiedUrl, options);
// ✅ Detect Cloudflare "Just a moment..." challenge
if (typeof response === 'string' && response.includes('<title>Just a moment')) {
throw new Error('Cloudflare challenge');
}
return response;
} catch (error) {
// If it's a 520 error (Cloudflare issue), try next proxy
if (attempt < 2) continue;
// On last proxy attempt failure, try direct access
return ctx.proxiedFetcher(url, options);
}
}
throw new Error('All fetch attempts failed');
}
async function getShareKey(
@ -95,20 +100,16 @@ async function getShareKey(
const data = response;
if (typeof data === 'object' && data !== null) {
// Check for nested data
if ('data' in data && typeof data.data === 'object' && data.data !== null && 'link' in data.data) {
return (data.data as any).link.split('/').pop();
}
if ('link' in data) {
// Maybe it returns a febbox link? https://www.febbox.com/share/KEY
return (data as any).link.split('/').pop();
}
if ('key' in data) {
return (data as any).key;
}
if ('data' in data) {
// generic data field
const val = (data as any).data;
if (typeof val === 'string' && val.includes('febbox.com')) {
return val.split('/').pop() || null;
@ -136,12 +137,9 @@ async function getShowboxId(
});
const $ = load(response);
const results = $('.film-poster-ahref');
if (results.length === 0) {
return null;
}
if (results.length === 0) return null;
let targetUrl: string | null = null;
for (const result of results.toArray()) {
@ -173,7 +171,6 @@ async function getShowboxId(
return showId || null;
}
// Fallback: extract from URL
const match = targetUrl.match(/-(\d+)$/);
if (match) {
return match[1];
@ -191,13 +188,10 @@ async function getFebboxFileList(
shareKey: string,
parentId: number = 0,
): Promise<any> {
const pidStr = parentId.toString();
const listUrl = `${FEBBOX_BASE}/file/file_share_list?share_key=${shareKey}&pwd=&parent_id=${pidStr}`;
const listUrl = `${FEBBOX_BASE}/file/file_share_list?share_key=${shareKey}&pwd=&parent_id=${parentId}`;
const response = await customFetcher(ctx, listUrl, {
headers: REQUEST_HEADERS,
});
return response;
}
@ -209,36 +203,34 @@ async function findFileFid(
episode?: number,
): Promise<number | null> {
const rootData = await getFebboxFileList(ctx, shareKey);
if (!rootData || !rootData.data || !rootData.data.file_list) {
return null;
}
if (!rootData?.data?.file_list) return null;
const files = rootData.data.file_list;
if (mediaType === 'movie') {
const videoFiles = files.filter((f: any) => f.ext && ['mp4', 'mkv', 'avi'].includes(f.ext.toLowerCase()));
if (videoFiles.length === 0) {
return null;
}
const videoFiles = files.filter((f: any) =>
f.ext && ['mp4', 'mkv', 'avi', 'm3u8'].includes(f.ext.toLowerCase())
);
if (videoFiles.length === 0) return null;
videoFiles.sort(
(a: any, b: any) => parseInt(b.file_size_bytes || '0', 10) - parseInt(a.file_size_bytes || '0', 10),
(a: any, b: any) => parseInt(b.file_size_bytes || '0', 10) - parseInt(a.file_size_bytes || '0', 10)
);
return videoFiles[0].fid;
}
if (mediaType === 'tv' && season && episode) {
const seasonFolderName = `Season ${season}`;
const seasonFolderNamePad = `Season ${season.toString().padStart(2, '0')}`;
if (mediaType === 'tv' && season != null && episode != null) {
const seasonNames = [
`Season ${season}`,
`Season ${season.toString().padStart(2, '0')}`,
`S${season.toString().padStart(2, '0')}`,
];
let seasonFid: number | null = null;
for (const f of files) {
if (f.is_dir === 1) {
const fname = f.file_name.trim();
if (
fname.toLowerCase() === seasonFolderName.toLowerCase() ||
fname.toLowerCase() === seasonFolderNamePad.toLowerCase()
) {
if (seasonNames.some(sn => fname.toLowerCase() === sn.toLowerCase())) {
seasonFid = f.fid;
break;
}
@ -246,47 +238,40 @@ async function findFileFid(
}
if (!seasonFid) {
const seasonRegex = new RegExp(`\\b[Ss]?0?${season}\\b`, 'i');
for (const f of files) {
if (f.is_dir === 1) {
const fname = f.file_name.trim();
const seasonRegex = new RegExp(`S0?${season}\\b`, 'i');
const seasonOnlyRegex = new RegExp(`\\b${season}\\b.*season`, 'i');
if (seasonRegex.test(fname) && !seasonOnlyRegex.test(fname)) {
seasonFid = f.fid;
break;
}
if (f.is_dir === 1 && seasonRegex.test(f.file_name)) {
seasonFid = f.fid;
break;
}
}
}
if (!seasonFid) {
return null;
}
if (!seasonFid) return null;
const seasonData = await getFebboxFileList(ctx, shareKey, seasonFid);
if (!seasonData || !seasonData.data || !seasonData.data.file_list) {
return null;
}
if (!seasonData?.data?.file_list) return null;
const episodeFiles = seasonData.data.file_list;
const episodeFiles = seasonData.data.file_list.filter(
(f: any) => f.is_dir === 0 && f.ext && ['mp4', 'mkv', 'avi', 'm3u8'].includes(f.ext.toLowerCase())
);
const episodePatterns = [
new RegExp(`S0?${season}E0?${episode}\\b`, 'i'),
new RegExp(`${season}x0?${episode}\\b`, 'i'),
new RegExp(`E0?${episode}\\b`, 'i'),
new RegExp(`\\b${episode}\\b`, 'i'), // Risky, might match other numbers
const epStr = episode.toString().padStart(2, '0');
const sStr = season.toString().padStart(2, '0');
const patterns = [
new RegExp(`[Ss]${sStr}[Ee]${epStr}`, 'i'),
new RegExp(`${season}x${epStr}`, 'i'),
new RegExp(`[Ee]${epStr}\\b`, 'i'),
];
for (const f of episodeFiles) {
if (f.is_dir === 0 && f.ext && ['mp4', 'mkv', 'avi'].includes(f.ext.toLowerCase())) {
const fname = f.file_name;
for (const pattern of episodePatterns) {
if (pattern.test(fname)) {
return f.fid;
}
}
const name = f.file_name;
if (patterns.some(p => p.test(name))) {
return f.fid;
}
}
if (episodeFiles.length === 1) return episodeFiles[0].fid;
}
return null;
@ -313,22 +298,29 @@ async function getStreamUrl(
body: JSON.stringify(payload),
});
if (response && response.sources && response.sources.length > 0) {
const sources = response.sources;
if (response?.sources?.length) {
const sources = [...response.sources];
// Sort by quality ranking
// Sort by quality
const qualityRank = (q: string): number => {
const quality = q.toUpperCase();
if (quality.includes('1080')) return 100;
if (quality.includes('720')) return 80;
if (quality === 'ORG') return 60;
if (quality.includes('480')) return 40;
if (quality.includes('360')) return 20;
const Q = q.toUpperCase();
if (Q.includes('1080')) return 100;
if (Q.includes('720')) return 80;
if (Q === 'ORG') return 60;
if (Q.includes('480')) return 40;
if (Q.includes('360')) return 20;
return 0;
};
sources.sort((a: any, b: any) => qualityRank(b.quality) - qualityRank(a.quality));
return sources[0].download_url;
// ✅ CRITICAL FIX: Prefer .m3u8 URLs for HLS playback
const hlsSource = sources.find(
(s: any) => (s.url || s.download_url)?.endsWith('.m3u8')
);
const best = hlsSource || sources[0];
return best.url || best.download_url;
}
return null;
@ -369,7 +361,6 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis
ctx.progress(50);
const streamUrl = await getStreamUrl(ctx, fid, shareKey);
if (!streamUrl) {
throw new NotFoundError('No stream found');
}
@ -381,7 +372,7 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis
stream: [
{
id: 'primary',
type: 'hls' as const,
type: 'hls',
playlist: streamUrl,
flags: [flags.CORS_ALLOWED],
captions: [],
@ -398,4 +389,4 @@ export const bludclartScraper = makeSourcerer({
flags: [flags.CORS_ALLOWED],
scrapeMovie: comboScraper,
scrapeShow: comboScraper,
});
});