chore: update Angular cache and TypeScript build info

This commit is contained in:
2025-09-23 21:45:57 -04:00
parent d372b7d509
commit 9a3983a253
18 changed files with 664 additions and 169 deletions
+14 -7
View File
@@ -6,18 +6,21 @@ const handler = {
label: 'Dailymotion',
/**
* @param {string} q
* @param {{ limit: number, page?: number }} opts
* @param {{ limit: number, page?: number, sort?: 'relevance'|'date'|'views' }} opts
* @returns {Promise<Array<any>>}
*/
async search(q, opts) {
const { limit = 10 } = opts;
const { limit = 10, page = 1, sort = 'relevance' } = opts || {};
try {
const response = await fetch(
`https://api.dailymotion.com/videos?` +
new URLSearchParams({
search: q,
limit: Math.min(limit, 100).toString(),
sort: 'relevance'
page: Math.max(1, Number(page || 1)).toString(),
// Map our sort to Dailymotion API sort: relevance | visited (views) | recent (date)
sort: (sort === 'date' ? 'recent' : (sort === 'views' ? 'visited' : 'relevance')),
fields: 'id,title,thumbnail_url,thumbnail_360_url,thumbnail_480_url,thumbnail_720_url,duration,views_total,owner.screenname,owner.avatar_80_url,created_time'
})
);
@@ -28,11 +31,15 @@ const handler = {
const data = await response.json();
return (data.list || []).map(item => ({
title: item.title,
id: item.id,
title: item.title || '',
id: item.id || '',
url: `https://www.dailymotion.com/video/${item.id}`,
thumbnail: item.thumbnail_360_url || item.thumbnail_180_url || item.thumbnail_url,
uploaderName: item.owner.screenname || item.owner.username,
thumbnail: item.thumbnail_720_url || item.thumbnail_480_url || item.thumbnail_360_url || item.thumbnail_url || '',
uploaderName: item['owner.screenname'] || item['owner.username'] || '',
uploaderAvatar: item['owner.avatar_80_url'] || '',
duration: Number(item.duration || 0),
views: Number(item.views_total || 0),
uploadedDate: item.created_time ? new Date(item.created_time * 1000).toISOString() : '',
type: 'video'
}));
} catch (error) {
+35 -17
View File
@@ -10,16 +10,19 @@ const handler = {
* @returns {Promise<Array<any>>}
*/
async search(q, opts) {
const { limit = 10 } = opts;
const { limit = 10, page = 1 } = opts || {};
try {
const response = await fetch(
`https://lighthouse.odysee.tv/content/search?` +
new URLSearchParams({
query: q,
size: Math.min(limit, 50).toString(),
page: '1'
})
);
const perPage = Math.min(Math.max(1, Number(limit || 10)), 50);
const pageNum = Math.max(1, Number(page || 1));
const params = new URLSearchParams({
s: q,
size: perPage.toString(),
from: ((pageNum - 1) * perPage).toString(),
include: 'channel,thumbnail_url,title,description,duration,release_time,claimId,name',
mediaType: 'video'
});
const response = await fetch(`https://lighthouse.odysee.tv/search?${params.toString()}`);
if (!response.ok) {
throw new Error(`Odysee API error: ${response.status}`);
@@ -27,14 +30,29 @@ const handler = {
const data = await response.json();
return (data || []).map(item => ({
title: item.title,
id: item.claimId,
url: `https://odysee.com/${item.canonical_url}`,
thumbnail: item.thumbnail_url,
uploaderName: item.channel_name || item.publisher_name,
type: 'video'
}));
return (Array.isArray(data) ? data : []).map(item => {
const rawThumb = item.thumbnail_url || '';
const thumbnail = rawThumb
? rawThumb.startsWith('http')
? rawThumb
: `https://thumbnails.odycdn.com/optimize/s:390:0/quality:85/plain/${rawThumb.replace(/^\//, '')}`
: undefined;
const name = item.name || '';
const claimId = item.claimId || item.claim_id || '';
const urlSegment = claimId ? `${encodeURIComponent(name)}:${claimId}` : encodeURIComponent(name);
return {
title: item.title || name,
id: claimId || name,
url: claimId ? `https://odysee.com/${urlSegment}` : `https://odysee.com/${encodeURIComponent(name)}`,
thumbnail,
uploaderName: item.channel || undefined,
type: 'video',
duration: typeof item.duration === 'number' && item.duration > 0
? Math.round(item.duration)
: (typeof item.video?.duration === 'number' && item.video.duration > 0 ? Math.round(item.video.duration) : undefined)
};
});
} catch (error) {
console.error('Odysee search error:', error);
return [];
+23 -10
View File
@@ -10,14 +10,15 @@ const handler = {
* @returns {Promise<Array<any>>}
*/
async search(q, opts) {
const { limit = 10 } = opts;
const { limit = 10, page = 1 } = opts || {};
try {
// Use PeerTube API search
const response = await fetch(
`https://sepiasearch.org/api/v1/search/videos?` +
new URLSearchParams({
search: q,
count: Math.min(limit, 50).toString()
count: Math.min(limit, 50).toString(),
start: Math.max(0, (Math.max(1, Number(page || 1)) - 1) * Math.min(limit, 50)).toString()
})
);
@@ -27,14 +28,26 @@ const handler = {
const data = await response.json();
return (data.data || []).map(item => ({
title: item.name,
id: item.uuid,
url: item.url,
thumbnail: item.thumbnailPath,
uploaderName: item.account.displayName || item.account.name,
type: 'video'
}));
return (data.data || []).map(item => {
const rawThumb = item.thumbnailUrl || item.thumbnailPath || '';
const thumbnail = rawThumb
? rawThumb.startsWith('http')
? rawThumb
: rawThumb.startsWith('//')
? `https:${rawThumb}`
: new URL(rawThumb, item.url || 'https://sepiasearch.org').toString()
: undefined;
return {
title: item.name,
id: item.uuid,
url: item.url,
thumbnail,
uploaderName: (item.account && (item.account.displayName || item.account.name)) || undefined,
type: 'video',
duration: typeof item.duration === 'number' && item.duration > 0 ? Math.round(item.duration) : undefined
};
});
} catch (error) {
console.error('PeerTube search error:', error);
return [];
+128 -17
View File
@@ -1,3 +1,5 @@
import { load } from 'cheerio';
/**
* Minimal Rumble provider handler
*/
@@ -10,30 +12,139 @@ const handler = {
* @returns {Promise<Array<any>>}
*/
async search(q, opts) {
const { limit = 10 } = opts;
const { limit = 10, page = 1 } = opts || {};
try {
const response = await fetch(
`https://rumble.com/api/search/videos?` +
new URLSearchParams({
q: q,
size: Math.min(limit, 50).toString()
})
);
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 data = await response.json();
const html = await response.text();
const $ = load(html);
const items = [];
return (data.videos || []).map(item => ({
title: item.title,
id: item.id,
url: `https://rumble.com${item.url}`,
thumbnail: item.thumbnail,
uploaderName: item.author.name,
type: 'video'
}));
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 [];
+42 -21
View File
@@ -10,7 +10,7 @@ const handler = {
* @returns {Promise<Array<any>>}
*/
async search(q, opts) {
const { limit = 10 } = opts;
const { limit = 10, page = 1 } = opts || {};
try {
// First, get OAuth token
const authResponse = await fetch('https://id.twitch.tv/oauth2/token', {
@@ -31,28 +31,49 @@ const handler = {
const { access_token } = await authResponse.json();
// Then search streams
const response = await fetch(
`https://api.twitch.tv/helix/search/channels?` +
new URLSearchParams({
query: q,
first: Math.min(limit, 100).toString()
}),
{
headers: {
'Client-ID': process.env.TWITCH_CLIENT_ID,
'Authorization': `Bearer ${access_token}`
}
}
);
// Then search channels with cursor-based pagination
const perPage = Math.min(Math.max(1, Number(limit || 10)), 100);
const targetPage = Math.max(1, Number(page || 1));
let after = '';
let currentPage = 1;
let lastData = [];
if (!response.ok) {
throw new Error(`Twitch API error: ${response.status}`);
while (currentPage <= targetPage) {
const params = new URLSearchParams({
query: q,
first: String(perPage)
});
if (after) params.set('after', after);
const response = await fetch(
`https://api.twitch.tv/helix/search/channels?` + params.toString(),
{
headers: {
'Client-ID': process.env.TWITCH_CLIENT_ID,
'Authorization': `Bearer ${access_token}`
}
}
);
if (!response.ok) {
throw new Error(`Twitch API error: ${response.status}`);
}
const data = await response.json();
if (currentPage === targetPage) {
lastData = Array.isArray(data.data) ? data.data : [];
break;
}
const nextCursor = data?.pagination?.cursor;
if (!nextCursor) {
lastData = [];
break;
}
after = String(nextCursor);
currentPage++;
}
const data = await response.json();
return data.data?.map(item => ({
return lastData.map(item => ({
title: item.title || item.display_name,
id: item.id,
url: `https://www.twitch.tv/${item.broadcaster_login}`,
@@ -60,7 +81,7 @@ const handler = {
uploaderName: item.display_name,
type: 'stream',
isLive: item.is_live || item.started_at
})) || [];
}));
} catch (error) {
console.error('Twitch search error:', error);
return [];
+88 -18
View File
@@ -10,12 +10,22 @@
* @property {string=} type
*/
function parseISODurationToSeconds(iso) {
if (typeof iso !== 'string' || !iso) return 0;
const match = iso.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/);
if (!match) return 0;
const hours = Number(match[1] || 0);
const minutes = Number(match[2] || 0);
const seconds = Number(match[3] || 0);
return (hours * 3600) + (minutes * 60) + seconds;
}
/** @type {{ id: 'yt', label: string, search: (q: string, opts: { limit: number, page?: number, sort?: 'relevance'|'date'|'views' }) => Promise<Suggestion[]> }} */
const handler = {
id: 'yt',
label: 'YouTube',
async search(q, opts) {
const { limit = 10, sort = 'relevance' } = opts || {};
const { limit = 10, page = 1, sort = 'relevance' } = opts || {};
try {
const API_KEY = process.env.YOUTUBE_API_KEY;
if (!API_KEY) {
@@ -26,32 +36,92 @@ const handler = {
if (sort === 'date') order = 'date';
else if (sort === 'views') order = 'viewCount';
const response = await fetch(
`https://www.googleapis.com/youtube/v3/search?` +
new URLSearchParams({
// Iterate nextPageToken to reach the requested page (1-based)
const perPage = Math.min(Math.max(1, Number(limit || 10)), 50);
const targetPage = Math.max(1, Number(page || 1));
let pageToken = '';
let currentPage = 1;
let lastItems = [];
while (currentPage <= targetPage) {
const params = new URLSearchParams({
part: 'snippet',
q: q,
type: 'video',
maxResults: Math.min(limit, 50).toString(),
maxResults: String(perPage),
key: API_KEY,
order
})
);
});
if (pageToken) params.set('pageToken', pageToken);
if (!response.ok) {
throw new Error(`YouTube API error: ${response.status}`);
const response = await fetch(`https://www.googleapis.com/youtube/v3/search?` + params.toString());
if (!response.ok) {
throw new Error(`YouTube API error: ${response.status}`);
}
const data = await response.json();
if (currentPage === targetPage) {
lastItems = Array.isArray(data.items) ? data.items : [];
break;
}
// Prepare for next iteration
const next = data.nextPageToken;
if (!next) {
// No more pages; requested page beyond available results
lastItems = [];
break;
}
pageToken = String(next);
currentPage++;
}
const data = await response.json();
const videoIds = (lastItems || [])
.map(item => item?.id?.videoId)
.filter(Boolean);
return (data.items || []).map(item => ({
title: item.snippet.title,
id: item.id.videoId,
url: `https://www.youtube.com/watch?v=${item.id.videoId}`,
thumbnail: item.snippet.thumbnails?.medium?.url || item.snippet.thumbnails?.default?.url,
uploaderName: item.snippet.channelTitle,
type: 'video'
}));
const detailsMap = new Map();
if (videoIds.length > 0) {
const detailsParams = new URLSearchParams({
part: 'contentDetails,statistics',
id: videoIds.join(','),
key: API_KEY
});
const detailsResp = await fetch(`https://www.googleapis.com/youtube/v3/videos?${detailsParams.toString()}`);
if (detailsResp.ok) {
const detailsData = await detailsResp.json();
for (const vid of detailsData?.items || []) {
if (vid?.id) detailsMap.set(vid.id, vid);
}
}
}
return (lastItems || []).map(item => {
const videoId = item?.id?.videoId;
const snippet = item?.snippet || {};
const thumb = snippet.thumbnails?.high?.url
|| snippet.thumbnails?.medium?.url
|| snippet.thumbnails?.default?.url
|| undefined;
const details = videoId ? detailsMap.get(videoId) : null;
const isoDuration = details?.contentDetails?.duration || '';
const duration = parseISODurationToSeconds(isoDuration);
const views = details?.statistics?.viewCount != null ? Number(details.statistics.viewCount) : undefined;
return {
title: snippet.title || '',
id: videoId,
url: videoId ? `https://www.youtube.com/watch?v=${videoId}` : undefined,
thumbnail: thumb,
uploaderName: snippet.channelTitle || undefined,
type: 'video',
duration: duration > 0 ? duration : undefined,
views,
publishedAt: snippet.publishedAt || undefined
};
});
} catch (error) {
console.error('YouTube search error:', error);
return [];
+28 -26
View File
@@ -309,46 +309,48 @@ async function scrapeRumbleList({ q, page = 1, limit = 24, sort = 'viral' }) {
if (thumb && thumb.startsWith('//')) thumb = 'https:' + thumb;
// Essayer plusieurs sélecteurs pour la durée, y compris les attributs data-
let durationText = '';
// Ajout de plus de sélecteurs spécifiques à Rumble
// Ajout de plus de sélecteurs spécifiques à Rumble pour la durée
const durationElement = card.find(
'.video-item--duration, .video-duration, .duration, .video-item__duration, ' +
'[data-duration], .videoDuration, .video-time, .time, ' +
'.video-card__duration, .media__duration, .thumb-time, ' +
'.video-listing-entry__duration, .video-item__duration'
'.video-listing-entry__duration, .video-item__duration, time'
).first();
const durationCandidates = [];
if (durationElement.length) {
// Essayer d'abord les attributs data-
durationText = durationElement.attr('data-duration') ||
durationElement.attr('data-time') ||
// Essayer aussi les attributs style ou autres qui pourraient contenir la durée
durationElement.attr('aria-label') ||
durationElement.attr('title') ||
// Essayer le contenu textuel
durationElement.text().trim();
durationCandidates.push(
durationElement.attr('data-duration'),
durationElement.attr('data-time'),
durationElement.attr('datetime'),
durationElement.attr('aria-label'),
durationElement.attr('title'),
durationElement.text()?.trim()
);
}
// Si on n'a pas trouvé de durée, essayer de la trouver dans le contenu de la card
if (!durationText) {
// Chercher un élément qui ressemble à une durée (mm:ss ou hh:mm:ss)
const timeMatch = card.html().match(/>\s*([0-9]+:[0-9]{2}(?::[0-9]{2})?)\s*</);
if (timeMatch && timeMatch[1]) {
durationText = timeMatch[1];
// Chercher dans le HTML de la carte un motif HH:MM(:SS)
try {
const htmlSnippet = card.html() || '';
const match = />\s*([0-9]+:[0-9]{2}(?::[0-9]{2})?)\s*</.exec(htmlSnippet);
if (match && match[1]) durationCandidates.push(match[1]);
} catch {}
let durationSeconds = 0;
for (const candidate of durationCandidates) {
const parsed = parseDurationToSeconds(candidate);
if (parsed > 0) {
durationSeconds = parsed;
break;
}
}
// Nettoyer le texte de durée avant de le parser
const cleanDurationText = durationText.replace(/[^0-9:]/g, '').trim();
// Extraire les vues
const viewsText =
card.find('.video-item--views, .rumbles-views, .views, .video-item__views, [data-views]').first()
.attr('data-views') ||
card.find('.video-item--views, .rumbles-views, .views, .video-item__views, .video-views').first().text().trim();
// Parser la durée
const duration = parseDurationToSeconds(cleanDurationText);
const views = parseInt((viewsText || '').replace(/[^\d]/g, ''), 10) || 0;
// Important: on renvoie TOUJOURS une URL canonique cohérente
@@ -372,7 +374,7 @@ async function scrapeRumbleList({ q, page = 1, limit = 24, sort = 'viral' }) {
thumbnail: thumb,
uploaderName: '',
views,
duration,
duration: durationSeconds,
uploadedDate: '',
url,
type: 'video'