MIGRATION VERS GITEA

This commit is contained in:
mathis committed 2026-08-25 01:23:13 +02:00
commit 5f9d991c15
333 files changed
+108514

No files matched your search

+561
View File
@@ -0,0 +1,561 @@
const { createClient } = require('@supabase/supabase-js');
const fs = require('fs');
const path = require('path');
// Charger les variables d'environnement depuis le fichier .env
try {
const envPath = path.join(__dirname, '../.env');
if (fs.existsSync(envPath)) {
const envContent = fs.readFileSync(envPath, 'utf8');
envContent.split('\n').forEach(line => {
const parts = line.split('=');
if (parts.length >= 2) {
const key = parts[0].trim();
const val = parts.slice(1).join('=').trim().replace(/^['"]|['"]$/g, '').replace(/\r/g, '');
if (key) process.env[key] = val;
}
});
}
} catch (e) {
console.error('[Cron] Impossible de charger le fichier .env :', e);
}
const supabaseUrl = process.env.REACT_APP_SUPABASE_URL || process.env.SUPABASE_URL;
const supabaseKey = process.env.REACT_APP_SUPABASE_ANON_KEY || process.env.SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseKey) {
console.error('[Cron] Clés Supabase manquantes dans le .env');
process.exit(1);
}
const supabase = createClient(supabaseUrl, supabaseKey);
function checkIfHoraireRunsOnDate(horaire, date = new Date()) {
const yyyy = date.getFullYear();
const mm = String(date.getMonth() + 1).padStart(2, '0');
const dd = String(date.getDate()).padStart(2, '0');
const dateStr = `${yyyy}-${mm}-${dd}`;
let joursPerso = [];
try {
joursPerso = typeof horaire.jours_personnalises === 'string'
? JSON.parse(horaire.jours_personnalises)
: (horaire.jours_personnalises || []);
} catch(e) {}
const matchPerso = joursPerso.find(jp => jp.date === dateStr);
if (matchPerso) return matchPerso.circule;
let joursNonCirc = [];
try {
joursNonCirc = typeof horaire.jours_non_circulation === 'string'
? JSON.parse(horaire.jours_non_circulation)
: (horaire.jours_non_circulation || []);
} catch(e) {}
if (joursNonCirc.includes(dateStr)) return false;
const day = date.getDay(); // 0 = Dimanche, 1 = Lundi, ...
switch(day) {
case 0: return !!horaire.circule_dimanche;
case 1: return !!horaire.circule_lundi;
case 2: return !!horaire.circule_mardi;
case 3: return !!horaire.circule_mercredi;
case 4: return !!horaire.circule_jeudi;
case 5: return !!horaire.circule_vendredi;
case 6: return !!horaire.circule_samedi;
}
return false;
}
function getTrainDetailsForGare(horaire, gareId, type) {
const stops = horaire.gares_desservies || [];
const stop = stops.find(s => s.gare_id === gareId);
if (stop) {
if (type === 'depart') {
return { time: stop.heure_depart || stop.heure_arrivee || '', quai: stop.quai_reel || stop.quai || '' };
} else {
return { time: stop.heure_arrivee || stop.heure_depart || '', quai: stop.quai_reel || stop.quai || '' };
}
}
if (type === 'depart' && horaire.gare_depart_id === gareId) {
return { time: horaire.heure_depart || '', quai: horaire.quai_reel || horaire.quai || '' };
}
if (type === 'arrivee' && horaire.gare_arrivee_id === gareId) {
return { time: horaire.heure_arrivee || '', quai: horaire.quai_reel || horaire.quai || '' };
}
return {
time: type === 'depart' ? horaire.heure_depart : horaire.heure_arrivee,
quai: horaire.quai_reel || horaire.quai || ''
};
}
function generateNomFichierAnnonce(typeAnnonce, ext = 'mp3') {
const date = new Date().toISOString().slice(0, 10);
const shortId = Math.random().toString(36).slice(2, 8);
return `${typeAnnonce}_${date}_${shortId}.${ext}`;
}
function concatenateAudio(buffers) {
if (buffers.length === 0) return { payload: null, format: 'mp3' };
if (buffers.length === 1) {
const isWav = buffers[0].subarray(0, 4).toString('ascii') === 'RIFF';
return { payload: buffers[0], format: isWav ? 'wav' : 'mp3' };
}
const isWav = buffers[0].subarray(0, 4).toString('ascii') === 'RIFF';
if (isWav) {
// Logic for WAV concatenation
let totalPcmLength = 0;
for (let i = 0; i < buffers.length; i++) {
const hasRiff = buffers[i].subarray(0, 4).toString('ascii') === 'RIFF';
totalPcmLength += hasRiff ? (buffers[i].length - 44) : buffers[i].length;
}
const header = Buffer.alloc(44);
buffers[0].copy(header, 0, 0, 44);
const totalFileSize = 44 + totalPcmLength;
header.writeUInt32LE(totalFileSize - 8, 4);
header.writeUInt32LE(totalPcmLength, 40);
const combinedBuffer = Buffer.alloc(totalFileSize);
header.copy(combinedBuffer, 0, 0, 44);
let offset = 44;
for (let i = 0; i < buffers.length; i++) {
const hasRiff = buffers[i].subarray(0, 4).toString('ascii') === 'RIFF';
const dataPayload = hasRiff ? buffers[i].subarray(44) : buffers[i];
dataPayload.copy(combinedBuffer, offset);
offset += dataPayload.length;
}
return { payload: combinedBuffer, format: 'wav' };
} else {
// Logic for MP3 concatenation (streamable, direct binary append)
return { payload: Buffer.concat(buffers), format: 'mp3' };
}
}
async function main() {
try {
const now = new Date();
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0);
const todayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
console.log(`[Cron Runner] Execution at ${now.toISOString()}`);
const [
{ data: gares },
{ data: horaires },
{ data: templates },
{ data: mots },
{ data: parametres },
{ data: todayAnnonces }
] = await Promise.all([
supabase.from('gares').select('*'),
supabase.from('horaires').select('*'),
supabase.from('annonces_templates').select('*').eq('actif', true),
supabase.from('banque_mots').select('*').eq('actif', true),
supabase.from('parametres').select('*'),
supabase.from('annonces_enregistrees')
.select('*')
.gte('date_diffusion', todayStart.toISOString())
.lte('date_diffusion', todayEnd.toISOString())
]);
// Si régénération demandée, supprimer de la BDD et du Storage toutes les annonces d'aujourd'hui
const regenerateParam = (parametres || []).find(p => p.cle === 'global_regenerate_requested');
const regenerate = (regenerateParam && regenerateParam.valeur === 'true');
if (regenerate && todayAnnonces && todayAnnonces.length > 0) {
console.log(`[Cron Runner] Deleting ${todayAnnonces.length} existing announcements for today...`);
const pathsToDelete = todayAnnonces.map(a => a.storage_path).filter(Boolean);
if (pathsToDelete.length > 0) {
await supabase.storage.from('banque-annonces').remove(pathsToDelete);
}
await supabase
.from('annonces_enregistrees')
.delete()
.in('id', todayAnnonces.map(a => a.id));
// Vider la liste pour forcer la régénération
todayAnnonces.length = 0;
}
// Réinitialiser le drapeau de régénération en base de données si nécessaire
if (regenerateParam && regenerateParam.valeur === 'true') {
await supabase
.from('parametres')
.update({ valeur: 'false', updated_at: now.toISOString() })
.eq('cle', 'global_regenerate_requested');
console.log('[Cron Runner] Reset global_regenerate_requested parameter in DB.');
}
const settingsByGare = {};
const dijonGare = (gares || []).find(g => g.nom && g.nom.toLowerCase().trim() === 'dijon');
const dijonSettingsParam = dijonGare ? (parametres || []).find(p => p.cle === `diffusion_settings_${dijonGare.id}`) : null;
const dijonSettings = dijonSettingsParam ? dijonSettingsParam.valeur : null;
(parametres || []).forEach(p => {
if (p.cle.startsWith('diffusion_settings_')) {
const gareId = p.cle.replace('diffusion_settings_', '');
settingsByGare[gareId] = p.valeur;
}
});
const runningHorairesGlobal = (horaires || []).filter(h => checkIfHoraireRunsOnDate(h, now));
// Compter le nombre total de tâches à générer
let totalTasksCount = 0;
const garesToProcess = (gares || []).filter(gare => {
let rule = settingsByGare[gare.id];
if (!rule && dijonSettings && dijonGare && dijonGare.id !== gare.id) {
rule = dijonSettings;
}
return !!rule;
});
const tasksToExecute = [];
for (const gare of garesToProcess) {
let rule = settingsByGare[gare.id];
if (!rule && dijonSettings && dijonGare && dijonGare.id !== gare.id) {
rule = dijonSettings;
}
const runningHorairesForGare = runningHorairesGlobal
.filter(h => {
if (h.gare_depart_id === gare.id || h.gare_arrivee_id === gare.id) return true;
const stops = h.gares_desservies || [];
return stops.some(s => s.gare_id === gare.id);
})
.sort((a, b) => {
const timeA = getTrainDetailsForGare(a, gare.id, 'depart').time || getTrainDetailsForGare(a, gare.id, 'arrivee').time || '00:00';
const timeB = getTrainDetailsForGare(b, gare.id, 'depart').time || getTrainDetailsForGare(b, gare.id, 'arrivee').time || '00:00';
return timeA.localeCompare(timeB);
});
for (const horaire of runningHorairesForGare) {
const isDepart = horaire.gare_depart_id === gare.id ||
(horaire.gares_desservies || []).some(s => s.gare_id === gare.id && s.heure_depart);
const isArrivee = horaire.gare_arrivee_id === gare.id ||
(horaire.gares_desservies || []).some(s => s.gare_id === gare.id && s.heure_arrivee);
if (isDepart) {
const typeRule = rule.depart;
if (typeRule && typeRule.templateId) {
const template = (templates || []).find(t => t.id === typeRule.templateId);
if (template) {
const exists = (todayAnnonces || []).some(a =>
a.gare_id === gare.id &&
a.type_annonce === 'depart' &&
a.variables_values?.numero_train === horaire.numero_train
);
if (!exists) {
tasksToExecute.push({ gare, typeAnnonce: 'depart', typeRule, template, horaire });
}
}
}
}
if (isArrivee) {
const typeRule = rule.arrivee;
if (typeRule && typeRule.templateId) {
const template = (templates || []).find(t => t.id === typeRule.templateId);
if (template) {
const exists = (todayAnnonces || []).some(a =>
a.gare_id === gare.id &&
a.type_annonce === 'arrivee' &&
a.variables_values?.numero_train === horaire.numero_train
);
if (!exists) {
tasksToExecute.push({ gare, typeAnnonce: 'arrivee', typeRule, template, horaire });
}
}
}
}
}
}
totalTasksCount = tasksToExecute.length;
console.log(`[Cron Runner] Total tasks to generate: ${totalTasksCount}`);
// Publier l'état initial
await supabase.from('parametres').upsert({
cle: 'global_generation_progress',
valeur: JSON.stringify({
status: totalTasksCount > 0 ? 'running' : 'done',
current: 0,
total: totalTasksCount,
progress: totalTasksCount > 0 ? 0 : 100,
message: totalTasksCount > 0 ? `Préparation de la génération... (0/${totalTasksCount})` : 'Toutes les annonces sont déjà générées.'
}),
updated_at: new Date().toISOString()
}, { onConflict: 'cle' });
// Exécuter séquentiellement et publier la progression
let currentTaskIndex = 0;
for (const task of tasksToExecute) {
const { gare, typeAnnonce, typeRule, template, horaire } = task;
const pct = Math.round((currentTaskIndex / totalTasksCount) * 100);
await supabase.from('parametres').upsert({
cle: 'global_generation_progress',
valeur: JSON.stringify({
status: 'running',
current: currentTaskIndex,
total: totalTasksCount,
progress: pct,
message: `Génération : ${gare.nom} - Train ${horaire.numero_train || ''} (${typeAnnonce === 'depart' ? 'Départ' : 'Arrivée'}) (${currentTaskIndex + 1}/${totalTasksCount})`
}),
updated_at: new Date().toISOString()
}, { onConflict: 'cle' });
try {
await generateAnnonce(gare, typeAnnonce, typeRule, template, horaire);
} catch (err) {
console.error(`[Cron Runner] Error generating task for ${gare.nom}:`, err);
}
currentTaskIndex++;
}
// Publier l'état final
await supabase.from('parametres').upsert({
cle: 'global_generation_progress',
valeur: JSON.stringify({
status: 'done',
current: totalTasksCount,
total: totalTasksCount,
progress: 100,
message: totalTasksCount > 0 ? 'Génération globale terminée avec succès !' : 'Toutes les annonces sont déjà générées.'
}),
updated_at: new Date().toISOString()
}, { onConflict: 'cle' });
// Fonction interne de génération séquentielle
async function generateAnnonce(gare, typeAnnonce, typeRule, template, horaire) {
const { time: trainTime, quai: trainQuai } = getTrainDetailsForGare(horaire, gare.id, typeAnnonce);
if (!trainTime) return;
const [hStr, mStr] = trainTime.split(':');
const trainHour = parseInt(hStr, 10);
const trainMin = parseInt(mStr, 10);
const scheduledDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), trainHour, trainMin);
scheduledDate.setMinutes(scheduledDate.getMinutes() - parseInt(typeRule.offset || 0, 10));
const isPast = scheduledDate <= now;
const direction = typeAnnonce === 'depart' ? `à destination de ${horaire.gare_arrivee_nom}` : `en provenance de ${horaire.gare_depart_nom}`;
const name = `Auto: ${horaire.numero_train} (${typeAnnonce}) ${direction} - ${trainTime}`;
const stopsList = (horaire.snapshot_gares_desservies || horaire.gares_desservies || []);
const intermediateStops = stopsList
.filter(g => !g.is_depart && !g.is_arrivee)
.map(g => g.gare_nom)
.join(', ');
const resolvedVars = {
numero_train: horaire.numero_train || '',
destination: horaire.gare_arrivee_nom || '',
provenance: horaire.gare_depart_nom || '',
heure_depart: horaire.heure_depart || '',
heure_arrivee: horaire.heure_arrivee || '',
heure: trainTime,
quai: trainQuai,
voie: trainQuai,
arrets_intermediaires: intermediateStops,
afficheurs: typeRule.afficheurs || [],
type_train: horaire.type_train || ''
};
let ttsText = template.tts_texte || '';
if (!ttsText) {
ttsText = (template.sequence || [])
.map(s => s.type === 'mot' ? s.label : s.type === 'variable' ? resolvedVars[s.variable] || '' : '')
.filter(Boolean)
.join(' ');
} else {
Object.entries(resolvedVars).forEach(([key, val]) => {
ttsText = ttsText.replaceAll(`{${key}}`, Array.isArray(val) ? val.join(', ') : val);
});
}
resolvedVars.tts_texte = ttsText;
const mode = typeRule.mode || ((template.nom?.toLowerCase().includes('sive') || template.description?.toLowerCase().includes('sive')) ? 'concatenation' : 'tts');
let audioPayload = Buffer.from('TTS_ONLY');
let format = 'mp3';
if (mode === 'concatenation') {
const targetGareSousType = template.nom?.toLowerCase().includes('sive') ? 'sive' : 'quai';
const buffers = [];
for (let idxInSeq = 0; idxInSeq < (template.sequence || []).length; idxInSeq++) {
const item = template.sequence[idxInSeq];
let motToPlay = null;
if (item.type === 'mot') {
motToPlay = mots.find(m => m.id === item.mot_id);
} else if (item.type === 'variable') {
const val = resolvedVars[item.variable];
if (val) {
const normValue = String(val).toLowerCase().trim();
if (item.variable === 'destination' || item.variable === 'provenance') {
const gMots = mots.filter(m => m.categorie === 'gare');
for (const m of gMots) {
try {
const parsed = JSON.parse(m.description);
if (parsed.gare_nom?.toLowerCase().trim() === normValue || m.label?.toLowerCase().trim() === normValue) {
if (parsed.sous_type === targetGareSousType) { motToPlay = m; break; }
if (!motToPlay) motToPlay = m;
}
} catch (_) {}
}
} else if (item.variable === 'quai' || item.variable === 'voie') {
const isLast = idxInSeq === template.sequence.length - 1;
const targetQuaiSousType = isLast ? 'descendant' : 'montant';
const qMots = mots.filter(m => m.categorie === 'quai');
for (const m of qMots) {
try {
const parsed = JSON.parse(m.description);
if (parsed.quai_nom?.toLowerCase().trim() === normValue || m.label?.toLowerCase().trim() === normValue) {
if (parsed.sous_type === targetQuaiSousType) { motToPlay = m; break; }
if (!motToPlay) motToPlay = m;
}
} catch (_) {}
}
} else if (item.variable === 'type_train') {
const ttMots = mots.filter(m => m.categorie === 'type_train');
for (const m of ttMots) {
try {
const parsed = JSON.parse(m.description);
if (parsed.type_train_nom?.toLowerCase().trim() === normValue || m.label?.toLowerCase().trim() === normValue) {
motToPlay = m;
break;
}
} catch (_) {}
}
}
if (!motToPlay) {
motToPlay = mots.find(m => m.label?.toLowerCase().trim() === normValue);
}
}
}
if (motToPlay) {
try {
const { data: fileData, error: dlError } = await supabase.storage.from('banque-annonces').download(motToPlay.storage_path);
if (!dlError && fileData) {
const arrayBuffer = await fileData.arrayBuffer();
buffers.push(Buffer.from(arrayBuffer));
}
} catch (e) {
console.warn(`[Cron] download failed for ${motToPlay.storage_path}`, e);
}
}
}
if (buffers.length > 0) {
const resAudio = concatenateAudio(buffers);
if (resAudio.payload) {
audioPayload = resAudio.payload;
format = resAudio.format;
}
}
}
const ext = format;
const nomFichier = generateNomFichierAnnonce(typeAnnonce, ext);
const storagePath = `annonces/${nomFichier}`;
const { error: uploadError } = await supabase.storage
.from('banque-annonces')
.upload(storagePath, audioPayload, {
cacheControl: '3600',
upsert: false,
contentType: ext === 'wav' ? 'audio/wav' : 'audio/mpeg'
});
if (uploadError) {
console.error('[Cron] Upload error:', uploadError);
return;
}
const { error: dbError } = await supabase
.from('annonces_enregistrees')
.insert({
nom: name,
type_annonce: typeAnnonce,
template_id: template.id,
storage_path: storagePath,
nom_fichier: nomFichier,
variables_values: resolvedVars,
mode_generation: mode,
duree_ms: null,
taille_bytes: audioPayload.length,
gare_id: gare.id,
notes: `Programmed Auto Standalone: ${typeAnnonce} | Afficheurs: ${(resolvedVars.afficheurs || []).join(', ')}`,
date_diffusion: scheduledDate.toISOString(),
diffuse: isPast
});
if (dbError) {
console.error('[Cron] DB error:', dbError);
await supabase.storage.from('banque-annonces').remove([storagePath]);
} else {
console.log(`[Cron] Generated and saved annonce for ${horaire.numero_train} at ${gare.nom}`);
}
}
const { data: dueAnnonces, error: dueError } = await supabase
.from('annonces_enregistrees')
.select('*')
.eq('diffuse', false)
.lte('date_diffusion', now.toISOString());
if (dueError) {
console.error('[Cron] dueAnnonces fetch error:', dueError);
} else if (dueAnnonces && dueAnnonces.length > 0) {
console.log(`[Cron] Found ${dueAnnonces.length} due announcements to broadcast.`);
const broadcastPromises = dueAnnonces.map(async (annonce) => {
try {
const { error: diffError } = await supabase
.from('annonces_diffusions')
.insert([
{
annonce_id: annonce.id,
gare_id: annonce.gare_id,
audio_path: annonce.mode_generation === 'tts' ? null : annonce.storage_path,
tts_texte: annonce.mode_generation === 'tts' ? (annonce.variables_values?.tts_texte || 'Attention, train en approche.') : null
}
]);
if (diffError) {
console.error(`[Cron] Error broadcasting ${annonce.id}:`, diffError);
} else {
await supabase
.from('annonces_enregistrees')
.update({ diffuse: true, updated_at: now.toISOString() })
.eq('id', annonce.id);
console.log(`[Cron] Broadcasted and updated ${annonce.id}`);
}
} catch (bErr) {
console.error(`[Cron] Standalone broadcast error for ${annonce.id}:`, bErr);
}
});
await Promise.all(broadcastPromises);
}
console.log('[Cron Runner] Done successfully.');
} catch (err) {
console.error('[Cron Runner] Global exception:', err);
}
process.exit(0);
}
main();