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>} */ 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;