Files
annu-kute-ced/src/lib/server/peertube.ts
T

237 lines
7.1 KiB
TypeScript
Raw Normal View History

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<string, string | number | boolean> = {}) {
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<PtVideoList>(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<VideoSummary[]> {
const data = await callApi('videos', { sort: '-publishedAt', count, isLocal: true });
return formatVideos(data?.data);
}
export async function getTrendingVideos(count: number = peertube.counts.trending): Promise<VideoSummary[]> {
const data = await callApi('videos', { sort: '-trending', count, isLocal: true });
return formatVideos(data?.data);
}
export async function getVideosByTag(tag: string, count: number): Promise<VideoSummary[]> {
const data = await callApi('videos', { tagsOneOf: tag, count, isLocal: true });
return formatVideos(data?.data);
}
export async function getShorts(count: number = peertube.counts.shorts): Promise<VideoSummary[]> {
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<VideoSummary[]> {
return getVideosByTag(peertube.tagIndependence, count);
}
export async function getVideosByCategory(
categoryId: number,
count: number = peertube.counts.category
): Promise<VideoSummary[]> {
const data = await callApi('videos', {
categoryOneOf: categoryId,
sort: '-publishedAt',
count,
isLocal: true
});
return formatVideos(data?.data);
}
export async function getLiveStream(): Promise<VideoSummary | null> {
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<VideoSummary[]> {
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<SearchEntry[]> {
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<number, { label: string; url: string }> = {
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<VideoDetails | null> {
const video = await fetchJson<PtVideoDetails>(`${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<VideoComment[]> {
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 ?? '')
}));
}