#!/usr/bin/env python3
# -*- encoding: utf-8 -*-
r"""
Created on 25 mars 2023
@author: Nicolas Bruschi
Exploite fichiers d'extension m3u dans les sous-répertoires du repertoire de
travail, pour constituer la playlist intégrale des fichiers mp3 classés.
[EN ENTREE]
[-h |--help : Demande usage] Optionnel
[-m |--mp3 : Verification existence fic mp3] Optionnel. Defaut = False
[-r |--repertoire] <repertoire de travail> Optionnel
defaut = Repertoire_travail
[-v |--version : Demande version] Optionnel
Tous les parametres acceptent casse minuscules/majuscules
[EN SORTIE]
0 OK # Constitution dans le repertoire de travail du fichier de sortie
1 KO
[VERSIONS]
[2023-03-25] BN V1.0 : Initialisation
[2023-03-26] BN V1.1 : Filtre les fichiers mp3 listés. Pylint.
Tests unitaires
[2023-03-28] BN V1.2 : Debug repertoire travail PureWindowsPath
[2023-03-29] BN V1.3 : issue 1-listemp3upy-sans-fichier-mp3
[2023-05-15] BN V1.4 : introduction 1 parametre OBLIGATOIRE
[2025-04-04] BN V1.5 : modification contenus fichiers .m3u
[2025-06-01] BN V1.6 : modification contenus fichiers .m3u
[2025-11-02] BN V1.6.1 : modification nom fichier .m3u
[2025-11-19] BN V1.9.0 : pipeline github action
[2025-11-23] BN V1.9.1 : suppression saut ligne après écriture + nb fic
[2025-12-07] BN V1.9.2 : ajout fct md5
[2025-12-09] BN V1.9.3 : revision format fichier m3u + sha512
# https://fr.wikipedia.org/wiki/M3U
[2025-12-12] BN V1.9.4 : mise au point
[2025-12-21] BN V1.9.6 : mise au point pipeline ci/cd
[2025-12-29] BN V1.9.7 : gestion fichier de sortie si identique précédent
[2026-02-15] BN V2.0.0 : Revue de code
[2026-02-23] BN V2.0.1 : Revue de code
[2026-03-03] BN V2.0.2 : Revue de code
[REFERENCES]
https://www.githubstatus.com/
https://www.sphinx-doc.org/fr/master/index.html
https://github.com/maltfield/rtd-github-pages/
https://docs.github.com/en/actions/
monitoring-and-troubleshooting-workflows/adding-a-workflow-status-badge
https://github.com/marketplace/actions/github-pages-action
https://github.com/marketplace/actions/sphinx-docs-to-github-pages
# pour memo : python3 -m http.server
# Le codage des fichiers m3u est en Latin-1
# https://docs.fileformat.com/fr/audio/m3u/
"""
# ************* Module src.listem3u
# src/listem3u.py:248: [W0621(redefined-outer-name), actionfinale]
# Redefining name 'coderetour' from outer scope (line 445)
## Bibliotheques ##
import sys
import getopt
import os
import unicodedata
import fnmatch
import hashlib
from datetime import datetime
from os.path import exists as file_exists
## Variables Globales ##
FILENAME = "listem3u.py"
VERSION = f"\n {FILENAME} version : [2026-03-03 BN V2.0.2]"
SEPARATEUR_REP = "\\"
REP_TRAV = f"P:{SEPARATEUR_REP}Morceaux_choisis"
USAGE = (
f"\n usage: {FILENAME} [OPTIONS]\n"
" OPTIONS:\n"
" [-h |--help : Demande usage] Optionnel\n"
" [-m |--mp3 : Verification existence fic mp3] Optionnel. Defaut = False\n"
" [-r |--repertoire] <repertoire de travail> OBLIGATOIRE.\n"
f" defaut si absent= {REP_TRAV}\n"
" [-v |--version : Demande version] Optionnel\n"
" Tous les parametres acceptent casse minuscules/majuscules.\n"
)
FICS_LISTE_TAMPON = "liste.m3u"
NOW = datetime.now()
FICS_LISTE = f"000-liste-{NOW.strftime('%d-%m-%Y')}.m3u"
FICS_LISTE_PROD = f"{FICS_LISTE}.prod"
DEFAUT_FICMP3 = False
DEFAUT_MENAGE = True
CODERETOUR = 0
### Fonctions ###
#################
[docs]
def hashlib_sha512(fname):
r"""
somme de controle sha512 d'un fichier
[ EN ENTREE ]
fname (chaine) fichier
[ EN SORTIE ]
somme_de_controle (chaine) sha512
"""
hash_sha512 = hashlib.sha512()
with open(fname, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_sha512.update(chunk)
return hash_sha512.hexdigest()
[docs]
def parametres(argv):
r"""
Gestion des parametres d'appel = repertoire, help et version
[ EN ENTREE ]
argv = Les parametres d'appel du script
[ EN SORTIE ]
codeexit (entier) 0, 1 ou 2
scom (chaine) commentaire
repertoire_travail (chaine)
test_presenceficmp3 (boolean)
"""
### parametre local
codeexit = 0
repertoire_travail = REP_TRAV # valeur defaut
oncontinue = 0 # si demande aide = 1 ou version = 10 ou les 2 = 11
scom = "" # commentaire si parametre imprevu
test_presenceficmp3 = DEFAUT_FICMP3 # val defaut
nb_param = 0
try:
# pylint: disable=unused-variable
options, _ = getopt.getopt(
argv[1:],
"hHmMvVr:R:",
[
"help",
"HELP",
"mp3",
"MP3",
"version",
"VERSION",
"repertoire=",
"REPERTOIRE=",
],
)
# print(f"debug param : {options},{remainder}\n")
for opt, arg in options:
nb_param += 1
if opt.upper() in ["-H", "--HELP"]:
oncontinue += 1
elif opt.upper() in ["-M", "--MP3"]:
test_presenceficmp3 = True
elif opt.upper() in ("-R", "--REPERTOIRE"):
# print(f"debug {arg}")
oncontinue = 0
repertoire_travail = arg
elif opt.upper() in ("-V", "--VERSION"):
oncontinue += 10
else:
### ne devrait pas passer là sans lever une exception
oncontinue = 2
scom = f"\n\t>>>>PARAMETRE(S): {opt}, {arg} IMPREVU(S)\n"
if nb_param == 0:
scom = f"\n\t>>>>PARAMETRE OBLIGATOIRE MANQUANT:\n{USAGE}\n"
codeexit = 1
else:
match oncontinue:
case 0:
if not os.path.exists(repertoire_travail):
scom = f"\n\t>>>>REPERTOIRE: {repertoire_travail} INACESSIBLE.\n"
codeexit = 1
else:
scom = f"\n\t>>>>REPERTOIRE CHOISI: {repertoire_travail} [OK]\n"
codeexit = 2
case 1:
scom = f"\n\t>>>>DEMANDE AIDE:\n{USAGE}\n"
# Ne devrait pas passer dans ce cas.
case 2:
codeexit = 1
case 10:
scom = f"\n\t>>>>DEMANDE VERSION:\n{VERSION}\n"
case 11:
scom = f"\n\t>>>>DEMANDE AIDE + VERSION:\n{USAGE}\n{VERSION}\n"
case _:
scom = "\n\t>>>>Fct parametres : Cas IMPREVU\n"
codeexit = 1
except getopt.GetoptError as error:
scom = f"\n\t>>>> ERREUR: {str(error)}\n{USAGE}"
codeexit = 1
return (codeexit, scom, repertoire_travail, test_presenceficmp3)
[docs]
def action(repert=None, fic_tampon=None, fic=None, testmp3=DEFAUT_FICMP3):
r"""
constitution du fichier de sortie dans le repertoire de travail
[ EN ENTREE ]
repert (chaine) répertoire de travail
fic_tampon (chaine) fichier de travail
fic (chaine) fichier resultat
testmp3 (boolean) DEFAUT_FICMP3
[ EN SORTIE ]
coderetourici (entier) 0 OK - 1 KO
sunecom (chaine) commentaire
"""
# pylint: disable=too-many-locals
### parametre local
nbrfics = 0
coderetourici = 0
sunecom = ""
# initial directory
# cwd = os.getcwd()
# print(f"DEBUG: {cwd}")
(coderetourici, fichiersmp3, sunecom) = _preprod(repert, fic_tampon, fic)
if coderetourici == 1:
print(f"\n\t>>>> Probleme _preprod : {sunecom}")
else:
# ecriture du resultat
# EXTM3U - Il s’agit de l’en-tête de fichier indiquant Extended M3U et doit
# être la première ligne du fichier.
# PLAYLIST : - Le titre de la playlist
with open(fic, "a", encoding="utf-8") as resultat:
# print("Debug:\n#EXTM3U\n#PLAYLIST:000\n")
resultat.write("#EXTM3U\n#PLAYLIST:000")
for elmt in fichiersmp3:
miseenforme = elmt.split("#")
### DO : revoir ce code avec
# os.path.join(REPERTOIRE,"000-liste-23-02-2026_ctrl.m3u")
lefich = (
f"{miseenforme[1].strip()}{SEPARATEUR_REP}{miseenforme[0].strip()}"
)
refhddfic = os.path.join(miseenforme[1].strip(), miseenforme[0].strip())
resultat.write(f"\n{lefich}")
nbrfics += 1
if testmp3 and not file_exists(refhddfic):
print(f"\n\t>>>> inexistant : {refhddfic}")
resultat.close()
sunecom = f"\n\t>>>> {nbrfics} fichiers dans {fic}\n"
return (coderetourici, sunecom)
[docs]
def actionfinale(repert, ficprod, coderetour, sunecom, supprfic):
r"""
Si le fichier produit par action est de meme signature que précédemment on ne
fait rien, sinon on produit le nouveau fichier avec son nom finalisé et on
supprime les anciens si spécifié (cf variable DEFAUT_MENAGE)
[ EN ENTREE ]
repert (chaine) répertoire de travail
ficprod (chaine) fichier resultat
ficfin (chaine) fichier resultat finalisé
coderetour (entier)
sunecom (chaine)
supprfic (booleen)
[ EN SORTIE ]
resultat (entier) 0 ou 1
scom (chaine) communication
"""
resultat = coderetour
scom = sunecom
leficprod = os.path.join(repert, ficprod)
leficref = ""
ancienfic000m3u = []
nbrancienfic = 0
renommer = False
if resultat == 0:
ancienfic000m3u = _find("000-liste-*.m3u", repert)
nbrancienfic = len(ancienfic000m3u)
if nbrancienfic > 0:
for fichier in ancienfic000m3u:
leficref = os.path.join(repert, fichier)
if hashlib_sha512(leficprod) != hashlib_sha512(leficref):
# on supprime si volonté
if supprfic:
os.unlink(leficref)
nbrancienfic -= 1
if nbrancienfic == 0:
renommer = True
elif supprfic:
os.unlink(leficprod)
scom += "\n\t>>>> Aucune difference de production.\n"
else:
renommer = True
if renommer:
try:
os.rename(
os.path.join(repert, ficprod), os.path.join(repert, FICS_LISTE)
)
except OSError as err:
resultat = 1
scom += (
f"\n\t>>>> Probleme renommage {repert}/{ficprod} en "
+ f"{repert}/{FICS_LISTE}\n. {err}\n"
)
return (resultat, scom)
### Sous Fonctions ###
######################
def _preprod(repert=None, fic_tampon=None, fic=None):
r"""
repertoire utile = repertoire production + gestion anciens fichiers
[ EN ENTREE ]
repert (chaine) répertoire de travail
fic_tampon (chaine) fichier de travail
fic (chaine) fichier resultat
[ EN SORTIE ]
n_resultat (entier) 0 ou 1
fichiersmp3 (liste) classee
scom (chaine) communication
"""
n_resultat = 0
scom = ""
ficfiltre = ""
ssrep = ""
fichiersmp3 = []
try:
os.chdir(repert)
# menage fichiers si existants
if file_exists(fic_tampon):
os.unlink(fic_tampon)
if file_exists(fic):
os.unlink(fic)
except (FileNotFoundError, NotADirectoryError, PermissionError):
n_resultat = 1
## Cette commentée ligne provoque un Quality Gate Failed sur sonarqube
## Operators should be used on compatible types python:S5607
## scom = f"Something wrong with specified" + \
## f" directory {repert}. Exception." + sys.exc_info()
scom = "Something wrong with specified" + f" directory {repert}. Exception."
if n_resultat == 0:
# trouve tous les fichiers de nom contenant -Playlist.m3u sous ./
ficm3u = _find("*-Playlist.m3u", "./")
# traite chaque fichier de nom contenant -Playlist.m3u sous repert
for fichier in ficm3u:
ssrep = os.path.dirname(fichier).split("/")[-1]
# print(f"debug action : {ssrep}")
with open(fichier, "r", encoding="utf-8") as lefic:
for ligne in lefic:
if _estexploitable(ligne):
# filtre sur ligne contenant des blancs...
ficfiltre = _filtreligne(ligne, ssrep)
# mention du ssrep...
miseenforme = f"{ficfiltre} # {ssrep}"
fichiersmp3.append(miseenforme)
lefic.close()
# on classe selon ordre alphabetic des chaines considérées en minuscules
fichiersmp3.sort(key=str.lower)
# print(f"debug {fichiersmp3}")
return (n_resultat, fichiersmp3, scom)
def _find(pattern, path):
r"""
Trouve les fichiers selon pattern sous path
[ EN ENTREE ]
pattern (chaine) recherche de fichier
path (chaine) repertoire
[ EN SORTIE ]
result (tableau de chaine) basename + fichier
"""
### parametre local
result = []
# pylint: disable=unused-variable
for root, _dirs, files in os.walk(path):
result.extend(
os.path.join(root, basename)
for basename in files
if fnmatch.fnmatch(basename, pattern)
)
return result
def _estexploitable(unechaine=None):
r"""
Pour ne pas avoir à traiter ensuite les lignes vides ou commentées
[ EN ENTREE ]
unechaine (chaine) une ligne du fichier m3u
[ EN SORTIE ]
booleen True ou False
"""
bretour = True
if unechaine is None or len(unechaine.strip()) == 0:
bretour = False
else:
tamp = unechaine.strip()
if tamp.startswith("#"):
bretour = False
return bretour
def _filtreligne(unechaine=None, ssrep=None):
r"""
Filtre une ligne de fichier m3u, alerte si contient un espace, ou plus
d'un tiret ou caractère imprévu et renvoie nom du fichier mp3
[ EN ENTREE ]
unechaine (chaine) une ligne du fichier m3u
ssrep (chaine) un repertoire
[ EN SORTIE ]
unechaine (chaine) nom du fichier mp3 retourné
suppression trailing-space.
alerte si contient un espace.
"""
pattern = (
unicodedata.normalize("NFKD", unechaine)
.encode("ascii", "ignore")
.decode("ascii")
)
# print(f"debug : {pattern}")
### parametre local
if pattern != unechaine:
print(f"\n\t>>>> Au moins un caractere imprevu : {ssrep} # {unechaine}")
elif " " in unechaine:
print(f"\n\t>>>> au moins un espace : {ssrep} # {unechaine}")
elif unechaine.count("-") > 1:
print(f"\n\t>>>> plus d'1 tiret : {ssrep} # {unechaine}")
else:
tamp = unechaine.split("-")
if tamp[0].capitalize() != tamp[0]:
print(f"\n\t>>>> majuscules : {ssrep} # {unechaine}")
return unechaine.strip()
### Principal ####
##################
# pragma: no cover
if __name__ == "__main__":
(CODERETOUR, SCOM, REP, TEST_PRESENCEFICMP3) = parametres(sys.argv)
if CODERETOUR == 2:
print(SCOM)
(CODERETOUR, SCOM) = action(
REP, FICS_LISTE_TAMPON, FICS_LISTE_PROD, TEST_PRESENCEFICMP3
)
(CODERETOUR, SCOM) = actionfinale(
REP, FICS_LISTE_PROD, CODERETOUR, SCOM, DEFAUT_MENAGE
)
print(SCOM)
sys.exit(CODERETOUR)