Auteur/autrice : sdgadmin

Lenovo

L’audio ne fonctionnait à nouveau plus correctement , malgré shutdown , reboot , et plus d’une minute avec bouton power enfoncé .

sudo /usr/local/bin//speaker.sh
[sudo] Mot de passe de stefan :  
Configuring amp at address 0x38 on i2c-17
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Done configuring amp at 0x38
Configuring amp at address 0x3e on i2c-17
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Error: Write failed
Done configuring amp at 0x3e

~ 6s
❯ cat  /usr/local/bin//speaker.sh
#!/bin/bash

i2c_bus=17
amp_addresses=(0x38 0x3e)

for i2c_addr in "${amp_addresses[@]}"; do
 echo "Configuring amp at address $i2c_addr on i2c-$i2c_bus"

 i2cset -f -y $i2c_bus $i2c_addr 0x00 0x00
 i2cset -f -y $i2c_bus $i2c_addr 0x7f 0x00
 i2cset -f -y $i2c_bus $i2c_addr 0x01 0x01
 i2cset -f -y $i2c_bus $i2c_addr 0x0e 0xc4
 i2cset -f -y $i2c_bus $i2c_addr 0x0f 0x40
 i2cset -f -y $i2c_bus $i2c_addr 0x5c 0xd9
 i2cset -f -y $i2c_bus $i2c_addr 0x60 0x10
 i2cset -f -y $i2c_bus $i2c_addr 0x0a 0x2e
 i2cset -f -y $i2c_bus $i2c_addr 0x0d 0x01
 i2cset -f -y $i2c_bus $i2c_addr 0x16 0x40

 i2cset -f -y $i2c_bus $i2c_addr 0x00 0x01
 i2cset -f -y $i2c_bus $i2c_addr 0x17 0xc8

 i2cset -f -y $i2c_bus $i2c_addr 0x00 0x04
 i2cset -f -y $i2c_bus $i2c_addr 0x30 0x00
 i2cset -f -y $i2c_bus $i2c_addr 0x31 0x00
 i2cset -f -y $i2c_bus $i2c_addr 0x32 0x00
 i2cset -f -y $i2c_bus $i2c_addr 0x33 0x01

 i2cset -f -y $i2c_bus $i2c_addr 0x00 0x08
 i2cset -f -y $i2c_bus $i2c_addr 0x18 0x00
 i2cset -f -y $i2c_bus $i2c_addr 0x19 0x00
 i2cset -f -y $i2c_bus $i2c_addr 0x1a 0x00
 i2cset -f -y $i2c_bus $i2c_addr 0x1b 0x00
 i2cset -f -y $i2c_bus $i2c_addr 0x28 0x40
 i2cset -f -y $i2c_bus $i2c_addr 0x29 0x00
 i2cset -f -y $i2c_bus $i2c_addr 0x2a 0x00
 i2cset -f -y $i2c_bus $i2c_addr 0x2b 0x00

 i2cset -f -y $i2c_bus $i2c_addr 0x00 0x0a
 i2cset -f -y $i2c_bus $i2c_addr 0x48 0x00
 i2cset -f -y $i2c_bus $i2c_addr 0x49 0x00
 i2cset -f -y $i2c_bus $i2c_addr 0x4a 0x00
 i2cset -f -y $i2c_bus $i2c_addr 0x4b 0x00
 i2cset -f -y $i2c_bus $i2c_addr 0x58 0x40
 i2cset -f -y $i2c_bus $i2c_addr 0x59 0x00
 i2cset -f -y $i2c_bus $i2c_addr 0x5a 0x00
 i2cset -f -y $i2c_bus $i2c_addr 0x5b 0x00

 i2cset -f -y $i2c_bus $i2c_addr 0x00 0x00
 i2cset -f -y $i2c_bus $i2c_addr 0x02 0x00

 echo "Done configuring amp at $i2c_addr"
done
lspci -nn | grep -i audio
00:1f.3 Multimedia audio controller [0401]: Intel Corporation Meteor Lake-P HD Audio Controller [8086:7e28] (rev 20)

~
❯ lspci -nnk | grep -iA 3 0403


La déconnexion de la batterie a permis une réinitialisation profonde du contrôleur intégré (EC) et des circuits de gestion d’alimentation de l’ordinateur portable. Sur des modèles comme le Lenovo Yoga Pro 9i, les amplificateurs audio (tels que TAS2781 ou similaires, intégrés au codec ALC287) peuvent entrer dans un état « bloqué » ou de basse puissance en raison de bogues firmware, d’interactions avec le noyau, ou de problèmes de gestion d’alimentation sous Linux. Cet état empêche les écritures I2C réussies (entraînant les erreurs « Write failed » dans le script), même si les périphériques sont physiquement présents. La réinitialisation efface cet état, restaurant une communication normale sans nécessairement modifier l’énumération des périphériques dans les outils comme lspci ou i2cdetect (qui sonde les accusés de réception I2C).
Pourquoi la détection reste identique : i2cdetect repose sur les réponses des périphériques avec un accusé de réception (ACK) lors d’une sonde. Si les amplificateurs étaient dans un mode de défaillance partielle, ils pouvaient ne pas répondre, entraînant un scan vide. Cependant, après la réinitialisation, ils peuvent maintenant accepter les écritures de votre script, permettant une configuration appropriée et une sortie audio complète (incluant les basses). Ce comportement est connu dans les configurations audio Linux pour les appareils Lenovo, où la visibilité et la fonctionnalité peuvent se dissocier en raison des particularités de gestion d’alimentation.

Compilation du kernel :

Latence sur ma connexion internet

Depuis quelques jours , je constate que ma connexion est plus lente que d’habitude .

❯ ping 8.8.8.8
  ping 1.1.1.1
  ping google.com

PING 8.8.8.8 (8.8.8.8) 56(84) octets de données.
64 octets de 8.8.8.8 : icmp_seq=1 ttl=115 temps=59.2 ms
64 octets de 8.8.8.8 : icmp_seq=2 ttl=115 temps=21.1 ms
64 octets de 8.8.8.8 : icmp_seq=3 ttl=115 temps=23.1 ms
64 octets de 8.8.8.8 : icmp_seq=4 ttl=115 temps=11.8 ms
64 octets de 8.8.8.8 : icmp_seq=5 ttl=115 temps=19.3 ms
64 octets de 8.8.8.8 : icmp_seq=6 ttl=115 temps=23.8 ms
64 octets de 8.8.8.8 : icmp_seq=7 ttl=115 temps=19.9 ms
64 octets de 8.8.8.8 : icmp_seq=13 ttl=115 temps=57.0 ms
64 octets de 8.8.8.8 : icmp_seq=14 ttl=115 temps=22.6 ms
64 octets de 8.8.8.8 : icmp_seq=15 ttl=115 temps=19.1 ms
64 octets de 8.8.8.8 : icmp_seq=16 ttl=115 temps=17.6 ms
^C
--- statistiques ping 8.8.8.8 ---
16 paquets transmis, 11 reçus, 31.25% packet loss, time 15156ms
rtt min/avg/max/mdev = 11.846/26.774/59.196/15.084 ms
PING 1.1.1.1 (1.1.1.1) 56(84) octets de données.
64 octets de 1.1.1.1 : icmp_seq=1 ttl=57 temps=11.1 ms
64 octets de 1.1.1.1 : icmp_seq=2 ttl=57 temps=19.3 ms
64 octets de 1.1.1.1 : icmp_seq=3 ttl=57 temps=25.2 ms
64 octets de 1.1.1.1 : icmp_seq=4 ttl=57 temps=51.7 ms
64 octets de 1.1.1.1 : icmp_seq=5 ttl=57 temps=17.6 ms
64 octets de 1.1.1.1 : icmp_seq=6 ttl=57 temps=19.4 ms
64 octets de 1.1.1.1 : icmp_seq=10 ttl=57 temps=19.6 ms
64 octets de 1.1.1.1 : icmp_seq=11 ttl=57 temps=17.9 ms
64 octets de 1.1.1.1 : icmp_seq=12 ttl=57 temps=16.2 ms
64 octets de 1.1.1.1 : icmp_seq=13 ttl=57 temps=20.8 ms
64 octets de 1.1.1.1 : icmp_seq=14 ttl=57 temps=22.2 ms
^C
--- statistiques ping 1.1.1.1 ---
14 paquets transmis, 11 reçus, 21.4286% packet loss, time 13113ms
rtt min/avg/max/mdev = 11.110/21.897/51.664/10.001 ms
PING google.com (64.233.167.113) 56(84) octets de données.
64 octets de wl-in-f113.1e100.net (64.233.167.113) : icmp_seq=1 ttl=105 temps=13.5 ms
64 octets de wl-in-f113.1e100.net (64.233.167.113) : icmp_seq=2 ttl=105 temps=21.0 ms
64 octets de wl-in-f113.1e100.net (64.233.167.113) : icmp_seq=3 ttl=105 temps=21.6 ms
64 octets de wl-in-f113.1e100.net (64.233.167.113) : icmp_seq=4 ttl=105 temps=22.7 ms
64 octets de wl-in-f113.1e100.net (64.233.167.113) : icmp_seq=5 ttl=105 temps=17.5 ms
64 octets de wl-in-f113.1e100.net (64.233.167.113) : icmp_seq=6 ttl=105 temps=23.2 ms
64 octets de wl-in-f113.1e100.net (64.233.167.113) : icmp_seq=7 ttl=105 temps=22.4 ms
^C
--- statistiques ping google.com ---
7 paquets transmis, 7 reçus, 0% packet loss, time 6010ms
rtt min/avg/max/mdev = 13.535/20.275/23.209/3.266 ms

~ 46s

traceroute -n -q 5 -w 2 8.8.8.8
  traceroute -n -q 5 -w 2 1.1.1.1
  traceroute -n -q 5 -w 2 google.com

traceroute to 8.8.8.8 (8.8.8.8), 30 hops max, 60 byte packets
 1  192.168.0.1  8.547 ms  8.480 ms  8.467 ms  9.657 ms  9.646 ms
 2  * * * * *
 3  * * * * *
 4  * * * * *
 5  * * * * *
 6  * * * * *
 7  * * * * *
 8  * * * * *
 9  * * * * *
10  8.8.8.8  55.159 ms  55.150 ms  55.141 ms  51.037 ms  51.010 ms
traceroute to 1.1.1.1 (1.1.1.1), 30 hops max, 60 byte packets
 1  192.168.0.1  7.002 ms  6.951 ms  6.938 ms  6.927 ms  6.916 ms
 2  * * * * *
 3  * * * * *
 4  * * * * *
 5  * * * * *
 6  * * * * *
 7  * * * * *
 8  * * 1.1.1.1  55.926 ms  55.873 ms  55.861 ms
traceroute to google.com (64.233.167.101), 30 hops max, 60 byte packets
 1  192.168.0.1  0.853 ms  0.820 ms  0.809 ms  36.450 ms  36.440 ms
 2  10.133.44.1  36.434 ms  36.425 ms  36.415 ms  36.405 ms  36.396 ms
 3  * * * * *
 4  212.224.245.182  69.081 ms  68.253 ms  68.226 ms  68.215 ms  32.562 ms
 5  212.224.245.177  32.548 ms  32.537 ms  42.889 ms  42.878 ms  56.051 ms
 6  81.52.186.121  55.987 ms  23.355 ms  23.317 ms  23.306 ms  23.295 ms
 7  81.52.200.120  31.039 ms  49.780 ms  49.769 ms  39.474 ms  39.447 ms
 8  * * * * *
 9  * * * * *
10  209.85.254.116  33.501 ms * * * *
11  * * * * *
12  * * * * *
13  * * * 108.170.231.161  22.849 ms 216.239.56.107  30.567 ms
14  * * * * *
15  * * * * *
16  * * * * *
17  * * * * *
18  * * * * *
19  * * * * *
20  * * * * *
21  * * * * *
22  * * * * *
23  * * * * *
24  * * * * *
25  * * * * *
26  * * * * *
27  * * * * *
28  * * * * *
29  * * * * *
30  * * * * *

J’ai contacté mon fai , en attende de sa réponse ….

Top 10 Web Application Security Risks

There are three new categories, four categories with naming and scoping changes, and some consolidation in the Top 10 for 2021.

Mapping
  • A01:2021-Broken Access Control moves up from the fifth position; 94% of applications were tested for some form of broken access control. The 34 Common Weakness Enumerations (CWEs) mapped to Broken Access Control had more occurrences in applications than any other category.
  • A02:2021-Cryptographic Failures shifts up one position to #2, previously known as Sensitive Data Exposure, which was broad symptom rather than a root cause. The renewed focus here is on failures related to cryptography which often leads to sensitive data exposure or system compromise.
  • A03:2021-Injection slides down to the third position. 94% of the applications were tested for some form of injection, and the 33 CWEs mapped into this category have the second most occurrences in applications. Cross-site Scripting is now part of this category in this edition.
  • A04:2021-Insecure Design is a new category for 2021, with a focus on risks related to design flaws. If we genuinely want to “move left” as an industry, it calls for more use of threat modeling, secure design patterns and principles, and reference architectures.
  • A05:2021-Security Misconfiguration moves up from #6 in the previous edition; 90% of applications were tested for some form of misconfiguration. With more shifts into highly configurable software, it’s not surprising to see this category move up. The former category for XML External Entities (XXE) is now part of this category.
  • A06:2021-Vulnerable and Outdated Components was previously titled Using Components with Known Vulnerabilities and is #2 in the Top 10 community survey, but also had enough data to make the Top 10 via data analysis. This category moves up from #9 in 2017 and is a known issue that we struggle to test and assess risk. It is the only category not to have any Common Vulnerability and Exposures (CVEs) mapped to the included CWEs, so a default exploit and impact weights of 5.0 are factored into their scores.
  • A07:2021-Identification and Authentication Failures was previously Broken Authentication and is sliding down from the second position, and now includes CWEs that are more related to identification failures. This category is still an integral part of the Top 10, but the increased availability of standardized frameworks seems to be helping.
  • A08:2021-Software and Data Integrity Failures is a new category for 2021, focusing on making assumptions related to software updates, critical data, and CI/CD pipelines without verifying integrity. One of the highest weighted impacts from Common Vulnerability and Exposures/Common Vulnerability Scoring System (CVE/CVSS) data mapped to the 10 CWEs in this category. Insecure Deserialization from 2017 is now a part of this larger category.
  • A09:2021-Security Logging and Monitoring Failures was previously Insufficient Logging & Monitoring and is added from the industry survey (#3), moving up from #10 previously. This category is expanded to include more types of failures, is challenging to test for, and isn’t well represented in the CVE/CVSS data. However, failures in this category can directly impact visibility, incident alerting, and forensics.
  • A10:2021-Server-Side Request Forgery is added from the Top 10 community survey (#1). The data shows a relatively low incidence rate with above average testing coverage, along with above-average ratings for Exploit and Impact potential. This category represents the scenario where the security community members are telling us this is important, even though it’s not illustrated in the data at this time.

Save

Le projet Save Videos est une application web inspiré des plugins yt-dlp et X_Media_download pour permettre aux utilisateurs de sauvegarder, organiser et visualiser des vidéos provenant de plateformes comme YouTube et X (anciennement Twitter). Conçue avec un accent sur la simplicité d’utilisation et la sécurité, cette plateforme répond aux besoins des individus souhaitant archiver du contenu multimédia de manière fiable et privée.
L’application intègre des fonctionnalités avancées tout en respectant les principes de confidentialité et de protection des données. Ce résumé présente les fonctionnalités principales, les mesures de sécurité implémentées, ainsi que les perspectives d’évolution futures.

Fonctionnalités Actuelles

L’application offre un ensemble de fonctionnalités essentielles pour une gestion efficace des vidéos :

  • Inscription et Connexion Sécurisées : Les utilisateurs peuvent créer un compte avec un nom d’utilisateur, un email et un mot de passe haché. Une vérification par code de confirmation envoyé par email est requise pour activer le compte, garantissant l’authenticité des adresses fournies.
  • Ajout et Édition de Vidéos : Possibilité d’ajouter des vidéos via URL (YouTube ou X) ou téléversement local (formats MP4, AVI, MKV). Les catégories multiples (séparées par virgules), commentaires et miniatures sont supportés. Une option permet de copier localement les vidéos pour éviter la censure.
  • Visualisation et Gestion : Une page dédiée affiche les vidéos en mode inline (iframe pour YouTube, tag video pour locales). Les utilisateurs peuvent éditer, supprimer ou rendre publiques leurs vidéos. Un système de catégories facilite la navigation.
  • Téléchargement Sécurisé : Les vidéos locales sont servies via un script dédié qui vérifie les permissions, empêchant les accès non autorisés.
  • Support Multimédia : Intégration de miniatures automatiques pour YouTube/X, et analyse antivirus (ClamAV) pour les uploads.
  • Vidéo publique , apparaît sur la home page , vidéo privé n’apparaît que sur la page de son utilisateur
  • Importation par playlist youtube
  • Importation de toutes les vidéos publiques d’un user youtube

Ces fonctionnalités sont accessibles via une interface responsive, stylisée avec Tailwind CSS, assurant une expérience utilisateur fluide sur tous les appareils.

Mesures de Sécurité Mises en Place

La sécurité est au cœur du projet, avec des implémentations robustes pour protéger les données et prévenir les attaques courantes :

  • Protection contre les Attaques CSRF : Tous les formulaires (inscription, connexion, ajout/édition de vidéos) incluent un jeton CSRF généré dynamiquement et validé côté serveur.
  • CAPTCHA Open Source : Utilisation de Securimage pour empêcher les inscriptions et connexions automatisées par bots, sans dépendance à des services externes comme Google.
  • Vérification par Email : Les nouveaux comptes nécessitent une confirmation via un code à 6 chiffres envoyé par email, avec expiration après une heure.
  • Contrôle d’Accès aux Fichiers : Les vidéos uploadées ne sont plus accessibles directement ; un script PHP vérifie l’authentification et les permissions avant de servir le contenu.
  • Analyse Antivirus : Tous les fichiers téléversés sont scannés avec ClamAV avant stockage.
  • Hachage des Mots de Passe : Utilisation de `password_hash` pour stocker les mots de passe de manière sécurisée.
  • Validation des Entrées : Filtrage strict des URLs, emails et fichiers pour prévenir les injections et les uploads malveillants.

Ces mesures alignent l’application sur les standards OWASP, réduisant significativement les risques d’exploitation.

Fonctionnalités Futures Envisagées

Pour étendre les capacités de l’application et renforcer sa robustesse, plusieurs améliorations sont prévues :

  • Améliorations Cryptographiques : Stockage des secrets (comme les mots de passe de base de données) via variables d’environnement, et enforcement de HTTPS pour les sessions.
  • Rate Limiting : Limitation des tentatives de connexion/inscription pour contrer les attaques par force brute.
  • Gestion Avancée des Uploads : Vérification du type MIME réel et quotas de stockage par utilisateur.
  • Fonctionnalités Utilisateur : Recherche avancée des vidéos, partage sécurisé, et intégration de notifications par email pour les mises à jour.
  • Mises à Jour Automatisées : Système de monitoring des dépendances pour détecter et patcher les vulnérabilités.
  • Headers de Sécurité HTTP : Ajout de CSP, HSTS et X-Frame-Options pour prévenir les attaques comme le clickjacking.
  • Amélioration du design
  • Ajout d’autres plateformes
  • Extension pour firefox : qui permettra de capturer l’URL d’une vidéo sur YouTube ou X et de l’envoyer directement à ce serveur

Ces évolutions viseront à maintenir l’application à jour face aux menaces émergentes, tout en améliorant l’expérience utilisateur.

Conclusion

Le projet Save Videos représente une solution pratique et sécurisée pour l’archivage de vidéos, avec un focus sur la confidentialité et la facilité d’utilisation. Grâce aux fonctionnalités actuelles et aux mesures de sécurité robustes, il offre une base solide pour les utilisateurs. Les développements futurs renforceront encore sa résilience, en faisant un outil indispensable pour la gestion multimédia.

Visit Save.sdg.ynh.fr

Plugin wordpress yt-dlp

Suivant la taille de la vidéo a télécharger , cela peut prendre plus ou moins , beaucoup de temps ,
Le serveur est pour l’instant un Raspberry 4 qui n’est pas une bête de course .

Veuillez ne pas fermer la page , ce qui interromprait le téléchargement de la vidéo.

Liste non exhaustive des sites internet compatibles :

1. Plateformes Générales de Vidéos (YouTube et Concurrents)

  • YouTube (y compris YouTube Music, YouTube Shorts, et canaux privés)
  • Vimeo
  • Dailymotion
  • Vevo
  • Rutube

2. Réseaux Sociaux et Partage de Vidéos Courts

  • TikTok
  • Instagram (Reels, IGTV, Stories)
  • Facebook (vidéos, Reels, Watch)
  • Twitter/X (vidéos et GIFs)
  • Reddit (vidéos intégrées)
  • Snapchat
  • Twitch (VOD et clips)

3. Plateformes de Streaming Professionnel et TV

  • Netflix (contenu accessible sans DRM)
  • Hulu
  • Disney+
  • Amazon Prime Video
  • BBC iPlayer
  • ARTE.tv
  • Canal+ (France)
  • TF1 (France)
  • France Télévisions (Pluzz, etc.)

4. Sites Éducatifs et Documentaires

  • TED
  • Khan Academy
  • Coursera
  • edX
  • Udemy

5. Plateformes de Musique et Audio

  • SoundCloud
  • Bandcamp
  • Mixcloud
  • Spotify (podcasts et playlists limités)
  • Deezer

6. Autres Catégories (Jeux, Actualités, Adultes, etc.)

  • Twitch (jeux en direct)
  • CNN, Fox News, et autres chaînes d’actualités
  • Pornhub, XVideos, etc. (catégorie adulte)
  • NicoNico (Japon)
  • Bilibili (Chine)
  • IQIYI (Chine)

Notes Importantes :

La compatibilité peut varier en fonction des mises à jour des sites

Fichiers récemment téléchargés (disponibles temporairement)

  • Aucun fichier récent disponible.

X_dwndl.py

x_downdl.py , script python permettant de télécharger des videos poster sur t sur X.com

Extrayant l’ID du post à partir d’une URL.
Récupérant un « guest token » pour accéder à l’API sans compte.
Analysant les données du post pour identifier les images et vidéos.
Téléchargeant les fichiers en qualité optimale (images en original, vidéos avec le bitrate le plus élevé).

Blackjack

compter les cartes

  • Blackjack : le but est d’avoir une main plus proche de 21 que le croupier sans dépasser 21.
  • Chaque carte a une valeur dans le comptage :
    • Cartes basses (2 à 6) = +1
    • Cartes neutres (7 à 9) = 0
    • Cartes hautes (10, figures, as) = -1

  • Si le reste du paquet est riche en cartes hautes (10, as), le joueur a plus de chances de faire blackjack et de gagner.
  • Si le reste est riche en cartes basses, le croupier a un avantage plus important.

L’astuce : augmenter les mises quand le paquet est favorable, et miser peu quand il ne l’est pas.


import random
import logging
import tkinter as tk
from tkinter import messagebox
from tkinter import ttk
from collections import deque
import os
import sys
import argparse

# -------------------------
# Import optionnels
# -------------------------
# PIL (images de cartes)
try:
    from PIL import Image, ImageTk
    PIL_AVAILABLE = True
except Exception:
    PIL_AVAILABLE = False

# Matplotlib (graphique)
try:
    import matplotlib.pyplot as plt
    MATPLOTLIB_AVAILABLE = True
except Exception:
    MATPLOTLIB_AVAILABLE = False

# -------------------------
# Configuration du logging
# -------------------------
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
logger = logging.getLogger('BlackjackTrainer')

# Mode debug détaillé (comportement et prints supplémentaires)
DEBUG_MODE = False

# -------------------------
# Constantes et utilitaires
# -------------------------
RANKS = ['A','2','3','4','5','6','7','8','9','10','J','Q','K']
SUITS = ['S','H','D','C']

# Valeur blackjack nominale (A peut être 11 ou 1 selon l'ajustement)
VALUE_MAP = { 'A':11, '2':2, '3':3, '4':4, '5':5, '6':6, '7':7, '8':8, '9':9, '10':10, 'J':10, 'Q':10, 'K':10 }

# Hi-Lo count mapping
HI_LO_MAP = { '2':1, '3':1, '4':1, '5':1, '6':1,
              '7':0, '8':0, '9':0,
              '10':-1, 'J':-1, 'Q':-1, 'K':-1, 'A':-1 }

# Nom du dossier d'images
CARD_IMAGE_DIR = 'cards'
CARD_IMAGE_SIZE = (72, 96)  # taille d'affichage (largeur, hauteur)

# Nombre max de slots (réduit pour éviter clipping)
MAX_CARD_SLOTS = 6

# -------------------------
# Fonctions utilitaires
# -------------------------

def build_shoe(num_decks):
    """Construit une 'shoe' (paquet multiple) sous forme deque de tuples (rank, suit)."""
    shoe = []
    for _ in range(num_decks):
        for s in SUITS:
            for r in RANKS:
                shoe.append((r, s))
    random.shuffle(shoe)
    return deque(shoe)


def card_value(card):
    """Retourne la valeur du rang (A=11 par défaut, on ajustera après)."""
    r, _ = card
    return VALUE_MAP[r]


def card_str(card):
    """Représentation texte d'une carte (ex: 'A♠')."""
    r, s = card
    suit_symbol = {'S':'','H':'','D':'','C':''}[s]
    return f"{r}{suit_symbol}"


def hi_lo_value(card):
    """Valeur Hi-Lo d'une carte donnée (int)."""
    r, _ = card
    return HI_LO_MAP[r]

# -------------------------
# Conseiller basique (Basic Strategy simplifiée)
# -------------------------

def basic_strategy_advice(player_hand, dealer_upcard):
    """
    Fournit un conseil simple: 'Hit' ou 'Stand' (et 'Double' si applicable).
    C'est une version simplifiée pour l'entraînement.
    """
    total = sum(card_value(c) for c in player_hand)
    aces = sum(1 for c in player_hand if c[0] == 'A')
    soft = False
    if aces and total <= 21:
        soft = True
    dealer_rank = dealer_upcard[0]
    dealer_val = 10 if dealer_rank in ['10','J','Q','K'] else (11 if dealer_rank=='A' else int(dealer_rank))

    if soft:
        if total >= 19:
            return 'Stand'
        if total == 18:
            if dealer_val in [9,10,11]:
                return 'Hit'
            return 'Stand'
        return 'Hit'
    else:
        if total >= 17:
            return 'Stand'
        if total <= 11:
            return 'Hit'
        if 12 <= total <= 16:
            if 2 <= dealer_val <= 6:
                return 'Stand'
            else:
                return 'Hit'
    return 'Hit'

# -------------------------
# Classe principale GUI
# -------------------------
class BlackjackTrainerGUI:
    def __init__(self, master, num_decks=6, base_bet=10):
        self.master = master
        master.title('Blackjack Trainer - Hi-Lo')

        # Paramètres
        self.num_decks = num_decks
        self.base_bet = base_bet
        self.player_money = 1000
        self.min_bet = base_bet
        self.max_bet = 10000
        self.debug = DEBUG_MODE

        # Delais configurables (ms)
        self.dealer_draw_delay_ms = 700
        self.initial_dealer_start_delay_ms = 150
        self.end_round_delay_ms = 200
        self.player_end_delay_ms = 800  # pour afficher la carte du joueur avant résolution

        # Shoe et compte
        self.shoe = build_shoe(self.num_decks)
        self.running_count = 0
        self.hands_played = 0
        self.history_money = [self.player_money]
        self.history_count = [self.running_count]

        # Chargement images si possible
        self.card_images = {}
        if PIL_AVAILABLE:
            self._load_card_images()
        else:
            logger.info('Pillow non disponible: affichage texte des cartes')

        # Construire UI
        self._build_ui()

        # Forcer une taille minimale pour éviter clipping des slots
        try:
            self.master.minsize(480, 260)
        except Exception:
            pass

        # Démarrer la première main
        self.new_round()

    # -------------------------
    # Chargement images
    # -------------------------
    def _load_card_images(self):
        dir_path = CARD_IMAGE_DIR
        if not os.path.isdir(dir_path):
            logger.warning(f"Dossier d'images '{dir_path}' introuvable. Utilisation du texte.")
            return
        loaded = 0
        for r in RANKS:
            for s in SUITS:
                fname = f"{r}{s}.png"
                path = os.path.join(dir_path, fname)
                if os.path.isfile(path):
                    try:
                        im = Image.open(path).convert('RGBA')
                        im = im.resize(CARD_IMAGE_SIZE, Image.LANCZOS)
                        self.card_images[(r,s)] = ImageTk.PhotoImage(im)
                        loaded += 1
                    except Exception as e:
                        logger.warning(f"Erreur chargement image {path}: {e}")
                else:
                    if self.debug:
                        logger.debug(f"Image non trouvée: {path}")
        logger.info(f"Images chargées: {loaded} cartes")

    # -------------------------
    # UI Builder
    # -------------------------
    def _build_ui(self):
        # Top info
        top = ttk.Frame(self.master, padding=8)
        top.pack(fill='x')

        self.info_var = tk.StringVar()
        info_label = ttk.Label(top, textvariable=self.info_var, font=('Courier', 11))
        info_label.pack(side='left', padx=6)

        # Buttons
        btn_frame = ttk.Frame(self.master, padding=6)
        btn_frame.pack(fill='x')

        self.hit_btn = ttk.Button(btn_frame, text='Hit', command=self.hit)
        self.stand_btn = ttk.Button(btn_frame, text='Stand', command=self.stand)
        self.new_btn = ttk.Button(btn_frame, text='Nouvelle main', command=self.new_round)
        if MATPLOTLIB_AVAILABLE:
            self.graph_btn = ttk.Button(btn_frame, text='Graphiques', command=self.show_graph)
        else:
            # bouton désactivé si matplotlib absent
            self.graph_btn = ttk.Button(btn_frame, text='Graphiques (requires matplotlib)', state='disabled', command=self._graph_unavailable)
        self.debug_btn = ttk.Button(btn_frame, text='Toggle Debug', command=self.toggle_debug)

        for w in (self.hit_btn, self.stand_btn, self.new_btn, self.graph_btn, self.debug_btn):
            w.pack(side='left', padx=4)

        # Board frames
        board = ttk.Frame(self.master, padding=6)
        board.pack(fill='both', expand=True)

        dealer_frame = ttk.LabelFrame(board, text='Croupier')
        dealer_frame.pack(fill='x', pady=4)
        # ici on crée des slots fixes pour le croupier
        self.dealer_canvas = ttk.Frame(dealer_frame)
        self.dealer_canvas.pack(fill='x', padx=6, pady=6)
        self.dealer_slots = []
        for _ in range(MAX_CARD_SLOTS):
            lbl = tk.Label(self.dealer_canvas, text='', borderwidth=1, relief='solid',
                           width=8, height=4, padx=6, pady=6, bg='white', fg='black', anchor='center')
            lbl.pack(side='left', padx=3)
            self.dealer_slots.append(lbl)

        player_frame = ttk.LabelFrame(board, text='Joueur')
        player_frame.pack(fill='x', pady=4)
        self.player_canvas = ttk.Frame(player_frame)
        self.player_canvas.pack(fill='x', padx=6, pady=6)
        self.player_slots = []
        for _ in range(MAX_CARD_SLOTS):
            lbl = tk.Label(self.player_canvas, text='', borderwidth=1, relief='solid',
                           width=8, height=4, padx=6, pady=6, bg='white', fg='black', anchor='center')
            lbl.pack(side='left', padx=3)
            self.player_slots.append(lbl)

        # Advice
        advice_frame = ttk.Frame(self.master, padding=6)
        advice_frame.pack(fill='x')
        self.advice_var = tk.StringVar()
        advice_label = ttk.Label(advice_frame, textvariable=self.advice_var, font=('Arial', 11, 'italic'))
        advice_label.pack(side='left')

        # Bet control
        bet_frame = ttk.Frame(self.master, padding=6)
        bet_frame.pack(fill='x')
        ttk.Label(bet_frame, text='Mise:').pack(side='left')
        self.bet_var = tk.IntVar(value=self.base_bet)
        self.bet_spin = ttk.Spinbox(bet_frame, from_=self.min_bet, to=self.max_bet, textvariable=self.bet_var, width=8)
        self.bet_spin.pack(side='left', padx=6)

    # -------------------------
    # Mécanique de jeu
    # -------------------------
    def _draw_from_shoe(self):
        """Tire une carte et gère le reshuffle quand la shoe est entamée."""
        # Remélanger si il reste moins de 25% de la shoe (paramètre configurable)
        if len(self.shoe) < int(0.25 * 52 * self.num_decks):
            logger.info('Reshuffle: pas assez de cartes, reshuffle')
            self.shoe = build_shoe(self.num_decks)
            self.running_count = 0
        card = self.shoe.popleft()
        # Mettre à jour running count
        self.running_count += hi_lo_value(card)
        if self.debug:
            logger.debug(f'Draw card: {card_str(card)} | hi-lo {hi_lo_value(card)} | running_count {self.running_count}')
        self.history_count.append(self.running_count)
        return card

    def _update_info(self):
        """Met à jour la barre d'information en haut (argent, compte, true count, total joueur)."""
        player_total = self._hand_value(self.player_hand) if hasattr(self, 'player_hand') else 0
        true_count = self._true_count()
        suggested_bet = self._suggest_bet()
        info_text = (f"Argent: {self.player_money}    "
                     f"Compte (running): {self.running_count}    "
                     f"True Count: {true_count:.2f}    "
                     f"Mise suggérée: {suggested_bet}    "
                     f"Main joueur: {player_total}")
        self.info_var.set(info_text)

    def _update_advice(self):
        """Met à jour le conseil de base affiché."""
        if hasattr(self, 'player_hand') and hasattr(self, 'dealer_hand') and len(self.dealer_hand) > 0:
            advice = basic_strategy_advice(self.player_hand, self.dealer_hand[0])
            self.advice_var.set(f"Conseil (basic): {advice}")
        else:
            self.advice_var.set("")

    def new_round(self):
        """Commence une nouvelle main: distribue deux cartes au joueur et au croupier."""
        # Réactiver contrôles (garantie)
        self._enable_controls()

        if self.player_money <= 0:
            messagebox.showwarning('Fin', "Vous n'avez plus d'argent. Fin de la session.")
            return

        # Distribution
        self.player_hand = [self._draw_from_shoe(), self._draw_from_shoe()]
        self.dealer_hand = [self._draw_from_shoe(), self._draw_from_shoe()]
        # récupérer la mise **courante** choisie par l'utilisateur
        self.current_bet = int(self.bet_var.get())
        self.hands_played += 1

        if self.debug:
            logger.debug(f'New round: player {self.player_hand}, dealer {self.dealer_hand}, bet {self.current_bet}')
            logger.debug(f'Dealer hand ranks: {[c[0] for c in self.dealer_hand]}')

        # Afficher la main du croupier avec la seconde carte cachée (placeholder)
        self._render_cards(hide_dealer_second=True)
        # Forcer le rendu immédiat de l'UI
        try:
            self.master.update_idletasks()
        except Exception:
            pass

        self._update_info()
        self._update_advice()

    def _render_cards(self, hide_dealer_second=False):
        """Met à jour les slots existants au lieu de recréer des widgets."""
        # Dealer
        for i in range(MAX_CARD_SLOTS):
            lbl = self.dealer_slots[i]
            if i < len(self.dealer_hand):
                # cas de la seconde carte cachée
                if i == 1 and hide_dealer_second:
                    lbl.config(text='[??]', image='', borderwidth=1, relief='solid', bg='white', fg='black')
                    lbl.image = None
                else:
                    card = self.dealer_hand[i]
                    if card in self.card_images:
                        lbl.config(image=self.card_images[card], text='', borderwidth=0, relief='flat', bg='white')
                        lbl.image = self.card_images[card]
                    else:
                        lbl.config(text=card_str(card), image='', borderwidth=1, relief='solid', bg='white', fg='black')
                        lbl.image = None
            else:
                # slot vide
                lbl.config(text='', image='', borderwidth=0, relief='flat', bg='white')
                lbl.image = None

        # Player
        for i in range(MAX_CARD_SLOTS):
            lbl = self.player_slots[i]
            if i < len(self.player_hand):
                card = self.player_hand[i]
                if card in self.card_images:
                    lbl.config(image=self.card_images[card], text='', borderwidth=0, relief='flat', bg='white')
                    lbl.image = self.card_images[card]
                else:
                    lbl.config(text=card_str(card), image='', borderwidth=1, relief='solid', bg='white', fg='black')
                    lbl.image = None
            else:
                lbl.config(text='', image='', borderwidth=0, relief='flat', bg='white')
                lbl.image = None

        # Debug: log what est affiché dans les slots
        if self.debug:
            try:
                dealer_texts = [self.dealer_slots[i].cget('text') for i in range(min(MAX_CARD_SLOTS, max(1, len(self.dealer_hand)+1)))]
                logger.debug(f"Rendering dealer slots (texts): {dealer_texts}")
            except Exception:
                pass

        # Forcer rafraîchissement UI
        try:
            self.master.update_idletasks()
        except Exception:
            pass

    def hit(self):
        """Le joueur tire une carte."""
        if not self._controls_enabled():
            return

        self.player_hand.append(self._draw_from_shoe())
        if self.debug:
            logger.debug(f'Player hits, hand now: {[card_str(c) for c in self.player_hand]}')
        total = self._hand_value(self.player_hand)

        # Toujours afficher la main complète
        self._render_cards(hide_dealer_second=True)
        self._update_info()
        self._update_advice()

        # Si la main est finie (21 ou bust), attendre un petit délai avant de résoudre pour que l'utilisateur voie la carte
        if total >= 21:
            self._disable_controls()
            self.master.after(self.player_end_delay_ms, self.resolve_round)

    def stand(self):
        """Le joueur reste: croupier joue selon règle (stand sur soft 17)."""
        self._render_cards(hide_dealer_second=False)
        self._update_info()
        self._disable_controls()
        self.master.after(self.initial_dealer_start_delay_ms, self._dealer_play)

    def _dealer_play(self):
        if self._hand_value(self.dealer_hand) < 17:
            self.dealer_hand.append(self._draw_from_shoe())
            if self.debug:
                logger.debug(f'Dealer draws: {card_str(self.dealer_hand[-1])}')
            self._render_cards(hide_dealer_second=False)
            self._update_info()
            self.master.after(self.dealer_draw_delay_ms, self._dealer_play)
        else:
            self.master.after(self.end_round_delay_ms, self._end_dealer_and_resolve)

    def _end_dealer_and_resolve(self):
        self._enable_controls()
        self.resolve_round()

    def resolve_round(self):
        player_total = self._hand_value(self.player_hand)
        dealer_total = self._hand_value(self.dealer_hand)
        bet = self.current_bet

        player_blackjack = (len(self.player_hand) == 2 and player_total == 21)
        dealer_blackjack = (len(self.dealer_hand) == 2 and dealer_total == 21)

        if player_blackjack and not dealer_blackjack:
            payout = int(1.5 * bet)
            self.player_money += payout
            result_text = f'Blackjack! Vous gagnez {payout}.'
        elif dealer_blackjack and not player_blackjack:
            self.player_money -= bet
            result_text = 'Le croupier a Blackjack. Vous perdez.'
        else:
            if player_total > 21:
                self.player_money -= bet
                result_text = 'Bust! Vous perdez.'
            elif dealer_total > 21 or player_total > dealer_total:
                self.player_money += bet
                result_text = 'Vous gagnez.'
            elif player_total == dealer_total:
                result_text = 'Push (égalité).'
            else:
                self.player_money -= bet
                result_text = 'Vous perdez.'

        self.history_money.append(self.player_money)

        message = (f"{result_text}\nArgent restant: {self.player_money}\n"
                   f"Compte running: {self.running_count} | True Count: {self._true_count():.2f}")
        messagebox.showinfo('Résultat', message)

        self.new_round()

    # -------------------------
    # Calculs & stratégies
    # -------------------------
    def _hand_value(self, hand):
        total = sum(card_value(c) for c in hand)
        aces = sum(1 for c in hand if c[0] == 'A')
        while total > 21 and aces:
            total -= 10
            aces -= 1
        return total

    def _true_count(self):
        decks_rem = max(1.0, len(self.shoe) / 52.0)
        return self.running_count / decks_rem

    def _suggest_bet(self):
        """Retourne la mise recommandée sans modifier la Spinbox de l'utilisateur."""
        tc = self._true_count()
        if tc <= 1:
            bet = self.base_bet
        else:
            level = int(tc)
            bet = min(self.player_money, self.base_bet * level)
        return bet

    # -------------------------
    # Contrôles utilitaires
    # -------------------------
    def _disable_controls(self):
        for w in (self.hit_btn, self.stand_btn, self.new_btn, self.graph_btn, self.debug_btn):
            try:
                w.state(['disabled'])
            except Exception:
                try:
                    w.config(state='disabled')
                except Exception:
                    pass
        try:
            self.bet_spin.config(state='disabled')
        except Exception:
            pass

    def _enable_controls(self):
        for w in (self.hit_btn, self.stand_btn, self.new_btn, self.graph_btn, self.debug_btn):
            try:
                w.state(['!disabled'])
            except Exception:
                try:
                    w.config(state='normal')
                except Exception:
                    pass
        try:
            self.bet_spin.config(state='normal')
        except Exception:
            pass

    def _controls_enabled(self):
        try:
            return 'disabled' not in self.hit_btn.state()
        except Exception:
            try:
                return self.hit_btn['state'] != 'disabled'
            except Exception:
                return True

    # -------------------------
    # Graphiques & utilitaires
    # -------------------------
    def show_graph(self):
        if not MATPLOTLIB_AVAILABLE:
            messagebox.showwarning('Graphiques indisponibles', 'matplotlib n\'est pas installé dans cet environnement.')
            return
        fig, ax1 = plt.subplots(figsize=(10,5))
        ax1.plot(self.history_money, label='Argent du joueur')
        ax1.set_xlabel('Mains jouées')
        ax1.set_ylabel('Argent')
        ax1.legend(loc='upper left')

        ax2 = ax1.twinx()
        ax2.plot(self.history_count, label='Running Count', linestyle='--')
        ax2.set_ylabel('Running Count')
        ax2.legend(loc='upper right')
        plt.title('Historique: Argent et Running Count')
        plt.grid(True)
        plt.show()

    def _graph_unavailable(self):
        messagebox.showinfo('Graphiques désactivés', 'La fonctionnalité de graphiques nécessite matplotlib.\nInstallez-le via "pip install matplotlib" si vous voulez activer cette fonction.')

    def toggle_debug(self):
        self.debug = not self.debug
        logger.setLevel(logging.DEBUG if self.debug else logging.INFO)
        logger.info(f'Debug mode set to {self.debug}')

# -------------------------
# Tests unitaires (non-GUI)
# -------------------------
def run_unit_tests():
    print('Running unit tests...')
    shoe = build_shoe(2)
    assert len(shoe) == 52 * 2, 'build_shoe: taille incorrecte'
    assert card_value(('A','S')) == 11
    assert card_value(('K','H')) == 10
    assert hi_lo_value(('2','C')) == 1
    assert hi_lo_value(('A','D')) == -1
    advice = basic_strategy_advice([('Q','H'),('4','D')], ('6','S'))
    assert advice == 'Stand', f'Expected Stand, got {advice}'
    advice2 = basic_strategy_advice([('A','H'),('6','D')], ('10','S'))
    assert advice2 == 'Hit', f'Expected Hit, got {advice2}'
    advice3 = basic_strategy_advice([('10','H'),('9','D')], ('7','C'))
    assert advice3 == 'Stand', f'Expected Stand, got {advice3}'
    print('All non-GUI tests passed.')

# -------------------------
# Main
# -------------------------
if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Blackjack Trainer GUI')
    parser.add_argument('--test', action='store_true', help='Run non-GUI unit tests and exit')
    args = parser.parse_args()

    if args.test:
        run_unit_tests()
        sys.exit(0)

    try:
        root = tk.Tk()
    except Exception as e:
        print('Impossible de démarrer l\'interface graphique:', e)
        sys.exit(1)

    gui = BlackjackTrainerGUI(root, num_decks=6, base_bet=10)
    root.mainloop()

Thème : Superposition par Kaira. CopyLerft 2025