J'ai un lecteur de CD audio qui supporte les CD mp3. Les 2 avantages de transformer ses CD (achetés!) en CD mp3 sont:
C'est le cas de mon autoradio, et j'y trouve en conséquence un avantage supplémentaire: les changements de CD peuvent devenir moins fréquents, ce qui donne un avantage de sécurité dans la conduite du véhicule.
Donc, et c'est l'objectif de cette page: il faut pouvoir lire le tag id3 de chaque fichier mp3 et s'en servir pour le renommer!
De plus, certains lecteurs mp3 ne supportent pas les espaces, ou les caractères accentués, etc… Et les titres de chansons figurant dans les tags id3 comportent quelquefois des caractères interdits dans les noms de fichiers (comme '“' ou '/' ou même '\' par exemple). Donc, il faut ajouter une fonction de modification des caractères.
Si les tags id3 des fichiers mp3 sont vides, on peut les remplir avec certains logiciels de lecture audio comme winamp. Celui-ci questionne par internet une base de données et trouve (en général) le bon album et les bons titres. Ce même logiciel est capable également de renommer les fichiers mp3 en même temps que la conversion mp3, mais cette conversion mp3 n'est possible que si on a payé la version pro.
J'ai utilisé le module id3reader, qu'on peut trouver ici: http://nedbatchelder.com/code/modules/id3reader.html
Pour l'installer, il suffit simplement de le mettre dans l'un des emplacements où Python pourra le trouver en temps que module, y compris dans le répertoire du programme.
D'après l'auteur, on peut lire les tags: ID3v1, ID3v2.2, ID3v2.3, or ID3v2.4.
Il dit aussi qu'il n'assure plus la maintenance du module depuis mars 2006, et je ne sais donc pas jusqu'à quel version de Python ça marchera. En tout cas, il marche encore avec Python v2.6.4.
Voilà un résumé de sa syntaxe:
import id3reader id3 = id3reader.Reader('fichier.mp3') id3.getValue('album') # = album id3.getValue('performer') # = auteur id3.getValue('title') # = titre id3.getValue('track') # = numéro de piste id3.getValue('year') # = année
D'après ce que j'ai expérimenté, tout est en chaine unicode. Y compris le numéro de piste ('track') que je préfère pour ma part transformer en entier pour le formater plus facilement.
En cas d'un fichier mp3 qui ne comporterait pas de tag id3, l'erreur est détectée dans le code par le fait que le numéro de piste est “None”.
#!/usr/bin/python # -*- coding:utf-8 -*- import os import id3reader def renommemp3(rep): """Renomme les fichiers mp3 du répertoire rep (unicode) selon leur tag id3""" # lecture des fichiers du répertoire try: fics = listefics(rep, selections=["*.mp3", "*.MP3"], exclusions=[""]) except: affiche(recuperreur()) return None # propositions de renommage nfr = [] # pour contenir tous les noms modifiés for nf in fics: nfc = os.path.join(rep, nf) id3 = id3reader.Reader(nfc) piste = id3.getValue('track') if piste == None: affiche(u"Pas de tag id3 dans le fichier", nf) nfr.append(None) continue else: titre = id3.getValue('title') nf2 = u"%02d-%s" % (int(piste), titre) nf2 = modifcar(nf2) + ".mp3" nfr.append(nf2) affiche("proposition:", nf, " ==> ", nf2) affiche() r = raw_input("Ok? (o/n)") # renommer if r in ['o', 'O', 'oui', 'OUI', 'Oui']: for i in xrange(0,len(fics)): if nfr[i] != None: nfc = os.path.join(rep, fics[i]) nfc2 = os.path.join(rep, nfr[i]) try: os.rename(nfc, nfc2) affiche("renommé:", fics[i], " => ", nfr[i]) except: affiche(recuperreur()) break else: affiche("arrêt demandée du programme de renommage")
Voilà les fonctions que ce code utilise et que nous allons définir ci-dessous:
Cette fonction commence par faire une proposition de renommage qu'on peut accepter ou refuser.
Voilà comment on peut utiliser la fonction ci-dessus:
import sys, os codin = sys.stdin.encoding # encodage de l'entrée console affiche(u"Programme de renommage des mp3 en fonction de leur tag id3") while True: affiche() affiche(u"Dans quel répertoire? (touche 'Entrée' pour terminer)") rep = raw_input().decode(codin) if rep == "": break else: rep = os.path.normpath(rep) if os.path.exists(rep) and os.path.isdir(rep): renommemp3(rep) else: affiche(u"Désolé, ce répertoire est introuvable") affiche(u"fin du programme de renommage")
Et voilà un exemple de renommage. Il s'agit du CD1 du best-off de Jacques Brel: “les 100 plus belles chansons” (une jolie boite en métal avec 5 CD):
Pour donner le répertoire à traiter, soyez simple: faites du copier-coller avec votre navigateur de fichier (windows explorer sous Windows)
Programme de renommage des mp3 en fonction de leur tag id3 Dans quel répertoire? (touche 'Entrée' pour terminer) D:\Musiques\CD_MP3\Jacques_Brel_1 proposition: 01.mp3 ==> 01-Amsterdam.mp3 proposition: 02.mp3 ==> 02-Grand_Jacques_(C'est_trop_facile).mp3 proposition: 03.mp3 ==> 03-Il_pleut_Les_carreaux.mp3 proposition: 04.mp3 ==> 04-Le_diable_Ca_va.mp3 proposition: 05.mp3 ==> 05-Il_nous_faut_regarder.mp3 proposition: 06.mp3 ==> 06-Il_peut_pleuvoir.mp3 proposition: 07.mp3 ==> 07-Quand_on_n'a_que_l'amour.mp3 proposition: 08.mp3 ==> 08-Les_pieds_dans_le_ruisseau.mp3 proposition: 09.mp3 ==> 09-La_bourree_du_celibataire.mp3 proposition: 10.mp3 ==> 10-J'en_appelle.mp3 proposition: 11.mp3 ==> 11-Heureux.mp3 proposition: 12.mp3 ==> 12-Au_printemps.mp3 proposition: 13.mp3 ==> 13-Je_ne_sais_pas.mp3 proposition: 14.mp3 ==> 14-La_lumiere_jaillira.mp3 proposition: 15.mp3 ==> 15-Voir.mp3 proposition: 16.mp3 ==> 16-L'aventure.mp3 proposition: 17.mp3 ==> 17-La_valse_a_mille_temps.mp3 proposition: 18.mp3 ==> 18-Seul.mp3 proposition: 19.mp3 ==> 19-Je_t'aime.mp3 proposition: 20.mp3 ==> 20-Les_Flamandes.mp3 Ok? (o/n)o renommé: 01.mp3 => 01-Amsterdam.mp3 renommé: 02.mp3 => 02-Grand_Jacques_(C'est_trop_facile).mp3 renommé: 03.mp3 => 03-Il_pleut_Les_carreaux.mp3 renommé: 04.mp3 => 04-Le_diable_Ca_va.mp3 renommé: 05.mp3 => 05-Il_nous_faut_regarder.mp3 renommé: 06.mp3 => 06-Il_peut_pleuvoir.mp3 renommé: 07.mp3 => 07-Quand_on_n'a_que_l'amour.mp3 renommé: 08.mp3 => 08-Les_pieds_dans_le_ruisseau.mp3 renommé: 09.mp3 => 09-La_bourree_du_celibataire.mp3 renommé: 10.mp3 => 10-J'en_appelle.mp3 renommé: 11.mp3 => 11-Heureux.mp3 renommé: 12.mp3 => 12-Au_printemps.mp3 renommé: 13.mp3 => 13-Je_ne_sais_pas.mp3 renommé: 14.mp3 => 14-La_lumiere_jaillira.mp3 renommé: 15.mp3 => 15-Voir.mp3 renommé: 16.mp3 => 16-L'aventure.mp3 renommé: 17.mp3 => 17-La_valse_a_mille_temps.mp3 renommé: 18.mp3 => 18-Seul.mp3 renommé: 19.mp3 => 19-Je_t'aime.mp3 renommé: 20.mp3 => 20-Les_Flamandes.mp3 Dans quel répertoire? (touche 'Entrée' pour terminer) fin du programme de renommage
Dans ce cas, par rapport au contenu des tags id3, le programme a par exemple transformé les espaces en '_', éliminé les accents ('é' ⇒ 'e') et supprimé les '”' qui auraient déclenché une erreur au renommage.
La phase de la proposition est là pour ça: il est possible que ces tags id3 contiennent encore des caractères interdits ou non souhaités dans les noms de fichier. Il faudra dans ce cas ajouter les corrections à faire dans le code de la fonction modifcar(). Et si, après acceptation, une erreur de renommage stoppe l'exécution en cours, ce n'est pas grave: il faut comprendre pourquoi (quel caractère interdit ais-je oublié?), ajouter la nouvelle correction à faire dans les ch1 et ch2 de la fonction modifcar() et recommencer.
import os from fnmatch import fnmatch #========================================================================== def okselect(nf, selections=["*"], exclusions=[""]): """renvoie True si un nom correspond à un motif de selection, sans être interdit par un motif d'exclusion """ for selection in selections: if fnmatch(nf,selection): # on a trouve un motif de selection qui marche for exclusion in exclusions: if fnmatch(nf,exclusion): # mais un motif d'exclusion l'interdit return False return True # une selection marche sans exclusion: c'est ok return False # aucun motif de selection ne marche #========================================================================== def listefics(rep, selections=["*"], exclusions=[""]): """renvoie la liste (unicode) des fichiers du répertoire rep (unicode) correspondant à la sélection-exclusion """ try: entrees = os.listdir(rep) # rep doit avoir le bon encodage (=unicode) except: raise ValueError ("Erreur: le répertoire n'existe pas ou droits insuffisants") entrees.sort() # tri alphabétique des noms (si tri français: créer fonction de comparaison) fics = [] for entree in entrees: if os.path.isfile(os.path.join(rep, entree)) and \ okselect(entree, selections, exclusions): fics.append(entree) return fics
La fonction listefics() renvoie la liste des fichiers du répertoire qui satisfont aux liste de sélections/exclusions (ici, mp3 par défaut).
Cette fonction utilise la fonction okselect() qui renvoie True ou False selon que le nom de fichier correspond ou non aux sélections/exclusions.
La fonction modifcar() prend la chaine ch en argument, analyse chacun de ses caractères, et si le caractère se trouve dans ch1, le modifie en fonction du caractère de ch2 de même indice:
Les chaines ch1 et ch2 peuvent (doivent) être adaptées aux spécificités du renommage à effectuer. Le minimum est d'effacer ou de remplacer tous les caractères qui se trouveraient dans les infos id3, et qui seraient interdits en tant que nom de fichier, tant sur le système d'exploitation en cours (Windows, Linux, Mac, …) que sur le lecteur mp3 “cible”. Au delà (espace ou non, caractères accentués ou non, etc…), ça ne dépend que de ce lecteur mp3.
Pour que ce code puisse traiter n'importe quel caractère, y compris des caractères français rares comme É, æ, Æ, œ, Œ, ÿ, Ÿ, € (,etc…), j'ai adopté les règles suivantes:
J'aurais normalement pu écrire les chaines directement avec ur“…”, mais… ça ne marche pas correctement. En effet, malgré le 'r', les chaines précédées de 'u' interprètent les '\'.
codesource = 'utf-8' # doit être identique à la ligne coding du code source def modifcar(ch): """Renvoie la chaine ch avec les caractères de ch1 remplacés par ceux de ch2 (si 'x' dans ch2: suppression simple) la chaine ch doit être en unicode """ global codesource ch1 = r''' /&"?.,:;!àÀâÂçÇéÉèÈêÊëËîÎïÏôÔùÙûÛüÜÿŸ'''.decode(codesource) ch2 = r'''_-xxxxxxxxaAaAcCeEeEeEeEiIiIoOuUuUuUyY'''.decode(codesource) s = "" for c in ch: i = ch1.find(c) if i >= 0: if ch2[i] != 'x': s += ch2[i] else: s += c return s
On pourrait facilement modifier ce code pour lui faire traiter des situations plus complexes. Par exemple que le 'æ' soit transformé en 'ae', donc qu'un seul caractère en donne 2. Il faudrait, bien entendu, remplacer les chaines ch1 et ch2 par des listes de chaines, ch1 devenant une liste de caractères. Par contre, pour trouver avec ch1 des 'motifs' composés de plusieurs caractères, ce serait un peu plus compliqué à faire (et cependant faisable!).
Cette fonction d'affichage remplace le print, en corrigeant les encodages pour un affichage correct dans la console en cours.
Grâce à l'argument '*ch', on peut l'utiliser comme print: affiche(chaine1, chaine2, chaine3)
Comme le présent code s'exécute en console, il faut prévoir que la console utilisée a un encodage spécifique, donc une représentation spécifique des mêmes caractères, voire un échec dans l'affichage qui stoppe le programme. Par exemple, la console DOS de Windows a un encodage 'cp850', la console Linux est en général en 'utf-8', etc… Et l'encodage de la console en cours est donnée par sys.stdout.encoding.
Selon le type de la chaine passée, on adopte le principe suivant:
Attention: une chaine encodée en 'utf-8' n'a pas le type 'unicode' en Python, mais 'str'.
codesource = 'utf-8' # doit correspondre a la ligne coding du code source codout = sys.stdout.encoding def affiche(*ch): """Affiche les chaines de caractères de la page de code sur la console avec le bon encodage """ global codesource, codout for x in ch: if isinstance(x,str): print x.decode(codesource).encode(codout, 'replace'), elif isinstance(x,unicode): print x.encode(codout, 'replace'), else: print x, print
Bien entendu, si on devait, pour un autre usage, utiliser cette même fonction pour afficher une chaine avec un autre encodage, par exemple 'latin-1', sans que ça coïncide avec le coding du code (chaine issue d'un fichier disque par exemple), il faudrait d'abord convertir cette chaine en unicode interne avec decode(). Par exemple pour la chaine 'chaine2':
affiche(chaine1, chaine2.decode('latin-1'), chaine3)
En cas d'erreur, cette fonction récupère le message pour affichage.
import sys def recuperreur(): """renvoie le message d'erreur (unicode) généré par une exception""" return u"%s" % (sys.exc_info()[1])
Voilà le code complet pouvant être récupéré en copier-coller:
#!/usr/bin/python # -*- coding:utf-8 -*- import os import sys from fnmatch import fnmatch import id3reader ############################################################################## # initialisation des variables globales # codesource = 'utf-8' # doit correspondre a la ligne coding du code source codin = sys.stdin.encoding # encodage de l'entrée console codout = sys.stdout.encoding # encodage de la sortie console ############################################################################## def recuperreur(): """renvoie le message d'erreur (unicode) généré par une exception""" return u"%s" % (sys.exc_info()[1]) ############################################################################## def affiche(*ch): """Affiche les chaines de caractères utf-8 sur la console avec le bon encodage""" global codesource, codout for x in ch: if isinstance(x,str): print x.decode(codesource).encode(codout, 'replace'), elif isinstance(x,unicode): print x.encode(codout, 'replace'), else: print x, print ############################################################################## def okselect(nf, selections=["*"], exclusions=[""]): """renvoie True si un nom correspond à un motif de selection, sans être interdit par un motif d'exclusion """ for selection in selections: if fnmatch(nf,selection): # on a trouve un motif de selection qui marche for exclusion in exclusions: if fnmatch(nf,exclusion): # mais un motif d'exclusion l'interdit return False return True # une selection marche sans exclusion: c'est ok return False # aucun motif de selection ne marche ############################################################################## def listefics(rep, selections=["*"], exclusions=[""]): """renvoie la liste (unicode) des fichiers du répertoire rep (unicode) correspondant à la sélection-exclusion """ try: entrees = os.listdir(rep) # rep doit avoir le bon encodage (=unicode) except: raise ValueError ("Erreur: le répertoire n'existe pas ou droits insuffisants") entrees.sort() # tri alphabétique des noms (si tri français: créer fonction de comparaison) fics = [] for entree in entrees: if os.path.isfile(os.path.join(rep, entree)) and \ okselect(entree, selections, exclusions): fics.append(entree) return fics ############################################################################## def modifcar(ch): """Renvoie la chaine ch avec les caractères de ch1 remplacés par ceux de ch2 (si 'x' dans ch2: suppression simple) la chaine ch doit être en unicode """ global codesource ch1 = r''' /&"?.,:;!àÀâÂçÇéÉèÈêÊëËîÎïÏôÔùÙûÛüÜÿŸ'''.decode(codesource) ch2 = r'''_-xxxxxxxxaAaAcCeEeEeEeEiIiIoOuUuUuUyY'''.decode(codesource) s = "" for c in ch: i = ch1.find(c) if i >= 0: if ch2[i] != 'x': s += ch2[i] else: s += c return s ############################################################################## def renommemp3(rep): """Renomme les fichiers mp3 du répertoire rep (unicode) selon leur tag id3""" # lecture des fichiers du répertoire try: fics = listefics(rep, selections=["*.mp3", "*.MP3"], exclusions=[""]) except: affiche(recuperreur()) return None # propositions de renommage nfr = [] # pour contenir tous les noms modifiés for nf in fics: nfc = os.path.join(rep, nf) id3 = id3reader.Reader(nfc) piste = id3.getValue('track') if piste == None: affiche(u"Pas de tag id3 dans le fichier", nf) nfr.append(None) continue else: titre = id3.getValue('title') nf2 = u"%02d-%s" % (int(piste), titre) nf2 = modifcar(nf2) + ".mp3" nfr.append(nf2) affiche("proposition:", nf, " ==> ", nf2) affiche() r = raw_input("Ok? (o/n)") # renommer if r in ['o', 'O', 'oui', 'OUI', 'Oui']: for i in xrange(0,len(fics)): if nfr[i] != None: nfc = os.path.join(rep, fics[i]) nfc2 = os.path.join(rep, nfr[i]) try: os.rename(nfc, nfc2) affiche("renommé:", fics[i], " => ", nfr[i]) except: affiche(recuperreur()) break else: affiche("arrêt demandée du programme de renommage") ############################################################################## affiche(u"Programme de renommage des mp3 en fonction de leur tag id3") while True: affiche() affiche(u"Dans quel répertoire? (touche 'Entrée' pour terminer)") rep = raw_input().decode(codin) if rep == "": break else: rep = os.path.normpath(rep) if os.path.exists(rep) and os.path.isdir(rep): renommemp3(rep) else: affiche(u"Désolé, ce répertoire est introuvable") affiche(u"fin du programme de renommage")
Amusez-vous bien!