forked from ORGANISATION-KA-INTERNATIONALE/FEDIVERSE-OKI
6c2df9b1cd
- scripts/fetch-podcast-cover.mjs : pochettes Castopod (flux + épisodes) téléchargées et converties en WebP 512px au prebuild (2,3 Mo → 59 Ko) - scripts/optimize-remote-images.mjs : vignettes PeerTube (640px), avatars (96px) et médias Mastodon (800px) optimisés en WebP + mapping src/lib/server/image-map.json consommé par mapImage() - a11y : liens footer soulignés dans les paragraphes, cibles tactiles ≥24px sur les liens des posts Mastodon - image-map.ts : import JSON statique (le createRequire pointait sur le bundle) - doc : étape prebuild documentée (dépendance ImageMagick) Lighthouse mobile : 97/100/100/100 — LCP 2,4s, TBT 50ms, CLS 0, poids accueil 900 Ko (budgets du playbook respectés)
237 lines
7.1 KiB
TypeScript
237 lines
7.1 KiB
TypeScript
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 ?? '')
|
|
}));
|
|
}
|