import { peertube } from '$lib/config'; import { fetchJson, markdownToHtml, sanitizeHtml } from './http'; import { mapImage } from './image-map'; import type { DownloadOption, SearchEntry, VideoComment, VideoDetails, VideoSummary } from '$lib/types'; const BASE = peertube.url; interface PtAvatar { path?: string; } interface PtVideo { uuid: string; name: string; previewPath?: string; duration: number; views: number; publishedAt: string; aspectRatio?: number; description?: string; tags?: string[]; isLive?: boolean; channel?: { displayName?: string; avatars?: PtAvatar[] }; } interface PtVideoList { data?: PtVideo[]; total?: number; } async function callApi(endpoint: string, params: Record = {}) { const query = new URLSearchParams(); for (const [key, value] of Object.entries(params)) query.set(key, String(value)); const url = `${BASE}/api/v1/${endpoint}${query.size ? `?${query}` : ''}`; return fetchJson(url); } function formatVideos(videos: PtVideo[] = []): VideoSummary[] { return videos.map((video) => ({ id: video.uuid, title: video.name, thumbnail: video.previewPath ? mapImage(`${BASE}${video.previewPath}`) : '/images/logo.png', duration: video.duration, channel: video.channel?.displayName ?? '', channelAvatar: video.channel?.avatars?.[0]?.path ? mapImage(`${BASE}${video.channel.avatars[0].path}`) : '/images/logo.png', views: video.views, date: video.publishedAt, aspectRatio: video.aspectRatio ?? 16 / 9, description: video.description ?? '', tags: video.tags ?? [], isLive: video.isLive ?? false })); } export async function getRecentVideos(count: number = peertube.counts.recent): Promise { const data = await callApi('videos', { sort: '-publishedAt', count, isLocal: true }); return formatVideos(data?.data); } export async function getTrendingVideos(count: number = peertube.counts.trending): Promise { const data = await callApi('videos', { sort: '-trending', count, isLocal: true }); return formatVideos(data?.data); } export async function getVideosByTag(tag: string, count: number): Promise { const data = await callApi('videos', { tagsOneOf: tag, count, isLocal: true }); return formatVideos(data?.data); } export async function getShorts(count: number = peertube.counts.shorts): Promise { const data = await callApi('videos', { sort: '-publishedAt', count: 100, isLocal: true }); const shorts = formatVideos(data?.data).filter( (video) => video.duration < peertube.shortsMaxDuration && video.aspectRatio <= 1 ); return shorts.slice(0, count); } export async function getIndependenceVideos( count: number = peertube.counts.independence ): Promise { return getVideosByTag(peertube.tagIndependence, count); } export async function getVideosByCategory( categoryId: number, count: number = peertube.counts.category ): Promise { const data = await callApi('videos', { categoryOneOf: categoryId, sort: '-publishedAt', count, isLocal: true }); return formatVideos(data?.data); } export async function getLiveStream(): Promise { const data = await callApi(`video-channels/${peertube.channelHandle}/videos`, { count: 1, isLocal: true, isLive: true, sort: '-publishedAt' }); const live = formatVideos(data?.data).find((video) => video.isLive); return live ?? null; } export async function getAllVideos(maxCount: number = peertube.counts.searchPool): Promise { const videos: VideoSummary[] = []; let start = 0; const pageSize = 100; while (videos.length < maxCount) { const data = await callApi('videos', { sort: '-publishedAt', count: pageSize, start, isLocal: true }); const page = formatVideos(data?.data); videos.push(...page); if (page.length < pageSize) break; start += pageSize; } return videos.slice(0, maxCount); } export async function getSearchIndex(): Promise { const videos = await getAllVideos(); return videos.map((video) => ({ id: video.id, title: video.title, description: video.description, tags: video.tags, thumbnail: video.thumbnail, duration: video.duration, date: video.date, channel: video.channel })); } interface PtVideoDetails extends PtVideo { category?: { id?: number; label?: string }; licence?: { id?: number; label?: string }; language?: { id?: string; label?: string }; files?: { resolution?: { id?: number; label?: string }; size?: number; fileDownloadUrl?: string }[]; streamingPlaylists?: { playlistUrl?: string; files?: { resolution?: { id?: number }; size?: number }[] }[]; } const CREATIVE_COMMONS: Record = { 1: { label: 'CC-BY', url: 'https://creativecommons.org/licenses/by/4.0/' }, 2: { label: 'CC-BY-SA', url: 'https://creativecommons.org/licenses/by-sa/4.0/' }, 3: { label: 'CC-BY-ND', url: 'https://creativecommons.org/licenses/by-nd/4.0/' }, 4: { label: 'CC-BY-NC', url: 'https://creativecommons.org/licenses/by-nc/4.0/' }, 5: { label: 'CC-BY-NC-SA', url: 'https://creativecommons.org/licenses/by-nc-sa/4.0/' }, 6: { label: 'CC-BY-NC-ND', url: 'https://creativecommons.org/licenses/by-nc-nd/4.0/' }, 7: { label: 'CC0', url: 'https://creativecommons.org/publicdomain/zero/1.0/' } }; export async function getVideoById(uuid: string): Promise { const video = await fetchJson(`${BASE}/api/v1/videos/${uuid}`); if (!video?.uuid) return null; const [summary] = formatVideos([video]); const downloads: DownloadOption[] = []; for (const file of video.files ?? []) { if (!file.fileDownloadUrl) continue; downloads.push({ label: file.resolution?.label ?? `${file.resolution?.id ?? '?'}p`, url: file.fileDownloadUrl, size: file.size ?? 0, resolution: file.resolution?.id ?? 0, type: 'file' }); } for (const playlist of video.streamingPlaylists ?? []) { if (!playlist.playlistUrl) continue; const size = (playlist.files ?? []).reduce((sum, f) => sum + (f.size ?? 0), 0); downloads.push({ label: 'HLS', url: playlist.playlistUrl, size, resolution: 0, type: 'hls' }); } const licence = video.licence?.id ? CREATIVE_COMMONS[video.licence.id] : undefined; return { ...summary, descriptionHtml: markdownToHtml(video.description ?? ''), category: video.category?.id ?? null, licence: licence ?? null, embedUrl: `${BASE}/videos/embed/${video.uuid}`, downloads, language: video.language?.label ?? null }; } interface PtComment { id: string | number; text?: string; createdAt?: string; url?: string; account?: { displayName?: string; url?: string; avatars?: PtAvatar[] }; } export async function getVideoComments(uuid: string): Promise { const data = await fetchJson<{ data?: PtComment[] }>( `${BASE}/api/v1/videos/${uuid}/comment-threads?sort=-createdAt` ); return (data?.data ?? []).map((comment) => ({ id: String(comment.id), author: comment.account?.displayName ?? '', authorAvatar: comment.account?.avatars?.[0]?.path ? `${BASE}${comment.account.avatars[0].path}` : null, authorUrl: comment.account?.url ?? null, date: comment.createdAt ?? '', text: sanitizeHtml(comment.text ?? '') })); }