forked from ORGANISATION-KA-INTERNATIONALE/FEDIVERSE-OKI
Réécriture SvelteKit statique (Svelte 5 runes + TS, adapter-static)
- Données PeerTube (GADE), Castopod (KUTE) et Mastodon (BOKANTE) bakées au build via +page.server.ts (throttle + retry 429 + déduplication) - Charte OKI complète : tokens, thème sombre par défaut (clair opt-in, anti-FOUC), Archivo/Inter self-hébergées, flag-bar, sprite SVG (zéro emoji en interface, zéro Font Awesome/CDN) - i18n FR/EN par routage [[locale]], bundles JSON, hreflang, %lang% serveur - Pages : accueil, /video/[uuid] (embed, téléchargements, partage, commentaires, JSON-LD VideoObject), /categories/[id], /recherche (index JSON baké, recherche client), /direct (live + annonce multi-fuseaux), /dons, /mentions-legales, /offline + 404 kréyòl - Motion gwoka : KineticText (scrub view()), ScrollProgressBar, FlagChip, reveal syncopé, View Transitions, gate prefers-reduced-motion unique - PWA : manifest + Workbox generateSW (fallback /offline/), registerSW statique - Sécurité : CSP par page en <meta> (SHA-256 des inline via scripts/postbuild-csp.mjs) + headers globaux (_headers Cloudflare et .htaccess o2switch), redirections des anciennes URLs PHP - Doc : docs/DEPLOIEMENT-SVELTEKIT.md
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import { castopod } from '$lib/config';
|
||||
import { fetchText } from './http';
|
||||
import type { Episode } from '$lib/types';
|
||||
|
||||
function tag(xml: string, name: string): string {
|
||||
const cdata = new RegExp(`<${name}[^>]*><!\\[CDATA\\[([\\s\\S]*?)\\]\\]></${name}>`, 'i');
|
||||
const plain = new RegExp(`<${name}[^>]*>([\\s\\S]*?)</${name}>`, 'i');
|
||||
const match = xml.match(cdata) ?? xml.match(plain);
|
||||
return match?.[1]?.trim() ?? '';
|
||||
}
|
||||
|
||||
function attr(xml: string, tagName: string, attrName: string): string {
|
||||
const tagMatch = xml.match(new RegExp(`<${tagName}\\b[^>]*>`, 'i'));
|
||||
if (!tagMatch) return '';
|
||||
const attrMatch = tagMatch[0].match(new RegExp(`${attrName}="([^"]*)"`, 'i'));
|
||||
return attrMatch?.[1] ?? '';
|
||||
}
|
||||
|
||||
function stripTags(html: string): string {
|
||||
return html
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/�?39;|'/g, "'")
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function parseDuration(raw: string): number {
|
||||
if (!raw) return 0;
|
||||
const parts = raw.split(':').map(Number);
|
||||
if (parts.some(Number.isNaN)) return 0;
|
||||
if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
||||
if (parts.length === 2) return parts[0] * 60 + parts[1];
|
||||
return parts[0] ?? 0;
|
||||
}
|
||||
|
||||
async function getEpisodesForSlug(slug: string): Promise<Episode[]> {
|
||||
const feedUrl = `${castopod.url}/@${slug}/feed`;
|
||||
const xml = await fetchText(feedUrl);
|
||||
if (!xml) return [];
|
||||
|
||||
const channelImage =
|
||||
tag(xml.split('<item')[0] ?? '', 'url') || attr(xml.split('<item')[0] ?? '', 'itunes:image', 'href');
|
||||
const items = xml.split(/<item[\s>]/i).slice(1);
|
||||
|
||||
return items.map((item, index) => {
|
||||
const body = item.split('</item>')[0] ?? item;
|
||||
const title = tag(body, 'title');
|
||||
const link = tag(body, 'link');
|
||||
const guid = tag(body, 'guid') || link || `${slug}-${index}`;
|
||||
const description = stripTags(tag(body, 'description') || tag(body, 'itunes:summary'));
|
||||
return {
|
||||
id: guid,
|
||||
title,
|
||||
description,
|
||||
audioUrl: attr(body, 'enclosure', 'url'),
|
||||
audioType: attr(body, 'enclosure', 'type') || 'audio/mpeg',
|
||||
duration: parseDuration(tag(body, 'itunes:duration')),
|
||||
date: tag(body, 'pubDate'),
|
||||
image: attr(body, 'itunes:image', 'href') || channelImage || null,
|
||||
link,
|
||||
podcast: slug
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCastopodEpisodes(count = castopod.episodesCount): Promise<Episode[]> {
|
||||
const all: Episode[] = [];
|
||||
for (const slug of castopod.podcastSlugs) {
|
||||
const episodes = await getEpisodesForSlug(slug);
|
||||
all.push(...episodes);
|
||||
if (castopod.podcastSlugs.length > 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
}
|
||||
}
|
||||
return all
|
||||
.filter((episode) => episode.audioUrl)
|
||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||
.slice(0, count);
|
||||
}
|
||||
Reference in New Issue
Block a user