Outils pour utilisateurs

Outils du site


renommer_mp3

Renommer des fichiers mp3 en fonction de leur tag id3

Objectif

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:

  • on peut mettre une dizaine de CD “normaux” sur un seul CD mp3
  • certains lecteurs affichent le nom du fichier comme titre de la chanson

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.

Code proposé commenté

Lecture des tags id3

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”.

Fonction principale

#!/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:

  • listefics(): retourne la liste des noms de fichier trouvés dans le répertoire rep
  • modifcar(): analyse le titre de la chanson et corrige les caractères selon les chaines de conversion sélectionnées.
  • affiche(): comme c'est un programme en console, “affiche()” permet d'afficher les caractères accentués dans la console en cours, avec le bon encodage
  • recuperreur(): en cas d'erreur, récupère et retourne le nom de l'erreur


Cette fonction commence par faire une proposition de renommage qu'on peut accepter ou refuser.

Exemple d'utilisation

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.

Lecture des fichiers du répertoire

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.

Modification des caractères

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:

  • si le caractère correspondant de ch2 est 'x', le caractère trouvé est simplement effacé du résultat (exemple: ':')
  • sinon, le caractère de ch2 remplace celui de ch1 dans le résultat (exemple: ' ' remplacé ici par '_')

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:

  • la page de code est éditée et enregistrée en utf-8, et il y a une ligne de coding qui le dit à Python: "# -*- coding:utf-8 -*-", ce qui fait que n'importe quel caractère du monde pourra être écrit dans ce code.
  • j'ai mis un 'r' devant les chaines ch1 et ch2 pour empêcher Python d'interpréter certains caractères comme '\'.
  • et j'ajoute la méthode décode() pour que ch1 et ch2 soient converties en unicode interne (UCS2). la variable globale “codesource='utf-8'” est nécessaire pour cela.
  • Et, bien sûr, la chaine passée en argument ch doit être en unicode interne.

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!).

Fonction d'affichage

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:

  • si c'est une chaine unicode, on la convertit dans l'encodage de la console avec encode()
  • sinon, il faut d'abord la convertir en unicode interne avec decode()

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)  

Fonction de récupération du code d'erreur

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])

Code complet

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!

renommer_mp3.txt · Dernière modification: 2010/01/21 08:02 par tyrtamos

Outils de la page