79 lines
2.3 KiB
TypeScript
79 lines
2.3 KiB
TypeScript
|
|
import fetch from 'node-fetch';
|
||
|
|
|
||
|
|
interface InvidiousVideoThumbnail {
|
||
|
|
quality: string;
|
||
|
|
url: string;
|
||
|
|
width: number;
|
||
|
|
height: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface InvidiousResult {
|
||
|
|
type: string;
|
||
|
|
title: string;
|
||
|
|
videoId: string;
|
||
|
|
playlistId: string;
|
||
|
|
author: string;
|
||
|
|
videoThumbnails?: InvidiousVideoThumbnail[];
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface SearchResult {
|
||
|
|
type: string;
|
||
|
|
title: string;
|
||
|
|
author: string;
|
||
|
|
mediaUrl: string;
|
||
|
|
thumbnailUrl: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
const USE_INVIDIOUS = process.env.USE_INVIDIOUS || true;
|
||
|
|
const INVIDIOUS_BASE_URL = process.env.INVIDIOUS_BASE_URL || 'http://invidious.nor';
|
||
|
|
const INVIDIOUS_API_ENDPOINT = `${INVIDIOUS_BASE_URL}/api/v1`;
|
||
|
|
|
||
|
|
export const getInvidiousSearchURL = (query: string): string =>
|
||
|
|
`${INVIDIOUS_API_ENDPOINT}/search?q=${encodeURIComponent(query)}`;
|
||
|
|
|
||
|
|
export const getInvidiousThumbnailURL = (url: string): string =>
|
||
|
|
`${INVIDIOUS_BASE_URL}${url}`;
|
||
|
|
|
||
|
|
const preferredThumbnailAPIURL = (thumbnails: InvidiousVideoThumbnail[] | undefined): string => {
|
||
|
|
if (!thumbnails || thumbnails.length === 0) {
|
||
|
|
return '/assets/placeholder.jpg';
|
||
|
|
}
|
||
|
|
|
||
|
|
const mediumThumbnail = thumbnails.find(t => t.quality === 'medium');
|
||
|
|
const thumbnail = mediumThumbnail || thumbnails[0];
|
||
|
|
return `/api/thumbnail?url=${encodeURIComponent(thumbnail.url)}`;
|
||
|
|
};
|
||
|
|
|
||
|
|
const getMediaURL = (result: InvidiousResult): string => {
|
||
|
|
if (result.type === 'video') {
|
||
|
|
return `https://www.youtube.com/watch?v=${result.videoId}`;
|
||
|
|
} else if (result.type === 'playlist') {
|
||
|
|
return `https://www.youtube.com/playlist?list=${result.playlistId}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new Error(`Unknown result type: ${result.type}`);
|
||
|
|
};
|
||
|
|
|
||
|
|
export const searchInvidious = async (query: string): Promise<SearchResult[]> => {
|
||
|
|
try {
|
||
|
|
const response = await fetch(getInvidiousSearchURL(query));
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error(`Invidious HTTP error: ${response.status}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = await response.json() as Array<InvidiousResult>;
|
||
|
|
return data.filter(item => {
|
||
|
|
return item.type === 'video' || item.type === 'playlist';
|
||
|
|
}).map(item => ({
|
||
|
|
type: item.type,
|
||
|
|
title: item.title,
|
||
|
|
author: item.author,
|
||
|
|
mediaUrl: getMediaURL(item),
|
||
|
|
thumbnailUrl: preferredThumbnailAPIURL(item.videoThumbnails)
|
||
|
|
}));
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Failed to search Invidious:', error);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|