diff options
Diffstat (limited to 'src/lib/api/youtube.ts')
| -rw-r--r-- | src/lib/api/youtube.ts | 253 |
1 files changed, 253 insertions, 0 deletions
diff --git a/src/lib/api/youtube.ts b/src/lib/api/youtube.ts new file mode 100644 index 0000000..8ade3c5 --- /dev/null +++ b/src/lib/api/youtube.ts @@ -0,0 +1,253 @@ +export interface VideoThumbnail { + url: string; + width?: number; + height?: number; +} + +export interface VideoInfo { + videoId: string; + title: string; + author: string; + authorId: string; + videoThumbnails: VideoThumbnail[]; + description: string; + viewCount: number; + publishedText: string; + lengthSeconds: number; +} + +export interface VideoFormat { + formatId: string; + url: string; + ext: string; + resolution?: string; + height?: number; + width?: number; + qualityLabel?: string; +} + +export interface VideoDetails extends VideoInfo { + likeCount?: number; + formats: VideoFormat[]; +} + +export interface ChannelInfo { + author: string; + authorId: string; + authorThumbnails: VideoThumbnail[]; + subCount: number; + description: string; + videos: VideoInfo[]; +} + +interface YtdlpSearchResult { + id: string; + title: string; + channel: string; + channel_id: string; + thumbnail: string; + thumbnails: { url: string; width?: number; height?: number }[]; + duration: number; + view_count: number; + upload_date: string; +} + +interface YtdlpVideo { + id: string; + title: string; + description: string; + channel: string; + channel_id: string; + thumbnail: string; + thumbnails: { url: string; width?: number; height?: number }[]; + duration: number; + view_count: number; + upload_date: string; + like_count?: number; + formats: { + format_id: string; + url: string; + ext: string; + resolution?: string; + height?: number; + width?: number; + format_note?: string; + }[]; +} + +interface YtdlpChannel { + id: string; + channel: string; + channel_id: string; + description: string; + channel_follower_count: number; + thumbnails: { url: string; width?: number; height?: number }[]; + entries?: YtdlpSearchResult[]; +} + +function formatUploadDate(dateStr: string): string { + if (!dateStr || dateStr.length !== 8) return ''; + const year = dateStr.slice(0, 4); + const month = dateStr.slice(4, 6); + const day = dateStr.slice(6, 8); + const date = new Date(`${year}-${month}-${day}`); + const now = new Date(); + const diff = now.getTime() - date.getTime(); + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + + if (days === 0) return 'Today'; + if (days === 1) return 'Yesterday'; + if (days < 7) return `${days} days ago`; + if (days < 30) return `${Math.floor(days / 7)} weeks ago`; + if (days < 365) return `${Math.floor(days / 30)} months ago`; + return `${Math.floor(days / 365)} years ago`; +} + +function transformSearchResult(item: YtdlpSearchResult): VideoInfo { + return { + videoId: item.id, + title: item.title, + author: item.channel, + authorId: item.channel_id, + videoThumbnails: item.thumbnails?.length + ? item.thumbnails + : [{ url: item.thumbnail }], + description: '', + viewCount: item.view_count || 0, + publishedText: formatUploadDate(item.upload_date), + lengthSeconds: item.duration || 0 + }; +} + +function transformVideo(item: YtdlpVideo): VideoDetails { + return { + videoId: item.id, + title: item.title, + author: item.channel, + authorId: item.channel_id, + videoThumbnails: item.thumbnails?.length + ? item.thumbnails + : [{ url: item.thumbnail }], + description: item.description || '', + viewCount: item.view_count || 0, + publishedText: formatUploadDate(item.upload_date), + lengthSeconds: item.duration || 0, + likeCount: item.like_count, + formats: item.formats.map(f => ({ + formatId: f.format_id, + url: f.url, + ext: f.ext, + resolution: f.resolution, + height: f.height, + width: f.width, + qualityLabel: f.format_note || (f.height ? `${f.height}p` : undefined) + })) + }; +} + +function transformChannel(item: YtdlpChannel): ChannelInfo { + return { + author: item.channel, + authorId: item.channel_id, + authorThumbnails: item.thumbnails || [], + subCount: item.channel_follower_count || 0, + description: item.description || '', + videos: (item.entries || []).map(transformSearchResult) + }; +} + +export async function search(query: string): Promise<VideoInfo[]> { + const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`); + if (!response.ok) { + throw new Error('Search failed'); + } + const results: YtdlpSearchResult[] = await response.json(); + return results.map(transformSearchResult); +} + +export async function getVideo(videoId: string): Promise<VideoDetails> { + const response = await fetch(`/api/video/${videoId}`); + if (!response.ok) { + throw new Error('Failed to fetch video'); + } + const video: YtdlpVideo = await response.json(); + return transformVideo(video); +} + +export async function getChannel(channelId: string): Promise<ChannelInfo> { + const response = await fetch(`/api/channel/${encodeURIComponent(channelId)}`); + if (!response.ok) { + throw new Error('Failed to fetch channel'); + } + const channel: YtdlpChannel = await response.json(); + return transformChannel(channel); +} + +export async function getTrending(): Promise<VideoInfo[]> { + const response = await fetch('/api/trending'); + if (!response.ok) { + throw new Error('Failed to fetch trending'); + } + const results: YtdlpSearchResult[] = await response.json(); + return results.map(transformSearchResult); +} + +export async function getRelatedVideos(videoId: string): Promise<VideoInfo[]> { + const response = await fetch(`/api/related/${videoId}`); + if (!response.ok) { + throw new Error('Failed to fetch related videos'); + } + const results: YtdlpSearchResult[] = await response.json(); + return results.map(transformSearchResult); +} + +export interface ImportedPlaylist { + id: string; + title: string; + channel: string; + channelId: string; + videos: VideoInfo[]; +} + +export async function importPlaylist(playlistUrl: string): Promise<ImportedPlaylist> { + const response = await fetch(`/api/playlist?url=${encodeURIComponent(playlistUrl)}`); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to import playlist'); + } + const data = await response.json(); + return { + id: data.id, + title: data.title, + channel: data.channel, + channelId: data.channel_id, + videos: data.entries.map(transformSearchResult) + }; +} + +export function formatDuration(seconds: number): string { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = seconds % 60; + + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + } + return `${minutes}:${secs.toString().padStart(2, '0')}`; +} + +export function formatViews(views: number): string { + if (views >= 1000000) { + return `${(views / 1000000).toFixed(1)}M views`; + } + if (views >= 1000) { + return `${(views / 1000).toFixed(1)}K views`; + } + return `${views} views`; +} + +export function getBestThumbnail(thumbnails: VideoThumbnail[]): string { + if (!thumbnails || thumbnails.length === 0) return ''; + const sorted = [...thumbnails].sort((a, b) => (b.width || 0) - (a.width || 0)); + return sorted[0]?.url || ''; +} |
