fix(shorts): embeds DM/PT/Rumble/Twitch + flux Pour toi multi-fournisseurs
CI / build-and-test (push) Successful in 14m16s

This commit is contained in:
2026-09-29 09:38:13 -04:00
parent 34f432a7c1
commit 5994841bda
5 changed files with 504 additions and 42 deletions
+39 -10
View File
@@ -346,21 +346,29 @@ function parseDurationToSeconds(text) {
return 0; // Par défaut si aucun format n'est reconnu
}
async function scrapeRumbleList({ q, page = 1, limit = 24, sort = 'viral' }) {
async function scrapeRumbleList({ q, page = 1, limit = 24, sort = 'viral', mode = 'videos' }) {
try {
const url = q
? `https://rumble.com/search/video?q=${encodeURIComponent(q)}&page=${page}`
: `https://rumble.com/videos?sort=${encodeURIComponent(sort)}&page=${page}`;
// Mode "shorts" : le flux natif rumble.com/shorts (vertical ≤ 90 s).
// Les liens sont de la forme /shorts/vXXXX (plus éventuellement ?tracking).
const isShortsMode = mode === 'shorts';
const url = isShortsMode
? `https://rumble.com/shorts?page=${page}`
: q
? `https://rumble.com/search/video?q=${encodeURIComponent(q)}&page=${page}`
: `https://rumble.com/videos?sort=${encodeURIComponent(sort)}&page=${page}`;
const html = await httpGet(url);
const $ = cheerio.load(html);
const found = [];
// 1) Cartes "vidéos" standards (li/article/div)
$('a[href^="/v"], a[href^="/video/"]').each((_, el) => {
const href = $(el).attr('href') || '';
// On préfère STRICTEMENT l’ID /vXXXX
let m = /^\/(v[0-9A-Za-z]+)(?:[-/.]|$)/.exec(href);
const linkSelector = isShortsMode
? 'a[href*="/shorts/v"], a[href^="/v"], a[href^="/video/"]'
: 'a[href^="/v"], a[href^="/video/"]';
$(linkSelector).each((_, el) => {
const href = ($(el).attr('href') || '').split('?')[0];
// On préfère STRICTEMENT l’ID /vXXXX (aussi après /shorts/).
let m = /\/(v[0-9A-Za-z]+)(?:[-/.]|$)/.exec(href);
let id = m?.[1] || null;
// Fallback minimaliste pour /video/123 → on ne convertit pas ici; on laissera /video/:id passer au détails qui normalise par parse de la page.
@@ -460,9 +468,11 @@ async function scrapeRumbleList({ q, page = 1, limit = 24, sort = 'viral' }) {
uploaderName,
views,
duration: durationSeconds,
// Indicateur natif pour le front : une vidéo ≤ 75 s est un Short probable.
// Indicateur natif pour le front. En mode "shorts", tout ce qui sort
// du flux rumble.com/shorts (vertical ≤ 90 s) est un Short, même si
// le badge de durée est absent du HTML.
// (Rumble n'expose pas de flag natif ; la durée reste la source de vérité.)
isShort: durationSeconds > 0 && durationSeconds <= 75,
isShort: isShortsMode ? (durationSeconds <= 0 || durationSeconds <= 90) : (durationSeconds > 0 && durationSeconds <= 75),
uploadedDate: '',
url,
type: 'video'
@@ -517,6 +527,25 @@ router.get('/browse', async (req, res) => {
return res.json(data);
});
/* ---------------- Route "shorts" natifs (rumble.com/shorts) -------------- */
/**
* GET /api/rumble/shorts?page=&limit=
* Scrape le flux natif des Shorts Rumble (vertical ≤ 90 s) : IDs /vXXXX
* propres, compatibles avec l'embed officiel /embed/vXXXX/.
*/
router.get('/shorts', async (req, res) => {
const page = Math.max(1, parseInt(String(req.query.page || '1'), 10) || 1);
const limit = Math.min(50, Math.max(1, parseInt(String(req.query.limit || '24'), 10) || 24));
const key = cacheKey('/shorts', { page, limit });
const cached = getCache(key);
if (cached) return res.json(cached);
const data = await scrapeRumbleList({ page, limit, mode: 'shorts' });
setCache(key, data);
return res.json(data);
});
router.get('/search', async (req, res) => {
const q = String(req.query.q || '').trim();
if (!q) return res.status(400).json({ error: 'Query parameter required' });