import { writeFileSync, existsSync, readdirSync, statSync, createReadStream, unlinkSync } from 'fs'; import { join, basename } from 'path'; import { createHash } from 'crypto'; import { fsCreateSession, fsDestroySession, fsGet } from '../flaresolverr.js'; import { upsertMediaFile } from '../db.js'; // Match the duplicate scanner in gallery.js — md5 of first 64KB + exact size. const HASH_BYTES = 65536; function hashFirst64kSync(filePath) { return new Promise((resolve, reject) => { const hash = createHash('md5'); const s = createReadStream(filePath, { start: 0, end: HASH_BYTES - 1 }); s.on('data', (c) => hash.update(c)); s.on('end', () => resolve(hash.digest('hex'))); s.on('error', reject); }); } // Build size -> [{filename, path, hash:null}] index for the folder. Hashes are // computed lazily only when a size collision is found. function buildSizeIndex(folderPath) { const idx = new Map(); let entries; try { entries = readdirSync(folderPath); } catch { return idx; } for (const name of entries) { if (name.startsWith('.')) continue; const p = join(folderPath, name); try { const st = statSync(p); if (!st.isFile()) continue; if (!idx.has(st.size)) idx.set(st.size, []); idx.get(st.size).push({ filename: name, path: p, hash: null }); } catch {} } return idx; } async function ensureCandidateHash(c) { if (c.hash != null) return c.hash; try { c.hash = await hashFirst64kSync(c.path); } catch { c.hash = ''; } return c.hash; } const TURBO_HOST_RE = /^https?:\/\/(?:www\.)?turbo\.\w+\//i; const TURBO_BASE = 'https://turbo.cr'; const DEFAULT_UA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; export function isTurboUrl(url) { return TURBO_HOST_RE.test(url); } function unescapeHtml(s) { return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); } function extractMp4FromHtml(html) { // Plyr renders the resolved URL into