chore: update Angular cache and TypeScript build info files
This commit is contained in:
@@ -12,6 +12,7 @@ import ffmpegPath from 'ffmpeg-static';
|
||||
import * as cheerio from 'cheerio';
|
||||
import axios from 'axios';
|
||||
import rumbleRouter from './rumble.mjs';
|
||||
import { providerRegistry, validateProviders } from './providers/registry.mjs';
|
||||
import {
|
||||
getUserByUsername,
|
||||
getUserById,
|
||||
@@ -1386,6 +1387,45 @@ app.all('/api/odysee/*', (req, res) => forwardJson(req, res, 'https://api.na-bac
|
||||
app.all('/api/twitch-api/*', (req, res) => forwardJson(req, res, 'https://api.twitch.tv'));
|
||||
app.all('/api/twitch-auth/*', (req, res) => forwardJson(req, res, 'https://id.twitch.tv'));
|
||||
|
||||
// -------------------- Unified search endpoint (GET) --------------------
|
||||
app.get('/api/search', async (req, res) => {
|
||||
try {
|
||||
const { q, providers } = req.query;
|
||||
if (!q || typeof q !== 'string' || q.trim().length === 0) {
|
||||
return res.status(400).json({ error: 'missing_query' });
|
||||
}
|
||||
// Validate and normalize providers list (default to all supported when none/invalid)
|
||||
const requested = typeof providers === 'string' ? String(providers) : '';
|
||||
const validProviders = validateProviders(requested);
|
||||
|
||||
// Execute search for each provider in parallel
|
||||
const results = await Promise.allSettled(
|
||||
validProviders.map((providerId) => {
|
||||
const mod = providerRegistry[/** @type {any} */(providerId)];
|
||||
if (!mod || typeof mod.search !== 'function') return Promise.resolve([]);
|
||||
// Basic options: limit per provider; could be extended with page, etc.
|
||||
return Promise.resolve().then(() => mod.search(q, { limit: 10 }));
|
||||
})
|
||||
);
|
||||
|
||||
// Group results by provider id
|
||||
const groups = /** @type {Record<string, any[]>} */ ({});
|
||||
results.forEach((result, index) => {
|
||||
const providerId = validProviders[index];
|
||||
if (result.status === 'fulfilled') {
|
||||
groups[providerId] = Array.isArray(result.value) ? result.value : [];
|
||||
} else {
|
||||
console.warn(`Search failed for provider ${providerId}:`, result.reason);
|
||||
groups[providerId] = [];
|
||||
}
|
||||
});
|
||||
|
||||
return res.json({ q, providers: validProviders, groups });
|
||||
} catch (e) {
|
||||
return res.status(500).json({ error: 'search_failed', details: String(e?.message || e) });
|
||||
}
|
||||
});
|
||||
|
||||
// -------------------- Static Frontend (Angular build) --------------------
|
||||
const distRoot = path.join(process.cwd(), 'dist');
|
||||
const distBrowser = path.join(distRoot, 'browser');
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Minimal Dailymotion provider handler
|
||||
*/
|
||||
const handler = {
|
||||
id: 'dm',
|
||||
label: 'Dailymotion',
|
||||
/**
|
||||
* @param {string} q
|
||||
* @param {{ limit: number, page?: number }} opts
|
||||
* @returns {Promise<Array<any>>}
|
||||
*/
|
||||
async search(q, opts) {
|
||||
const { limit = 10 } = opts;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://api.dailymotion.com/videos?` +
|
||||
new URLSearchParams({
|
||||
search: q,
|
||||
limit: Math.min(limit, 100).toString(),
|
||||
sort: 'relevance'
|
||||
})
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Dailymotion API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return (data.list || []).map(item => ({
|
||||
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,
|
||||
type: 'video'
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Dailymotion search error:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
};
|
||||
export default handler;
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Minimal Odysee provider handler
|
||||
*/
|
||||
const handler = {
|
||||
id: 'od',
|
||||
label: 'Odysee',
|
||||
/**
|
||||
* @param {string} q
|
||||
* @param {{ limit: number, page?: number }} opts
|
||||
* @returns {Promise<Array<any>>}
|
||||
*/
|
||||
async search(q, opts) {
|
||||
const { limit = 10 } = opts;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://lighthouse.odysee.tv/content/search?` +
|
||||
new URLSearchParams({
|
||||
query: q,
|
||||
size: Math.min(limit, 50).toString(),
|
||||
page: '1'
|
||||
})
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Odysee API error: ${response.status}`);
|
||||
}
|
||||
|
||||
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'
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Odysee search error:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
};
|
||||
export default handler;
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Minimal PeerTube provider handler
|
||||
*/
|
||||
const handler = {
|
||||
id: 'pt',
|
||||
label: 'PeerTube',
|
||||
/**
|
||||
* @param {string} q
|
||||
* @param {{ limit: number, page?: number }} opts
|
||||
* @returns {Promise<Array<any>>}
|
||||
*/
|
||||
async search(q, opts) {
|
||||
const { limit = 10 } = 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()
|
||||
})
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`PeerTube API error: ${response.status}`);
|
||||
}
|
||||
|
||||
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'
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('PeerTube search error:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
};
|
||||
export default handler;
|
||||
@@ -0,0 +1,39 @@
|
||||
// Server-side provider registry for search fan-out
|
||||
// Each provider module should export an object with a `search(q, opts)` function returning Promise<Suggestion[]>
|
||||
|
||||
/**
|
||||
* @typedef {Object} Suggestion
|
||||
* @property {string} title
|
||||
* @property {string} id
|
||||
* @property {number=} duration
|
||||
* @property {boolean=} isShort
|
||||
* @property {string=} url
|
||||
* @property {string=} thumbnail
|
||||
* @property {string=} uploaderName
|
||||
* @property {string=} type
|
||||
*/
|
||||
|
||||
/** @typedef {'yt'|'dm'|'tw'|'pt'|'od'|'ru'} ProviderId */
|
||||
|
||||
/** @type {Record<ProviderId, { id: ProviderId, label: string, search: (q: string, opts: { limit: number, page?: number }) => Promise<Suggestion[]> }>} */
|
||||
export const providerRegistry = {
|
||||
/** @type {any} */ yt: (await import('./youtube.mjs')).default,
|
||||
/** @type {any} */ dm: (await import('./dailymotion.mjs')).default,
|
||||
/** @type {any} */ tw: (await import('./twitch.mjs')).default,
|
||||
/** @type {any} */ pt: (await import('./peertube.mjs')).default,
|
||||
/** @type {any} */ od: (await import('./odysee.mjs')).default,
|
||||
/** @type {any} */ ru: (await import('./rumble.mjs')).default,
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate and normalize a comma separated providers list
|
||||
* @param {string} input
|
||||
* @returns {ProviderId[]}
|
||||
*/
|
||||
export function validateProviders(input) {
|
||||
const raw = String(input || '').split(',').map(s => s.trim()).filter(Boolean);
|
||||
const allowed = /** @type {ProviderId[]} */(['yt','dm','tw','pt','od','ru']);
|
||||
const filtered = raw.filter(p => allowed.includes(/** @type {any} */(p)));
|
||||
if (filtered.length === 0) return allowed;
|
||||
return /** @type {ProviderId[]} */(filtered);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Minimal Rumble provider handler
|
||||
*/
|
||||
const handler = {
|
||||
id: 'ru',
|
||||
label: 'Rumble',
|
||||
/**
|
||||
* @param {string} q
|
||||
* @param {{ limit: number, page?: number }} opts
|
||||
* @returns {Promise<Array<any>>}
|
||||
*/
|
||||
async search(q, opts) {
|
||||
const { limit = 10 } = opts;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://rumble.com/api/search/videos?` +
|
||||
new URLSearchParams({
|
||||
q: q,
|
||||
size: Math.min(limit, 50).toString()
|
||||
})
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Rumble API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
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'
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Rumble search error:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
};
|
||||
export default handler;
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Minimal Twitch provider handler
|
||||
*/
|
||||
const handler = {
|
||||
id: 'tw',
|
||||
label: 'Twitch',
|
||||
/**
|
||||
* @param {string} q
|
||||
* @param {{ limit: number, page?: number }} opts
|
||||
* @returns {Promise<Array<any>>}
|
||||
*/
|
||||
async search(q, opts) {
|
||||
const { limit = 10 } = opts;
|
||||
try {
|
||||
// Use Twitch Kraken API (older but public)
|
||||
const response = await fetch(
|
||||
`https://api.twitch.tv/kraken/search/streams?` +
|
||||
new URLSearchParams({
|
||||
query: q,
|
||||
limit: Math.min(limit, 25).toString()
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
'Accept': 'application/vnd.twitchtv.v5+json',
|
||||
'Client-ID': process.env.TWITCH_CLIENT_ID || ''
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Twitch API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return (data.streams || []).map(item => ({
|
||||
title: item.channel.status || item.channel.display_name,
|
||||
id: item.channel.name, // Channel name as ID
|
||||
url: `https://www.twitch.tv/${item.channel.name}`,
|
||||
thumbnail: item.preview?.medium || item.channel.logo,
|
||||
uploaderName: item.channel.display_name,
|
||||
type: 'stream',
|
||||
isLive: true
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Twitch search error:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
};
|
||||
export default handler;
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* @typedef {Object} Suggestion
|
||||
* @property {string} title
|
||||
* @property {string} id
|
||||
* @property {number=} duration
|
||||
* @property {boolean=} isShort
|
||||
* @property {string=} url
|
||||
* @property {string=} thumbnail
|
||||
* @property {string=} uploaderName
|
||||
* @property {string=} type
|
||||
*/
|
||||
|
||||
/** @type {{ id: 'yt', label: string, search: (q: string, opts: { limit: number, page?: number }) => Promise<Suggestion[]> }} */
|
||||
const handler = {
|
||||
id: 'yt',
|
||||
label: 'YouTube',
|
||||
async search(q, opts) {
|
||||
const { limit = 10 } = opts;
|
||||
try {
|
||||
const API_KEY = process.env.YOUTUBE_API_KEY;
|
||||
if (!API_KEY) {
|
||||
throw new Error('YOUTUBE_API_KEY not configured');
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`https://www.googleapis.com/youtube/v3/search?` +
|
||||
new URLSearchParams({
|
||||
part: 'snippet',
|
||||
q: q,
|
||||
type: 'video',
|
||||
maxResults: Math.min(limit, 50).toString(),
|
||||
key: API_KEY,
|
||||
order: 'relevance'
|
||||
})
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`YouTube API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
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'
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('YouTube search error:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default handler;
|
||||
Reference in New Issue
Block a user