diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0cb9909 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +legacy/Scores +_builds/ +missing.xml diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c6a31bb --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,134 @@ +# AGENTS.md - Development Guidelines + +This file contains essential information for AI agents working on this codebase. + +## Project Overview + +PHP website for "Harmonie de Montpellier-Jacou" (music band) with a Vue.js 2 frontend for score management. +- **Backend**: PHP with MySQL (legacy codebase) +- **Frontend**: Vue.js 2 + Bootstrap Vue (in `frontend/score/`) +- **API**: RESTful PHP API in `api/` directory + +## Build Commands + +### Frontend (Vue.js) +```bash +cd frontend/score +npm install +npm run serve # Development server +npm run build # Production build +npm run lint # ESLint check +``` + +### PHP +No build step required. Deploy to PHP-enabled web server. + +## Testing + +**No test framework configured.** + +To add tests: +- **PHP**: Consider PHPUnit +- **Vue**: Add Jest or Vitest via Vue CLI + +Run single test (when configured): +```bash +# Example for Jest (not yet configured) +npm test -- --testNamePattern="test name" +``` + +## Code Style Guidelines + +### PHP +- Use `= 1; len--) { + if (i + len <= parts.length) { + const combined = parts.slice(i, i + len).join('_'); + if (instruments[combined]) { + return { name: instruments[combined], endIndex: i + len }; + } + } + } + } + return null; +} + +function normalizeFilename(filename, forcedPart = null) { + let originalName = filename; + let name = filename.replace(/\.pdf$/i, ''); + + // Remove "part_X_" prefix if present (not a real part number) + name = name.replace(/^part_\d+_/i, ''); + + // Preprocessing: replace bugle_b, bugle_c to avoid being detected as tonalities + // b = sib (si bémol), c = ut (do) + let bugleTonalite = null; + if (/bugle_b/i.test(name)) { + bugleTonalite = 'sib'; + name = name.replace(/bugle_b/gi, 'BUGLETONALITY'); + } else if (/bugle_c/i.test(name)) { + bugleTonalite = 'ut'; + name = name.replace(/bugle_c/gi, 'BUGLETONALITY'); + } + + let voice = ''; + + // Extraire voix (A1, A2, B, C, etc.) AVANT extraction de partie + // Pattern like _A1, _A2 at end - EXCEPT when part of "a_default/a_defaut" or when it's an instrument (cor, etc.) + // ALSO skip if the letter is a tonality (c=C/ut, f=F/fa, etc.) + const tonaliteLetters = ['c', 'f']; // Letters that are tonalities, not voices + const voiceMatchOriginal = name.match(/_([A-Za-z])(\d+)$/); + const instrumentLetters = ['cor', 'b', 'c']; // Letters that are also instrument names or bugle tonalities + if (voiceMatchOriginal && !name.toLowerCase().includes('default') && !name.toLowerCase().includes('defaut') && !instrumentLetters.includes(voiceMatchOriginal[1].toLowerCase()) && !tonaliteLetters.includes(voiceMatchOriginal[1].toLowerCase())) { + voice = voiceMatchOriginal[1].toLowerCase(); + name = name.replace(/_[A-Za-z]\d+$/, '_' + voiceMatchOriginal[2]); + } else { + // Pattern like _B at end (followed by & or nothing) - EXCEPT when part of "a_default/a_defaut" or when it's an instrument or tonality + const voiceOnlyMatch = name.match(/_([A-Za-z])(?:&|_|$)/); + if (voiceOnlyMatch && voiceOnlyMatch[1].length === 1 && !name.toLowerCase().includes('default') && !name.toLowerCase().includes('defaut') && !instrumentLetters.includes(voiceOnlyMatch[1].toLowerCase()) && !tonaliteLetters.includes(voiceOnlyMatch[1].toLowerCase())) { + voice = voiceOnlyMatch[1].toLowerCase(); + name = name.replace(/_[A-Za-z](?:&|_|$)/, ''); + } + } + + // Detect & for multiple instruments (not for part numbers like 1&2) + // Check if & appears between letters (instruments), not between digits + // Can have underscores around & or not (like "Baritone_&_Tuba" or "Basse&Contrebasse") + // More strict: & must be between actual words (not between a digit and a letter) + const hasAmpersandBetweenInstruments = name.match(/[a-zA-Z]+[\s_-]*&[\s_-]*[a-zA-Z]+/); + const hasAmpersand = !!hasAmpersandBetweenInstruments; + + // Also check for multiple instruments without & (like "Bb_Baritone_1_Bb_Tenor_Tuba_1") + // This is indicated by two different tonalities in the name + // More strict: first tonality must be at start or after separator, second must be after some content + const hasDualTonalitiesNoAmpersand = name.match(/^(Bb|Eb|F|Ut)_.+_(Bb|Eb|F|Ut)_/i); + const hasDualInstrumentsNoAmpersand = !!hasDualTonalitiesNoAmpersand; + + // Also check for & between digits (parts like 1&2, 2&3) + const hasAmpersandBetweenDigits = name.match(/\d+&\d+/); + + // Initialize segments - will be set in the segment processing block below + let segments; + + // Traiter les parties combinées (1&2, 3&4) - AVANT extraction de partie + // Only for single instrument files (no & between instruments) + let combinedPart = ''; + if (!hasAmpersand && hasAmpersandBetweenDigits) { + const combinedMatch = name.match(/(\d+)&(\d+)/); + if (combinedMatch) { + combinedPart = `${combinedMatch[1]}_${combinedMatch[2]}`; + name = name.replace(/\d+&\d+/, ''); + name = name.replace(/_+/g, '_').replace(/^_+|_+$/g, ''); + } + } + + // Extraire numéro de partie (milieu ou fin) + // Si forcedPart est fourni (cas version_X), l'utiliser à la place + let partie = forcedPart || '1'; + + // If filename starts with a number followed by underscore (like "1_Clarinette"), + // this is likely a version/alternate prefix, NOT a part number. Strip it. + if (!forcedPart && name.match(/^\d+_/)) { + name = name.replace(/^\d+_/, ''); + } + + // Remove "_page_X" patterns (file errors, not part numbers) + if (!forcedPart && name.match(/_page_\d+/i)) { + name = name.replace(/_page_\d+/gi, ''); + } + + // Also remove patterns like _page_X (with different underscore patterns) + if (!forcedPart && name.match(/_page\d+/i)) { + name = name.replace(/_page\d+/gi, ''); + } + + if (!forcedPart) { + // Chercher d'abord un pattern _X_ au milieu (ex: Trombone_1_Sib) + // But don't remove if what follows looks like a tonality (C, F, Bb, Eb, etc.) + const middleMatch = name.match(/_(\d+)_([^_])/); + if (middleMatch) { + const potentialTonality = middleMatch[2].toLowerCase(); + // Check if this is a tonality abbreviation + const isTonality = tonalitesLong[potentialTonality] || tonalitesShort[potentialTonality]; + if (!isTonality) { + // Normal part, extract and remove + partie = middleMatch[1]; + const regex = new RegExp('_' + partie + '_', 'g'); + name = name.replace(regex, '_'); + } else { + // This is actually a tonality after the part number + // Keep the tonality in the name for later extraction + // But record the partie number + partie = middleMatch[1]; + } + } else { + // Chercher à la fin + const partieMatch = name.match(/_(\d+)$/); + if ( partieMatch) { + partie = partieMatch[1]; + name = name.slice(0, -partieMatch[0].length); + } + } + } + + // "solo" = partie speciale - check for number after solo + let soloPart = null; + const soloMatch = name.toLowerCase().match(/solo[_\s]*(\d+)/i); + if (soloMatch) { + soloPart = 'solo_' + soloMatch[1]; + } else if (name.toLowerCase().includes('solo')) { + soloPart = 'solo'; + } + + // Détecter clef - only if not already handled as clefa/clesol part + let clef = ''; + const lowerName = name.toLowerCase(); + if (!soloPart) { + for (const [k, v] of Object.entries(clefs)) { + if (lowerName.includes(k)) { + clef = v; + name = name.replace(new RegExp(k, 'gi'), ''); + break; + } + } + } + + // Split into segments for multiple instruments + // Case 1: has & between instruments + // Case 2: has dual tonalities indicating multiple instruments (like "Bb_Baritone_1_Bb_Tenor_Tuba_1") + if (hasAmpersand || hasDualInstrumentsNoAmpersand) { + let rawSegments; + if (hasAmpersand) { + // Split by & but keep instrument groups together + rawSegments = name.split(/&/).map(s => s.trim().replace(/^_+|_+$/g, '')).filter(s => s); + } else { + // Split by second tonality - find position of second tonality pattern + // Look for pattern like _Bb_ or _Eb_ etc. after the first tonality + const secondTonalityMatch = name.match(/[_-](Bb|Eb|F|Ut)[_-]/i); + if (secondTonalityMatch) { + const splitPos = secondTonalityMatch.index; + const firstPart = name.substring(0, splitPos); + const secondPart = name.substring(splitPos + 1); + rawSegments = [firstPart, secondPart].map(s => s.trim().replace(/^_+|_+$/g, '')).filter(s => s); + } else { + rawSegments = [name]; + } + } + + // Process each segment to extract instrument-specific info + segments = rawSegments; + } + + // Default segments if not set + if (!segments) { + segments = [name]; + } + + // Extraire instruments de chaque segment + const foundInstruments = []; + const foundTonalites = []; + const foundVariantes = []; + const foundParties = []; + let defaultInstrument = null; + + // Extraire l'instrument qui suit "default" ou "defaut" (pour "a_default_cor" -> extraire "cor") + // ET retire cette partie du nom pour éviter les doublons + const defaultMatch = name.match(/_?a_?defaut(?:l)?_?([a-zA-Z]+)/i); + if (defaultMatch) { + const defaultInstr = defaultMatch[1].toLowerCase(); + const mappedInstr = instruments[defaultInstr]; + defaultInstrument = mappedInstr || defaultInstr; + // Retirer la partie "a_defaut_..." du nom pour éviter les doublons + name = name.replace(/_?a_?defaut(?:l)?_?[a-zA-Z]+/i, ''); + } + + for (const segment of segments) { + const parts = segment.split('_').filter(p => p && p !== 'et'); + + // Chercher TOUS les instruments (pas juste le premier) + const seenInstruments = new Set(); + const usedIndices = new Set(); + + // First pass: check 3-word combinations + for (let i = 0; i < parts.length; i++) { + if (usedIndices.has(i)) continue; + if (i + 3 <= parts.length) { + const combined3 = parts.slice(i, i + 3).join('_').toLowerCase(); + if (instruments[combined3]) { + foundInstruments.push(instruments[combined3]); + seenInstruments.add(instruments[combined3]); + usedIndices.add(i); usedIndices.add(i+1); usedIndices.add(i+2); + } + } + } + // Second pass: check 2-word combinations + for (let i = 0; i < parts.length; i++) { + if (usedIndices.has(i)) continue; + if (i + 2 <= parts.length) { + const combined2 = parts.slice(i, i + 2).join('_').toLowerCase(); + if (instruments[combined2]) { + foundInstruments.push(instruments[combined2]); + seenInstruments.add(instruments[combined2]); + usedIndices.add(i); usedIndices.add(i+1); + } + } + } + // Third pass: check single words + for (let i = 0; i < parts.length; i++) { + if (usedIndices.has(i)) continue; + const lowerPart = parts[i].toLowerCase(); + if (instruments[lowerPart]) { + foundInstruments.push(instruments[lowerPart]); + seenInstruments.add(instruments[lowerPart]); + } + } + + // Chercher tonalité - prioritize full names (sib, mib, fa), then abbreviations only at start + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + const lowerPart = part.toLowerCase(); + + // First check for explicit full tonality names (exact match, any position) + let found = false; + for (const [key, val] of Object.entries(tonalitesLong)) { + if (lowerPart === key) { + // Only allow duplicates for multi-instrument files + if (foundInstruments.length >= 2 || !foundTonalites.includes(val)) { + foundTonalites.push(val); + } + found = true; + break; + } + } + + // Check abbreviations - only if not already found AND avoid duplicates with long forms + if (!found) { + for (const [key, val] of Object.entries(tonalitesShort)) { + if (lowerPart === key) { + // Only allow duplicates for multi-instrument files + if (foundInstruments.length >= 2 || !foundTonalites.includes(val)) { + foundTonalites.push(val); + } + found = true; + break; + } + } + } + + // Also check for tonality + number combo (like F1, Bb2, Eb3) + // This handles cases like "French_Horn_F1" where F1 is not split + if (!found) { + const tonalityWithNumber = part.match(/^([A-Za-z]+)(\d+)$/); + if (tonalityWithNumber) { + const tonality = tonalityWithNumber[1].toLowerCase(); + const num = tonalityWithNumber[2]; + // Check if the letter part is a tonality + for (const [key, val] of Object.entries(tonalitesShort)) { + if (tonality === key) { + if (!foundTonalites.includes(val)) { + foundTonalites.push(val); + } + // Also extract the part number + if (!foundParties.includes(num)) { + foundParties.push(num); + } + found = true; + break; + } + } + } + } + } + + // Extraire partie de ce segment + for (const part of parts) { + if (/^\d+$/.test(part)) { + foundParties.push(part); + } + } + + // Chercher variante + for (const part of parts) { + if (variantes.includes(part) && !foundVariantes.includes(part)) { + foundVariantes.push(part); + } + } + + // Chercher qualificatifs spéciaux - TOUJOURS garder ces qualificatifs pour éviter les doublons + const qualifiers = ['solo', 'tutti', 'petite', 'grande', 'bc', 'tc', 'i', 'ii', 'mini', 'junior']; + for (const part of parts) { + const lowerPart = part.toLowerCase(); + for (const q of qualifiers) { + // Skip "solo" if we're already using it as soloPart + if (q === 'solo' && soloPart) continue; + // For short qualifiers (i, ii), use exact match only; for longer ones, allow partial + if (q.length <= 2) { + if (lowerPart === q) { + if (!foundVariantes.includes(q)) { + foundVariantes.push(q); + } + } + } else { + if (lowerPart === q || lowerPart.includes(q)) { + if (!foundVariantes.includes(q)) { + foundVariantes.push(q); + } + } + } + } + } + } + + // Construire le résultat - Interleave instruments with their tonalities and parties for multi-instrument files + let result; + + if (foundInstruments.length >= 2 && foundTonalites.length >= 2) { + // Multiple instruments with separate tonalities: interleave them + // Format: instrument1-tonality1-clef1-part1+instrument2-tonality2-clef2-part2 + const segmentResults = []; + for (let i = 0; i < foundInstruments.length; i++) { + let seg = foundInstruments[i]; + if (foundTonalites[i]) seg += '-' + foundTonalites[i]; + // Add clef before partie + if (clef) seg += '-' + clef; + // Add party - always add at least 1 for multi-instrument + const party = foundParties[i] || '1'; + seg += '-' + party; + segmentResults.push(seg); + } + result = segmentResults.join('+'); + + // Convert French "defaut" to English "default" + result = result.replace(/defaut/g, 'default'); + result = result.toLowerCase(); + return result + '.pdf'; + } + + // Original logic for single instrument or cases without separate tonalities + if (foundInstruments.length >= 2) { + // Several instruments - interleave with parties if we have them + const segmentResults = []; + for (let i = 0; i < foundInstruments.length; i++) { + let seg = foundInstruments[i]; + // Add tonality: if each instrument has its own (foundTonalites.length >= foundInstruments.length), use that + // Otherwise if there's one tonality, use it for all + if (foundTonalites.length >= foundInstruments.length && foundTonalites[i]) { + seg += '-' + foundTonalites[i]; + } else if (foundTonalites.length === 1 && foundTonalites[0]) { + seg += '-' + foundTonalites[0]; + } + // Add party for this segment - always add at least 1 for multi-instrument + const party = foundParties[i] || '1'; + seg += '-' + party; + segmentResults.push(seg); + } + result = segmentResults.join('+'); + } else if (foundInstruments.length === 1) { + result = foundInstruments[0]; + } else { + // Fallback : utiliser le nom nettoyé + const fallbackParts = name.split('_').filter(p => p && p.length > 2); + result = fallbackParts[0] || 'inconnu'; + } + + // Skip tonality addition for multi-instrument case (already handled above) + const skipTonality = foundInstruments.length >= 2; + + // Ajouter variantes AVANT tonalité + const uniqueVariantes = foundVariantes.filter(v => { + if (result.includes(v)) return false; + return !foundInstruments.some(instr => instr === v || instr.includes('-' + v)); + }); + if (uniqueVariantes.length > 0) { + result += '-' + uniqueVariantes.join('-'); + } + + // ALWAYS include tonalité if found (even if only one instrument) - skip for multi-instr + if (!skipTonality && foundTonalites.length > 0) { + result += '-' + foundTonalites.join('-'); + } + + // Ajouter "default" et l'instrument qui suit (ex: default-cor) + if (defaultInstrument) { + result += '-default-' + defaultInstrument; + } + + // Skip adding parties for multi-instrument (already added in segment building) + const isMultiInstrument = foundInstruments.length >= 2; + + // Add clef BEFORE parte (according to spec: instrument-tonalité-clef-partie) + if (clef && !isMultiInstrument) { + result += '-' + clef; + } + + // Utiliser les parties des segments si plusieurs instruments OU si on a des parties collectées (cas tonalité+numéro comme F1, F2) + if (isMultiInstrument) { + // Already handled in segment building above + } else if (foundParties.length > 0) { + result += '-' + foundParties.join('-'); + } else if (combinedPart) { + result += '-' + (voice ? voice : '') + combinedPart; + } else if (soloPart) { + // If soloPart has number (solo_1, solo_2), keep it; otherwise add parte number with separator + if (soloPart.includes('_')) { + result += '-' + soloPart; + } else { + result += '-' + soloPart + '-' + partie; + } + } else if (voice) { + result += '-' + voice + partie; + } else { + result += '-' + partie; + } + + // Convert French "defaut" to English "default" + result = result.replace(/defaut/g, 'default'); + + // Add bugle tonality if present + if (bugleTonalite) { + if (result.includes('BUGLETONALITY')) { + result = result.replace('BUGLETONALITY', 'bugle-' + bugleTonalite); + } + } + + // Always lowercase the result + result = result.toLowerCase(); + + return result + '.pdf'; +} + +// Collecter tous les changements +// Structure: NUM/PIECE/INSTRUMENT/VERSION/PARTIE +const changes = []; +const scores = fs.readdirSync(scoresPath).filter(d => { + const p = path.join(scoresPath, d); + return fs.statSync(p).isDirectory() && /^\d+$/.test(d); +}); + +for (const scoreId of scores) { + const scorePath = path.join(scoresPath, scoreId); + const pieceDirs = fs.readdirSync(scorePath).filter(d => { + const p = path.join(scorePath, d); + return fs.statSync(p).isDirectory() && d !== 'score.ini'; + }); + + for (const pieceId of pieceDirs) { + const piecePath = path.join(scorePath, pieceId); + const instrDirs = fs.readdirSync(piecePath).filter(d => { + const p = path.join(piecePath, d); + return fs.statSync(p).isDirectory(); + }); + + for (const instrId of instrDirs) { + const instrPath = path.join(piecePath, instrId); + const entries = fs.readdirSync(instrPath); + const versionDirs = entries.filter(e => /^\d+$/.test(e)); + + for (const versionDir of versionDirs) { + const versionPath = path.join(instrPath, versionDir); + if (!fs.statSync(versionPath).isDirectory()) continue; + + const files = fs.readdirSync(versionPath).filter(f => f.toLowerCase().endsWith('.pdf')); + + for (const file of files) { + // Pour les fichiers "version_X", utiliser le répertoire comme partie SEULEMENT si pas de numéro de partie dans le nom (après l'instrument) + const isVersion = file.toLowerCase().startsWith('version'); + let forcedPart = null; + let fileForNormalization = file; + + if (isVersion) { + // Enlever le prefixe version_X_ pour le check (sans le .pdf) + const nameWithoutVersion = file.replace(/\.pdf$/i, '').replace(/^version_\d+_/i, ''); + // Chercher un numéro de partie SEULEMENT à la fin du nom (après l'instrument) + const hasPartAfterInstrument = nameWithoutVersion.match(/_(\d+)$/); + if (!hasPartAfterInstrument) { + forcedPart = versionDir; + } + // Enlever le prefixe version_X_ pour le traitement (garder le .pdf) + fileForNormalization = file.replace(/^version_\d+_/i, ''); + } + + const newName = normalizeFilename(fileForNormalization, forcedPart); + changes.push({ + oldPath: path.join(versionPath, file), + newPath: path.join(piecePath, instrId, versionDir, newName), + oldName: file, + newName, + scoreId, + pieceId, + instrId, + versionDir + }); + } + } + } + } +} + +// Gérer les conflits +const usedNames = new Set(); +for (const change of changes) { + let finalName = change.newName; + let counter = 1; + + while (usedNames.has(change.newPath)) { + const base = change.newName.replace('.pdf', ''); + finalName = `${base}_alt${counter}.pdf`; + change.newPath = path.join(scoresPath, change.scoreId, change.pieceId, change.instrId, change.versionDir, finalName); + counter++; + } + + usedNames.add(change.newPath); + change.finalName = finalName; +} + +// Validate against spec: {instrument}[-{variante}][-{tonalité}][-{clef}]{-partie} +// Multi: instrument1[-variante1][-tonalité1][-clef1]{-partie1}+instrument2... +// Combined parts: instrument-...-X_Y +function validateSpec(filename) { + const base = filename.replace('.pdf', ''); + + // Multi-instrument: split by + + if (base.includes('+')) { + const parts = base.split('+'); + for (const part of parts) { + const subparts = part.split('-'); + if (subparts.length < 1) return false; + + const instrument = subparts[0]; + if (!instrument || !/^[a-z_]+$/.test(instrument)) return false; + + // Check each segment doesn't have duplicates + for (let i = 0; i < subparts.length - 1; i++) { + if (subparts[i] === subparts[i+1]) return false; + } + } + return true; + } + + // Check if it's combined parts (single instrument with 1_2) + // Pattern: instrument-...-X_Y where X and Y are numbers + const combinedPartsMatch = base.match(/^([a-z_]+).*-(\d+)_(\d+)(-.+)?$/); + if (combinedPartsMatch) { + const instrument = combinedPartsMatch[1]; + if (!instrument || !/^[a-z_]+$/.test(instrument)) return false; + return true; + } + + // Single instrument: must start with valid instrument name + const firstPart = base.split('-')[0]; + if (!firstPart || !/^[a-z_]+$/.test(firstPart)) return false; + + // Check for duplicates + const comps = base.split('-'); + for (let i = 0; i < comps.length - 1; i++) { + if (comps[i] === comps[i+1]) return false; + } + + return true; +} + +// Validate all generated names +const invalidNames = []; +for (const c of changes) { + if (!validateSpec(c.finalName)) { + invalidNames.push(c); + } +} + +// Afficher résultats +console.log(`Total fichiers : ${changes.length}\n`); + +if (invalidNames.length > 0) { + console.log(`⚠️ ${invalidNames.length} noms ne collent pas à la spec:\n`); + for (const inv of invalidNames.slice(0, 20)) { + console.log(` ${inv.oldName} → ${inv.finalName}`); + } + if (invalidNames.length > 20) { + console.log(` ... et ${invalidNames.length - 20} autres`); + } + console.log(''); +} + +// Afficher exemples +console.log('=== EXEMPLES DE CONVERSIONS ===\n'); +const uniquePatterns = []; +const seen = new Set(); + +for (const c of changes) { + const pattern = `${c.oldName} → ${c.finalName}`; + if (!seen.has(pattern) && uniquePatterns.length < 30) { + seen.add(pattern); + uniquePatterns.push(c); + } +} + +for (const ex of uniquePatterns) { + console.log(`${ex.oldName}`); + console.log(` → ${ex.finalName}\n`); +} + +// Générer les fichiers CSV +const conversionComplete = []; +const duplicates = []; +const nameToFiles = new Map(); + +for (const c of changes) { + const key = `${c.scoreId}/${c.instrId}/${c.numDir}/${c.finalName}`; + if (!nameToFiles.has(key)) { + nameToFiles.set(key, []); + } + nameToFiles.get(key).push(c); +} + +for (const [key, files] of nameToFiles) { + const [scoreId, instrId, numDir, finalName] = key.split('/'); + const oldPaths = files.map(f => f.oldName).join('; '); + + if (files.length === 1) { + conversionComplete.push(`${files[0].oldPath};${finalName}`); + } else { + for (const f of files) { + duplicates.push(`${f.oldPath};${finalName}`); + } + } +} + +fs.writeFileSync(path.join(__dirname, 'conversion_complete.csv'), conversionComplete.join('\n')); +fs.writeFileSync(path.join(__dirname, 'duplicates.csv'), duplicates.join('\n')); + +console.log(`\nFichiers générés:`); +console.log(`- conversion_complete.csv: ${conversionComplete.length} fichiers`); +console.log(`- duplicates.csv: ${duplicates.length} fichiers (doublons)`); + +// Exécuter si confirmé +if (confirmed) { + console.log('\nExécution...\n'); + let success = 0; + let errors = 0; + let removedDirs = 0; + + for (const change of changes) { + try { + fs.renameSync(change.oldPath, change.newPath); + success++; + } catch (err) { + console.error(`❌ ${change.oldName} : ${err.message}`); + errors++; + } + } + + // Supprimer répertoires vides + for (const scoreId of scores) { + const scorePath = path.join(scoresPath, scoreId); + const instrDirs = fs.readdirSync(scorePath).filter(d => { + const p = path.join(scorePath, d); + return fs.statSync(p).isDirectory() && d !== 'score.ini'; + }); + + for (const instrId of instrDirs) { + const instrPath = path.join(scorePath, instrId); + const numDirs = fs.readdirSync(instrPath).filter(e => /^\d+$/.test(e)); + + for (const numDir of numDirs) { + const numPath = path.join(instrPath, numDir); + try { + const remaining = fs.readdirSync(numPath); + if (remaining.length === 0) { + fs.rmdirSync(numPath); + removedDirs++; + } + } catch (e) {} + } + } + } + + console.log(`\n✅ Terminé !`); + console.log(`Fichiers convertis : ${success}`); + console.log(`Erreurs : ${errors}`); + console.log(`Répertoires supprimés : ${removedDirs}`); +} else { + console.log('\nPour confirmer la conversion :'); + console.log(`node scripts/convert_final_v2.js ${scoresPath} confirm`); +}