11import { load } from 'cheerio' ;
22import type { Element } from 'domhandler' ;
3+ import pMap from 'p-map' ;
34
45import type { DataItem , Route } from '@/types' ;
56import { ViewType } from '@/types' ;
@@ -8,10 +9,18 @@ import ofetch from '@/utils/ofetch';
89import { parseDate } from '@/utils/parse-date' ;
910
1011const rootUrl = 'https://rumble.com' ;
12+ type RumbleVideoObject = {
13+ author ?: {
14+ name ?: string ;
15+ } ;
16+ description ?: string ;
17+ embedUrl ?: string ;
18+ thumbnailUrl ?: string ;
19+ } ;
1120
1221export const route : Route = {
1322 path : '/c/:channel/:embed?' ,
14- categories : [ 'social-media ' ] ,
23+ categories : [ 'multimedia ' ] ,
1524 view : ViewType . Videos ,
1625 name : 'Channel' ,
1726 maintainers : [ 'luckycold' ] ,
@@ -39,13 +48,7 @@ export const route: Route = {
3948} ;
4049
4150function parseChannelTitle ( $ : ReturnType < typeof load > ) : string {
42- const h1 = $ ( 'h1' ) . first ( ) . text ( ) . trim ( ) ;
43- if ( h1 ) {
44- return h1 ;
45- }
46-
47- const title = $ ( 'title' ) . first ( ) . text ( ) . trim ( ) ;
48- return title || 'Rumble' ;
51+ return $ ( 'title' ) . first ( ) . text ( ) . trim ( ) || 'Rumble' ;
4952}
5053
5154function parseDescription ( $ : ReturnType < typeof load > , fallback : string | undefined ) : string | undefined {
@@ -58,124 +61,45 @@ function parseDescription($: ReturnType<typeof load>, fallback: string | undefin
5861 return paragraphs || $ ( 'meta[name="description"]' ) . attr ( 'content' ) ?. trim ( ) || fallback || undefined ;
5962}
6063
61- function parseStructuredVideoObject ( $ : ReturnType < typeof load > ) {
62- for ( const element of $ ( 'script[type="application/ld+json"]' ) . toArray ( ) ) {
63- const content = $ ( element ) . text ( ) . trim ( ) ;
64- if ( ! content ) {
65- continue ;
66- }
67-
68- try {
69- const parsed = JSON . parse ( content ) ;
70- const entries = Array . isArray ( parsed ) ? parsed : [ parsed ] ;
71-
72- for ( const entry of entries ) {
73- if ( entry ?. [ '@type' ] === 'VideoObject' ) {
74- return entry as {
75- description ?: string ;
76- embedUrl ?: string ;
77- genre ?: string | string [ ] ;
78- keywords ?: string | string [ ] ;
79- author ?: {
80- name ?: string ;
81- } ;
82- thumbnailUrl ?: string | string [ ] ;
83- } ;
84- }
85- }
86- } catch {
87- continue ;
88- }
64+ function parseStructuredVideoObject ( $ : ReturnType < typeof load > ) : RumbleVideoObject | undefined {
65+ const content = $ ( 'script[type="application/ld+json"]' ) . first ( ) . text ( ) . trim ( ) ;
66+ if ( ! content ) {
67+ return ;
8968 }
90- }
91-
92- function parseImage ( $ : ReturnType < typeof load > , videoObject : ReturnType < typeof parseStructuredVideoObject > ) {
93- const thumbnailUrl = Array . isArray ( videoObject ?. thumbnailUrl ) ? videoObject . thumbnailUrl [ 0 ] : videoObject ?. thumbnailUrl ;
94- const image = thumbnailUrl || $ ( 'meta[property="og:image"]' ) . attr ( 'content' ) ?. trim ( ) ;
9569
96- return image ? new URL ( image , rootUrl ) . href : undefined ;
70+ try {
71+ const parsed = JSON . parse ( content ) ;
72+ const entries = Array . isArray ( parsed ) ? parsed : [ parsed ] ;
73+ return entries . find ( ( entry ) => entry ?. [ '@type' ] === 'VideoObject' ) as RumbleVideoObject | undefined ;
74+ } catch {
75+ return ;
76+ }
9777}
9878
99- async function mapLimit < T , R > ( values : T [ ] , limit : number , mapper : ( value : T , index : number ) => Promise < R > ) {
100- const results = Array . from ( { length : values . length } ) as R [ ] ;
101- let nextIndex = 0 ;
79+ function parseImage ( $ : ReturnType < typeof load > , videoObject : RumbleVideoObject | undefined ) {
80+ const image = videoObject ?. thumbnailUrl || $ ( 'meta[property="og:image"]' ) . attr ( 'content' ) ?. trim ( ) ;
10281
103- const worker = async ( ) : Promise < void > => {
104- const currentIndex = nextIndex ;
105- nextIndex += 1 ;
106-
107- if ( currentIndex >= values . length ) {
108- return ;
109- }
110-
111- results [ currentIndex ] = await mapper ( values [ currentIndex ] , currentIndex ) ;
112- await worker ( ) ;
113- } ;
114-
115- await Promise . all ( Array . from ( { length : Math . min ( limit , values . length ) } , ( ) => worker ( ) ) ) ;
116-
117- return results ;
82+ return image ? new URL ( image , rootUrl ) . href : undefined ;
11883}
11984
12085function renderDescription ( image : string | undefined , description : string | undefined , embedUrl : string | undefined , includeEmbed : boolean ) : string | undefined {
121- const parts : string [ ] = [ ] ;
86+ let descriptionHtml = '' ;
12287
12388 if ( includeEmbed && embedUrl ) {
124- parts . push ( `<iframe src="${ embedUrl } " width="640" height="360" frameborder="0" allowfullscreen></iframe>` ) ;
89+ descriptionHtml += `<iframe src="${ embedUrl } " width="640" height="360" frameborder="0" allowfullscreen></iframe>` ;
12590 } else if ( image ) {
126- parts . push ( `<p><img src="${ image } "></p>` ) ;
91+ descriptionHtml += `<p><img src="${ image } "></p>` ;
12792 }
12893
12994 if ( description ) {
130- parts . push ( description ) ;
95+ descriptionHtml += description ;
13196 }
13297
133- return parts . join ( '' ) ;
134- }
135-
136- function fetchVideoDetails ( link : string ) {
137- return cache . tryGet ( link , async ( ) => {
138- const response = await ofetch ( link , {
139- retryStatusCodes : [ 403 ] ,
140- } ) ;
141-
142- const $ = load ( response ) ;
143- const videoObject = parseStructuredVideoObject ( $ ) ;
144- const image = parseImage ( $ , videoObject ) ;
145-
146- return {
147- author : videoObject ?. author ?. name || $ ( '.channel-header--title' ) . first ( ) . text ( ) . trim ( ) || undefined ,
148- description : parseDescription ( $ , videoObject ?. description ?. trim ( ) ) ,
149- embedUrl : videoObject ?. embedUrl ,
150- image,
151- } ;
152- } ) ;
98+ return descriptionHtml || undefined ;
15399}
154100
155- async function parseItemFromVideoElement ( $ : ReturnType < typeof load > , videoElement : Element , includeEmbed : boolean ) : Promise < DataItem | null > {
156- const $video = $ ( videoElement ) ;
157- const $link = $video . find ( '.title__link[href], .videostream__link[href]' ) . first ( ) ;
158- const href = $link . attr ( 'href' ) ?. trim ( ) ;
159- if ( ! href ) {
160- return null ;
161- }
162-
163- const url = new URL ( href , rootUrl ) ;
164- url . search = '' ;
165- url . hash = '' ;
166-
167- const $title = $video . find ( '.thumbnail__title' ) . first ( ) ;
168- const title = $title . attr ( 'title' ) ?. trim ( ) || $title . text ( ) . trim ( ) || url . pathname ;
169-
170- const $img = $video . find ( 'img.thumbnail__image, .thumbnail__thumb img' ) . first ( ) ;
171- const imageRaw = $img . attr ( 'src' ) || $img . attr ( 'data-src' ) ;
172- const listImage = imageRaw ? new URL ( imageRaw , rootUrl ) . href : undefined ;
173- const pubDateRaw = $video . find ( 'time.videostream__time[datetime], time[datetime]' ) . first ( ) . attr ( 'datetime' ) ?. trim ( ) ;
174- const pubDate = pubDateRaw ? parseDate ( pubDateRaw ) : undefined ;
175- const details = await fetchVideoDetails ( url . href ) ;
176- const image = listImage || details . image ;
177-
178- const media = image
101+ function getMedia ( image : string | undefined ) : DataItem [ 'media' ] {
102+ return image
179103 ? {
180104 thumbnail : {
181105 url : image ,
@@ -186,17 +110,27 @@ async function parseItemFromVideoElement($: ReturnType<typeof load>, videoElemen
186110 } ,
187111 }
188112 : undefined ;
113+ }
189114
190- const description = renderDescription ( image , details . description , details . embedUrl , includeEmbed ) ;
115+ async function buildItem ( link : string , title : string , listImage : string | undefined , pubDate : Date | undefined , includeEmbed : boolean ) : Promise < DataItem > {
116+ const response = await ofetch ( link , {
117+ retryStatusCodes : [ 403 ] ,
118+ } ) ;
119+
120+ const $ = load ( response ) ;
121+ const videoObject = parseStructuredVideoObject ( $ ) ;
122+ const image = listImage || parseImage ( $ , videoObject ) ;
123+ const description = renderDescription ( image , parseDescription ( $ , videoObject ?. description ?. trim ( ) ) , videoObject ?. embedUrl , includeEmbed ) ;
124+ const author = videoObject ?. author ?. name || $ ( '.channel-header--title' ) . first ( ) . text ( ) . trim ( ) || undefined ;
191125
192126 return {
193127 title,
194- author : details . author ,
128+ author,
195129 image,
196- link : url . href ,
130+ link,
197131 description,
198132 itunes_item_image : image ,
199- media,
133+ media : getMedia ( image ) ,
200134 pubDate,
201135 } ;
202136}
@@ -215,19 +149,35 @@ async function handler(ctx) {
215149
216150 const title = parseChannelTitle ( $ ) ;
217151
218- const uniqueIds = new Set < string > ( ) ;
219- const videoElements = $ ( '.channel-listing__container .videostream[data-video-id], .videostream.thumbnail__grid--item[data-video-id]' )
220- . toArray ( )
221- . filter ( ( element ) => {
222- const videoId = $ ( element ) . attr ( 'data-video-id' ) ?. trim ( ) ;
223- if ( ! videoId || uniqueIds . has ( videoId ) ) {
224- return false ;
152+ const videoElements = $ ( '.videostream.thumbnail__grid--item[data-video-id]' ) . toArray ( ) ;
153+ const items = await pMap (
154+ videoElements ,
155+ ( element : Element ) => {
156+ const $video = $ ( element ) ;
157+ const $link = $video . find ( '.videostream__link[href]' ) . first ( ) ;
158+ const href = $link . attr ( 'href' ) ?. trim ( ) ;
159+ if ( ! href ) {
160+ return null ;
161+ }
162+
163+ const url = new URL ( href , rootUrl ) ;
164+ url . search = '' ;
165+
166+ const $title = $video . find ( '.thumbnail__title' ) . first ( ) ;
167+ const title = $title . attr ( 'title' ) ?. trim ( ) || $title . text ( ) . trim ( ) ;
168+ if ( ! title ) {
169+ return null ;
225170 }
226171
227- uniqueIds . add ( videoId ) ;
228- return true ;
229- } ) ;
230- const items = await mapLimit ( videoElements , 5 , ( element ) => parseItemFromVideoElement ( $ , element , includeEmbed ) ) ;
172+ const imageRaw = $video . find ( 'img.thumbnail__image, .thumbnail__thumb img' ) . first ( ) . attr ( 'src' ) ;
173+ const listImage = imageRaw ? new URL ( imageRaw , rootUrl ) . href : undefined ;
174+ const pubDateRaw = $video . find ( 'time.videostream__time[datetime]' ) . first ( ) . attr ( 'datetime' ) ?. trim ( ) ;
175+ const pubDate = pubDateRaw ? parseDate ( pubDateRaw ) : undefined ;
176+
177+ return cache . tryGet ( `${ url . href } :${ includeEmbed ? 'embed' : 'noembed' } ` , ( ) => buildItem ( url . href , title , listImage , pubDate , includeEmbed ) ) ;
178+ } ,
179+ { concurrency : 5 }
180+ ) ;
231181
232182 return {
233183 title : `Rumble - ${ title } ` ,
0 commit comments