import { readFileSync, readdirSync } from 'node:fs'; import { resolve, dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { createHash, randomUUID } from 'node:crypto'; import { getDb } from '../db/index.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); const SEED_DIR = resolve(__dirname, '../../data/seed'); // Deterministic UUID v5-style derived from slug; stable across DB rebuilds. function uuidFromSlug(slug) { const h = createHash('sha1').update('oradio:' + slug).digest('hex'); return [ h.slice(0, 8), h.slice(8, 12), '5' + h.slice(13, 16), '8' + h.slice(17, 20), h.slice(20, 32) ].join('-'); } function loadAllSeedFiles() { const files = readdirSync(SEED_DIR) .filter((f) => f.startsWith('stations') && f.endsWith('.json')) .sort(); const all = []; for (const f of files) { try { const data = JSON.parse(readFileSync(join(SEED_DIR, f), 'utf8')); if (Array.isArray(data)) all.push(...data); } catch (err) { console.warn(`[seed] failed to load ${f}:`, err.message); } } return all; } export function loadSeedFile() { return loadAllSeedFiles(); } export function loadCategoriesFile() { try { const txt = readFileSync(join(SEED_DIR, 'categories.json'), 'utf8'); return JSON.parse(txt); } catch { return []; } } /** * Merge-by-UUID seeder. Inserts stations and streams whose UUIDs are not yet in * the database. Existing stations are left untouched (admin edits are preserved). */ export function applySeed() { const db = getDb(); const stationByUuid = db.prepare('SELECT id FROM stations WHERE uuid = ?'); const streamByUuid = db.prepare('SELECT id FROM streams WHERE uuid = ?'); const insertStation = db.prepare(` INSERT INTO stations (uuid, name, slug, homepage, country, genres, description, image_url, category, source, source_ref) VALUES (@uuid, @name, @slug, @homepage, @country, @genres, @description, @image_url, @category, 'seed', @slug) `); const insertStream = db.prepare(` INSERT INTO streams (uuid, station_id, url, format, bitrate, label, priority) VALUES (@uuid, @station_id, @url, @format, @bitrate, @label, @priority) `); const entries = loadAllSeedFiles(); let inserted = 0; let streamsInserted = 0; let skipped = 0; const tx = db.transaction((list) => { for (const s of list) { const uuid = s.uuid || uuidFromSlug(s.slug); const existing = stationByUuid.get(uuid); if (existing) { skipped++; continue; } const info = insertStation.run({ uuid, name: s.name, slug: s.slug, homepage: s.homepage ?? null, country: s.country ?? null, genres: JSON.stringify(s.genres ?? []), description: s.description ?? null, image_url: s.image_url ?? null, category: s.category ?? null }); const stationId = info.lastInsertRowid; let priority = 0; for (const st of s.streams ?? []) { const streamUuid = st.uuid || randomUUID(); if (streamByUuid.get(streamUuid)) continue; insertStream.run({ uuid: streamUuid, station_id: stationId, url: st.url, format: st.format ?? 'unknown', bitrate: st.bitrate ?? null, label: st.label ?? null, priority: st.priority ?? priority }); streamsInserted++; priority++; } inserted++; } }); tx(entries); return { inserted, streamsInserted, skipped, total: entries.length }; } // Back-compat shim: bootstrap and reseed call applySeedIfEmpty(); now always merges. export function applySeedIfEmpty() { return applySeed(); }