#!/usr/bin/env node /** * Conversion finale avec gestion des instruments multiples (option A) * Format : instrument1_et_instrument2_ton_partie[_clef].pdf */ const fs = require('fs'); const path = require('path'); const scoresPath = process.argv[2] || path.join(__dirname, '..', 'legacy', 'Scores'); const confirmed = process.argv[3] === 'confirm'; if (!fs.existsSync(scoresPath)) { console.error(`❌ Le dossier ${scoresPath} n'existe pas !`); process.exit(1); } if (!confirmed) { console.log('Mode SIMULATION (ajoutez "confirm" pour exécuter)\n'); } const instruments = { 'clarinet': 'clarinette', 'clarinette': 'clarinette', 'clarinettte': 'clarinette', 'petite_clarinette': 'petite_clarinette', 'petite_clarineette': 'petite_clarinette', 'grande_clarinette': 'grande_clarinette', 'es_clarinette': 'petite_clarinette', 'clarinette_alto': 'clarinette_alto', 'clarinet_alto': 'clarinette_alto', 'alto_clarinet': 'clarinette_alto', 'alto_clarinette': 'clarinette_alto', 'clarinette_basse': 'clarinette_basse', 'clarinet_basse': 'clarinette_basse', 'clarinette_bass': 'clarinette_basse', 'basse_clarinet': 'clarinette_basse', 'bass_clarinet': 'clarinette_basse', 'bass_clarinette': 'clarinette_basse', 'clarinette_basse': 'clarinette_basse', 'Bb_Clarinet_Bass': 'clarinette_basse', 'bb_clarinet_bass': 'clarinette_basse', 'trumpet': 'trompette', 'trompette': 'trompette', 'trrumpet': 'trompette', 'trombone': 'trombone', 'tuba': 'tuba', 'tenor_tuba': 'tuba_tenor', 'tuba_tenor': 'tuba_tenor', 'contre_tuba': 'contre_tuba', 'flute': 'flute', 'flauto': 'flute', 'grande_flute': 'grande_flute', 'petite_flute': 'petite_flute', 'oboe': 'hautbois', 'hautbois': 'hautbois', 'bassoon': 'basson', 'basson': 'basson', 'contre_basson': 'contre_basson', 'contrebasson': 'contre_basson', 'string_electric_bass': 'basse_electrique', 'string': 'contrebasse', 'string_bass': 'contrebasse', 'contrebasse': 'contrebasse', 'contre_basse': 'contrebasse', 'contrabass': 'contrebasse', 'doublebass': 'contrebasse', 'double_bass': 'contrebasse', 'electric_bass': 'basse_electrique', 'electric_bass_guitar': 'basse_electrique', 'electric': 'electrique', 'optional_electric_bass': 'basse_electrique', 'optional': '', 'basse_a_cordes': 'basse_a_cordes', 'guitar': 'guitare', 'electric_guitar': 'guitare_electrique', 'cornet': 'cornet', 'bass': 'basse', 'basse': 'basse', 'baritone': 'baryton', 'baryton': 'baryton', 'bariton': 'baryton', 'baritione': 'baryton', 'bartitone': 'baryton', 'tenorhorn': 'tenorhorn', 'baryton_euphonium': 'baryton_et_euphonium', 'baritone_euphonium': 'baryton_et_euphonium', 'horn': 'cor', 'french_horn': 'cor', 'frenchhorn': 'cor', 'corno': 'cor', 'flugelhorn': 'bugle', 'flugel': 'bugle', 'flugel_horn': 'bugle', 'bugle': 'bugle', 'petit_bugle': 'petit_bugle', 'piccolo': 'piccolo', 'euphonium': 'euphonium', 'euphononium': 'euphonium', 'sax': 'saxophone', 'saxe': 'sax', 'saxophone': 'saxophone', 'saxophone_tenor': 'sax_tenor', 'tenor_saxophone': 'sax_tenor', 'tenor_sax': 'sax_tenor', 'saxophone_alto': 'sax_alto', 'alto_saxophone': 'sax_alto', 'alto_sax': 'sax_alto', 'saxophone_basse': 'sax_basse', 'bass_saxophone': 'sax_basse', 'bass_sax': 'sax_basse', 'bb_bass_saxophone': 'sax_basse', 'Bb_Bass_saxophone': 'sax_basse', 'saxo': 'saxophone', 'saxo_alto': 'sax_alto', 'saxo_tenor': 'sax_tenor', 'sax_soprano': 'sax_soprano', 'soprano_sax': 'sax_soprano', 'sax_alto': 'sax_alto', 'sax_tenor': 'sax_tenor', 'sax_baryton': 'sax_baryton', 'saxbaryton': 'sax_baryton', 'saxalto': 'sax_alto', 'saxtenor': 'sax_tenor', 'sax_bariton': 'sax_baryton', 'sax_baritone': 'sax_baryton', 'bartitone_sax': 'sax_baryton', 'bartitone_saxophone': 'sax_baryton', 'saxophone_baryton': 'sax_baryton', 'saxophone_baritone': 'sax_baryton', 'baritone_saxophone': 'sax_baryton', 'baritone_sax': 'sax_baryton', 'saxe_baryton': 'sax_baryton', 'saxe_baritone': 'sax_baryton', 'saxophone_basse': 'sax_basse', 'sax_basse': 'sax_basse', 'bass_saxophone': 'sax_basse', 'bass_sax': 'sax_basse', 'trombone_bass': 'trombone', 'bass_saxophone': 'sax_basse', 'bass_sax': 'sax_basse', 'saxophone_basse': 'sax_basse', 'trombone_basse': 'trombone_basse', 'bass_trombone': 'trombone_basse', 'bas_trombone': 'trombone_basse', 'trombone_a_piston': 'trombone', 'bass_guitar': 'basse_electrique', 'cornet_trumpet': 'cornet', 'trumpet_cornet': 'cornet', 'trumpet': 'trompette', 'trompette': 'trompette', 'xylophone': 'xylophone', 'vibraphone': 'vibraphone', 'glockenspiel': 'glockenspiel', 'bass_saxophone': 'saxophone_basse', 'saxophone_basse': 'saxophone_basse', 'bass_drum': 'grosse_caisse', 'grosse_caisse': 'grosse_caisse', 'grossecaisse': 'grosse_caisse', 'tuba_basse': 'tuba', 'bass_tuba': 'tuba', 'maillet_percussion': 'malette_percussion', 'mallet_percussion': 'malette_percussion', 'malette_percussion': 'malette_percussion', 'bombardon': 'bombardon', 'percussion': 'percussion', 'drums': 'batterie', 'drum': 'batterie', 'timbales': 'timbales', 'timbale': 'timbales', 'cymbale': 'cymbale', 'cymbales': 'cymbale', 'cymbal': 'cymbale', 'tambour': 'tambour', 'tambour_de_basque': 'tambour_de_basque', 'tom': 'tom', 'tom_grave': 'tom_grave', 'triangle': 'triangle', 'caisse_claire': 'caisse_claire', 'caisses_claires': 'caisse_claire', 'caisseclaire': 'caisse_claire', 'timpani': 'timbales', 'timbales': 'timbales', 'gong': 'gong', 'tam_tam': 'tam_tam', 'wind_chimes': 'carillon', 'windchimes': 'carillon', 'carillon_a_vent': 'carillon_a_vent', 'woodblock': 'woodblock', 'claves': 'claves', 'batterie': 'batterie', 'maracas': 'maracas', 'conducteur': 'conducteur', 'conducteur_bois': 'conducteur_bois', 'conducteur_cuivres': 'conducteur_cuivres', 'conducteur_cordes': 'conducteur_cordes', 'chant': 'chant', 'en_attente': 'en_attente', }; const variantes = ['tenor', 'soprano', 'basse']; // Only unambiguous full names: sib, mib, fa, do (la is too ambiguous - matches "la" in titles) const tonalitesLong = { 'sib': 'sib', 'mib': 'mib', 'fa': 'fa', 'do': 'ut', 'ut': 'ut', 'sol': 'sol', 're': 're', 're_b': 'reb', 'mi': 'mi', 'si': 'si', 'en_sib': 'sib', 'en_mib': 'mib', 'en_fa': 'fa' }; // Only unambiguous abbreviations: bb=bb, eb=eb, plus note letters when before instrument const tonalitesShort = { 'bb': 'sib', 'eb': 'mib', 'f': 'fa', 'c': 'ut' }; const clefs = { 'clefsol': 'clesol', 'clesol': 'clesol', 'clefa': 'clefa', 'cle_ut': 'cle_ut', 'tc': 'tc' }; function removeAccents(str) { return str.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); } function extractInstrument(parts) { for (let i = 0; i < parts.length; i++) { for (let len = 3; len >= 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`); }