"""Python 3.6 - GUI Themed Tkinter pour créer des fichiers playlists pour Volumio 2.x à partir de palylists m3u étendues et/ou xspf. Ne traite que des .m3u étendues (#EXTM3U en 1ere ligne) sans lignes de commentaires (#EXTREM:) et dont les lignes infos (#EXTINF:) sont dites 'standard' (Durée,Artiste - Titre). Les fichiers m3u et xspf testés venaient de vlc. Les fichiers audio doivent être pointés en relatif ou absolu, ils doivent alors être situés dans un dossier source unique. Adapter le champ ".." en conséquence. Le champ "USB/USB-Music" doit être adapté au chemin qui permet à Volumio de trouver vos chansons. Les playlists crées doivent être copiées dans le dossier "/data/playlist" de Volumio 2x. Il peut être pratique de partager ce dossier en lui accordant tous les droits comme "/data/INTERNAL" et en éditant le fichier "/etc/samba/smb.conf" de Volumio 2x. Merci à smoneck pour l'idée : https://github.com/suuuehgi/m3u2volumio En attandant le plugin qui fera le job :) : https://github.com/volumio/Volumio2/pull/1723 Laurent SIMEON""" from pathlib import Path # accès aux systèmes de fichier linux et windows from sys import argv # arguments ligne de commande from urllib.parse import unquote # déquoter (%20 etc) l'url des chemins des chansons (xspf) import xml.etree.ElementTree as ET # parser un fichier xml (xspf) import tkinter as tk # widgets Tcl/Tk pour le GUI from tkinter import ttk # Themed widgets from tkinter import filedialog # boites de dialogues fichier et dossier from tkinter import messagebox # boites de messages #==========Classe GUI======================= class PLtoVPL_gui(ttk.Frame): def __init__(self, frm=None): # construit la fenètre avec un cadre et l'affiche super().__init__(frm) frm.title("PLtoVolumioPL") # titre de la fenètre frm.geometry("650x720") # taille de la fenêtre à l'ouverture x-y frm.grid() # mode de placement colonne/ligne des widgets dans la fenêtre # le cadre FICHIERS==========================================================1 cadreFichiers = ttk.LabelFrame(frm, text=" Playlists ") cadreFichiers.grid(column=0, row=0, sticky='EW', padx=5, pady=5, ipadx=5, ipady=5) # le label self.lblFichier = tk.StringVar() # Affiche le dossier ou le fichier à traiter self.lblFichier.set(Path.cwd()) # valeur par defaut : le dossier courant lblF = ttk.Label(cadreFichiers, textvariable=self.lblFichier, anchor='w', foreground='black', background='white') lblF.grid(column=0, row=0, sticky='EW', padx=5, pady=5) # le bouton self.btcUnfichier = ttk.Button(cadreFichiers, text=" ... ", command=selectFichier) self.btcUnfichier.grid(column=1, row=0, padx=5, pady=5, ipadx=15) # le checkbox self.chkToutf = tk.StringVar() self.chkToutf.set('oui') chkF = ttk.Checkbutton(cadreFichiers, variable=self.chkToutf, text=" All playlists in folder ", onvalue='oui', offvalue='non', command=optionFichier) chkF.grid(column=0, row=1, sticky='W', padx=5) # les boutons d'options dans un cadre self.cadreBtcradio = ttk.Frame(cadreFichiers) self.cadreBtcradio.grid(column=0, row=2, sticky='EW', padx=45) self.vals = ['A', 'B', 'C'] etiqs = ['m3u ', 'xspf ', 'both'] self.btcOpt = tk.StringVar() self.btcOpt.set('C') for i in range(3): self.btcOptbout = ttk.Radiobutton(self.cadreBtcradio, variable=self.btcOpt, text=etiqs[i], value=self.vals[i]) self.btcOptbout.grid(column=(i), row=0, sticky='EW') # le btc Voir dans les PL self.btcVoir = ttk.Button(cadreFichiers, text=" View\nin playlist ", command=voirPL) self.btcVoir.grid(column=1, row=1, rowspan=2, sticky='W', padx=5, pady=5, ipadx=15) # le cadre CHEMIN DES FICHERS AUDIO et le btc Voir les PL====================2 cadreOptions = ttk.LabelFrame(frm, text= " Source Path of audio files in playlists ") cadreOptions.grid(column=0, row=1, sticky='EW', padx=5, pady=5, ipadx=5, ipady=5) # les labels lblChsource = ttk.Label(cadreOptions, text="Replace :") lblChsource.grid(column=0, row=0, sticky='E', padx=5, pady=5) lblChdestin = ttk.Label(cadreOptions, text="by :") lblChdestin.grid(column=0, row=1, sticky='E', padx=5, pady=5) # les textbox Entry self.txtChsource = tk.StringVar() # Mémorise la chaine à remplacer self.txtChsource.set('..') # valeur par defaut txtChS = ttk.Entry(cadreOptions, textvariable=self.txtChsource, justify='left', foreground='black', background='white') txtChS.grid(column=1, row=0, sticky='E,W', padx=5, pady=5) self.txtChdestin = tk.StringVar() # Mémorise la chaine à remplacer self.txtChdestin.set('USB/USB-Music') # valeur par defaut txtChD = ttk.Entry(cadreOptions, textvariable=self.txtChdestin, justify='left', foreground='black', background='white') txtChD.grid(column=1, row=1, sticky='E,W', padx=5, pady=5) # Un Canvas pour accueillir le logo du GUI self.canZone = tk.Canvas(cadreOptions, width =80, height =70) self.canZone.grid(column=2, row=0, rowspan=2, sticky='N,S,E,W', padx=20) #self.canZone['borderwidth'] = 1 #self.canZone['relief'] = 'solid' # le cadre DOSSIER DES PLAYLISTS VOLUMIO=====================================3 cadreDossier = ttk.LabelFrame(frm, text= " Volumio playlists folder ") cadreDossier.grid(column=0, row=2, sticky='EW', padx=5, pady=5, ipadx=5, ipady=5) # le label self.lblDoscible = tk.StringVar() # Affiche le dossier cible des PL volumio self.lblDoscible.set(Path.cwd() / 'VolumioPL') # dossier cible par defaut lblD = ttk.Label(cadreDossier, textvariable=self.lblDoscible, anchor='w', foreground='black', background='white') lblD.grid(column=0, row=0, sticky='EW', padx=5, pady=5) # le bouton btcUndossier = ttk.Button(cadreDossier, text=" ... ", command=dossierCiblePL) btcUndossier.grid(column=1, row=0, padx=5, pady=5, ipadx=15) # les boutons ? et CREER=====================================================4 # le cadre contenant frmCmd = ttk.Frame(frm) frmCmd.grid(column=0, row=3, sticky='EW', padx=130) # les boutons btcInf = ttk.Button(frmCmd, text=" ? ", command=infos) btcInf.grid(column=0, row=0, sticky='W', padx=5, pady=5, ipadx=15) btcCre = ttk.Button(frmCmd, text=" Create ", command=btcCreer) btcCre.grid(column=1, row=0, sticky='E', padx=5, pady=5, ipadx=15) # la zone de MESSAGE en Text+ScrollbarV======================================5 # le cadre contenant les 2 widgets cadreMessage=ttk.Frame(frm) cadreMessage.grid(column=0, row=4, sticky='N,S,E,W', padx=5, pady=5) #, ipadx=5, ipady=5) # cadreMessage['borderwidth'] = 1 # cadreMessage['relief'] = 'solid' # Le Text self.txtMessage = tk.Text(cadreMessage, wrap='word', font=("", 10)) self.txtMessage.insert('end', 'Ready...\n') self.txtMessage.grid(column=0,row=0, sticky='N,S,E,W', ipadx=5, ipady=5) # La scrollbar scrMessBar = ttk.Scrollbar(cadreMessage) scrMessBar.grid(column=1,row=0, sticky='N,S,W') # Les connections pour srcoller la zone Text self.txtMessage.config(yscrollcommand= scrMessBar.set) scrMessBar.config(command= self.txtMessage.yview) # Autodimensionne les grilles de la fenêtre et des cadres frm.grid_columnconfigure(0, weight=1) frm.grid_rowconfigure(4, weight=1) cadreFichiers.grid_columnconfigure(0, weight=1) self.cadreBtcradio.grid_rowconfigure(0, weight=1) cadreOptions.grid_columnconfigure(1, weight=20) cadreDossier.grid_columnconfigure(0, weight=1) cadreMessage.grid_columnconfigure(0, weight=1) cadreMessage.grid_rowconfigure(0, weight=1) frmCmd.grid_columnconfigure(0, weight=1) # Autorise le redimensionnement H et V de la fenêtre pricipale # maximise tout de même en cliquant le btc maximiser sous Cinnamon... frm.resizable(True, True) frm.minsize(650,720) #==========Variables globales========================================================= listM3U = [] # contient les chemins de fichier m3u listXSPF = [] # contient les chemins de fichier xspf imgobj =[] # contient en [0] la ref de l'image issue de photoimage #==========Fonctions================================================================== def fichiers_M3u_Xspf (rep="", fic=""): if not fic: # si on traite un dossier for f in Path(rep).glob('*.*'): # parcours les fichiers du dossier ext = Path(f).suffix.lower() # recup de l'extension du fichier en minuscule if ext == '.m3u': # est-ce un .m3u data = ouvertureFichier (f) # lit le fichier dans une liste if data[0] == '#EXTM3U\n': # est-ce un m3u étendu a = 0 for i in range(1, len(data), 2): if not '#EXTINF:' in data[i]: # présence des infos étendues a = a + 1 if a == 0: listM3U.append (f) # construit la liste des fichiers à traiter else: app.txtMessage.insert('end', '\t' + str(f) + ' will be ignored. Not an extented m3u file.\n') else: app.txtMessage.insert('end', '\t' + str(f) + ' will be ignored. Not an extented m3u file.\n') elif ext == '.xspf': # est-ce un .xspf data = ouvertureFichier (f) # lit le fichier dans une liste if 'playlist' in data[1]: # est-ce un xml playlist listXSPF.append (f) # construit la liste des fichiers à traiter else: app.txtMessage.insert('end', '\t' + str(f) + ' will be ignored. Invalide xspf file.\n') else: #si on ne traite qu'un fichier ext = Path(fic).suffix.lower() # recup de l'extension du fichier en minuscule if ext == '.m3u': # est-ce un .m3u data = ouvertureFichier (fic) # lit le fichier dans une liste if data[0] == '#EXTM3U\n': # est-ce un m3u étendu a = 0 for i in range(1, len(data), 2): if not '#EXTINF:' in data[i]: # présence des infos étendues a = a + 1 if a == 0: listM3U.append (fic) # renseigne la liste m3u traite_M3U(listM3U) # traite le fichier else: app.txtMessage.insert('end', '\t\twill be ignored. Not an extented m3u file.\n') else: app.txtMessage.insert('end', '\t\twill be ignored. Not an extented m3u file.\n') elif ext == '.xspf': # est-ce un .xspf data = ouvertureFichier (fic) # lit le fichier dans une liste if 'playlist' in data[1]: # est-ce un xml playlist listXSPF.append (fic) # reneigne la liste xspf traite_XSPF(listXSPF) # traite le fichier else: app.txtMessage.insert('end', '\t\twill be ignored. Invalide xspf file.\n') def prepareDonnees_m3u (fichier, chsrc="", chrpl=""): tmp = ouvertureFichier(fichier) # lit le fichier dans une liste data = [] try: for nb in range(1, len(tmp)): # ne prend pas l'index 0 car vaut #EXTM3U\n if '#EXTINF:' in tmp[nb]: # si ligne info, ne prend qu'après la ',' # et suprime les \n en fin de ligne # recup 'artiste - titre' derrière la 1ere ',' peut y avoir +ieurs ',' test = tmp[nb].split(',',1)[1].strip() # les champs artiste et titre ne doivent pas être vides, IndexError test.split(' - ')[0].strip() test.split(' - ')[1].strip() # on renseigne data.append(test) else: # adapte le chemin et suprime les \n en fin de ligne tmp[nb] = tmp[nb].replace(chsrc, chrpl).strip() data.append(tmp[nb].strip('file:/')) # strip file:/ au cas où présent app.txtMessage.insert('end', 'File ' + str(Path(fichier).name) + ' contains ' + str(int(len(data)/2)) + ' songs.\n') # check si pas de "guillemet droit" car séparateur dans playlists volumio ! data = checksyntaxe_volumioPL (data) except IndexError: data = [] app.txtMessage.insert('end', 'File ' + str(Path(fichier).name) + '. At least one empty field (Artist, Title or both).\n') return data def traite_M3U(lst): if app.chkToutf.get() == 'oui': app.txtMessage.insert('end', 'Number of m3u files : ' + str(len(lst)) + '\n') for nb in range(len(lst)): # pour chaque fichier m3u # construit les données brutes adaptées en chemin et expurgées des #EXT... et \n donnee = prepareDonnees_m3u (lst[nb], app.txtChsource.get(), app.txtChdestin.get()) # construit le contenu fichier pour une playlist Volumio 2x sortiefichier = volumioPL (donnee) # ecrit le fichier pour volumio 2x dans le dossier "VolumioPL" ecritureFichier (sortiefichier, str(lst[nb]).split('.')[0], app.lblDoscible.get()) def prepareDonnees_xspf (fichier, chsrc="", chrpl=""): arbre = ET.parse(fichier) # charge le fichier xspf tete = arbre.getroot() # recupère le noeud root de l'arbre ns = '{http://xspf.org/ns/0/}' # name space des fichiers xspf issu de vlc datatmp = [] try: for piste in tete.find(ns + 'trackList'): # parcours la branche trackList et pour # chaque piste # récupère l'uri de la chanson uri = piste.find(ns + 'location') uri.text = uri.text.replace(chsrc, chrpl) # adapte le chemin uri.text = uri.text.strip('file:/') # et enlève le début de l'uri datatmp.append (unquote(uri.text)) # renseigne la liste tmp en convertissant # l'encodage url en unicode # récupère son titre title = piste.find(ns + 'title') datatmp.append (title.text) # renseigne la liste tmp # récupère l'artiste artist = piste.find(ns + 'creator') datatmp.append (artist.text) # renseigne la liste tmp data = [] for nb in range(0, len(datatmp), 3): # permutations et concaténation pour retrouver # le format attendu de la fonction 'volumioPL' data.append (datatmp[nb+2] + ' - ' + datatmp[nb+1]) data.append (datatmp[nb]) app.txtMessage.insert('end', 'File ' + str(Path(fichier).name) + ' contains ' + str(int(len(data)/2)) + ' songs.\n') # check si pas de "guillemet droit" car séparateur dans playlists volumio ! data = checksyntaxe_volumioPL (data) except TypeError: data = [] app.txtMessage.insert('end', 'File ' + str(Path(fichier).name) + '. At least one empty field (location, title or creator).\n') except AttributeError: data = [] app.txtMessage.insert('end', 'File ' + str(Path(fichier).name) + '. At least one absent field (location, title or creator).\n') return data def traite_XSPF(lst): if app.chkToutf.get() == 'oui': app.txtMessage.insert('end', 'Number of xspf files : ' + str(len(lst)) + '\n') for nb in range(len(lst)): # pour chaque fichier xspf # construit les données brutes expurgées et adaptées en chemin donnee = prepareDonnees_xspf (lst[nb], app.txtChsource.get(), app.txtChdestin.get()) # construit le contenu fichier pour une playlist Volumio 2x sortiefichier = volumioPL (donnee) # ecrit le fichier pour volumio 2x dans le dossier "VolumioPL" ecritureFichier (sortiefichier, str(lst[nb]).split('.')[0], app.lblDoscible.get()) def checksyntaxe_volumioPL (liste): for nb in range(len(liste)): # pour chaque ligne de la liste if '"' in liste[nb]: # si guillemet droit présent liste[nb] = liste[nb].replace("\"","\\\"") # on protege le guillemet return liste def volumioPL (lst): lstvol = [] for nb in range(0, len(lst), 2): # print(str(nb) + ' ' + lst[nb]) # enlève les espaces éventuels en début et fin de chaine, prend le debut artist = lst[nb].split(' - ')[0].strip() # enlève les espaces éventuels en début et fin de chaine, prend la fin title = lst[nb].split(' - ')[1].strip() # remplace les \ des chemins par des / pour volumio uri = lst[nb+1].replace('\\', '/') # Construit la liste des données nécessaires pour pointer une chanson sous volumio lstvol.append('{' + '"service":"mpd","uri":"'+ uri + #'"}') '","title":"' + title + #'"}') '","artist":"' + artist + '"}') # concatène les éléments de la liste dans une lo.......................ongue ligne outfic = '[' + ','.join(lstvol) + ']' return outfic def ecritureFichier (cont, nom, srep): nom = Path(nom).name # récupère le nom du fichier sans le chemin if cont == '[]': # si rien à écrire app.txtMessage.insert('end', '\tFile ' + nom + " for Volumio 2x was not created.\n") return # sort de la fonction if not Path(srep).exists(): # vérifie l'existance du dossier cible des playlists volumio app.txtMessage.insert('end', 'Folder ' + str(srep) + " doesn't exists or not accessible !\n" + '\tFile ' + nom + " for Volumio 2x was not created.\n") return # sort de la fonction ch = Path(srep) / nom # construit le chemin du fichier playlist volumio # ouvre le fichier en écriture et le ferme en fin de bloc with open(ch, 'w', encoding='utf-8') as file: file.write(cont) # écrit dans le fichier la lo.....................ongue ligne app.txtMessage.insert('end', '\tFile ' + nom + ' for Volumio 2x was created.\n') def ouvertureFichier (nom): # certains fichiers m3u ne sont pas codé unicode mais latin-1 et provoque une erreur # à l'ouverture... try: # ouvre le fichier en lecture et le ferme en fin de bloc with open(nom, 'r', encoding='utf-8') as file: # lit le fichier dans une liste, un item par ligne, chaque retour à la ligne devient \n cont = file.readlines() except UnicodeDecodeError: with open(nom, 'r', encoding='Latin-1') as file: cont = file.readlines() return cont def checkPathenLocal (nom): # vérifie la présence des chansons localement par leur chemin ext = Path(nom).suffix.lower() # recup de l'extension du fichier en minuscule if ext == '.m3u': # est-ce un .m3u data = ouvertureFichier (nom) # lit le fichier dans une liste if data[0] == '#EXTM3U\n': # est-ce un m3u étendu erChLoc = 0 for nb in range(1, len(data)): # ne prend pas l'index 0 car vaut #EXTM3U\n if not '#EXTINF:' in data[nb]: # si pas une ligne info if 'file:///' in data[nb]: # chemin absolu unix # construit le chemin local de la chanson pour unix chsong = '/' + data[nb].strip('file:///') else: # chemin abolu windows ou relatif ..\ # construit le chemin local de la chanson pour windows chsong = data[nb].strip('file://') chsong = unquote(chsong.strip()) # enleve les %20 si y a if not Path(chsong).is_file(): erChLoc += 1 cont = '\t' + str(int(len(data)/2)) + ' songs. ' + str(erChLoc) + ' unresolved paths locally.\n' return cont else: # format non reconnu return '\t Sorry, unable to resolve paths locally : Content not recognized.\n' elif ext == '.xspf': # est-ce un .xspf arbre = ET.parse(nom) # charge le fichier xspf tete = arbre.getroot() # recupère le noeud root de l'arbre ns = '{http://xspf.org/ns/0/}' # name space des fichiers xspf issu de vlc erChLoc = 0 nbsong = 0 for piste in tete.find(ns + 'trackList'): # parcours la branche trackList et pour # chaque piste # récupère l'uri de la chanson uri = piste.find(ns + 'location') if 'file:///' in uri.text: # chemin absolu unix # construit le chemin local de la chanson pour unix chsong = '/' + uri.text.strip('file:///') else: # chemin abolu windows # construit le chemin local de la chanson pour windows chsong = uri.text.strip('file://') chsong = unquote(chsong.strip()) # enleve les %20 si y a if not Path(chsong).is_file(): erChLoc += 1 nbsong += 1 cont = '\t' + str(nbsong) + ' songs. ' + str(erChLoc) + ' unresolved paths locally.\n' return cont else: # format non reconnu return '\t Sorry strange file format, unable to resolve paths locally.\n' #==========Fonctions evenementielles================================================== def dossierspardefaut(): # récupère eventuellement le dossier source des playlists à traiter et le dossier # cible des playlists volumio # PLtoVolumioPL.py -s "dossier source" -c "dossier cible" -r "chaine à remplacer" -b "chaine de remplacement" if len(argv) > 1: for i in range(1, len(argv) - 1, 2): if argv[i] == '-s' and Path(argv[i+1]).is_dir(): # dossier source des playjists par la ligne de commande app.lblFichier.set(argv[i+1]) if argv[i] == '-t' and Path(argv[i+1]).is_dir(): app.lblDoscible.set(argv[i+1]) # dossier cible des playlists volumio if argv[i] == '-r': app.txtChsource.set(argv[i+1]) # chaine à remplacer if argv[i] == '-b': app.txtChdestin.set(argv[i+1]) # chaine de remplacement # Charge l'image png dans le canvas si présente avec le srcipt # la variable __file__ contient le chemin absolu du script imgfic = Path(__file__).parent / "PLtoVolumioPL.png" if Path(imgfic).is_file(): # charge l'image si présente # cette variable imgobj doit etre globale pour garder la ref de l'image : imgobj.append(tk.PhotoImage(file=imgfic)) app.canZone.create_image(int(app.canZone['width']) /2, int(app.canZone['height']) /2, image=imgobj[0]) def selectFichier(): # s'asssure qu'un dossier et non un fichier est dans le label, pour ouvrir la boite dedans if not Path(app.lblFichier.get()).is_dir(): app.lblFichier.set(Path(app.lblFichier.get()).parent) # ouvre un dialogue de sélection de fichier # sous windows tout le réseau est accessible sans pbs # sous linux, tkinter affiche une boite basique sans aucun accès au réseau, # les dossiers montés sont alors accessibles par le chemin "/run/user/(...)/gvfs" :(( fichier=filedialog.askopenfilename( title="Select a playlist to process", initialdir=Path(app.lblFichier.get()), parent=app, filetypes=[("Playlists","*.m3u *.xspf")]) if fichier: app.lblFichier.set(Path(fichier)) # renseigne le label app.btcOpt.set('C') # repositionne à both le radiobutton app.chkToutf.set('non') # décoche le checkbox tous les fichiers for child in app.cadreBtcradio.winfo_children(): # grise les btc radios if child.winfo_class() == 'TRadiobutton': child['state'] = 'disabled' else: # coche tous les fichiers du dossier app.chkToutf.set('oui') optionFichier() app.btcUnfichier.state(['focus']) def optionFichier(): # regarde le checkbox, si tous les fichiers du dossier c'est 'oui' if app.chkToutf.get() == 'oui': # récupère le dossier du fichier selectionné if not Path(app.lblFichier.get()).is_dir(): # pas necessaire si déjà dossier app.lblFichier.set(Path(app.lblFichier.get()).parent) for child in app.cadreBtcradio.winfo_children(): # dégrise les boutons radios if child.winfo_class() == 'TRadiobutton': child['state'] = 'normal' else: app.chkToutf.set('oui') def dossierCiblePL(): # ouvre un dialogue de sélection de répertoire # sous windows tout le réseau est accessible sans pbs # sous linux, tkinter affiche une boite basique sans aucun accès au réseau, # les dossiers montés sont alors accessibles par le chemin "/run/user/(...)/gvfs" :(( dossier = filedialog.askdirectory( title="Select target folder for Volumio playlists", mustexist=True, initialdir=Path(app.lblDoscible.get()), parent=app) # un dossier a vraiment été sélectionné ? if dossier: # on renseigne la variable du label app.lblDoscible.set(Path(dossier)) def infos(): # sous linux, tkinter.messagebox affiche une boite ugly en police gras et taille énorme... # showinfo("About", """ blabla """), je préfère donc utiliser la text+scrollbar strInfo1 = """GUI to create playlists files for Volumio 2.x from extended m3u \ palylists and / or xspf.\n\n \ Only process extended .m3u (#EXTM3U in first line) without comment lines (#EXTREM:) \ and whose info lines (#EXTINF:) \ are said 'standard' (Duration,Artist - Title).\n \ Tested m3u and xspf files created by vlc.\n\n \ Audio files path in playlists should be pointed in relative or absolute, then they must be \ located in a folder single source. Adapt the ".." in 'Replace' field accordingly. \ "USB/USB-Music" in 'by' field must be adapted to path that allows Volumio to find your songs.\n\n \ Created playlists must be copied to Volumio 2x "/data/playlist" folder. So, it can be \ convenient to share this folder by granting it all rights as "/data/INTERNAL" \ and editing the "/etc/samba/smb.conf" file of Volumio 2x. \n\n \ Command line arguments :\n \ \t-s Source playlists files folder\n \ \t-t Volumio playlists folder\n \ \t-r Chain to replace in audio files paths\n \ \t-b Replacement chain\n \ Sample in Terminal (linux) : python3 PLtoVolumioPL.py -s /home/user/Music/Lists -t \ /home/user/Music/Lists/Volumio -r /home/user/Music -b USB/USB-Music\n \ Sample in Cmd (windows) : "C:\path to\PLtoVolumioPL.py" -s "C:\Path to my Music\Lists" -t \ "C:\Path to my Music\Lists\Volumio" -r "C:\Path to my Music" -b "USB/USB-Music" """ app.txtMessage.delete(0.0,'end') # vide la zone messages app.txtMessage.insert('end', strInfo1 + '\n') def voirPL(): # si un fichier est dans le label, ouvre la Playlist direct if Path(app.lblFichier.get()).is_file(): fichier = Path(app.lblFichier.get()) ext = Path(fichier).suffix.lower() else: # ouvre un dialogue de sélection de fichier # sous windows tout le réseau est accessible sans pbs # sous linux, tkinter affiche une boite basique sans aucun accès au réseau, # les dossiers montés sont alors accessibles par le chemin "/run/user/(...)/gvfs" :(( fichier=filedialog.askopenfilename( title="Select a playlist to check", initialdir=Path(app.lblFichier.get()), parent=app, filetypes=[("Playlists","*.m3u *.xspf"), ("VolumioPL","*")]) if fichier == (): # bizarement un tuple au lieu d'un string est renvoyé si cancel est # appuyé à la première ouverture de la boite de dialogue lorsque # celle ci est la permière filedialog ouverte... fichier = '' ext = '' else: ext = Path(fichier).suffix.lower() if fichier != "" and (ext == ".m3u" or ext == ".xspf" or ext == ""): # vide les listes de fichiers à traiter listM3U[:] = [] listXSPF[:] = [] app.txtMessage.delete(0.0,'end') # vide la zone messages # check le fichier et affiche son contenu app.txtMessage.insert('end', 'File content : ' + str(Path(fichier)) + '\n') app.txtMessage.insert('end', ''.join(checkPathenLocal(fichier)) + '\n') app.txtMessage.insert('end', ''.join(ouvertureFichier(fichier))) else: app.txtMessage.delete(0.0,'end') # vide la zone messages app.txtMessage.insert('end', 'Ready...\n') app.btcVoir.state(['focus']) def btcCreer (): # vide les listes de fichiers à traiter listM3U[:] = [] listXSPF[:] = [] app.txtMessage.delete(0.0,'end') # vide la zone messages # Variables du GUI # app.lblFichier.get() # par defaut Path.cwd(), dossier ou fichier à traiter # app.chkToutf.get() # 'oui' tout le dossier => radiobutton pour le type (par defaut) # 'non' un seul fichier à traiter m3u ou xspf # app.btcOpt.get() # 'A' m3u 'B' xspf et 'C' les 2 types (par defaut) # app.txtChsource.get() # par defaut '..' chaine à remplacer dans les chemins # app.txtChdestin.get() # par defaut 'USB/USB-Music' chaine de remplacement # app.lblDoscible.get() # par defaut Path.cwd() / 'VolumioPL' dossier cible # des playlists Volumio, doit exister # app.txtMessage.insert() # par defaut ('end', 'Prêt...\n') # app.canZone.create_image # charge une image dans le canvas if app.chkToutf.get() == 'oui': # si tous les fichiers du dossier rep = messagebox.askyesno(message='Processes all playlists\n in folder ?', #''Are you realy sure ?', icon='question', title='', default='no') # yes renvoie True ; no renvoi False if not rep: # si no, sort de la fonction sans créer app.txtMessage.insert('end', 'Ready...\n') return app.txtMessage.insert('end', 'Processes files in folder : ' + app.lblFichier.get() + '\n') # Construit les listes de fichiers à traiter depuis le dossier fichiers_M3u_Xspf (rep=app.lblFichier.get()) if app.btcOpt.get() == 'C': # traite m3u et xspf traite_M3U(listM3U) traite_XSPF(listXSPF) elif app.btcOpt.get() == 'A': # traite m3u traite_M3U(listM3U) else: # 'B': # traite xspf traite_XSPF(listXSPF) else: # app.chkToutf.get() == 'non': un seul fichier m3u ou xspf app.txtMessage.insert('end', 'Processes file : ' + app.lblFichier.get() + '\n') fichiers_M3u_Xspf (fic=app.lblFichier.get()) # traite le fichier choisi app.chkToutf.set('oui') optionFichier() app.txtMessage.insert('end', 'Ready...\n') #==========Main======================================================================= if __name__ == "__main__": root = tk.Tk() # pour créer l'objet fenêtre root.style = ttk.Style() # pour appliquer un style global aux widgets ttk # liste des thèmes dispo sous linux ('clam', 'alt', 'default', 'classic') root.style.theme_use("default") app = PLtoVPL_gui(root) # instanciation de la fenêtre avec ses widgets dossierspardefaut() # lit les arguments et l'image pour le canvas app.mainloop() # attend les évenements du GUI