[FEAT] Update the database for the scores. And follow the new file format
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
legacy/Scores
|
||||||
|
_builds/
|
||||||
|
missing.xml
|
||||||
134
AGENTS.md
Normal file
134
AGENTS.md
Normal file
@@ -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 `<?php` opening tags (avoid short `<?` tags for consistency)
|
||||||
|
- Classes: PascalCase (e.g., `Score`, `Database`)
|
||||||
|
- Methods: camelCase (e.g., `readScore`, `getEntryName`)
|
||||||
|
- Variables: snake_case or camelCase (be consistent within files)
|
||||||
|
- Indent: 3 spaces (legacy style) - maintain consistency with surrounding code
|
||||||
|
- Place opening braces on same line as class/function declaration
|
||||||
|
- Use `include_once` for required files
|
||||||
|
- **Note**: Uses legacy MySQL functions (consider upgrading to PDO/MySQLi)
|
||||||
|
|
||||||
|
### JavaScript/Vue
|
||||||
|
- ESLint configuration in `package.json`:
|
||||||
|
- Extends: `plugin:vue/essential`, `eslint:recommended`
|
||||||
|
- Parser: `babel-eslint`
|
||||||
|
- Use ES6+ features (async/await, arrow functions)
|
||||||
|
- Components: PascalCase
|
||||||
|
- Data properties: camelCase
|
||||||
|
- Use 2-space indentation
|
||||||
|
- Semicolons required
|
||||||
|
|
||||||
|
### General
|
||||||
|
- UTF-8 encoding for all files
|
||||||
|
- French language for user-facing content
|
||||||
|
- Maintain GPL v2 license headers in PHP files
|
||||||
|
|
||||||
|
## File Organization
|
||||||
|
|
||||||
|
```
|
||||||
|
/
|
||||||
|
├── api/ # REST API endpoints
|
||||||
|
│ ├── config/ # Database configuration
|
||||||
|
│ ├── objects/ # Data models
|
||||||
|
│ └── score/ # API endpoints
|
||||||
|
├── frontend/score/ # Vue.js application
|
||||||
|
│ ├── src/ # Source code
|
||||||
|
│ └── public/ # Static assets
|
||||||
|
├── Scripts/ # PHP page controllers
|
||||||
|
├── fpdf/ # PDF generation library
|
||||||
|
├── Imgs/ # Image assets
|
||||||
|
├── Textes/ # Text content files
|
||||||
|
└── index.php # Main entry point
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
MySQL database connection configured in `api/config/database.php`:
|
||||||
|
- Uses legacy `mysql_*` functions
|
||||||
|
- Database name: `ohmj2`
|
||||||
|
- Tables: `repertoire` (for scores)
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- PHP: Uses `@` error suppression operator in some places
|
||||||
|
- Vue: Uses try/catch with console.log for errors
|
||||||
|
- Return HTTP 200 on success in API responses
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- API uses `Access-Control-Allow-Origin: *` (CORS enabled for all)
|
||||||
|
- SQL queries use string concatenation (vulnerable to SQL injection)
|
||||||
|
- Database credentials stored in plain text in `api/config/database.php`
|
||||||
|
- Sanitize user inputs before database queries
|
||||||
|
|
||||||
|
## Git
|
||||||
|
|
||||||
|
- No CI/CD configured
|
||||||
|
- Commit to main branch
|
||||||
|
- Standard `.gitignore` for Node.js projects
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- Vue 2.6.11
|
||||||
|
- Bootstrap Vue 2.17.3
|
||||||
|
- Axios 0.21.0
|
||||||
|
- Vue CLI 4.5.0
|
||||||
|
|
||||||
|
### PHP
|
||||||
|
- FPDF library for PDF generation
|
||||||
|
- MySQL extension (legacy)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- This is a legacy codebase with mixed coding styles
|
||||||
|
- Prefer consistency with existing code over strict style enforcement
|
||||||
|
- Site targets French-speaking users
|
||||||
|
- Production URL: `ohmj2.free.fr`
|
||||||
|
|
||||||
|
## Temporary Work Files
|
||||||
|
|
||||||
|
- Use `_builds/` directory for temporary scripts and working files
|
||||||
|
- Only `scripts/convert_final_v2.js` should be kept in the scripts folder (committed to git)
|
||||||
|
- CSV output files belong in `_builds/`
|
||||||
372
PLAN.md
Normal file
372
PLAN.md
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
# Plan de développement - Application de partitions OHMJ
|
||||||
|
|
||||||
|
## Structure finale du projet
|
||||||
|
|
||||||
|
```
|
||||||
|
/home/jbnadal/sources/jb/ohmj/ohmj2/
|
||||||
|
├── legacy/ # Backup ancien code
|
||||||
|
│ ├── api/ # Ancien PHP legacy (MySQL)
|
||||||
|
│ ├── frontend/ # Ancien Vue.js 2
|
||||||
|
│ ├── fpdf/
|
||||||
|
│ ├── Imgs/
|
||||||
|
│ ├── Scripts/
|
||||||
|
│ ├── Textes/
|
||||||
|
│ └── index.php
|
||||||
|
├── api/ # NOUVEAU backend PHP moderne
|
||||||
|
│ ├── index.php # Router / entry point API
|
||||||
|
│ ├── config/
|
||||||
|
│ │ └── users.json # Users avec passwords hashés
|
||||||
|
│ └── lib/
|
||||||
|
│ ├── Auth.php # JWT authentication
|
||||||
|
│ └── ScoreScanner.php # Lecture fichiers ini + scan partitions
|
||||||
|
├── partitions/ # NOUVEAU frontend SvelteKit
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── routes/
|
||||||
|
│ │ │ ├── +page.svelte # Login page
|
||||||
|
│ │ │ ├── scores/+page.svelte # Liste des morceaux
|
||||||
|
│ │ │ ├── scores/[id]/+page.svelte # Détail partition
|
||||||
|
│ │ │ └── +layout.svelte # Layout avec auth guard
|
||||||
|
│ │ ├── lib/
|
||||||
|
│ │ │ ├── api.ts # Client API
|
||||||
|
│ │ │ └── stores/auth.ts # Store JWT
|
||||||
|
│ │ └── app.html
|
||||||
|
│ ├── package.json
|
||||||
|
│ └── svelte.config.js
|
||||||
|
└── Scores/ # Répertoire partitions (hors git)
|
||||||
|
└── {id}/
|
||||||
|
├── score.ini
|
||||||
|
├── cla/
|
||||||
|
│ └── 1/
|
||||||
|
│ └── *.pdf
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 1 : Backend PHP (api/)
|
||||||
|
|
||||||
|
### 1.1 ScoreScanner.php ✓ (FAIT)
|
||||||
|
- [x] Classe pour scanner les partitions
|
||||||
|
- [x] Lecture des fichiers score.ini
|
||||||
|
- [x] Scan récursif instruments → parties → fichiers PDF
|
||||||
|
- [x] Mapping codes instruments vers noms
|
||||||
|
|
||||||
|
### 1.2 Auth.php
|
||||||
|
- [ ] Classe JWT pour authentification
|
||||||
|
- [ ] Génération token JWT
|
||||||
|
- [ ] Vérification token JWT
|
||||||
|
- [ ] Lecture users.json
|
||||||
|
- [ ] Vérification password hashé (bcrypt)
|
||||||
|
|
||||||
|
### 1.3 users.json
|
||||||
|
- [ ] Fichier JSON avec structure users
|
||||||
|
- [ ] Passwords hashés avec bcrypt
|
||||||
|
- [ ] Script pour ajouter/modifier users
|
||||||
|
|
||||||
|
### 1.4 index.php (Router API)
|
||||||
|
- [ ] CORS headers
|
||||||
|
- [ ] Routing : POST /login
|
||||||
|
- [ ] Routing : GET /scores (protégé JWT)
|
||||||
|
- [ ] Routing : GET /scores/:id (protégé JWT)
|
||||||
|
- [ ] Routing : GET /download/:path (protégé JWT, stream PDF)
|
||||||
|
- [ ] Gestion erreurs JSON
|
||||||
|
|
||||||
|
## Phase 2 : Frontend SvelteKit (partitions/)
|
||||||
|
|
||||||
|
### 2.1 Initialisation projet
|
||||||
|
- [ ] npm create svelte@latest partitions
|
||||||
|
- [ ] TypeScript + ESLint + Prettier
|
||||||
|
- [ ] npm install (axios, etc.)
|
||||||
|
|
||||||
|
### 2.2 Configuration
|
||||||
|
- [ ] svelte.config.js (adapter static)
|
||||||
|
- [ ] API base URL config
|
||||||
|
|
||||||
|
### 2.3 Stores
|
||||||
|
- [ ] lib/stores/auth.ts
|
||||||
|
- [ ] Store JWT token
|
||||||
|
- [ ] Store user info
|
||||||
|
- [ ] Persistance localStorage
|
||||||
|
|
||||||
|
### 2.4 Client API
|
||||||
|
- [ ] lib/api.ts
|
||||||
|
- [ ] Instance axios avec baseURL
|
||||||
|
- [ ] Intercepteur pour ajouter header Authorization
|
||||||
|
- [ ] Méthodes : login(), getScores(), getScore(id), downloadFile(path)
|
||||||
|
|
||||||
|
### 2.5 Routes
|
||||||
|
|
||||||
|
#### Login (/)
|
||||||
|
- [ ] Formulaire : username, password
|
||||||
|
- [ ] Appel API login
|
||||||
|
- [ ] Stockage JWT
|
||||||
|
- [ ] Redirection vers /scores
|
||||||
|
- [ ] Gestion erreurs
|
||||||
|
|
||||||
|
#### Liste des morceaux (/scores)
|
||||||
|
- [ ] Auth guard (redirect si pas connecté)
|
||||||
|
- [ ] Table/DataGrid avec :
|
||||||
|
- [ ] Numéro (ID)
|
||||||
|
- [ ] Nom du morceau
|
||||||
|
- [ ] Compositeur
|
||||||
|
- [ ] Tri possible
|
||||||
|
- [ ] Click → navigation /scores/:id
|
||||||
|
- [ ] Loading state
|
||||||
|
|
||||||
|
#### Détail partition (/scores/[id])
|
||||||
|
- [ ] Auth guard
|
||||||
|
- [ ] Header :
|
||||||
|
- [ ] Numéro
|
||||||
|
- [ ] Nom du morceau
|
||||||
|
- [ ] Compositeur
|
||||||
|
- [ ] Bouton retour
|
||||||
|
- [ ] Liste des instruments (grid ou accordion) :
|
||||||
|
- [ ] Nom de l'instrument
|
||||||
|
- [ ] Pour chaque partie :
|
||||||
|
- [ ] Numéro de partie
|
||||||
|
- [ ] Liste des fichiers PDF
|
||||||
|
- [ ] Bouton téléchargement par fichier
|
||||||
|
- [ ] Loading state
|
||||||
|
|
||||||
|
#### Layout
|
||||||
|
- [ ] Auth guard global
|
||||||
|
- [ ] Header avec :
|
||||||
|
- [ ] Titre app
|
||||||
|
- [ ] User connecté
|
||||||
|
- [ ] Bouton logout
|
||||||
|
- [ ] Footer (optionnel)
|
||||||
|
|
||||||
|
### 2.6 Styles
|
||||||
|
- [ ] CSS moderne (Tailwind ou CSS vanilla)
|
||||||
|
- [ ] Responsive (mobile-friendly)
|
||||||
|
- [ ] Thème harmonie (couleurs)
|
||||||
|
|
||||||
|
## Phase 3 : Déploiement
|
||||||
|
|
||||||
|
### 3.1 Build frontend
|
||||||
|
- [ ] npm run build
|
||||||
|
- [ ] Output dans dist/
|
||||||
|
|
||||||
|
### 3.2 Configuration serveur
|
||||||
|
- [ ] /partitions → contenu dist/
|
||||||
|
- [ ] /api → PHP
|
||||||
|
- [ ] /legacy/Scores → accès aux PDFs
|
||||||
|
- [ ] .htaccess ou config nginx pour routing
|
||||||
|
|
||||||
|
### 3.3 Sécurité
|
||||||
|
- [ ] HTTPS obligatoire
|
||||||
|
- [ ] JWT secret fort
|
||||||
|
- [ ] Protection download (vérif JWT)
|
||||||
|
- [ ] Headers sécurité
|
||||||
|
|
||||||
|
## Notes techniques
|
||||||
|
|
||||||
|
### Backend PHP
|
||||||
|
- Pas de base de données
|
||||||
|
- JWT pour auth stateless
|
||||||
|
- Lecture fichiers uniquement
|
||||||
|
- Stream PDF pour download
|
||||||
|
|
||||||
|
### Frontend SvelteKit
|
||||||
|
- Svelte 5 (runes)
|
||||||
|
- TypeScript strict
|
||||||
|
- Pas de store complexe (juste auth)
|
||||||
|
- Client API simple
|
||||||
|
|
||||||
|
### Contraintes
|
||||||
|
- Chemin partitions : ../legacy/Scores/ (sera déplacé après DL)
|
||||||
|
- Fichiers ini présents partout
|
||||||
|
- PDFs uniquement
|
||||||
|
- Auth obligatoire pour tout accès
|
||||||
|
|
||||||
|
## Phase Préliminaire : Normalisation des données
|
||||||
|
|
||||||
|
### 0.1 Structure des partitions (à valider)
|
||||||
|
- [ ] Analyser tous les instruments pour voir si le répertoire "1" est nécessaire
|
||||||
|
- [ ] Identifier les cas où il n'y a qu'une seule partie (supprimer le niveau "1" ?)
|
||||||
|
- [ ] Valider la structure finale avant codage
|
||||||
|
|
||||||
|
### 0.2 Harmonisation des noms de fichiers ✓
|
||||||
|
**Convention finale :**
|
||||||
|
- Format : `{instrument}[_{instrument2}][_{variante}][_{tonalité}]_{partie}[_clef].pdf`
|
||||||
|
- Exemples :
|
||||||
|
- `clarinette_sib_1.pdf` (clarinette en Si♭, partie 1)
|
||||||
|
- `clarinette_alto_mib_1.pdf` (clarinette alto en Mi♭, partie 1)
|
||||||
|
- `basse_et_contrebasse_sib_1.pdf` (partition pour 2 instruments)
|
||||||
|
- `cor_mib_1_et_2.pdf` (cor en Mi♭, parties 1 et 2 combinées)
|
||||||
|
- `euphonium_sib_2_clefa.pdf` (euphonium en Si♭, partie 2, clé de fa)
|
||||||
|
- Tout en minuscule, français, ASCII sans accents
|
||||||
|
- Tonalités : sib (Bb), mib (Eb), fa (F), do (C), etc.
|
||||||
|
- **Garder les sous-répertoires numérotés (1/, 2/, etc.)** pour distinguer les versions
|
||||||
|
- **Script :** `scripts/convert_final_v2.js`
|
||||||
|
- **Usage :** `node scripts/convert_final_v2.js /chemin/vers/Scores confirm`
|
||||||
|
|
||||||
|
### 0.3 À propos des répertoires numérotés (1/, 2/, etc.)
|
||||||
|
**Découverte importante :**
|
||||||
|
- Le répertoire `1/` = version principale du morceau
|
||||||
|
- Le répertoire `2/` (ou plus) = alternative du même morceau
|
||||||
|
- Cela permet d'avoir plusieurs versions d'un même morceau dans le même ID
|
||||||
|
- Exemple : Score 210, dossier `cla/1/` et `cla/2/` contiennent des versions différentes
|
||||||
|
- **Action:** Le script de conversion doit utiliser le numéro du répertoire comme numéro de partie UNIQUEMENT pour les fichiers "version_X"
|
||||||
|
|
||||||
|
### 0.4 À propos du répertoire "sup"
|
||||||
|
- Le répertoire `sup/` signifie "supérieur" ou "combiné"
|
||||||
|
- C'est une partition où deux instruments jouent sur la même partition
|
||||||
|
- Ex: baryton + tuba sur une même partition
|
||||||
|
- **Note API:** Ces partitions combinées doivent être gérées différemment dans l'interface (afficher les deux instruments)
|
||||||
|
|
||||||
|
### 0.5 Conventions de nommage - Instruments
|
||||||
|
**Orthographe standardisée (français) :**
|
||||||
|
- Clarinette → clarinette
|
||||||
|
- Saxophone → saxophone (ou sax_baryton, sax_tenor, sax_alto, sax_soprano pour les variantes)
|
||||||
|
- Cor → cor
|
||||||
|
- Trompette → trompette
|
||||||
|
- Trombone → trombone
|
||||||
|
- Tuba → tuba (ou tuba_tenor pour tenor tuba)
|
||||||
|
- Basse → basse
|
||||||
|
- Contrebasse → contrebasse (UN SEUL MOT)
|
||||||
|
- Baryton → baryton
|
||||||
|
- Euphonium → euphonium
|
||||||
|
- Flute → flute (ou petite_flute, grande_flute)
|
||||||
|
- Hautbois → hautbois
|
||||||
|
- Basson → basson
|
||||||
|
- Contre-basson → contre_basson
|
||||||
|
- Bugle → bugle (inclut flugelhorn)
|
||||||
|
- Percussion → percussion (timbales, grosse_caisse, cymbale, caisse_claire, triangle, etc.)
|
||||||
|
|
||||||
|
**Instrumentsperaussion reconnus :**
|
||||||
|
- timpani, glockenspiel, bongos, maracas, tambourine, vibraphone, xylophone
|
||||||
|
- woodblock, claves, batterie
|
||||||
|
|
||||||
|
**Tonalités :**
|
||||||
|
- sib (Bb), mib (Eb), fa (F), do (ut), sol, re, mi, si, reb
|
||||||
|
- NOTE: "do" devient "ut" dans les noms de fichiers
|
||||||
|
|
||||||
|
**Cas spéciaux gérés :**
|
||||||
|
- "default_cor" = instrument de substitution (ex: baryton_default_cor_2)
|
||||||
|
- Parties combinées avec & = format "instrument1_et_instrument2_info1_info2_partie"
|
||||||
|
|
||||||
|
### 0.6 Structure du nom de fichier final
|
||||||
|
**Format :** `{instrument}[_{variante}][_{tonalité}][_{default}][_{instrument_subst}]_{partie}[_clef].pdf`
|
||||||
|
|
||||||
|
**Exemples :**
|
||||||
|
- `clarinette_sib_1.pdf`
|
||||||
|
- `sax_baryton_mib_1.pdf`
|
||||||
|
- `cor_fa_1_et_2.pdf`
|
||||||
|
- `baryton_sib_default_cor_2.pdf`
|
||||||
|
- `sax_tenor_sib_1.pdf`
|
||||||
|
- `baryton_mib_2_clesol.pdf`
|
||||||
|
- `baryton_et_tuba_tenor_sib_clefa.pdf` (parties combinées sup)
|
||||||
|
|
||||||
|
**Note:** Pour les parties combinées (sup), chaque instrument garde ses propres infos (tonalité, partie, clef)
|
||||||
|
|
||||||
|
## Phase 3 : Déploiement
|
||||||
|
|
||||||
|
### 3.1 Build frontend
|
||||||
|
- [ ] npm run build
|
||||||
|
- [ ] Output dans dist/
|
||||||
|
|
||||||
|
### 3.2 Configuration serveur
|
||||||
|
- [ ] /partitions → contenu dist/
|
||||||
|
- [ ] /api → PHP
|
||||||
|
- [ ] /legacy/Scores → accès aux PDFs
|
||||||
|
- [ ] .htaccess ou config nginx pour routing
|
||||||
|
|
||||||
|
### 3.3 Sécurité
|
||||||
|
- [ ] HTTPS obligatoire
|
||||||
|
- [ ] JWT secret fort
|
||||||
|
- [ ] Protection download (vérif JWT)
|
||||||
|
- [ ] Headers sécurité
|
||||||
|
|
||||||
|
## Phase 4 : Admin (post-déploiement)
|
||||||
|
|
||||||
|
### 4.1 Page d'administration
|
||||||
|
- [ ] Route `/admin` protégée (super-user)
|
||||||
|
- [ ] Interface CRUD pour les partitions
|
||||||
|
- [ ] Formulaire : ID, nom du morceau, compositeur
|
||||||
|
- [ ] Création automatique du répertoire et du score.ini
|
||||||
|
- [ ] Upload drag & drop des fichiers PDF
|
||||||
|
- [ ] Organisation automatique des fichiers dans la bonne structure
|
||||||
|
- [ ] Validation du format (convention de nommage)
|
||||||
|
|
||||||
|
## Prochaines étapes
|
||||||
|
|
||||||
|
1. ✓ Backup legacy (FAIT)
|
||||||
|
2. ✓ Renommage IDs avec zéros (FAIT)
|
||||||
|
3. ✓ Correction noms fichiers (& et espaces) (FAIT)
|
||||||
|
4. → Définir structure finale des partitions
|
||||||
|
5. → Harmoniser tous les noms de fichiers
|
||||||
|
6. → Créer Auth.php
|
||||||
|
7. → Créer users.json
|
||||||
|
8. → Créer index.php (router)
|
||||||
|
9. → Tester backend
|
||||||
|
10. → Créer frontend SvelteKit
|
||||||
|
11. → Développer routes
|
||||||
|
12. → Tester intégration
|
||||||
|
13. → Déployer
|
||||||
|
14. → Développer page admin
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conversion des noms de fichiers (2026-02-17)
|
||||||
|
|
||||||
|
### Structure finale
|
||||||
|
`NUM/PIECE/INSTRUMENT/VERSION/PARTIE.pdf`
|
||||||
|
|
||||||
|
- **NUM**: numéro du score (ex: 102, 390)
|
||||||
|
- **PIECE**: numéro de pièce (1 = premier morceau)
|
||||||
|
- **INSTRUMENT**: trigramme de l'instrument (ex: cla, sax, trp)
|
||||||
|
- **VERSION**: numéro de version (1 = première version)
|
||||||
|
- **PARTIE**: numéro de partie (ex: 1, 2, 3...)
|
||||||
|
|
||||||
|
### Exemples
|
||||||
|
|
||||||
|
#### Score normal (1 seul morceau)
|
||||||
|
- `102/1/cla/1/1/clarinette-sib-1.pdf`
|
||||||
|
|
||||||
|
#### Score 390 (5 morceaux différents)
|
||||||
|
- `390/1/sax/1/1/sax_alto-1.pdf` (pièce 1)
|
||||||
|
- `390/2/sax/1/1/sax_alto-1.pdf` (pièce 2)
|
||||||
|
|
||||||
|
### Format score.ini
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[info]
|
||||||
|
name = La part d'Euterpe
|
||||||
|
compositor = Michaël CUVILLON
|
||||||
|
|
||||||
|
[pieces]
|
||||||
|
count = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour 390:
|
||||||
|
```ini
|
||||||
|
[info]
|
||||||
|
name = La part d'Euterpe
|
||||||
|
compositor = Michaël CUVILLON
|
||||||
|
|
||||||
|
[pieces]
|
||||||
|
count = 5
|
||||||
|
1 = La Part d Euterpe
|
||||||
|
2 = Calliope s Songs
|
||||||
|
3 = Interlude
|
||||||
|
4 = Oxola
|
||||||
|
5 = Steps to Paranassus
|
||||||
|
```
|
||||||
|
|
||||||
|
### Étapes realizadas
|
||||||
|
|
||||||
|
1. ✅ score.ini: ajouté section [info] et [pieces] pour tous les scores
|
||||||
|
2. ✅ Répertoires 2 chiffres renommés (72→072, 73→073, 98→098)
|
||||||
|
3. ✅ Structure NUM/1/INSTRUMENT/VERSION appliquée pour tous les scores (sauf 390)
|
||||||
|
4. ✅ Structure NUM/PIECE/INSTRUMENT/VERSION appliquée pour 390 (PIECE=1-5)
|
||||||
|
5. ✅ Script convert_final_v2.js mis à jour pour supporter la nouvelle structure
|
||||||
|
6. ✅ Conversion appliquée: 6388 fichiers convertis, 0 erreurs
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
|
||||||
|
- ✅ Multi instruments: utilise `+` (ex: `basse-sib-1+contrebasse-sib-1.pdf`)
|
||||||
|
- ✅ Parties combinées: utilise `_` (ex: `cor-mib-1_2.pdf`)
|
||||||
|
- ✅ Variantes: `solo` (ex: `clarinette-sib-solo-1.pdf`)
|
||||||
|
- ✅ Clefs: `clesol`, `clefa` (ex: `basse-sib-clesol-1.pdf`)
|
||||||
|
- ✅ Mots composés: `sax_alto`, `clarinette_basse`
|
||||||
|
- ✅ Pas de `-et-` (0 occurrences)
|
||||||
|
- ✅ Pas de `--` (0 occurrences)
|
||||||
814
scripts/convert_final_v2.js
Normal file
814
scripts/convert_final_v2.js
Normal file
@@ -0,0 +1,814 @@
|
|||||||
|
#!/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`);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user