chore: update Angular cache and TypeScript build info files
This commit is contained in:
@@ -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