reorg: separate frontend and backend
This commit is contained in:
102
backend/src/InvidiousAPI.ts
Normal file
102
backend/src/InvidiousAPI.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
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;
|
||||
}
|
||||
|
||||
export interface ThumbnailResponse {
|
||||
data: NodeJS.ReadableStream;
|
||||
contentType: 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;
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchThumbnail = async (thumbnailUrl: string): Promise<ThumbnailResponse> => {
|
||||
let path = thumbnailUrl;
|
||||
if (thumbnailUrl.startsWith('http://') || thumbnailUrl.startsWith('https://')) {
|
||||
const url = new URL(thumbnailUrl);
|
||||
path = url.pathname + url.search;
|
||||
}
|
||||
path = path.replace(/^\/+/, ''); // Strip leading slash
|
||||
|
||||
const response = await fetch(getInvidiousThumbnailURL(path));
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return {
|
||||
data: response.body,
|
||||
contentType: response.headers.get('content-type') || 'image/jpeg'
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user