Skip to content

Commit 8555624

Browse files
committed
fix(route/rumble): improve feed metadata
Use structured detail-page data for Rumble descriptions and fallback thumbnails while keeping the channel listing on stable server-rendered markup. Limit detail fetch concurrency so the route is less likely to trip anti-bot protections.
1 parent e26f35b commit 8555624

1 file changed

Lines changed: 51 additions & 19 deletions

File tree

lib/routes/rumble/channel.ts

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,14 @@ function parseChannelTitle($: ReturnType<typeof load>): string {
4949
return title || 'Rumble';
5050
}
5151

52-
function parseDescription($: ReturnType<typeof load>): string | undefined {
52+
function parseDescription($: ReturnType<typeof load>, fallback: string | undefined): string | undefined {
5353
const paragraphs = $('div[data-js="media_long_description_container"] > p.media-description')
5454
.toArray()
5555
.map((element) => $.html(element))
5656
.filter(Boolean)
5757
.join('');
5858

59-
return paragraphs || $('meta[name="description"]').attr('content')?.trim() || undefined;
59+
return paragraphs || $('meta[name="description"]').attr('content')?.trim() || fallback || undefined;
6060
}
6161

6262
function parseStructuredVideoObject($: ReturnType<typeof load>) {
@@ -73,10 +73,14 @@ function parseStructuredVideoObject($: ReturnType<typeof load>) {
7373
for (const entry of entries) {
7474
if (entry?.['@type'] === 'VideoObject') {
7575
return entry as {
76+
description?: string;
7677
embedUrl?: string;
78+
genre?: string | string[];
79+
keywords?: string | string[];
7780
author?: {
7881
name?: string;
7982
};
83+
thumbnailUrl?: string | string[];
8084
};
8185
}
8286
}
@@ -86,6 +90,34 @@ function parseStructuredVideoObject($: ReturnType<typeof load>) {
8690
}
8791
}
8892

93+
function parseImage($: ReturnType<typeof load>, videoObject: ReturnType<typeof parseStructuredVideoObject>) {
94+
const thumbnailUrl = Array.isArray(videoObject?.thumbnailUrl) ? videoObject.thumbnailUrl[0] : videoObject?.thumbnailUrl;
95+
const image = thumbnailUrl || $('meta[property="og:image"]').attr('content')?.trim();
96+
97+
return image ? new URL(image, rootUrl).href : undefined;
98+
}
99+
100+
async function mapLimit<T, R>(values: T[], limit: number, mapper: (value: T, index: number) => Promise<R>) {
101+
const results = Array.from({ length: values.length }) as R[];
102+
let nextIndex = 0;
103+
104+
const worker = async (): Promise<void> => {
105+
const currentIndex = nextIndex;
106+
nextIndex += 1;
107+
108+
if (currentIndex >= values.length) {
109+
return;
110+
}
111+
112+
results[currentIndex] = await mapper(values[currentIndex], currentIndex);
113+
await worker();
114+
};
115+
116+
await Promise.all(Array.from({ length: Math.min(limit, values.length) }, () => worker()));
117+
118+
return results;
119+
}
120+
89121
function renderDescription(image: string | undefined, description: string | undefined, embedUrl: string | undefined, includeEmbed: boolean): string | undefined {
90122
const parts: string[] = [];
91123

@@ -113,13 +145,13 @@ function fetchVideoDetails(link: string) {
113145

114146
const $ = load(response);
115147
const videoObject = parseStructuredVideoObject($);
116-
const category = $('.media-by--category a').first().text().trim();
148+
const image = parseImage($, videoObject);
117149

118150
return {
119151
author: videoObject?.author?.name || $('.channel-header--title').first().text().trim() || undefined,
120-
category: category || undefined,
121-
description: parseDescription($),
152+
description: parseDescription($, videoObject?.description?.trim()),
122153
embedUrl: videoObject?.embedUrl,
154+
image,
123155
};
124156
});
125157
}
@@ -141,10 +173,11 @@ async function parseItemFromVideoElement($: ReturnType<typeof load>, videoElemen
141173

142174
const $img = $video.find('img.thumbnail__image, .thumbnail__thumb img').first();
143175
const imageRaw = $img.attr('src') || $img.attr('data-src');
144-
const image = imageRaw ? new URL(imageRaw, rootUrl).href : undefined;
176+
const listImage = imageRaw ? new URL(imageRaw, rootUrl).href : undefined;
145177
const pubDateRaw = $video.find('time.videostream__time[datetime], time[datetime]').first().attr('datetime')?.trim();
146178
const pubDate = pubDateRaw ? parseDate(pubDateRaw) : undefined;
147179
const details = await fetchVideoDetails(url.href);
180+
const image = listImage || details.image;
148181

149182
const media = image
150183
? {
@@ -163,7 +196,7 @@ async function parseItemFromVideoElement($: ReturnType<typeof load>, videoElemen
163196
return {
164197
title,
165198
author: details.author,
166-
category: details.category ? [details.category] : undefined,
199+
image,
167200
link: url.href,
168201
description,
169202
itunes_item_image: image,
@@ -189,19 +222,18 @@ async function handler(ctx) {
189222
const title = parseChannelTitle($);
190223

191224
const uniqueIds = new Set<string>();
192-
const items = await Promise.all(
193-
$('.channel-listing__container .videostream[data-video-id], .videostream.thumbnail__grid--item[data-video-id]')
194-
.toArray()
195-
.map((element) => {
196-
const videoId = $(element).attr('data-video-id')?.trim();
197-
if (!videoId || uniqueIds.has(videoId)) {
198-
return null;
199-
}
225+
const videoElements = $('.channel-listing__container .videostream[data-video-id], .videostream.thumbnail__grid--item[data-video-id]')
226+
.toArray()
227+
.filter((element) => {
228+
const videoId = $(element).attr('data-video-id')?.trim();
229+
if (!videoId || uniqueIds.has(videoId)) {
230+
return false;
231+
}
200232

201-
uniqueIds.add(videoId);
202-
return parseItemFromVideoElement($, element, includeEmbed);
203-
})
204-
);
233+
uniqueIds.add(videoId);
234+
return true;
235+
});
236+
const items = await mapLimit(videoElements, 5, (element) => parseItemFromVideoElement($, element, includeEmbed));
205237

206238
return {
207239
title: `Rumble - ${title}`,

0 commit comments

Comments
 (0)