You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

150 lines
4.5 KiB

import axios from 'axios';
import { env } from '../config/env.js';
const YOUTUBE_API_BASE = 'https://www.googleapis.com/youtube/v3';
export function parseChannelInput(input: string): { type: 'id' | 'handle' | 'username', value: string } {
input = input.trim();
// Direct channel ID (UC...)
if (/^UC[\w-]{21}[AQgw]$/.test(input)) {
return { type: 'id', value: input };
}
// @handle format
if (input.startsWith('@')) {
return { type: 'handle', value: input.substring(1) };
}
// Full YouTube URLs
const urlPatterns = [
{ regex: /youtube\.com\/channel\/(UC[\w-]{21}[AQgw])/, type: 'id' as const },
{ regex: /youtube\.com\/@([\w-]+)/, type: 'handle' as const },
{ regex: /youtube\.com\/c\/([\w-]+)/, type: 'username' as const },
{ regex: /youtube\.com\/user\/([\w-]+)/, type: 'username' as const }
];
for (const pattern of urlPatterns) {
const match = input.match(pattern.regex);
if (match) {
return { type: pattern.type, value: match[1] };
}
}
throw new Error('Invalid YouTube channel format. Use channel ID, @handle, or valid YouTube URL');
}
export async function fetchChannelInfo(input: string) {
const parsed = parseChannelInput(input);
let params: any = {
part: 'snippet,statistics,contentDetails',
key: env.youtubeApiKey
};
// Use appropriate parameter based on input type
if (parsed.type === 'id') {
params.id = parsed.value;
} else if (parsed.type === 'handle') {
params.forHandle = parsed.value;
} else {
params.forUsername = parsed.value;
}
try {
const response = await axios.get(`${YOUTUBE_API_BASE}/channels`, { params });
if (!response.data.items?.length) {
throw new Error('Channel not found on YouTube');
}
const channel = response.data.items[0];
return {
id: channel.id,
name: channel.snippet.title,
customUrl: channel.snippet.customUrl || null,
thumbnailUrl: channel.snippet.thumbnails.high.url,
description: channel.snippet.description,
subscriberCount: parseInt(channel.statistics.subscriberCount || '0'),
videoCount: parseInt(channel.statistics.videoCount || '0'),
uploadsPlaylistId: channel.contentDetails.relatedPlaylists.uploads
};
} catch (error: any) {
if (error.response?.status === 403) {
throw new Error('YouTube API quota exceeded. Please try again later.');
}
if (error.response?.status === 400) {
throw new Error('Invalid channel identifier');
}
throw error;
}
}
export async function fetchChannelVideos(
uploadsPlaylistId: string,
maxResults: number = 50
) {
try {
// Step 1: Get video IDs from playlist
const playlistResponse = await axios.get(`${YOUTUBE_API_BASE}/playlistItems`, {
params: {
part: 'contentDetails',
playlistId: uploadsPlaylistId,
maxResults,
key: env.youtubeApiKey
}
});
if (!playlistResponse.data.items?.length) {
return [];
}
const videoIds = playlistResponse.data.items
.map((item: any) => item.contentDetails.videoId)
.join(',');
// Step 2: Get video details
const videosResponse = await axios.get(`${YOUTUBE_API_BASE}/videos`, {
params: {
part: 'snippet,statistics,contentDetails',
id: videoIds,
key: env.youtubeApiKey
}
});
return videosResponse.data.items.map((video: any) => ({
id: video.id,
title: video.snippet.title,
description: video.snippet.description,
thumbnailUrl: video.snippet.thumbnails.maxresdefault?.url ||
video.snippet.thumbnails.high.url,
publishedAt: video.snippet.publishedAt,
viewCount: parseInt(video.statistics.viewCount || '0'),
likeCount: parseInt(video.statistics.likeCount || '0'),
duration: video.contentDetails.duration
}));
} catch (error: any) {
if (error.response?.status === 403) {
throw new Error('YouTube API quota exceeded');
}
throw error;
}
}
// Helper to format ISO 8601 duration to readable format
export function formatDuration(isoDuration: string): string {
const match = isoDuration.match(/PT(\d+H)?(\d+M)?(\d+S)?/);
if (!match) return '0:00';
const hours = (match[1] || '').replace('H', '');
const minutes = (match[2] || '').replace('M', '');
const seconds = (match[3] || '0').replace('S', '');
if (hours) {
return `${hours}:${minutes.padStart(2, '0')}:${seconds.padStart(2, '0')}`;
}
return `${minutes || '0'}:${seconds.padStart(2, '0')}`;
}