155 lines
5.5 KiB
JavaScript
155 lines
5.5 KiB
JavaScript
import { load } from 'cheerio';
|
|
|
|
/**
|
|
* Minimal Rumble provider handler
|
|
*/
|
|
const handler = {
|
|
id: 'ru',
|
|
label: 'Rumble',
|
|
/**
|
|
* @param {string} q
|
|
* @param {{ limit: number, page?: number }} opts
|
|
* @returns {Promise<Array<any>>}
|
|
*/
|
|
async search(q, opts) {
|
|
const { limit = 10, page = 1 } = opts || {};
|
|
try {
|
|
const perPage = Math.min(Math.max(1, Number(limit || 10)), 50);
|
|
const pageNum = Math.max(1, Number(page || 1));
|
|
const params = new URLSearchParams({ q: q });
|
|
if (pageNum > 1) params.set('page', pageNum.toString());
|
|
|
|
const response = await fetch(`https://rumble.com/search/video?${params.toString()}` , {
|
|
headers: {
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Rumble API error: ${response.status}`);
|
|
}
|
|
|
|
const html = await response.text();
|
|
const $ = load(html);
|
|
const items = [];
|
|
|
|
const parseDurationToSeconds = (raw) => {
|
|
if (raw == null) return undefined;
|
|
const value = String(raw).trim();
|
|
if (!value) return undefined;
|
|
|
|
// Plain numeric seconds
|
|
const numeric = Number(value);
|
|
if (Number.isFinite(numeric) && numeric > 0) return Math.floor(numeric);
|
|
|
|
// ISO-8601 style: PT#H#M#S
|
|
const isoMatch = value.match(/^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$/i);
|
|
if (isoMatch) {
|
|
const hours = Number(isoMatch[1] || 0);
|
|
const minutes = Number(isoMatch[2] || 0);
|
|
const seconds = Number(isoMatch[3] || 0);
|
|
const totalIso = (hours * 3600) + (minutes * 60) + seconds;
|
|
if (totalIso > 0) return totalIso;
|
|
}
|
|
|
|
// Text formats like "1h 2m 3s" or "15m13s"
|
|
const textMatch = value.match(/^(?:(\d+)\s*h(?:ours?)?)?\s*(?:(\d+)\s*m(?:in(?:utes)?)?)?\s*(?:(\d+)\s*s(?:ec(?:onds)?)?)?$/i);
|
|
if (textMatch && (textMatch[1] || textMatch[2] || textMatch[3])) {
|
|
const hours = Number(textMatch[1] || 0);
|
|
const minutes = Number(textMatch[2] || 0);
|
|
const seconds = Number(textMatch[3] || 0);
|
|
const totalText = (hours * 3600) + (minutes * 60) + seconds;
|
|
if (totalText > 0) return totalText;
|
|
}
|
|
|
|
// Colon separated HH:MM:SS or MM:SS
|
|
const colonCandidate = value.replace(/[^0-9:]/g, '');
|
|
if (colonCandidate.includes(':')) {
|
|
const segments = colonCandidate.split(':').filter(Boolean).map(s => Number(s));
|
|
if (segments.length >= 2 && segments.every(n => Number.isFinite(n))) {
|
|
while (segments.length < 3) segments.unshift(0);
|
|
const [hours, minutes, seconds] = segments.slice(-3);
|
|
const totalColon = (hours * 3600) + (minutes * 60) + seconds;
|
|
if (totalColon > 0) return totalColon;
|
|
}
|
|
}
|
|
|
|
// Fallback: first integer found, assume seconds if >0
|
|
const fallbackDigits = value.match(/(\d+)/);
|
|
if (fallbackDigits) {
|
|
const seconds = Number(fallbackDigits[1]);
|
|
if (Number.isFinite(seconds) && seconds > 0) return seconds;
|
|
}
|
|
|
|
return undefined;
|
|
};
|
|
|
|
$('li.video-listing-entry').each((_idx, el) => {
|
|
if (items.length >= perPage) return false;
|
|
const $el = $(el);
|
|
const anchor = ($el.find('a.video-item--a').attr('href') || '').trim();
|
|
if (!anchor) return;
|
|
|
|
const img = $el.find('img.video-item--img');
|
|
const rawThumbnail =
|
|
img.attr('data-src') ||
|
|
img.attr('data-original') ||
|
|
img.attr('src') ||
|
|
'';
|
|
const normalizedThumb = rawThumbnail.replace(/\s+/g, '').trim();
|
|
const title = $el.find('h3.video-item--title').text().replace(/\s+/g, ' ').trim();
|
|
const uploaderName = $el.find('.ellipsis-1').text().replace(/\s+/g, ' ').trim();
|
|
|
|
const url = anchor.startsWith('http') ? anchor : `https://rumble.com${anchor}`;
|
|
|
|
const id =
|
|
$el.attr('data-id') ||
|
|
url.split('/').filter(Boolean).pop() ||
|
|
String(Math.random());
|
|
|
|
const thumbnail = normalizedThumb
|
|
? normalizedThumb.startsWith('//')
|
|
? `https:${normalizedThumb}`
|
|
: normalizedThumb
|
|
: undefined;
|
|
|
|
const durationCandidates = [
|
|
$el.attr('data-duration'),
|
|
$el.attr('data-video-duration'),
|
|
$el.data('duration'),
|
|
$el.find('[data-duration]').attr('data-duration'),
|
|
$el.find('[data-video-duration]').attr('data-video-duration'),
|
|
$el.find('time[datetime]').attr('datetime'),
|
|
$el.find('.video-item--duration, .video-item--meta time, .video-item--meta .duration, .video-item--meta-duration, .video-item--length').first().text(),
|
|
];
|
|
|
|
let durationSeconds;
|
|
for (const candidate of durationCandidates) {
|
|
const parsed = parseDurationToSeconds(candidate);
|
|
if (typeof parsed === 'number' && parsed > 0) {
|
|
durationSeconds = parsed;
|
|
break;
|
|
}
|
|
}
|
|
|
|
items.push({
|
|
title: title || url,
|
|
id,
|
|
url,
|
|
thumbnail,
|
|
uploaderName: uploaderName || undefined,
|
|
type: 'video',
|
|
duration: durationSeconds
|
|
});
|
|
});
|
|
|
|
return items;
|
|
} catch (error) {
|
|
console.error('Rumble search error:', error);
|
|
return [];
|
|
}
|
|
}
|
|
};
|
|
export default handler;
|