165 lines
5.9 KiB
TypeScript
165 lines
5.9 KiB
TypeScript
import { SearchResult, VideoLink, MediaType } from '../../src/types/source.js';
|
|
|
|
/**
|
|
* Parse le HTML AJAX de résultats de recherche ou pages catégories FS24.
|
|
* Supporte les blocs `.search-item` et `.short`
|
|
*/
|
|
export function parseListingHTML(html: string, mediaType: MediaType): SearchResult[] {
|
|
const results: SearchResult[] = [];
|
|
|
|
// 1. Matches pour les blocs de recherche AJAX (.search-item)
|
|
const searchRegex = /<div class=['"]search-item['"][^>]*onclick="location\.href='([^']+)'"[^>]*>([\s\S]*?)(?=<div class=['"]search-item['"]|$)/g;
|
|
let match: RegExpExecArray | null;
|
|
|
|
while ((match = searchRegex.exec(html)) !== null) {
|
|
const hrefPath = match[1]!;
|
|
const block = match[2]!;
|
|
|
|
const imgMatch = block.match(/<img\s[^>]*src=['"]([^'"]+)['"]/);
|
|
const image = imgMatch ? imgMatch[1]! : null;
|
|
|
|
const titleMatch = block.match(/<div class=['"]search-title['"]>([^<]+)<\/div>/);
|
|
if (!titleMatch) continue;
|
|
|
|
let titleRaw = titleMatch[1]!.trim();
|
|
let year: string | null = null;
|
|
const yearMatch = titleRaw.match(/\((\d{4})\)/);
|
|
if (yearMatch) {
|
|
year = yearMatch[1]!;
|
|
titleRaw = titleRaw.replace(/\s*\(\d{4}\)\s*/, '').trim();
|
|
}
|
|
|
|
if (titleRaw && hrefPath) {
|
|
results.push({ title: titleRaw, year, image, hrefPath, type: mediaType, source: 'fs24' });
|
|
}
|
|
}
|
|
|
|
// 2. Matches pour les pages régulières DLE (.short)
|
|
const shortRegex = /<div class=['"]short['"]>([\s\S]*?)<\/div>\s*<!-- \/short -->|<div class=['"]short['"]>([\s\S]*?)(?=<div class=['"]short['"]|$)/g;
|
|
while ((match = shortRegex.exec(html)) !== null) {
|
|
const block = match[1] || match[2];
|
|
if (!block) continue;
|
|
|
|
// Extract poster
|
|
const imgMatch = block.match(/<img\s[^>]*src=['"]([^'"]+)['"]/);
|
|
const image = imgMatch ? imgMatch[1]! : null;
|
|
|
|
// Extract title
|
|
const titleMatch = block.match(/<div class=['"]short-title['"]>([^<]+)<\/div>/);
|
|
if (!titleMatch) continue;
|
|
let titleRaw = titleMatch[1]!.trim();
|
|
|
|
// Extract link
|
|
const linkMatch = block.match(/<a class=['"]short-poster[^>]*href=['"]([^'"]+)['"]/);
|
|
let hrefPath = linkMatch ? linkMatch[1]! : null;
|
|
if (!hrefPath) continue;
|
|
|
|
// Remove domain if the link is absolute to keep paths source-agnostic
|
|
if (hrefPath.startsWith('http')) {
|
|
try {
|
|
const u = new URL(hrefPath);
|
|
hrefPath = u.pathname + u.search;
|
|
} catch { /* ignore */ }
|
|
}
|
|
|
|
let year: string | null = null;
|
|
const yearMatch = titleRaw.match(/\((\d{4})\)/);
|
|
if (yearMatch) {
|
|
year = yearMatch[1]!;
|
|
titleRaw = titleRaw.replace(/\s*\(\d{4}\)\s*/, '').trim();
|
|
}
|
|
|
|
if (titleRaw && hrefPath) {
|
|
results.push({ title: titleRaw, year, image, hrefPath, type: mediaType, source: 'fs24' });
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Extrait le news_id depuis la page HTML (attribut data-news-id du bloc commu-releases-block).
|
|
*/
|
|
export function extractNewsId(html: string): string | null {
|
|
const match = html.match(/data-news-id="(\d+)"/);
|
|
return match ? match[1]! : null;
|
|
}
|
|
|
|
/**
|
|
* Décode un lien fsprotect double-Base64 en URL finale.
|
|
* Format: base64 → "url:<second_b64>|metadata|timestamp|hash"
|
|
* second_b64 → URL finale (ex: https://1fichier.com/...)
|
|
*/
|
|
export function decodeFsProtectLink(rawHref: string): string | null {
|
|
try {
|
|
// Extract the ?t= parameter
|
|
const tParamMatch = rawHref.match(/[?&]t=([^&]+)/);
|
|
if (!tParamMatch) return null;
|
|
|
|
const base64t = tParamMatch[1]!;
|
|
// First Base64 decode
|
|
const decodedT = Buffer.from(base64t, 'base64').toString('utf-8');
|
|
// Format: url:<second_base64>|<metadata>|<timestamp>|<hash>
|
|
if (!decodedT.startsWith('url:')) return null;
|
|
|
|
const firstPart = decodedT.substring(4).split('|')[0]!;
|
|
if (!firstPart) return null;
|
|
|
|
// Second Base64 decode → final URL
|
|
return Buffer.from(firstPart, 'base64').toString('utf-8');
|
|
} catch (e: any) {
|
|
console.error('[FS24] Erreur décodage lien Base64:', e.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function formatBytes(bytes: number): string {
|
|
if (!bytes || bytes <= 0) return '';
|
|
if (bytes > 1073741824) return (bytes / 1073741824).toFixed(2) + ' GB';
|
|
if (bytes > 1048576) return (bytes / 1048576).toFixed(0) + ' MB';
|
|
return (bytes / 1024).toFixed(0) + ' KB';
|
|
}
|
|
|
|
/**
|
|
* Parse la réponse JSON de l'API release-api.php en VideoLink[].
|
|
*/
|
|
export function parseReleasesJSON(data: any): VideoLink[] {
|
|
const links: VideoLink[] = [];
|
|
if (!data || !data.ok || !Array.isArray(data.items)) return links;
|
|
|
|
for (const item of data.items) {
|
|
const rawLink = item.original_link || '';
|
|
const finalUrl = decodeFsProtectLink(rawLink);
|
|
if (!finalUrl) continue;
|
|
|
|
const releaseName = item.release_name || 'Inconnu';
|
|
const lowerName = releaseName.toLowerCase();
|
|
|
|
// Detect language from release name
|
|
const langs: string[] = [];
|
|
if (lowerName.includes('multi')) langs.push('vf', 'vostfr');
|
|
else if (lowerName.includes('vostfr')) langs.push('vostfr');
|
|
else if (lowerName.includes('truefrench') || lowerName.includes('french')) langs.push('vf');
|
|
else langs.push('vf');
|
|
|
|
// Detect host from URL
|
|
let host = 'Inconnu';
|
|
try {
|
|
const urlObj = new URL(finalUrl);
|
|
host = urlObj.hostname.replace('www.', '');
|
|
} catch { /* ignore */ }
|
|
|
|
links.push({
|
|
id: String(item.id),
|
|
host,
|
|
url: finalUrl,
|
|
quality: item.quality || '',
|
|
size: formatBytes(item.size_bytes),
|
|
releaseName: item.is_team ? `[TEAM] ${releaseName}` : releaseName,
|
|
langs
|
|
});
|
|
}
|
|
|
|
return links;
|
|
}
|