chore: update Angular cache and TypeScript build info files

This commit is contained in:
2025-09-22 09:34:22 -04:00
parent 08fd0682a4
commit 4eb339eb22
34 changed files with 1511 additions and 154 deletions
+40
View File
@@ -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');
+44
View File
@@ -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;
+44
View File
@@ -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;
+44
View File
@@ -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;
+39
View File
@@ -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);
}
+43
View File
@@ -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;
+51
View File
@@ -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;
+58
View File
@@ -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;