Affichage dynamique en Python sous Debian

De Wiki de Mémoire Vive
Aller à la navigation Aller à la recherche

Versions des programmes modifiées par l’intelligence artificielle Le Chat de la société Mistral.

Sous Debian 13

Les prés-requis suite à l’installation de Debian 13. Vous n’êtes pas obligé d’installer le serveur SSH, si des commandes Shell sont inaccessibles, modifiez la variable d’environnement PATH, paramétrez en IP fixe, commentez la ligne concernant le CD d’installation dans le sources.list. J'ai choisi l'interface graphique Cinnamon.

Principe de fonctionnement

Un partage réseau Samba héberge un diaporama LibreOffice Impress, quand ce dernier est modifié il est automatiquement téléchargé par un programme en Python et affiché sur la télévision qui est connectée en HDMI à un PC sous la distribution GNU Linux Debian 13. Les dernières versions de mes programmes intègrent également la possibilité de télécharger et de lire en boucle une vidéo. Les paramétrages son enregistrés dans un fichier ini « diaporamaini ». Les fichiers en Python doivent se trouver dans le même dossier.

Le partage Réseau

Le partage Samba sur le réseau à été créé. Les paramètres sont le nom du partage, l’utilisateur y ayant accès avec son mot de passe.

Adressage IP du PC sous Debian

Le PC sous Debian qui héberge les scripts en Python doit se trouver sur le même réseau IP que le partage hébergeant le diaporama.


Environnement pour la programmation et les tests

Pour travailler sur les programmes, j’utilise un PC ou une machine virtuelle sous Windows qui héberge le partage', une machine virtuelle Debian 13 sous VirtualBox permet de programmer et de tester les résultats. La machine virtuelle est sur le même réseau IP que la machine hôte grâce à un pont sur la carte réseau. J’utilise l’éditeur Sublime Text. Un dossier est accessible par les deux machines pour pouvoir récupérer les fichiers créés, l’explorateur de fichiers de Debian doit être lancé en administrateur root su avec la commande nautilus ou nemo.


Éditeur de code Sublime Text

Installation de Sublime Text sous Debian :

https://wiki.debian.org/SublimeText

Connexion à la machine sous Debian

J’utilise un petit clavier Bluetooth quand je n’ai pas la possibilité de connecter un clavier filaire.

Mini Clavier Rii i8 disponible chez Kubii


Prérequis pour l’installation sur la machine sous Debian 13

Version de Python installée

Pour savoir quelle est la version de Python installée :

python3 --version

Python 3.13.5 dans mon cas.


Adapter l’entête des fichiers Python à votre version. Par exemple #!/usr/bin/env python3.9 peut devenir #!/usr/bin/env python3.13. On peut modifier les fichiers avec L'éditeur Nano.

Sous Debian, pour éditer mon fichier, je peux également utiliser Sublime Text :

https://www.sublimetext.com/docs/linux_repositories.html

Voir l’installation pour apt (avec Debian en administrateur root su pas de commande sudo).

Configuration de Sublime Text pour Python.

Depuis le menu View ==> Syntax, sélectionnez Python.


Pour l’interface graphique avec Tkinter

Le paquet python3-tk doit être présent sinon on l’installe.

En administrateur root, su + mot de passe :

apt-get install python3-tk

Accès au partage Samba

Vous devez disposer d’un partage réseau Windows ou sous Linux (Samba) avec les login/mot de passe pour y avoir accès. Pour pouvoir accéder au partage réseau vous devez installer le paquet smbclient (client samba) sous Linux en administrateur root su :

apt-get install smbclient

En cas de problème, il peut être utile de vérifier la connexion au partage depuis le terminal :

smbclient -U utilisateur //ip_du_serveur/nom_du_partage

Pour installer les bibliothèques externes pour Python 3 et les dépendances

Installer pysmbclient (client samba) pour l’accès au partage réseau depuis Python.

Debian 13 – PySmbClient

On peut télécharger les fichiers et les copier directement dans le dossier /usr/local/lib/python3.13/dist-packages/.

Pour télécharger les fichiers :

https://pypi.org/project/PySmbClient/

Téléchargement des fichiers ==> PySmbClient-x.x.x.tar.gz

Dans mon cas j'obtient le fichier PySmbClient-0.1.5.tar.gz.

Depuis le dossier Téléchargements :

tar zxvf PySmbClient-0.1.5.tar.gz

Depuis le dossier PySmbClient-0.1.5 :

mv * /usr/local/lib/python3.13/dist-packages/

Debian 13 – unidecode

Installer la bibliothèque unidecode pour supprimer les caractères accentués des noms de fichiers vidéo :

apt-get install python3-unidecode

Pour la lecture des vidéos sous Debian mplayer doit être installé

apt-get install mplayer

La commande utilisée pour lire un fichier vidéo en boucle et en plein écran est :

mplayer Fichier_video -loop 0 -fs

Les droits sur les fichiers

Les deux programmes doivent être exécutables et je réduis les droits à l’utilisateur :

chmod 700 nom_du_fichier

Pour le fichier diaporamaini, droits en lecture et en écriture au seul utilisateur :

chmod 600 diaporamaini

Pour une installation sous Debian, j’ai également installé le paquet libreoffice-java-common :

apt-get install libreoffice-java-common

Pour exécuter un programme

./nom_du_fichier.py

Pour supprimer un fichier vidéo téléchargé précédemment :

rm nom_du_fichier

Installation de Java JRE

J’ai également installé Java JRE, attention le nom du fichier téléchargé peut être différent !

Pour télécharger Java JRE pour Linux :

https://www.java.com/fr/download/linux_manual.jsp

J’ai sélectionné Linux x64.

En administrateur root dans un terminal :

cd /usr
mkdir java
cd java

Lancez le navigateur nautilus ou nemo depuis le terminal en administrateur root su pour copier les fichier de l’archive TAR téléchargée dans /usr/java ou utilisez la ligne de commande.

Décompressez l’archive TAR (jre-8u451-linux-x64.tar.gz dans mon cas) pour installer Java depuis le dossier Téléchargements :

tar zxvf jre-8u481-linux-x64.tar.gz

Depuis le dossier jre1.8.0_481 :

mv * /usr/java/

On peut également déplacer les fichiers avec une commande mv depuis un terminal on en SSH.


conf.py

Programme permettant de paramétrer l’application et de télécharger et lire en boucle les vidéos.

Vous devez saisir les paramètres de configuration dans le fichier ini.

Pour que le programme soit exécutable :

chmod +x conf.py

Réduire les droits à l'utilisateur :

chmod 700 conf.py

Pour lancer le programme depuis le dossier où il se trouve :

./conf.py

Le programme avec les paramètres de mon environnement de tests.

Les paramètres, nom du serveur ou IP, partage réseau, utilisateur, mot de passe, nom de domaine, fichier de diaporama doivent être saisis puis enregistrés dans le fichier ini.


  • [Lancer le Diaporama] Permet de lancer le diaporama en utilisant le paramètres du fichier ini.
  • [Enregistrer le fichier ini] Permet d’enregistrer les paramètres saisis dans les champs dans le fichier .ini « diaporamaini ».
  • [Lister les fichiers du partage depuis ini] Permet de lister les fichiers contenus dans le partage réseau à partir des paramètres du fichier ini « diaporamaini ».
  • [Transférer et lancer la vidéo] Permet de télécharger depuis le partage et de lire en boucle un fichier vidéo sélectionné dans la liste. Le nom du fichier est modifié automatiquement si nécessaire, les espaces remplacés par des _ et les accents supprimés.

Pour arrêter l’exécution du programme en Python depuis le terminal [Ctrl] + [C].

Depuis le menu Fichier ==> Quitter

conf.py code source

#!/usr/bin/env python3.13
# -- coding: utf-8 --

import os
import pickle
import subprocess
import unidecode
from tkinter import *
from tkinter import messagebox
import smbclient

# Variables globales
serveursmb = "vide"
partagesmb = "vide"
utilisateur = "vide"
mot_de_passe = "vide"
domaine = "vide"
fichier_diaporama = "vide"

# Chemins relatifs
SCRIPT_DIR = os.path.dirname(__file__)
INI_FILE = os.path.join(SCRIPT_DIR, "diaporamaini")

# Extensions vidéo autorisées
EXTENSIONS_VIDEO = {
    '.mp4', '.avi', '.mkv', '.ogm', '.wmv', '.mov',
    '.flv', '.webm', '.mpeg', '.mpg', '.vob'
}

# --- Fonctions utilitaires ---
def nom_de_fichier(nom: str) -> str:
    """Nettoie le nom d'un fichier en supprimant les espaces et les accents."""
    nom = nom.replace(" ", "_")
    return unidecode.unidecode(nom)

# --- Gestion du fichier INI ---
def lire_ini() -> None:
    """Lit le fichier de configuration et initialise les variables globales."""
    global serveursmb, partagesmb, utilisateur, mot_de_passe, domaine, fichier_diaporama

    if os.path.isfile(INI_FILE):
        try:
            with open(INI_FILE, "rb") as fichier:
                variables = pickle.load(fichier)
                serveursmb, partagesmb, utilisateur, mot_de_passe, domaine, fichier_diaporama = variables
                print("Variables initialisées depuis le fichier INI.")
        except Exception as e:
            print(f"Erreur lors de la lecture du fichier INI : {e}")
            serveursmb = partagesmb = utilisateur = mot_de_passe = domaine = fichier_diaporama = "vide"
    else:
        print(f"Fichier {INI_FILE} non trouvé.")
        serveursmb = partagesmb = utilisateur = mot_de_passe = domaine = fichier_diaporama = "vide"

def enregistrer() -> None:
    """Enregistre les paramètres dans le fichier INI."""
    global serveursmb, partagesmb, utilisateur, mot_de_passe, domaine, fichier_diaporama

    serveursmb = Serveur_Entry.get()
    partagesmb = Partage_Entry.get()
    utilisateur = Utilisateur_Entry.get()
    mot_de_passe = Mot_de_passe_Entry.get()
    domaine = Domaine_Entry.get()
    fichier_diaporama = Fichier_du_diaporama_Entry.get()

    if all(var != "vide" for var in [serveursmb, partagesmb, utilisateur, mot_de_passe, domaine, fichier_diaporama]):
        variables = [serveursmb, partagesmb, utilisateur, mot_de_passe, domaine, fichier_diaporama]
        try:
            with open(INI_FILE, "wb") as fichier:
                pickle.dump(variables, fichier)
            Mon_bouton_Lister_Fichiers['state'] = NORMAL
            Mon_bouton_Lancer_Diaporama['state'] = NORMAL
            menufichier.entryconfigure(0, state=NORMAL)
            menudiaporama.entryconfigure(0, state=NORMAL)
        except Exception as e:
            messagebox.showerror("Erreur", f"Impossible d'enregistrer le fichier INI : {e}")
    else:
        messagebox.showwarning("Attention", "Tous les champs doivent être remplis.")

# --- Gestion du diaporama ---
def lancer_diaporama() -> None:
    """Lance le script du diaporama."""
    try:
        subprocess.Popen(["/usr/bin/env", "python3.13", os.path.join(SCRIPT_DIR, "ia.py")])
    except Exception as e:
        messagebox.showerror("Erreur", f"Impossible de lancer le diaporama : {e}")

# --- Gestion des fichiers Samba ---
def lister_fichiers() -> None:
    """Liste les fichiers du partage Samba."""
    global smb

    lire_ini()
    Liste_Fichiers.delete(0, END)

    try:
        smb = smbclient.SambaClient(
            server=serveursmb,
            share=partagesmb,
            username=utilisateur,
            password=mot_de_passe,
            domain=domaine
        )
        dirs = smb.listdir("/")
        for i, fichier in enumerate(dirs, start=1):
            Liste_Fichiers.insert(i, fichier)
        Mon_bouton_Video['state'] = NORMAL
        menuvideo.entryconfigure(0, state=NORMAL)
    except Exception as e:
        messagebox.showerror("Erreur", f"Impossible de lister les fichiers : {e}")

# --- Gestion des vidéos ---
def transferer_et_lancer_video() -> None:
    """Télécharge et lance une vidéo depuis le partage Samba."""
    try:
        selection = Liste_Fichiers.curselection()
        if not selection:
            raise ValueError("Aucun fichier sélectionné.")

        fichier_video = Liste_Fichiers.get(selection[0])
        _, ext = os.path.splitext(fichier_video)
        ext = ext.lower()

        if ext not in EXTENSIONS_VIDEO:
            raise ValueError("Le fichier sélectionné n'est pas une vidéo valide.")

        local_path = os.path.join(SCRIPT_DIR, fichier_video)
        smb.download(fichier_video, local_path)

        # Nettoyage du nom de fichier
        nouveau_nom = nom_de_fichier(fichier_video)
        if nouveau_nom != fichier_video:
            os.rename(local_path, os.path.join(SCRIPT_DIR, nouveau_nom))
            local_path = os.path.join(SCRIPT_DIR, nouveau_nom)

        # Lancement de la vidéo
        subprocess.Popen(['mplayer', local_path, '-loop', '0', '-fs'])
    except Exception as e:
        messagebox.showerror("Erreur", f"Impossible de transférer ou lancer la vidéo : {e}")

# --- Interface graphique ---
def creer_interface() -> Tk:
    """Crée et configure l'interface graphique."""
    global Serveur_Entry, Partage_Entry, Utilisateur_Entry, Mot_de_passe_Entry, Domaine_Entry
    global Fichier_du_diaporama_Entry, Liste_Fichiers, Mon_bouton_Lancer_Diaporama
    global Mon_bouton_Enregistrer, Mon_bouton_Video, Mon_bouton_Lister_Fichiers
    global menufichier, menudiaporama, menuvideo

    Fenetre = Tk()
    Fenetre.title('Paramètres Diaporama et Vidéo - Programme : D Renaudeau')
    Fenetre.geometry("500x425")
    Fenetre.resizable(width=False, height=False)

    # Menus
    menubar = Menu(Fenetre)
    menufichier = Menu(menubar, tearoff=0)
    menufichier.add_command(label="Lister", state=DISABLED, command=lister_fichiers)
    menufichier.add_command(label="Enregistrer", command=enregistrer)
    menufichier.add_separator()
    menufichier.add_command(label="Quitter", command=Fenetre.destroy)
    menubar.add_cascade(label="Fichier", menu=menufichier)

    menudiaporama = Menu(menubar, tearoff=0)
    menudiaporama.add_command(label="Lancer", state=DISABLED, command=lancer_diaporama)
    menubar.add_cascade(label="Diaporama", menu=menudiaporama)

    menuvideo = Menu(menubar, tearoff=0)
    menuvideo.add_command(label="Transférer et lancer la vidéo", state=DISABLED, command=transferer_et_lancer_video)
    menubar.add_cascade(label="Vidéo", menu=menuvideo)

    menuaide = Menu(menubar, tearoff=0)
    menuaide.add_command(label="À propos", command=lambda: messagebox.showinfo("À propos", "Programme Dominique Renaudeau - Collège Celles et Melle 2026."))
    menubar.add_cascade(label="Aide", menu=menuaide)

    Fenetre.config(menu=menubar)

    # Champs de saisie
    labels = [
        ("Votre serveur - Nom ou IP :", 0),
        ("Votre partage réseau :", 1),
        ("Votre utilisateur :", 2),
        ("Votre mot de passe :", 3),
        ("Votre nom de domaine :", 4),
        ("Votre fichier de diaporama :", 5)
    ]

    entries = []
    for label_text, row in labels:
        Label(Fenetre, text=label_text).grid(row=row, column=0)
        entry = Entry(Fenetre, show="*" if "mot de passe" in label_text.lower() else None)
        entry.grid(row=row, column=1, ipadx=30)
        entries.append(entry)

    Serveur_Entry, Partage_Entry, Utilisateur_Entry, Mot_de_passe_Entry, Domaine_Entry, Fichier_du_diaporama_Entry = entries

    # Boutons
    Mon_bouton_Lancer_Diaporama = Button(Fenetre, text='Lancer le Diaporama', state=DISABLED, command=lancer_diaporama)
    Mon_bouton_Lancer_Diaporama.grid(row=6, column=0, ipadx=0)

    Mon_bouton_Enregistrer = Button(Fenetre, text='Enregistrer le fichier ini', command=enregistrer)
    Mon_bouton_Enregistrer.grid(row=6, column=1, ipadx=0)

    Liste_Fichiers = Listbox(Fenetre)
    Liste_Fichiers.grid(row=7, column=1, ipadx=30)

    Mon_bouton_Lister_Fichiers = Button(Fenetre, text='Lister les fichiers du partage depuis ini', state=DISABLED, command=lister_fichiers)
    Mon_bouton_Lister_Fichiers.grid(row=8, column=1, ipadx=0)

    Mon_bouton_Video = Button(Fenetre, text='Transférer et lancer la vidéo', state=DISABLED, command=transferer_et_lancer_video)
    Mon_bouton_Video.grid(row=8, column=0, ipadx=0)

    # Initialisation des champs
    Serveur_Entry.insert(0, serveursmb)
    Partage_Entry.insert(0, partagesmb)
    Utilisateur_Entry.insert(0, utilisateur)
    Mot_de_passe_Entry.insert(0, mot_de_passe)
    Domaine_Entry.insert(0, domaine)
    Fichier_du_diaporama_Entry.insert(0, fichier_diaporama)

    # Activation des boutons si les paramètres sont déjà définis
    if serveursmb != "vide":
        Mon_bouton_Lister_Fichiers['state'] = NORMAL
        Mon_bouton_Lancer_Diaporama['state'] = NORMAL
        menufichier.entryconfigure(0, state=NORMAL)
        menudiaporama.entryconfigure(0, state=NORMAL)

    return Fenetre

# --- Point d'entrée ---
if __name__ == "__main__":
    os.chdir(SCRIPT_DIR)
    lire_ini()
    Fenetre = creer_interface()
    Fenetre.mainloop()

Vous pouvez créer le fichier avec nano, votre utilisateur doit être le propriétaire pas root.


ia.py

Programme qui gère le diaporama

Le programme qui gère le diaporama n’a pas d’interface graphique, il lit les données du fichier ini puis télécharge le diaporama et lance LibreOffice Impress pour l’afficher. Quand le diaporama est modifié dans le partage du réseau LibreOffice est arrêté, le nouveau fichier est téléchargé automatiquement, Impress est ensuite relancé avec le fichier mis à jour.

Pour que le programme soit exécutable :

chmod +x ia.py

Réduire les droits à l'utilisateur :

chmod 700 ia.py

Le mieux est de lancer le programme en lignes de commande depuis un terminal avec votre utilisateur.

Pour lancer le programme depuis le dossier où il se trouve :

./ia.py

Pour arrêter l’affichage du diaporama [Alt] + [F4].

Pour arrêter l’exécution du programme en Python depuis le terminal [Ctrl] + [C].

ia.py code source

#!/usr/bin/env python3.13
# -- coding: utf-8 --

import pickle
import os
import smbclient
import subprocess
import time
from typing import Optional, Tuple

# Constantes
INI_FILE = "diaporamaini"
LOCAL_DIAPORAMA_PATH = "./"

def charger_variables_ini(nom_fichier: str) -> Optional[Tuple[str, str, str, str, str, str]]:
    """Charge les variables depuis le fichier ini."""
    if not os.path.isfile(nom_fichier):
        print(f"Fichier {nom_fichier} non trouvé.")
        return None

    try:
        with open(nom_fichier, "rb") as fichier:
            variables = pickle.load(fichier)
        return variables
    except Exception as e:
        print(f"Erreur lors de la lecture du fichier {nom_fichier}: {e}")
        return None

def arreter_libreoffice() -> None:
    """Arrête les processus LibreOffice en cours."""
    try:
        pid = subprocess.run(["pidof", "soffice.bin"], capture_output=True, text=True).stdout.strip()
        if pid:
            print("Arrêt de l'application LibreOffice...")
            subprocess.run(["pkill", "office"], check=True)
            time.sleep(2)
    except subprocess.CalledProcessError:
        print("LibreOffice n'est pas en cours d'exécution.")

def telecharger_fichier_smb(
    smb_client: smbclient.SambaClient,
    fichier_source: str,
    chemin_destination: str
) -> bool:
    """Télécharge un fichier depuis un partage SMB."""
    try:
        if os.path.isfile(chemin_destination):
            os.remove(chemin_destination)
        smb_client.download(fichier_source, chemin_destination)
        return True
    except Exception as e:
        print(f"Erreur lors du téléchargement du fichier {fichier_source}: {e}")
        return False

def lancer_diaporama(fichier_diaporama: str) -> subprocess.Popen:
    """Lance le diaporama avec LibreOffice."""
    try:
        return subprocess.Popen(
            ["libreoffice", "--norestore", "--show", fichier_diaporama],
            shell=False
        )
    except Exception as e:
        print(f"Erreur lors du lancement de LibreOffice: {e}")
        raise

def main() -> None:
    # Chargement des variables
    variables = charger_variables_ini(INI_FILE)
    if not variables:
        exit(1)

    serveursmb, partagesmb, utilisateur, mot_de_passe, domaine, fichier_diaporama = variables
    local_path = os.path.join(LOCAL_DIAPORAMA_PATH, fichier_diaporama)

    # Connexion au partage SMB
    try:
        smb = smbclient.SambaClient(
            server=serveursmb,
            share=partagesmb,
            username=utilisateur,
            password=mot_de_passe,
            domain=domaine
        )
    except Exception as e:
        print(f"Erreur de connexion au partage SMB: {e}")
        exit(1)

    version_old = None
    while True:
        try:
            version_new = smb.info(fichier_diaporama)
        except Exception:
            version_new = version_old

        if version_new != version_old:
            print("Nouvelle version détectée, mise à jour en cours...")
            arreter_libreoffice()
            if telecharger_fichier_smb(smb, fichier_diaporama, local_path):
                try:
                    processus = lancer_diaporama(local_path)
                    version_old = version_new
                    time.sleep(60)
                except:
                    print("Erreur lors de l'affichage du diaporama.")
            else:
                print("Échec du téléchargement, réessayez plus tard.")
        else:
            time.sleep(20)  # Pas de mise à jour, on attend 20 secondes

        # Vérification de l'état de LibreOffice
        pid = subprocess.run(["pidof", "soffice.bin"], capture_output=True, text=True).stdout.strip()
        if not pid:
            print("LibreOffice n'est pas lancé. Vérifiez les paramètres et l'accès au partage réseau.")
            exit(1)

if __name__ == "__main__":
    print("***** Programme de Dominique Renaudeau en langage Python *****")
    print("Initialisation des variables...")
    main()

Vous pouvez créer le fichier avec nano, votre utilisateur doit être le propriétaire pas root.


Si cela ne fonctionne pas sous Debian

Avec Samba sous Debian 13

Pour tester la connexion au partage Samba depuis le terminal sous Linux :

smbclient -U utilisateur //ip_du_serveur/nom_du_partage

Si vous avez le message suivant :

protocol negotiation failed: NT_STATUS_CONNECTION_DISCONNECTED

Solution

Depuis la version 4.11 Samba ne prend plus en charge le vieux protocole SMBv1, seuls SMB2 et SMB3 sont gérés. Certains partages ne sont plus accessibles. Pour voir la version du client Samba :

smbclient --version

Dans le fichier /etc/samba/smb.conf sous la section [global] j’ai ajouté :

client min protocol = NT1


Pour désactiver la boîte de dialogue « Astuce du jour » dans LibreOffice

Outils > Options > LibreOffice > Général > décocher Afficher la boîte de dialogue « Astuces du jour » au démarrage.

Accéder à un partage Samba depuis Linux

Accéder à un partage Samba depuis Linux Mint